@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
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
4
|
|
|
5
5
|
const highPatterns = [
|
|
6
6
|
{
|
|
7
7
|
pattern: /(?:exec|execSync|spawn)\s*\([^)]*npm\s+(?:install|link)\s*\./i,
|
|
8
|
-
label: 'programmatic self-propagation via npm install/link'
|
|
8
|
+
label: 'programmatic self-propagation via npm install/link',
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
|
-
pattern:
|
|
12
|
-
|
|
11
|
+
pattern:
|
|
12
|
+
/fs\.(?:writeFile|writeFileSync|copyFile|copyFileSync)\s*\([^)]*(?:node_modules\/(?!\.)[^/]+).*(?:index\.js|main\.js|package\.json)/i,
|
|
13
|
+
label: 'direct file write to peer node_modules',
|
|
13
14
|
},
|
|
14
15
|
{
|
|
15
16
|
pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*package\.json[^)]*["']scripts["']/i,
|
|
16
|
-
label: 'package.json script injection in another package'
|
|
17
|
+
label: 'package.json script injection in another package',
|
|
17
18
|
},
|
|
18
19
|
{
|
|
19
20
|
pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*\.\.\/[^)]*package\.json/i,
|
|
20
|
-
label: 'writes modified package.json to sibling package'
|
|
21
|
+
label: 'writes modified package.json to sibling package',
|
|
21
22
|
},
|
|
22
23
|
{
|
|
23
24
|
pattern: /(?:exec|execSync|spawn)\s*\([^)]*(?:\.\.\/|process\.env\.INIT_CWD).*npm\s+install/i,
|
|
24
|
-
label: 'cross-directory npm install propagation'
|
|
25
|
+
label: 'cross-directory npm install propagation',
|
|
25
26
|
},
|
|
26
27
|
];
|
|
27
28
|
|
|
@@ -32,7 +33,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
32
33
|
severity: 'high',
|
|
33
34
|
title: 'Transitive propagation (worm)',
|
|
34
35
|
description: `Package attempts lateral worm-style spread: ${label}`,
|
|
35
|
-
evidence: 'transitive propagation pattern detected'
|
|
36
|
+
evidence: 'transitive propagation pattern detected',
|
|
36
37
|
});
|
|
37
38
|
break;
|
|
38
39
|
}
|
|
@@ -42,19 +43,19 @@ export async function scan(pkgJson, files = []) {
|
|
|
42
43
|
const mediumPatterns = [
|
|
43
44
|
{
|
|
44
45
|
pattern: /process\.env\.npm_package_name/,
|
|
45
|
-
label: 'reads own package name from env (self-awareness indicator)'
|
|
46
|
+
label: 'reads own package name from env (self-awareness indicator)',
|
|
46
47
|
},
|
|
47
48
|
{
|
|
48
49
|
pattern: /fs\.symlink(?:Sync)?\s*\([^)]*node_modules/,
|
|
49
|
-
label: 'creates symlinks in node_modules (worm spreading mechanism)'
|
|
50
|
+
label: 'creates symlinks in node_modules (worm spreading mechanism)',
|
|
50
51
|
},
|
|
51
52
|
{
|
|
52
53
|
pattern: /fs\.(?:mkdir|mkdirSync)\s*\([^)]*\.\.\/[^)]*node_modules/,
|
|
53
|
-
label: 'creates directories in parent node_modules'
|
|
54
|
+
label: 'creates directories in parent node_modules',
|
|
54
55
|
},
|
|
55
56
|
{
|
|
56
57
|
pattern: /__dirname.*\.\.\/[^/]+\/node_modules.*require\(/,
|
|
57
|
-
label: 'dynamic parent-node_modules require for lateral spread'
|
|
58
|
+
label: 'dynamic parent-node_modules require for lateral spread',
|
|
58
59
|
},
|
|
59
60
|
];
|
|
60
61
|
|
|
@@ -65,7 +66,7 @@ export async function scan(pkgJson, files = []) {
|
|
|
65
66
|
severity: 'medium',
|
|
66
67
|
title: 'Transitive propagation (worm)',
|
|
67
68
|
description: label,
|
|
68
|
-
evidence: 'potential propagation indicator'
|
|
69
|
+
evidence: 'potential propagation indicator',
|
|
69
70
|
});
|
|
70
71
|
break;
|
|
71
72
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const BLOCKED_VERSIONS = new Map([
|
|
2
|
-
['axios', ['1.14.1', '0.30.4']],
|
|
3
|
-
]);
|
|
1
|
+
const BLOCKED_VERSIONS = new Map([['axios', ['1.14.1', '0.30.4']]]);
|
|
4
2
|
|
|
5
3
|
export function scanVersionBlocklist(pkgJson) {
|
|
6
4
|
const pkgName = pkgJson?.name || '';
|
|
7
5
|
const pkgVersion = pkgJson?.version || '';
|
|
8
6
|
|
|
9
7
|
const blocked = BLOCKED_VERSIONS.get(pkgName);
|
|
10
|
-
if (!blocked)
|
|
8
|
+
if (!blocked) {
|
|
9
|
+
return { triggered: false, stopCondition: false, matchedVersion: null };
|
|
10
|
+
}
|
|
11
11
|
|
|
12
12
|
if (blocked.includes(pkgVersion)) {
|
|
13
13
|
return {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const KNOWN_DECOYS = ['plain-crypto-js'];
|
|
2
2
|
|
|
3
3
|
export function scanDecoyDependency(pkgJson) {
|
|
4
|
-
const deps = {
|
|
4
|
+
const deps = {
|
|
5
|
+
...pkgJson?.dependencies,
|
|
6
|
+
...pkgJson?.devDependencies,
|
|
7
|
+
...pkgJson?.peerDependencies,
|
|
8
|
+
};
|
|
5
9
|
const findings = [];
|
|
6
10
|
|
|
7
11
|
for (const depName of KNOWN_DECOYS) {
|
|
@@ -7,7 +7,8 @@ const CRON_PERSIST_RE = /crontab\s+-[ei]|@reboot\s+|@daily\s+|@hourly\s+/;
|
|
|
7
7
|
const DLL_LOAD_RE = /LoadLibrary|dlopen|LoadLibraryEx|lib\.(?:LoadLibrary|dlopen)/;
|
|
8
8
|
const PROCESS_INJECT_RE = /CreateRemoteThread|VirtualAllocEx|WriteProcessMemory|NtCreateThreadEx/;
|
|
9
9
|
const NET_CALLBACK_RE = /(?:https?:\/\/|wss?:\/\/|ws:\/\/)(?:[^\s'"]*\.[^\s'"]{2,})/;
|
|
10
|
-
const BINARY_DROP_RE =
|
|
10
|
+
const BINARY_DROP_RE =
|
|
11
|
+
/(?:fs\.writeFileSync|writeFile|writeFileSync)\s*\([^)]*(?:\.exe|\.dll|\.bin|\.bat|\.ps1)/;
|
|
11
12
|
|
|
12
13
|
const SUSPICIOUS_HOOK_PATTERNS = [
|
|
13
14
|
/curl|wget|fetch|https?:\/\//,
|
|
@@ -22,7 +23,7 @@ const SUSPICIOUS_HOOK_PATTERNS = [
|
|
|
22
23
|
|
|
23
24
|
export function scanPostinstallRAT(pkgJson, files = []) {
|
|
24
25
|
const scripts = pkgJson?.scripts || {};
|
|
25
|
-
const code = files.map(f => f.content || '').join('\n');
|
|
26
|
+
const code = files.map((f) => f.content || '').join('\n');
|
|
26
27
|
|
|
27
28
|
const activeHooks = [];
|
|
28
29
|
for (const hook of SUSPICIOUS_HOOKS) {
|
|
@@ -32,39 +33,76 @@ export function scanPostinstallRAT(pkgJson, files = []) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
if (activeHooks.length === 0) {
|
|
35
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
triggered: false,
|
|
38
|
+
platforms: [],
|
|
39
|
+
c2Indicators: [],
|
|
40
|
+
payloadType: null,
|
|
41
|
+
hooks: [],
|
|
42
|
+
hasBinaryDrop: false,
|
|
43
|
+
};
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
const combined = code + '\n' + activeHooks.map(h => h.command).join('\n');
|
|
46
|
+
const combined = code + '\n' + activeHooks.map((h) => h.command).join('\n');
|
|
39
47
|
|
|
40
|
-
const hasSuspiciousCode = SUSPICIOUS_HOOK_PATTERNS.some(p => p.test(combined));
|
|
48
|
+
const hasSuspiciousCode = SUSPICIOUS_HOOK_PATTERNS.some((p) => p.test(combined));
|
|
41
49
|
|
|
42
50
|
if (activeHooks.length > 0 && !hasSuspiciousCode) {
|
|
43
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
triggered: false,
|
|
53
|
+
platforms: [],
|
|
54
|
+
c2Indicators: [],
|
|
55
|
+
payloadType: null,
|
|
56
|
+
hooks: [],
|
|
57
|
+
hasBinaryDrop: false,
|
|
58
|
+
};
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
const platforms = [];
|
|
47
62
|
let c2Indicators = [];
|
|
48
63
|
let hasBinaryDrop = false;
|
|
49
64
|
|
|
50
|
-
if (POWERSHELL_RE.test(combined))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
if (POWERSHELL_RE.test(combined)) {
|
|
66
|
+
platforms.push('windows');
|
|
67
|
+
}
|
|
68
|
+
if (LAUNCHD_RE.test(combined)) {
|
|
69
|
+
platforms.push('macos');
|
|
70
|
+
}
|
|
71
|
+
if (SYSTEMD_SERVICE_RE.test(combined) || CRON_PERSIST_RE.test(combined)) {
|
|
72
|
+
platforms.push('linux');
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
TEMP_DIR_RE.test(combined) &&
|
|
76
|
+
(POWERSHELL_RE.test(combined) || BINARY_DROP_RE.test(combined))
|
|
77
|
+
) {
|
|
78
|
+
if (!platforms.includes('windows')) {
|
|
79
|
+
platforms.push('windows');
|
|
80
|
+
}
|
|
81
|
+
if (!platforms.includes('linux')) {
|
|
82
|
+
platforms.push('linux');
|
|
83
|
+
}
|
|
84
|
+
if (!platforms.includes('macos')) {
|
|
85
|
+
platforms.push('macos');
|
|
86
|
+
}
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
if (DLL_LOAD_RE.test(combined))
|
|
60
|
-
|
|
89
|
+
if (DLL_LOAD_RE.test(combined)) {
|
|
90
|
+
platforms.push('windows');
|
|
91
|
+
}
|
|
92
|
+
if (PROCESS_INJECT_RE.test(combined)) {
|
|
93
|
+
platforms.push('windows');
|
|
94
|
+
}
|
|
61
95
|
|
|
62
96
|
if (NET_CALLBACK_RE.test(combined)) {
|
|
63
97
|
const urls = combined.match(NET_CALLBACK_RE);
|
|
64
|
-
c2Indicators = urls
|
|
98
|
+
c2Indicators = urls
|
|
99
|
+
? [...new Set(urls.map((u) => u.replace(/['")]/g, '')))]
|
|
100
|
+
: ['Network callback to external server'];
|
|
65
101
|
}
|
|
66
102
|
|
|
67
|
-
if (BINARY_DROP_RE.test(combined))
|
|
103
|
+
if (BINARY_DROP_RE.test(combined)) {
|
|
104
|
+
hasBinaryDrop = true;
|
|
105
|
+
}
|
|
68
106
|
|
|
69
107
|
let payloadType = null;
|
|
70
108
|
if (platforms.length >= 2 && c2Indicators.length > 0 && hasBinaryDrop) {
|
|
@@ -81,10 +119,17 @@ export function scanPostinstallRAT(pkgJson, files = []) {
|
|
|
81
119
|
payloadType,
|
|
82
120
|
platforms: [...new Set(platforms)],
|
|
83
121
|
c2Indicators,
|
|
84
|
-
hooks: activeHooks.map(h => h.hook),
|
|
122
|
+
hooks: activeHooks.map((h) => h.hook),
|
|
85
123
|
hasBinaryDrop,
|
|
86
124
|
};
|
|
87
125
|
}
|
|
88
126
|
|
|
89
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
triggered: false,
|
|
129
|
+
platforms: [],
|
|
130
|
+
c2Indicators: [],
|
|
131
|
+
payloadType: null,
|
|
132
|
+
hooks: [],
|
|
133
|
+
hasBinaryDrop: false,
|
|
134
|
+
};
|
|
90
135
|
}
|
|
@@ -7,47 +7,58 @@ const RULE_SEVERITY = { D1: 'critical', D2: 'critical', D3: 'critical' };
|
|
|
7
7
|
const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
8
8
|
|
|
9
9
|
function highestSeverity(severities) {
|
|
10
|
-
for (const s of SEVERITY_ORDER)
|
|
10
|
+
for (const s of SEVERITY_ORDER) {
|
|
11
|
+
if (severities.includes(s)) {
|
|
12
|
+
return s;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
11
15
|
return 'none';
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
export async function scan(pkgJson, files = [],
|
|
18
|
+
export async function scan(pkgJson, files = [], _registryMeta = null, allFiles = null) {
|
|
15
19
|
const pkgName = pkgJson?.name || 'unknown';
|
|
16
20
|
const pkgVersion = pkgJson?.version || '0.0.0';
|
|
17
21
|
const fileList = allFiles || files || [];
|
|
18
22
|
|
|
19
23
|
const d1Result = scanVersionBlocklist(pkgJson);
|
|
20
24
|
if (d1Result.stopCondition) {
|
|
21
|
-
const evidence = attachProvenance(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
const evidence = attachProvenance(
|
|
26
|
+
{
|
|
27
|
+
rule: 'AXS-VER-001',
|
|
28
|
+
campaign: 'AXIOS_POISONING',
|
|
29
|
+
triggeredChecks: ['D1'],
|
|
30
|
+
matchedVersion: d1Result.matchedVersion,
|
|
31
|
+
action: 'BLOCK_IMMEDIATELY',
|
|
32
|
+
remediation: `Upgrade to axios@1.14.2 or later, or use pinned safe version`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
ruleId: 'AXS-VER-001',
|
|
36
|
+
ruleName: 'Compromised Axios Version Fingerprinting',
|
|
37
|
+
campaignName: 'Axios Registry Poisoning',
|
|
38
|
+
pkgName,
|
|
39
|
+
pkgVersion,
|
|
40
|
+
triggered: true,
|
|
41
|
+
severity: 'critical',
|
|
42
|
+
indicators: [{ type: 'known_malicious_version', value: `${pkgName}@${pkgVersion}` }],
|
|
43
|
+
ruleProvenanceUrl:
|
|
44
|
+
'https://github.com/lateos/npm-scan/blob/main/backend/detectors/axios-poisoning/d1-version-fingerprint.js',
|
|
45
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
46
|
+
}
|
|
47
|
+
);
|
|
41
48
|
|
|
42
|
-
return [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
id: 'AXIOS_POISONING',
|
|
52
|
+
severity: 'critical',
|
|
53
|
+
title: 'Axios Registry Poisoning campaign',
|
|
54
|
+
description: `HALT: ${pkgName}@${pkgVersion} is a known compromised version in the Axios registry poisoning campaign. Block install immediately.`,
|
|
55
|
+
evidence: JSON.stringify(evidence),
|
|
56
|
+
mitigation: d1Result.reason
|
|
57
|
+
? `BLOCK IMMEDIATELY. ${d1Result.reason}. Upgrade to axios@1.14.2 or later, or use pinned safe version.`
|
|
58
|
+
: 'BLOCK IMMEDIATELY. Upgrade to a safe version.',
|
|
59
|
+
stopCondition: true,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
const d2Result = scanDecoyDependency(pkgJson);
|
|
@@ -59,36 +70,42 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
59
70
|
.filter(([_, r]) => r.triggered)
|
|
60
71
|
.map(([id]) => id);
|
|
61
72
|
|
|
62
|
-
if (triggered.length === 0)
|
|
73
|
+
if (triggered.length === 0) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
63
76
|
|
|
64
|
-
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
77
|
+
const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
|
|
65
78
|
|
|
66
|
-
const evidence = attachProvenance(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
const evidence = attachProvenance(
|
|
80
|
+
{
|
|
81
|
+
campaign: 'AXIOS_POISONING',
|
|
82
|
+
triggeredChecks: triggered,
|
|
83
|
+
details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
ruleId: 'AXIOS_POISONING',
|
|
87
|
+
ruleName: 'Axios Registry Poisoning Detection',
|
|
88
|
+
campaignName: 'Axios Registry Poisoning',
|
|
89
|
+
pkgName,
|
|
90
|
+
pkgVersion,
|
|
91
|
+
triggered: true,
|
|
92
|
+
severity,
|
|
93
|
+
indicators: triggered.map((id) => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
|
|
94
|
+
ruleProvenanceUrl:
|
|
95
|
+
'https://github.com/lateos/npm-scan/blob/main/backend/detectors/axios-poisoning/',
|
|
96
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
97
|
+
}
|
|
98
|
+
);
|
|
85
99
|
|
|
86
|
-
return [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
id: 'AXIOS_POISONING',
|
|
103
|
+
severity,
|
|
104
|
+
title: 'Axios Registry Poisoning campaign',
|
|
105
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
106
|
+
evidence: JSON.stringify(evidence),
|
|
107
|
+
mitigation:
|
|
108
|
+
'If decoy dependency detected: verify all axios dependencies are legitimate. If RAT payload detected: run full malware scan on the system, rotate all credentials, check for unauthorized network connections. Upgrade to axios@1.14.2+ or pin to a known safe version.',
|
|
109
|
+
},
|
|
110
|
+
];
|
|
94
111
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detector confidence thresholds (calibrated post-validation)
|
|
3
|
+
*
|
|
4
|
+
* Format: { detector: { flag_threshold, warn_threshold } }
|
|
5
|
+
* Thresholds calibrated against:
|
|
6
|
+
* - 3 real May 2026 attack campaigns (validation)
|
|
7
|
+
* - Top 1,000 npm packages (false positive calibration)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
'TIER1-VERSION-ANOMALY': {
|
|
12
|
+
flag_threshold: 72,
|
|
13
|
+
warn_threshold: 60,
|
|
14
|
+
notes:
|
|
15
|
+
'Sentinel patterns (99.99.99/11.11.11/10.10.10) always flag at 92 regardless of threshold',
|
|
16
|
+
},
|
|
17
|
+
'TIER1-OBFUSCATION-HEURISTICS': {
|
|
18
|
+
flag_threshold: 75,
|
|
19
|
+
warn_threshold: 60,
|
|
20
|
+
notes: 'Increased from 70 post-FP analysis; bundlers (webpack, terser) exempt via whitelist',
|
|
21
|
+
},
|
|
22
|
+
'TIER1-BINARY-EMBED': {
|
|
23
|
+
flag_threshold: 80,
|
|
24
|
+
warn_threshold: 65,
|
|
25
|
+
notes:
|
|
26
|
+
'High threshold justified; platform-specific binary sets are rare in legitimate packages',
|
|
27
|
+
},
|
|
28
|
+
'TIER1-LIFECYCLE-HOOK': {
|
|
29
|
+
flag_threshold: 65,
|
|
30
|
+
warn_threshold: 50,
|
|
31
|
+
notes: 'Moderate threshold; lifecycle hooks common but uncommon in top 1K packages',
|
|
32
|
+
},
|
|
33
|
+
'TIER1-INFOSTEALER': {
|
|
34
|
+
flag_threshold: 72,
|
|
35
|
+
warn_threshold: 55,
|
|
36
|
+
notes: 'Pattern-based; calibrated for C2 signatures, credential exfil patterns',
|
|
37
|
+
},
|
|
38
|
+
'TIER1-TYPOSQUAT': {
|
|
39
|
+
flag_threshold: 85,
|
|
40
|
+
warn_threshold: 70,
|
|
41
|
+
notes:
|
|
42
|
+
'Calibrated to 85 post-FP analysis on top 1,000 packages; 46 edit-distance=1 FPs eliminated at this threshold',
|
|
43
|
+
},
|
|
44
|
+
'TIER1-METADATA-SPOOF': {
|
|
45
|
+
flag_threshold: 70,
|
|
46
|
+
warn_threshold: 55,
|
|
47
|
+
notes: 'Namespace/repo URL spoofing; moderate threshold for legitimate clones',
|
|
48
|
+
},
|
|
49
|
+
'TIER1-VERSION-CONFUSION': {
|
|
50
|
+
flag_threshold: 75,
|
|
51
|
+
warn_threshold: 60,
|
|
52
|
+
notes: 'High-version heuristics (major >= 9); tuned to avoid FP on pre-release tags',
|
|
53
|
+
},
|
|
54
|
+
'TIER1-CLOUD-IMDS': {
|
|
55
|
+
flag_threshold: 80,
|
|
56
|
+
warn_threshold: 65,
|
|
57
|
+
notes: 'IMDS endpoint targeting is rarely legitimate; high threshold',
|
|
58
|
+
},
|
|
59
|
+
'TIER1-MULTISTAGE-POSTINSTALL': {
|
|
60
|
+
flag_threshold: 75,
|
|
61
|
+
warn_threshold: 60,
|
|
62
|
+
notes: 'Two-stage download+exec patterns; moderate threshold',
|
|
63
|
+
},
|
|
64
|
+
'TIER1-SLSA-ATTESTATION': {
|
|
65
|
+
flag_threshold: 85,
|
|
66
|
+
warn_threshold: 70,
|
|
67
|
+
notes: 'Placeholder; threshold TBD when API stabilizes',
|
|
68
|
+
},
|
|
69
|
+
'TIER1-SELF-PROPAGATION': {
|
|
70
|
+
flag_threshold: 75,
|
|
71
|
+
warn_threshold: 60,
|
|
72
|
+
burst_window_minutes: 60,
|
|
73
|
+
min_packages_burst: 3,
|
|
74
|
+
identical_payload_weight: 40,
|
|
75
|
+
notes: 'D10: Detects burst republish patterns (Miasma campaign)',
|
|
76
|
+
},
|
|
77
|
+
'TIER1-ENCRYPTED-C2': {
|
|
78
|
+
flag_threshold: 70,
|
|
79
|
+
warn_threshold: 50,
|
|
80
|
+
known_c2_endpoints: [
|
|
81
|
+
'filev2.getsession.org',
|
|
82
|
+
'api.signal.org',
|
|
83
|
+
'*.briarproject.org',
|
|
84
|
+
'api.ricochet.im',
|
|
85
|
+
],
|
|
86
|
+
onion_pattern_weight: 30,
|
|
87
|
+
encoded_url_weight: 35,
|
|
88
|
+
env_var_c2_weight: 40,
|
|
89
|
+
notes: 'D11: Detects Session/Oxen, Signal, Briar, Tor C2 channels',
|
|
90
|
+
},
|
|
91
|
+
'TIER1-TRANSITIVE-DEPS': {
|
|
92
|
+
flag_threshold: 80,
|
|
93
|
+
warn_threshold: 50,
|
|
94
|
+
new_package_days: 7,
|
|
95
|
+
unknown_depth_weight: 45,
|
|
96
|
+
typosquat_depth_weight: 50,
|
|
97
|
+
different_maintainer_weight: 35,
|
|
98
|
+
notes: 'D12: Deep dependency tree analysis for injection attacks',
|
|
99
|
+
},
|
|
100
|
+
'TIER1-MAINTAINER-COMPROMISE': {
|
|
101
|
+
flag_threshold: 75,
|
|
102
|
+
warn_threshold: 60,
|
|
103
|
+
velocity_burst_multiplier: 5,
|
|
104
|
+
burst_window_hours: 24,
|
|
105
|
+
min_velocity_baseline: 0.5,
|
|
106
|
+
duplicate_version_weight: 40,
|
|
107
|
+
unusual_timing_weight: 25,
|
|
108
|
+
cross_package_burst_weight: 50,
|
|
109
|
+
notes: 'D13: Version velocity anomaly and maintainer compromise detection',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packages": [
|
|
3
|
+
{
|
|
4
|
+
"name": "webpack",
|
|
5
|
+
"reason": "Bundler; naturally high entropy in bundled code",
|
|
6
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "terser",
|
|
10
|
+
"reason": "Minifier library; intentional obfuscation",
|
|
11
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "uglify-js",
|
|
15
|
+
"reason": "Minifier library; intentional obfuscation",
|
|
16
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "browserify",
|
|
20
|
+
"reason": "Bundler; bundled JS has high entropy",
|
|
21
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "rollup",
|
|
25
|
+
"reason": "Bundler; bundled JS has high entropy",
|
|
26
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "esbuild",
|
|
30
|
+
"reason": "Bundler/compiler; bundled JS has high entropy",
|
|
31
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "@babel/core",
|
|
35
|
+
"reason": "Transpiler; generated code has high pattern frequency",
|
|
36
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "typescript",
|
|
40
|
+
"reason": "Compiler; generated JS has high entropy",
|
|
41
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "lodash",
|
|
45
|
+
"reason": "Utility library; high pattern frequency from common JS idioms",
|
|
46
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "underscore",
|
|
50
|
+
"reason": "Utility library; high pattern frequency",
|
|
51
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "moment",
|
|
55
|
+
"reason": "Date library; legitimate build artifacts with binary-like data",
|
|
56
|
+
"detectors": ["TIER1-BINARY-EMBED"]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "crypto-js",
|
|
60
|
+
"reason": "Cryptography library; legitimate use of hex/unicode escapes and bitwise ops",
|
|
61
|
+
"detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "preact",
|
|
65
|
+
"reason": "React alternative; shares naming similarity with react, triggering TYPOSQUAT_VPMDHAJ",
|
|
66
|
+
"detectors": ["TYPOSQUAT_VPMDHAJ"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "@commitlint/read",
|
|
70
|
+
"reason": "Legitimate commitlint scoped sub-package; edit-distance FP",
|
|
71
|
+
"detectors": ["TIER1-TYPOSQUAT"]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -11,11 +11,12 @@ const AUTH_CONTEXT_PATHS = [
|
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
const URL_PATH_PATTERN = /request\.url\.path|req\.url\.path|self\.request\.url\.path/g;
|
|
14
|
-
const SCOPE_PATH_PATTERN =
|
|
14
|
+
const SCOPE_PATH_PATTERN =
|
|
15
|
+
/request\.scope\s*\[\s*["']path["']\s*\]|request\.scope\.get\s*\(\s*["']path["']\s*\)/g;
|
|
15
16
|
|
|
16
17
|
function hasAuthContext(filePath) {
|
|
17
18
|
const lower = filePath.toLowerCase();
|
|
18
|
-
return AUTH_CONTEXT_PATHS.some(ctx => lower.includes(ctx));
|
|
19
|
+
return AUTH_CONTEXT_PATHS.some((ctx) => lower.includes(ctx));
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function findFunctionBoundaries(lines) {
|
|
@@ -34,7 +35,13 @@ function findFunctionBoundaries(lines) {
|
|
|
34
35
|
currentFn = defMatch[1];
|
|
35
36
|
fnBodyStart = i;
|
|
36
37
|
indent = line.length - line.trimStart().length;
|
|
37
|
-
} else if (
|
|
38
|
+
} else if (
|
|
39
|
+
currentFn &&
|
|
40
|
+
line.trim() &&
|
|
41
|
+
line.length - line.trimStart().length <= indent &&
|
|
42
|
+
!line.trim().startsWith('#') &&
|
|
43
|
+
!line.trim().startsWith('@')
|
|
44
|
+
) {
|
|
38
45
|
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
|
|
39
46
|
currentFn = null;
|
|
40
47
|
}
|
|
@@ -48,7 +55,9 @@ function findFunctionBoundaries(lines) {
|
|
|
48
55
|
|
|
49
56
|
function hasScopePathInFunction(lines, fnStart, fnEnd) {
|
|
50
57
|
for (let i = fnStart; i <= fnEnd && i < lines.length; i++) {
|
|
51
|
-
if (SCOPE_PATH_PATTERN.test(lines[i]))
|
|
58
|
+
if (SCOPE_PATH_PATTERN.test(lines[i])) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
return false;
|
|
54
63
|
}
|
|
@@ -56,11 +65,15 @@ function hasScopePathInFunction(lines, fnStart, fnEnd) {
|
|
|
56
65
|
export function scanCodePatterns(allFiles) {
|
|
57
66
|
const findings = [];
|
|
58
67
|
|
|
59
|
-
for (const file of
|
|
68
|
+
for (const file of allFiles || []) {
|
|
60
69
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
61
|
-
if (!content)
|
|
70
|
+
if (!content) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
62
73
|
const path = file.path || '';
|
|
63
|
-
if (!path.endsWith('.py'))
|
|
74
|
+
if (!path.endsWith('.py')) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
64
77
|
|
|
65
78
|
const lines = content.split('\n');
|
|
66
79
|
const isAuthContext = hasAuthContext(path);
|
|
@@ -82,14 +95,18 @@ export function scanCodePatterns(allFiles) {
|
|
|
82
95
|
let m;
|
|
83
96
|
while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
|
|
84
97
|
const lineNumber = content.slice(0, m.index).split('\n').length;
|
|
85
|
-
if (suppressedLines.has(lineNumber))
|
|
98
|
+
if (suppressedLines.has(lineNumber)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
86
101
|
findings.push(codePatternAuthFinding(path, lineNumber));
|
|
87
102
|
}
|
|
88
103
|
} else {
|
|
89
104
|
let m;
|
|
90
105
|
while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
|
|
91
106
|
const lineNumber = content.slice(0, m.index).split('\n').length;
|
|
92
|
-
if (suppressedLines.has(lineNumber))
|
|
107
|
+
if (suppressedLines.has(lineNumber)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
93
110
|
findings.push(codePatternInfoFinding(path, lineNumber));
|
|
94
111
|
}
|
|
95
112
|
}
|