Adjust antivirus feature

This commit is contained in:
lethemanh
2025-04-18 17:05:22 +07:00
parent f0b6315d76
commit 8789b91a7a
17 changed files with 210 additions and 20 deletions
@@ -0,0 +1,43 @@
<% layout('./_structure') %>
<% it.title = 'Twake Drive Infected Document Deletion Alert' %>
<%~ includeFile("../common/_body.eta", {
paragraphs: [
`
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%" >
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0 0 8px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:24px;font-weight:800;line-height:29px;text-align:center;color:#FF0000;">
Important: Antivirus Alert on Your Twake Drive
</div>
</td>
</tr>
</tbody>
</table>
`,
`
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%" >
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0 0 16px;word-break:break-word;" >
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:29px;text-align:center;color:#000000;">
<span style="font-weight: 500">A file on your Twake Drive was flagged during an antivirus scan and has been deleted to protect your account.</span>
</div>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:29px;text-align:center;color:#000000;">
<span style="font-weight: 500">
File: ${it.notifications[0].item.name}
</span>
</div>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:29px;text-align:center;color:#000000;">
<span style="color: #FF0000; font-weight: 600;">
Issue: ${ it.notifications[0].item.av_status === "scan_failed" ? "Scan Failed 🔍" : it.notifications[0].item.av_status === "malicious" ? "Malicious Content Detected ⚠️" : "File too large to be scanned 🚫" }
</span>
</div>
</td>
</tr>
</tbody>
</table>
`
]
}) %>
@@ -0,0 +1,43 @@
<% layout('./_structure') %>
<% it.title = 'Alerte de suppression de document infecté sur Twake Drive' %>
<%~ includeFile("../common/_body.eta", {
paragraphs: [
`
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%" >
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0 0 8px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:24px;font-weight:800;line-height:29px;text-align:center;color:#FF0000;">
Important : Alerte antivirus sur votre Twake Drive
</div>
</td>
</tr>
</tbody>
</table>
`,
`
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%" >
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0 0 16px;word-break:break-word;" >
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:29px;text-align:center;color:#000000;">
<span style="font-weight: 500">Un fichier sur votre Twake Drive a été signalé lors dune analyse antivirus et a été supprimé afin de protéger votre compte.</span>
</div>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:29px;text-align:center;color:#000000;">
<span style="font-weight: 500">
Fichier: ${it.notifications[0].item.name}
</span>
</div>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:29px;text-align:center;color:#000000;">
<span style="color: #FF0000; font-weight: 600;">
Problème: ${ it.notifications[0].item.av_status === "scan_failed" ? "Échec de lanalyse 🔍" : it.notifications[0].item.av_status === "malicious" ? "Contenu Malveillant Détecté ⚠️" : "Fichier trop volumineux pour être analysé 🚫" }
</span>
</div>
</td>
</tr>
</tbody>
</table>
`
]
}) %>
@@ -0,0 +1 @@
Alerte de suppression de document infecté sur Twake Drive
@@ -3,7 +3,7 @@ import { getLogger, logger, TdriveLogger } from "../../../core/platform/framewor
import NodeClam from "clamscan";
import { AVStatus, DriveFile } from "src/services/documents/entities/drive-file";
import { FileVersion } from "src/services/documents/entities/file-version";
import { DriveExecutionContext } from "src/services/documents/types";
import { DriveExecutionContext, NotificationActionType } from "../../documents/types";
import globalResolver from "../../../services/global-resolver";
import { getFilePath } from "../../files/services";
import { getConfigOrDefault } from "../../../utils/get-config";
@@ -14,7 +14,8 @@ export class AVServiceImpl implements TdriveServiceProvider, Initializable {
av: NodeClam = null;
logger: TdriveLogger = getLogger("Antivirus Service");
avEnabled: boolean = getConfigOrDefault<boolean>("drive.featureAntivirus", false);
private MAX_FILE_SIZE: number = getConfigOrDefault<number>("av.maxFileSize", 26214400); // 25 MB
deleteInfectedFileEnabled: boolean = getConfigOrDefault<boolean>("av.deleteInfectedFiles", false);
private MAX_FILE_SIZE: number = getConfigOrDefault<number>("av.maxFileSize", 4294967295); // 4GB
async init(): Promise<this> {
try {
@@ -56,6 +57,7 @@ export class AVServiceImpl implements TdriveServiceProvider, Initializable {
this.logger.error(`File ${version.file_metadata.external_id} not found`);
throw AVException.fileNotFound(`File ${version.file_metadata.external_id} not found`);
}
// check if the file is too large
if (file.upload_data.size > this.MAX_FILE_SIZE) {
this.logger.info(
@@ -79,6 +81,11 @@ export class AVServiceImpl implements TdriveServiceProvider, Initializable {
} else if (isInfected) {
await onScanComplete("malicious");
this.logger.info(`Item ${item.id} is malicious. Viruses found: ${viruses.join(", ")}`);
// Delete infected files if feature flag is enabled
if (this.deleteInfectedFileEnabled) {
await this.deleteInfectedFile(item, context);
}
} else {
await onScanComplete("safe");
this.logger.info(`Item ${item.id} is safe with no viruses detected.`);
@@ -95,4 +102,27 @@ export class AVServiceImpl implements TdriveServiceProvider, Initializable {
throw AVException.scanFailed("Document scanning encountered an error");
}
}
async deleteInfectedFile(item: Partial<DriveFile>, context: DriveExecutionContext) {
try {
// Delete infected file for permanent
await globalResolver.services.documents.documents.delete(item.id, null, context, true);
this.logger.info(`Infected file ${item.id} was automatically deleted`);
// Send notification to user about the file deletion
globalResolver.services.documents.engine.notifyInfectedDocumentRemoved({
context,
item: {
...item,
company_id: context.company.id,
} as DriveFile,
type: NotificationActionType.DIRECT,
notificationReceiver: context.user.id,
notificationEmitter: "",
});
this.logger.info(`Sent notification to ${context.user.id} about file deletion`);
} catch (error) {
this.logger.error(`Failed to delete infected file ${item.id}: ${error}`);
}
}
}
@@ -83,6 +83,13 @@ export class DocumentsEngine implements Initializable {
},
);
localEventBus.subscribe(
DocumentEvents.INFECTED_DOCUMENT_REMOVED,
async (e: NotificationPayloadType) => {
await this.DispatchDocumentEvent(e, DocumentEvents.INFECTED_DOCUMENT_REMOVED);
},
);
return this;
}
@@ -97,4 +104,8 @@ export class DocumentsEngine implements Initializable {
notifyDocumentAVScanAlert(notificationPayload: NotificationPayloadType) {
localEventBus.publish(DocumentEvents.DOCUMENT_AV_SCAN_ALERT, notificationPayload);
}
notifyInfectedDocumentRemoved(notificationPayload: NotificationPayloadType) {
localEventBus.publish(DocumentEvents.INFECTED_DOCUMENT_REMOVED, notificationPayload);
}
}
@@ -718,6 +718,7 @@ export class DocumentsService {
id: string | RootType | TrashType,
item?: DriveFile,
context?: DriveExecutionContext,
isPermanentlyDelete?: boolean,
): Promise<void> => {
if (!id) {
//We can't remove the root folder
@@ -864,6 +865,10 @@ export class DocumentsService {
}
await this.update(item.id, item, context);
if (isPermanentlyDelete) {
await this.delete(item.id, item, context);
}
}
await updateItemSize(previousParentId, this.repository, context);
}
@@ -1833,7 +1838,10 @@ export class DocumentsService {
item.av_status = status;
await this.repository.save(item);
if (["malicious", "scan_failed"].includes(status)) {
if (
(!globalResolver.services.av?.deleteInfectedFileEnabled && status === "malicious") ||
status === "scan_failed"
) {
await this.notifyAVScanAlert(item, context);
}
};
@@ -123,12 +123,14 @@ export enum DocumentEvents {
DOCUMENT_SAHRED = "document_shared",
DOCUMENT_VERSION_UPDATED = "document_version_updated",
DOCUMENT_AV_SCAN_ALERT = "document_av_scan_alert",
INFECTED_DOCUMENT_REMOVED = "infected_document_removed",
}
export const eventToTemplateMap: Record<string, any> = {
[DocumentEvents.DOCUMENT_AV_SCAN_ALERT]: "notification-document-av-scan-alert",
[DocumentEvents.DOCUMENT_VERSION_UPDATED]: "notification-document-version-updated",
[DocumentEvents.DOCUMENT_SAHRED]: "notification-document-shared",
[DocumentEvents.INFECTED_DOCUMENT_REMOVED]: "notification-infected-document-deletion-alert",
};
export enum NotificationActionType {
@@ -70,6 +70,8 @@ export function formatCompany(
[CompanyFeaturesEnum.COMPANY_AV_ENABLED]: JSON.parse(
config.get("drive.featureAntivirus") || "false",
),
[CompanyFeaturesEnum.COMPANY_AV_STATUS_ALLOWED]:
config.get("drive.featureAvStatusAllowed") || {},
},
{
...(res.plan?.features || {}),
@@ -97,6 +97,7 @@ export const companyObjectSchema = {
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_AV_ENABLED]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_AV_STATUS_ALLOWED]: {} as { [key: string]: string[] },
guests: { type: "number" }, // to rename or delete
members: { type: "number" }, // to rename or delete
storage: { type: "number" }, // to rename or delete
@@ -88,6 +88,7 @@ export enum CompanyFeaturesEnum {
COMPANY_USER_QUOTA = "company:user_quota",
COMPANY_MANAGE_ACCESS = "company:managed_access",
COMPANY_AV_ENABLED = "company:av_enabled",
COMPANY_AV_STATUS_ALLOWED = "company:av_status_allowed",
}
export type CompanyFeaturesObject = {
@@ -102,6 +103,7 @@ export type CompanyFeaturesObject = {
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]?: boolean;
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]?: boolean;
[CompanyFeaturesEnum.COMPANY_AV_ENABLED]?: boolean;
[CompanyFeaturesEnum.COMPANY_AV_STATUS_ALLOWED]?: { [key: string]: string[] };
};
export type CompanyLimitsObject = {