feat: add ClamAV to scan for malicious files

This commit is contained in:
Elias Schneider
2023-01-13 10:16:35 +01:00
parent 16b697053a
commit 76088cc76a
18 changed files with 284 additions and 171 deletions

View File

@@ -12,6 +12,7 @@ import { JobsModule } from "./jobs/jobs.module";
import { PrismaModule } from "./prisma/prisma.module";
import { ShareModule } from "./share/share.module";
import { UserModule } from "./user/user.module";
import { ClamscanModule } from "./clamscan/clamscan.module";
@Module({
imports: [
@@ -28,6 +29,7 @@ import { UserModule } from "./user/user.module";
limit: 100,
}),
ScheduleModule.forRoot(),
ClamscanModule,
],
providers: [
{

View File

@@ -0,0 +1,10 @@
import { forwardRef, Module } from "@nestjs/common";
import { FileModule } from "src/file/file.module";
import { ClamScanService } from "./clamscan.service";
@Module({
imports: [forwardRef(() => FileModule)],
providers: [ClamScanService],
exports: [ClamScanService],
})
export class ClamscanModule {}

View File

@@ -0,0 +1,86 @@
import { Injectable } from "@nestjs/common";
import * as NodeClam from "clamscan";
import * as fs from "fs";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
const clamscanConfig = {
clamdscan: {
host: process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1",
port: 3310,
localFallback: false,
},
preference: "clamdscan",
};
@Injectable()
export class ClamScanService {
constructor(
private fileService: FileService,
private prisma: PrismaService
) {}
private ClamScan: Promise<NodeClam | null> = new NodeClam()
.init(clamscanConfig)
.then((res) => {
console.log("ClamAV is active");
return res;
})
.catch(() => {
console.log("ClamAV is not active");
return null;
});
async check(shareId: string) {
const clamScan = await this.ClamScan;
if (!clamScan) return [];
const infectedFiles = [];
const files = fs
.readdirSync(`./data/uploads/shares/${shareId}`)
.filter((file) => file != "archive.zip");
for (const fileId of files) {
const { isInfected } = await clamScan
.isInfected(`./data/uploads/shares/${shareId}/${fileId}`)
.catch(() => {
console.log("ClamAV is not active");
return { isInfected: false };
});
const fileName = (
await this.prisma.file.findUnique({ where: { id: fileId } })
).name;
if (isInfected) {
infectedFiles.push({ id: fileId, name: fileName });
}
}
return infectedFiles;
}
async checkAndRemove(shareId: string) {
const infectedFiles = await this.check(shareId);
if (infectedFiles.length > 0) {
await this.fileService.deleteAllFiles(shareId);
await this.prisma.file.deleteMany({ where: { shareId } });
const fileNames = infectedFiles.map((file) => file.name).join(", ");
await this.prisma.share.update({
where: { id: shareId },
data: {
removedReason: `Your share got removed because the file(s) ${fileNames} are malicious.`,
},
});
console.log(
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`
);
}
}
}

View File

@@ -11,7 +11,7 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.use(bodyParser.raw({type:'application/octet-stream', limit:'20mb'}));
app.use(bodyParser.raw({ type: "application/octet-stream", limit: "20mb" }));
app.use(cookieParser());
app.set("trust proxy", true);

View File

@@ -1,12 +1,18 @@
import { forwardRef, Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { ClamscanModule } from "src/clamscan/clamscan.module";
import { EmailModule } from "src/email/email.module";
import { FileModule } from "src/file/file.module";
import { ShareController } from "./share.controller";
import { ShareService } from "./share.service";
@Module({
imports: [JwtModule.register({}), EmailModule, forwardRef(() => FileModule)],
imports: [
JwtModule.register({}),
EmailModule,
ClamscanModule,
forwardRef(() => FileModule),
],
controllers: [ShareController],
providers: [ShareService],
exports: [ShareService],

View File

@@ -10,6 +10,7 @@ import * as archiver from "archiver";
import * as argon from "argon2";
import * as fs from "fs";
import * as moment from "moment";
import { ClamScanService } from "src/clamscan/clamscan.service";
import { ConfigService } from "src/config/config.service";
import { EmailService } from "src/email/email.service";
import { FileService } from "src/file/file.service";
@@ -23,7 +24,8 @@ export class ShareService {
private fileService: FileService,
private emailService: EmailService,
private config: ConfigService,
private jwtService: JwtService
private jwtService: JwtService,
private clasmScanService: ClamScanService
) {}
async create(share: CreateShareDTO, user?: User) {
@@ -123,6 +125,9 @@ export class ShareService {
);
}
// Check if any file is malicious with ClamAV
this.clasmScanService.checkAndRemove(share.id);
return await this.prisma.share.update({
where: { id },
data: { uploadLocked: true },
@@ -157,7 +162,7 @@ export class ShareService {
}
async get(id: string) {
const share: any = await this.prisma.share.findUnique({
const share = await this.prisma.share.findUnique({
where: { id },
include: {
files: true,
@@ -165,10 +170,13 @@ export class ShareService {
},
});
if (share.removedReason)
throw new NotFoundException(share.removedReason, "share_removed");
if (!share || !share.uploadLocked)
throw new NotFoundException("Share not found");
return share;
return share as any;
}
async getMetaData(id: string) {