@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
|
@@ -12,25 +12,29 @@ const NPMJS_DOMAIN_RE = /npmjs\.(?:com|org)/i;
|
|
|
12
12
|
const AWS_KEY_RE = /AKIA[0-9A-Z]{16}/g;
|
|
13
13
|
const NPM_TOKEN_RE = /npm_[a-zA-Z0-9]{36}/g;
|
|
14
14
|
const GH_TOKEN_RE = /ghp_[a-zA-Z0-9]{30,40}/g;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const _GH_OLD_TOKEN_RE = /gho_[a-zA-Z0-9]{36}/g;
|
|
16
|
+
const _GITLAB_TOKEN_RE = /glpat-[a-zA-Z0-9_-]{20,}/g;
|
|
17
17
|
|
|
18
18
|
const ENV_DUMP_RE = /process\.env\.(?:AWS_[A-Z_]+|NPM_TOKEN|NPM_AUTH_TOKEN|GIT_TOKEN|SSH_KEY)/g;
|
|
19
19
|
|
|
20
20
|
const EVAL_RE = /\beval\s*\(/g;
|
|
21
21
|
const FUNCTION_CTOR_RE = /\bFunction\s*\(/g;
|
|
22
|
-
const
|
|
22
|
+
const _B64_STRING_RE = /['"`]([A-Za-z0-9+/]{40,}={0,2})['"`]/g;
|
|
23
23
|
|
|
24
24
|
// Named malware signatures — zero-FP string literals for confirmed campaigns
|
|
25
25
|
const NAMED_SIGNATURES = [
|
|
26
|
-
'Miasma: The Spreading Blight',
|
|
26
|
+
'Miasma: The Spreading Blight', // Miasma campaign, June 2026, @redhat-cloud-services compromise
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
function shannonEntropy(s) {
|
|
30
30
|
const len = s.length;
|
|
31
|
-
if (len === 0)
|
|
31
|
+
if (len === 0) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
32
34
|
const freq = {};
|
|
33
|
-
for (const ch of s)
|
|
35
|
+
for (const ch of s) {
|
|
36
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
37
|
+
}
|
|
34
38
|
let entropy = 0;
|
|
35
39
|
for (const count of Object.values(freq)) {
|
|
36
40
|
const p = count / len;
|
|
@@ -43,7 +47,9 @@ function isMinified(content) {
|
|
|
43
47
|
const identifiers = content.match(/\b[a-zA-Z_$][\w$]*\b/g);
|
|
44
48
|
if (identifiers && identifiers.length > 0) {
|
|
45
49
|
const avgLen = identifiers.reduce((s, id) => s + id.length, 0) / identifiers.length;
|
|
46
|
-
if (avgLen < 3)
|
|
50
|
+
if (avgLen < 3) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
return shannonEntropy(content) > 5.5;
|
|
49
55
|
}
|
|
@@ -94,9 +100,12 @@ function patternMatcher(f, content) {
|
|
|
94
100
|
isObfuscated: false,
|
|
95
101
|
};
|
|
96
102
|
|
|
97
|
-
if (!content)
|
|
103
|
+
if (!content) {
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
98
106
|
|
|
99
|
-
result.isObfuscated =
|
|
107
|
+
result.isObfuscated =
|
|
108
|
+
isMinified(content) || EVAL_RE.test(content) || FUNCTION_CTOR_RE.test(content);
|
|
100
109
|
|
|
101
110
|
FS_READ_RE.lastIndex = 0;
|
|
102
111
|
HTTP_FETCH_RE.lastIndex = 0;
|
|
@@ -109,13 +118,17 @@ function patternMatcher(f, content) {
|
|
|
109
118
|
const hasCurlWget = CURL_WGET_RE.test(content);
|
|
110
119
|
|
|
111
120
|
const domains = extractDomains(content);
|
|
112
|
-
const externalDomains = domains.filter(d => !NPMJS_DOMAIN_RE.test(d));
|
|
113
|
-
const gitHubDomains = domains.filter(d => GITHUB_DOMAIN_RE.test(d) && !NPMJS_DOMAIN_RE.test(d));
|
|
121
|
+
const externalDomains = domains.filter((d) => !NPMJS_DOMAIN_RE.test(d));
|
|
122
|
+
const gitHubDomains = domains.filter((d) => GITHUB_DOMAIN_RE.test(d) && !NPMJS_DOMAIN_RE.test(d));
|
|
114
123
|
|
|
115
124
|
if (hasFsRead && hasHttpFetch) {
|
|
116
|
-
const isGithubOnly =
|
|
125
|
+
const isGithubOnly =
|
|
126
|
+
gitHubDomains.length > 0 && externalDomains.length === gitHubDomains.length;
|
|
117
127
|
result.hasPattern = true;
|
|
118
|
-
result.patterns.push({
|
|
128
|
+
result.patterns.push({
|
|
129
|
+
subtype: isGithubOnly ? 'nw_exfil_to_github' : 'fs_exfil',
|
|
130
|
+
baseScore: 80,
|
|
131
|
+
});
|
|
119
132
|
result.domainsFound.push(...domains);
|
|
120
133
|
FS_READ_RE.lastIndex = 0;
|
|
121
134
|
const fsMatch = FS_READ_RE.exec(content);
|
|
@@ -123,15 +136,21 @@ function patternMatcher(f, content) {
|
|
|
123
136
|
const lc = getLineColumn(content, fsMatch.index);
|
|
124
137
|
result.locations.push({ file, line: lc.line, column: lc.column });
|
|
125
138
|
}
|
|
126
|
-
result.evidence.push(
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
result.evidence.push(
|
|
140
|
+
isGithubOnly
|
|
141
|
+
? 'pattern: fs.readFile + network to GitHub'
|
|
142
|
+
: 'pattern: fs.readFile + external fetch'
|
|
143
|
+
);
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
if (hasFsRead && (hasChildProc || hasCurlWget)) {
|
|
132
|
-
const isGithubOnly =
|
|
147
|
+
const isGithubOnly =
|
|
148
|
+
gitHubDomains.length > 0 && externalDomains.length === gitHubDomains.length;
|
|
133
149
|
result.hasPattern = true;
|
|
134
|
-
result.patterns.push({
|
|
150
|
+
result.patterns.push({
|
|
151
|
+
subtype: isGithubOnly ? 'nw_exfil_to_github' : 'fs_exfil',
|
|
152
|
+
baseScore: 80,
|
|
153
|
+
});
|
|
135
154
|
result.domainsFound.push(...domains);
|
|
136
155
|
FS_READ_RE.lastIndex = 0;
|
|
137
156
|
const fsMatch = FS_READ_RE.exec(content);
|
|
@@ -139,9 +158,11 @@ function patternMatcher(f, content) {
|
|
|
139
158
|
const lc = getLineColumn(content, fsMatch.index);
|
|
140
159
|
result.locations.push({ file, line: lc.line, column: lc.column });
|
|
141
160
|
}
|
|
142
|
-
result.evidence.push(
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
result.evidence.push(
|
|
162
|
+
isGithubOnly
|
|
163
|
+
? 'pattern: fs.readFile + child_process to GitHub'
|
|
164
|
+
: 'pattern: fs.readFile + child_process network'
|
|
165
|
+
);
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
const creds = extractCredentials(content);
|
|
@@ -152,7 +173,7 @@ function patternMatcher(f, content) {
|
|
|
152
173
|
result.patterns.push({ subtype: primaryType, baseScore: 85 });
|
|
153
174
|
const lc = getLineColumn(content, creds[0].index);
|
|
154
175
|
result.locations.push({ file, line: lc.line, column: lc.column });
|
|
155
|
-
const typeNames = [...new Set(creds.map(c => c.type))];
|
|
176
|
+
const typeNames = [...new Set(creds.map((c) => c.type))];
|
|
156
177
|
result.evidence.push(`hardcoded_credentials: ${creds.length} (${typeNames.join(', ')})`);
|
|
157
178
|
}
|
|
158
179
|
|
|
@@ -171,9 +192,11 @@ function patternMatcher(f, content) {
|
|
|
171
192
|
|
|
172
193
|
export const name = 'tier1-infostealer';
|
|
173
194
|
|
|
174
|
-
export async function scan(pkgJson, jsFiles,
|
|
195
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
175
196
|
const pkgName = pkgJson?.name;
|
|
176
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
197
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
177
200
|
|
|
178
201
|
const files = jsFiles || [];
|
|
179
202
|
|
|
@@ -181,39 +204,49 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
181
204
|
const sigTexts = [];
|
|
182
205
|
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
183
206
|
for (const value of Object.values(pkgJson.scripts)) {
|
|
184
|
-
if (typeof value === 'string')
|
|
207
|
+
if (typeof value === 'string') {
|
|
208
|
+
sigTexts.push(value);
|
|
209
|
+
}
|
|
185
210
|
}
|
|
186
211
|
}
|
|
187
212
|
for (const f of files) {
|
|
188
|
-
if (f?.content)
|
|
213
|
+
if (f?.content) {
|
|
214
|
+
sigTexts.push(f.content);
|
|
215
|
+
}
|
|
189
216
|
}
|
|
190
217
|
for (const sig of NAMED_SIGNATURES) {
|
|
191
218
|
for (const text of sigTexts) {
|
|
192
219
|
if (text.includes(sig)) {
|
|
193
|
-
return [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
detector: 'tier1-infostealer',
|
|
223
|
+
id: 'TIER1-INFOSTEALER',
|
|
224
|
+
severity: 'critical',
|
|
225
|
+
confidence: 'CRITICAL',
|
|
226
|
+
confidenceScore: 98,
|
|
227
|
+
subtype: 'named_signature_miasma',
|
|
228
|
+
message: `Named malware signature detected: "${sig}"`,
|
|
229
|
+
evidence: [sig],
|
|
230
|
+
locations: [{ file: '', line: 0 }],
|
|
231
|
+
crossFiles: [],
|
|
232
|
+
reference: 'Campaign 2 & 3',
|
|
233
|
+
},
|
|
234
|
+
];
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
237
|
}
|
|
209
238
|
|
|
210
|
-
if (files.length === 0)
|
|
239
|
+
if (files.length === 0) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
211
242
|
|
|
212
243
|
let parseFailCount = 0;
|
|
213
244
|
|
|
214
245
|
for (const f of files) {
|
|
215
246
|
const content = f.content || '';
|
|
216
|
-
if (!content)
|
|
247
|
+
if (!content) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
217
250
|
try {
|
|
218
251
|
acorn.parse(content, { ecmaVersion: 'latest' });
|
|
219
252
|
} catch {
|
|
@@ -221,12 +254,16 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
221
254
|
}
|
|
222
255
|
}
|
|
223
256
|
|
|
224
|
-
if (files.length >= 20 && parseFailCount / files.length >= 0.1)
|
|
257
|
+
if (files.length >= 20 && parseFailCount / files.length >= 0.1) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
225
260
|
|
|
226
|
-
const perFile = files.map(f => patternMatcher(f, f.content || ''));
|
|
227
|
-
const filesWithPatterns = perFile.filter(p => p.hasPattern);
|
|
261
|
+
const perFile = files.map((f) => patternMatcher(f, f.content || ''));
|
|
262
|
+
const filesWithPatterns = perFile.filter((p) => p.hasPattern);
|
|
228
263
|
|
|
229
|
-
if (filesWithPatterns.length === 0)
|
|
264
|
+
if (filesWithPatterns.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
230
267
|
|
|
231
268
|
let highestBase = 0;
|
|
232
269
|
let mainSubtype = '';
|
|
@@ -234,13 +271,17 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
234
271
|
const allEvidence = [];
|
|
235
272
|
const allLocations = [];
|
|
236
273
|
const involvedFiles = [];
|
|
237
|
-
const
|
|
274
|
+
const _hasCreds = false;
|
|
238
275
|
|
|
239
276
|
for (const f of filesWithPatterns) {
|
|
240
|
-
if (!involvedFiles.includes(f.file))
|
|
277
|
+
if (!involvedFiles.includes(f.file)) {
|
|
278
|
+
involvedFiles.push(f.file);
|
|
279
|
+
}
|
|
241
280
|
allLocations.push(...f.locations);
|
|
242
281
|
allEvidence.push(...f.evidence);
|
|
243
|
-
if (f.isObfuscated)
|
|
282
|
+
if (f.isObfuscated) {
|
|
283
|
+
isObfuscated = true;
|
|
284
|
+
}
|
|
244
285
|
for (const p of f.patterns) {
|
|
245
286
|
if (p.baseScore > highestBase) {
|
|
246
287
|
highestBase = p.baseScore;
|
|
@@ -251,12 +292,16 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
251
292
|
|
|
252
293
|
let baseScore = highestBase;
|
|
253
294
|
|
|
254
|
-
const anyCredPattern = filesWithPatterns.some(f =>
|
|
295
|
+
const anyCredPattern = filesWithPatterns.some((f) =>
|
|
296
|
+
f.patterns.some((p) => p.subtype.startsWith('cred_'))
|
|
297
|
+
);
|
|
255
298
|
if (anyCredPattern) {
|
|
256
299
|
baseScore = Math.min(100, Math.round(baseScore * 2.5));
|
|
257
300
|
}
|
|
258
301
|
|
|
259
|
-
if (isObfuscated)
|
|
302
|
+
if (isObfuscated) {
|
|
303
|
+
baseScore += 15;
|
|
304
|
+
}
|
|
260
305
|
|
|
261
306
|
if (involvedFiles.length > 1) {
|
|
262
307
|
baseScore = Math.min(100, Math.round(baseScore * 1.3));
|
|
@@ -265,8 +310,12 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
265
310
|
const confidenceScore = Math.max(50, Math.min(100, baseScore));
|
|
266
311
|
|
|
267
312
|
function confidenceLabel(score) {
|
|
268
|
-
if (score >= 95)
|
|
269
|
-
|
|
313
|
+
if (score >= 95) {
|
|
314
|
+
return 'CRITICAL';
|
|
315
|
+
}
|
|
316
|
+
if (score >= 80) {
|
|
317
|
+
return 'HIGH';
|
|
318
|
+
}
|
|
270
319
|
return 'MEDIUM';
|
|
271
320
|
}
|
|
272
321
|
|
|
@@ -276,14 +325,16 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
276
325
|
const locationMap = new Map();
|
|
277
326
|
for (const loc of allLocations) {
|
|
278
327
|
const key = `${loc.file}:${loc.line}:${loc.column}`;
|
|
279
|
-
if (!locationMap.has(key))
|
|
328
|
+
if (!locationMap.has(key)) {
|
|
329
|
+
locationMap.set(key, loc);
|
|
330
|
+
}
|
|
280
331
|
}
|
|
281
332
|
|
|
282
333
|
const isCritical = anyCredPattern;
|
|
283
334
|
const severity = isCritical ? 'critical' : confidenceScore >= 80 ? 'high' : 'medium';
|
|
284
335
|
|
|
285
|
-
const
|
|
286
|
-
.flatMap(f => f.domainsFound)
|
|
336
|
+
const _domainSummary = filesWithPatterns
|
|
337
|
+
.flatMap((f) => f.domainsFound)
|
|
287
338
|
.filter(Boolean)
|
|
288
339
|
.slice(0, 3);
|
|
289
340
|
|
|
@@ -300,17 +351,19 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
300
351
|
message = 'Filesystem exfiltration to external domain detected';
|
|
301
352
|
}
|
|
302
353
|
|
|
303
|
-
return [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
detector: 'tier1-infostealer',
|
|
357
|
+
id: 'TIER1-INFOSTEALER',
|
|
358
|
+
severity,
|
|
359
|
+
confidence: confidenceLabel(confidenceScore),
|
|
360
|
+
confidenceScore,
|
|
361
|
+
subtype: mainSubtype || 'fs_exfil',
|
|
362
|
+
message,
|
|
363
|
+
evidence,
|
|
364
|
+
locations: [...locationMap.values()],
|
|
365
|
+
crossFiles: [...new Set(involvedFiles)],
|
|
366
|
+
reference: 'Campaign 2 & 3',
|
|
367
|
+
},
|
|
368
|
+
];
|
|
316
369
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
2
|
|
|
3
|
-
const HOOK_NAMES = [
|
|
3
|
+
const HOOK_NAMES = [
|
|
4
|
+
'postinstall',
|
|
5
|
+
'preinstall',
|
|
6
|
+
'install',
|
|
7
|
+
'prepare',
|
|
8
|
+
'preuninstall',
|
|
9
|
+
'postuninstall',
|
|
10
|
+
];
|
|
4
11
|
|
|
5
12
|
const CURL_WGET_RE = /\b(?:curl|wget|powershell|bash|sh)\b/i;
|
|
6
13
|
const CHILD_PROC_RE = /\b(?:exec|execSync|spawn|spawnSync|fork)\s*\(/g;
|
|
@@ -17,9 +24,13 @@ const REQUIRE_RE = /\brequire\s*\(/g;
|
|
|
17
24
|
|
|
18
25
|
function shannonEntropy(s) {
|
|
19
26
|
const len = s.length;
|
|
20
|
-
if (len === 0)
|
|
27
|
+
if (len === 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
21
30
|
const freq = {};
|
|
22
|
-
for (const ch of s)
|
|
31
|
+
for (const ch of s) {
|
|
32
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
33
|
+
}
|
|
23
34
|
let entropy = 0;
|
|
24
35
|
for (const count of Object.values(freq)) {
|
|
25
36
|
const p = count / len;
|
|
@@ -29,20 +40,32 @@ function shannonEntropy(s) {
|
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
function isObfuscated(content) {
|
|
32
|
-
if (!content)
|
|
43
|
+
if (!content) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
33
46
|
const noWhitespace = !/\s/.test(content.trim());
|
|
34
47
|
const identifiers = content.match(/\b[a-zA-Z_$][\w$]*\b/g);
|
|
35
48
|
let avgIdLen = 0;
|
|
36
49
|
if (identifiers && identifiers.length > 0) {
|
|
37
50
|
avgIdLen = identifiers.reduce((s, id) => s + id.length, 0) / identifiers.length;
|
|
38
51
|
}
|
|
39
|
-
if (noWhitespace && identifiers && identifiers.length > 0 && avgIdLen < 3)
|
|
40
|
-
|
|
52
|
+
if (noWhitespace && identifiers && identifiers.length > 0 && avgIdLen < 3) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (noWhitespace && /^[a-zA-Z_$][\w$]*\([^)]*\)$/.test(content.trim())) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
41
58
|
HEX_STRING_RE.lastIndex = 0;
|
|
42
|
-
if (HEX_STRING_RE.test(content))
|
|
59
|
+
if (HEX_STRING_RE.test(content)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
43
62
|
B64_RE.lastIndex = 0;
|
|
44
|
-
if (B64_RE.test(content))
|
|
45
|
-
|
|
63
|
+
if (B64_RE.test(content)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (shannonEntropy(content) > 5.5) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
46
69
|
return false;
|
|
47
70
|
}
|
|
48
71
|
|
|
@@ -58,9 +81,11 @@ function extractUrls(content) {
|
|
|
58
81
|
|
|
59
82
|
export const name = 'tier1-lifecycle-hook';
|
|
60
83
|
|
|
61
|
-
export async function scan(pkgJson,
|
|
84
|
+
export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
|
|
62
85
|
const pkgName = pkgJson?.name;
|
|
63
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
86
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
64
89
|
|
|
65
90
|
const scripts = pkgJson?.scripts || {};
|
|
66
91
|
const hooks = {};
|
|
@@ -71,18 +96,23 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
if (Object.keys(hooks).length === 0)
|
|
99
|
+
if (Object.keys(hooks).length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
75
102
|
|
|
76
103
|
const findings = [];
|
|
77
104
|
|
|
78
105
|
for (const [hookName, scriptContent] of Object.entries(hooks)) {
|
|
79
106
|
const content = typeof scriptContent === 'string' ? scriptContent : '';
|
|
80
|
-
if (!content)
|
|
107
|
+
if (!content) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
81
110
|
|
|
82
111
|
const truncated = content.length > 10240 ? content.slice(0, 10240) : content;
|
|
83
112
|
|
|
84
113
|
const obfuscated = isObfuscated(truncated);
|
|
85
|
-
const hasEval =
|
|
114
|
+
const hasEval =
|
|
115
|
+
EVAL_RE.test(truncated) || FUNCTION_CTOR_RE.test(truncated) || ZERO_EVAL_RE.test(truncated);
|
|
86
116
|
const hasNetwork = CURL_WGET_RE.test(truncated) || CHILD_PROC_RE.test(truncated);
|
|
87
117
|
const hasUrls = URL_RE.test(truncated) || IP_RE.test(truncated);
|
|
88
118
|
const urls = extractUrls(truncated);
|
|
@@ -111,7 +141,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
111
141
|
}
|
|
112
142
|
const domainInfo = hasInternal ? 'internal domain' : 'external URL';
|
|
113
143
|
evidence.push(`patterns: hardcoded ${domainInfo} in hook`);
|
|
114
|
-
if (urls.length > 0)
|
|
144
|
+
if (urls.length > 0) {
|
|
145
|
+
evidence.push(`target: ${urls[0]}`);
|
|
146
|
+
}
|
|
115
147
|
}
|
|
116
148
|
|
|
117
149
|
if (envExfil) {
|
|
@@ -143,13 +175,19 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
143
175
|
baseScore = Math.min(100, Math.round(baseScore * 2.5));
|
|
144
176
|
}
|
|
145
177
|
|
|
146
|
-
if (baseScore === 0)
|
|
178
|
+
if (baseScore === 0) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
147
181
|
|
|
148
182
|
const confidenceScore = Math.max(50, Math.min(100, baseScore));
|
|
149
183
|
|
|
150
184
|
function confidenceLabel(score) {
|
|
151
|
-
if (score >= 95)
|
|
152
|
-
|
|
185
|
+
if (score >= 95) {
|
|
186
|
+
return 'CRITICAL';
|
|
187
|
+
}
|
|
188
|
+
if (score >= 80) {
|
|
189
|
+
return 'HIGH';
|
|
190
|
+
}
|
|
153
191
|
return 'MEDIUM';
|
|
154
192
|
}
|
|
155
193
|
|
|
@@ -162,11 +200,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
162
200
|
subtype,
|
|
163
201
|
message: `Suspicious lifecycle hook "${hookName}"`,
|
|
164
202
|
evidence,
|
|
165
|
-
locations: [
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
203
|
+
locations: [
|
|
204
|
+
{
|
|
205
|
+
file: 'package.json',
|
|
206
|
+
field: `scripts.${hookName}`,
|
|
207
|
+
value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
170
210
|
crossFiles: [],
|
|
171
211
|
reference: 'Campaign 1',
|
|
172
212
|
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const THRESHOLDS = {
|
|
4
|
+
flag_threshold: 75,
|
|
5
|
+
warn_threshold: 60,
|
|
6
|
+
velocity_burst_multiplier: 5,
|
|
7
|
+
burst_window_hours: 24,
|
|
8
|
+
min_velocity_baseline: 0.5,
|
|
9
|
+
duplicate_version_weight: 40,
|
|
10
|
+
unusual_timing_weight: 25,
|
|
11
|
+
cross_package_burst_weight: 50,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function parseVersionHistory(registryMeta) {
|
|
15
|
+
const timeData = registryMeta?.time;
|
|
16
|
+
if (!timeData || typeof timeData !== 'object') return [];
|
|
17
|
+
|
|
18
|
+
return Object.entries(timeData)
|
|
19
|
+
.map(([ver, ts]) => ({
|
|
20
|
+
version: ver,
|
|
21
|
+
time: new Date(ts).getTime(),
|
|
22
|
+
date: new Date(ts),
|
|
23
|
+
}))
|
|
24
|
+
.filter((e) => !isNaN(e.time))
|
|
25
|
+
.sort((a, b) => a.time - b.time);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getHour(date) {
|
|
29
|
+
return date.getUTCHours();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function calculateVelocity(history) {
|
|
33
|
+
if (history.length < 2) return { perWeek: 0, perDay: 0 };
|
|
34
|
+
const firstTime = history[0].time;
|
|
35
|
+
const lastTime = history[history.length - 1].time;
|
|
36
|
+
const spanMs = lastTime - firstTime;
|
|
37
|
+
const spanDays = spanMs / (1000 * 60 * 60 * 24);
|
|
38
|
+
if (spanDays < 1) return { perWeek: history.length, perDay: history.length };
|
|
39
|
+
const perDay = history.length / Math.max(spanDays, 1);
|
|
40
|
+
const perWeek = perDay * 7;
|
|
41
|
+
return { perWeek, perDay };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectBursts(history) {
|
|
45
|
+
const bursts = [];
|
|
46
|
+
const windowMs = (THRESHOLDS.burst_window_hours || 24) * 60 * 60 * 1000;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < history.length; i++) {
|
|
49
|
+
const windowEnd = history[i].time + windowMs;
|
|
50
|
+
const group = [];
|
|
51
|
+
for (let j = i; j < history.length && history[j].time <= windowEnd; j++) {
|
|
52
|
+
group.push(history[j]);
|
|
53
|
+
}
|
|
54
|
+
if (group.length >= 3) {
|
|
55
|
+
const groupHour = group.map((e) => getHour(e.date));
|
|
56
|
+
const timings = groupHour.filter((h) => h >= 0 && h <= 5);
|
|
57
|
+
bursts.push({
|
|
58
|
+
count: group.length,
|
|
59
|
+
windowHours: windowMs / (1000 * 60 * 60),
|
|
60
|
+
versions: group.map((e) => e.version),
|
|
61
|
+
unusualTimings: [...new Set(timings)]
|
|
62
|
+
.sort()
|
|
63
|
+
.map((h) => `${String(h).padStart(2, '0')}:00 UTC`),
|
|
64
|
+
startTime: group[0].date.toISOString(),
|
|
65
|
+
endTime: group[group.length - 1].date.toISOString(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return bursts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function computeConfidence(bursts, velocity, unusualTimingsCount) {
|
|
73
|
+
if (bursts.length === 0) return 0;
|
|
74
|
+
let base = 40;
|
|
75
|
+
|
|
76
|
+
const burst = bursts[0];
|
|
77
|
+
const burstMultiplier =
|
|
78
|
+
velocity.perWeek > 0
|
|
79
|
+
? burst.count / Math.max(velocity.perWeek, THRESHOLDS.min_velocity_baseline)
|
|
80
|
+
: burst.count;
|
|
81
|
+
|
|
82
|
+
if (burstMultiplier >= THRESHOLDS.velocity_burst_multiplier) {
|
|
83
|
+
base += Math.min(burstMultiplier * 3, 30);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (unusualTimingsCount > 0) {
|
|
87
|
+
base += Math.min(unusualTimingsCount * THRESHOLDS.unusual_timing_weight, 20);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (burst.count >= 10) {
|
|
91
|
+
base += 15;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Math.min(100, Math.max(0, base));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function severityLabel(score) {
|
|
98
|
+
if (score >= 80) return 'critical';
|
|
99
|
+
if (score >= 60) return 'high';
|
|
100
|
+
return 'medium';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function confidenceLabel(score) {
|
|
104
|
+
if (score >= 80) return 'CRITICAL';
|
|
105
|
+
if (score >= 60) return 'HIGH';
|
|
106
|
+
return 'MEDIUM';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const name = 'tier1-maintainer-compromise';
|
|
110
|
+
|
|
111
|
+
export async function scan(pkgJson, _jsFiles, registryMeta, _allFiles) {
|
|
112
|
+
const pkgName = pkgJson?.name;
|
|
113
|
+
if (!pkgName) return [];
|
|
114
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
115
|
+
|
|
116
|
+
const history = parseVersionHistory(registryMeta);
|
|
117
|
+
if (history.length < 3) return [];
|
|
118
|
+
|
|
119
|
+
const velocity = calculateVelocity(history);
|
|
120
|
+
const bursts = detectBursts(history);
|
|
121
|
+
if (bursts.length === 0) return [];
|
|
122
|
+
|
|
123
|
+
const burst = bursts[0];
|
|
124
|
+
const unusualTimings = burst.unusualTimings;
|
|
125
|
+
const burstMultiplier =
|
|
126
|
+
velocity.perWeek > 0
|
|
127
|
+
? burst.count / Math.max(velocity.perWeek, THRESHOLDS.min_velocity_baseline)
|
|
128
|
+
: burst.count;
|
|
129
|
+
|
|
130
|
+
const crossPackageBurst = registryMeta?.crossPackageBurst || false;
|
|
131
|
+
|
|
132
|
+
const confidenceScore = computeConfidence(bursts, velocity, unusualTimings.length);
|
|
133
|
+
if (confidenceScore < THRESHOLDS.warn_threshold) return [];
|
|
134
|
+
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
detector: 'tier1-maintainer-compromise',
|
|
138
|
+
id: 'TIER1-MAINTAINER-COMPROMISE',
|
|
139
|
+
severity: severityLabel(confidenceScore),
|
|
140
|
+
confidence: confidenceLabel(confidenceScore),
|
|
141
|
+
confidenceScore,
|
|
142
|
+
subtype: 'maintainer_compromise_burst',
|
|
143
|
+
message: `Maintainer compromise detected: ${burst.count} versions in ${burst.windowHours}h window (${burstMultiplier.toFixed(1)}x normal velocity)`,
|
|
144
|
+
evidence: [
|
|
145
|
+
`normal_velocity: ${velocity.perWeek.toFixed(1)}/week (${velocity.perDay.toFixed(1)}/day)`,
|
|
146
|
+
`burst_count: ${burst.count} in ${burst.windowHours}h`,
|
|
147
|
+
`burst_multiplier: ${burstMultiplier.toFixed(1)}x`,
|
|
148
|
+
`unusual_timings: ${unusualTimings.length > 0 ? unusualTimings.join(', ') : 'none'}`,
|
|
149
|
+
`cross_package: ${crossPackageBurst}`,
|
|
150
|
+
`versions: ${burst.versions.slice(0, 5).join(', ')}${burst.versions.length > 5 ? `... (+${burst.versions.length - 5} more)` : ''}`,
|
|
151
|
+
],
|
|
152
|
+
locations: [{ file: 'package.json', line: 1, column: 1 }],
|
|
153
|
+
crossFiles: [],
|
|
154
|
+
reference: 'D13: @redhat-cloud-services maintainer compromise',
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|