1
20import { readFile } from "node:fs/promises";
21import {
22 getClamdVersion,
23 scanFileViaSocket,
24 scanStreamViaTcp,
25 type ScanReport
26} from "../../../lib/clamav.js";
27import { RetryableError } from "../errors.js";
28import type { FilePipelineContext } from "../types.js";
29import type { WorkerEnv } from "../../../config.js";
30import { getLogger } from "../../../logger.js";
31
32const log = getLogger("files.scan");
33
34
35export function assertScannerConfigured(env: WorkerEnv): void {
36 if (env.NODE_ENV === "production" && !env.CLAMD_SOCKET && !env.CLAMAV_HOST) {
37 throw new Error(
38 "worker refusing to boot: NODE_ENV=production requires a ClamAV scanner " +
39 "(set CLAMD_SOCKET or CLAMAV_HOST/CLAMAV_PORT) — fail closed (TASK-0102 §3)"
40 );
41 }
42}
43
44export interface ScanOutcome {
45 report: ScanReport;
46 infected: boolean;
47}
48
49
50export async function scanStep(ctx: FilePipelineContext): Promise<ScanOutcome> {
51 const { env } = ctx;
52
53 if (!env.CLAMD_SOCKET && !env.CLAMAV_HOST) {
54 log.warn("file scan skipped (CLAMD_SOCKET not configured)");
55 return {
56 report: {
57 engine: "none",
58 scanned: false,
59 infected: false,
60 skipped: true,
61 reason: "CLAMD_SOCKET not configured (dev mode)"
62 },
63 infected: false
64 };
65 }
66
67 let report: ScanReport;
68 if (env.CLAMD_SOCKET) {
69 report = await scanFileViaSocket(env.CLAMD_SOCKET, ctx.temp.path, env.CLAMAV_TIMEOUT_MS);
70 } else {
71 const buffer = await readFile(ctx.temp.path);
72 report = await scanStreamViaTcp(env.CLAMAV_HOST, env.CLAMAV_PORT, buffer, env.CLAMAV_TIMEOUT_MS);
73 }
74
75 if (report.error) {
76
77 throw new RetryableError(`clamav scan failed: ${report.error}`);
78 }
79
80
81
82 if (report.scanned) {
83 report.version = await getClamdVersion({
84 socket: env.CLAMD_SOCKET,
85 host: env.CLAMAV_HOST,
86 port: env.CLAMAV_PORT
87 });
88 }
89
90 if (report.scanned && report.infected) {
91 log.error(
92 { fileId: ctx.job.fileId, signature: report.signature },
93 "VIRUS DETECTED — quarantining file (no thumbs/index/event)"
94 );
95 }
96 return { report, infected: report.scanned && report.infected };
97}
98