pompelmi 1.17.0 → 1.19.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.
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  <p align="center">
12
12
  <a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/v/pompelmi.svg" alt="npm version"></a>
13
13
  <a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/dw/pompelmi" alt="weekly downloads"></a>
14
+ <a href="https://hub.docker.com/r/justsouichi/pompelmi-scanner"><img src="https://img.shields.io/docker/pulls/justsouichi/pompelmi-scanner" alt="Docker Pulls"></a>
14
15
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies">
15
16
  <img src="https://img.shields.io/badge/license-ISC-blue" alt="license">
16
17
  <a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img src="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
@@ -73,6 +74,10 @@ Most integrations require parsing ClamAV's stdout with regex, managing a clamd d
73
74
  ## Features
74
75
 
75
76
  - Standalone CLI — scan files from any terminal with `npx pompelmi scan`
77
+ - **Official Docker image** — `justsouichi/pompelmi-scanner` on Docker Hub: ClamAV + HTTP scan API in one pull ([docs](./docs/docker-image.html))
78
+ - **Security scorecard** — grade your upload security A–F with `npx pompelmi scorecard` ([docs](./docs/scorecard.html))
79
+ - **VS Code extension** — scan files directly from the IDE (scaffold in `packages/vscode/`) ([docs](./docs/vscode.html))
80
+ - **Quarantine mode** — `watch ./uploads --quarantine ./quarantine` auto-moves infected files with sidecar JSON
76
81
  - HTML security dashboard — generate beautiful scan reports with `--report` ([docs](./docs/dashboard.html))
