pompelmi 0.35.5 → 1.1.0

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.
Files changed (135) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/LICENSE +12 -18
  3. package/README.md +174 -181
  4. package/eslint.config.mjs +8 -0
  5. package/package.json +26 -251
  6. package/src/ClamAVDatabaseUpdater.js +48 -0
  7. package/src/ClamAVInstaller.js +49 -0
  8. package/src/ClamAVScanner.js +37 -0
  9. package/src/ClamdScanner.js +81 -0
  10. package/src/InstallerCommand.js +11 -0
  11. package/src/config.js +22 -0
  12. package/src/constants.js +3 -0
  13. package/src/favicon.ico +0 -0
  14. package/src/grapefruit.png +0 -0
  15. package/src/index.js +5 -0
  16. package/test_out.txt +74 -0
  17. package/CHANGELOG.md +0 -71
  18. package/dist/pompelmi.audit.cjs +0 -128
  19. package/dist/pompelmi.audit.cjs.map +0 -1
  20. package/dist/pompelmi.audit.esm.js +0 -107
  21. package/dist/pompelmi.audit.esm.js.map +0 -1
  22. package/dist/pompelmi.browser.cjs +0 -1549
  23. package/dist/pompelmi.browser.cjs.map +0 -1
  24. package/dist/pompelmi.browser.esm.js +0 -1523
  25. package/dist/pompelmi.browser.esm.js.map +0 -1
  26. package/dist/pompelmi.cjs +0 -2591
  27. package/dist/pompelmi.cjs.map +0 -1
  28. package/dist/pompelmi.esm.js +0 -2525
  29. package/dist/pompelmi.esm.js.map +0 -1
  30. package/dist/pompelmi.hooks.cjs +0 -75
  31. package/dist/pompelmi.hooks.cjs.map +0 -1
  32. package/dist/pompelmi.hooks.esm.js +0 -72
  33. package/dist/pompelmi.hooks.esm.js.map +0 -1
  34. package/dist/pompelmi.policy-packs.cjs +0 -240
  35. package/dist/pompelmi.policy-packs.cjs.map +0 -1
  36. package/dist/pompelmi.policy-packs.esm.js +0 -232
  37. package/dist/pompelmi.policy-packs.esm.js.map +0 -1
  38. package/dist/pompelmi.quarantine.cjs +0 -317
  39. package/dist/pompelmi.quarantine.cjs.map +0 -1
  40. package/dist/pompelmi.quarantine.esm.js +0 -293
  41. package/dist/pompelmi.quarantine.esm.js.map +0 -1
  42. package/dist/pompelmi.react.cjs +0 -1580
  43. package/dist/pompelmi.react.cjs.map +0 -1
  44. package/dist/pompelmi.react.esm.js +0 -1553
  45. package/dist/pompelmi.react.esm.js.map +0 -1
  46. package/dist/types/audit.d.ts +0 -84
  47. package/dist/types/browser-index.d.ts +0 -29
  48. package/dist/types/config.d.ts +0 -143
  49. package/dist/types/engines/dynamic-taint.d.ts +0 -102
  50. package/dist/types/engines/hybrid-orchestrator.d.ts +0 -65
  51. package/dist/types/engines/hybrid-taint-integration.d.ts +0 -129
  52. package/dist/types/engines/taint-policies.d.ts +0 -84
  53. package/dist/types/hipaa-compliance.d.ts +0 -110
  54. package/dist/types/hooks.d.ts +0 -89
  55. package/dist/types/index.d.ts +0 -29
  56. package/dist/types/magic.d.ts +0 -7
  57. package/dist/types/node/scanDir.d.ts +0 -30
  58. package/dist/types/policy-packs.d.ts +0 -98
  59. package/dist/types/policy.d.ts +0 -12
  60. package/dist/types/presets.d.ts +0 -72
  61. package/dist/types/quarantine/index.d.ts +0 -18
  62. package/dist/types/quarantine/storage.d.ts +0 -77
  63. package/dist/types/quarantine/types.d.ts +0 -78
  64. package/dist/types/quarantine/workflow.d.ts +0 -97
  65. package/dist/types/react-index.d.ts +0 -13
  66. package/dist/types/risk.d.ts +0 -18
  67. package/dist/types/scan/remote.d.ts +0 -12
  68. package/dist/types/scan.d.ts +0 -17
  69. package/dist/types/scanners/common-heuristics.d.ts +0 -14
  70. package/dist/types/scanners/zip-bomb-guard.d.ts +0 -9
  71. package/dist/types/scanners/zipTraversalGuard.d.ts +0 -19
  72. package/dist/types/src/audit.d.ts +0 -84
  73. package/dist/types/src/browser-index.d.ts +0 -29
  74. package/dist/types/src/config.d.ts +0 -143
  75. package/dist/types/src/engines/dynamic-taint.d.ts +0 -102
  76. package/dist/types/src/engines/hybrid-orchestrator.d.ts +0 -65
  77. package/dist/types/src/engines/hybrid-taint-integration.d.ts +0 -129
  78. package/dist/types/src/engines/taint-policies.d.ts +0 -84
  79. package/dist/types/src/hipaa-compliance.d.ts +0 -110
  80. package/dist/types/src/hooks.d.ts +0 -89
  81. package/dist/types/src/index.d.ts +0 -29
  82. package/dist/types/src/magic.d.ts +0 -7
  83. package/dist/types/src/node/scanDir.d.ts +0 -30
  84. package/dist/types/src/policy-packs.d.ts +0 -98
  85. package/dist/types/src/policy.d.ts +0 -12
  86. package/dist/types/src/presets.d.ts +0 -72
  87. package/dist/types/src/quarantine/index.d.ts +0 -18
  88. package/dist/types/src/quarantine/storage.d.ts +0 -77
  89. package/dist/types/src/quarantine/types.d.ts +0 -78
  90. package/dist/types/src/quarantine/workflow.d.ts +0 -97
  91. package/dist/types/src/react-index.d.ts +0 -13
  92. package/dist/types/src/risk.d.ts +0 -18
  93. package/dist/types/src/scan/remote.d.ts +0 -12
  94. package/dist/types/src/scan.d.ts +0 -17
  95. package/dist/types/src/scanners/common-heuristics.d.ts +0 -14
  96. package/dist/types/src/scanners/zip-bomb-guard.d.ts +0 -11
  97. package/dist/types/src/scanners/zipTraversalGuard.d.ts +0 -19
  98. package/dist/types/src/stream.d.ts +0 -10
  99. package/dist/types/src/types/decompilation.d.ts +0 -96
  100. package/dist/types/src/types/taint-tracking.d.ts +0 -495
  101. package/dist/types/src/types.d.ts +0 -48
  102. package/dist/types/src/useFileScanner.d.ts +0 -15
  103. package/dist/types/src/utils/advanced-detection.d.ts +0 -21
  104. package/dist/types/src/utils/batch-scanner.d.ts +0 -62
  105. package/dist/types/src/utils/cache-manager.d.ts +0 -95
  106. package/dist/types/src/utils/export.d.ts +0 -51
  107. package/dist/types/src/utils/performance-metrics.d.ts +0 -68
  108. package/dist/types/src/utils/threat-intelligence.d.ts +0 -96
  109. package/dist/types/src/validate.d.ts +0 -7
  110. package/dist/types/src/verdict.d.ts +0 -2
  111. package/dist/types/src/yara/browser.d.ts +0 -7
  112. package/dist/types/src/yara/index.d.ts +0 -17
  113. package/dist/types/src/yara/node.d.ts +0 -2
  114. package/dist/types/src/yara/remote.d.ts +0 -10
  115. package/dist/types/src/yara-bridge.d.ts +0 -3
  116. package/dist/types/src/zip.d.ts +0 -13
  117. package/dist/types/stream.d.ts +0 -10
  118. package/dist/types/types/decompilation.d.ts +0 -96
  119. package/dist/types/types/taint-tracking.d.ts +0 -495
  120. package/dist/types/types.d.ts +0 -48
  121. package/dist/types/useFileScanner.d.ts +0 -15
  122. package/dist/types/utils/advanced-detection.d.ts +0 -21
  123. package/dist/types/utils/batch-scanner.d.ts +0 -62
  124. package/dist/types/utils/cache-manager.d.ts +0 -95
  125. package/dist/types/utils/export.d.ts +0 -51
  126. package/dist/types/utils/performance-metrics.d.ts +0 -68
  127. package/dist/types/utils/threat-intelligence.d.ts +0 -96
  128. package/dist/types/validate.d.ts +0 -7
  129. package/dist/types/verdict.d.ts +0 -2
  130. package/dist/types/yara/browser.d.ts +0 -7
  131. package/dist/types/yara/index.d.ts +0 -17
  132. package/dist/types/yara/node.d.ts +0 -2
  133. package/dist/types/yara/remote.d.ts +0 -10
  134. package/dist/types/yara-bridge.d.ts +0 -3
  135. package/dist/types/zip.d.ts +0 -13
