@lateos/npm-scan 0.16.0 → 0.16.5
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 +199 -199
- package/LICENSING.md +19 -19
- package/README.de.md +708 -669
- package/README.fr.md +707 -668
- package/README.ja.md +704 -665
- package/README.md +826 -801
- package/README.zh.md +708 -669
- 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/axios-poisoning/d1-version-fingerprint.js +24 -0
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +24 -0
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +90 -0
- package/backend/detectors/axios-poisoning/index.js +94 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +75 -38
- 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/msh-supplement/d1-obfuscation.js +18 -0
- package/backend/detectors/msh-supplement/d2-persistence.js +47 -0
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +35 -0
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +33 -0
- package/backend/detectors/msh-supplement/index.js +107 -0
- package/backend/detectors/tier1-binary-embed.js +219 -0
- package/backend/detectors/tier1-infostealer.js +280 -0
- package/backend/detectors/tier1-lifecycle-hook.js +176 -0
- package/backend/detectors/tier1-metadata-spoof.js +180 -0
- package/backend/detectors/tier1-typosquat.js +219 -0
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +77 -0
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +37 -0
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +66 -0
- package/backend/detectors/typosquat-vpmdhaj/index.js +98 -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 -176
- package/backend/provenance.js +79 -0
- 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/scripts/generate-campaign-fixtures.js +170 -0
- package/src/config/top-5000.json +87 -0
- 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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const TOP_PACKAGES = require('../../src/config/top-5000.json');
|
|
6
|
+
const TOP_SET = new Set(TOP_PACKAGES);
|
|
7
|
+
|
|
8
|
+
function levenshtein(a, b) {
|
|
9
|
+
if (Math.abs(a.length - b.length) > 2) return 3;
|
|
10
|
+
const m = a.length, n = b.length;
|
|
11
|
+
if (m === 0) return n;
|
|
12
|
+
if (n === 0) return m;
|
|
13
|
+
let prev = new Int32Array(n + 1);
|
|
14
|
+
let curr = new Int32Array(n + 1);
|
|
15
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
16
|
+
for (let i = 1; i <= m; i++) {
|
|
17
|
+
curr[0] = i;
|
|
18
|
+
let rowMin = curr[0];
|
|
19
|
+
for (let j = 1; j <= n; j++) {
|
|
20
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
21
|
+
curr[j] = Math.min(
|
|
22
|
+
prev[j] + 1,
|
|
23
|
+
curr[j - 1] + 1,
|
|
24
|
+
prev[j - 1] + cost
|
|
25
|
+
);
|
|
26
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
27
|
+
}
|
|
28
|
+
if (rowMin > 2) return 3;
|
|
29
|
+
const tmp = prev; prev = curr; curr = tmp;
|
|
30
|
+
}
|
|
31
|
+
return prev[n];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function jaroWinkler(a, b) {
|
|
35
|
+
if (a === b) return 1;
|
|
36
|
+
const m = a.length, n = b.length;
|
|
37
|
+
if (m === 0 || n === 0) return 0;
|
|
38
|
+
const matchDist = Math.floor(Math.max(m, n) / 2) - 1;
|
|
39
|
+
const aMatch = new Array(m).fill(false);
|
|
40
|
+
const bMatch = new Array(n).fill(false);
|
|
41
|
+
let matches = 0;
|
|
42
|
+
for (let i = 0; i < m; i++) {
|
|
43
|
+
const start = Math.max(0, i - matchDist);
|
|
44
|
+
const end = Math.min(n, i + matchDist + 1);
|
|
45
|
+
for (let j = start; j < end; j++) {
|
|
46
|
+
if (bMatch[j] || a[i] !== b[j]) continue;
|
|
47
|
+
aMatch[i] = true;
|
|
48
|
+
bMatch[j] = true;
|
|
49
|
+
matches++;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (matches === 0) return 0;
|
|
54
|
+
let t = 0, k = 0;
|
|
55
|
+
for (let i = 0; i < m; i++) {
|
|
56
|
+
if (!aMatch[i]) continue;
|
|
57
|
+
while (!bMatch[k]) k++;
|
|
58
|
+
if (a[i] !== b[k]) t++;
|
|
59
|
+
k++;
|
|
60
|
+
}
|
|
61
|
+
const jaro = (matches / m + matches / n + (matches - t / 2) / matches) / 3;
|
|
62
|
+
let prefix = 0;
|
|
63
|
+
const limit = Math.min(4, m, n);
|
|
64
|
+
for (let i = 0; i < limit; i++) {
|
|
65
|
+
if (a[i] === b[i]) prefix++;
|
|
66
|
+
else break;
|
|
67
|
+
}
|
|
68
|
+
return jaro + prefix * 0.1 * (1 - jaro);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function soundex(s) {
|
|
72
|
+
if (!s) return '';
|
|
73
|
+
s = s.toLowerCase();
|
|
74
|
+
const first = s[0];
|
|
75
|
+
const rest = s.slice(1)
|
|
76
|
+
.replace(/[aeiouyhw]/g, '')
|
|
77
|
+
.replace(/[bfpv]/g, '1')
|
|
78
|
+
.replace(/[cgjkqsxz]/g, '2')
|
|
79
|
+
.replace(/[dt]/g, '3')
|
|
80
|
+
.replace(/l/g, '4')
|
|
81
|
+
.replace(/[mn]/g, '5')
|
|
82
|
+
.replace(/r/g, '6')
|
|
83
|
+
.replace(/(\d)\1+/g, '$1');
|
|
84
|
+
return (first + rest + '000').slice(0, 4);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function homoglyphScore(a, b) {
|
|
88
|
+
if (a.length !== b.length) return 0;
|
|
89
|
+
const map = { '0': 'o', '1': 'l', '3': 'e', '4': 'a', '5': 's', '6': 'g', '7': 't', '8': 'b', '@': 'a' };
|
|
90
|
+
let swaps = 0;
|
|
91
|
+
for (let i = 0; i < a.length; i++) {
|
|
92
|
+
if (a[i] === b[i]) continue;
|
|
93
|
+
const na = map[a[i]] || a[i];
|
|
94
|
+
const nb = map[b[i]] || b[i];
|
|
95
|
+
if (na === b[i] || a[i] === nb || na === nb) swaps++;
|
|
96
|
+
else return 0;
|
|
97
|
+
}
|
|
98
|
+
return swaps > 0 ? 1 - swaps / a.length : 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function computeConfidence(dist, phonetic, homoglyph, registryMeta) {
|
|
102
|
+
let subtype, base;
|
|
103
|
+
if (dist === 1) {
|
|
104
|
+
subtype = 'edit_distance_1';
|
|
105
|
+
base = 75;
|
|
106
|
+
} else if (dist === 2) {
|
|
107
|
+
subtype = 'edit_distance_2';
|
|
108
|
+
base = 45;
|
|
109
|
+
} else if (phonetic > 0.85) {
|
|
110
|
+
subtype = 'phonetic_match';
|
|
111
|
+
base = 50;
|
|
112
|
+
} else if (homoglyph > 0.7) {
|
|
113
|
+
subtype = 'homoglyph_swap';
|
|
114
|
+
base = 60;
|
|
115
|
+
} else {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
let score = base;
|
|
119
|
+
if (registryMeta) {
|
|
120
|
+
const age = registryMeta.age || 0;
|
|
121
|
+
const downloads = registryMeta.weeklyDownloads || 0;
|
|
122
|
+
if (age < 30) score += 15;
|
|
123
|
+
if (downloads < 1000) score += 10;
|
|
124
|
+
if (age > 365 && downloads > 100000) score -= 30;
|
|
125
|
+
}
|
|
126
|
+
score = Math.max(50, Math.min(100, score));
|
|
127
|
+
return { subtype, score };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function severityLabel(score) {
|
|
131
|
+
if (score >= 80) return 'high';
|
|
132
|
+
if (score >= 60) return 'medium';
|
|
133
|
+
return 'low';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function confidenceLabel(score) {
|
|
137
|
+
if (score >= 80) return 'HIGH';
|
|
138
|
+
if (score >= 60) return 'MEDIUM';
|
|
139
|
+
return 'LOW';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const name = 'tier1-typosquat';
|
|
143
|
+
|
|
144
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
145
|
+
const findings = [];
|
|
146
|
+
const pkgName = pkgJson?.name;
|
|
147
|
+
if (!pkgName) return findings;
|
|
148
|
+
|
|
149
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return findings;
|
|
150
|
+
|
|
151
|
+
const namesToCheck = [];
|
|
152
|
+
let scopedName = null;
|
|
153
|
+
|
|
154
|
+
if (pkgName.startsWith('@')) {
|
|
155
|
+
const parts = pkgName.split('/');
|
|
156
|
+
if (parts.length === 2) {
|
|
157
|
+
scopedName = parts[1];
|
|
158
|
+
namesToCheck.push(pkgName, scopedName);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
namesToCheck.push(pkgName);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let best = null;
|
|
165
|
+
let bestScore = 0;
|
|
166
|
+
|
|
167
|
+
for (const checkName of namesToCheck) {
|
|
168
|
+
for (const target of TOP_PACKAGES) {
|
|
169
|
+
if (checkName === target) {
|
|
170
|
+
if (pkgName.startsWith('@') && checkName === scopedName && !KNOWN_REPUTABLE_PACKAGES.has(scopedName)) {
|
|
171
|
+
let score = 80;
|
|
172
|
+
if (registryMeta) {
|
|
173
|
+
if ((registryMeta.age || 0) < 30) score += 15;
|
|
174
|
+
if ((registryMeta.weeklyDownloads || 0) < 1000) score += 10;
|
|
175
|
+
}
|
|
176
|
+
score = Math.max(50, Math.min(100, score));
|
|
177
|
+
if (score > bestScore) {
|
|
178
|
+
bestScore = score;
|
|
179
|
+
best = { target, dist: 0, phonetic: 1, homoglyph: 0, subtype: 'edit_distance_1', isScopeSquat: true };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (Math.abs(checkName.length - target.length) > 2) continue;
|
|
185
|
+
const dist = levenshtein(checkName, target);
|
|
186
|
+
if (dist > 2) continue;
|
|
187
|
+
const phonetic = jaroWinkler(checkName, target);
|
|
188
|
+
const homoglyph = homoglyphScore(checkName, target);
|
|
189
|
+
const conf = computeConfidence(dist, phonetic, homoglyph, registryMeta);
|
|
190
|
+
if (!conf || conf.score <= bestScore) continue;
|
|
191
|
+
bestScore = conf.score;
|
|
192
|
+
best = { target, dist, phonetic, homoglyph, subtype: conf.subtype, isScopeSquat: false };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (best) {
|
|
197
|
+
findings.push({
|
|
198
|
+
detector: 'tier1-typosquat',
|
|
199
|
+
id: 'TIER1-TYPOSQUAT',
|
|
200
|
+
severity: severityLabel(bestScore),
|
|
201
|
+
confidence: confidenceLabel(bestScore),
|
|
202
|
+
confidenceScore: bestScore,
|
|
203
|
+
subtype: best.subtype,
|
|
204
|
+
message: `Package name "${pkgName}" is typo of "${best.target}"${best.dist > 0 ? ` (distance ${best.dist})` : ''}`,
|
|
205
|
+
evidence: [
|
|
206
|
+
`distance: ${best.dist}`,
|
|
207
|
+
`phonetic_score: ${(best.phonetic || 1).toFixed(2)}`,
|
|
208
|
+
`similar_package: ${best.target}`,
|
|
209
|
+
`age_days: ${registryMeta?.age || 0}`,
|
|
210
|
+
`weekly_downloads: ${registryMeta?.weeklyDownloads || 0}`,
|
|
211
|
+
],
|
|
212
|
+
crossFiles: [],
|
|
213
|
+
locations: [{ file: 'package.json', line: 2, column: 10 }],
|
|
214
|
+
reference: 'Campaign 2: Cloud-Secret Typosquatting',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return findings;
|
|
219
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const BLOCKED_MAINTAINERS = ['vpmdhaj'];
|
|
2
|
+
const VPMDHAJ_PREFIX_RE = /^vpmdhaj-/;
|
|
3
|
+
const TYPOSQUAT_TARGETS = [
|
|
4
|
+
'opensearch-setup', 'env-config-manager',
|
|
5
|
+
'express', 'lodash', 'axios', 'react', 'vue', 'angular',
|
|
6
|
+
'babel', 'webpack', 'typescript', 'moment', 'dotenv',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function levenshteinDistance(a, b) {
|
|
10
|
+
const m = a.length, n = b.length;
|
|
11
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
12
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
13
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
14
|
+
for (let i = 1; i <= m; i++) {
|
|
15
|
+
for (let j = 1; j <= n; j++) {
|
|
16
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return dp[m][n];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function scanMaintainerAnomaly(pkgJson, registryMeta) {
|
|
23
|
+
const pkgName = pkgJson?.name || '';
|
|
24
|
+
const currentVersion = pkgJson?.version || '';
|
|
25
|
+
const versionMeta = registryMeta?.versions?.[currentVersion];
|
|
26
|
+
const publisherName = versionMeta?._npmUser?.name || '';
|
|
27
|
+
|
|
28
|
+
if (BLOCKED_MAINTAINERS.includes(publisherName)) {
|
|
29
|
+
return {
|
|
30
|
+
triggered: true,
|
|
31
|
+
stopCondition: true,
|
|
32
|
+
maintainer: publisherName,
|
|
33
|
+
suspiciousAliases: [],
|
|
34
|
+
reason: 'Blocked maintainer detected',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (VPMDHAJ_PREFIX_RE.test(pkgName)) {
|
|
39
|
+
return {
|
|
40
|
+
triggered: true,
|
|
41
|
+
stopCondition: true,
|
|
42
|
+
maintainer: publisherName || 'unknown',
|
|
43
|
+
suspiciousAliases: [pkgName],
|
|
44
|
+
reason: 'Package name matches vpmdhaj attacker namespace',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const suspiciousAliases = [];
|
|
49
|
+
for (const target of TYPOSQUAT_TARGETS) {
|
|
50
|
+
if (pkgName.includes(target) && pkgName !== target && !pkgName.startsWith('@')) {
|
|
51
|
+
const dist = levenshteinDistance(pkgName.toLowerCase(), target.toLowerCase());
|
|
52
|
+
if (dist <= 2 && dist > 0) {
|
|
53
|
+
suspiciousAliases.push(pkgName);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (pkgName.toLowerCase().includes('opensearch')) {
|
|
58
|
+
suspiciousAliases.push(pkgName);
|
|
59
|
+
}
|
|
60
|
+
if (pkgName.toLowerCase().includes('env-config') || pkgName.toLowerCase().includes('envconfig')) {
|
|
61
|
+
suspiciousAliases.push(pkgName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (suspiciousAliases.length > 0) {
|
|
65
|
+
return {
|
|
66
|
+
triggered: true,
|
|
67
|
+
stopCondition: false,
|
|
68
|
+
maintainer: publisherName || 'unknown',
|
|
69
|
+
suspiciousAliases,
|
|
70
|
+
reason: 'Package name typosquats popular package',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { triggered: false, stopCondition: false, maintainer: '', suspiciousAliases: [], reason: '' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { BLOCKED_MAINTAINERS };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SUSPICIOUS_HOOKS = ['preinstall'];
|
|
2
|
+
const LOADER_SCRIPTS = ['setup.mjs', 'loader.js', 'stager.js', 'init.mjs'];
|
|
3
|
+
const BUN_RUN_RE = /\bbun\s+run\b/;
|
|
4
|
+
const NODE_SETUP_RE = /\bnode\s+(setup\.mjs|init\.mjs|loader\.js|stager\.js)\b/;
|
|
5
|
+
const PREINSTALL_STAGER_RE = /preinstall\s*[:=]/;
|
|
6
|
+
|
|
7
|
+
export function scanPreinstallLoader(pkgJson) {
|
|
8
|
+
const scripts = pkgJson?.scripts || {};
|
|
9
|
+
const triggered = [];
|
|
10
|
+
|
|
11
|
+
for (const hook of SUSPICIOUS_HOOKS) {
|
|
12
|
+
const cmd = scripts[hook];
|
|
13
|
+
if (!cmd) continue;
|
|
14
|
+
|
|
15
|
+
const details = { hookType: hook, hookCommand: cmd };
|
|
16
|
+
|
|
17
|
+
if (BUN_RUN_RE.test(cmd)) {
|
|
18
|
+
details.runtimeAbuse = 'Bun as stealthy loader';
|
|
19
|
+
triggered.push(details);
|
|
20
|
+
} else if (NODE_SETUP_RE.test(cmd)) {
|
|
21
|
+
const match = cmd.match(/node\s+(setup\.mjs|init\.mjs|loader\.js|stager\.js)\b/);
|
|
22
|
+
details.generation = match && match[1] === 'stager.js' ? 2 : 1;
|
|
23
|
+
triggered.push(details);
|
|
24
|
+
} else {
|
|
25
|
+
triggered.push(details);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (triggered.length > 0) {
|
|
30
|
+
return {
|
|
31
|
+
triggered: true,
|
|
32
|
+
details: triggered,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { triggered: false, details: [] };
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const AWS_IMDS_RE = /169\.254\.169\.254/;
|
|
2
|
+
const ECS_CRED_RE = /AWS_CONTAINER_AUTHORIZATION_TOKEN|AWS_CONTAINER_CREDENTIALS_FULL_URI/;
|
|
3
|
+
const VAULT_CRED_RE = /VAULT_ADDR|VAULT_TOKEN/;
|
|
4
|
+
const GITHUB_TOKEN_RE = /GITHUB_TOKEN|GH_TOKEN/;
|
|
5
|
+
const AWS_ACCESS_KEY_RE = /AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN/;
|
|
6
|
+
const BASE64_OBFUSCATION_RE = /Buffer\.from\([^)]+['"]base64['"]\)|btoa\(|atob\(/;
|
|
7
|
+
const HTTP_POST_EXFIL_RE = /(?:fetch|axios|request|got|curl)\s*\([^)]*(?:https?:\/\/[^'"\s)\]]+)[^)]*(?:method\s*[:=]\s*['"]POST['"]|\.post\s*\()/;
|
|
8
|
+
const DOMAIN_EXFIL_RE = /(?:fetch|axios|request|got|curl)\s*\(['"](?:https?:\/\/)?[^'"\s)\]]*\.[^'"\s)\]]{2,}[^)]*\)/;
|
|
9
|
+
|
|
10
|
+
const TARGET_ENV_VARS = {
|
|
11
|
+
AWS: ['AWS_CONTAINER_CREDENTIALS_FULL_URI', 'AWS_CONTAINER_AUTHORIZATION_TOKEN', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'],
|
|
12
|
+
VAULT: ['VAULT_ADDR', 'VAULT_TOKEN'],
|
|
13
|
+
GITHUB: ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function scanCredExfil(files = [], pkgJson) {
|
|
17
|
+
const code = files.map(f => f.content || '').join('\n');
|
|
18
|
+
if (!code) return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
|
|
19
|
+
|
|
20
|
+
const targets = [];
|
|
21
|
+
const detectedEnvVars = [];
|
|
22
|
+
|
|
23
|
+
if (AWS_IMDS_RE.test(code)) targets.push('AWS_IMDSv2');
|
|
24
|
+
if (ECS_CRED_RE.test(code)) {
|
|
25
|
+
targets.push('ECS_TASK_ROLE');
|
|
26
|
+
for (const v of TARGET_ENV_VARS.AWS) {
|
|
27
|
+
if (code.includes(v)) detectedEnvVars.push(v);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (VAULT_CRED_RE.test(code)) {
|
|
31
|
+
targets.push('VAULT_CREDENTIALS');
|
|
32
|
+
for (const v of TARGET_ENV_VARS.VAULT) {
|
|
33
|
+
if (code.includes(v)) detectedEnvVars.push(v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (GITHUB_TOKEN_RE.test(code)) {
|
|
37
|
+
targets.push('GITHUB_TOKEN');
|
|
38
|
+
for (const v of TARGET_ENV_VARS.GITHUB) {
|
|
39
|
+
if (code.includes(v)) detectedEnvVars.push(v);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (AWS_ACCESS_KEY_RE.test(code)) {
|
|
43
|
+
targets.push('AWS_ACCESS_KEYS');
|
|
44
|
+
for (const v of TARGET_ENV_VARS.AWS) {
|
|
45
|
+
if (code.includes(v) && !detectedEnvVars.includes(v)) detectedEnvVars.push(v);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (targets.length === 0) return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
|
|
50
|
+
|
|
51
|
+
let exfilMethod = null;
|
|
52
|
+
if (HTTP_POST_EXFIL_RE.test(code)) {
|
|
53
|
+
exfilMethod = 'HTTP POST to attacker domain';
|
|
54
|
+
} else if (DOMAIN_EXFIL_RE.test(code)) {
|
|
55
|
+
exfilMethod = 'HTTP request to external domain';
|
|
56
|
+
} else if (BASE64_OBFUSCATION_RE.test(code)) {
|
|
57
|
+
exfilMethod = 'Base64 obfuscation of credential strings';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
triggered: true,
|
|
62
|
+
targets,
|
|
63
|
+
exfilMethod: exfilMethod || 'Suspicious credential access pattern',
|
|
64
|
+
detectedEnvVars,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { scanMaintainerAnomaly } from './d1-maintainer.js';
|
|
2
|
+
import { scanPreinstallLoader } from './d2-preinstall-loader.js';
|
|
3
|
+
import { scanCredExfil } from './d3-cred-exfil.js';
|
|
4
|
+
import { attachProvenance } from '../../provenance.js';
|
|
5
|
+
|
|
6
|
+
const RULE_SEVERITY = { D1: 'critical', D2: 'critical', D3: 'critical' };
|
|
7
|
+
const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
8
|
+
|
|
9
|
+
function highestSeverity(severities) {
|
|
10
|
+
for (const s of SEVERITY_ORDER) if (severities.includes(s)) return s;
|
|
11
|
+
return 'none';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
|
|
15
|
+
const pkgName = pkgJson?.name || 'unknown';
|
|
16
|
+
const pkgVersion = pkgJson?.version || '0.0.0';
|
|
17
|
+
const fileList = allFiles || files || [];
|
|
18
|
+
|
|
19
|
+
const d1Result = scanMaintainerAnomaly(pkgJson, registryMeta);
|
|
20
|
+
if (d1Result.stopCondition) {
|
|
21
|
+
const evidence = attachProvenance({
|
|
22
|
+
rule: 'TSQ-MAINT-001',
|
|
23
|
+
campaign: 'TYPOSQUAT_VPMDHAJ',
|
|
24
|
+
triggeredChecks: ['D1'],
|
|
25
|
+
maintainer: d1Result.maintainer,
|
|
26
|
+
suspiciousAliases: d1Result.suspiciousAliases,
|
|
27
|
+
action: 'BLOCK',
|
|
28
|
+
}, {
|
|
29
|
+
ruleId: 'TSQ-MAINT-001',
|
|
30
|
+
ruleName: 'Maintainer & Package Alias Anomalies',
|
|
31
|
+
severity: 'CRITICAL',
|
|
32
|
+
campaignName: 'Mass Typosquatting (vpmdhaj)',
|
|
33
|
+
pkgName,
|
|
34
|
+
pkgVersion,
|
|
35
|
+
triggered: true,
|
|
36
|
+
severity: 'critical',
|
|
37
|
+
indicators: [{ type: 'blocked_maintainer', value: d1Result.maintainer }, ...d1Result.suspiciousAliases.map(a => ({ type: 'suspicious_alias', value: a }))],
|
|
38
|
+
ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js',
|
|
39
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return [{
|
|
43
|
+
id: 'TYPOSQUAT_VPMDHAJ',
|
|
44
|
+
severity: 'critical',
|
|
45
|
+
title: 'Mass Typosquatting campaign (vpmdhaj) — blocked maintainer',
|
|
46
|
+
description: d1Result.reason,
|
|
47
|
+
evidence: JSON.stringify(evidence),
|
|
48
|
+
mitigation: 'BLOCK IMMEDIATELY. Do not install packages from maintainer vpmdhaj. Audit all packages from your lockfile for this maintainer. Check for typosquatting of popular packages.',
|
|
49
|
+
stopCondition: true,
|
|
50
|
+
}];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const d2Result = scanPreinstallLoader(pkgJson);
|
|
54
|
+
const d3Result = scanCredExfil(fileList, pkgJson);
|
|
55
|
+
|
|
56
|
+
const results = {
|
|
57
|
+
D1: d1Result,
|
|
58
|
+
D2: d2Result,
|
|
59
|
+
D3: d3Result,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const triggered = Object.entries(results)
|
|
63
|
+
.filter(([_, r]) => r.triggered)
|
|
64
|
+
.map(([id]) => id);
|
|
65
|
+
|
|
66
|
+
if (triggered.length === 0) return [];
|
|
67
|
+
|
|
68
|
+
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
69
|
+
|
|
70
|
+
const evidence = attachProvenance({
|
|
71
|
+
campaign: 'TYPOSQUAT_VPMDHAJ',
|
|
72
|
+
triggeredChecks: triggered,
|
|
73
|
+
details: Object.fromEntries(
|
|
74
|
+
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
75
|
+
),
|
|
76
|
+
}, {
|
|
77
|
+
ruleId: 'TYPOSQUAT_VPMDHAJ',
|
|
78
|
+
ruleName: 'Mass Typosquatting Campaign Detection',
|
|
79
|
+
severity: severity.toUpperCase(),
|
|
80
|
+
campaignName: 'Mass Typosquatting (vpmdhaj)',
|
|
81
|
+
pkgName,
|
|
82
|
+
pkgVersion,
|
|
83
|
+
triggered: true,
|
|
84
|
+
severity,
|
|
85
|
+
indicators: triggered.map(id => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
|
|
86
|
+
ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/',
|
|
87
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return [{
|
|
91
|
+
id: 'TYPOSQUAT_VPMDHAJ',
|
|
92
|
+
severity,
|
|
93
|
+
title: 'Mass Typosquatting campaign (vpmdhaj)',
|
|
94
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
95
|
+
evidence: JSON.stringify(evidence),
|
|
96
|
+
mitigation: 'Block install immediately. Revoke any npm tokens. Rotate CI/CD secrets. Audit all packages from maintainer vpmdhaj. If credential exfiltration detected: rotate AWS IAM keys, Vault tokens, and GitHub tokens immediately. Verify CloudTrail/audit logs for unauthorized access.',
|
|
97
|
+
}];
|
|
98
|
+
}
|