@lateos/npm-scan 0.18.1 → 0.18.2
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/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +233 -199
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +826 -826
- package/README.zh.md +708 -708
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +81 -75
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-cloud-imds.js +124 -0
- package/backend/detectors/tier1-infostealer.js +36 -0
- package/backend/detectors/tier1-multistage-postinstall.js +81 -0
- package/backend/detectors/tier1-version-confusion.js +107 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lastUpdated": "2026-05-24T00:00:00.000Z",
|
|
3
|
-
"waves": {
|
|
4
|
-
"wave1": {
|
|
5
|
-
"id": "mini-shai-hulud-wave1",
|
|
6
|
-
"description": "TanStack CI/CD hijack (mid-May 2026) — 84 malicious versions across 42 packages in ~6 minutes via compromised GitHub Actions CI. Forged SLSA BL3 provenance attestations.",
|
|
7
|
-
"windowMinutes": 6,
|
|
8
|
-
"iocs": [
|
|
9
|
-
{
|
|
10
|
-
"type": "packageScope",
|
|
11
|
-
"value": "@tanstack",
|
|
12
|
-
"maliciousVersionRanges": [],
|
|
13
|
-
"notes": "Seed IOC — update from threat intel feed. Affected: @tanstack/router, @tanstack/react-router, @tanstack/query, @tanstack/form, @tanstack/store, @tanstack/virtual, @tanstack/ranger, @tanstack/table."
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"wave2": {
|
|
18
|
-
"id": "mini-shai-hulud-wave2",
|
|
19
|
-
"description": "AntV/atool maintainer account compromise (late May 2026) — 600+ malicious versions across 300+ packages in ~22 minutes. ~16M weekly download blast radius.",
|
|
20
|
-
"windowMinutes": 22,
|
|
21
|
-
"iocs": [
|
|
22
|
-
{
|
|
23
|
-
"type": "publisherAccount",
|
|
24
|
-
"value": "atool",
|
|
25
|
-
"compromiseWindowStart": "2026-05-20T00:00:00.000Z",
|
|
26
|
-
"compromiseWindowEnd": null,
|
|
27
|
-
"notes": "Seed IOC — compromised @antv/atool maintainer account. Update compromise window from threat intel."
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"type": "packageScope",
|
|
31
|
-
"value": "@antv",
|
|
32
|
-
"maliciousVersionRanges": [],
|
|
33
|
-
"notes": "Blast radius: @antv/g2, @antv/g6, @antv/x6, @antv/l7, echarts-for-react, timeago.js. Seed IOC — update from threat intel."
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
"wave3": {
|
|
38
|
-
"id": "nx-console-wave3",
|
|
39
|
-
"description": "Nx Console 18.95.0 VS Code extension compromise (May 18, 2026, CVE-2026-48027, TeamPCP) — contributor token stolen via TanStack wave1 (May 11), 7-day dwell, malicious extension published using npx to fetch 498KB obfuscated Bun payload from dangling orphan commit on nrwl/nx repo. ~3M installs exposed.",
|
|
40
|
-
"windowMinutes": 36,
|
|
41
|
-
"iocs": [
|
|
42
|
-
{
|
|
43
|
-
"type": "extensionId",
|
|
44
|
-
"value": "nrwl.angular-console",
|
|
45
|
-
"maliciousVersionRanges": ["18.95.0"],
|
|
46
|
-
"notes": "Nx Console v18.95.0 — malicious VS Code extension. CVE-2026-48027. Exposure window: 11 min on Marketplace, 36 min on Open VSX."
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"type": "publisherAccount",
|
|
50
|
-
"value": "nrwl",
|
|
51
|
-
"compromiseWindowStart": "2026-05-11T00:00:00.000Z",
|
|
52
|
-
"compromiseWindowEnd": "2026-05-18T13:09:00.000Z",
|
|
53
|
-
"notes": "Nx contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publishing malicious extension on May 18."
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"type": "packageScope",
|
|
57
|
-
"value": "@nx",
|
|
58
|
-
"maliciousVersionRanges": [],
|
|
59
|
-
"notes": "NX_CONSOLE_DOWNSTREAM: npm packages under @nx scope deployed by compromised Nx contributor. Check for versions published within 7 days of 2026-05-18."
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"type": "packageScope",
|
|
63
|
-
"value": "nrwl",
|
|
64
|
-
"maliciousVersionRanges": [],
|
|
65
|
-
"notes": "NX_CONSOLE_DOWNSTREAM: nrwl-scoped npm packages — monitor for anomalous burst publishing."
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"iocs": [
|
|
71
|
-
{
|
|
72
|
-
"type": "sha512",
|
|
73
|
-
"value": "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
74
|
-
"package": "@antv/g2",
|
|
75
|
-
"wave": 2,
|
|
76
|
-
"notes": "Placeholder sha512 — replace with actual SHA-512 integrity hash from npm dist.integrity of a confirmed malicious version."
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"lastUpdated": "2026-05-24T00:00:00.000Z",
|
|
3
|
+
"waves": {
|
|
4
|
+
"wave1": {
|
|
5
|
+
"id": "mini-shai-hulud-wave1",
|
|
6
|
+
"description": "TanStack CI/CD hijack (mid-May 2026) — 84 malicious versions across 42 packages in ~6 minutes via compromised GitHub Actions CI. Forged SLSA BL3 provenance attestations.",
|
|
7
|
+
"windowMinutes": 6,
|
|
8
|
+
"iocs": [
|
|
9
|
+
{
|
|
10
|
+
"type": "packageScope",
|
|
11
|
+
"value": "@tanstack",
|
|
12
|
+
"maliciousVersionRanges": [],
|
|
13
|
+
"notes": "Seed IOC — update from threat intel feed. Affected: @tanstack/router, @tanstack/react-router, @tanstack/query, @tanstack/form, @tanstack/store, @tanstack/virtual, @tanstack/ranger, @tanstack/table."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"wave2": {
|
|
18
|
+
"id": "mini-shai-hulud-wave2",
|
|
19
|
+
"description": "AntV/atool maintainer account compromise (late May 2026) — 600+ malicious versions across 300+ packages in ~22 minutes. ~16M weekly download blast radius.",
|
|
20
|
+
"windowMinutes": 22,
|
|
21
|
+
"iocs": [
|
|
22
|
+
{
|
|
23
|
+
"type": "publisherAccount",
|
|
24
|
+
"value": "atool",
|
|
25
|
+
"compromiseWindowStart": "2026-05-20T00:00:00.000Z",
|
|
26
|
+
"compromiseWindowEnd": null,
|
|
27
|
+
"notes": "Seed IOC — compromised @antv/atool maintainer account. Update compromise window from threat intel."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "packageScope",
|
|
31
|
+
"value": "@antv",
|
|
32
|
+
"maliciousVersionRanges": [],
|
|
33
|
+
"notes": "Blast radius: @antv/g2, @antv/g6, @antv/x6, @antv/l7, echarts-for-react, timeago.js. Seed IOC — update from threat intel."
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"wave3": {
|
|
38
|
+
"id": "nx-console-wave3",
|
|
39
|
+
"description": "Nx Console 18.95.0 VS Code extension compromise (May 18, 2026, CVE-2026-48027, TeamPCP) — contributor token stolen via TanStack wave1 (May 11), 7-day dwell, malicious extension published using npx to fetch 498KB obfuscated Bun payload from dangling orphan commit on nrwl/nx repo. ~3M installs exposed.",
|
|
40
|
+
"windowMinutes": 36,
|
|
41
|
+
"iocs": [
|
|
42
|
+
{
|
|
43
|
+
"type": "extensionId",
|
|
44
|
+
"value": "nrwl.angular-console",
|
|
45
|
+
"maliciousVersionRanges": ["18.95.0"],
|
|
46
|
+
"notes": "Nx Console v18.95.0 — malicious VS Code extension. CVE-2026-48027. Exposure window: 11 min on Marketplace, 36 min on Open VSX."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "publisherAccount",
|
|
50
|
+
"value": "nrwl",
|
|
51
|
+
"compromiseWindowStart": "2026-05-11T00:00:00.000Z",
|
|
52
|
+
"compromiseWindowEnd": "2026-05-18T13:09:00.000Z",
|
|
53
|
+
"notes": "Nx contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publishing malicious extension on May 18."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "packageScope",
|
|
57
|
+
"value": "@nx",
|
|
58
|
+
"maliciousVersionRanges": [],
|
|
59
|
+
"notes": "NX_CONSOLE_DOWNSTREAM: npm packages under @nx scope deployed by compromised Nx contributor. Check for versions published within 7 days of 2026-05-18."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"type": "packageScope",
|
|
63
|
+
"value": "nrwl",
|
|
64
|
+
"maliciousVersionRanges": [],
|
|
65
|
+
"notes": "NX_CONSOLE_DOWNSTREAM: nrwl-scoped npm packages — monitor for anomalous burst publishing."
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"iocs": [
|
|
71
|
+
{
|
|
72
|
+
"type": "sha512",
|
|
73
|
+
"value": "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
74
|
+
"package": "@antv/g2",
|
|
75
|
+
"wave": 2,
|
|
76
|
+
"notes": "Placeholder sha512 — replace with actual SHA-512 integrity hash from npm dist.integrity of a confirmed malicious version."
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const GCP_PATTERNS = [
|
|
2
|
+
'metadata.google.internal',
|
|
3
|
+
'computeMetadata/v1',
|
|
4
|
+
'metadata.google.internal/computeMetadata',
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const AZURE_PATTERNS = [
|
|
8
|
+
'169.254.169.254/metadata/instance',
|
|
9
|
+
'169.254.169.254/metadata/identity',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const AZURE_IP = '169.254.169.254';
|
|
13
|
+
const METADATA_HEADER_RE = /Metadata\s*:\s*true/i;
|
|
14
|
+
|
|
15
|
+
function severityLabel(score) {
|
|
16
|
+
if (score >= 80) return 'high';
|
|
17
|
+
return 'medium';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function confidenceLabel(score) {
|
|
21
|
+
if (score >= 80) return 'HIGH';
|
|
22
|
+
if (score >= 60) return 'MEDIUM';
|
|
23
|
+
return 'LOW';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasGcpPattern(text) {
|
|
27
|
+
return GCP_PATTERNS.some(p => text.includes(p));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasAzurePath(text) {
|
|
31
|
+
return AZURE_PATTERNS.some(p => text.includes(p));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasAzureHeaderPattern(text) {
|
|
35
|
+
const lines = text.split('\n');
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
if (!lines[i].includes(AZURE_IP)) continue;
|
|
38
|
+
const start = Math.max(0, i - 5);
|
|
39
|
+
const end = Math.min(lines.length, i + 6);
|
|
40
|
+
for (let j = start; j < end; j++) {
|
|
41
|
+
if (METADATA_HEADER_RE.test(lines[j])) return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasAzurePattern(text) {
|
|
48
|
+
return hasAzurePath(text) || hasAzureHeaderPattern(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectTexts(pkgJson, jsFiles) {
|
|
52
|
+
const texts = [];
|
|
53
|
+
|
|
54
|
+
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
55
|
+
for (const value of Object.values(pkgJson.scripts)) {
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
texts.push(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (jsFiles && Array.isArray(jsFiles)) {
|
|
63
|
+
for (const file of jsFiles) {
|
|
64
|
+
if (file?.content && typeof file.content === 'string') {
|
|
65
|
+
texts.push(file.content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return texts;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const name = 'tier1-cloud-imds';
|
|
74
|
+
|
|
75
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
76
|
+
const texts = collectTexts(pkgJson, jsFiles);
|
|
77
|
+
if (texts.length === 0) return [];
|
|
78
|
+
|
|
79
|
+
let hasGcp = false;
|
|
80
|
+
let hasAzure = false;
|
|
81
|
+
|
|
82
|
+
for (const text of texts) {
|
|
83
|
+
if (!hasGcp && hasGcpPattern(text)) hasGcp = true;
|
|
84
|
+
if (!hasAzure && hasAzurePattern(text)) hasAzure = true;
|
|
85
|
+
if (hasGcp && hasAzure) break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!hasGcp && !hasAzure) return [];
|
|
89
|
+
|
|
90
|
+
let confidenceScore;
|
|
91
|
+
let subtype;
|
|
92
|
+
|
|
93
|
+
if (hasGcp && hasAzure) {
|
|
94
|
+
confidenceScore = 92;
|
|
95
|
+
subtype = 'multi_cloud_imds';
|
|
96
|
+
} else if (hasGcp) {
|
|
97
|
+
confidenceScore = 82;
|
|
98
|
+
subtype = 'gcp_metadata';
|
|
99
|
+
} else {
|
|
100
|
+
confidenceScore = 82;
|
|
101
|
+
subtype = 'azure_imds';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [{
|
|
105
|
+
detector: 'tier1-cloud-imds',
|
|
106
|
+
id: 'TIER1-CLOUD-IMDS',
|
|
107
|
+
severity: severityLabel(confidenceScore),
|
|
108
|
+
confidence: confidenceLabel(confidenceScore),
|
|
109
|
+
confidenceScore,
|
|
110
|
+
subtype,
|
|
111
|
+
message: hasGcp && hasAzure
|
|
112
|
+
? `Package references both GCP metadata and Azure IMDS endpoints — cloud credential harvesting`
|
|
113
|
+
: hasGcp
|
|
114
|
+
? `Package references GCP metadata server endpoint — cloud credential harvesting`
|
|
115
|
+
: `Package references Azure IMDS endpoint — cloud credential harvesting`,
|
|
116
|
+
evidence: [
|
|
117
|
+
...(hasGcp ? ['gcp: metadata.google.internal / computeMetadata/v1 pattern detected'] : []),
|
|
118
|
+
...(hasAzure ? ['azure: 169.254.169.254/metadata pattern detected'] : []),
|
|
119
|
+
],
|
|
120
|
+
crossFiles: [],
|
|
121
|
+
locations: [{ file: '', line: 0 }],
|
|
122
|
+
reference: 'Miasma Cloud IMDS',
|
|
123
|
+
}];
|
|
124
|
+
}
|
|
@@ -21,6 +21,11 @@ const EVAL_RE = /\beval\s*\(/g;
|
|
|
21
21
|
const FUNCTION_CTOR_RE = /\bFunction\s*\(/g;
|
|
22
22
|
const B64_STRING_RE = /['"`]([A-Za-z0-9+/]{40,}={0,2})['"`]/g;
|
|
23
23
|
|
|
24
|
+
// Named malware signatures — zero-FP string literals for confirmed campaigns
|
|
25
|
+
const NAMED_SIGNATURES = [
|
|
26
|
+
'Miasma: The Spreading Blight', // Miasma campaign, June 2026, @redhat-cloud-services compromise
|
|
27
|
+
];
|
|
28
|
+
|
|
24
29
|
function shannonEntropy(s) {
|
|
25
30
|
const len = s.length;
|
|
26
31
|
if (len === 0) return 0;
|
|
@@ -171,6 +176,37 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
171
176
|
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
172
177
|
|
|
173
178
|
const files = jsFiles || [];
|
|
179
|
+
|
|
180
|
+
// Named malware signature check — zero-FP string literals, early return
|
|
181
|
+
const sigTexts = [];
|
|
182
|
+
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
183
|
+
for (const value of Object.values(pkgJson.scripts)) {
|
|
184
|
+
if (typeof value === 'string') sigTexts.push(value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const f of files) {
|
|
188
|
+
if (f?.content) sigTexts.push(f.content);
|
|
189
|
+
}
|
|
190
|
+
for (const sig of NAMED_SIGNATURES) {
|
|
191
|
+
for (const text of sigTexts) {
|
|
192
|
+
if (text.includes(sig)) {
|
|
193
|
+
return [{
|
|
194
|
+
detector: 'tier1-infostealer',
|
|
195
|
+
id: 'TIER1-INFOSTEALER',
|
|
196
|
+
severity: 'critical',
|
|
197
|
+
confidence: 'CRITICAL',
|
|
198
|
+
confidenceScore: 98,
|
|
199
|
+
subtype: 'named_signature_miasma',
|
|
200
|
+
message: `Named malware signature detected: "${sig}"`,
|
|
201
|
+
evidence: [sig],
|
|
202
|
+
locations: [{ file: '', line: 0 }],
|
|
203
|
+
crossFiles: [],
|
|
204
|
+
reference: 'Campaign 2 & 3',
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
if (files.length === 0) return [];
|
|
175
211
|
|
|
176
212
|
let parseFailCount = 0;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const SCAN_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
2
|
+
|
|
3
|
+
const REMOTE_FETCH_RE = /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
|
|
4
|
+
const BINARY_EXEC_RE = /\b(?:execFile|execFileSync|execSync|exec|spawnSync|spawn)\s*\(/;
|
|
5
|
+
const DETACHED_RE = /detached\s*:\s*true/;
|
|
6
|
+
|
|
7
|
+
function severityLabel(score) {
|
|
8
|
+
if (score >= 95) return 'critical';
|
|
9
|
+
if (score >= 80) return 'high';
|
|
10
|
+
if (score >= 60) return 'medium';
|
|
11
|
+
return 'low';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function confidenceLabel(score) {
|
|
15
|
+
if (score >= 95) return 'CRITICAL';
|
|
16
|
+
if (score >= 80) return 'HIGH';
|
|
17
|
+
if (score >= 60) return 'MEDIUM';
|
|
18
|
+
return 'LOW';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const name = 'tier1-multistage-postinstall';
|
|
22
|
+
|
|
23
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
24
|
+
const scripts = pkgJson?.scripts;
|
|
25
|
+
if (!scripts || typeof scripts !== 'object') return [];
|
|
26
|
+
|
|
27
|
+
const findings = [];
|
|
28
|
+
|
|
29
|
+
for (const hookName of SCAN_HOOKS) {
|
|
30
|
+
const content = scripts[hookName];
|
|
31
|
+
if (!content || typeof content !== 'string') continue;
|
|
32
|
+
|
|
33
|
+
const hasRemoteFetch = REMOTE_FETCH_RE.test(content);
|
|
34
|
+
const hasBinaryExec = BINARY_EXEC_RE.test(content);
|
|
35
|
+
const hasDetached = DETACHED_RE.test(content);
|
|
36
|
+
|
|
37
|
+
const signalA = hasRemoteFetch && hasBinaryExec;
|
|
38
|
+
const signalB = hasDetached;
|
|
39
|
+
|
|
40
|
+
if (!signalA && !signalB) continue;
|
|
41
|
+
|
|
42
|
+
let confidenceScore;
|
|
43
|
+
let subtype;
|
|
44
|
+
|
|
45
|
+
if (signalA && signalB) {
|
|
46
|
+
confidenceScore = 95;
|
|
47
|
+
subtype = 'two_stage_plus_detached';
|
|
48
|
+
} else if (signalA) {
|
|
49
|
+
confidenceScore = 82;
|
|
50
|
+
subtype = 'two_stage_download_exec';
|
|
51
|
+
} else {
|
|
52
|
+
confidenceScore = 78;
|
|
53
|
+
subtype = 'detached_background_process';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const evidence = [`hook: ${hookName}`];
|
|
57
|
+
if (hasRemoteFetch) evidence.push('pattern: remote fetch call');
|
|
58
|
+
if (hasBinaryExec) evidence.push('pattern: binary execution call');
|
|
59
|
+
if (hasDetached) evidence.push('pattern: detached background process');
|
|
60
|
+
|
|
61
|
+
findings.push({
|
|
62
|
+
detector: 'tier1-multistage-postinstall',
|
|
63
|
+
id: 'TIER1-MULTISTAGE-POSTINSTALL',
|
|
64
|
+
severity: severityLabel(confidenceScore),
|
|
65
|
+
confidence: confidenceLabel(confidenceScore),
|
|
66
|
+
confidenceScore,
|
|
67
|
+
subtype,
|
|
68
|
+
message: `Multi-stage install hook detected in "${hookName}" — ${subtype}`,
|
|
69
|
+
evidence,
|
|
70
|
+
locations: [{
|
|
71
|
+
file: 'package.json',
|
|
72
|
+
field: `scripts.${hookName}`,
|
|
73
|
+
value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
74
|
+
}],
|
|
75
|
+
crossFiles: [],
|
|
76
|
+
reference: 'Sonatype-2026-003429',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return findings;
|
|
81
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const SENTINEL_EXACT = ['99.99.99'];
|
|
4
|
+
const SENTINEL_FAMILY = ['9.9.9', '9.9.10', '10.10.10', '11.11.11'];
|
|
5
|
+
|
|
6
|
+
function severityLabel(score) {
|
|
7
|
+
if (score >= 80) return 'high';
|
|
8
|
+
if (score >= 60) return 'medium';
|
|
9
|
+
return 'low';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function confidenceLabel(score) {
|
|
13
|
+
if (score >= 80) return 'HIGH';
|
|
14
|
+
if (score >= 60) return 'MEDIUM';
|
|
15
|
+
return 'LOW';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseVersion(version) {
|
|
19
|
+
if (!version || typeof version !== 'string') return null;
|
|
20
|
+
const parts = version.split('.');
|
|
21
|
+
if (parts.length !== 3) return null;
|
|
22
|
+
const [major, minor, patch] = parts.map(Number);
|
|
23
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) return null;
|
|
24
|
+
return { major, minor, patch };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function matchesHeuristic(parsed) {
|
|
28
|
+
return parsed.major >= 9 && parsed.minor >= 5 && parsed.patch >= 5 && parsed.major !== 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const name = 'tier1-version-confusion';
|
|
32
|
+
|
|
33
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
34
|
+
const pkgName = pkgJson?.name;
|
|
35
|
+
const version = pkgJson?.version;
|
|
36
|
+
|
|
37
|
+
if (!pkgName || !version) return [];
|
|
38
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
39
|
+
|
|
40
|
+
const parsed = parseVersion(version);
|
|
41
|
+
if (!parsed) return [];
|
|
42
|
+
|
|
43
|
+
const vStr = version;
|
|
44
|
+
|
|
45
|
+
// Priority: SENTINEL_EXACT > SENTINEL_FAMILY > HEURISTIC
|
|
46
|
+
if (SENTINEL_EXACT.includes(vStr)) {
|
|
47
|
+
const score = 85;
|
|
48
|
+
return [{
|
|
49
|
+
detector: 'tier1-version-confusion',
|
|
50
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
51
|
+
severity: severityLabel(score),
|
|
52
|
+
confidence: confidenceLabel(score),
|
|
53
|
+
confidenceScore: score,
|
|
54
|
+
subtype: 'sentinel_exact',
|
|
55
|
+
message: `Package "${pkgName}" uses exact sentinel version ${vStr} — dependency confusion indicator`,
|
|
56
|
+
evidence: [
|
|
57
|
+
`version: ${vStr}`,
|
|
58
|
+
`sentinel: exact match`,
|
|
59
|
+
],
|
|
60
|
+
crossFiles: [],
|
|
61
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
62
|
+
reference: 'Sonatype-2026-003429',
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (SENTINEL_FAMILY.includes(vStr)) {
|
|
67
|
+
const score = 65;
|
|
68
|
+
return [{
|
|
69
|
+
detector: 'tier1-version-confusion',
|
|
70
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
71
|
+
severity: severityLabel(score),
|
|
72
|
+
confidence: confidenceLabel(score),
|
|
73
|
+
confidenceScore: score,
|
|
74
|
+
subtype: 'sentinel_family',
|
|
75
|
+
message: `Package "${pkgName}" uses sentinel family version ${vStr} — dependency confusion indicator`,
|
|
76
|
+
evidence: [
|
|
77
|
+
`version: ${vStr}`,
|
|
78
|
+
`sentinel: family match`,
|
|
79
|
+
],
|
|
80
|
+
crossFiles: [],
|
|
81
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
82
|
+
reference: 'Sonatype-2026-003429',
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (matchesHeuristic(parsed)) {
|
|
87
|
+
const score = 62;
|
|
88
|
+
return [{
|
|
89
|
+
detector: 'tier1-version-confusion',
|
|
90
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
91
|
+
severity: severityLabel(score),
|
|
92
|
+
confidence: confidenceLabel(score),
|
|
93
|
+
confidenceScore: score,
|
|
94
|
+
subtype: 'high_version_heuristic',
|
|
95
|
+
message: `Package "${pkgName}" version ${vStr} matches high-version heuristic — possible dependency confusion`,
|
|
96
|
+
evidence: [
|
|
97
|
+
`version: ${vStr}`,
|
|
98
|
+
`major: ${parsed.major}, minor: ${parsed.minor}, patch: ${parsed.patch}`,
|
|
99
|
+
],
|
|
100
|
+
crossFiles: [],
|
|
101
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
102
|
+
reference: 'Microsoft Scope Confusion',
|
|
103
|
+
}];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [];
|
|
107
|
+
}
|