package/package.json CHANGED
@@ -1,263 +1,38 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "0.35.5",
4
- "description": "Inspect untrusted uploads before storage in Node.js. Open-source upload security with checks for spoofing, archive abuse, risky document and binary signals, and optional YARA.",
5
- "main": "./dist/pompelmi.cjs",
6
- "module": "./dist/pompelmi.esm.js",
7
- "type": "module",
8
- "browser": {
9
- "yara": false,
10
- "util": false,
11
- "crypto": false,
12
- "os": false,
13
- "path": false,
14
- "unzipper": false,
15
- "child_process": false
16
- },
3
+ "version": "1.1.0",
4
+ "description": "ClamAV for humans scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.",
5
+ "license": "ISC",
6
+ "author": "pompelmi contributors",
7
+ "homepage": "https://pompelmi.app",
17
8
  "repository": {
18
9
  "type": "git",
19
10
  "url": "https://github.com/pompelmi/pompelmi.git"
20
11
  },
21
- "homepage": "https://pompelmi.github.io/pompelmi/",
22
- "funding": {
23
- "type": "github",
24
- "url": "https://github.com/sponsors/pompelmi"
25
- },
26
- "pnpm": {
27
- "overrides": {
28
- "process": "0.11.10",
29
- "regjsgen": "0.8.0",
30
- "fflate": "0.8.2",
31
- "@tokenizer/inflate>fflate": "0.8.2",
32
- "file-type>fflate": "0.8.2",
33
- "regexpu-core>regjsgen": "0.8.0",
34
- "@babel/helper-create-regexp-features-plugin>regjsgen": "0.8.0",
35
- "vitest": "2.1.9",
36
- "@vitest/coverage-v8": "2.1.9",
37
- "babel-plugin-polyfill-corejs3": "^0.13.0",
38
- "@types/cookies": "0.9.1",
39
- "@types/koa>@types/cookies": "0.9.1",
40
- "pompelmi": "workspace:*",
41
- "@pompelmi/core": "workspace:*",
42
- "katex": "0.16.21",
43
- "react": "^19.2.0",
44
- "react-dom": "^19.2.0",
45
- "@types/react": "^19.2.0",
46
- "@types/react-dom": "^19.2.0",
47
- "esbuild@<=0.24.2": ">=0.25.0",
48
- "devalue@<5.3.2": ">=5.3.2",
49
- "vite@>=6.0.0 <=6.3.5": ">=6.3.6",
50
- "katex@>=0.12.0 <=0.16.20": ">=0.16.21",
51
- "astro@<5.14.3": ">=5.14.3",
52
- "vite@>=6.0.0 <=6.4.0": ">=6.4.1",
53
- "astro@>=2.16.0 <5.15.5": ">=5.15.5",
54
- "js-yaml@<3.14.2": ">=3.14.2",
55
- "js-yaml@>=4.0.0 <4.1.1": ">=4.1.1",
56
- "glob@>=10.2.0 <10.5.0": ">=10.5.0",
57
- "astro@<=5.15.6": ">=5.15.8",
58
- "body-parser@>=2.2.0 <2.2.1": ">=2.2.1",
59
- "astro@<5.15.9": ">=5.15.9",
60
- "astro@<5.15.8": ">=5.15.8",
61
- "astro@>=5.2.0 <5.15.6": ">=5.15.6",
62
- "mdast-util-to-hast@>=13.0.0 <13.2.1": ">=13.2.1",
63
- "next@>=16.0.0-canary.0 <16.0.7": ">=16.0.7",
64
- "next@>=16.0.0-beta.0 <16.0.9": ">=16.0.9",
65
- "qs@<6.14.2": ">=6.14.2",
66
- "multer@<2.0.2": ">=2.0.2",
67
- "@isaacs/brace-expansion@<=5.0.0": ">=5.0.1",
68
- "ajv@<8.18.0": ">=8.18.0",
69
- "fastify@<5.7.3": ">=5.7.3",
70
- "next@>=16.0.9 <16.1.5": ">=16.1.5",
71
- "preact@>=10.28.0 <10.28.2": ">=10.28.2",
72
- "devalue@>=5.1.0 <5.6.2": ">=5.6.2",
73
- "h3@<=1.15.4": ">=1.15.5",
74
- "koa@>=2.16.2 <2.16.3": ">=2.16.3",
75
- "lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23",
76
- "lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
77
- "diff@>=5.0.0 <5.2.2": ">=5.2.2"
78
- }
79
- },
80
- "scripts": {
81
- "build": "rollup -c",
82
- "test": "vitest run --passWithNoTests",
83
- "test:coverage": "vitest run --coverage --passWithNoTests",
84
- "test:coverage:ci": "vitest run --coverage --reporter=verbose --passWithNoTests",
85
- "prepublishOnly": "npm run build && npm run pack:strict",
86
- "yara:node:smoke": "tsx scripts/yara-node-smoke.ts",
87
- "yara:int:smoke": "tsx scripts/yara-integration-smoke.ts",
88
- "dev:remote": "tsx examples/remote-yara-server.ts",
89
- "docs:build": "hugo -s docs -D -d docs",
90
- "predocs:deploy": "npm run docs:build",
91
- "docs:deploy": "gh-pages -d docs -b gh-pages",
92
- "format": "biome format --write .",
93
- "format:check": "biome format .",
94
- "lint": "biome ci .",
95
- "lint:fix": "biome check --write .",
96
- "yara:check": "node scripts/yara-quick-check-cli.mjs",
97
- "build:core": "pnpm -r --filter '!./examples/*' --if-present build",
98
- "preview": "npm pack --dry-run",
99
- "typecheck": "tsc -p tsconfig.json --noEmit || tsc -p tsconfig.build.json --noEmit",
100
- "typecheck:strict": "tsc -p tsconfig.strict.json --noEmit",
101
- "smoke": "node scripts/smoke.mjs",
102
- "test:e2e": "node scripts/e2e.mjs",
103
- "repo:doctor": "pnpm install --frozen-lockfile && pnpm -r --if-present build && pnpm -r --if-present test && npm run -s preview || true && node scripts/smoke.mjs && node scripts/e2e.mjs || true",
104
- "audit:deps": "depcheck --skip-missing true || true",
105
- "audit:code": "knip --reporter compact || true",
106
- "audit:exports": "ts-prune -p tsconfig.json || true",
107
- "repo:audit": "node scripts/audit.mjs",
108
- "pack:check": "node scripts/pack-check.mjs",
109
- "pack:list": "pnpm -r --filter \"@pompelmi/*\" --if-present pack --json --dry-run",
110
- "pack:strict": "node scripts/pack-check.mjs --strict",
111
- "clean": "rimraf dist",
112
- "mentions:find": "node scripts/find-mentions.mjs",
113
- "mentions:render": "node scripts/render-mentions-readme.mjs",
114
- "mentions:inject": "node scripts/inject-mentions-readme.mjs",
115
- "mentions:update": "npm run mentions:find && npm run mentions:render && npm run mentions:inject"
116
- },
117
- "license": "MIT",
118
- "devDependencies": {
119
- "@biomejs/biome": "^2.2.4",
120
- "@pompelmi/core": "workspace:*",
121
- "@pompelmi/engine": "workspace:*",
122
- "@pompelmi/engine-heuristics": "workspace:^0.2.0",
123
- "@rollup/plugin-commonjs": "^29.0.2",
124
- "@rollup/plugin-node-resolve": "^16.0.1",
125
- "@rollup/plugin-typescript": "^12.1.4",
126
- "@types/cors": "^2.8.19",
127
- "@types/express": "^5.0.3",
128
- "@types/multer": "^2.0.0",
129
- "@types/node": "^25.5.0",
130
- "@types/react": "^19.1.8",
131
- "@types/unzipper": "^0.10.11",
132
- "@vitest/coverage-v8": "^4",
133
- "cors": "^2.8.5",
134
- "depcheck": "^1.4.7",
135
- "express": "^5.1.0",
136
- "gh-pages": "^6.3.0",
137
- "knip": "^6.1.1",
138
- "multer": "^2.0.2",
139
- "react": "^19.2.0",
140
- "rollup": "^4.x",
141
- "ts-prune": "^0.10.3",
142
- "tslib": "^2.8.1",
143
- "tsup": "^8",
144
- "tsx": "^4.20.3",
145
- "typescript": "^6.0.2",
146
- "vitest": "4.1.2"
147
- },
148
- "peerDependencies": {
149
- "react": "^18.0.0 || ^19.0.0",
150
- "react-dom": "^18.0.0 || ^19.0.0"
151
- },
152
- "peerDependenciesMeta": {
153
- "react": {
154
- "optional": true
155
- },
156
- "react-dom": {
157
- "optional": true
158
- }
12
+ "bugs": {
13
+ "url": "https://github.com/pompelmi/pompelmi/issues"
159
14
  },
160
- "optionalDependencies": {
161
- "@litko/yara-x": "^0.2.1"
162
- },
163
- "exports": {
164
- ".": {
165
- "types": "./dist/types/index.d.ts",
166
- "import": "./dist/pompelmi.esm.js",
167
- "require": "./dist/pompelmi.cjs",
168
- "default": "./dist/pompelmi.esm.js"
169
- },
170
- "./node": {
171
- "types": "./dist/types/index.d.ts",
172
- "import": "./dist/pompelmi.esm.js",
173
- "require": "./dist/pompelmi.cjs",
174
- "default": "./dist/pompelmi.esm.js"
175
- },
176
- "./browser": {
177
- "types": "./dist/types/browser-index.d.ts",
178
- "import": "./dist/pompelmi.browser.esm.js",
179
- "require": "./dist/pompelmi.browser.cjs",
180
- "default": "./dist/pompelmi.browser.esm.js"
181
- },
182
- "./react": {
183
- "types": "./dist/types/react-index.d.ts",
184
- "import": "./dist/pompelmi.react.esm.js",
185
- "require": "./dist/pompelmi.react.cjs",
186
- "default": "./dist/pompelmi.react.esm.js"
187
- },
188
- "./quarantine": {
189
- "types": "./dist/types/quarantine/index.d.ts",
190
- "import": "./dist/pompelmi.quarantine.esm.js",
191
- "require": "./dist/pompelmi.quarantine.cjs",
192
- "default": "./dist/pompelmi.quarantine.esm.js"
193
- },
194
- "./hooks": {
195
- "types": "./dist/types/hooks.d.ts",
196
- "import": "./dist/pompelmi.hooks.esm.js",
197
- "require": "./dist/pompelmi.hooks.cjs",
198
- "default": "./dist/pompelmi.hooks.esm.js"
199
- },
200
- "./audit": {
201
- "types": "./dist/types/audit.d.ts",
202
- "import": "./dist/pompelmi.audit.esm.js",
203
- "require": "./dist/pompelmi.audit.cjs",
204
- "default": "./dist/pompelmi.audit.esm.js"
205
- },
206
- "./policy-packs": {
207
- "types": "./dist/types/policy-packs.d.ts",
208
- "import": "./dist/pompelmi.policy-packs.esm.js",
209
- "require": "./dist/pompelmi.policy-packs.cjs",
210
- "default": "./dist/pompelmi.policy-packs.esm.js"
211
- },
212
- "./package.json": "./package.json"
213
- },
214
- "files": [
215
- "dist/",
216
- "README.md",
217
- "LICENSE",
218
- "package.json",
219
- "CHANGELOG*"
220
- ],
221
15
  "keywords": [
222
- "secure-file-upload",
223
- "file-upload-security",
224
- "upload-security",
225
- "upload-scanning",
226
- "file-scanning",
227
- "file-validation",
228
- "malware-scanner",
229
- "mime-sniffing",
230
- "magic-bytes",
231
- "archive-security",
232
- "zip-bomb-protection",
233
- "yara",
234
- "yara-rules",
235
- "local-first-security",
236
- "self-hosted-security",
237
- "nodejs-security",
238
- "typescript",
239
- "nodejs",
240
- "express",
241
- "nextjs",
242
- "nestjs",
243
- "fastify",
244
- "koa",
245
- "nuxt"
16
+ "clamav",
17
+ "antivirus",
18
+ "malware",
19
+ "virus",
20
+ "scan",
21
+ "security",
22
+ "file-scan"
246
23
  ],
247
- "directories": {
248
- "example": "examples"
249
- },
250
- "author": "Tommaso Bertocchi",
251
- "packageManager": "pnpm@9.12.0",
252
- "resolutions": {
253
- "process": "0.11.10"
254
- },
255
- "sideEffects": false,
256
- "engines": {
257
- "node": ">=18"
24
+ "type": "commonjs",
25
+ "main": "./src/index.js",
26
+ "scripts": {
27
+ "test": "node --test test/unit.test.js && node test/scan.test.js",
28
+ "lint": "eslint src/"
258
29
  },
259
- "publishConfig": {
260
- "access": "public"
30
+ "dependencies": {
31
+ "cross-spawn": "^7.0.6"
261
32
  },
262
- "types": "./dist/types/index.d.ts"
33
+ "devDependencies": {
34
+ "@eslint/js": "^10.0.1",
35
+ "eslint": "^10.2.0",
36
+ "globals": "^17.4.0"
37
+ }
263
38
  }
@@ -0,0 +1,48 @@
1
+ const spawn = require("cross-spawn");
2
+ const fs = require("fs");
3
+ const { getUpdaterCommand } = require('./InstallerCommand.js');
4
+ const { PLATFORM } = require('./constants.js');
5
+ const { DB_PATHS } = require('./config.js');
6
+
7
+ const MESSAGES = {
8
+ DB_PRESENT: "Virus database already present, skipping.",
9
+ SUCCESS: "Database updated successfully!",
10
+ FAILURE: (code) => `Database update failed with exit code: ${code}`,
11
+ STARTING: "Downloading virus definitions...",
12
+ PLATFORM_NOT_SUPPORTED: "Current platform is not supported."
13
+ };
14
+
15
+ function isDatabasePresent() {
16
+ const dbPath = DB_PATHS[PLATFORM];
17
+ return dbPath ? fs.existsSync(dbPath) : false;
18
+ }
19
+
20
+ function updateClamAVDatabase() {
21
+ return new Promise((resolve, reject) => {
22
+ if (isDatabasePresent()) {
23
+ console.log(MESSAGES.DB_PRESENT);
24
+ return resolve(MESSAGES.DB_PRESENT);
25
+ }
26
+
27
+ const [command, args] = getUpdaterCommand(PLATFORM);
28
+
29
+ if (!command) {
30
+ console.log(MESSAGES.PLATFORM_NOT_SUPPORTED);
31
+ return resolve(MESSAGES.PLATFORM_NOT_SUPPORTED);
32
+ }
33
+
34
+ console.log(MESSAGES.STARTING);
35
+
36
+ const child = spawn(command, args, { stdio: 'inherit' });
37
+ child.on('error', (err) => reject(err));
38
+ child.on('close', (code) => {
39
+ if (code !== 0) {
40
+ return reject(new Error(MESSAGES.FAILURE(code)));
41
+ }
42
+ console.log(MESSAGES.SUCCESS);
43
+ resolve(MESSAGES.SUCCESS);
44
+ });
45
+ });
46
+ }
47
+
48
+ module.exports = { updateClamAVDatabase };
@@ -0,0 +1,49 @@
1
+ const spawn = require("cross-spawn");
2
+ const { execSync } = require("child_process");
3
+ const { getInstallerCommand } = require('./InstallerCommand.js');
4
+ const { PLATFORM } = require('./constants.js')
5
+
6
+ const MESSAGES = {
7
+ ALREADY_INSTALLED: "ClamAV is already installed, skipping.",
8
+ SUCCESS: "Installation completed successfully!",
9
+ PLATFORM_NOT_SUPPORTED: "Current platform is not supported.",
10
+ FAILURE: (code) => `Installation failed with exit code: ${code}`
11
+ };
12
+
13
+ function isClamAVInstalled() {
14
+ const command = PLATFORM === 'win32' ? 'where clamscan' : 'which clamscan';
15
+ try {
16
+ execSync(command, { stdio: 'ignore' });
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ function ClamAVInstaller() {
24
+ return new Promise((resolve, reject) => {
25
+ if (isClamAVInstalled()) {
26
+ console.log(MESSAGES.ALREADY_INSTALLED);
27
+ return resolve(MESSAGES.ALREADY_INSTALLED);
28
+ }
29
+
30
+ const [packageManager, packageToInstall] = getInstallerCommand(PLATFORM);
31
+
32
+ if (!packageManager) {
33
+ console.log(MESSAGES.PLATFORM_NOT_SUPPORTED);
34
+ return resolve(MESSAGES.PLATFORM_NOT_SUPPORTED);
35
+ }
36
+
37
+ const child = spawn(packageManager, packageToInstall, { stdio: 'inherit' });
38
+ child.on('error', (err) => reject(err));
39
+ child.on('close', (code) => {
40
+ if (code !== 0) {
41
+ return reject(new Error(MESSAGES.FAILURE(code)));
42
+ }
43
+ console.log(MESSAGES.SUCCESS);
44
+ resolve(MESSAGES.SUCCESS);
45
+ });
46
+ });
47
+ }
48
+
49
+ module.exports = { ClamAVInstaller};
@@ -0,0 +1,37 @@
1
+ const spawn = require("cross-spawn");
2
+ const fs = require("fs");
3
+ const { SCAN_RESULTS } = require('./config.js');
4
+ const { scanViaClamd } = require('./ClamdScanner.js');
5
+
6
+ const MESSAGES = {
7
+ FILE_NOT_FOUND: (filePath) => `File not found: ${filePath}`,
8
+ UNEXPECTED_EXIT_CODE: (code) => `Unexpected exit code: ${code}`,
9
+ PROCESS_KILLED: (signal) => `Process killed by signal: ${signal}`,
10
+ };
11
+
12
+ function scan(filePath, options = {}) {
13
+ // When a host or port is provided, delegate to the clamd TCP path.
14
+ if (options.host !== undefined || options.port !== undefined) {
15
+ return scanViaClamd(filePath, options);
16
+ }
17
+
18
+ return new Promise((resolve, reject) => {
19
+ if (typeof filePath !== 'string') {
20
+ return reject(new Error('filePath must be a string'));
21
+ }
22
+ if (!fs.existsSync(filePath)) {
23
+ return reject(new Error(MESSAGES.FILE_NOT_FOUND(filePath)));
24
+ }
25
+
26
+ const child = spawn('clamscan', ['--no-summary', filePath]);
27
+ child.on('error', (err) => reject(err));
28
+ child.on('close', (code, signal) => {
29
+ if (code === null) return reject(new Error(MESSAGES.PROCESS_KILLED(signal)));
30
+ const result = SCAN_RESULTS[code];
31
+ if (!result) return reject(new Error(MESSAGES.UNEXPECTED_EXIT_CODE(code)));
32
+ resolve(result);
33
+ });
34
+ });
35
+ }
36
+
37
+ module.exports = { scan };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+ const fs = require('fs');
5
+
6
+ // ClamAV INSTREAM protocol:
7
+ // 1. Send "zINSTREAM\0"
8
+ // 2. Send N chunks, each prefixed with a 4-byte big-endian length
9
+ // 3. Terminate with four zero bytes
10
+ // 4. Read response line: "stream: OK", "stream: <name> FOUND", or an error
11
+ const CLAMD_INSTREAM = Buffer.from('zINSTREAM\0');
12
+ const CHUNK_SIZE = 64 * 1024; // 64 KB — well within clamd's default StreamMaxLength
13
+
14
+ function parseClamdResponse(raw) {
15
+ const text = raw.toString('utf8').trim();
16
+ if (text === 'stream: OK') return 'Clean';
17
+ if (text.endsWith(' FOUND')) return 'Malicious';
18
+ return 'ScanError';
19
+ }
20
+
21
+ /**
22
+ * Scan a file by streaming it to a running clamd instance over TCP.
23
+ *
24
+ * @param {string} filePath - Absolute or relative path to the file to scan.
25
+ * @param {object} [options]
26
+ * @param {string} [options.host='127.0.0.1']
27
+ * @param {number} [options.port=3310]
28
+ * @param {number} [options.timeout=15000] - Socket idle timeout in ms.
29
+ * @returns {Promise<'Clean'|'Malicious'|'ScanError'>}
30
+ */
31
+ function scanViaClamd(filePath, { host = '127.0.0.1', port = 3310, timeout = 15_000 } = {}) {
32
+ return new Promise((resolve, reject) => {
33
+ if (typeof filePath !== 'string') {
34
+ return reject(new Error('filePath must be a string'));
35
+ }
36
+ if (!fs.existsSync(filePath)) {
37
+ return reject(new Error(`File not found: ${filePath}`));
38
+ }
39
+
40
+ const socket = net.createConnection({ host, port });
41
+ const chunks = [];
42
+ let settled = false;
43
+
44
+ function settle(fn, value) {
45
+ if (settled) return;
46
+ settled = true;
47
+ socket.destroy();
48
+ fn(value);
49
+ }
50
+
51
+ socket.setTimeout(timeout);
52
+ socket.on('timeout', () =>
53
+ settle(reject, new Error(`clamd connection timed out after ${timeout}ms`))
54
+ );
55
+ socket.on('error', (err) => settle(reject, err));
56
+ socket.on('data', (chunk) => chunks.push(chunk));
57
+ socket.on('end', () => settle(resolve, parseClamdResponse(Buffer.concat(chunks))));
58
+
59
+ socket.on('connect', () => {
60
+ socket.write(CLAMD_INSTREAM);
61
+
62
+ const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
63
+
64
+ fileStream.on('error', (err) => settle(reject, err));
65
+
66
+ fileStream.on('data', (chunk) => {
67
+ const header = Buffer.allocUnsafe(4);
68
+ header.writeUInt32BE(chunk.length, 0);
69
+ socket.write(header);
70
+ socket.write(chunk);
71
+ });
72
+
73
+ fileStream.on('end', () => {
74
+ socket.write(Buffer.alloc(4)); // terminating zero-length chunk
75
+ socket.end();
76
+ });
77
+ });
78
+ });
79
+ }
80
+
81
+ module.exports = { scanViaClamd };
@@ -0,0 +1,11 @@
1
+ const { INSTALLER_COMMANDS, UPDATER_COMMANDS } = require('./config.js');
2
+
3
+ function getInstallerCommand(platform) {
4
+ return INSTALLER_COMMANDS[platform] ?? [null, []];
5
+ }
6
+
7
+ function getUpdaterCommand(platform) {
8
+ return UPDATER_COMMANDS[platform] ?? [null, []];
9
+ }
10
+
11
+ module.exports = { getInstallerCommand, getUpdaterCommand };
package/src/config.js ADDED
@@ -0,0 +1,22 @@
1
+ module.exports = Object.freeze({
2
+ INSTALLER_COMMANDS: Object.freeze({
3
+ win32: ['choco', ['install', 'clamav', '-y']],
4
+ darwin: ['brew', ['install', 'clamav']],
5
+ linux: ['sudo', ['apt-get', 'install', '-y', 'clamav', 'clamav-daemon']],
6
+ }),
7
+ UPDATER_COMMANDS: Object.freeze({
8
+ win32: ['freshclam', []],
9
+ darwin: ['freshclam', []],
10
+ linux: ['sudo', ['freshclam']],
11
+ }),
12
+ DB_PATHS: Object.freeze({
13
+ darwin: '/usr/local/share/clamav/main.cvd',
14
+ linux: '/var/lib/clamav/main.cvd',
15
+ win32: 'C:\\ProgramData\\ClamAV\\main.cvd',
16
+ }),
17
+ SCAN_RESULTS: Object.freeze({
18
+ 0: 'Clean',
19
+ 1: 'Malicious',
20
+ 2: 'ScanError'
21
+ }),
22
+ });
@@ -0,0 +1,3 @@
1
+ module.exports = Object.freeze({
2
+ PLATFORM: process.platform
3
+ });
Binary file
Binary file
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ const { scan } = require('./ClamAVScanner.js');
2
+
3
+ const pompelmi = { scan };
4
+
5
+ module.exports = pompelmi;
package/test_out.txt ADDED
@@ -0,0 +1,74 @@
1
+ ClamAV is already installed, skipping.
2
+ Current platform is not supported.
3
+ Installation completed successfully!
4
+ Virus database already present, skipping.
5
+ Current platform is not supported.
6
+ Downloading virus definitions...
7
+ Database updated successfully!
8
+ Downloading virus definitions...
9
+ Downloading virus definitions...
10
+ ▶ InstallerCommand
11
+ ▶ getInstallerCommand
12
+ ✔ darwin → brew install clamav (1.172833ms)
13
+ ✔ linux → sudo apt-get install (0.163833ms)
14
+ ✔ win32 → choco install clamav (0.07125ms)
15
+ ✔ unknown → [null, []] (0.054792ms)
16
+ ✔ getInstallerCommand (1.877625ms)
17
+ ▶ getUpdaterCommand
18
+ ✔ darwin → freshclam (0.094625ms)
19
+ ✔ linux → sudo freshclam (0.255ms)
20
+ ✔ win32 → freshclam (0.055834ms)
21
+ ✔ unknown → [null, []] (0.308416ms)
22
+ ✔ getUpdaterCommand (0.965833ms)
23
+ ✔ InstallerCommand (3.261333ms)
24
+ ▶ ClamAVScanner
25
+ ✔ rejects if filePath is not a string (10.062042ms)
26
+ ✔ rejects if file does not exist (0.441042ms)
27
+ ✔ exit code 0 → resolves to "Clean" (0.847959ms)
28
+ ✔ exit code 1 → resolves to "Malicious" (0.890875ms)
29
+ ✔ exit code 2 → resolves to "ScanError" (0.5805ms)
30
+ ✔ exit code 99 → rejects with exit code message (0.535625ms)
31
+ ✔ spawn error → rejects with the error (0.550542ms)
32
+ ✔ signal kill → rejects with signal name (0.405667ms)
33
+ ✔ ClamAVScanner (14.502167ms)
34
+ ▶ ClamAVInstaller
35
+ ✔ resolves if ClamAV is already installed (1.17225ms)
36
+ ✔ resolves with platform-not-supported message (1.279459ms)
37
+ ✔ resolves after successful installation (0.755125ms)
38
+ ✔ rejects when installation exits non-zero (0.553375ms)
39
+ ✔ rejects on spawn error (0.969208ms)
40
+ ✔ ClamAVInstaller (4.845042ms)
41
+ ▶ ClamdScanner
42
+ ✔ rejects if filePath is not a string (0.186125ms)
43
+ ✔ rejects if file does not exist (0.371125ms)
44
+ ✔ "stream: OK" → "Clean" (0.388458ms)
45
+ ✔ "stream: ... FOUND" → "Malicious" (0.163792ms)
46
+ ✔ any other clamd response → "ScanError" (0.294458ms)
47
+ ✔ socket error (ECONNREFUSED) → rejects with that error (0.187ms)
48
+ ✔ socket timeout → rejects with timeout message (0.119208ms)
49
+ ✔ file read stream error → rejects with that error (0.118084ms)
50
+ ✔ sends zINSTREAM command, chunk header, chunk data, and terminator (0.327083ms)
51
+ ✔ ClamdScanner (2.320125ms)
52
+ ▶ ClamAVScanner (TCP routing)
53
+ ✔ routes to clamd when { port } is given (2.303ms)
54
+ ✔ routes to clamd when { host } is given (0.207792ms)
55
+ ✔ routes to clamd when { host, port } are both given (0.27525ms)
56
+ ✔ forwards filePath and options unchanged to scanViaClamd (0.267083ms)
57
+ ✔ uses the CLI path when called without options (0.31125ms)
58
+ ✔ uses the CLI path when called with an empty options object {} (0.318041ms)
59
+ ✔ ClamAVScanner (TCP routing) (3.793958ms)
60
+ ▶ ClamAVDatabaseUpdater
61
+ ✔ resolves if database is already present (0.793833ms)
62
+ ✔ resolves with platform-not-supported message (0.326166ms)
63
+ ✔ resolves after successful update (0.37675ms)
64
+ ✔ rejects when update exits non-zero (0.330167ms)
65
+ ✔ rejects on spawn error (0.545833ms)
66
+ ✔ ClamAVDatabaseUpdater (2.478542ms)
67
+ ℹ tests 41
68
+ ℹ suites 8
69
+ ℹ pass 41
70
+ ℹ fail 0
71
+ ℹ cancelled 0
72
+ ℹ skipped 0
73
+ ℹ todo 0
74
+ ℹ duration_ms 107.87375