77
82
  - SVG share card — shareable scan result card with `--share-card` ([docs](./docs/dashboard.html#share-card))
78
83
  - GitHub App — one-click installation for organizations, zero-config PR scanning ([docs](./docs/github-app.html))
@@ -85,6 +90,10 @@ Most integrations require parsing ClamAV's stdout with regex, managing a clamd d
85
90
  - `watch(dirPath, [options], callbacks)` — watch a directory and auto-scan new/modified files (300 ms debounce)
86
91
  - `notify(webhookUrl, scanResult, [options])` — send a POST webhook notification when a virus is detected; optional HMAC-SHA256 signing via `X-Pompelmi-Signature`; zero extra dependencies
87
92
  - `createScanner([options])` — EventEmitter-based scanner; call `.scan(filePath)` or `.scanDirectory(dirPath)` and listen to `'clean'`, `'malicious'`, `'scanError'`, and `'error'` events
93
+ - **SHA256 scan cache** — `createCache([options])` — skip rescanning known-clean files; LRU eviction, configurable TTL, optional file-backed persistence; zero extra dependencies ([docs](./docs/cache.html))
94
+ - **Scan policies** — `createPolicy(rules)` — unified size, MIME type, extension, and virus rules in one object; Express middleware and NestJS guard included ([docs](./docs/policy.html))
95
+ - **Multi-engine scanning** — `createMultiEngine(options)` — combine ClamAV and VirusTotal with `any`/`all`/`majority` consensus; per-engine verdict breakdown; zero extra dependencies ([docs](./docs/multi-engine.html))
96
+ - **Directory streaming** — `scanDirectory.stream(dirPath)` — async-iterable progress events (`progress` / `result` / `complete`) for real-time UI feedback
88
97
  - Auto-retry on connection error — `retries` and `retryDelay` options on every scan function
89
98
  - Symbol-based verdicts (`Verdict.Clean` / `Verdict.Malicious` / `Verdict.ScanError`) — typo-proof comparisons
90
99
  - Full clamd support via the INSTREAM protocol — TCP (`host`/`port`) or UNIX socket (`socket`) with configurable timeout
@@ -617,6 +626,8 @@ Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing. To r
617
626
  - [x] Cloudflare Workers support — `@pompelmi/cloudflare` ships in v1.17.0
618
627
  - [x] NestJS official module — `PompelmiModule.forRoot()` with injectable `PompelmiService`
619
628
 
629
+ **Pompelmi Cloud** is on the horizon — a hosted REST API for file scanning with zero infrastructure to manage. Drop-in HTTP endpoint, no ClamAV to maintain, no daemon to run. [Join the waitlist](https://pompelmi.app/cloud.html) to be notified when it launches.
630
+
620
631
  ---
621
632
 
622
633
  ## License
package/bin/pompelmi.js CHANGED
@@ -71,12 +71,14 @@ function parseArgs(argv) {
71
71
  reportOutput: null,
72
72
  shareCard: false,
73
73
  shareCardOutput: null,
74
+ quarantine: null,
75
+ scorecardConfig: null,
74
76
  };
75
77
 
76
78
  let i = 0;
77
79
  while (i < args.length) {
78
80
  const a = args[i];
79
- if (a === 'scan' || a === 'watch' || a === 'version' || a === 'help') {
81
+ if (a === 'scan' || a === 'watch' || a === 'version' || a === 'help' || a === 'scorecard') {
80
82
  opts.command = a;
81
83
  } else if (a === '--json') {
82
84
  opts.json = true;
@@ -104,6 +106,10 @@ function parseArgs(argv) {
104
106
  const next = args[++i];
105
107
  if (next && next.endsWith('.svg')) opts.shareCardOutput = next;
106
108
  else opts.reportOutput = next;
109
+ } else if (a === '--quarantine') {
110
+ opts.quarantine = args[++i];
111
+ } else if (a === '--config') {
112
+ opts.scorecardConfig = args[++i];
107
113
  } else if (!a.startsWith('-') && opts.command && !opts.target) {
108
114
  opts.target = a;
109
115
  }
@@ -333,7 +339,9 @@ async function cmdWatch(opts) {
333
339
  if (!opts.json && !opts.quiet) await printLogo();
334
340
 
335
341
  const { watch } = require('../src/Watcher.js');
336
- const scanOpts = buildScanOpts(opts);
342
+ const quarantineDir = opts.quarantine ? path.resolve(opts.quarantine) : null;
343
+ const watchOpts = { ...buildScanOpts(opts) };
344
+ if (quarantineDir) watchOpts.quarantine = quarantineDir;
337
345
 
338
346
  let scanned = 0, clean = 0, infected = 0;
339
347
 
@@ -345,7 +353,7 @@ async function cmdWatch(opts) {
345
353
 
346
354
  status();
347
355
 
348
- watch(target, scanOpts, {
356
+ watch(target, watchOpts, {
349
357
  onClean(fp) {
350
358
  scanned++; clean++;
351
359
  if (!opts.quiet) process.stdout.write(`\n\x1b[32m✅ CLEAN\x1b[0m ${fp}\n`);
@@ -354,6 +362,7 @@ async function cmdWatch(opts) {
354
362
  onMalicious(fp) {
355
363
  scanned++; infected++;
356
364
  process.stdout.write(`\n\x1b[31m🚨 INFECTED\x1b[0m ${fp}\n`);
365
+ if (quarantineDir && !opts.quiet) process.stdout.write(` Quarantined to ${quarantineDir}\n`);
357
366
  status();
358
367
  },
359
368
  onError(err) {
@@ -366,6 +375,44 @@ async function cmdWatch(opts) {
366
375
  process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
367
376
  }
368
377
 
378
+ async function cmdScorecard(opts) {
379
+ let config = {};
380
+ if (opts.scorecardConfig) {
381
+ const cfgPath = path.resolve(opts.scorecardConfig);
382
+ if (!fs.existsSync(cfgPath)) {
383
+ console.error(`Error: config file not found: ${cfgPath}`);
384
+ process.exit(2);
385
+ }
386
+ config = require(cfgPath);
387
+ }
388
+
389
+ const { generateScorecard } = require('../src/Scorecard.js');
390
+ const scorecard = await generateScorecard(config);
391
+
392
+ const gradeColor = { A: '\x1b[32m', B: '\x1b[32m', C: '\x1b[33m', D: '\x1b[33m', F: '\x1b[31m' };
393
+ const color = gradeColor[scorecard.grade] || '\x1b[0m';
394
+ const reset = '\x1b[0m';
395
+
396
+ if (!opts.quiet) await printLogo();
397
+
398
+ console.log(`\n Upload Security Scorecard\n`);
399
+ console.log(` Grade: ${color}${scorecard.grade}${reset} Score: ${scorecard.score}/100\n`);
400
+
401
+ for (const f of scorecard.findings) {
402
+ const icon = f.status === 'pass' ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
403
+ console.log(` ${icon} ${f.check.padEnd(40)} (weight: ${f.weight})`);
404
+ }
405
+
406
+ if (scorecard.recommendations.length > 0) {
407
+ console.log(`\n Recommendations:`);
408
+ for (const r of scorecard.recommendations) {
409
+ console.log(` • ${r}`);
410
+ }
411
+ }
412
+
413
+ console.log('');
414
+ }
415
+
369
416
  function cmdVersion() {
370
417
  console.log(pkg.version);
371
418
  }
@@ -380,6 +427,7 @@ USAGE
380
427
  COMMANDS
381
428
  scan <file|dir> Scan a file or directory for viruses
382
429
  watch <dir> Watch a directory and auto-scan new/modified files
430
+ scorecard Grade your upload security configuration (A-F)
383
431
  version Print version number
384
432
  help Show this help message
385
433
 
@@ -400,6 +448,10 @@ SCAN OPTIONS
400
448
 
401
449
  WATCH OPTIONS
402
450
  --host, --port, --socket, --timeout (same as scan)
451
+ --quarantine <dir> Move infected files to this directory (auto-created)
452
+
453
+ SCORECARD OPTIONS
454
+ --config <file> Path to a pompelmi.config.js file with your upload config
403
455
 
404
456
  EXIT CODES
405
457
  0 All files clean
@@ -428,6 +480,12 @@ EXAMPLES
428
480
 
429
481
  # Use with npx (no install required)
430
482
  npx pompelmi scan ./uploads
483
+
484
+ # Watch a directory with quarantine
485
+ pompelmi watch ./uploads --quarantine ./quarantine
486
+
487
+ # Grade your upload security config
488
+ npx pompelmi scorecard --config ./pompelmi.config.js
431
489
  `);
432
490
  }
433
491
 
@@ -456,6 +514,11 @@ async function main() {
456
514
  await cmdWatch(opts);
457
515
  return;
458
516
  }
517
+
518
+ if (opts.command === 'scorecard') {
519
+ await cmdScorecard(opts);
520
+ return;
521
+ }
459
522
  }
460
523
 
461
524
  main().catch(err => {
@@ -0,0 +1,18 @@
1
+ FROM node:20-slim
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y --no-install-recommends \
5
+ clamav clamav-freshclam curl && \
6
+ rm -rf /var/lib/apt/lists/* && \
7
+ sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf && \
8
+ mkdir -p /var/lib/clamav /run/clamav /uploads
9
+
10
+ WORKDIR /app
11
+ RUN npm install -g pompelmi
12
+
13
+ COPY docker/entrypoint.sh /entrypoint.sh
14
+ RUN chmod +x /entrypoint.sh
15
+
16
+ EXPOSE 3310 8080
17
+
18
+ ENTRYPOINT ["/entrypoint.sh"]
@@ -0,0 +1,62 @@
1
+ # justsouichi/pompelmi-scanner Docker Image
2
+
3
+ Official Docker image for pompelmi — ClamAV antivirus scanning for Node.js.
4
+
5
+ The image ships with:
6
+ - ClamAV + clamd pre-installed
7
+ - freshclam for automatic definition updates on startup
8
+ - A minimal HTTP API server on port 8080
9
+
10
+ ## Quick start
11
+
12
+ ```bash
13
+ docker pull justsouichi/pompelmi-scanner
14
+ docker run -p 8080:8080 justsouichi/pompelmi-scanner
15
+ ```
16
+
17
+ ## Scan a file
18
+
19
+ ```bash
20
+ curl -F "file=@./document.pdf" http://localhost:8080/scan
21
+ ```
22
+
23
+ Response:
24
+
25
+ ```json
26
+ { "verdict": "clean", "file": "document.pdf", "viruses": [] }
27
+ ```
28
+
29
+ For an infected file the verdict is `"malicious"`.
30
+
31
+ ## Endpoints
32
+
33
+ | Method | Path | Description |
34
+ |--------|-----------|---------------------------------------------------------|
35
+ | POST | /scan | Accepts `multipart/form-data` with a `file` field. Returns `{ verdict, file, viruses }` |
36
+ | GET | /health | Returns `{ status: "ok", clamd: "running" }` |
37
+ | GET | /stats | Returns scan statistics (total, clean, infected, errors, uptime) |
38
+
39
+ ## Docker Compose
40
+
41
+ ```yaml
42
+ services:
43
+ pompelmi:
44
+ image: justsouichi/pompelmi-scanner
45
+ ports:
46
+ - "8080:8080"
47
+ volumes:
48
+ - /uploads:/uploads
49
+ restart: unless-stopped
50
+ ```
51
+
52
+ ## Environment
53
+
54
+ The container exposes two ports:
55
+ - **8080** — HTTP scan API
56
+ - **3310** — clamd TCP socket (for direct clamd connections from other containers)
57
+
58
+ ## Building locally
59
+
60
+ ```bash
61
+ docker build -f docker/Dockerfile -t justsouichi/pompelmi-scanner .
62
+ ```
@@ -0,0 +1,148 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # ── Update ClamAV virus definitions ──────────────────────────────────────────
5
+ echo "[pompelmi] Updating ClamAV definitions via freshclam..."
6
+ freshclam --quiet || echo "[pompelmi] freshclam failed — continuing with existing definitions"
7
+
8
+ # ── Start clamd in background ─────────────────────────────────────────────────
9
+ echo "[pompelmi] Starting clamd..."
10
+ clamd &
11
+ CLAMD_PID=$!
12
+
13
+ # Wait for clamd socket/port to be ready
14
+ echo "[pompelmi] Waiting for clamd to be ready..."
15
+ for i in $(seq 1 30); do
16
+ if clamdscan --no-summary /dev/null 2>/dev/null; then
17
+ echo "[pompelmi] clamd is ready"
18
+ break
19
+ fi
20
+ sleep 1
21
+ done
22
+
23
+ # ── Stats counters (stored in files for simplicity) ───────────────────────────
24
+ STATS_DIR=/tmp/pompelmi-stats
25
+ mkdir -p "$STATS_DIR"
26
+ echo 0 > "$STATS_DIR/total"
27
+ echo 0 > "$STATS_DIR/clean"
28
+ echo 0 > "$STATS_DIR/infected"
29
+ echo 0 > "$STATS_DIR/errors"
30
+
31
+ # ── Minimal HTTP server ───────────────────────────────────────────────────────
32
+ # Uses Node.js (already available from the base image)
33
+ node - <<'EOF'
34
+ 'use strict';
35
+
36
+ const http = require('http');
37
+ const fs = require('fs');
38
+ const os = require('os');
39
+ const path = require('path');
40
+ const { execSync, exec } = require('child_process');
41
+ const { scanBuffer } = require('pompelmi');
42
+ const { Verdict } = require('pompelmi');
43
+
44
+ const PORT = 8080;
45
+ const STATS = { total: 0, clean: 0, infected: 0, errors: 0, startedAt: new Date().toISOString() };
46
+
47
+ function parseMultipart(req) {
48
+ return new Promise((resolve, reject) => {
49
+ const boundary = (req.headers['content-type'] || '').split('boundary=')[1];
50
+ if (!boundary) return reject(new Error('Missing multipart boundary'));
51
+
52
+ const chunks = [];
53
+ req.on('data', c => chunks.push(c));
54
+ req.on('error', reject);
55
+ req.on('end', () => {
56
+ const body = Buffer.concat(chunks);
57
+ const delim = Buffer.from('\r\n--' + boundary);
58
+ const parts = [];
59
+ let start = body.indexOf('--' + boundary);
60
+
61
+ while (start !== -1) {
62
+ const headerEnd = body.indexOf('\r\n\r\n', start);
63
+ if (headerEnd === -1) break;
64
+ const headerStr = body.slice(start, headerEnd).toString();
65
+ const nameMatch = headerStr.match(/name="([^"]+)"/);
66
+ const fileMatch = headerStr.match(/filename="([^"]+)"/);
67
+ const dataStart = headerEnd + 4;
68
+ const nextDelim = body.indexOf(delim, dataStart);
69
+ const dataEnd = nextDelim === -1 ? body.indexOf('\r\n--' + boundary + '--', dataStart) : nextDelim;
70
+ if (dataEnd === -1) break;
71
+ parts.push({
72
+ name: nameMatch ? nameMatch[1] : '',
73
+ filename: fileMatch ? fileMatch[1] : '',
74
+ data: body.slice(dataStart, dataEnd),
75
+ });
76
+ start = body.indexOf('--' + boundary, dataEnd);
77
+ if (body.slice(start, start + boundary.length + 6).toString().includes('--\r\n')) break;
78
+ }
79
+ resolve(parts);
80
+ });
81
+ });
82
+ }
83
+
84
+ function isClamdRunning() {
85
+ try { execSync('clamdscan --no-summary /dev/null 2>/dev/null'); return true; }
86
+ catch { return false; }
87
+ }
88
+
89
+ const server = http.createServer(async (req, res) => {
90
+ res.setHeader('Content-Type', 'application/json');
91
+
92
+ if (req.method === 'GET' && req.url === '/health') {
93
+ res.writeHead(200);
94
+ return res.end(JSON.stringify({ status: 'ok', clamd: isClamdRunning() ? 'running' : 'unavailable' }));
95
+ }
96
+
97
+ if (req.method === 'GET' && req.url === '/stats') {
98
+ res.writeHead(200);
99
+ return res.end(JSON.stringify({ ...STATS, uptime: process.uptime() }));
100
+ }
101
+
102
+ if (req.method === 'POST' && req.url === '/scan') {
103
+ try {
104
+ const parts = await parseMultipart(req);
105
+ const filePart = parts.find(p => p.filename);
106
+ if (!filePart) {
107
+ res.writeHead(400);
108
+ return res.end(JSON.stringify({ error: 'No file field in request' }));
109
+ }
110
+
111
+ STATS.total++;
112
+ const verdict = await scanBuffer(filePart.data);
113
+
114
+ let result;
115
+ if (verdict === Verdict.Clean) {
116
+ STATS.clean++;
117
+ result = { verdict: 'clean', file: filePart.filename, viruses: [] };
118
+ } else if (verdict === Verdict.Malicious) {
119
+ STATS.infected++;
120
+ result = { verdict: 'malicious', file: filePart.filename, viruses: [] };
121
+ } else {
122
+ STATS.errors++;
123
+ result = { verdict: 'error', file: filePart.filename, viruses: [] };
124
+ }
125
+
126
+ res.writeHead(200);
127
+ return res.end(JSON.stringify(result));
128
+ } catch (err) {
129
+ STATS.errors++;
130
+ res.writeHead(500);
131
+ return res.end(JSON.stringify({ error: err.message }));
132
+ }
133
+ }
134
+
135
+ res.writeHead(404);
136
+ res.end(JSON.stringify({ error: 'Not found' }));
137
+ });
138
+
139
+ server.listen(PORT, () => {
140
+ console.log(`[pompelmi] HTTP scan server listening on port ${PORT}`);
141
+ console.log(`[pompelmi] POST /scan — scan a file`);
142
+ console.log(`[pompelmi] GET /health — health check`);
143
+ console.log(`[pompelmi] GET /stats — scan statistics`);
144
+ });
145
+
146
+ process.on('SIGTERM', () => { server.close(); process.exit(0); });
147
+ process.on('SIGINT', () => { server.close(); process.exit(0); });
148
+ EOF
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "ClamAV for humans — scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.",
5
5
  "license": "ISC",
6
6
  "author": "pompelmi contributors",
@@ -0,0 +1,61 @@
1
+ # pompelmi — VS Code Extension
2
+
3
+ Scan files for viruses directly from VS Code using pompelmi and ClamAV.
4
+
5
+ ## Requirements
6
+
7
+ - [ClamAV](https://www.clamav.net/) installed (or clamd running via Docker)
8
+ - Node.js and npm available in your PATH
9
+
10
+ ## Installation
11
+
12
+ Install from the VS Code Marketplace:
13
+
14
+ 1. Open VS Code
15
+ 2. Press `Ctrl+P` (or `Cmd+P` on macOS)
16
+ 3. Run: `ext install pompelmi.pompelmi`
17
+
18
+ ## Usage
19
+
20
+ ### Scan a single file
21
+
22
+ Right-click any file in the Explorer sidebar and select **"Scan with pompelmi"**.
23
+
24
+ Results appear as VS Code notifications:
25
+
26
+ - `✅ file.pdf — Clean`
27
+ - `🚨 malware.exe — INFECTED (Win.Malware.Agent)`
28
+
29
+ ### Scan the workspace
30
+
31
+ Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run:
32
+
33
+ ```
34
+ pompelmi: Scan Workspace
35
+ ```
36
+
37
+ ### Configure
38
+
39
+ Run `pompelmi: Configure` from the Command Palette to open settings.
40
+
41
+ ## Settings
42
+
43
+ | Setting | Default | Description |
44
+ |-------------------|-------------|------------------------------------------|
45
+ | `pompelmi.host` | `localhost` | clamd host |
46
+ | `pompelmi.port` | `3310` | clamd port |
47
+ | `pompelmi.socket` | *(empty)* | clamd UNIX socket path (takes precedence over host/port) |
48
+
49
+ ## Using with Docker
50
+
51
+ Start clamd via the official pompelmi Docker image:
52
+
53
+ ```bash
54
+ docker run -p 3310:3310 -p 8080:8080 justsouichi/pompelmi-scanner
55
+ ```
56
+
57
+ Then set `pompelmi.host` to `localhost` and `pompelmi.port` to `3310`.
58
+
59
+ ## License
60
+
61
+ ISC © pompelmi contributors
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "pompelmi",
3
+ "displayName": "pompelmi — Antivirus File Scanner",
4
+ "description": "Scan files for viruses directly from VS Code using pompelmi and ClamAV.",
5
+ "version": "0.1.0",
6
+ "publisher": "pompelmi",
7
+ "license": "ISC",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/pompelmi/pompelmi.git"
11
+ },
12
+ "engines": {
13
+ "vscode": "^1.85.0"
14
+ },
15
+ "categories": ["Other", "Linters"],
16
+ "keywords": ["antivirus", "clamav", "security", "malware", "virus"],
17
+ "icon": "icon.png",
18
+ "activationEvents": [
19
+ "onCommand:pompelmi.scanFile"
20
+ ],
21
+ "main": "./src/extension.js",
22
+ "contributes": {
23
+ "commands": [
24
+ {
25
+ "command": "pompelmi.scanFile",
26
+ "title": "Scan with pompelmi",
27
+ "category": "pompelmi"
28
+ },
29
+ {
30
+ "command": "pompelmi.scanWorkspace",
31
+ "title": "Scan Workspace",
32
+ "category": "pompelmi"
33
+ },
34
+ {
35
+ "command": "pompelmi.configure",
36
+ "title": "Configure pompelmi",
37
+ "category": "pompelmi"
38
+ }
39
+ ],
40
+ "menus": {
41
+ "explorer/context": [
42
+ {
43
+ "command": "pompelmi.scanFile",
44
+ "when": "resourceScheme == 'file'",
45
+ "group": "7_modification"
46
+ }
47
+ ]
48
+ },
49
+ "configuration": {
50
+ "title": "pompelmi",
51
+ "properties": {
52
+ "pompelmi.host": {
53
+ "type": "string",
54
+ "default": "localhost",
55
+ "description": "clamd host"
56
+ },
57
+ "pompelmi.port": {
58
+ "type": "number",
59
+ "default": 3310,
60
+ "description": "clamd port"
61
+ },
62
+ "pompelmi.socket": {
63
+ "type": "string",
64
+ "default": "",
65
+ "description": "clamd UNIX socket path (e.g. /run/clamav/clamd.sock)"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const { execFile } = require('child_process');
4
+ const path = require('path');
5
+
6
+ function getScanOptions(vscode) {
7
+ const cfg = vscode.workspace.getConfiguration('pompelmi');
8
+ const host = cfg.get('host', 'localhost');
9
+ const port = cfg.get('port', 3310);
10
+ const socket = cfg.get('socket', '');
11
+ const args = ['scan'];
12
+ if (socket) { args.push('--socket', socket); }
13
+ else { args.push('--host', host, '--port', String(port)); }
14
+ args.push('--json');
15
+ return args;
16
+ }
17
+
18
+ function runScan(filePath, args, callback) {
19
+ const allArgs = [...args, filePath];
20
+ execFile('npx', ['pompelmi', ...allArgs], { timeout: 30000 }, (err, stdout) => {
21
+ if (err && !stdout) return callback(err, null);
22
+ try {
23
+ const result = JSON.parse(stdout);
24
+ callback(null, result);
25
+ } catch {
26
+ callback(new Error('Failed to parse pompelmi output'), null);
27
+ }
28
+ });
29
+ }
30
+
31
+ function activate(context) {
32
+ const vscode = require('vscode');
33
+
34
+ const scanFileCmd = vscode.commands.registerCommand('pompelmi.scanFile', async (uri) => {
35
+ const filePath = uri ? uri.fsPath : vscode.window.activeTextEditor?.document.uri.fsPath;
36
+ if (!filePath) {
37
+ vscode.window.showErrorMessage('pompelmi: No file selected.');
38
+ return;
39
+ }
40
+
41
+ const args = getScanOptions(vscode);
42
+ vscode.window.withProgress(
43
+ { location: vscode.ProgressLocation.Notification, title: `pompelmi: scanning ${path.basename(filePath)}...` },
44
+ () => new Promise(resolve => {
45
+ runScan(filePath, args, (err, result) => {
46
+ resolve();
47
+ if (err) {
48
+ vscode.window.showErrorMessage(`pompelmi error: ${err.message}`);
49
+ return;
50
+ }
51
+ const r = result?.results?.[0];
52
+ if (!r) {
53
+ vscode.window.showErrorMessage('pompelmi: unexpected response');
54
+ return;
55
+ }
56
+ if (r.verdict === 'clean') {
57
+ vscode.window.showInformationMessage(`✅ ${path.basename(filePath)} — Clean`);
58
+ } else if (r.verdict === 'infected') {
59
+ const virus = r.viruses?.[0] ? ` (${r.viruses[0]})` : '';
60
+ vscode.window.showErrorMessage(`🚨 ${path.basename(filePath)} — INFECTED${virus}`);
61
+ } else {
62
+ vscode.window.showWarningMessage(`⚠️ ${path.basename(filePath)} — Scan error`);
63
+ }
64
+ });
65
+ })
66
+ );
67
+ });
68
+
69
+ const scanWorkspaceCmd = vscode.commands.registerCommand('pompelmi.scanWorkspace', async () => {
70
+ const folders = vscode.workspace.workspaceFolders;
71
+ if (!folders || folders.length === 0) {
72
+ vscode.window.showErrorMessage('pompelmi: No workspace folder open.');
73
+ return;
74
+ }
75
+
76
+ const args = getScanOptions(vscode);
77
+ const dirPath = folders[0].uri.fsPath;
78
+
79
+ vscode.window.withProgress(
80
+ { location: vscode.ProgressLocation.Notification, title: 'pompelmi: scanning workspace...' },
81
+ () => new Promise(resolve => {
82
+ runScan(dirPath, args, (err, result) => {
83
+ resolve();
84
+ if (err) {
85
+ vscode.window.showErrorMessage(`pompelmi error: ${err.message}`);
86
+ return;
87
+ }
88
+ const infected = result?.infected ?? 0;
89
+ const scanned = result?.scanned ?? 0;
90
+ if (infected === 0) {
91
+ vscode.window.showInformationMessage(`✅ Workspace scan complete — ${scanned} files scanned, all clean.`);
92
+ } else {
93
+ vscode.window.showErrorMessage(`🚨 Workspace scan — ${infected} infected file(s) found out of ${scanned} scanned.`);
94
+ }
95
+ });
96
+ })
97
+ );
98
+ });
99
+
100
+ const configureCmd = vscode.commands.registerCommand('pompelmi.configure', () => {
101
+ vscode.commands.executeCommand('workbench.action.openSettings', 'pompelmi');
102
+ });
103
+
104
+ context.subscriptions.push(scanFileCmd, scanWorkspaceCmd, configureCmd);
105
+ }
106
+
107
+ function deactivate() {}
108
+
109
+ module.exports = { activate, deactivate };