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 +29 -0
- package/README.md +1 -0
- package/action/Dockerfile +8 -14
- package/action/package.json +11 -0
- package/action/scanner.js +77 -1
- package/package.json +1 -1
- package/src/index.js +2 -1
- package/src/middleware.js +36 -0
- package/pr_info.tmp +0 -2
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
|
+
[](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
|
+
[](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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
COPY
|
|
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"]
|
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('
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
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