@lateos/npm-scan 1.0.0 → 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/README.md +864 -861
- 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
|
@@ -4,23 +4,37 @@ const SENTINEL_EXACT = ['99.99.99'];
|
|
|
4
4
|
const SENTINEL_FAMILY = ['9.9.9', '9.9.10', '10.10.10', '11.11.11'];
|
|
5
5
|
|
|
6
6
|
function severityLabel(score) {
|
|
7
|
-
if (score >= 80)
|
|
8
|
-
|
|
7
|
+
if (score >= 80) {
|
|
8
|
+
return 'high';
|
|
9
|
+
}
|
|
10
|
+
if (score >= 60) {
|
|
11
|
+
return 'medium';
|
|
12
|
+
}
|
|
9
13
|
return 'low';
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
function confidenceLabel(score) {
|
|
13
|
-
if (score >= 80)
|
|
14
|
-
|
|
17
|
+
if (score >= 80) {
|
|
18
|
+
return 'HIGH';
|
|
19
|
+
}
|
|
20
|
+
if (score >= 60) {
|
|
21
|
+
return 'MEDIUM';
|
|
22
|
+
}
|
|
15
23
|
return 'LOW';
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
function parseVersion(version) {
|
|
19
|
-
if (!version || typeof version !== 'string')
|
|
27
|
+
if (!version || typeof version !== 'string') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
20
30
|
const parts = version.split('.');
|
|
21
|
-
if (parts.length !== 3)
|
|
31
|
+
if (parts.length !== 3) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
22
34
|
const [major, minor, patch] = parts.map(Number);
|
|
23
|
-
if (isNaN(major) || isNaN(minor) || isNaN(patch))
|
|
35
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
24
38
|
return { major, minor, patch };
|
|
25
39
|
}
|
|
26
40
|
|
|
@@ -30,77 +44,83 @@ function matchesHeuristic(parsed) {
|
|
|
30
44
|
|
|
31
45
|
export const name = 'tier1-version-confusion';
|
|
32
46
|
|
|
33
|
-
export async function scan(pkgJson,
|
|
47
|
+
export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
|
|
34
48
|
const pkgName = pkgJson?.name;
|
|
35
49
|
const version = pkgJson?.version;
|
|
36
50
|
|
|
37
|
-
if (!pkgName || !version)
|
|
38
|
-
|
|
51
|
+
if (!pkgName || !version) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
39
57
|
|
|
40
58
|
const parsed = parseVersion(version);
|
|
41
|
-
if (!parsed)
|
|
59
|
+
if (!parsed) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
42
62
|
|
|
43
63
|
const vStr = version;
|
|
44
64
|
|
|
45
65
|
// Priority: SENTINEL_EXACT > SENTINEL_FAMILY > HEURISTIC
|
|
46
66
|
if (SENTINEL_EXACT.includes(vStr)) {
|
|
47
67
|
const score = 85;
|
|
48
|
-
return [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`version: ${vStr}`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}];
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
detector: 'tier1-version-confusion',
|
|
71
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
72
|
+
severity: severityLabel(score),
|
|
73
|
+
confidence: confidenceLabel(score),
|
|
74
|
+
confidenceScore: score,
|
|
75
|
+
subtype: 'sentinel_exact',
|
|
76
|
+
message: `Package "${pkgName}" uses exact sentinel version ${vStr} — dependency confusion indicator`,
|
|
77
|
+
evidence: [`version: ${vStr}`, `sentinel: exact match`],
|
|
78
|
+
crossFiles: [],
|
|
79
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
80
|
+
reference: 'Sonatype-2026-003429',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
if (SENTINEL_FAMILY.includes(vStr)) {
|
|
67
86
|
const score = 65;
|
|
68
|
-
return [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`version: ${vStr}`,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}];
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
detector: 'tier1-version-confusion',
|
|
90
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
91
|
+
severity: severityLabel(score),
|
|
92
|
+
confidence: confidenceLabel(score),
|
|
93
|
+
confidenceScore: score,
|
|
94
|
+
subtype: 'sentinel_family',
|
|
95
|
+
message: `Package "${pkgName}" uses sentinel family version ${vStr} — dependency confusion indicator`,
|
|
96
|
+
evidence: [`version: ${vStr}`, `sentinel: family match`],
|
|
97
|
+
crossFiles: [],
|
|
98
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
99
|
+
reference: 'Sonatype-2026-003429',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
if (matchesHeuristic(parsed)) {
|
|
87
105
|
const score = 62;
|
|
88
|
-
return [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
detector: 'tier1-version-confusion',
|
|
109
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
110
|
+
severity: severityLabel(score),
|
|
111
|
+
confidence: confidenceLabel(score),
|
|
112
|
+
confidenceScore: score,
|
|
113
|
+
subtype: 'high_version_heuristic',
|
|
114
|
+
message: `Package "${pkgName}" version ${vStr} matches high-version heuristic — possible dependency confusion`,
|
|
115
|
+
evidence: [
|
|
116
|
+
`version: ${vStr}`,
|
|
117
|
+
`major: ${parsed.major}, minor: ${parsed.minor}, patch: ${parsed.patch}`,
|
|
118
|
+
],
|
|
119
|
+
crossFiles: [],
|
|
120
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
121
|
+
reference: 'Microsoft Scope Confusion',
|
|
122
|
+
},
|
|
123
|
+
];
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
return [];
|
|
@@ -10,7 +10,9 @@ export function scanCampaignMarker(allFiles) {
|
|
|
10
10
|
const ext = path.includes('.') ? '.' + path.split('.').pop() : '';
|
|
11
11
|
|
|
12
12
|
const isTarget = TARGET_FILENAMES.has(basename) || TARGET_EXTENSIONS.includes(ext);
|
|
13
|
-
if (!isTarget)
|
|
13
|
+
if (!isTarget) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
if (content.includes('P-2024-001')) {
|
|
16
18
|
matches.push({ file: path });
|
|
@@ -12,7 +12,7 @@ export function scanPayloadFingerprint(allFiles) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
if (byteSize === 48485) {
|
|
15
|
-
const alreadyMatched = matches.some(m => m.file === path);
|
|
15
|
+
const alreadyMatched = matches.some((m) => m.file === path);
|
|
16
16
|
if (!alreadyMatched) {
|
|
17
17
|
matches.push({ file: path, matchType: 'byteSize', byteSize });
|
|
18
18
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export function scanPublisherBlocklist(pkgJson, registryMeta) {
|
|
2
|
-
const publisherAccount =
|
|
3
|
-
|
|
4
|
-
||
|
|
2
|
+
const publisherAccount =
|
|
3
|
+
registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name ||
|
|
4
|
+
registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name ||
|
|
5
|
+
null;
|
|
5
6
|
|
|
6
7
|
if (publisherAccount === 'asdxzxc') {
|
|
7
8
|
return { triggered: true, publisher: publisherAccount };
|
|
@@ -3,8 +3,10 @@ const C2_PATTERNS = [/ddjidd564\.github\.io/i, /gist\.github\.com/i];
|
|
|
3
3
|
|
|
4
4
|
function scanContent(content, filePath) {
|
|
5
5
|
const matches = [];
|
|
6
|
-
const hasC2 = C2_PATTERNS.some(p => p.test(content));
|
|
7
|
-
if (!hasC2)
|
|
6
|
+
const hasC2 = C2_PATTERNS.some((p) => p.test(content));
|
|
7
|
+
if (!hasC2) {
|
|
8
|
+
return matches;
|
|
9
|
+
}
|
|
8
10
|
|
|
9
11
|
const hasCredPath = CRED_PATH_PATTERNS.test(content);
|
|
10
12
|
if (hasCredPath) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const ZERO_WIDTH_RANGES = [
|
|
2
|
-
[
|
|
3
|
-
[
|
|
2
|
+
[0x200b, 0x200d],
|
|
3
|
+
[0xfeff, 0xfeff],
|
|
4
4
|
];
|
|
5
5
|
|
|
6
6
|
function isZeroWidthChar(code) {
|
|
@@ -14,7 +14,9 @@ export function scanAIPoisoning(allFiles) {
|
|
|
14
14
|
|
|
15
15
|
for (const file of allFiles) {
|
|
16
16
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
17
|
-
if (!TARGET_FILES.test(path))
|
|
17
|
+
if (!TARGET_FILES.test(path)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
18
20
|
|
|
19
21
|
const content = file.content || '';
|
|
20
22
|
const found = [];
|
|
@@ -12,16 +12,21 @@ const LURE_PATTERNS = [
|
|
|
12
12
|
|
|
13
13
|
export function scanLureName(pkgJson, registryMeta) {
|
|
14
14
|
const pkgName = pkgJson?.name || '';
|
|
15
|
-
const matchedPattern = LURE_PATTERNS.find(p => p.test(pkgName));
|
|
16
|
-
if (!matchedPattern)
|
|
15
|
+
const matchedPattern = LURE_PATTERNS.find((p) => p.test(pkgName));
|
|
16
|
+
if (!matchedPattern) {
|
|
17
|
+
return { triggered: false };
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
const timeMap = registryMeta?.time || {};
|
|
19
|
-
const versions = Object.keys(timeMap).filter(v => v !== 'created' && v !== 'modified');
|
|
20
|
-
const firstVersion =
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const versions = Object.keys(timeMap).filter((v) => v !== 'created' && v !== 'modified');
|
|
22
|
+
const firstVersion =
|
|
23
|
+
versions.length > 0
|
|
24
|
+
? versions.sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]))[0]
|
|
25
|
+
: null;
|
|
23
26
|
|
|
24
|
-
if (!firstVersion)
|
|
27
|
+
if (!firstVersion) {
|
|
28
|
+
return { triggered: false };
|
|
29
|
+
}
|
|
25
30
|
|
|
26
31
|
const firstPubDate = new Date(timeMap[firstVersion]);
|
|
27
32
|
const now = new Date();
|
|
@@ -7,8 +7,8 @@ export function scanCryptoPrimitives(allFiles, pkgJson) {
|
|
|
7
7
|
.map(([hook, content]) => ({ file: `script:${hook}`, content }));
|
|
8
8
|
|
|
9
9
|
const jsFiles = allFiles
|
|
10
|
-
.filter(f => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
|
|
11
|
-
.map(f => ({ file: f.path, content: f.content || '' }));
|
|
10
|
+
.filter((f) => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
|
|
11
|
+
.map((f) => ({ file: f.path, content: f.content || '' }));
|
|
12
12
|
|
|
13
13
|
for (const { file, content } of [...scriptEntries, ...jsFiles]) {
|
|
14
14
|
const hasFernet = /Fernet/i.test(content);
|
|
@@ -2,9 +2,14 @@ export function scanXorKey(allFiles) {
|
|
|
2
2
|
const matches = [];
|
|
3
3
|
for (const file of allFiles) {
|
|
4
4
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
5
|
-
const isLockFile =
|
|
5
|
+
const isLockFile =
|
|
6
|
+
/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|pnpm-lock\.yml|Cargo\.lock|Cargo\.toml)/i.test(
|
|
7
|
+
path
|
|
8
|
+
);
|
|
6
9
|
const isBundled = /\.node$|vendor|native/i.test(path);
|
|
7
|
-
if (!isLockFile && !isBundled)
|
|
10
|
+
if (!isLockFile && !isBundled) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
const content = file.content || '';
|
|
10
15
|
if (content.includes('cargo-build-helper-2026')) {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
const CRED_VALIDATION_PATTERNS = [
|
|
2
|
-
/sts\.amazonaws\.com/i,
|
|
3
|
-
/api\.github\.com\/user/i,
|
|
4
|
-
];
|
|
1
|
+
const CRED_VALIDATION_PATTERNS = [/sts\.amazonaws\.com/i, /api\.github\.com\/user/i];
|
|
5
2
|
|
|
6
3
|
export function scanCredValidation(allFiles, pkgJson) {
|
|
7
4
|
const matches = [];
|
|
@@ -19,7 +16,9 @@ export function scanCredValidation(allFiles, pkgJson) {
|
|
|
19
16
|
|
|
20
17
|
for (const file of allFiles) {
|
|
21
18
|
const path = file.path || '';
|
|
22
|
-
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs'))
|
|
19
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
23
22
|
const content = file.content || '';
|
|
24
23
|
for (const pattern of CRED_VALIDATION_PATTERNS) {
|
|
25
24
|
if (pattern.test(content)) {
|
|
@@ -24,7 +24,9 @@ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
|
24
24
|
|
|
25
25
|
function highestSeverity(severities) {
|
|
26
26
|
for (const s of SEVERITY_ORDER) {
|
|
27
|
-
if (severities.includes(s))
|
|
27
|
+
if (severities.includes(s)) {
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
28
30
|
}
|
|
29
31
|
return 'none';
|
|
30
32
|
}
|
|
@@ -48,16 +50,16 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
48
50
|
.filter(([_, r]) => r.triggered)
|
|
49
51
|
.map(([id]) => id);
|
|
50
52
|
|
|
51
|
-
if (triggered.length === 0)
|
|
53
|
+
if (triggered.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
52
56
|
|
|
53
|
-
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
57
|
+
const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
|
|
54
58
|
|
|
55
59
|
const evidence = {
|
|
56
60
|
campaign: 'TRAPDOOR',
|
|
57
61
|
triggeredRules: triggered,
|
|
58
|
-
details: Object.fromEntries(
|
|
59
|
-
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
60
|
-
),
|
|
62
|
+
details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
|
|
61
63
|
iocSummary: {
|
|
62
64
|
publisher: 'asdxzxc',
|
|
63
65
|
c2Domain: 'ddjidd564.github.io',
|
|
@@ -66,12 +68,15 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
66
68
|
},
|
|
67
69
|
};
|
|
68
70
|
|
|
69
|
-
return [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
id: 'TRAPDOOR',
|
|
74
|
+
severity,
|
|
75
|
+
title: 'TrapDoor cross-ecosystem supply chain attack campaign',
|
|
76
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
77
|
+
evidence: JSON.stringify(evidence),
|
|
78
|
+
mitigation:
|
|
79
|
+
'Block install immediately. Revoke any npm tokens associated with this package. Rotate CI/CD secrets. Audit for postinstall scripts accessing credentials. Check for AI config poisoning (.cursorrules/CLAUDE.md). Verify all package versions from publisher asdxzxc. If confirmed compromise, follow incident response procedures per SECURITY.md.',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
77
82
|
}
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
const BLOCKED_MAINTAINERS = ['vpmdhaj'];
|
|
2
2
|
const VPMDHAJ_PREFIX_RE = /^vpmdhaj-/;
|
|
3
3
|
const TYPOSQUAT_TARGETS = [
|
|
4
|
-
'opensearch-setup',
|
|
5
|
-
'
|
|
6
|
-
'
|
|
4
|
+
'opensearch-setup',
|
|
5
|
+
'env-config-manager',
|
|
6
|
+
'express',
|
|
7
|
+
'lodash',
|
|
8
|
+
'axios',
|
|
9
|
+
'react',
|
|
10
|
+
'vue',
|
|
11
|
+
'angular',
|
|
12
|
+
'babel',
|
|
13
|
+
'webpack',
|
|
14
|
+
'typescript',
|
|
15
|
+
'moment',
|
|
16
|
+
'dotenv',
|
|
7
17
|
];
|
|
8
18
|
|
|
9
19
|
function levenshteinDistance(a, b) {
|
|
10
|
-
const m = a.length,
|
|
20
|
+
const m = a.length,
|
|
21
|
+
n = b.length;
|
|
11
22
|
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
12
|
-
for (let i = 0; i <= m; i++)
|
|
13
|
-
|
|
23
|
+
for (let i = 0; i <= m; i++) {
|
|
24
|
+
dp[i][0] = i;
|
|
25
|
+
}
|
|
26
|
+
for (let j = 0; j <= n; j++) {
|
|
27
|
+
dp[0][j] = j;
|
|
28
|
+
}
|
|
14
29
|
for (let i = 1; i <= m; i++) {
|
|
15
30
|
for (let j = 1; j <= n; j++) {
|
|
16
|
-
dp[i][j] =
|
|
31
|
+
dp[i][j] =
|
|
32
|
+
a[i - 1] === b[j - 1]
|
|
33
|
+
? dp[i - 1][j - 1]
|
|
34
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
17
35
|
}
|
|
18
36
|
}
|
|
19
37
|
return dp[m][n];
|
|
@@ -71,7 +89,13 @@ export function scanMaintainerAnomaly(pkgJson, registryMeta) {
|
|
|
71
89
|
};
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
triggered: false,
|
|
94
|
+
stopCondition: false,
|
|
95
|
+
maintainer: '',
|
|
96
|
+
suspiciousAliases: [],
|
|
97
|
+
reason: '',
|
|
98
|
+
};
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
export { BLOCKED_MAINTAINERS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const SUSPICIOUS_HOOKS = ['preinstall'];
|
|
2
|
-
const
|
|
2
|
+
const _LOADER_SCRIPTS = ['setup.mjs', 'loader.js', 'stager.js', 'init.mjs'];
|
|
3
3
|
const BUN_RUN_RE = /\bbun\s+run\b/;
|
|
4
4
|
const NODE_SETUP_RE = /\bnode\s+(setup\.mjs|init\.mjs|loader\.js|stager\.js)\b/;
|
|
5
|
-
const
|
|
5
|
+
const _PREINSTALL_STAGER_RE = /preinstall\s*[:=]/;
|
|
6
6
|
|
|
7
7
|
export function scanPreinstallLoader(pkgJson) {
|
|
8
8
|
const scripts = pkgJson?.scripts || {};
|
|
@@ -10,7 +10,9 @@ export function scanPreinstallLoader(pkgJson) {
|
|
|
10
10
|
|
|
11
11
|
for (const hook of SUSPICIOUS_HOOKS) {
|
|
12
12
|
const cmd = scripts[hook];
|
|
13
|
-
if (!cmd)
|
|
13
|
+
if (!cmd) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
const details = { hookType: hook, hookCommand: cmd };
|
|
16
18
|
|
|
@@ -4,49 +4,71 @@ const VAULT_CRED_RE = /VAULT_ADDR|VAULT_TOKEN/;
|
|
|
4
4
|
const GITHUB_TOKEN_RE = /GITHUB_TOKEN|GH_TOKEN/;
|
|
5
5
|
const AWS_ACCESS_KEY_RE = /AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN/;
|
|
6
6
|
const BASE64_OBFUSCATION_RE = /Buffer\.from\([^)]+['"]base64['"]\)|btoa\(|atob\(/;
|
|
7
|
-
const HTTP_POST_EXFIL_RE =
|
|
8
|
-
|
|
7
|
+
const HTTP_POST_EXFIL_RE =
|
|
8
|
+
/(?:fetch|axios|request|got|curl)\s*\([^)]*(?:https?:\/\/[^'"\s)\]]+)[^)]*(?:method\s*[:=]\s*['"]POST['"]|\.post\s*\()/;
|
|
9
|
+
const DOMAIN_EXFIL_RE =
|
|
10
|
+
/(?:fetch|axios|request|got|curl)\s*\(['"](?:https?:\/\/)?[^'"\s)\]]*\.[^'"\s)\]]{2,}[^)]*\)/;
|
|
9
11
|
|
|
10
12
|
const TARGET_ENV_VARS = {
|
|
11
|
-
AWS: [
|
|
13
|
+
AWS: [
|
|
14
|
+
'AWS_CONTAINER_CREDENTIALS_FULL_URI',
|
|
15
|
+
'AWS_CONTAINER_AUTHORIZATION_TOKEN',
|
|
16
|
+
'AWS_ACCESS_KEY_ID',
|
|
17
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
18
|
+
'AWS_SESSION_TOKEN',
|
|
19
|
+
],
|
|
12
20
|
VAULT: ['VAULT_ADDR', 'VAULT_TOKEN'],
|
|
13
21
|
GITHUB: ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
14
22
|
};
|
|
15
23
|
|
|
16
|
-
export function scanCredExfil(files = [],
|
|
17
|
-
const code = files.map(f => f.content || '').join('\n');
|
|
18
|
-
if (!code)
|
|
24
|
+
export function scanCredExfil(files = [], _pkgJson) {
|
|
25
|
+
const code = files.map((f) => f.content || '').join('\n');
|
|
26
|
+
if (!code) {
|
|
27
|
+
return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
const targets = [];
|
|
21
31
|
const detectedEnvVars = [];
|
|
22
32
|
|
|
23
|
-
if (AWS_IMDS_RE.test(code))
|
|
33
|
+
if (AWS_IMDS_RE.test(code)) {
|
|
34
|
+
targets.push('AWS_IMDSv2');
|
|
35
|
+
}
|
|
24
36
|
if (ECS_CRED_RE.test(code)) {
|
|
25
37
|
targets.push('ECS_TASK_ROLE');
|
|
26
38
|
for (const v of TARGET_ENV_VARS.AWS) {
|
|
27
|
-
if (code.includes(v))
|
|
39
|
+
if (code.includes(v)) {
|
|
40
|
+
detectedEnvVars.push(v);
|
|
41
|
+
}
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
44
|
if (VAULT_CRED_RE.test(code)) {
|
|
31
45
|
targets.push('VAULT_CREDENTIALS');
|
|
32
46
|
for (const v of TARGET_ENV_VARS.VAULT) {
|
|
33
|
-
if (code.includes(v))
|
|
47
|
+
if (code.includes(v)) {
|
|
48
|
+
detectedEnvVars.push(v);
|
|
49
|
+
}
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
if (GITHUB_TOKEN_RE.test(code)) {
|
|
37
53
|
targets.push('GITHUB_TOKEN');
|
|
38
54
|
for (const v of TARGET_ENV_VARS.GITHUB) {
|
|
39
|
-
if (code.includes(v))
|
|
55
|
+
if (code.includes(v)) {
|
|
56
|
+
detectedEnvVars.push(v);
|
|
57
|
+
}
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
if (AWS_ACCESS_KEY_RE.test(code)) {
|
|
43
61
|
targets.push('AWS_ACCESS_KEYS');
|
|
44
62
|
for (const v of TARGET_ENV_VARS.AWS) {
|
|
45
|
-
if (code.includes(v) && !detectedEnvVars.includes(v))
|
|
63
|
+
if (code.includes(v) && !detectedEnvVars.includes(v)) {
|
|
64
|
+
detectedEnvVars.push(v);
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
if (targets.length === 0)
|
|
69
|
+
if (targets.length === 0) {
|
|
70
|
+
return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
let exfilMethod = null;
|
|
52
74
|
if (HTTP_POST_EXFIL_RE.test(code)) {
|