@lateos/npm-scan 0.18.2 → 1.0.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 +265 -233
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +861 -826
- package/README.zh.md +708 -708
- package/VALIDATION.md +92 -0
- package/backend/cra.js +68 -68
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/config/thresholds.js +66 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +87 -81
- package/backend/detectors/lib/ast-patterns.js +21 -0
- package/backend/detectors/lib/entropy-analyzer.js +24 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-binary-embed.js +34 -5
- package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-version-anomaly.js +187 -0
- package/backend/detectors.test.js +88 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/scripts/analyze-false-positives.js +146 -0
- package/backend/scripts/analyze-validation.js +151 -0
- package/backend/scripts/detect-false-positives.js +93 -0
- package/backend/scripts/fetch-top-packages.js +129 -0
- package/backend/scripts/validate-detectors.js +142 -0
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/tests-d5-enhanced.test.js +46 -0
- package/backend/tests-d6-version-anomaly.test.js +58 -0
- package/backend/tests-d6.test.js +116 -0
- package/backend/tests-d6c.test.js +106 -0
- package/backend/tests-d7-obfuscation.test.js +91 -0
- package/backend/tests.test.js +898 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/package.json +74 -57
- 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,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function analyzeFalsePositives(fpFile) {
|
|
6
|
+
const analysis = {
|
|
7
|
+
total_fps: 0,
|
|
8
|
+
scanned_packages: 0,
|
|
9
|
+
fp_rate: '0%',
|
|
10
|
+
detectors: {},
|
|
11
|
+
high_fp_detectors: [],
|
|
12
|
+
recommendations: [],
|
|
13
|
+
per_package: {},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const absPath = resolve(fpFile);
|
|
17
|
+
if (!existsSync(absPath)) {
|
|
18
|
+
console.error(`[ERROR] False positives file not found: ${absPath}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const text = readFileSync(absPath, 'utf-8');
|
|
23
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const fp = JSON.parse(line);
|
|
27
|
+
analysis.total_fps += 1;
|
|
28
|
+
|
|
29
|
+
const detector = fp.detector;
|
|
30
|
+
if (!analysis.detectors[detector]) {
|
|
31
|
+
analysis.detectors[detector] = {
|
|
32
|
+
fp_count: 0,
|
|
33
|
+
avg_confidence: 0,
|
|
34
|
+
confidences: [],
|
|
35
|
+
severities: [],
|
|
36
|
+
examples: [],
|
|
37
|
+
unique_packages: new Set(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
analysis.detectors[detector].fp_count += 1;
|
|
42
|
+
analysis.detectors[detector].confidences.push(fp.confidence);
|
|
43
|
+
analysis.detectors[detector].severities.push(fp.severity);
|
|
44
|
+
analysis.detectors[detector].unique_packages.add(fp.package);
|
|
45
|
+
|
|
46
|
+
if (analysis.detectors[detector].examples.length < 5) {
|
|
47
|
+
analysis.detectors[detector].examples.push({
|
|
48
|
+
package: fp.package,
|
|
49
|
+
version: fp.version,
|
|
50
|
+
confidence: fp.confidence,
|
|
51
|
+
subtype: fp.subtype,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!analysis.per_package[fp.package]) {
|
|
56
|
+
analysis.per_package[fp.package] = [];
|
|
57
|
+
}
|
|
58
|
+
analysis.per_package[fp.package].push({
|
|
59
|
+
detector: fp.detector,
|
|
60
|
+
confidence: fp.confidence,
|
|
61
|
+
version: fp.version,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [detectorName, stats] of Object.entries(analysis.detectors)) {
|
|
66
|
+
stats.avg_confidence = stats.confidences.length > 0
|
|
67
|
+
? (stats.confidences.reduce((a, b) => a + b, 0) / stats.confidences.length).toFixed(1)
|
|
68
|
+
: '0.0';
|
|
69
|
+
stats.unique_package_count = stats.unique_packages.size;
|
|
70
|
+
delete stats.unique_packages;
|
|
71
|
+
|
|
72
|
+
const fpShare = (stats.fp_count / analysis.total_fps * 100).toFixed(1);
|
|
73
|
+
|
|
74
|
+
if (stats.fp_count >= 5) {
|
|
75
|
+
analysis.high_fp_detectors.push(detectorName);
|
|
76
|
+
analysis.recommendations.push({
|
|
77
|
+
detector: detectorName,
|
|
78
|
+
fp_count: stats.fp_count,
|
|
79
|
+
unique_packages: stats.unique_package_count,
|
|
80
|
+
share_of_total_fps: fpShare + '%',
|
|
81
|
+
avg_confidence: stats.avg_confidence,
|
|
82
|
+
severity_distribution: stats.severities.reduce((acc, s) => {
|
|
83
|
+
acc[s] = (acc[s] || 0) + 1;
|
|
84
|
+
return acc;
|
|
85
|
+
}, {}),
|
|
86
|
+
suggested_action: `Increase confidence threshold from current to ${Math.min(100, Math.ceil(parseFloat(stats.avg_confidence)) + 5)}`,
|
|
87
|
+
examples: stats.examples,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return analysis;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const fpFile = process.argv[2] || 'false-positives.jsonl';
|
|
96
|
+
|
|
97
|
+
console.log(`[INFO] Analyzing ${fpFile}...`);
|
|
98
|
+
const analysis = analyzeFalsePositives(fpFile);
|
|
99
|
+
|
|
100
|
+
console.log('\n=== FALSE POSITIVE ANALYSIS ===');
|
|
101
|
+
console.log(`Total FPs: ${analysis.total_fps}`);
|
|
102
|
+
console.log(`Detectors with FPs: ${Object.keys(analysis.detectors).length}`);
|
|
103
|
+
|
|
104
|
+
if (analysis.high_fp_detectors.length > 0) {
|
|
105
|
+
console.log(`\nHigh-FP detectors (>= 5 FPs): ${analysis.high_fp_detectors.join(', ')}`);
|
|
106
|
+
} else {
|
|
107
|
+
console.log('\nNo high-FP detectors found (all < 5 FPs) — thresholds are well-calibrated');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('\n=== PER-DETECTOR BREAKDOWN ===');
|
|
111
|
+
console.log('Detector FPs UniquePkgs AvgConf Top Examples');
|
|
112
|
+
console.log('─'.repeat(90));
|
|
113
|
+
for (const [name, stats] of Object.entries(analysis.detectors).sort(
|
|
114
|
+
(a, b) => b[1].fp_count - a[1].fp_count
|
|
115
|
+
)) {
|
|
116
|
+
const dName = name.padEnd(32).slice(0, 32);
|
|
117
|
+
const examples = stats.examples.slice(0, 2).map(e => e.package).join(', ');
|
|
118
|
+
console.log(
|
|
119
|
+
`${dName} ${String(stats.fp_count).padStart(4)} ${String(stats.unique_package_count).padStart(11)} ` +
|
|
120
|
+
`${stats.avg_confidence.padStart(7)} ${examples}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (analysis.recommendations.length > 0) {
|
|
125
|
+
console.log('\n=== RECOMMENDATIONS ===');
|
|
126
|
+
for (const rec of analysis.recommendations) {
|
|
127
|
+
console.log(`\n${rec.detector}:`);
|
|
128
|
+
console.log(` FPs: ${rec.fp_count} (${rec.share_of_total_fps} of total) across ${rec.unique_packages} unique packages`);
|
|
129
|
+
console.log(` Avg confidence: ${rec.avg_confidence}`);
|
|
130
|
+
console.log(` Severity breakdown: ${JSON.stringify(rec.severity_distribution)}`);
|
|
131
|
+
console.log(` Suggestion: ${rec.suggested_action}`);
|
|
132
|
+
console.log(` Examples:`);
|
|
133
|
+
for (const ex of rec.examples.slice(0, 3)) {
|
|
134
|
+
console.log(` ${ex.package}@${ex.version} (${ex.confidence}%) [${ex.subtype}]`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log('\n=== RECOMMENDATIONS ===');
|
|
139
|
+
console.log('No threshold adjustments needed — FP rates are within acceptable bounds.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const outPath = resolve('fp-analysis.json');
|
|
143
|
+
writeFileSync(outPath, JSON.stringify(analysis, null, 2), 'utf-8');
|
|
144
|
+
console.log(`\n[INFO] Full analysis written to ${outPath}`);
|
|
145
|
+
|
|
146
|
+
process.exit(0);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
async function analyzeValidation(resultsFile) {
|
|
6
|
+
const stats = {
|
|
7
|
+
total_packages: 0,
|
|
8
|
+
total_detections: 0,
|
|
9
|
+
total_expected: 0,
|
|
10
|
+
total_matched: 0,
|
|
11
|
+
campaigns: {},
|
|
12
|
+
detectors: {},
|
|
13
|
+
detection_matrix: {},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const absPath = resolve(resultsFile);
|
|
17
|
+
if (!existsSync(absPath)) {
|
|
18
|
+
console.error(`[ERROR] Results file not found: ${absPath}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const text = readFileSync(absPath, 'utf-8');
|
|
23
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const result = JSON.parse(line);
|
|
27
|
+
if (result.error) continue;
|
|
28
|
+
|
|
29
|
+
stats.total_packages += 1;
|
|
30
|
+
stats.total_detections += result.detection_count;
|
|
31
|
+
|
|
32
|
+
const campaignId = result.campaign_id;
|
|
33
|
+
if (!stats.campaigns[campaignId]) {
|
|
34
|
+
stats.campaigns[campaignId] = {
|
|
35
|
+
name: result.campaign_name,
|
|
36
|
+
total: 0,
|
|
37
|
+
detected: 0,
|
|
38
|
+
detection_rate: 0,
|
|
39
|
+
total_expected: 0,
|
|
40
|
+
total_matched: 0,
|
|
41
|
+
avg_confidence: 0,
|
|
42
|
+
confidences: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const campaign = stats.campaigns[campaignId];
|
|
47
|
+
campaign.total += 1;
|
|
48
|
+
campaign.total_expected += result.expected_detectors.length;
|
|
49
|
+
|
|
50
|
+
const matched = result.expected_detectors.filter(
|
|
51
|
+
id => result.detected_detectors.includes(id)
|
|
52
|
+
);
|
|
53
|
+
campaign.total_matched += matched.length;
|
|
54
|
+
stats.total_expected += result.expected_detectors.length;
|
|
55
|
+
stats.total_matched += matched.length;
|
|
56
|
+
|
|
57
|
+
if (matched.length > 0) {
|
|
58
|
+
campaign.detected += 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const detection of result.detections) {
|
|
62
|
+
const detectorName = detection.id || detection.detector;
|
|
63
|
+
if (!stats.detectors[detectorName]) {
|
|
64
|
+
stats.detectors[detectorName] = {
|
|
65
|
+
total_hits: 0,
|
|
66
|
+
expected_count: 0,
|
|
67
|
+
avg_confidence: 0,
|
|
68
|
+
confidences: [],
|
|
69
|
+
severities: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
stats.detectors[detectorName].total_hits += 1;
|
|
74
|
+
stats.detectors[detectorName].confidences.push(detection.confidenceScore);
|
|
75
|
+
stats.detectors[detectorName].severities.push(detection.severity);
|
|
76
|
+
|
|
77
|
+
if (result.expected_detectors.includes(detectorName)) {
|
|
78
|
+
stats.detectors[detectorName].expected_count += 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!stats.detection_matrix[campaignId]) {
|
|
82
|
+
stats.detection_matrix[campaignId] = {};
|
|
83
|
+
}
|
|
84
|
+
if (!stats.detection_matrix[campaignId][detectorName]) {
|
|
85
|
+
stats.detection_matrix[campaignId][detectorName] = 0;
|
|
86
|
+
}
|
|
87
|
+
stats.detection_matrix[campaignId][detectorName] += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const campaignId of Object.keys(stats.campaigns)) {
|
|
92
|
+
const campaign = stats.campaigns[campaignId];
|
|
93
|
+
campaign.detection_rate = campaign.total > 0
|
|
94
|
+
? ((campaign.detected / campaign.total) * 100).toFixed(1) + '%'
|
|
95
|
+
: '0%';
|
|
96
|
+
campaign.expected_match_rate = campaign.total_expected > 0
|
|
97
|
+
? ((campaign.total_matched / campaign.total_expected) * 100).toFixed(1) + '%'
|
|
98
|
+
: '0%';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const detectorName of Object.keys(stats.detectors)) {
|
|
102
|
+
const detector = stats.detectors[detectorName];
|
|
103
|
+
detector.avg_confidence = detector.confidences.length > 0
|
|
104
|
+
? (detector.confidences.reduce((a, b) => a + b, 0) / detector.confidences.length).toFixed(1)
|
|
105
|
+
: '0.0';
|
|
106
|
+
detector.precision = detector.total_hits > 0
|
|
107
|
+
? ((detector.expected_count / detector.total_hits) * 100).toFixed(1) + '%'
|
|
108
|
+
: '0%';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return stats;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resultsFile = process.argv[2] || 'validation-results.jsonl';
|
|
115
|
+
|
|
116
|
+
console.log(`[INFO] Analyzing ${resultsFile}...`);
|
|
117
|
+
const stats = await analyzeValidation(resultsFile);
|
|
118
|
+
|
|
119
|
+
console.log('\n=== CAMPAIGN DETECTION RATES ===');
|
|
120
|
+
console.log('Campaign Packages Detected Rate Expected Matched Match%');
|
|
121
|
+
console.log('─'.repeat(95));
|
|
122
|
+
for (const [id, campaign] of Object.entries(stats.campaigns)) {
|
|
123
|
+
const name = campaign.name.padEnd(33).slice(0, 33);
|
|
124
|
+
console.log(
|
|
125
|
+
`${name} ${String(campaign.total).padStart(8)} ${String(campaign.detected).padStart(9)} ` +
|
|
126
|
+
`${campaign.detection_rate.padStart(7)} ${String(campaign.total_expected).padStart(9)} ` +
|
|
127
|
+
`${String(campaign.total_matched).padStart(8)} ${campaign.expected_match_rate.padStart(7)}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
console.log(`\nTotal: ${stats.total_packages} packages, ${stats.total_detections} detections`);
|
|
131
|
+
|
|
132
|
+
console.log('\n=== DETECTOR PERFORMANCE ===');
|
|
133
|
+
console.log('Detector Hits Expected Precision Avg Confidence');
|
|
134
|
+
console.log('─'.repeat(80));
|
|
135
|
+
for (const [name, detector] of Object.entries(stats.detectors).sort(
|
|
136
|
+
(a, b) => b[1].total_hits - a[1].total_hits
|
|
137
|
+
)) {
|
|
138
|
+
const dName = name.padEnd(32).slice(0, 32);
|
|
139
|
+
console.log(
|
|
140
|
+
`${dName} ${String(detector.total_hits).padStart(5)} ${String(detector.expected_count).padStart(9)} ` +
|
|
141
|
+
`${detector.precision.padStart(10)} ${detector.avg_confidence.padStart(14)}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('\n=== DETECTION MATRIX (Hits per Campaign × Detector) ===');
|
|
146
|
+
console.log(JSON.stringify(stats.detection_matrix, null, 2));
|
|
147
|
+
|
|
148
|
+
writeFileSync('detection-rates.json', JSON.stringify(stats, null, 2), 'utf-8');
|
|
149
|
+
console.log('\n[INFO] Full results written to detection-rates.json');
|
|
150
|
+
|
|
151
|
+
process.exit(0);
|
|
@@ -0,0 +1,93 @@
|
|
|
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) continue;
|
|
52
|
+
if (whitelistedDetectors && whitelistedDetectors.has(detection.id)) continue;
|
|
53
|
+
|
|
54
|
+
falsePositives.push({
|
|
55
|
+
package: pkgName,
|
|
56
|
+
version: pkg.version,
|
|
57
|
+
detector: detection.id,
|
|
58
|
+
confidence: detection.confidenceScore,
|
|
59
|
+
severity: detection.severity,
|
|
60
|
+
subtype: detection.subtype,
|
|
61
|
+
message: detection.message,
|
|
62
|
+
evidence: detection.evidence,
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (falsePositives.length <= 10) {
|
|
67
|
+
console.log(`[FLAG] ${pkgName}@${pkg.version}: ${detection.id} (${detection.confidenceScore}%)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(`[ERROR] ${pkgName}: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const outPath = resolve('false-positives.jsonl');
|
|
76
|
+
const outputData = falsePositives.map(fp => JSON.stringify(fp)).join('\n') + '\n';
|
|
77
|
+
writeFileSync(outPath, outputData, 'utf-8');
|
|
78
|
+
|
|
79
|
+
const scannedCount = count - skipped;
|
|
80
|
+
console.log(`\n[SUMMARY] Scanned ${scannedCount} packages (skipped ${skipped} whitelisted)`);
|
|
81
|
+
console.log(`[SUMMARY] Found ${falsePositives.length} potential false positives (${(falsePositives.length / scannedCount * 100).toFixed(1)}% FP rate)`);
|
|
82
|
+
console.log(`[INFO] Written to ${outPath}`);
|
|
83
|
+
|
|
84
|
+
return falsePositives;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const topPackagesFile = process.argv[2] || 'top-packages.jsonl';
|
|
88
|
+
const confidenceThreshold = parseInt(process.argv[3]) || 70;
|
|
89
|
+
|
|
90
|
+
detectFalsePositives(topPackagesFile, confidenceThreshold).then(() => process.exit(0)).catch(err => {
|
|
91
|
+
console.error(`[FATAL] ${err.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
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) break;
|
|
30
|
+
packages.push({
|
|
31
|
+
name: result.package.name,
|
|
32
|
+
version: result.package.version,
|
|
33
|
+
description: result.package.description || '',
|
|
34
|
+
keywords: result.package.keywords || [],
|
|
35
|
+
publisher: result.package.publisher ? result.package.publisher.username : null,
|
|
36
|
+
date: result.package.date,
|
|
37
|
+
score: result.score ? {
|
|
38
|
+
final: result.score.final,
|
|
39
|
+
quality: result.score.detail?.quality,
|
|
40
|
+
popularity: result.score.detail?.popularity,
|
|
41
|
+
maintenance: result.score.detail?.maintenance,
|
|
42
|
+
} : null,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(` Retrieved ${packages.length} packages so far`);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`[ERROR] Failed page ${page + 1}: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (packages.length >= limit) break;
|
|
52
|
+
|
|
53
|
+
if (page < numPages - 1) {
|
|
54
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (packages.length === 0) {
|
|
59
|
+
console.log('[ERROR] No packages fetched. Using fallback known-top list.');
|
|
60
|
+
return await fallbackList(limit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const outPath = resolve('top-packages.jsonl');
|
|
64
|
+
const lines = packages.map(pkg => JSON.stringify(pkg)).join('\n') + '\n';
|
|
65
|
+
writeFileSync(outPath, lines, 'utf-8');
|
|
66
|
+
console.log(`\n[INFO] Written ${packages.length} packages to ${outPath}`);
|
|
67
|
+
return packages;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fallbackList(limit) {
|
|
71
|
+
const knownTop = [
|
|
72
|
+
'lodash', 'chalk', 'react', 'express', 'commander', 'axios', 'moment',
|
|
73
|
+
'webpack', 'eslint', 'typescript', 'prettier', 'babel', 'next', 'vue',
|
|
74
|
+
'angular', 'redux', 'jest', 'mocha', 'chai', 'sinon', 'nodemon',
|
|
75
|
+
'debug', 'async', 'request', 'colors', 'mkdirp', 'fs-extra', 'glob',
|
|
76
|
+
'yargs', 'minimist', 'uuid', 'date-fns', 'crypto-js', 'jsonwebtoken',
|
|
77
|
+
'passport', 'socket.io', 'ws', 'graphql', 'apollo', 'prisma',
|
|
78
|
+
'mongoose', 'pg', 'mysql2', 'redis', 'sequelize', 'typeorm',
|
|
79
|
+
'dotenv', 'cross-env', 'rimraf', 'semver', 'rimraf', 'tar',
|
|
80
|
+
'inquirer', 'ora', 'listr', 'conf', 'env-paths', 'find-up',
|
|
81
|
+
'p-locate', 'locate-path', 'path-exists', 'y18n', 'yallist',
|
|
82
|
+
'minipass', 'minizlib', 'supports-color', 'has-flag', 'wrap-ansi',
|
|
83
|
+
'string-width', 'strip-ansi', 'ansi-regex', 'is-fullwidth-code-point',
|
|
84
|
+
'emoji-regex', 'cliui', 'escalade', 'get-caller-file', 'require-directory',
|
|
85
|
+
'npm', 'node-fetch', 'got', 'phin', 'undici', 'make-fetch-happen',
|
|
86
|
+
'cacache', 'ssri', 'unique-filename', 'unique-slug', 'imurmurhash',
|
|
87
|
+
'signal-exit', 'which', 'isexe', 'minimatch', 'brace-expansion',
|
|
88
|
+
'balanced-match', 'concat-map', 'lru-cache', 'yallist', 'semver',
|
|
89
|
+
'json5', 'tslib', 'source-map', 'source-map-js', 'ms', 'mime',
|
|
90
|
+
'cookie', 'express-session', 'body-parser', 'cors', 'helmet',
|
|
91
|
+
'morgan', 'compression', 'serve-static', 'send', 'fresh',
|
|
92
|
+
'etag', 'parseurl', 'utils-merge', 'methods', 'array-flatten',
|
|
93
|
+
'qs', 'merge-descriptors', 'path-to-regexp', 'iconv-lite',
|
|
94
|
+
'raw-body', 'on-finished', 'ee-first', 'inherits', 'depd',
|
|
95
|
+
'http-errors', 'statuses', 'setprototypeof', 'toidentifier',
|
|
96
|
+
'content-type', 'negotiator', 'accepts', 'type-is', 'vary',
|
|
97
|
+
'encodeurl', 'escape-html', 'destroy', 'bytes', 'unpipe',
|
|
98
|
+
'finalhandler', 'media-typer', 'http-proxy', 'http-proxy-middleware',
|
|
99
|
+
'morgan', 'connect', 'pino', 'winston', 'bunyan', 'log4js',
|
|
100
|
+
'nanoid', 'uid', 'ulid', 'cuid', 'shortid', 'uuidv4', 'uuidv7',
|
|
101
|
+
'bcrypt', 'bcryptjs', 'argon2', 'scrypt', 'pbkdf2', 'crypto',
|
|
102
|
+
'node-forge', 'pkijs', 'asn1js', 'jsrsasign', 'jose', 'jwk',
|
|
103
|
+
];
|
|
104
|
+
const pkgs = knownTop.slice(0, limit).map((name, i) => ({
|
|
105
|
+
name,
|
|
106
|
+
version: '1.0.0',
|
|
107
|
+
description: '',
|
|
108
|
+
keywords: [],
|
|
109
|
+
publisher: null,
|
|
110
|
+
date: null,
|
|
111
|
+
score: { final: 1 - i / knownTop.length, quality: 0.9, popularity: 0.9, maintenance: 0.9 },
|
|
112
|
+
}));
|
|
113
|
+
console.log(`[FALLBACK] Using ${pkgs.length} known top packages`);
|
|
114
|
+
|
|
115
|
+
const outPath = resolve('top-packages.jsonl');
|
|
116
|
+
const lines = pkgs.map(pkg => JSON.stringify(pkg)).join('\n') + '\n';
|
|
117
|
+
writeFileSync(outPath, lines, 'utf-8');
|
|
118
|
+
console.log(`[INFO] Written ${pkgs.length} packages to ${outPath}`);
|
|
119
|
+
return pkgs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const limit = parseInt(process.argv[2]) || 1000;
|
|
123
|
+
fetchTopPackages(limit).then(pkgs => {
|
|
124
|
+
console.log(`[DONE] ${pkgs.length} packages fetched`);
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}).catch(err => {
|
|
127
|
+
console.error(`[FATAL] ${err.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
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.split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchNpmMetadata(pkgName, version) {
|
|
23
|
+
try {
|
|
24
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${version}`;
|
|
25
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
26
|
+
if (!response.ok) return null;
|
|
27
|
+
return await response.json();
|
|
28
|
+
} catch {
|
|
29
|
+
console.warn(` [WARN] Registry fetch failed for ${pkgName}@${version}; using fixture data`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function constructRegistryMeta(pkg, liveMeta) {
|
|
35
|
+
if (liveMeta) return liveMeta;
|
|
36
|
+
if (pkg.mockRegistryMeta) return pkg.mockRegistryMeta;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function constructPkgJson(pkg) {
|
|
41
|
+
const base = { name: pkg.package, version: pkg.version };
|
|
42
|
+
if (pkg.mockPackageJson) {
|
|
43
|
+
return { ...base, ...pkg.mockPackageJson };
|
|
44
|
+
}
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function validateDetectors(campaigns, outputFile) {
|
|
49
|
+
const allResults = [];
|
|
50
|
+
|
|
51
|
+
const campaignKeys = campaigns === 'all'
|
|
52
|
+
? Object.keys(CAMPAIGN_FIXTURES)
|
|
53
|
+
: [campaigns];
|
|
54
|
+
|
|
55
|
+
for (const campaignKey of campaignKeys) {
|
|
56
|
+
const fixturePath = CAMPAIGN_FIXTURES[campaignKey];
|
|
57
|
+
if (!fixturePath) {
|
|
58
|
+
console.error(`[ERROR] Unknown campaign: ${campaignKey}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`\n[${new Date().toISOString()}] Validating ${campaignKey}...`);
|
|
63
|
+
const packages = loadFixture(fixturePath);
|
|
64
|
+
console.log(` Loaded ${packages.length} packages from fixture`);
|
|
65
|
+
|
|
66
|
+
for (const pkg of packages) {
|
|
67
|
+
try {
|
|
68
|
+
const pkgJson = constructPkgJson(pkg);
|
|
69
|
+
const liveMeta = await fetchNpmMetadata(pkg.package, pkg.version);
|
|
70
|
+
const registryMeta = constructRegistryMeta(pkg, liveMeta);
|
|
71
|
+
|
|
72
|
+
const findings = await runAll(pkgJson, [], registryMeta, []);
|
|
73
|
+
|
|
74
|
+
const detectedIds = [...new Set(findings.map(f => f.id))];
|
|
75
|
+
|
|
76
|
+
const result = {
|
|
77
|
+
package: pkg.package,
|
|
78
|
+
version: pkg.version,
|
|
79
|
+
campaign_id: pkg.campaign_id,
|
|
80
|
+
campaign_name: pkg.campaign_name,
|
|
81
|
+
attack_vector: pkg.attack_vector,
|
|
82
|
+
expected_detectors: pkg.expected_detectors,
|
|
83
|
+
detected_detectors: detectedIds,
|
|
84
|
+
detection_count: findings.length,
|
|
85
|
+
detections: findings.map(f => ({
|
|
86
|
+
id: f.id,
|
|
87
|
+
detector: f.detector,
|
|
88
|
+
severity: f.severity,
|
|
89
|
+
confidence: f.confidence,
|
|
90
|
+
confidenceScore: f.confidenceScore,
|
|
91
|
+
subtype: f.subtype,
|
|
92
|
+
message: f.message,
|
|
93
|
+
})),
|
|
94
|
+
metadata_available: !!liveMeta,
|
|
95
|
+
registry_source: liveMeta ? 'live' : 'fixture',
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
allResults.push(result);
|
|
100
|
+
|
|
101
|
+
const expectedCount = pkg.expected_detectors.length;
|
|
102
|
+
const hitCount = detectedIds.filter(id => pkg.expected_detectors.includes(id)).length;
|
|
103
|
+
console.log(
|
|
104
|
+
` ${hitCount > 0 ? '✓' : '✗'} ${pkg.package}@${pkg.version}: ${hitCount}/${expectedCount} expected detectors fired`
|
|
105
|
+
);
|
|
106
|
+
for (const f of findings) {
|
|
107
|
+
console.log(` ${f.id} (${f.confidenceScore}%, ${f.severity})`);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(` ✗ ${pkg.package}@${pkg.version}: ${err.message}`);
|
|
111
|
+
allResults.push({
|
|
112
|
+
package: pkg.package,
|
|
113
|
+
version: pkg.version,
|
|
114
|
+
campaign_id: pkg.campaign_id,
|
|
115
|
+
error: err.message,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (outputFile) {
|
|
123
|
+
const lines = allResults.map(r => JSON.stringify(r)).join('\n') + '\n';
|
|
124
|
+
writeFileSync(outputFile, lines, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const processed = allResults.filter(r => !r.error).length;
|
|
128
|
+
const errors = allResults.filter(r => r.error).length;
|
|
129
|
+
console.log(`\n[SUMMARY] Processed ${processed} packages, ${errors} errors`);
|
|
130
|
+
console.log(`[INFO] Results written to ${outputFile}`);
|
|
131
|
+
|
|
132
|
+
return allResults;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
const campaignArg = args[0] || 'all';
|
|
137
|
+
const outputArg = args[1] ? resolve(args[1]) : resolve('validation-results.jsonl');
|
|
138
|
+
|
|
139
|
+
validateDetectors(campaignArg, outputArg).then(() => process.exit(0)).catch(err => {
|
|
140
|
+
console.error(`[ERROR] ${err.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|