@lateos/npm-scan 0.18.3 → 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.
- package/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +138 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +147 -0
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
package/backend/pdf.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
|
2
2
|
|
|
3
3
|
const SEV_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
4
|
-
const SEV_COLORS = {
|
|
4
|
+
const SEV_COLORS = {
|
|
5
|
+
critical: rgb(0.8, 0.2, 0.2),
|
|
6
|
+
high: rgb(0.75, 0.15, 0.15),
|
|
7
|
+
medium: rgb(0.9, 0.5, 0.1),
|
|
8
|
+
low: rgb(0.8, 0.7, 0.1),
|
|
9
|
+
};
|
|
5
10
|
|
|
6
11
|
const NIST_SR_MAP = {
|
|
7
12
|
'ATK-001': { control: 'SR-3.1', title: 'Malicious code detection' },
|
|
@@ -29,24 +34,34 @@ function wrapText(text, font, size, maxWidth) {
|
|
|
29
34
|
for (const word of words) {
|
|
30
35
|
const test = current ? current + ' ' + word : word;
|
|
31
36
|
if (font.widthOfTextAtSize(test, size) > maxWidth) {
|
|
32
|
-
if (current)
|
|
37
|
+
if (current) {
|
|
38
|
+
lines.push(current);
|
|
39
|
+
}
|
|
33
40
|
current = word;
|
|
34
41
|
} else {
|
|
35
42
|
current = test;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
|
-
if (current)
|
|
45
|
+
if (current) {
|
|
46
|
+
lines.push(current);
|
|
47
|
+
}
|
|
39
48
|
return lines;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
function
|
|
51
|
+
function _drawTableRow(page, font, columns, y, colWidths, fontSize, _isHeader) {
|
|
43
52
|
let x = MARGIN;
|
|
44
53
|
const rowH = fontSize + 6;
|
|
45
54
|
for (let i = 0; i < columns.length; i++) {
|
|
46
55
|
const text = columns[i];
|
|
47
56
|
const lines = wrapText(text, font, fontSize, colWidths[i] - 4);
|
|
48
57
|
for (let j = 0; j < lines.length; j++) {
|
|
49
|
-
page.drawText(lines[j], {
|
|
58
|
+
page.drawText(lines[j], {
|
|
59
|
+
x: x + 2,
|
|
60
|
+
y: y - j * fontSize - 2,
|
|
61
|
+
size: fontSize,
|
|
62
|
+
font,
|
|
63
|
+
color: rgb(0, 0, 0),
|
|
64
|
+
});
|
|
50
65
|
}
|
|
51
66
|
x += colWidths[i];
|
|
52
67
|
}
|
|
@@ -55,7 +70,12 @@ function drawTableRow(page, font, columns, y, colWidths, fontSize, isHeader) {
|
|
|
55
70
|
|
|
56
71
|
function drawPageHeader(page, font, text, y) {
|
|
57
72
|
page.drawText(text, { x: MARGIN, y, size: 14, font, color: rgb(0.2, 0.2, 0.2) });
|
|
58
|
-
page.drawLine({
|
|
73
|
+
page.drawLine({
|
|
74
|
+
start: { x: MARGIN, y: y - 4 },
|
|
75
|
+
end: { x: PAGE_W - MARGIN, y: y - 4 },
|
|
76
|
+
thickness: 1,
|
|
77
|
+
color: rgb(0.7, 0.7, 0.7),
|
|
78
|
+
});
|
|
59
79
|
return y - 20;
|
|
60
80
|
}
|
|
61
81
|
|
|
@@ -68,8 +88,10 @@ export async function generatePDF(scans) {
|
|
|
68
88
|
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
69
89
|
let totalFindings = 0;
|
|
70
90
|
for (const s of scans) {
|
|
71
|
-
for (const f of
|
|
72
|
-
if (sevCounts[f.severity] !== undefined)
|
|
91
|
+
for (const f of s.findings || []) {
|
|
92
|
+
if (sevCounts[f.severity] !== undefined) {
|
|
93
|
+
sevCounts[f.severity]++;
|
|
94
|
+
}
|
|
73
95
|
totalFindings++;
|
|
74
96
|
}
|
|
75
97
|
}
|
|
@@ -80,20 +102,41 @@ export async function generatePDF(scans) {
|
|
|
80
102
|
|
|
81
103
|
page.drawText('npm-scan Report', { x: MARGIN, y, size: 24, font: boldFont, color: rgb(0, 0, 0) });
|
|
82
104
|
y -= 30;
|
|
83
|
-
page.drawText(`Generated: ${new Date().toISOString()}`, {
|
|
105
|
+
page.drawText(`Generated: ${new Date().toISOString()}`, {
|
|
106
|
+
x: MARGIN,
|
|
107
|
+
y,
|
|
108
|
+
size: 10,
|
|
109
|
+
font,
|
|
110
|
+
color: rgb(0.4, 0.4, 0.4),
|
|
111
|
+
});
|
|
84
112
|
y -= 14;
|
|
85
|
-
page.drawText(
|
|
113
|
+
page.drawText(
|
|
114
|
+
`Version: ${version} | Packages scanned: ${scans.length} | Total findings: ${totalFindings}`,
|
|
115
|
+
{ x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) }
|
|
116
|
+
);
|
|
86
117
|
y -= 30;
|
|
87
118
|
|
|
88
119
|
// Severity summary
|
|
89
|
-
page.drawText('Severity Summary', {
|
|
120
|
+
page.drawText('Severity Summary', {
|
|
121
|
+
x: MARGIN,
|
|
122
|
+
y,
|
|
123
|
+
size: 14,
|
|
124
|
+
font: boldFont,
|
|
125
|
+
color: rgb(0, 0, 0),
|
|
126
|
+
});
|
|
90
127
|
y -= 20;
|
|
91
128
|
|
|
92
129
|
for (const sev of SEV_ORDER) {
|
|
93
130
|
const count = sevCounts[sev] || 0;
|
|
94
131
|
const color = SEV_COLORS[sev] || rgb(0, 0, 0);
|
|
95
132
|
page.drawCircle({ x: MARGIN + 6, y: y - 4, size: 4, color });
|
|
96
|
-
page.drawText(`${sev}: ${count}`, {
|
|
133
|
+
page.drawText(`${sev}: ${count}`, {
|
|
134
|
+
x: MARGIN + 16,
|
|
135
|
+
y: y - 8,
|
|
136
|
+
size: 11,
|
|
137
|
+
font,
|
|
138
|
+
color: rgb(0, 0, 0),
|
|
139
|
+
});
|
|
97
140
|
y -= 18;
|
|
98
141
|
}
|
|
99
142
|
|
|
@@ -102,15 +145,33 @@ export async function generatePDF(scans) {
|
|
|
102
145
|
// Per-package summary
|
|
103
146
|
for (const s of scans) {
|
|
104
147
|
const findings = s.findings || [];
|
|
105
|
-
if (y < MARGIN + 60) {
|
|
148
|
+
if (y < MARGIN + 60) {
|
|
149
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
150
|
+
y = PAGE_H - MARGIN;
|
|
151
|
+
}
|
|
106
152
|
|
|
107
|
-
page.drawText(`${s.package_name}@${s.version || 'unknown'}`, {
|
|
153
|
+
page.drawText(`${s.package_name}@${s.version || 'unknown'}`, {
|
|
154
|
+
x: MARGIN,
|
|
155
|
+
y,
|
|
156
|
+
size: 12,
|
|
157
|
+
font: boldFont,
|
|
158
|
+
color: rgb(0, 0, 0),
|
|
159
|
+
});
|
|
108
160
|
y -= 16;
|
|
109
|
-
page.drawText(` ${findings.length} findings`, {
|
|
161
|
+
page.drawText(` ${findings.length} findings`, {
|
|
162
|
+
x: MARGIN,
|
|
163
|
+
y,
|
|
164
|
+
size: 10,
|
|
165
|
+
font,
|
|
166
|
+
color: rgb(0.4, 0.4, 0.4),
|
|
167
|
+
});
|
|
110
168
|
y -= 14;
|
|
111
169
|
|
|
112
170
|
for (const f of findings) {
|
|
113
|
-
if (y < MARGIN + 20) {
|
|
171
|
+
if (y < MARGIN + 20) {
|
|
172
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
173
|
+
y = PAGE_H - MARGIN;
|
|
174
|
+
}
|
|
114
175
|
const sevColor = SEV_COLORS[f.severity] || rgb(0, 0, 0);
|
|
115
176
|
page.drawCircle({ x: MARGIN + 3, y: y + 2, size: 3, color: sevColor });
|
|
116
177
|
const line = `${f.atk_id || f.id} ${f.severity} ${(f.description || f.title || '').slice(0, 70)}`;
|
|
@@ -136,8 +197,8 @@ export async function generatePDF(scans) {
|
|
|
136
197
|
}
|
|
137
198
|
y -= 16;
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
for (const f of
|
|
200
|
+
for (const s of scans) {
|
|
201
|
+
for (const f of s.findings || []) {
|
|
141
202
|
if (y < MARGIN + 20) {
|
|
142
203
|
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
143
204
|
y = PAGE_H - MARGIN;
|
|
@@ -156,10 +217,12 @@ export async function generatePDF(scans) {
|
|
|
156
217
|
let maxLines = 1;
|
|
157
218
|
for (let i = 0; i < rowData.length; i++) {
|
|
158
219
|
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
159
|
-
if (lines.length > maxLines)
|
|
220
|
+
if (lines.length > maxLines) {
|
|
221
|
+
maxLines = lines.length;
|
|
222
|
+
}
|
|
160
223
|
}
|
|
161
224
|
|
|
162
|
-
if (y -
|
|
225
|
+
if (y - maxLines * 11 < MARGIN) {
|
|
163
226
|
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
164
227
|
y = PAGE_H - MARGIN;
|
|
165
228
|
y = drawPageHeader(page, boldFont, 'All Findings (continued)', y);
|
|
@@ -172,14 +235,19 @@ export async function generatePDF(scans) {
|
|
|
172
235
|
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
173
236
|
for (let j = 0; j < lines.length; j++) {
|
|
174
237
|
const color = i === 1 && SEV_COLORS[f.severity] ? SEV_COLORS[f.severity] : rgb(0, 0, 0);
|
|
175
|
-
page.drawText(lines[j], { x: x + 2, y: rowY -
|
|
238
|
+
page.drawText(lines[j], { x: x + 2, y: rowY - j * 11 - 2, size: 9, font, color });
|
|
176
239
|
}
|
|
177
240
|
x += colWidths[i];
|
|
178
241
|
}
|
|
179
242
|
|
|
180
243
|
const lineY = rowY + 2;
|
|
181
|
-
page.drawLine({
|
|
182
|
-
|
|
244
|
+
page.drawLine({
|
|
245
|
+
start: { x: MARGIN, y: lineY },
|
|
246
|
+
end: { x: PAGE_W - MARGIN, y: lineY },
|
|
247
|
+
thickness: 0.5,
|
|
248
|
+
color: rgb(0.85, 0.85, 0.85),
|
|
249
|
+
});
|
|
250
|
+
y = rowY - maxLines * 11 - 4;
|
|
183
251
|
}
|
|
184
252
|
}
|
|
185
253
|
|
|
@@ -200,9 +268,11 @@ export async function generatePDF(scans) {
|
|
|
200
268
|
|
|
201
269
|
const atkMap = {};
|
|
202
270
|
for (const s of scans) {
|
|
203
|
-
for (const f of
|
|
271
|
+
for (const f of s.findings || []) {
|
|
204
272
|
const key = f.atk_id || f.id;
|
|
205
|
-
if (!atkMap[key])
|
|
273
|
+
if (!atkMap[key]) {
|
|
274
|
+
atkMap[key] = [];
|
|
275
|
+
}
|
|
206
276
|
atkMap[key].push(f);
|
|
207
277
|
}
|
|
208
278
|
}
|
|
@@ -228,16 +298,25 @@ export async function generatePDF(scans) {
|
|
|
228
298
|
x += rowWidths[i];
|
|
229
299
|
}
|
|
230
300
|
|
|
231
|
-
page.drawLine({
|
|
301
|
+
page.drawLine({
|
|
302
|
+
start: { x: MARGIN, y: y + 4 },
|
|
303
|
+
end: { x: PAGE_W - MARGIN, y: y + 4 },
|
|
304
|
+
thickness: 0.5,
|
|
305
|
+
color: rgb(0.85, 0.85, 0.85),
|
|
306
|
+
});
|
|
232
307
|
y -= 18;
|
|
233
308
|
}
|
|
234
309
|
|
|
235
310
|
// Footer
|
|
236
311
|
const pages = doc.getPages();
|
|
237
312
|
for (const p of pages) {
|
|
238
|
-
const { width } = p.getSize();
|
|
313
|
+
const { width: _width } = p.getSize();
|
|
239
314
|
p.drawText(`npm-scan v${version} | Apache-2.0 + Commons Clause`, {
|
|
240
|
-
x: MARGIN,
|
|
315
|
+
x: MARGIN,
|
|
316
|
+
y: 20,
|
|
317
|
+
size: 8,
|
|
318
|
+
font,
|
|
319
|
+
color: rgb(0.6, 0.6, 0.6),
|
|
241
320
|
});
|
|
242
321
|
}
|
|
243
322
|
|
package/backend/policy.js
CHANGED
|
@@ -5,26 +5,89 @@ const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical'];
|
|
|
5
5
|
const VALID_SEVERITIES = new Set(SEVERITY_ORDER);
|
|
6
6
|
|
|
7
7
|
const KNOWN_REPUTABLE_PACKAGES = new Set([
|
|
8
|
-
'react',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
8
|
+
'react',
|
|
9
|
+
'react-dom',
|
|
10
|
+
'vue',
|
|
11
|
+
'angular',
|
|
12
|
+
'next',
|
|
13
|
+
'nuxt',
|
|
14
|
+
'express',
|
|
15
|
+
'fastify',
|
|
16
|
+
'hono',
|
|
17
|
+
'koa',
|
|
18
|
+
'connect',
|
|
19
|
+
'webpack',
|
|
20
|
+
'vite',
|
|
21
|
+
'rollup',
|
|
22
|
+
'esbuild',
|
|
23
|
+
'typescript',
|
|
24
|
+
'babel-core',
|
|
25
|
+
'lodash',
|
|
26
|
+
'ramda',
|
|
27
|
+
'underscore',
|
|
28
|
+
'axios',
|
|
29
|
+
'node-fetch',
|
|
30
|
+
'got',
|
|
31
|
+
'superagent',
|
|
32
|
+
'sequelize',
|
|
33
|
+
'prisma',
|
|
34
|
+
'typeorm',
|
|
35
|
+
'mongoose',
|
|
36
|
+
'jest',
|
|
37
|
+
'mocha',
|
|
38
|
+
'vitest',
|
|
39
|
+
'ava',
|
|
40
|
+
'prettier',
|
|
41
|
+
'eslint',
|
|
42
|
+
'stylelint',
|
|
43
|
+
'socket.io',
|
|
44
|
+
'ws',
|
|
45
|
+
'rimraf',
|
|
46
|
+
'glob',
|
|
47
|
+
'minimatch',
|
|
48
|
+
'fs-extra',
|
|
49
|
+
'electron',
|
|
50
|
+
'puppeteer',
|
|
51
|
+
'playwright',
|
|
52
|
+
'sharp',
|
|
53
|
+
'node-canvas',
|
|
54
|
+
'ffmpeg-static',
|
|
55
|
+
'turbo',
|
|
56
|
+
'react-scripts',
|
|
57
|
+
'@angular/cli',
|
|
58
|
+
'gatsby',
|
|
59
|
+
'parcel',
|
|
60
|
+
'tslib',
|
|
61
|
+
'core-js',
|
|
62
|
+
'regenerator-runtime',
|
|
63
|
+
'buffer',
|
|
64
|
+
'node-gyp',
|
|
65
|
+
'node-pre-gyp',
|
|
66
|
+
'winston',
|
|
67
|
+
'uuid',
|
|
68
|
+
'moment',
|
|
69
|
+
'dotenv',
|
|
70
|
+
'pg',
|
|
71
|
+
'semver',
|
|
72
|
+
'redux',
|
|
73
|
+
'redis',
|
|
74
|
+
'dayjs',
|
|
75
|
+
'luxon',
|
|
76
|
+
'chalk',
|
|
77
|
+
'debug',
|
|
78
|
+
'cors',
|
|
79
|
+
'helmet',
|
|
80
|
+
'multer',
|
|
81
|
+
'body-parser',
|
|
82
|
+
'cheerio',
|
|
83
|
+
'bluebird',
|
|
84
|
+
'bcrypt',
|
|
85
|
+
'commander',
|
|
86
|
+
'yargs',
|
|
87
|
+
'passport',
|
|
88
|
+
'jsonwebtoken',
|
|
89
|
+
'nodemailer',
|
|
90
|
+
'class-validator',
|
|
28
91
|
]);
|
|
29
92
|
|
|
30
93
|
function severityIndex(s) {
|
|
@@ -32,8 +95,12 @@ function severityIndex(s) {
|
|
|
32
95
|
}
|
|
33
96
|
|
|
34
97
|
function matchesFilePath(filePath, pattern) {
|
|
35
|
-
if (!pattern)
|
|
36
|
-
|
|
98
|
+
if (!pattern) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (pattern === '*') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
37
104
|
const regexPattern = pattern
|
|
38
105
|
.replace(/\./g, '\\.')
|
|
39
106
|
.replace(/\*\*/g, '___DOUBLE_STAR___')
|
|
@@ -44,49 +111,90 @@ function matchesFilePath(filePath, pattern) {
|
|
|
44
111
|
|
|
45
112
|
function matchesContext(finding, rule) {
|
|
46
113
|
const ctx = finding.context;
|
|
47
|
-
if (!ctx)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (rule.context?.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (rule.context?.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
if (!ctx) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (rule.context?.is_dist_build === true && !ctx.is_dist_build) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (rule.context?.is_dist_build === false && ctx.is_dist_build) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
59
146
|
if (rule.context?.url_domain) {
|
|
60
|
-
if (!ctx.url_domain)
|
|
147
|
+
if (!ctx.url_domain) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
61
150
|
const domainPattern = rule.context.url_domain.replace(/\*/g, '.*');
|
|
62
|
-
if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain))
|
|
151
|
+
if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
63
154
|
}
|
|
64
155
|
|
|
65
156
|
return true;
|
|
66
157
|
}
|
|
67
158
|
|
|
68
159
|
function matchesKnownReputable(packageName) {
|
|
69
|
-
if (KNOWN_REPUTABLE_PACKAGES.has(packageName))
|
|
160
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
70
163
|
const [scope, name] = packageName.split('/');
|
|
71
|
-
if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`))
|
|
164
|
+
if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
72
167
|
return false;
|
|
73
168
|
}
|
|
74
169
|
|
|
75
170
|
function getPackageReputationTier(pkgName) {
|
|
76
171
|
const name = pkgName?.replace(/^@/, '').replace(/\/.*/, '') || '';
|
|
77
|
-
if (matchesKnownReputable(name))
|
|
172
|
+
if (matchesKnownReputable(name)) {
|
|
173
|
+
return 'trusted';
|
|
174
|
+
}
|
|
78
175
|
return 'unknown';
|
|
79
176
|
}
|
|
80
177
|
|
|
81
178
|
function matchesSuppressRule(finding, pkgName, rule) {
|
|
82
|
-
if (rule.atk_id !== (finding.atk_id || finding.id))
|
|
83
|
-
|
|
179
|
+
if (rule.atk_id !== (finding.atk_id || finding.id)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (rule.package && rule.package !== '*' && rule.package !== pkgName) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
84
185
|
|
|
85
|
-
if (rule.context && !matchesContext(finding, rule))
|
|
186
|
+
if (rule.context && !matchesContext(finding, rule)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
86
189
|
|
|
87
190
|
if (rule.reputation_tier) {
|
|
88
191
|
const tier = getPackageReputationTier(pkgName);
|
|
89
|
-
if (
|
|
192
|
+
if (
|
|
193
|
+
rule.reputation_tier !== tier &&
|
|
194
|
+
!(rule.reputation_tier === '*' || rule.reputation_tier === 'any')
|
|
195
|
+
) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
90
198
|
}
|
|
91
199
|
|
|
92
200
|
return true;
|
|
@@ -109,13 +217,17 @@ function loadPolicy(path) {
|
|
|
109
217
|
if (policy.severity_overrides) {
|
|
110
218
|
for (const [atkId, severity] of Object.entries(policy.severity_overrides)) {
|
|
111
219
|
if (!VALID_SEVERITIES.has(severity)) {
|
|
112
|
-
throw new Error(
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`
|
|
222
|
+
);
|
|
113
223
|
}
|
|
114
224
|
}
|
|
115
225
|
}
|
|
116
226
|
|
|
117
227
|
if (policy.fail_on && !VALID_SEVERITIES.has(policy.fail_on)) {
|
|
118
|
-
throw new Error(
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`
|
|
230
|
+
);
|
|
119
231
|
}
|
|
120
232
|
|
|
121
233
|
if (policy.suppress) {
|
|
@@ -143,7 +255,7 @@ function sanitizePolicy(policy) {
|
|
|
143
255
|
allow: { packages: policy.allow?.packages ?? [] },
|
|
144
256
|
severity_overrides: policy.severity_overrides ?? {},
|
|
145
257
|
fail_on: policy.fail_on ?? 'none',
|
|
146
|
-
suppress: (policy.suppress ?? []).map(r => ({
|
|
258
|
+
suppress: (policy.suppress ?? []).map((r) => ({
|
|
147
259
|
atk_id: r.atk_id,
|
|
148
260
|
package: r.package || '*',
|
|
149
261
|
reason: r.reason || '',
|
|
@@ -154,23 +266,29 @@ function sanitizePolicy(policy) {
|
|
|
154
266
|
}
|
|
155
267
|
|
|
156
268
|
function isAllowed(packageName, policy) {
|
|
157
|
-
if (!policy.allow.packages.length)
|
|
269
|
+
if (!policy.allow.packages.length) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
158
272
|
const nameOnly = packageName.split('@')[0];
|
|
159
|
-
return policy.allow.packages.some(p => p === packageName || p === nameOnly);
|
|
273
|
+
return policy.allow.packages.some((p) => p === packageName || p === nameOnly);
|
|
160
274
|
}
|
|
161
275
|
|
|
162
276
|
function applyPolicy(findings, packageName, policy) {
|
|
163
277
|
let filtered = [...findings];
|
|
164
278
|
|
|
165
279
|
if (policy.suppress.length) {
|
|
166
|
-
filtered = filtered.filter(f => {
|
|
167
|
-
if (f.context?.is_lifecycle_hook)
|
|
168
|
-
|
|
169
|
-
|
|
280
|
+
filtered = filtered.filter((f) => {
|
|
281
|
+
if (f.context?.is_lifecycle_hook) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (f.context?.is_multi_layer) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return !policy.suppress.some((r) => matchesSuppressRule(f, packageName, r));
|
|
170
288
|
});
|
|
171
289
|
}
|
|
172
290
|
|
|
173
|
-
filtered = filtered.map(f => {
|
|
291
|
+
filtered = filtered.map((f) => {
|
|
174
292
|
const override = policy.severity_overrides[f.atk_id || f.id];
|
|
175
293
|
if (override) {
|
|
176
294
|
return { ...f, severity: override, _severityOverridden: true };
|
|
@@ -184,10 +302,19 @@ function applyPolicy(findings, packageName, policy) {
|
|
|
184
302
|
}
|
|
185
303
|
|
|
186
304
|
function checkFailOn(findings, policy) {
|
|
187
|
-
if (policy.fail_on === 'none')
|
|
305
|
+
if (policy.fail_on === 'none') {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
188
308
|
|
|
189
309
|
const threshold = severityIndex(policy.fail_on);
|
|
190
|
-
return findings.some(f => severityIndex(f.severity) >= threshold);
|
|
310
|
+
return findings.some((f) => severityIndex(f.severity) >= threshold);
|
|
191
311
|
}
|
|
192
312
|
|
|
193
|
-
export {
|
|
313
|
+
export {
|
|
314
|
+
loadPolicy,
|
|
315
|
+
applyPolicy,
|
|
316
|
+
isAllowed,
|
|
317
|
+
getPackageReputationTier,
|
|
318
|
+
matchesContext,
|
|
319
|
+
KNOWN_REPUTABLE_PACKAGES,
|
|
320
|
+
};
|
package/backend/provenance.js
CHANGED
|
@@ -12,7 +12,13 @@ export function signManifest(manifest, key = HMAC_KEY) {
|
|
|
12
12
|
return createHmac('sha256', key).update(JSON.stringify(manifest)).digest('hex');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function buildDetectionRule({
|
|
15
|
+
export function buildDetectionRule({
|
|
16
|
+
ruleId,
|
|
17
|
+
ruleName,
|
|
18
|
+
severity,
|
|
19
|
+
cveReferences = [],
|
|
20
|
+
campaignName,
|
|
21
|
+
}) {
|
|
16
22
|
return {
|
|
17
23
|
rule_id: ruleId,
|
|
18
24
|
rule_name: ruleName,
|
|
@@ -41,7 +47,12 @@ export function buildDetectionResult({ triggered, severity, indicators = [] }) {
|
|
|
41
47
|
|
|
42
48
|
export function buildAuditTrail({ detectionLogic, ruleProvenanceUrl, campaignSourceUrl }) {
|
|
43
49
|
const contentHash = hashContent(detectionLogic);
|
|
44
|
-
const manifest = {
|
|
50
|
+
const manifest = {
|
|
51
|
+
contentHash,
|
|
52
|
+
ruleProvenanceUrl,
|
|
53
|
+
campaignSourceUrl,
|
|
54
|
+
generatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
45
56
|
return {
|
|
46
57
|
content_hash: contentHash,
|
|
47
58
|
rule_provenance_url: ruleProvenanceUrl,
|
|
@@ -60,7 +71,21 @@ export function buildDetectionRecord({ rule, scanMetadata, detectionResult, audi
|
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
export function attachProvenance(
|
|
74
|
+
export function attachProvenance(
|
|
75
|
+
evidence,
|
|
76
|
+
{
|
|
77
|
+
ruleId,
|
|
78
|
+
ruleName,
|
|
79
|
+
severity,
|
|
80
|
+
campaignName,
|
|
81
|
+
pkgName,
|
|
82
|
+
pkgVersion,
|
|
83
|
+
triggered,
|
|
84
|
+
indicators,
|
|
85
|
+
ruleProvenanceUrl,
|
|
86
|
+
campaignSourceUrl,
|
|
87
|
+
}
|
|
88
|
+
) {
|
|
64
89
|
const rule = buildDetectionRule({ ruleId, ruleName, severity, campaignName });
|
|
65
90
|
const scanMetadata = buildScanMetadata({
|
|
66
91
|
scannerVersion: '@lateos/npm-scan',
|