pompelmi 1.7.0 → 1.8.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/BADGE.md ADDED
@@ -0,0 +1,29 @@
1
+ # Scanned by pompelmi — Badge
2
+
3
+ Add this badge to your repository's `README.md` to show that your file uploads are scanned with pompelmi.
4
+
5
+ ## Markdown
6
+
7
+ ```markdown
8
+ [![Scanned by pompelmi](https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=github)](https://github.com/pompelmi/pompelmi)
9
+ ```
10
+
11
+ ## HTML
12
+
13
+ ```html
14
+ <a href="https://github.com/pompelmi/pompelmi">
15
+ <img src="https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=github" alt="Scanned by pompelmi">
16
+ </a>
17
+ ```
18
+
19
+ ## RST (reStructuredText)
20
+
21
+ ```rst
22
+ .. image:: https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=github
23
+ :target: https://github.com/pompelmi/pompelmi
24
+ :alt: Scanned by pompelmi
25
+ ```
26
+
27
+ ## Preview
28
+
29
+ [![Scanned by pompelmi](https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=github)](https://github.com/pompelmi/pompelmi)
package/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  <img src="https://img.shields.io/badge/license-ISC-blue" alt="license">
16
16
  <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>
17
17
  <a href="https://github.com/pompelmi/pompelmi/actions/workflows/release.yml"><img src="https://github.com/pompelmi/pompelmi/actions/workflows/release.yml/badge.svg" alt="npm publish"></a>
18
+ <a href="https://github.com/pompelmi/pompelmi"><img src="https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=github" alt="Scanned by pompelmi"></a>
18
19
  </p>
19
20
 
20
21
  ---
package/action/Dockerfile CHANGED
@@ -1,24 +1,18 @@
1
1
  FROM node:20-slim
2
2
 
3
- ENV DEBIAN_FRONTEND=noninteractive
4
-
5
3
  RUN apt-get update && \
6
- apt-get install -y --no-install-recommends \
7
- clamav \
8
- clamav-freshclam \
9
- && rm -rf /var/lib/apt/lists/* \
10
- && sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf \
11
- && mkdir -p /var/lib/clamav /run/clamav
4
+ apt-get install -y --no-install-recommends clamav clamav-freshclam && \
5
+ rm -rf /var/lib/apt/lists/* && \
6
+ sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf && \
7
+ mkdir -p /var/lib/clamav /run/clamav
12
8
 
13
9
  WORKDIR /action
14
10
 
15
- # Bundle the pompelmi library from this repository
16
- COPY src/ ./src/
17
- COPY package.json ./
11
+ RUN npm install pompelmi
12
+
13
+ COPY entrypoint.sh ./entrypoint.sh
14
+ COPY scanner.js ./scanner.js
18
15
 
19
- # Action scripts
20
- COPY action/entrypoint.sh ./entrypoint.sh
21
- COPY action/scanner.js ./scanner.js
22
16
  RUN chmod +x ./entrypoint.sh
23
17
 
24
18
  ENTRYPOINT ["/action/entrypoint.sh"]
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "pompelmi-action",
3
+ "version": "1.8.0",
4
+ "private": true,
5
+ "description": "GitHub Action entrypoint for pompelmi ClamAV scanner",
6
+ "main": "scanner.js",
7
+ "scripts": {},
8
+ "dependencies": {
9
+ "@actions/artifact": "^2.1.11"
10
+ }
11
+ }
package/action/scanner.js CHANGED
@@ -2,11 +2,82 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { scan, scanDirectory, Verdict } = require('./src/index.js');
5
+ const { scan, scanDirectory, Verdict } = require('pompelmi');
6
6
 
7
7
  const scanPath = process.argv[2] || '.';
8
8
  const failOnVirus = process.argv[3] !== 'false';
9
9
 
10
+ async function writeReport(clean, malicious, errors, outputDir) {
11
+ const total = clean.length + malicious.length + errors.length;
12
+ const status = malicious.length > 0 ? 'infected' : 'clean';
13
+
14
+ const rows = [
15
+ ...clean.map(f => ({ file: f, status: 'clean', verdict: 'Clean' })),
16
+ ...malicious.map(f => ({ file: f, status: 'infected', verdict: 'Malicious' })),
17
+ ...errors.map(f => ({ file: f, status: 'error', verdict: 'ScanError' })),
18
+ ];
19
+
20
+ const json = JSON.stringify({ status, total, clean: clean.length, malicious: malicious.length, errors: errors.length, files: rows }, null, 2);
21
+ fs.writeFileSync(path.join(outputDir, 'report.json'), json);
22
+
23
+ const tableRows = rows.map(r => {
24
+ const color = r.status === 'clean' ? '#22c55e' : r.status === 'infected' ? '#ef4444' : '#f59e0b';
25
+ return `<tr><td>${escHtml(r.file)}</td><td style="color:${color};font-weight:bold">${r.verdict}</td></tr>`;
26
+ }).join('\n');
27
+
28
+ const html = `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="utf-8">
32
+ <title>Pompelmi Scan Report</title>
33
+ <style>
34
+ body { font-family: sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
35
+ h1 { font-size: 1.5rem; }
36
+ .summary { display: flex; gap: 24px; margin: 16px 0; }
37
+ .stat { background: #f1f5f9; border-radius: 8px; padding: 12px 20px; text-align: center; }
38
+ .stat span { display: block; font-size: 1.8rem; font-weight: bold; }
39
+ table { width: 100%; border-collapse: collapse; margin-top: 24px; }
40
+ th { background: #0f172a; color: #fff; text-align: left; padding: 8px 12px; }
41
+ td { padding: 8px 12px; border-bottom: 1px solid #e2e8f0; word-break: break-all; }
42
+ tr:hover td { background: #f8fafc; }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <h1>${status === 'clean' ? '✅' : '❌'} Pompelmi Scan Report</h1>
47
+ <div class="summary">
48
+ <div class="stat"><span>${total}</span>Total</div>
49
+ <div class="stat"><span style="color:#22c55e">${clean.length}</span>Clean</div>
50
+ <div class="stat"><span style="color:#ef4444">${malicious.length}</span>Infected</div>
51
+ <div class="stat"><span style="color:#f59e0b">${errors.length}</span>Errors</div>
52
+ </div>
53
+ <table>
54
+ <thead><tr><th>File</th><th>Verdict</th></tr></thead>
55
+ <tbody>
56
+ ${tableRows}
57
+ </tbody>
58
+ </table>
59
+ </body>
60
+ </html>`;
61
+ fs.writeFileSync(path.join(outputDir, 'report.html'), html);
62
+
63
+ return { jsonPath: path.join(outputDir, 'report.json'), htmlPath: path.join(outputDir, 'report.html') };
64
+ }
65
+
66
+ function escHtml(s) {
67
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
68
+ }
69
+
70
+ async function uploadArtifact(jsonPath, htmlPath) {
71
+ try {
72
+ const { DefaultArtifactClient } = require('@actions/artifact');
73
+ const client = new DefaultArtifactClient();
74
+ await client.uploadArtifact('pompelmi-scan-report', [jsonPath, htmlPath], path.dirname(jsonPath));
75
+ console.log('Scan report artifact uploaded: pompelmi-scan-report');
76
+ } catch (err) {
77
+ console.warn(`Could not upload artifact (not running in Actions?): ${err.message}`);
78
+ }
79
+ }
80
+
10
81
  async function main() {
11
82
  const resolved = path.resolve(scanPath);
12
83
 
@@ -66,6 +137,11 @@ async function main() {
66
137
  fs.appendFileSync(summaryFile, rows.join('\n') + '\n');
67
138
  }
68
139
 
140
+ // --- Report artifact ---
141
+ const reportDir = process.env.RUNNER_TEMP || '/tmp';
142
+ const { jsonPath, htmlPath } = await writeReport(clean, malicious, errors, reportDir);
143
+ await uploadArtifact(jsonPath, htmlPath);
144
+
69
145
  // --- Console ---
70
146
  console.log(`\nScan complete — ${total} file(s) scanned`);
71
147
  console.log(` Clean: ${clean.length}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { scan, scanBuffer, scanStream, scanDirectory } = require('./ClamAVScanner.js');
2
2
  const { Verdict } = require('./verdicts.js');
3
+ const { middleware } = require('./middleware.js');
3
4
 
4
- module.exports = { scan, scanBuffer, scanStream, scanDirectory, Verdict };
5
+ module.exports = { scan, scanBuffer, scanStream, scanDirectory, Verdict, middleware };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { scanBuffer, Verdict } = require('./ClamAVScanner.js');
4
+
5
+ function middleware(opts = {}) {
6
+ const field = opts.uploadField || 'file';
7
+
8
+ return async function pompelmiMiddleware(req, res, next) {
9
+ let buffers = [];
10
+
11
+ if (req.file) {
12
+ buffers = [req.file.buffer];
13
+ } else if (req.files) {
14
+ const files = Array.isArray(req.files)
15
+ ? req.files
16
+ : (req.files[field] || Object.values(req.files).flat());
17
+ buffers = files.map(f => f.buffer).filter(Boolean);
18
+ }
19
+
20
+ if (buffers.length === 0) return next();
21
+
22
+ try {
23
+ for (const buf of buffers) {
24
+ const verdict = await scanBuffer(buf, opts);
25
+ if (verdict === Verdict.Malicious) {
26
+ return res.status(403).json({ error: 'Malicious file detected' });
27
+ }
28
+ }
29
+ next();
30
+ } catch (err) {
31
+ next(err);
32
+ }
33
+ };
34
+ }
35
+
36
+ module.exports = { middleware };
package/pr_info.tmp DELETED
@@ -1,2 +0,0 @@
1
- {"branch":null,"number":null,"url":null}
2
- EOF