@uploadista/flow-security-clamscan 0.0.20-beta.9 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/.turbo/turbo-check.log +6 -0
- package/.turbo/turbo-test.log +136 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
- package/tests/clamscan-plugin.test.ts +435 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @uploadista/flow-security-clamscan@0.0
|
|
3
|
+
> @uploadista/flow-security-clamscan@0.1.0-beta.4 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
|
|
4
4
|
> tsc --noEmit && tsdown
|
|
5
5
|
|
|
6
|
-
[34mℹ[39m tsdown [2mv0.
|
|
6
|
+
[34mℹ[39m tsdown [2mv0.19.0[22m powered by rolldown [2mv1.0.0-beta.59[22m
|
|
7
7
|
[34mℹ[39m config file: [4m/Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan/tsdown.config.ts[24m
|
|
8
8
|
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
9
9
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [2m0.25 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
15
15
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [2m1.68 kB[22m [2m│ gzip: 0.73 kB[22m
|
|
16
16
|
[34mℹ[39m [33m[CJS][39m 2 files, total: 1.92 kB
|
|
17
|
+
[32m✔[39m Build complete in [32m6173ms[39m
|
|
17
18
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m2.27 kB[22m [2m│ gzip: 0.97 kB[22m
|
|
18
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m9.
|
|
19
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m9.40 kB[22m [2m│ gzip: 2.93 kB[22m
|
|
19
20
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m0.25 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
20
21
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m1.68 kB[22m [2m│ gzip: 0.73 kB[22m
|
|
21
|
-
[34mℹ[39m [34m[ESM][39m 4 files, total: 13.
|
|
22
|
-
[32m✔[39m Build complete in [
|
|
23
|
-
[32m✔[39m Build complete in [32m7005ms[39m
|
|
22
|
+
[34mℹ[39m [34m[ESM][39m 4 files, total: 13.59 kB
|
|
23
|
+
[32m✔[39m Build complete in [32m6202ms[39m
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @uploadista/flow-security-clamscan@0.1.0-beta.2 check /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
|
|
4
|
+
> biome check --write ./src
|
|
5
|
+
|
|
6
|
+
[0m[34mChecked [0m[0m[34m3[0m[0m[34m [0m[0m[34mfiles[0m[0m[34m in [0m[0m[34m57[0m[0m[2m[34mms[0m[0m[34m.[0m[0m[34m No fixes applied.[0m
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @uploadista/flow-security-clamscan@0.1.0-beta.2 test /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
|
|
4
|
+
> vitest run
|
|
5
|
+
|
|
6
|
+
[?25l
|
|
7
|
+
[1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan[39m
|
|
8
|
+
|
|
9
|
+
[?2026h
|
|
10
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m [queued][22m
|
|
11
|
+
|
|
12
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
13
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
14
|
+
[2m Start at [22m16:59:24
|
|
15
|
+
[2m Duration [22m424ms
|
|
16
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
17
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m [queued][22m
|
|
18
|
+
|
|
19
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
20
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
21
|
+
[2m Start at [22m16:59:24
|
|
22
|
+
[2m Duration [22m954ms
|
|
23
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
24
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m [queued][22m
|
|
25
|
+
|
|
26
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
27
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
28
|
+
[2m Start at [22m16:59:24
|
|
29
|
+
[2m Duration [22m2.14s
|
|
30
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
31
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m [queued][22m
|
|
32
|
+
|
|
33
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
34
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
35
|
+
[2m Start at [22m16:59:24
|
|
36
|
+
[2m Duration [22m3.10s
|
|
37
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
38
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m [queued][22m
|
|
39
|
+
|
|
40
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
41
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
42
|
+
[2m Start at [22m16:59:24
|
|
43
|
+
[2m Duration [22m4.07s
|
|
44
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
45
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m [queued][22m
|
|
46
|
+
|
|
47
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
48
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
49
|
+
[2m Start at [22m16:59:24
|
|
50
|
+
[2m Duration [22m5.11s
|
|
51
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
52
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m 0/25[22m
|
|
53
|
+
|
|
54
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
55
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (25)[39m
|
|
56
|
+
[2m Start at [22m16:59:24
|
|
57
|
+
[2m Duration [22m5.64s
|
|
58
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
59
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m 1/25[22m
|
|
60
|
+
|
|
61
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
62
|
+
[2m Tests [22m[1m[32m1 passed[39m[22m[90m (25)[39m
|
|
63
|
+
[2m Start at [22m16:59:24
|
|
64
|
+
[2m Duration [22m5.84s
|
|
65
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
66
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m 8/25[22m
|
|
67
|
+
|
|
68
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
69
|
+
[2m Tests [22m[1m[32m8 passed[39m[22m[90m (25)[39m
|
|
70
|
+
[2m Start at [22m16:59:24
|
|
71
|
+
[2m Duration [22m6.05s
|
|
72
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
73
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m 9/25[22m
|
|
74
|
+
|
|
75
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
76
|
+
[2m Tests [22m[1m[32m9 passed[39m[22m[90m (25)[39m
|
|
77
|
+
[2m Start at [22m16:59:24
|
|
78
|
+
[2m Duration [22m6.16s
|
|
79
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
80
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m 20/25[22m
|
|
81
|
+
|
|
82
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
83
|
+
[2m Tests [22m[1m[32m20 passed[39m[22m[90m (25)[39m
|
|
84
|
+
[2m Start at [22m16:59:24
|
|
85
|
+
[2m Duration [22m6.26s
|
|
86
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
|
|
87
|
+
[1m[33m ❯ [39m[22mtests/clamscan-plugin.test.ts[2m 24/25[22m
|
|
88
|
+
|
|
89
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
|
|
90
|
+
[2m Tests [22m[1m[32m24 passed[39m[22m[90m (25)[39m
|
|
91
|
+
[2m Start at [22m16:59:24
|
|
92
|
+
[2m Duration [22m6.36s
|
|
93
|
+
[?2026l[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K [32m✓[39m tests/clamscan-plugin.test.ts [2m([22m[2m25 tests[22m[2m)[22m[33m 628[2mms[22m[39m
|
|
94
|
+
[32m✓[39m ClamScan Virus Plugin [2m(25)[22m
|
|
95
|
+
[32m✓[39m virusScanPlugin factory [2m(2)[22m
|
|
96
|
+
[32m✓[39m should create a plugin layer with default configuration[32m 9[2mms[22m[39m
|
|
97
|
+
[32m✓[39m should create a plugin layer with custom configuration[32m 6[2mms[22m[39m
|
|
98
|
+
[32m✓[39m scan method [2m(5)[22m
|
|
99
|
+
[32m✓[39m should return clean result for non-infected file[32m 87[2mms[22m[39m
|
|
100
|
+
[32m✓[39m should return infected result with virus names[32m 14[2mms[22m[39m
|
|
101
|
+
[32m✓[39m should handle null virus array for infected files[32m 15[2mms[22m[39m
|
|
102
|
+
[32m✓[39m should handle binary data correctly[32m 10[2mms[22m[39m
|
|
103
|
+
[32m✓[39m should handle large file data[32m 18[2mms[22m[39m
|
|
104
|
+
[32m✓[39m getVersion method [2m(3)[22m
|
|
105
|
+
[32m✓[39m should return ClamAV version string[32m 45[2mms[22m[39m
|
|
106
|
+
[32m✓[39m should return 'Unknown' when version is undefined[32m 5[2mms[22m[39m
|
|
107
|
+
[32m✓[39m should return 'Unknown' when version is empty[32m 3[2mms[22m[39m
|
|
108
|
+
[32m✓[39m error handling [2m(3)[22m
|
|
109
|
+
[32m✓[39m should fail with CLAMAV_NOT_INSTALLED when initialization fails[32m 24[2mms[22m[39m
|
|
110
|
+
[32m✓[39m should fail with VIRUS_SCAN_FAILED when scan fails[32m 48[2mms[22m[39m
|
|
111
|
+
[32m✓[39m should fail with VIRUS_SCAN_FAILED when getVersion fails[32m 1[2mms[22m[39m
|
|
112
|
+
[32m✓[39m configuration options [2m(6)[22m
|
|
113
|
+
[32m✓[39m should accept clamdscan preference[32m 0[2mms[22m[39m
|
|
114
|
+
[32m✓[39m should accept clamscan preference[32m 0[2mms[22m[39m
|
|
115
|
+
[32m✓[39m should accept socket configuration for clamd[32m 0[2mms[22m[39m
|
|
116
|
+
[32m✓[39m should accept TCP host and port for clamd[32m 0[2mms[22m[39m
|
|
117
|
+
[32m✓[39m should accept debug mode configuration[32m 0[2mms[22m[39m
|
|
118
|
+
[32m✓[39m should accept remove_infected configuration[32m 0[2mms[22m[39m
|
|
119
|
+
[32m✓[39m EICAR test file detection [2m(1)[22m
|
|
120
|
+
[32m✓[39m should detect EICAR test file as infected[32m 40[2mms[22m[39m
|
|
121
|
+
[32m✓[39m temporary file handling [2m(2)[22m
|
|
122
|
+
[32m✓[39m should clean up temporary files after successful scan[32m 18[2mms[22m[39m
|
|
123
|
+
[32m✓[39m should clean up temporary files even when scan fails[32m 27[2mms[22m[39m
|
|
124
|
+
[32m✓[39m lazy initialization [2m(1)[22m
|
|
125
|
+
[32m✓[39m should initialize scanner lazily on first use[32m 23[2mms[22m[39m
|
|
126
|
+
[32m✓[39m multiple virus detection [2m(1)[22m
|
|
127
|
+
[32m✓[39m should report multiple viruses when detected[32m 85[2mms[22m[39m
|
|
128
|
+
[32m✓[39m empty file handling [2m(1)[22m
|
|
129
|
+
[32m✓[39m should handle empty file input[32m 32[2mms[22m[39m
|
|
130
|
+
|
|
131
|
+
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
132
|
+
[2m Tests [22m [1m[32m25 passed[39m[22m[90m (25)[39m
|
|
133
|
+
[2m Start at [22m 16:59:24
|
|
134
|
+
[2m Duration [22m 6.49s[2m (transform 1.52s, setup 0ms, import 5.22s, tests 628ms, environment 0ms)[22m
|
|
135
|
+
|
|
136
|
+
[?25h
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport type { ScanResult, VirusScanPluginShape } from \"@uploadista/core/flow\";\nimport { VirusScanPlugin } from \"@uploadista/core/flow\";\nimport { withOperationSpan } from \"@uploadista/observability\";\nimport NodeClam from \"clamscan\";\nimport { Effect, Layer } from \"effect\";\n\n/**\n * Configuration options for the ClamAV Virus Scan plugin\n */\nexport interface VirusScanPluginConfig {\n /**\n * Preference for scanning method\n * - \"clamdscan\": Use clamd daemon (faster, recommended)\n * - \"clamscan\": Use command-line binary\n */\n preference?: \"clamdscan\" | \"clamscan\";\n\n /**\n * Path to clamd socket (for daemon mode)\n * Default: /var/run/clamd.scan/clamd.sock\n */\n clamdscan_socket?: string;\n\n /**\n * TCP host for clamd (alternative to socket)\n */\n clamdscan_host?: string;\n\n /**\n * TCP port for clamd\n * Default: 3310\n */\n clamdscan_port?: number;\n\n /**\n * Whether to remove infected files automatically\n * Default: false (not recommended in flow context)\n */\n remove_infected?: boolean;\n\n /**\n * Debug mode for clamscan library\n * Default: false\n */\n debug_mode?: boolean;\n}\n\n/**\n * ClamAV implementation of the VirusScanPlugin\n *\n * This plugin uses the `clamscan` npm package to scan files for viruses\n * using ClamAV antivirus engine. It supports both clamd daemon mode (fast)\n * and binary mode (slower but more portable).\n *\n * @example\n * ```typescript\n * import { ClamScanPluginLayer } from \"@uploadista/flow-security-clamscan\";\n *\n * const program = Effect.gen(function* () {\n * const virusScan = yield* VirusScanPlugin;\n * const result = yield* virusScan.scan(fileBytes);\n * console.log(result.isClean ? \"Clean\" : \"Infected\");\n * }).pipe(Effect.provide(ClamScanPluginLayer));\n * ```\n */\nclass ClamScanPluginImpl implements VirusScanPluginShape {\n private clamscan: NodeClam | null = null;\n\n constructor(private config: VirusScanPluginConfig = {}) {}\n\n /**\n * Initialize the ClamAV scanner\n * This is called lazily on first use\n */\n private initScanner(): Effect.Effect<NodeClam, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n if (this.clamscan) {\n return this.clamscan;\n }\n\n const scanner = yield* Effect.tryPromise({\n try: async () => {\n // Initialize clamscan with configuration\n return await new NodeClam().init({\n preference: this.config.preference ?? \"clamdscan\",\n remove_infected: this.config.remove_infected ?? false,\n debug_mode: this.config.debug_mode ?? false,\n clamdscan: {\n socket: this.config.clamdscan_socket,\n host: this.config.clamdscan_host,\n port: this.config.clamdscan_port ?? 3310,\n timeout: 60000,\n local_fallback: true, // Fall back to binary if daemon unavailable\n },\n clamscan: {\n path: \"/usr/bin/clamscan\", // Standard path\n scan_archives: true,\n active: true,\n },\n });\n },\n catch: (error) =>\n UploadistaError.fromCode(\"CLAMAV_NOT_INSTALLED\", {\n body: `ClamAV initialization failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { error },\n }),\n });\n\n this.clamscan = scanner;\n return scanner;\n }.bind(this),\n ).pipe(\n withOperationSpan(\"virus-scan\", \"init\", {\n \"scan.preference\": this.config.preference ?? \"clamdscan\",\n }),\n );\n }\n\n /**\n * Scans a file for viruses using ClamAV\n *\n * @param input - File contents as Uint8Array\n * @returns Effect with scan results\n */\n scan(input: Uint8Array): Effect.Effect<ScanResult, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n // Initialize scanner (lazy initialization)\n const scanner = yield* this.initScanner();\n\n // Create temporary file path for scanning\n const tmpDir = os.tmpdir();\n const fileName = `uploadista-scan-${randomUUID()}`;\n const tempFilePath = path.join(tmpDir, fileName);\n\n // Write file data to temp file\n yield* Effect.tryPromise({\n try: () => fs.writeFile(tempFilePath, input),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: \"Failed to create temporary file for scanning\",\n details: { error },\n }),\n });\n\n // Scan the file and ensure cleanup\n const result = yield* Effect.tryPromise({\n try: () => scanner.isInfected(tempFilePath),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: `Virus scan failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { error },\n }),\n }).pipe(\n Effect.map((scanResult) => ({\n isClean: !scanResult.isInfected,\n detectedViruses: scanResult.viruses || [],\n })),\n Effect.ensuring(\n // Clean up temporary file (ignore errors)\n Effect.tryPromise({\n try: () => fs.unlink(tempFilePath),\n catch: () => undefined,\n }).pipe(Effect.ignore),\n ),\n );\n\n return result;\n }.bind(this),\n ).pipe(\n withOperationSpan(\"virus-scan\", \"scan\", {\n \"scan.file_size\": input.byteLength,\n }),\n );\n }\n\n /**\n * Gets the ClamAV engine version\n *\n * @returns Effect with version string\n */\n getVersion(): Effect.Effect<string, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n // Initialize scanner (lazy initialization)\n const scanner = yield* this.initScanner();\n\n // Get version from ClamAV\n const versionResult = yield* Effect.tryPromise({\n try: () => scanner.getVersion(),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: \"Failed to get ClamAV version\",\n details: { error },\n }),\n });\n\n return versionResult.version || \"Unknown\";\n }.bind(this),\n ).pipe(withOperationSpan(\"virus-scan\", \"get-version\", {}));\n }\n}\n\n/**\n * Creates a Virus Scan Plugin layer using ClamAV\n *\n * @param config - Optional ClamAV configuration\n * @returns Layer providing VirusScanPlugin\n *\n * @example\n * ```typescript\n * // Use with default configuration\n * const layer = ClamScanPluginLayer();\n *\n * // Use with custom configuration\n * const customLayer = ClamScanPluginLayer({\n * preference: \"clamdscan\",\n * clamdscan_socket: \"/var/run/clamav/clamd.ctl\"\n * });\n * ```\n */\nexport function virusScanPlugin(\n config: VirusScanPluginConfig = {},\n): Layer.Layer<VirusScanPlugin, never, never> {\n return Layer.succeed(VirusScanPlugin, new ClamScanPluginImpl(config));\n}\n"],"mappings":"qXAsEA,IAAM,EAAN,KAAyD,CACvD,SAAoC,KAEpC,YAAY,EAAwC,EAAE,CAAE,CAApC,KAAA,OAAA,EAMpB,aAAgE,CAC9D,OAAO,EAAO,IACZ,WAAqC,CACnC,GAAI,KAAK,SACP,OAAO,KAAK,SAGd,IAAM,EAAU,MAAO,EAAO,WAAW,CACvC,IAAK,SAEI,MAAM,IAAI,GAAU,CAAC,KAAK,CAC/B,WAAY,KAAK,OAAO,YAAc,YACtC,gBAAiB,KAAK,OAAO,iBAAmB,GAChD,WAAY,KAAK,OAAO,YAAc,GACtC,UAAW,CACT,OAAQ,KAAK,OAAO,iBACpB,KAAM,KAAK,OAAO,eAClB,KAAM,KAAK,OAAO,gBAAkB,KACpC,QAAS,IACT,eAAgB,GACjB,CACD,SAAU,CACR,KAAM,oBACN,cAAe,GACf,OAAQ,GACT,CACF,CAAC,CAEJ,MAAQ,GACN,EAAgB,SAAS,uBAAwB,CAC/C,KAAM,iCAAiC,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAC7F,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAGF,MADA,MAAK,SAAW,EACT,GACP,KAAK,KAAK,CACb,CAAC,KACA,EAAkB,aAAc,OAAQ,CACtC,kBAAmB,KAAK,OAAO,YAAc,YAC9C,CAAC,CACH,CASH,KAAK,EAA+D,CAClE,OAAO,EAAO,IACZ,WAAqC,CAEnC,IAAM,EAAU,MAAO,KAAK,aAAa,CAGnC,EAAS,EAAG,QAAQ,CACpB,EAAW,mBAAmB,GAAY,GAC1C,EAAe,EAAK,KAAK,EAAQ,EAAS,CAkChD,OA/BA,MAAO,EAAO,WAAW,CACvB,QAAW,EAAG,UAAU,EAAc,EAAM,CAC5C,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,+CACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAGa,MAAO,EAAO,WAAW,CACtC,QAAW,EAAQ,WAAW,EAAa,CAC3C,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,sBAAsB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAClF,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAAC,KACD,EAAO,IAAK,IAAgB,CAC1B,QAAS,CAAC,EAAW,WACrB,gBAAiB,EAAW,SAAW,EAAE,CAC1C,EAAE,CACH,EAAO,SAEL,EAAO,WAAW,CAChB,QAAW,EAAG,OAAO,EAAa,CAClC,UAAa,IAAA,GACd,CAAC,CAAC,KAAK,EAAO,OAAO,CACvB,CACF,EAGD,KAAK,KAAK,CACb,CAAC,KACA,EAAkB,aAAc,OAAQ,CACtC,iBAAkB,EAAM,WACzB,CAAC,CACH,CAQH,YAAqD,CACnD,OAAO,EAAO,IACZ,WAAqC,CAEnC,IAAM,EAAU,MAAO,KAAK,aAAa,CAYzC,OATsB,MAAO,EAAO,WAAW,CAC7C,QAAW,EAAQ,YAAY,CAC/B,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,+BACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,EAEmB,SAAW,WAChC,KAAK,KAAK,CACb,CAAC,KAAK,EAAkB,aAAc,cAAe,EAAE,CAAC,CAAC,GAsB9D,SAAgB,EACd,EAAgC,EAAE,CACU,CAC5C,OAAO,EAAM,QAAQ,EAAiB,IAAI,EAAmB,EAAO,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/flow-security-clamscan",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0-beta.5",
|
|
5
5
|
"description": "ClamAV virus scanning plugin for Uploadista Flow",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"clamscan": "^2.3.3",
|
|
18
|
-
"@uploadista/core": "0.0
|
|
19
|
-
"@uploadista/observability": "0.0
|
|
18
|
+
"@uploadista/core": "0.1.0-beta.5",
|
|
19
|
+
"@uploadista/observability": "0.1.0-beta.5"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"effect": "^3.0.0",
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@effect/vitest": "0.27.0",
|
|
27
|
-
"@types/node": "24.10.
|
|
28
|
-
"effect": "3.19.
|
|
29
|
-
"tsdown": "0.
|
|
30
|
-
"vitest": "4.0.
|
|
31
|
-
"zod": "4.
|
|
32
|
-
"@uploadista/typescript-config": "0.0
|
|
27
|
+
"@types/node": "24.10.8",
|
|
28
|
+
"effect": "3.19.14",
|
|
29
|
+
"tsdown": "0.19.0",
|
|
30
|
+
"vitest": "4.0.17",
|
|
31
|
+
"zod": "4.3.5",
|
|
32
|
+
"@uploadista/typescript-config": "0.1.0-beta.5"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc --noEmit && tsdown",
|
|
36
36
|
"format": "biome format --write ./src",
|
|
37
37
|
"lint": "biome lint --write ./src",
|
|
38
38
|
"check": "biome check --write ./src",
|
|
39
|
-
"test": "vitest",
|
|
39
|
+
"test": "vitest run",
|
|
40
40
|
"test:run": "vitest run",
|
|
41
41
|
"test:watch": "vitest watch"
|
|
42
42
|
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { VirusScanPlugin } from "@uploadista/core/flow";
|
|
4
|
+
import { virusScanPlugin, type VirusScanPluginConfig } from "../src/clamscan-plugin";
|
|
5
|
+
|
|
6
|
+
// Shared mock state
|
|
7
|
+
let mockIsInfectedResult: { isInfected: boolean; file: string; viruses: string[] | null } = {
|
|
8
|
+
isInfected: false,
|
|
9
|
+
file: "/tmp/test-file",
|
|
10
|
+
viruses: [],
|
|
11
|
+
};
|
|
12
|
+
let mockVersionResult: { version: string | undefined } = { version: "ClamAV 1.0.0" };
|
|
13
|
+
let mockInitShouldFail = false;
|
|
14
|
+
let mockIsInfectedShouldFail = false;
|
|
15
|
+
let mockGetVersionShouldFail = false;
|
|
16
|
+
|
|
17
|
+
// Mock the clamscan module
|
|
18
|
+
vi.mock("clamscan", () => {
|
|
19
|
+
return {
|
|
20
|
+
default: class MockNodeClam {
|
|
21
|
+
async init() {
|
|
22
|
+
if (mockInitShouldFail) {
|
|
23
|
+
throw new Error("ClamAV not found");
|
|
24
|
+
}
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
async isInfected() {
|
|
28
|
+
if (mockIsInfectedShouldFail) {
|
|
29
|
+
throw new Error("Scan timeout");
|
|
30
|
+
}
|
|
31
|
+
return mockIsInfectedResult;
|
|
32
|
+
}
|
|
33
|
+
async getVersion() {
|
|
34
|
+
if (mockGetVersionShouldFail) {
|
|
35
|
+
throw new Error("Connection refused");
|
|
36
|
+
}
|
|
37
|
+
return mockVersionResult;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("ClamScan Virus Plugin", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
// Reset mock state before each test
|
|
46
|
+
mockIsInfectedResult = {
|
|
47
|
+
isInfected: false,
|
|
48
|
+
file: "/tmp/test-file",
|
|
49
|
+
viruses: [],
|
|
50
|
+
};
|
|
51
|
+
mockVersionResult = { version: "ClamAV 1.0.0" };
|
|
52
|
+
mockInitShouldFail = false;
|
|
53
|
+
mockIsInfectedShouldFail = false;
|
|
54
|
+
mockGetVersionShouldFail = false;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("virusScanPlugin factory", () => {
|
|
58
|
+
it("should create a plugin layer with default configuration", () => {
|
|
59
|
+
const layer = virusScanPlugin();
|
|
60
|
+
expect(layer).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should create a plugin layer with custom configuration", () => {
|
|
64
|
+
const config: VirusScanPluginConfig = {
|
|
65
|
+
preference: "clamscan",
|
|
66
|
+
clamdscan_socket: "/custom/socket/path",
|
|
67
|
+
clamdscan_host: "localhost",
|
|
68
|
+
clamdscan_port: 3311,
|
|
69
|
+
remove_infected: false,
|
|
70
|
+
debug_mode: true,
|
|
71
|
+
};
|
|
72
|
+
const layer = virusScanPlugin(config);
|
|
73
|
+
expect(layer).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("scan method", () => {
|
|
78
|
+
it("should return clean result for non-infected file", async () => {
|
|
79
|
+
mockIsInfectedResult = {
|
|
80
|
+
isInfected: false,
|
|
81
|
+
file: "/tmp/test-file",
|
|
82
|
+
viruses: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const program = Effect.gen(function* () {
|
|
86
|
+
const plugin = yield* VirusScanPlugin;
|
|
87
|
+
const fileData = new TextEncoder().encode("clean file content");
|
|
88
|
+
return yield* plugin.scan(fileData);
|
|
89
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
90
|
+
|
|
91
|
+
const result = await Effect.runPromise(program);
|
|
92
|
+
expect(result.isClean).toBe(true);
|
|
93
|
+
expect(result.detectedViruses).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should return infected result with virus names", async () => {
|
|
97
|
+
mockIsInfectedResult = {
|
|
98
|
+
isInfected: true,
|
|
99
|
+
file: "/tmp/test-file",
|
|
100
|
+
viruses: ["Eicar-Test-Signature", "Win.Trojan.Generic"],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const program = Effect.gen(function* () {
|
|
104
|
+
const plugin = yield* VirusScanPlugin;
|
|
105
|
+
const fileData = new TextEncoder().encode("infected file content");
|
|
106
|
+
return yield* plugin.scan(fileData);
|
|
107
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
108
|
+
|
|
109
|
+
const result = await Effect.runPromise(program);
|
|
110
|
+
expect(result.isClean).toBe(false);
|
|
111
|
+
expect(result.detectedViruses).toEqual(["Eicar-Test-Signature", "Win.Trojan.Generic"]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should handle null virus array for infected files", async () => {
|
|
115
|
+
mockIsInfectedResult = {
|
|
116
|
+
isInfected: true,
|
|
117
|
+
file: "/tmp/test-file",
|
|
118
|
+
viruses: null,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const program = Effect.gen(function* () {
|
|
122
|
+
const plugin = yield* VirusScanPlugin;
|
|
123
|
+
const fileData = new TextEncoder().encode("infected file");
|
|
124
|
+
return yield* plugin.scan(fileData);
|
|
125
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
126
|
+
|
|
127
|
+
const result = await Effect.runPromise(program);
|
|
128
|
+
expect(result.isClean).toBe(false);
|
|
129
|
+
expect(result.detectedViruses).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should handle binary data correctly", async () => {
|
|
133
|
+
mockIsInfectedResult = {
|
|
134
|
+
isInfected: false,
|
|
135
|
+
file: "/tmp/test-file",
|
|
136
|
+
viruses: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const program = Effect.gen(function* () {
|
|
140
|
+
const plugin = yield* VirusScanPlugin;
|
|
141
|
+
// Binary data that isn't valid UTF-8
|
|
142
|
+
const binaryData = new Uint8Array([0x00, 0xff, 0xfe, 0x89, 0x50, 0x4e, 0x47]);
|
|
143
|
+
return yield* plugin.scan(binaryData);
|
|
144
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
145
|
+
|
|
146
|
+
const result = await Effect.runPromise(program);
|
|
147
|
+
expect(result.isClean).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should handle large file data", async () => {
|
|
151
|
+
mockIsInfectedResult = {
|
|
152
|
+
isInfected: false,
|
|
153
|
+
file: "/tmp/test-file",
|
|
154
|
+
viruses: [],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const program = Effect.gen(function* () {
|
|
158
|
+
const plugin = yield* VirusScanPlugin;
|
|
159
|
+
// 1MB of data
|
|
160
|
+
const largeData = new Uint8Array(1024 * 1024).fill(0x41);
|
|
161
|
+
return yield* plugin.scan(largeData);
|
|
162
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
163
|
+
|
|
164
|
+
const result = await Effect.runPromise(program);
|
|
165
|
+
expect(result.isClean).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("getVersion method", () => {
|
|
170
|
+
it("should return ClamAV version string", async () => {
|
|
171
|
+
mockVersionResult = {
|
|
172
|
+
version: "ClamAV 1.0.0/26789/Tue Jan 14 09:00:00 2025",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const program = Effect.gen(function* () {
|
|
176
|
+
const plugin = yield* VirusScanPlugin;
|
|
177
|
+
return yield* plugin.getVersion();
|
|
178
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
179
|
+
|
|
180
|
+
const result = await Effect.runPromise(program);
|
|
181
|
+
expect(result).toBe("ClamAV 1.0.0/26789/Tue Jan 14 09:00:00 2025");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should return 'Unknown' when version is undefined", async () => {
|
|
185
|
+
mockVersionResult = {
|
|
186
|
+
version: undefined,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const program = Effect.gen(function* () {
|
|
190
|
+
const plugin = yield* VirusScanPlugin;
|
|
191
|
+
return yield* plugin.getVersion();
|
|
192
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
193
|
+
|
|
194
|
+
const result = await Effect.runPromise(program);
|
|
195
|
+
expect(result).toBe("Unknown");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should return 'Unknown' when version is empty", async () => {
|
|
199
|
+
mockVersionResult = {
|
|
200
|
+
version: "",
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const program = Effect.gen(function* () {
|
|
204
|
+
const plugin = yield* VirusScanPlugin;
|
|
205
|
+
return yield* plugin.getVersion();
|
|
206
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
207
|
+
|
|
208
|
+
const result = await Effect.runPromise(program);
|
|
209
|
+
expect(result).toBe("Unknown");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("error handling", () => {
|
|
214
|
+
it("should fail with CLAMAV_NOT_INSTALLED when initialization fails", async () => {
|
|
215
|
+
mockInitShouldFail = true;
|
|
216
|
+
|
|
217
|
+
const program = Effect.gen(function* () {
|
|
218
|
+
const plugin = yield* VirusScanPlugin;
|
|
219
|
+
const fileData = new TextEncoder().encode("test");
|
|
220
|
+
return yield* plugin.scan(fileData);
|
|
221
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
222
|
+
|
|
223
|
+
const result = await Effect.runPromise(Effect.either(program));
|
|
224
|
+
expect(result._tag).toBe("Left");
|
|
225
|
+
if (result._tag === "Left") {
|
|
226
|
+
expect(result.left.code).toBe("CLAMAV_NOT_INSTALLED");
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should fail with VIRUS_SCAN_FAILED when scan fails", async () => {
|
|
231
|
+
mockIsInfectedShouldFail = true;
|
|
232
|
+
|
|
233
|
+
const program = Effect.gen(function* () {
|
|
234
|
+
const plugin = yield* VirusScanPlugin;
|
|
235
|
+
const fileData = new TextEncoder().encode("test content");
|
|
236
|
+
return yield* plugin.scan(fileData);
|
|
237
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
238
|
+
|
|
239
|
+
const result = await Effect.runPromise(Effect.either(program));
|
|
240
|
+
expect(result._tag).toBe("Left");
|
|
241
|
+
if (result._tag === "Left") {
|
|
242
|
+
expect(result.left.code).toBe("VIRUS_SCAN_FAILED");
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should fail with VIRUS_SCAN_FAILED when getVersion fails", async () => {
|
|
247
|
+
mockGetVersionShouldFail = true;
|
|
248
|
+
|
|
249
|
+
const program = Effect.gen(function* () {
|
|
250
|
+
const plugin = yield* VirusScanPlugin;
|
|
251
|
+
return yield* plugin.getVersion();
|
|
252
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
253
|
+
|
|
254
|
+
const result = await Effect.runPromise(Effect.either(program));
|
|
255
|
+
expect(result._tag).toBe("Left");
|
|
256
|
+
if (result._tag === "Left") {
|
|
257
|
+
expect(result.left.code).toBe("VIRUS_SCAN_FAILED");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("configuration options", () => {
|
|
263
|
+
it("should accept clamdscan preference", () => {
|
|
264
|
+
const layer = virusScanPlugin({ preference: "clamdscan" });
|
|
265
|
+
expect(layer).toBeDefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should accept clamscan preference", () => {
|
|
269
|
+
const layer = virusScanPlugin({ preference: "clamscan" });
|
|
270
|
+
expect(layer).toBeDefined();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should accept socket configuration for clamd", () => {
|
|
274
|
+
const layer = virusScanPlugin({
|
|
275
|
+
preference: "clamdscan",
|
|
276
|
+
clamdscan_socket: "/var/run/clamav/clamd.ctl",
|
|
277
|
+
});
|
|
278
|
+
expect(layer).toBeDefined();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should accept TCP host and port for clamd", () => {
|
|
282
|
+
const layer = virusScanPlugin({
|
|
283
|
+
preference: "clamdscan",
|
|
284
|
+
clamdscan_host: "127.0.0.1",
|
|
285
|
+
clamdscan_port: 3310,
|
|
286
|
+
});
|
|
287
|
+
expect(layer).toBeDefined();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should accept debug mode configuration", () => {
|
|
291
|
+
const layer = virusScanPlugin({
|
|
292
|
+
debug_mode: true,
|
|
293
|
+
});
|
|
294
|
+
expect(layer).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should accept remove_infected configuration", () => {
|
|
298
|
+
const layer = virusScanPlugin({
|
|
299
|
+
remove_infected: false,
|
|
300
|
+
});
|
|
301
|
+
expect(layer).toBeDefined();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("EICAR test file detection", () => {
|
|
306
|
+
it("should detect EICAR test file as infected", async () => {
|
|
307
|
+
// EICAR is a standard antivirus test signature
|
|
308
|
+
const EICAR_SIGNATURE =
|
|
309
|
+
"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
|
310
|
+
|
|
311
|
+
mockIsInfectedResult = {
|
|
312
|
+
isInfected: true,
|
|
313
|
+
file: "/tmp/eicar-test",
|
|
314
|
+
viruses: ["Eicar-Test-Signature"],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const program = Effect.gen(function* () {
|
|
318
|
+
const plugin = yield* VirusScanPlugin;
|
|
319
|
+
const eicarData = new TextEncoder().encode(EICAR_SIGNATURE);
|
|
320
|
+
return yield* plugin.scan(eicarData);
|
|
321
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
322
|
+
|
|
323
|
+
const result = await Effect.runPromise(program);
|
|
324
|
+
expect(result.isClean).toBe(false);
|
|
325
|
+
expect(result.detectedViruses).toContain("Eicar-Test-Signature");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("temporary file handling", () => {
|
|
330
|
+
it("should clean up temporary files after successful scan", async () => {
|
|
331
|
+
mockIsInfectedResult = {
|
|
332
|
+
isInfected: false,
|
|
333
|
+
file: "/tmp/test-file",
|
|
334
|
+
viruses: [],
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const program = Effect.gen(function* () {
|
|
338
|
+
const plugin = yield* VirusScanPlugin;
|
|
339
|
+
const fileData = new TextEncoder().encode("test content");
|
|
340
|
+
return yield* plugin.scan(fileData);
|
|
341
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
342
|
+
|
|
343
|
+
// The test verifies the scan completes - cleanup is handled internally
|
|
344
|
+
const result = await Effect.runPromise(program);
|
|
345
|
+
expect(result).toBeDefined();
|
|
346
|
+
expect(result.isClean).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should clean up temporary files even when scan fails", async () => {
|
|
350
|
+
mockIsInfectedShouldFail = true;
|
|
351
|
+
|
|
352
|
+
const program = Effect.gen(function* () {
|
|
353
|
+
const plugin = yield* VirusScanPlugin;
|
|
354
|
+
const fileData = new TextEncoder().encode("test content");
|
|
355
|
+
return yield* plugin.scan(fileData);
|
|
356
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
357
|
+
|
|
358
|
+
// Scan should fail but temp file cleanup should still happen
|
|
359
|
+
const result = await Effect.runPromise(Effect.either(program));
|
|
360
|
+
expect(result._tag).toBe("Left");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("lazy initialization", () => {
|
|
365
|
+
it("should initialize scanner lazily on first use", async () => {
|
|
366
|
+
mockIsInfectedResult = {
|
|
367
|
+
isInfected: false,
|
|
368
|
+
file: "/tmp/test",
|
|
369
|
+
viruses: [],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const layer = virusScanPlugin();
|
|
373
|
+
|
|
374
|
+
// First scan initializes the scanner
|
|
375
|
+
const program1 = Effect.gen(function* () {
|
|
376
|
+
const plugin = yield* VirusScanPlugin;
|
|
377
|
+
return yield* plugin.scan(new TextEncoder().encode("test1"));
|
|
378
|
+
}).pipe(Effect.provide(layer));
|
|
379
|
+
|
|
380
|
+
await Effect.runPromise(program1);
|
|
381
|
+
|
|
382
|
+
// Second scan reuses the initialized scanner (same layer instance)
|
|
383
|
+
const program2 = Effect.gen(function* () {
|
|
384
|
+
const plugin = yield* VirusScanPlugin;
|
|
385
|
+
return yield* plugin.scan(new TextEncoder().encode("test2"));
|
|
386
|
+
}).pipe(Effect.provide(layer));
|
|
387
|
+
|
|
388
|
+
const result = await Effect.runPromise(program2);
|
|
389
|
+
expect(result.isClean).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("multiple virus detection", () => {
|
|
394
|
+
it("should report multiple viruses when detected", async () => {
|
|
395
|
+
mockIsInfectedResult = {
|
|
396
|
+
isInfected: true,
|
|
397
|
+
file: "/tmp/multi-virus",
|
|
398
|
+
viruses: ["Trojan.Generic", "Worm.Mydoom", "Virus.Sality"],
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const program = Effect.gen(function* () {
|
|
402
|
+
const plugin = yield* VirusScanPlugin;
|
|
403
|
+
const fileData = new TextEncoder().encode("malicious content");
|
|
404
|
+
return yield* plugin.scan(fileData);
|
|
405
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
406
|
+
|
|
407
|
+
const result = await Effect.runPromise(program);
|
|
408
|
+
expect(result.isClean).toBe(false);
|
|
409
|
+
expect(result.detectedViruses).toHaveLength(3);
|
|
410
|
+
expect(result.detectedViruses).toContain("Trojan.Generic");
|
|
411
|
+
expect(result.detectedViruses).toContain("Worm.Mydoom");
|
|
412
|
+
expect(result.detectedViruses).toContain("Virus.Sality");
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("empty file handling", () => {
|
|
417
|
+
it("should handle empty file input", async () => {
|
|
418
|
+
mockIsInfectedResult = {
|
|
419
|
+
isInfected: false,
|
|
420
|
+
file: "/tmp/empty-file",
|
|
421
|
+
viruses: [],
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const program = Effect.gen(function* () {
|
|
425
|
+
const plugin = yield* VirusScanPlugin;
|
|
426
|
+
const emptyData = new Uint8Array(0);
|
|
427
|
+
return yield* plugin.scan(emptyData);
|
|
428
|
+
}).pipe(Effect.provide(virusScanPlugin()));
|
|
429
|
+
|
|
430
|
+
const result = await Effect.runPromise(program);
|
|
431
|
+
expect(result.isClean).toBe(true);
|
|
432
|
+
expect(result.detectedViruses).toEqual([]);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|