Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow multiple av services to exist #87

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions processes.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
module.exports = [{
script: './dist/index.js',
name: 'host_my_shit',
time: true,
source_map_support: true,
watch: false,
autorestart: true,
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
env: {
NODE_ENV: "production"
}
}];
module.exports = [
{
script: "./dist/index.js",
name: "waifu_valt",
time: true,
source_map_support: true,
watch: false,
autorestart: true,
error_file: "./logs/err.log",
out_file: "./logs/out.log",
log_file: "./logs/combined.log",
env: {
NODE_ENV: "production",
},
},
];
21 changes: 18 additions & 3 deletions src/engine/IAvEngine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { AvScanResult } from "../utils/typeings.js";
import { AvScanResult, Awaitable } from "../utils/typeings.js";

export interface IAvEngine {
scan(resource: string): Promise<AvScanResult> | AvScanResult;
/**
* Scan a file using the av service defined
* @param {string} resource
* @returns {Awaitable<AvScanResult>}
*/
scan(resource: string): Awaitable<AvScanResult>;

get enabled(): boolean;
/**
* Return true if this engine is enabled
* @returns {Awaitable<boolean>}
*/
get enabled(): Awaitable<boolean>;

/**
* Get the name of this engine. normally returns the name of the AV service being used
* @returns {Awaitable<string>}
*/
get name(): Awaitable<string>;
}
8 changes: 7 additions & 1 deletion src/engine/impl/av/ClamAvEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ export class ClamAvEngine implements IAvEngine {
return {
errorCode: e.code,
passed: false,
additionalMessage: e.messag,
additionalMessage: e.message,
engineName: this.name,
};
}
return {
passed: true,
engineName: this.name,
};
}

public get name(): string {
return "ClamAv";
}
}
8 changes: 7 additions & 1 deletion src/engine/impl/av/MsDefenderEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ export class MsDefenderEngine implements IAvEngine {
return {
errorCode: e.code,
passed: false,
additionalMessage: e.messag,
additionalMessage: e.message,
engineName: this.name,
};
}
return {
passed: true,
engineName: this.name,
};
}

public get name(): string {
return "MsDefender";
}
}
12 changes: 8 additions & 4 deletions src/factory/AvFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ export class AvFactory {
public constructor(@Inject(AV_ENGINE) private readonly engines: IAvEngine[]) {}

/**
* Get the first enabled AV engine
* @returns {IAvEngine | null}
* Get all enabled av engines
* @returns {IAvEngine[]}
*/
public getFirstAvailableAvEngine(): IAvEngine | null {
return this.engines.find(e => e.enabled) ?? null;
public async getAvEngines(): Promise<IAvEngine[]> {
const enabledEngines = await Promise.all(
this.engines.map(async engine => ((await engine.enabled) ? engine : null)),
);

return enabledEngines.filter(engine => engine !== null) as IAvEngine[];
}
}
49 changes: 36 additions & 13 deletions src/manager/AvManager.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
import { Inject, Injectable } from "@tsed/di";
import { Inject, Injectable, OnInit } from "@tsed/di";
import { AvFactory } from "../factory/AvFactory.js";
import type { PlatformMulterFile } from "@tsed/common";
import { Logger } from "@tsed/logger";
import { FileEngine } from "../engine/impl/index.js";
import { BadRequest } from "@tsed/exceptions";
import path from "node:path";
import { IAvEngine } from "../engine/IAvEngine.js";
import { AvScanResult } from "../utils/typeings.js";

@Injectable()
export class AvManager {
export class AvManager implements OnInit {
private avEngines: IAvEngine[];

public constructor(
@Inject() private readonly avFactory: AvFactory,
@Inject() private avFactory: AvFactory,
@Inject() private logger: Logger,
@Inject() private fileEngine: FileEngine,
) {}

public async $onInit(): Promise<void> {
this.avEngines = await this.avFactory.getAvEngines();
if (this.avEngines.length === 0) {
this.logger.warn("No AV is enabled! AV scanning is disabled");
} else if (this.avEngines.length === 1) {
this.logger.info(`Using av engine ${this.avEngines[0].name} to scan files`);
} else {
this.logger.info(
`Multiple AV engines are enabled, the engines will be used to scan files in the following order: ${this.avEngines.map(e => e.name).join(", ")}`,
);
}
}

public async scanFile(file: string | PlatformMulterFile): Promise<void> {
const avEngineToUse = this.avFactory.getFirstAvailableAvEngine();
if (!avEngineToUse) {
this.logger.warn("No AV is enabled!");
if (this.avEngines.length === 0) {
return;
}
const resource = typeof file === "string" ? path.basename(file) : file.filename;
const scanResult = await avEngineToUse.scan(resource);
if (!scanResult.passed) {
const scanResults = await this.doScan(resource);

for (const scanResult of scanResults) {
if (scanResult.passed) {
continue;
}
const fileExists = await this.fileEngine.fileExists(resource);
if (fileExists) {
try {
Expand All @@ -33,19 +52,23 @@ export class AvManager {
throw new BadRequest(e.message);
}
}
let errStr = "";
let errStr = `AV engine ${scanResult.engineName} found issues `;
if (scanResult.additionalMessage) {
errStr += `AV scan of resource ${resource} failed.`;
errStr += `AV scan of resource ${resource} for issues terminated with message "${scanResult.additionalMessage}"`;
}
if (scanResult.errorCode) {
errStr += ` scan filed with error code ${scanResult.errorCode}`;
errStr += ` scan exited with code ${scanResult.errorCode}`;
}
if (errStr === "") {
// the scan failed but nothing was reported
errStr = "AV Scan failed with no reported reason";
// the scan found issues but nothing was reported
errStr = "AV Scan ended with no reported reason";
}
this.logger.warn(errStr);
throw new BadRequest("Failed to store file");
}
}

private doScan(resource: string): Promise<AvScanResult[]> {
return Promise.all(this.avEngines.map(engine => engine.scan(resource)));
}
}
3 changes: 3 additions & 0 deletions src/utils/typeings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type EntrySettings = {

export type AvScanResult = {
passed: boolean;
engineName: string;
errorCode?: number;
additionalMessage?: string;
};
Expand Down Expand Up @@ -54,3 +55,5 @@ export type IpBlockedAwareFileEntry = {
};

export type ProtectionLevel = "Encrypted" | "Password" | "None";

export type Awaitable<T> = Promise<T> | T;