@uploadista/flow-security-clamscan 0.0.18-beta.9 → 0.0.19
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 +9 -9
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/clamscan-plugin.ts +51 -56
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @uploadista/flow-security-clamscan@0.0.18
|
|
3
|
+
> @uploadista/flow-security-clamscan@0.0.18 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
|
|
4
4
|
> tsdown
|
|
5
5
|
|
|
6
6
|
[34mℹ[39m tsdown [2mv0.16.8[22m powered by rolldown [2mv1.0.0-beta.52[22m
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
10
10
|
[34mℹ[39m Build start
|
|
11
11
|
[34mℹ[39m Cleaning 7 files
|
|
12
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.cjs[22m [2m2.
|
|
13
|
-
[34mℹ[39m [33m[CJS][39m 1 files, total: 2.
|
|
12
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.cjs[22m [2m2.89 kB[22m [2m│ gzip: 1.22 kB[22m
|
|
13
|
+
[34mℹ[39m [33m[CJS][39m 1 files, total: 2.89 kB
|
|
14
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m2.27 kB[22m [2m│ gzip: 0.98 kB[22m
|
|
15
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m9.40 kB[22m [2m│ gzip: 2.93 kB[22m
|
|
16
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m0.25 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
17
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m1.65 kB[22m [2m│ gzip: 0.73 kB[22m
|
|
18
|
+
[34mℹ[39m [34m[ESM][39m 4 files, total: 13.57 kB
|
|
14
19
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [2m0.25 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
15
20
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [2m1.65 kB[22m [2m│ gzip: 0.73 kB[22m
|
|
16
21
|
[34mℹ[39m [33m[CJS][39m 2 files, total: 1.90 kB
|
|
17
|
-
[
|
|
18
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m9.32 kB[22m [2m│ gzip: 2.88 kB[22m
|
|
19
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m0.25 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
20
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m1.65 kB[22m [2m│ gzip: 0.73 kB[22m
|
|
21
|
-
[34mℹ[39m [34m[ESM][39m 4 files, total: 13.46 kB
|
|
22
|
-
[32m✔[39m Build complete in [32m6223ms[39m
|
|
22
|
+
[32m✔[39m Build complete in [32m4330ms[39m
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:crypto`),l=require(`node:fs/promises`);l=s(l);let u=require(`node:os`);u=s(u);let d=require(`node:path`);d=s(d);let f=require(`@uploadista/core/errors`),p=require(`@uploadista/core/flow`),m=require(`clamscan`);
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:crypto`),l=require(`node:fs/promises`);l=s(l);let u=require(`node:os`);u=s(u);let d=require(`node:path`);d=s(d);let f=require(`@uploadista/core/errors`),p=require(`@uploadista/core/flow`),m=require(`@uploadista/observability`),h=require(`clamscan`);h=s(h);let g=require(`effect`);var _=class{clamscan=null;constructor(e={}){this.config=e}initScanner(){return g.Effect.gen(function*(){if(this.clamscan)return this.clamscan;let e=yield*g.Effect.tryPromise({try:async()=>await new h.default().init({preference:this.config.preference??`clamdscan`,remove_infected:this.config.remove_infected??!1,debug_mode:this.config.debug_mode??!1,clamdscan:{socket:this.config.clamdscan_socket,host:this.config.clamdscan_host,port:this.config.clamdscan_port??3310,timeout:6e4,local_fallback:!0},clamscan:{path:`/usr/bin/clamscan`,scan_archives:!0,active:!0}}),catch:e=>f.UploadistaError.fromCode(`CLAMAV_NOT_INSTALLED`,{body:`ClamAV initialization failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})});return this.clamscan=e,e}.bind(this)).pipe((0,m.withOperationSpan)(`virus-scan`,`init`,{"scan.preference":this.config.preference??`clamdscan`}))}scan(e){return g.Effect.gen(function*(){let t=yield*this.initScanner(),n=u.tmpdir(),r=`uploadista-scan-${(0,c.randomUUID)()}`,i=d.join(n,r);return yield*g.Effect.tryPromise({try:()=>l.writeFile(i,e),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to create temporary file for scanning`,details:{error:e}})}),yield*g.Effect.tryPromise({try:()=>t.isInfected(i),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Virus scan failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})}).pipe(g.Effect.map(e=>({isClean:!e.isInfected,detectedViruses:e.viruses||[]})),g.Effect.ensuring(g.Effect.tryPromise({try:()=>l.unlink(i),catch:()=>void 0}).pipe(g.Effect.ignore)))}.bind(this)).pipe((0,m.withOperationSpan)(`virus-scan`,`scan`,{"scan.file_size":e.byteLength}))}getVersion(){return g.Effect.gen(function*(){let e=yield*this.initScanner();return(yield*g.Effect.tryPromise({try:()=>e.getVersion(),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to get ClamAV version`,details:{error:e}})})).version||`Unknown`}.bind(this)).pipe((0,m.withOperationSpan)(`virus-scan`,`get-version`,{}))}};function v(e={}){return g.Layer.succeed(p.VirusScanPlugin,new _(e))}exports.ClamScanPluginLayer=v;
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqNgB,UArNC,cAAA,CAqNkB;EACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,mBAAA,UACN,iBACP,KAAA,CAAM,MAAM"}
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqNgB,UArNC,cAAA,CAqNkB;EACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,mBAAA,UACN,iBACP,KAAA,CAAM,MAAM"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import*as t from"node:fs/promises";import*as n from"node:os";import*as r from"node:path";import{UploadistaError as i}from"@uploadista/core/errors";import{VirusScanPlugin as a}from"@uploadista/core/flow";import o from"clamscan";import{Effect as
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import*as t from"node:fs/promises";import*as n from"node:os";import*as r from"node:path";import{UploadistaError as i}from"@uploadista/core/errors";import{VirusScanPlugin as a}from"@uploadista/core/flow";import{withOperationSpan as o}from"@uploadista/observability";import s from"clamscan";import{Effect as c,Layer as l}from"effect";var u=class{clamscan=null;constructor(e={}){this.config=e}initScanner(){return c.gen(function*(){if(this.clamscan)return this.clamscan;let e=yield*c.tryPromise({try:async()=>await new s().init({preference:this.config.preference??`clamdscan`,remove_infected:this.config.remove_infected??!1,debug_mode:this.config.debug_mode??!1,clamdscan:{socket:this.config.clamdscan_socket,host:this.config.clamdscan_host,port:this.config.clamdscan_port??3310,timeout:6e4,local_fallback:!0},clamscan:{path:`/usr/bin/clamscan`,scan_archives:!0,active:!0}}),catch:e=>i.fromCode(`CLAMAV_NOT_INSTALLED`,{body:`ClamAV initialization failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})});return this.clamscan=e,e}.bind(this)).pipe(o(`virus-scan`,`init`,{"scan.preference":this.config.preference??`clamdscan`}))}scan(a){return c.gen(function*(){let o=yield*this.initScanner(),s=n.tmpdir(),l=`uploadista-scan-${e()}`,u=r.join(s,l);return yield*c.tryPromise({try:()=>t.writeFile(u,a),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to create temporary file for scanning`,details:{error:e}})}),yield*c.tryPromise({try:()=>o.isInfected(u),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Virus scan failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})}).pipe(c.map(e=>({isClean:!e.isInfected,detectedViruses:e.viruses||[]})),c.ensuring(c.tryPromise({try:()=>t.unlink(u),catch:()=>void 0}).pipe(c.ignore)))}.bind(this)).pipe(o(`virus-scan`,`scan`,{"scan.file_size":a.byteLength}))}getVersion(){return c.gen(function*(){let e=yield*this.initScanner();return(yield*c.tryPromise({try:()=>e.getVersion(),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to get ClamAV version`,details:{error:e}})})).version||`Unknown`}.bind(this)).pipe(o(`virus-scan`,`get-version`,{}))}};function d(e={}){return l.succeed(a,new u(e))}export{d as ClamScanPluginLayer};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["config: ClamScanConfig"],"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 NodeClam from \"clamscan\";\nimport { Effect, Layer } from \"effect\";\n\n/**\n * Configuration options for the ClamAV plugin\n */\nexport interface ClamScanConfig {\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: ClamScanConfig = {}) {}\n\n /**\n * Initialize the ClamAV scanner\n * This is called lazily on first use\n */\n private
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["config: ClamScanConfig"],"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 plugin\n */\nexport interface ClamScanConfig {\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: ClamScanConfig = {}) {}\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 VirusScanPlugin 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 ClamScanPluginLayer(\n config: ClamScanConfig = {},\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,EAAiC,EAAE,CAAE,CAA7B,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,EAAyB,EAAE,CACiB,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.0.19",
|
|
5
5
|
"description": "ClamAV virus scanning plugin for Uploadista Flow",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"clamscan": "^2.3.3",
|
|
18
|
-
"@uploadista/core": "0.0.
|
|
18
|
+
"@uploadista/core": "0.0.19",
|
|
19
|
+
"@uploadista/observability": "0.0.19"
|
|
19
20
|
},
|
|
20
21
|
"peerDependencies": {
|
|
21
22
|
"effect": "^3.0.0",
|
|
@@ -26,9 +27,9 @@
|
|
|
26
27
|
"@types/node": "24.10.1",
|
|
27
28
|
"effect": "3.19.8",
|
|
28
29
|
"tsdown": "0.16.8",
|
|
29
|
-
"vitest": "4.0.
|
|
30
|
+
"vitest": "4.0.15",
|
|
30
31
|
"zod": "4.1.13",
|
|
31
|
-
"@uploadista/typescript-config": "0.0.
|
|
32
|
+
"@uploadista/typescript-config": "0.0.19"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsdown",
|
package/src/clamscan-plugin.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import { UploadistaError } from "@uploadista/core/errors";
|
|
6
6
|
import type { ScanResult, VirusScanPluginShape } from "@uploadista/core/flow";
|
|
7
7
|
import { VirusScanPlugin } from "@uploadista/core/flow";
|
|
8
|
+
import { withOperationSpan } from "@uploadista/observability";
|
|
8
9
|
import NodeClam from "clamscan";
|
|
9
10
|
import { Effect, Layer } from "effect";
|
|
10
11
|
|
|
@@ -76,39 +77,49 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
|
|
|
76
77
|
* Initialize the ClamAV scanner
|
|
77
78
|
* This is called lazily on first use
|
|
78
79
|
*/
|
|
79
|
-
private
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
80
|
+
private initScanner(): Effect.Effect<NodeClam, UploadistaError> {
|
|
81
|
+
return Effect.gen(
|
|
82
|
+
function* (this: ClamScanPluginImpl) {
|
|
83
|
+
if (this.clamscan) {
|
|
84
|
+
return this.clamscan;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const scanner = yield* Effect.tryPromise({
|
|
88
|
+
try: async () => {
|
|
89
|
+
// Initialize clamscan with configuration
|
|
90
|
+
return await new NodeClam().init({
|
|
91
|
+
preference: this.config.preference ?? "clamdscan",
|
|
92
|
+
remove_infected: this.config.remove_infected ?? false,
|
|
93
|
+
debug_mode: this.config.debug_mode ?? false,
|
|
94
|
+
clamdscan: {
|
|
95
|
+
socket: this.config.clamdscan_socket,
|
|
96
|
+
host: this.config.clamdscan_host,
|
|
97
|
+
port: this.config.clamdscan_port ?? 3310,
|
|
98
|
+
timeout: 60000,
|
|
99
|
+
local_fallback: true, // Fall back to binary if daemon unavailable
|
|
100
|
+
},
|
|
101
|
+
clamscan: {
|
|
102
|
+
path: "/usr/bin/clamscan", // Standard path
|
|
103
|
+
scan_archives: true,
|
|
104
|
+
active: true,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
catch: (error) =>
|
|
109
|
+
UploadistaError.fromCode("CLAMAV_NOT_INSTALLED", {
|
|
110
|
+
body: `ClamAV initialization failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
111
|
+
details: { error },
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.clamscan = scanner;
|
|
116
|
+
return scanner;
|
|
117
|
+
}.bind(this),
|
|
118
|
+
).pipe(
|
|
119
|
+
withOperationSpan("virus-scan", "init", {
|
|
120
|
+
"scan.preference": this.config.preference ?? "clamdscan",
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
/**
|
|
@@ -121,17 +132,7 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
|
|
|
121
132
|
return Effect.gen(
|
|
122
133
|
function* (this: ClamScanPluginImpl) {
|
|
123
134
|
// Initialize scanner (lazy initialization)
|
|
124
|
-
const scanner = yield*
|
|
125
|
-
try: () => this.initScanner(),
|
|
126
|
-
catch: (error) =>
|
|
127
|
-
UploadistaError.fromCode("CLAMAV_NOT_INSTALLED", {
|
|
128
|
-
body:
|
|
129
|
-
error instanceof Error
|
|
130
|
-
? error.message
|
|
131
|
-
: "ClamAV is not installed or not available",
|
|
132
|
-
details: { error },
|
|
133
|
-
}),
|
|
134
|
-
});
|
|
135
|
+
const scanner = yield* this.initScanner();
|
|
135
136
|
|
|
136
137
|
// Create temporary file path for scanning
|
|
137
138
|
const tmpDir = os.tmpdir();
|
|
@@ -172,6 +173,10 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
|
|
|
172
173
|
|
|
173
174
|
return result;
|
|
174
175
|
}.bind(this),
|
|
176
|
+
).pipe(
|
|
177
|
+
withOperationSpan("virus-scan", "scan", {
|
|
178
|
+
"scan.file_size": input.byteLength,
|
|
179
|
+
}),
|
|
175
180
|
);
|
|
176
181
|
}
|
|
177
182
|
|
|
@@ -184,17 +189,7 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
|
|
|
184
189
|
return Effect.gen(
|
|
185
190
|
function* (this: ClamScanPluginImpl) {
|
|
186
191
|
// Initialize scanner (lazy initialization)
|
|
187
|
-
const scanner = yield*
|
|
188
|
-
try: () => this.initScanner(),
|
|
189
|
-
catch: (error) =>
|
|
190
|
-
UploadistaError.fromCode("CLAMAV_NOT_INSTALLED", {
|
|
191
|
-
body:
|
|
192
|
-
error instanceof Error
|
|
193
|
-
? error.message
|
|
194
|
-
: "ClamAV is not installed or not available",
|
|
195
|
-
details: { error },
|
|
196
|
-
}),
|
|
197
|
-
});
|
|
192
|
+
const scanner = yield* this.initScanner();
|
|
198
193
|
|
|
199
194
|
// Get version from ClamAV
|
|
200
195
|
const versionResult = yield* Effect.tryPromise({
|
|
@@ -208,7 +203,7 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
|
|
|
208
203
|
|
|
209
204
|
return versionResult.version || "Unknown";
|
|
210
205
|
}.bind(this),
|
|
211
|
-
);
|
|
206
|
+
).pipe(withOperationSpan("virus-scan", "get-version", {}));
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
209
|
|