@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
|
@@ -8,36 +8,49 @@ const CHILD_PROC_RE = /\b(?:spawn|exec|execSync|spawnSync|fork)\s*\(/g;
|
|
|
8
8
|
const FS_CHMOD_RE = /fs\.chmod\s*\(/g;
|
|
9
9
|
|
|
10
10
|
function detectMagicBytes(content) {
|
|
11
|
-
if (!content || content.length < 4)
|
|
11
|
+
if (!content || content.length < 4) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
12
14
|
|
|
13
15
|
const c0 = content.charCodeAt(0);
|
|
14
16
|
const c1 = content.charCodeAt(1);
|
|
15
17
|
const c2 = content.charCodeAt(2);
|
|
16
18
|
const c3 = content.charCodeAt(3);
|
|
17
19
|
|
|
18
|
-
if (c0 === 0x7f && content.slice(1, 4) === 'ELF')
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
if (c0 === 0x7f && content.slice(1, 4) === 'ELF') {
|
|
21
|
+
return 'elf_embedded';
|
|
22
|
+
}
|
|
23
|
+
if (c0 === 0x4d && c1 === 0x5a) {
|
|
24
|
+
return 'pe_embedded';
|
|
25
|
+
}
|
|
26
|
+
if (c0 === 0x00 && content.slice(1, 4) === 'asm') {
|
|
27
|
+
return 'wasm_embedded';
|
|
28
|
+
}
|
|
21
29
|
|
|
22
|
-
const machO =
|
|
30
|
+
const machO =
|
|
31
|
+
(c0 === 0xfe && c1 === 0xed && c2 === 0xfa && (c3 === 0xce || c3 === 0xcf)) ||
|
|
23
32
|
(c0 === 0xce && c1 === 0xfa && c2 === 0xed && (c3 === 0xfe || c3 === 0xcf)) ||
|
|
24
33
|
(c0 === 0xcf && c1 === 0xfa && c2 === 0xed && c3 === 0xfe);
|
|
25
|
-
if (machO)
|
|
34
|
+
if (machO) {
|
|
35
|
+
return 'macho_embedded';
|
|
36
|
+
}
|
|
26
37
|
|
|
27
38
|
const universal = c0 === 0xca && c1 === 0xfe && c2 === 0xba && c3 === 0xbe;
|
|
28
|
-
if (universal)
|
|
39
|
+
if (universal) {
|
|
40
|
+
return 'macho_embedded';
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
return null;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
function isInBinaryDir(filePath) {
|
|
34
47
|
const normalized = filePath.replace(/\\/g, '/');
|
|
35
|
-
return BINARY_DIRS.some(dir => normalized.includes(`/${dir}`) || normalized.startsWith(dir));
|
|
48
|
+
return BINARY_DIRS.some((dir) => normalized.includes(`/${dir}`) || normalized.startsWith(dir));
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
function hasBinaryExt(filePath) {
|
|
39
52
|
const lower = filePath.toLowerCase();
|
|
40
|
-
return BINARY_EXTS.some(ext => lower.endsWith(ext));
|
|
53
|
+
return BINARY_EXTS.some((ext) => lower.endsWith(ext));
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
function isKnownBinaryName(fileName) {
|
|
@@ -45,13 +58,16 @@ function isKnownBinaryName(fileName) {
|
|
|
45
58
|
return BINARY_FILENAMES.includes(base);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
const CROSS_PLATFORM_RE =
|
|
61
|
+
const CROSS_PLATFORM_RE =
|
|
62
|
+
/-(?:linux|darwin|macos|win32|windows|win)-(?:x64|x86|arm64|ia32)\.?(?:exe)?$/i;
|
|
49
63
|
|
|
50
64
|
function detectCrossPlatformSets(binaries) {
|
|
51
65
|
const sets = {};
|
|
52
66
|
for (const bin of binaries) {
|
|
53
67
|
const base = bin.file.replace(CROSS_PLATFORM_RE, '').split(/[/\\]/).pop();
|
|
54
|
-
if (!sets[base])
|
|
68
|
+
if (!sets[base]) {
|
|
69
|
+
sets[base] = [];
|
|
70
|
+
}
|
|
55
71
|
sets[base].push(bin.file);
|
|
56
72
|
}
|
|
57
73
|
for (const [base, files] of Object.entries(sets)) {
|
|
@@ -63,22 +79,39 @@ function detectCrossPlatformSets(binaries) {
|
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
function isDeclared(pkgJson, fileName) {
|
|
66
|
-
if (!pkgJson)
|
|
82
|
+
if (!pkgJson) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
67
85
|
const baseName = fileName.split(/[/\\]/).pop();
|
|
68
86
|
|
|
69
87
|
if (pkgJson.bin) {
|
|
70
|
-
if (typeof pkgJson.bin === 'string' && pkgJson.bin === baseName)
|
|
71
|
-
|
|
88
|
+
if (typeof pkgJson.bin === 'string' && pkgJson.bin === baseName) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
typeof pkgJson.bin === 'object' &&
|
|
93
|
+
Object.values(pkgJson.bin).some((v) => v === baseName || v.endsWith(`/${baseName}`))
|
|
94
|
+
) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
if (pkgJson.optionalDependencies) {
|
|
75
|
-
for (const [name,
|
|
76
|
-
if (name === baseName)
|
|
100
|
+
for (const [name, _val] of Object.entries(pkgJson.optionalDependencies)) {
|
|
101
|
+
if (name === baseName) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
107
|
+
if (
|
|
108
|
+
pkgJson.gypfile === true ||
|
|
109
|
+
pkgJson.scripts?.install?.includes('node-gyp') ||
|
|
110
|
+
pkgJson.scripts?.install?.includes('node-pre-gyp')
|
|
111
|
+
) {
|
|
112
|
+
if (baseName.endsWith('.node')) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
return false;
|
|
@@ -88,15 +121,26 @@ export const name = 'tier1-binary-embed';
|
|
|
88
121
|
|
|
89
122
|
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
90
123
|
const pkgName = pkgJson?.name;
|
|
91
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
124
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
92
127
|
|
|
93
|
-
if (!allFiles || allFiles.length === 0)
|
|
128
|
+
if (!allFiles || allFiles.length === 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
94
131
|
|
|
95
|
-
if (
|
|
96
|
-
pkgName
|
|
97
|
-
pkgName === '
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
if (
|
|
133
|
+
pkgName &&
|
|
134
|
+
(pkgName === 'electron' ||
|
|
135
|
+
pkgName === 'puppeteer' ||
|
|
136
|
+
pkgName === 'sharp' ||
|
|
137
|
+
pkgName === 'esbuild' ||
|
|
138
|
+
pkgName === 'node-gyp' ||
|
|
139
|
+
pkgName === 'node-pre-gyp' ||
|
|
140
|
+
pkgName === '@mapbox/node-pre-gyp')
|
|
141
|
+
) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
100
144
|
|
|
101
145
|
const binaries = [];
|
|
102
146
|
|
|
@@ -128,11 +172,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
128
172
|
}
|
|
129
173
|
}
|
|
130
174
|
|
|
131
|
-
if (binaries.length === 0)
|
|
175
|
+
if (binaries.length === 0) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
132
178
|
|
|
133
179
|
const crossPlatformSet = detectCrossPlatformSets(binaries);
|
|
134
180
|
|
|
135
|
-
const jsCode = (jsFiles || []).map(f => f.content || '').join('\n');
|
|
181
|
+
const jsCode = (jsFiles || []).map((f) => f.content || '').join('\n');
|
|
136
182
|
const invoked = CHILD_PROC_RE.test(jsCode) || FS_CHMOD_RE.test(jsCode);
|
|
137
183
|
|
|
138
184
|
const invokedFiles = [];
|
|
@@ -154,7 +200,11 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
154
200
|
let subtype;
|
|
155
201
|
|
|
156
202
|
// Cross-platform platform set boost
|
|
157
|
-
const isCrossPlatform =
|
|
203
|
+
const isCrossPlatform =
|
|
204
|
+
crossPlatformSet &&
|
|
205
|
+
crossPlatformSet.files.some(
|
|
206
|
+
(f) => f === bin.file || f.includes(bin.file) || bin.file.includes(f.replace(/\.exe$/, ''))
|
|
207
|
+
);
|
|
158
208
|
|
|
159
209
|
if (bin.magic === 'elf_embedded') {
|
|
160
210
|
baseScore = 95;
|
|
@@ -175,26 +225,44 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
175
225
|
|
|
176
226
|
let score = baseScore;
|
|
177
227
|
|
|
178
|
-
if (isCrossPlatform)
|
|
228
|
+
if (isCrossPlatform) {
|
|
229
|
+
score += 25;
|
|
230
|
+
}
|
|
179
231
|
|
|
180
|
-
if (bin.inBinDir)
|
|
232
|
+
if (bin.inBinDir) {
|
|
233
|
+
score += 15;
|
|
234
|
+
}
|
|
181
235
|
|
|
182
|
-
if (!bin.declared)
|
|
236
|
+
if (!bin.declared) {
|
|
237
|
+
score += 50;
|
|
238
|
+
}
|
|
183
239
|
|
|
184
|
-
if (invoked && invokedFiles.length > 0)
|
|
240
|
+
if (invoked && invokedFiles.length > 0) {
|
|
241
|
+
score += 25;
|
|
242
|
+
}
|
|
185
243
|
|
|
186
244
|
const confidenceScore = Math.max(50, Math.min(100, score));
|
|
187
245
|
|
|
188
246
|
function severityLabel(sc) {
|
|
189
|
-
if (sc >= 90)
|
|
190
|
-
|
|
247
|
+
if (sc >= 90) {
|
|
248
|
+
return 'critical';
|
|
249
|
+
}
|
|
250
|
+
if (sc >= 70) {
|
|
251
|
+
return 'high';
|
|
252
|
+
}
|
|
191
253
|
return 'medium';
|
|
192
254
|
}
|
|
193
255
|
|
|
194
256
|
function confidenceLabel(sc) {
|
|
195
|
-
if (sc >= 95)
|
|
196
|
-
|
|
197
|
-
|
|
257
|
+
if (sc >= 95) {
|
|
258
|
+
return 'CRITICAL';
|
|
259
|
+
}
|
|
260
|
+
if (sc >= 80) {
|
|
261
|
+
return 'HIGH';
|
|
262
|
+
}
|
|
263
|
+
if (sc >= 60) {
|
|
264
|
+
return 'MEDIUM';
|
|
265
|
+
}
|
|
198
266
|
return 'LOW';
|
|
199
267
|
}
|
|
200
268
|
|
|
@@ -204,7 +272,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
204
272
|
`declared: ${bin.declared}`,
|
|
205
273
|
];
|
|
206
274
|
if (isCrossPlatform) {
|
|
207
|
-
evidence.push(
|
|
275
|
+
evidence.push(
|
|
276
|
+
`cross-platform binary set: ${crossPlatformSet.count} variants of "${crossPlatformSet.base}"`
|
|
277
|
+
);
|
|
208
278
|
evidence.push(`platform_files: ${crossPlatformSet.files.join(', ')}`);
|
|
209
279
|
}
|
|
210
280
|
|
|
@@ -213,9 +283,7 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
213
283
|
evidence.push(`invoked_file: ${invokedFiles[0]}`);
|
|
214
284
|
}
|
|
215
285
|
|
|
216
|
-
const locations = [
|
|
217
|
-
{ file: bin.file, size: bin.size },
|
|
218
|
-
];
|
|
286
|
+
const locations = [{ file: bin.file, size: bin.size }];
|
|
219
287
|
|
|
220
288
|
if (invokedFiles.length > 0) {
|
|
221
289
|
locations.push({ file: invokedFiles[0], line: 0 });
|
|
@@ -4,41 +4,48 @@ const GCP_PATTERNS = [
|
|
|
4
4
|
'metadata.google.internal/computeMetadata',
|
|
5
5
|
];
|
|
6
6
|
|
|
7
|
-
const AZURE_PATTERNS = [
|
|
8
|
-
'169.254.169.254/metadata/instance',
|
|
9
|
-
'169.254.169.254/metadata/identity',
|
|
10
|
-
];
|
|
7
|
+
const AZURE_PATTERNS = ['169.254.169.254/metadata/instance', '169.254.169.254/metadata/identity'];
|
|
11
8
|
|
|
12
9
|
const AZURE_IP = '169.254.169.254';
|
|
13
10
|
const METADATA_HEADER_RE = /Metadata\s*:\s*true/i;
|
|
14
11
|
|
|
15
12
|
function severityLabel(score) {
|
|
16
|
-
if (score >= 80)
|
|
13
|
+
if (score >= 80) {
|
|
14
|
+
return 'high';
|
|
15
|
+
}
|
|
17
16
|
return 'medium';
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
function confidenceLabel(score) {
|
|
21
|
-
if (score >= 80)
|
|
22
|
-
|
|
20
|
+
if (score >= 80) {
|
|
21
|
+
return 'HIGH';
|
|
22
|
+
}
|
|
23
|
+
if (score >= 60) {
|
|
24
|
+
return 'MEDIUM';
|
|
25
|
+
}
|
|
23
26
|
return 'LOW';
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
function hasGcpPattern(text) {
|
|
27
|
-
return GCP_PATTERNS.some(p => text.includes(p));
|
|
30
|
+
return GCP_PATTERNS.some((p) => text.includes(p));
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function hasAzurePath(text) {
|
|
31
|
-
return AZURE_PATTERNS.some(p => text.includes(p));
|
|
34
|
+
return AZURE_PATTERNS.some((p) => text.includes(p));
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function hasAzureHeaderPattern(text) {
|
|
35
38
|
const lines = text.split('\n');
|
|
36
39
|
for (let i = 0; i < lines.length; i++) {
|
|
37
|
-
if (!lines[i].includes(AZURE_IP))
|
|
40
|
+
if (!lines[i].includes(AZURE_IP)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
38
43
|
const start = Math.max(0, i - 5);
|
|
39
44
|
const end = Math.min(lines.length, i + 6);
|
|
40
45
|
for (let j = start; j < end; j++) {
|
|
41
|
-
if (METADATA_HEADER_RE.test(lines[j]))
|
|
46
|
+
if (METADATA_HEADER_RE.test(lines[j])) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
return false;
|
|
@@ -72,20 +79,30 @@ function collectTexts(pkgJson, jsFiles) {
|
|
|
72
79
|
|
|
73
80
|
export const name = 'tier1-cloud-imds';
|
|
74
81
|
|
|
75
|
-
export async function scan(pkgJson, jsFiles,
|
|
82
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
76
83
|
const texts = collectTexts(pkgJson, jsFiles);
|
|
77
|
-
if (texts.length === 0)
|
|
84
|
+
if (texts.length === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
78
87
|
|
|
79
88
|
let hasGcp = false;
|
|
80
89
|
let hasAzure = false;
|
|
81
90
|
|
|
82
91
|
for (const text of texts) {
|
|
83
|
-
if (!hasGcp && hasGcpPattern(text))
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
if (!hasGcp && hasGcpPattern(text)) {
|
|
93
|
+
hasGcp = true;
|
|
94
|
+
}
|
|
95
|
+
if (!hasAzure && hasAzurePattern(text)) {
|
|
96
|
+
hasAzure = true;
|
|
97
|
+
}
|
|
98
|
+
if (hasGcp && hasAzure) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
if (!hasGcp && !hasAzure)
|
|
103
|
+
if (!hasGcp && !hasAzure) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
89
106
|
|
|
90
107
|
let confidenceScore;
|
|
91
108
|
let subtype;
|
|
@@ -101,24 +118,27 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
101
118
|
subtype = 'azure_imds';
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
return [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
detector: 'tier1-cloud-imds',
|
|
124
|
+
id: 'TIER1-CLOUD-IMDS',
|
|
125
|
+
severity: severityLabel(confidenceScore),
|
|
126
|
+
confidence: confidenceLabel(confidenceScore),
|
|
127
|
+
confidenceScore,
|
|
128
|
+
subtype,
|
|
129
|
+
message:
|
|
130
|
+
hasGcp && hasAzure
|
|
131
|
+
? `Package references both GCP metadata and Azure IMDS endpoints — cloud credential harvesting`
|
|
132
|
+
: hasGcp
|
|
133
|
+
? `Package references GCP metadata server endpoint — cloud credential harvesting`
|
|
134
|
+
: `Package references Azure IMDS endpoint — cloud credential harvesting`,
|
|
135
|
+
evidence: [
|
|
136
|
+
...(hasGcp ? ['gcp: metadata.google.internal / computeMetadata/v1 pattern detected'] : []),
|
|
137
|
+
...(hasAzure ? ['azure: 169.254.169.254/metadata pattern detected'] : []),
|
|
138
|
+
],
|
|
139
|
+
crossFiles: [],
|
|
140
|
+
locations: [{ file: '', line: 0 }],
|
|
141
|
+
reference: 'Miasma Cloud IMDS',
|
|
142
|
+
},
|
|
143
|
+
];
|
|
124
144
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const THRESHOLDS = {
|
|
4
|
+
flag_threshold: 70,
|
|
5
|
+
warn_threshold: 50,
|
|
6
|
+
known_c2_endpoints: [
|
|
7
|
+
'filev2.getsession.org',
|
|
8
|
+
'api.signal.org',
|
|
9
|
+
'*.briarproject.org',
|
|
10
|
+
'api.ricochet.im',
|
|
11
|
+
],
|
|
12
|
+
onion_pattern_weight: 30,
|
|
13
|
+
encoded_url_weight: 35,
|
|
14
|
+
env_var_c2_weight: 40,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const KNOWN_C2_RE =
|
|
18
|
+
/(?:filev2\.getsession\.org|api\.signal\.org|(?:[\w-]+\.)?briarproject\.org|api\.ricochet\.im|signal-cli|signal-desktop|tor\s*(?:proxy|socks|connect|bridge))/gi;
|
|
19
|
+
const ONION_RE = /(?:[a-z2-7]{16,56}\.onion|\.onion|tor\s*(?:proxy|socks|connect|bridge))/gi;
|
|
20
|
+
const ENCODED_URL_RE =
|
|
21
|
+
/(?:atob|Buffer\.from|decodeURIComponent)\s*\((?:['"`][A-Za-z0-9+/=]{20,}['"`]|['"`](?:[0-9a-fA-F]{2,})['"`])/gi;
|
|
22
|
+
const HEX_DOMAIN_RE = /(?:0x[0-9a-fA-F]{2,}){4,}/g;
|
|
23
|
+
const SESSION_RE = /session|oxen|filev2|getsession/i;
|
|
24
|
+
const SIGNAL_RE = /signal|signal-cli|signal-desktop/i;
|
|
25
|
+
const BRIAR_RE = /briar|briarproject/i;
|
|
26
|
+
|
|
27
|
+
function detectC2InContent(content) {
|
|
28
|
+
const findings = [];
|
|
29
|
+
|
|
30
|
+
let match;
|
|
31
|
+
KNOWN_C2_RE.lastIndex = 0;
|
|
32
|
+
while ((match = KNOWN_C2_RE.exec(content)) !== null) {
|
|
33
|
+
findings.push({
|
|
34
|
+
type: 'known_endpoint',
|
|
35
|
+
endpoint: match[0],
|
|
36
|
+
weight: 80,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ONION_RE.lastIndex = 0;
|
|
41
|
+
while ((match = ONION_RE.exec(content)) !== null) {
|
|
42
|
+
findings.push({
|
|
43
|
+
type: 'onion_service',
|
|
44
|
+
pattern: match[0],
|
|
45
|
+
weight: THRESHOLDS.onion_pattern_weight,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ENCODED_URL_RE.lastIndex = 0;
|
|
50
|
+
while ((match = ENCODED_URL_RE.exec(content)) !== null) {
|
|
51
|
+
findings.push({
|
|
52
|
+
type: 'encoded_url',
|
|
53
|
+
snippet: match[0].substring(0, 60),
|
|
54
|
+
weight: THRESHOLDS.encoded_url_weight,
|
|
55
|
+
encoding: match[0].includes('atob')
|
|
56
|
+
? 'base64'
|
|
57
|
+
: match[0].includes('Buffer.from')
|
|
58
|
+
? 'hex_or_other'
|
|
59
|
+
: 'other',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
HEX_DOMAIN_RE.lastIndex = 0;
|
|
64
|
+
while ((match = HEX_DOMAIN_RE.exec(content)) !== null) {
|
|
65
|
+
findings.push({
|
|
66
|
+
type: 'hex_encoded_domain',
|
|
67
|
+
snippet: match[0].substring(0, 40),
|
|
68
|
+
weight: THRESHOLDS.encoded_url_weight,
|
|
69
|
+
encoding: 'hex',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function computeConfidence(c2Findings, hasSession, hasSignal, hasBriar) {
|
|
77
|
+
let base = 0;
|
|
78
|
+
|
|
79
|
+
const totalWeight = c2Findings.reduce((s, f) => s + f.weight, 0);
|
|
80
|
+
base += Math.min(totalWeight, 80);
|
|
81
|
+
|
|
82
|
+
if (c2Findings.length === 0) {
|
|
83
|
+
base = 20;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (hasSession) base += 20;
|
|
87
|
+
if (hasSignal) base += 20;
|
|
88
|
+
if (hasBriar) base += 15;
|
|
89
|
+
|
|
90
|
+
if (c2Findings.length > 0) {
|
|
91
|
+
base += 20;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (c2Findings.length > 1) {
|
|
95
|
+
base += Math.min(c2Findings.length * 5, 15);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Math.min(100, Math.max(0, base));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function severityLabel(score) {
|
|
102
|
+
if (score >= 80) return 'critical';
|
|
103
|
+
if (score >= 60) return 'high';
|
|
104
|
+
return 'medium';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function confidenceLabel(score) {
|
|
108
|
+
if (score >= 80) return 'CRITICAL';
|
|
109
|
+
if (score >= 60) return 'HIGH';
|
|
110
|
+
return 'MEDIUM';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const name = 'tier1-encrypted-c2';
|
|
114
|
+
|
|
115
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
116
|
+
const pkgName = pkgJson?.name;
|
|
117
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
118
|
+
|
|
119
|
+
const allContents = [];
|
|
120
|
+
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
121
|
+
for (const val of Object.values(pkgJson.scripts)) {
|
|
122
|
+
if (typeof val === 'string') {
|
|
123
|
+
allContents.push({ source: 'package.json scripts', content: val });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const files = jsFiles || [];
|
|
129
|
+
for (const f of files) {
|
|
130
|
+
if (f?.content) {
|
|
131
|
+
allContents.push({ source: f.path || f.name || 'unknown', content: f.content });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (allContents.length === 0) return [];
|
|
136
|
+
|
|
137
|
+
const hasSession = SESSION_RE.test(JSON.stringify(allContents.map((c) => c.content)));
|
|
138
|
+
const hasSignal = SIGNAL_RE.test(JSON.stringify(allContents.map((c) => c.content)));
|
|
139
|
+
const hasBriar = BRIAR_RE.test(JSON.stringify(allContents.map((c) => c.content)));
|
|
140
|
+
|
|
141
|
+
const allFindings = [];
|
|
142
|
+
for (const { source, content } of allContents) {
|
|
143
|
+
const c2Findings = detectC2InContent(content);
|
|
144
|
+
if (c2Findings.length > 0) {
|
|
145
|
+
allFindings.push({ source, c2Findings });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const flatC2 = allFindings.flatMap((f) => f.c2Findings);
|
|
150
|
+
const totalSignals =
|
|
151
|
+
flatC2.length + (hasSession ? 1 : 0) + (hasSignal ? 1 : 0) + (hasBriar ? 1 : 0);
|
|
152
|
+
if (totalSignals === 0) return [];
|
|
153
|
+
|
|
154
|
+
const confidenceScore = computeConfidence(flatC2, hasSession, hasSignal, hasBriar);
|
|
155
|
+
if (confidenceScore < THRESHOLDS.warn_threshold) return [];
|
|
156
|
+
|
|
157
|
+
const endpointTypes = [...new Set(flatC2.map((f) => f.type))];
|
|
158
|
+
const primaryType = endpointTypes.includes('known_endpoint')
|
|
159
|
+
? 'known_endpoint'
|
|
160
|
+
: endpointTypes.includes('onion_service')
|
|
161
|
+
? 'onion_service'
|
|
162
|
+
: endpointTypes.includes('encoded_url')
|
|
163
|
+
? 'encoded_url'
|
|
164
|
+
: endpointTypes.includes('hex_encoded_domain')
|
|
165
|
+
? 'hex_encoded_domain'
|
|
166
|
+
: 'protocol_signal';
|
|
167
|
+
|
|
168
|
+
const evidence = flatC2.map((f) => {
|
|
169
|
+
if (f.type === 'known_endpoint') return `c2_endpoint: ${f.endpoint}`;
|
|
170
|
+
if (f.type === 'onion_service') return `onion_pattern: ${f.pattern}`;
|
|
171
|
+
return `encoded: ${f.snippet}`;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (hasSession) evidence.push('protocol: Session/Oxen messenger');
|
|
175
|
+
if (hasSignal) evidence.push('protocol: Signal messenger');
|
|
176
|
+
if (hasBriar) evidence.push('protocol: Briar project');
|
|
177
|
+
|
|
178
|
+
const locations =
|
|
179
|
+
allFindings.length > 0
|
|
180
|
+
? allFindings.map((f) => ({ file: f.source, line: 1, column: 1 })).slice(0, 5)
|
|
181
|
+
: [{ file: 'package.json', line: 1, column: 1 }];
|
|
182
|
+
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
detector: 'tier1-encrypted-c2',
|
|
186
|
+
id: 'TIER1-ENCRYPTED-C2',
|
|
187
|
+
severity: severityLabel(confidenceScore),
|
|
188
|
+
confidence: confidenceLabel(confidenceScore),
|
|
189
|
+
confidenceScore,
|
|
190
|
+
subtype: primaryType,
|
|
191
|
+
message: `${flatC2.length > 0 ? flatC2.length + ' encrypted C2 signal(s)' : 'Encrypted C2 protocol(s) detected'}${hasSession || hasSignal || hasBriar ? ' (' + [hasSession ? 'Session' : '', hasSignal ? 'Signal' : '', hasBriar ? 'Briar' : ''].filter(Boolean).join(', ') + ')' : ''}`,
|
|
192
|
+
evidence: evidence.slice(0, 8),
|
|
193
|
+
locations,
|
|
194
|
+
crossFiles: [],
|
|
195
|
+
reference: 'D11: TanStack Mini Shai-Hulud encrypted C2',
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}
|