🐞 Add antivirus inside Twake Drive (#725)

🐞 Add antivirus inside Twake Drive (#725)
This commit is contained in:
Montassar Ghanmy
2024-11-19 17:39:18 +01:00
committed by GitHub
parent cc53cd9a94
commit 00f819fb6d
49 changed files with 1341 additions and 257 deletions
@@ -135,6 +135,20 @@ export class DriveApiClient {
);
}
static async checkMalware(companyId: string, id: string) {
return await Api.post<any, DriveItem>(
`/internal/services/documents/v1/companies/${companyId}/item/${id}/check_malware${appendTdriveToken()}`,
{},
);
}
static async reScan(companyId: string, id: string) {
return await Api.post<any, DriveItem>(
`/internal/services/documents/v1/companies/${companyId}/item/${id}/rescan${appendTdriveToken()}`,
{},
);
}
static getDownloadUrl(companyId: string, id: string, versionId?: string) {
if (versionId)
return Api.route(`/internal/services/documents/v1/companies/${companyId}/item/${id}/download?version_id=${versionId}`);
@@ -3,12 +3,20 @@ import useRouterCompany from '@features/router/hooks/use-router-company';
import { useCallback } from 'react';
import { useRecoilValue, useRecoilCallback, useRecoilState } from 'recoil';
import { DriveApiClient } from '../api-client/api-client';
import { DriveItemAtom, DriveItemChildrenAtom, DriveItemPagination, DriveItemSort } from '../state/store';
import {
DriveItemAtom,
DriveItemChildrenAtom,
DriveItemPagination,
DriveItemSort,
} from '../state/store';
import { BrowseFilter, DriveItem, DriveItemVersion } from '../types';
import { SharedWithMeFilterState } from '../state/shared-with-me-filter';
import Languages from 'features/global/services/languages-service';
import { useUserQuota } from 'features/users/hooks/use-user-quota';
import AlertManager from 'app/features/global/services/alert-manager-service';
import FeatureTogglesService, {
FeatureNames,
} from '@features/global/services/feature-toggles-service';
/**
* Returns the children of a drive item
* @returns
@@ -19,6 +27,7 @@ export const useDriveActions = (inPublicSharing?: boolean) => {
const sortItem = useRecoilValue(DriveItemSort);
const [ paginateItem ] = useRecoilState(DriveItemPagination);
const { getQuota } = useUserQuota();
const AVEnabled = FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_AV_ENABLED);
const refresh = useRecoilCallback(
({ set, snapshot }) =>
@@ -35,7 +44,13 @@ export const useDriveActions = (inPublicSharing?: boolean) => {
set(DriveItemPagination, pagination);
}
try {
const details = await DriveApiClient.browse(companyId, parentId, filter, sortItem, pagination);
const details = await DriveApiClient.browse(
companyId,
parentId,
filter,
sortItem,
pagination,
);
set(DriveItemChildrenAtom(parentId), details.children);
set(DriveItemAtom(parentId), details);
for (const child of details.children) {
@@ -87,10 +102,31 @@ export const useDriveActions = (inPublicSharing?: boolean) => {
);
const download = useCallback(
async (id: string, versionId?: string) => {
async (id: string, isMalicious = false, versionId?: string) => {
try {
const url = DriveApiClient.getDownloadUrl(companyId, id, versionId);
(window as any).open(url, '_blank').focus();
// if AV is enabled
if (AVEnabled) {
// if the file is malicious
if (isMalicious) {
// toggle confirm for user
AlertManager.confirm(
() => {
(window as any).open(url, '_blank').focus();
},
() => {
return;
},
{
text: Languages.t('hooks.use-drive-actions.av_confirm_file_download'),
},
);
} else {
(window as any).open(url, '_blank').focus();
}
} else {
(window as any).open(url, '_blank').focus();
}
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file'));
}
@@ -99,10 +135,34 @@ export const useDriveActions = (inPublicSharing?: boolean) => {
);
const downloadZip = useCallback(
async (ids: string[], isDirectory = false) => {
async (ids: string[], isDirectory = false, containsMalicious = false) => {
try {
const url = await DriveApiClient.getDownloadZipUrl(companyId, ids, isDirectory);
(window as any).open(url, '_blank').focus();
const triggerDownload = async () => {
const url = await DriveApiClient.getDownloadZipUrl(companyId, ids, isDirectory);
(window as any).open(url, '_blank').focus();
};
if (AVEnabled) {
const containsMaliciousFiles =
containsMalicious ||
(ids.length === 1 && (await DriveApiClient.checkMalware(companyId, ids[0])));
if (containsMaliciousFiles) {
AlertManager.confirm(
async () => {
await triggerDownload();
},
() => {
return;
},
{
text: Languages.t('hooks.use-drive-actions.av_confirm_folder_download'),
},
);
} else {
await triggerDownload();
}
} else {
await triggerDownload();
}
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file'));
}
@@ -142,7 +202,12 @@ export const useDriveActions = (inPublicSharing?: boolean) => {
try {
const newItem = await DriveApiClient.update(companyId, id, update);
if (previousName && previousName !== newItem.name && !update.name)
ToasterService.warn(Languages.t('hooks.use-drive-actions.update_caused_a_rename', [previousName, newItem.name]));
ToasterService.warn(
Languages.t('hooks.use-drive-actions.update_caused_a_rename', [
previousName,
newItem.name,
]),
);
await refresh(id || '', true);
if (!inPublicSharing) await refresh(parentId || '', true);
if (update?.parent_id !== parentId) await refresh(update?.parent_id || '', true);
@@ -183,12 +248,47 @@ export const useDriveActions = (inPublicSharing?: boolean) => {
parentId,
filter,
sortItem,
pagination
pagination,
);
return details;
},
[paginateItem, refresh],
);
const checkMalware = useCallback(
async (item: Partial<DriveItem>) => {
try {
await DriveApiClient.checkMalware(companyId, item.id || '');
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_rescan_file'));
}
},
[refresh],
);
return { create, refresh, download, downloadZip, remove, restore, update, updateLevel, nextPage };
const reScan = useCallback(
async (item: Partial<DriveItem>) => {
try {
await DriveApiClient.reScan(companyId, item.id || '');
await refresh(item.parent_id || '', true);
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_rescan_file'));
}
},
[refresh],
);
return {
create,
refresh,
download,
downloadZip,
remove,
restore,
update,
updateLevel,
reScan,
checkMalware,
nextPage,
};
};
@@ -56,6 +56,7 @@ export type DriveItem = {
size: number;
scope: string;
av_status: string;
};
export type DriveFileAccessLevelForInherited = 'none' | 'manage';
@@ -12,6 +12,7 @@ export enum FeatureNames {
COMPANY_DISPLAY_EMAIL = 'company:display_email',
COMPANY_USER_QUOTA = 'company:user_quota',
COMPANY_MANAGE_ACCESS = 'company:managed_access',
COMPANY_AV_ENABLED = 'company:av_enabled',
}
export type FeatureValueType = boolean | number;
@@ -31,6 +32,7 @@ availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SHARED_DRIVE, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_DISPLAY_EMAIL, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_USER_QUOTA, false);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_MANAGE_ACCESS, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_AV_ENABLED, false);
/**
* ChannelServiceImpl that allow you to manage feature flipping in Tdrive using react feature toggles
@@ -44,7 +44,7 @@ export const useOnBuildContextMenu = (
DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }),
);
const { download, downloadZip, update, restore } = useDriveActions();
const { download, downloadZip, update, restore, reScan } = useDriveActions();
const setCreationModalState = useSetRecoilState(CreateModalAtom);
const setUploadModalState = useSetRecoilState(UploadModelAtom);
const setSelectorModalState = useSetRecoilState(SelectorModalAtom);
@@ -66,6 +66,8 @@ export const useOnBuildContextMenu = (
const inTrash = parent.path?.[0]?.id.includes('trash') || viewId?.includes('trash');
const isPersonal = item?.scope === 'personal';
const selectedCount = checked.length;
const notSafe =
!item?.is_directory && !['uploaded', 'safe'].includes(item?.av_status || '');
let menu: any[] = [];
@@ -75,26 +77,41 @@ export const useOnBuildContextMenu = (
const access = upToDateItem.access || 'none';
const hideShareItem = access === 'read' || getPublicLinkToken() || inTrash;
const hideManageAccessItem =
access === 'read'
|| getPublicLinkToken()
|| inTrash
|| !FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS);
access === 'read' ||
getPublicLinkToken() ||
inTrash ||
!FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS);
const newMenuActions = [
{
type: 'menu',
icon: 'share-alt',
text: Languages.t('components.item_context_menu.share'),
hide: hideShareItem,
hide: hideShareItem || notSafe,
onClick: () => setPublicLinkModalState({ open: true, id: item.id }),
},
{
type: 'menu',
icon: 'users-alt',
text: Languages.t('components.item_context_menu.manage_access'),
hide: hideManageAccessItem,
hide: hideManageAccessItem || notSafe,
onClick: () => setAccessModalState({ open: true, id: item.id }),
},
{ type: 'separator', hide: inTrash || (hideShareItem && hideManageAccessItem) },
{
type: 'menu',
icon: 'shield-check',
text: Languages.t('components.item_context_menu.rescan_document'),
hide: !(item.av_status === 'scan_failed'),
onClick: () => {
reScan(item);
},
},
{
type: 'separator',
hide:
inTrash ||
(hideShareItem && hideManageAccessItem) ||
(notSafe && !(item.av_status === 'scan_failed')),
},
{
type: 'menu',
icon: 'download-alt',
@@ -104,7 +121,7 @@ export const useOnBuildContextMenu = (
downloadZip([item!.id]);
console.log(item!.id);
} else {
download(item.id);
download(item.id, notSafe);
}
},
},
@@ -120,12 +137,12 @@ export const useOnBuildContextMenu = (
window.open(route, '_blank');
}
}, // */
{ type: 'separator' },
{ type: 'separator', hide: notSafe },
{
type: 'menu',
icon: 'folder-question',
text: Languages.t('components.item_context_menu.move'),
hide: access === 'read' || inTrash || inPublicSharing,
hide: access === 'read' || inTrash || inPublicSharing || notSafe,
onClick: () =>
setSelectorModalState({
open: true,
@@ -150,7 +167,7 @@ export const useOnBuildContextMenu = (
type: 'menu',
icon: 'file-edit-alt',
text: Languages.t('components.item_context_menu.rename'),
hide: access === 'read' || inTrash,
hide: access === 'read' || inTrash || notSafe,
onClick: () => setPropertiesModalState({ open: true, id: item.id, inPublicSharing }),
},
{
@@ -160,7 +177,8 @@ export const useOnBuildContextMenu = (
hide:
!item.access_info.public?.level ||
item.access_info.public?.level === 'none' ||
inTrash,
inTrash ||
notSafe,
onClick: () => {
copyToClipboard(getPublicLink(item || parent?.item));
ToasterService.success(
@@ -172,10 +190,10 @@ export const useOnBuildContextMenu = (
type: 'menu',
icon: 'history',
text: Languages.t('components.item_context_menu.versions'),
hide: item.is_directory || inTrash,
hide: item.is_directory || inTrash || notSafe,
onClick: () => setVersionModal({ open: true, id: item.id }),
},
{ type: 'separator', hide: access !== 'manage' || inTrash },
{ type: 'separator', hide: access !== 'manage' || inTrash || notSafe },
{
type: 'menu',
icon: 'trash',
@@ -238,15 +256,25 @@ export const useOnBuildContextMenu = (
type: 'menu',
text: Languages.t('components.item_context_menu.download_multiple'),
hide: inTrash,
onClick: () =>
selectedCount === 1 ? download(checked[0].id) : downloadZip(checked.map(c => c.id)),
onClick: () => {
const containsMalicious = checked.some(c => c.av_status === 'malicious');
if (selectedCount === 1) {
download(checked[0].id);
} else {
downloadZip(
checked.map(c => c.id),
false,
containsMalicious,
);
}
},
},
{
type: 'menu',
text: Languages.t('components.item_context_menu.clear_selection'),
onClick: () => setChecked({}),
},
{ type: 'separator', hide: parent.access === 'read' },
{ type: 'separator', hide: parent.access === 'read' || notSafe },
{
type: 'menu',
text: Languages.t('components.item_context_menu.delete_multiple'),
@@ -517,7 +545,9 @@ export const useOnBuildFileContextMenu = () => {
{
type: 'menu',
text: Languages.t('components.item_context_menu.download'),
onClick: () => download(item.id),
onClick: () => {
download(item.id);
},
},
];
return menuItems;
@@ -1,19 +1,29 @@
import { DotsHorizontalIcon } from '@heroicons/react/outline';
import {
DotsHorizontalIcon,
ShieldCheckIcon,
ShieldExclamationIcon,
BanIcon,
} from '@heroicons/react/outline';
import { Button } from '@atoms/button/button';
import { Base, BaseSmall } from '@atoms/text';
import Menu from '@components/menus/menu';
import useRouterCompany from '@features/router/hooks/use-router-company';
import { useDrivePreview } from '@features/drive/hooks/use-drive-preview';
import { formatBytes } from '@features/drive/utils';
import Languages from '@features/global/services/languages-service';
import { useState } from 'react';
import { PublicIcon } from '../components/public-icon';
import { CheckableIcon, DriveItemOverlayProps, DriveItemProps } from './common';
import './style.scss';
import { useHistory } from 'react-router-dom';
import RouterServices from '@features/router/services/router-service';
import { DocumentIcon } from './document-icon';
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';
import { formatDateShort } from 'app/features/global/utils/Numbers';
import FeatureTogglesService, {
FeatureNames,
} from '@features/global/services/feature-toggles-service';
export const DocumentRow = ({
item,
@@ -27,6 +37,7 @@ export const DocumentRow = ({
const [hover, setHover] = useState(false);
const {open} = useDrivePreview();
const company = useRouterCompany();
const notSafe = ['malicious', 'skipped', 'scan_failed'].includes(item.av_status);
const preview = () => {
open(item);
@@ -39,7 +50,7 @@ export const DocumentRow = ({
className={
'flex flex-row items-center border border-zinc-200 dark:border-zinc-800 px-4 py-3 cursor-pointer ' +
(checked
? 'bg-blue-500 bg-opacity-10 hover:bg-opacity-25 '
? (notSafe ? 'bg-rose-500' : 'bg-blue-500') + ' bg-opacity-10 hover:bg-opacity-25'
: 'hover:bg-zinc-500 hover:bg-opacity-10 ') +
(className || '')
}
@@ -48,7 +59,9 @@ export const DocumentRow = ({
onClick={e => {
if (e.shiftKey || e.ctrlKey) onCheck(!checked);
else if (onClick) onClick();
else preview();
else {
if (!notSafe) preview();
}
}}
>
<div
@@ -76,6 +89,20 @@ export const DocumentRow = ({
<div className="shrink-0 ml-4 text-right lg:w-24 sm:w-20 ">
<BaseSmall>{formatBytes(item.size)}</BaseSmall>
</div>
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_AV_ENABLED) && (
<div className="shrink-0 ml-4 text-right lg:w-24 sm:w-20 ">
<BaseSmall title={Languages.t(`scenes.app.drive.document_row.av_${item?.av_status}`)}>
{item?.av_status === 'scanning' && (
<ShieldExclamationIcon className="w-5 text-yellow-400" />
)}
{item?.av_status === 'malicious' && (
<ShieldExclamationIcon className="w-5 text-rose-400" />
)}
{item?.av_status === 'skipped' && <BanIcon className="w-5 text-gray-400" />}
{item?.av_status === 'scan_failed' && <BanIcon className="w-5 text-gray-400" />}
</BaseSmall>
</div>
)}
<div className="shrink-0 ml-4">
<Menu menu={onBuildContextMenu}>
<Button
@@ -49,7 +49,7 @@ const VersionModalContent = ({ id }: { id: string }) => {
if (!item?.last_version_cache) return <></>;
return (
<ModalContent title={Languages.t('components.VersionModalContent_version') + " " + item?.name}>
<ModalContent title={Languages.t('components.VersionModalContent_version') + ' ' + item?.name}>
<UploadZone
overClassName={'!m-4'}
disableClick
@@ -66,12 +66,14 @@ const VersionModalContent = ({ id }: { id: string }) => {
}}
>
{access !== 'read' && (
<div className={'flex flex-row items-center bg-zinc-100 dark:bg-zinc-900 rounded-md mb-4 p-4'}>
<div
className={
'flex flex-row items-center bg-zinc-100 dark:bg-zinc-900 rounded-md mb-4 p-4'
}
>
<div className="flex flex-row">
<div className="grow flex items-center">
<Base>
{Languages.t('components.VersionModalContent_version_dec')}
</Base>
<Base>{Languages.t('components.VersionModalContent_version_dec')}</Base>
</div>
<div className="shrink-0 ml-4 flex items-center">
<Button
@@ -108,8 +110,11 @@ const VersionModalContent = ({ id }: { id: string }) => {
<BaseSmall>{formatBytes(version.file_metadata.size || 0)}</BaseSmall>
</div>
<div className="shrink-0 ml-4">
<Button theme="outline" onClick={() => download(id, version.id)}>
{Languages.t('components.VersionModalContent_donwload')}
<Button
theme="outline"
onClick={() => download(id, item.av_status === 'malicious', version.id)}
>
{Languages.t('components.VersionModalContent_donwload')}
</Button>
</div>
</div>
@@ -26,7 +26,7 @@ export default class ErrorBoundary extends React.Component<PropsType, { hasError
ErrorBoundary.lastError = {
error: {
name: error,
info: errorInfo.componentStack,
info: "",
},
};
RouterServices.replace(RouterServices.addRedirection(RouterServices.pathnames.ERROR));