@lateos/npm-scan 0.15.3 → 0.15.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.
@@ -0,0 +1,118 @@
1
+ import { checkBurstPublish } from './d1-burst-publish.js';
2
+ import { checkSiblingCompromise, clearSiblingCache } from './d2-sibling-compromise.js';
3
+ import { checkSlsaMismatch } from './d3-slsa-mismatch.js';
4
+ import { checkMaintainerAnomaly } from './d4-maintainer-anomaly.js';
5
+ import { checkIOC } from './d5-ioc-check.js';
6
+ import { checkTokenExfil } from './d6-token-exfil.js';
7
+
8
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
9
+ const config = {};
10
+
11
+ const burstResult = await checkBurstPublish(registryMeta, config);
12
+ const maintainerResult = await checkMaintainerAnomaly(registryMeta, config);
13
+
14
+ const pkgName = pkgJson?.name || '';
15
+ const pkgVersion = pkgJson?.version || '';
16
+ const sha512 = registryMeta?.versions?.[pkgVersion]?.dist?.integrity || null;
17
+ const publisherAccount = registryMeta?.versions?.[pkgVersion]?._npmUser?.name || null;
18
+ const timeMap = registryMeta?.time || {};
19
+
20
+ const iocResult = await checkIOC(pkgName, pkgVersion, sha512, publisherAccount, timeMap);
21
+ const exfilResult = checkTokenExfil(allFiles || files, pkgJson);
22
+
23
+ let siblingResult = { triggered: false };
24
+ let slsaResult = { triggered: false };
25
+
26
+ if (burstResult.triggered) {
27
+ siblingResult = await checkSiblingCompromise(pkgJson, config);
28
+ slsaResult = await checkSlsaMismatch(pkgName, pkgVersion, burstResult, timeMap, config);
29
+ }
30
+
31
+ const nxDownstreamResult = checkNxConsoleDownstream(pkgJson, allFiles || files);
32
+
33
+ const triggeredChecks = [];
34
+ if (burstResult.triggered) triggeredChecks.push('D1_BURST');
35
+ if (siblingResult.triggered) triggeredChecks.push('D2_SIBLING');
36
+ if (slsaResult.triggered) triggeredChecks.push('D3_SLSA');
37
+ if (maintainerResult.triggered) triggeredChecks.push('D4_MAINTAINER');
38
+ if (iocResult.triggered) triggeredChecks.push('D5_IOC');
39
+ if (exfilResult.triggered) triggeredChecks.push('D6_EXFIL');
40
+ if (nxDownstreamResult.triggered) triggeredChecks.push('D7_NX_CONSOLE');
41
+
42
+ if (triggeredChecks.length === 0) return [];
43
+
44
+ let waveAttribution = 'unknown';
45
+ if (pkgName.startsWith('@tanstack')) {
46
+ waveAttribution = 'wave1-tanstack';
47
+ } else if (pkgName.startsWith('@antv')) {
48
+ waveAttribution = 'wave2-antv';
49
+ } else if (nxDownstreamResult.triggered) {
50
+ waveAttribution = 'wave3-nx-console';
51
+ } else if (iocResult.matches && iocResult.matches.length > 0) {
52
+ const waves = [...new Set(iocResult.matches.map(m => m.wave))];
53
+ if (waves.length === 1) {
54
+ waveAttribution = waves[0] === 1 ? 'wave1-tanstack' : waves[0] === 2 ? 'wave2-antv' : 'wave3-nx-console';
55
+ }
56
+ }
57
+
58
+ const isCritical = slsaResult.triggered || iocResult.triggered || nxDownstreamResult.triggered;
59
+
60
+ const evidence = {
61
+ campaign: 'MINI_SHAI_HULUD',
62
+ waveAttribution,
63
+ triggeredChecks,
64
+ burstWindow: burstResult.triggered
65
+ ? { start: burstResult.windowStart, end: burstResult.windowEnd, versionCount: burstResult.versionCount }
66
+ : null,
67
+ siblingPackages: siblingResult.triggered
68
+ ? siblingResult.results.flatMap(r => r.siblingPackages)
69
+ : null,
70
+ attestationAnomalies: slsaResult.triggered ? slsaResult.anomalies : null,
71
+ iocMatches: iocResult.triggered ? iocResult.matches : null,
72
+ installScriptSnippets: exfilResult.triggered ? exfilResult.snippets : null,
73
+ nxConsoleDownstream: nxDownstreamResult.triggered
74
+ ? { nxDeps: nxDownstreamResult.nxDeps, vsCodeExtensions: nxDownstreamResult.vsCodeExtensions }
75
+ : null,
76
+ };
77
+
78
+ return [{
79
+ id: 'MINI_SHAI_HULUD',
80
+ severity: isCritical ? 'critical' : 'high',
81
+ title: 'Mini Shai-Hulud worm campaign',
82
+ description: `${triggeredChecks.length} signal(s): ${triggeredChecks.join(', ')}`,
83
+ evidence: JSON.stringify(evidence),
84
+ mitigation: 'Revoke all npm tokens immediately. Rotate CI/CD secrets. Audit maintainer access on all scoped packages. Review recent version publish history for anomalous bursts. Check for postinstall scripts accessing credentials or environment variables. If Wave 1 (TanStack scope): inspect GitHub Actions workflow logs for unauthorized build steps. If Wave 2 (atool/AntV scope): rotate all npm tokens associated with @antv/* packages. If Wave 3 (Nx Console): remove nrwl.angular-console extension immediately, revoke all npm tokens used in CI/CD, and audit @nx/* dependency versions.',
85
+ }];
86
+ }
87
+
88
+ function checkNxConsoleDownstream(pkgJson, allFiles) {
89
+ const deps = { ...pkgJson?.dependencies, ...pkgJson?.devDependencies, ...pkgJson?.peerDependencies };
90
+ const nxDeps = Object.keys(deps).filter(d => d.startsWith('@nx/') || d.startsWith('nrwl/'));
91
+ if (nxDeps.length === 0) return { triggered: false, nxDeps: [], vsCodeExtensions: [] };
92
+
93
+ let vsCodeExtensions = [];
94
+ if (allFiles && Array.isArray(allFiles)) {
95
+ for (const file of allFiles) {
96
+ if (file.path && (file.path.endsWith('.vscode/extensions.json') || file.path.endsWith('.vscode/extensions.json'))) {
97
+ try {
98
+ const content = typeof file.content === 'string' ? file.content : '';
99
+ const parsed = JSON.parse(content);
100
+ const allExts = [
101
+ ...(parsed.recommendations || []),
102
+ ...(parsed.unwantedRecommendations || []),
103
+ ];
104
+ const matched = allExts.filter(e => e.includes('nrwl.angular-console'));
105
+ if (matched.length > 0) {
106
+ vsCodeExtensions = matched;
107
+ }
108
+ } catch {
109
+ // non-JSON extensions.json, skip
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ return { triggered: true, nxDeps, vsCodeExtensions };
116
+ }
117
+
118
+ export { clearSiblingCache } from './d2-sibling-compromise.js';
@@ -0,0 +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
+ }
@@ -0,0 +1,116 @@
1
+ const ACTIVATION_RISK_MATRIX = {
2
+ '*': { base: 'critical', label: 'Wildcard (all files)' },
3
+ 'onStartupFinished': { base: 'high', label: 'Startup finished' },
4
+ 'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
5
+ 'workspaceContains': { base: 'high', label: 'Workspace contains' },
6
+ 'onCommand:*': { base: 'low', label: 'Any command' },
7
+ };
8
+
9
+ const DEFAULT_BASE_RISK = 'medium';
10
+
11
+ const ESCALATION_KEYWORDS = [
12
+ 'npx', 'bun', 'curl', 'wget', 'fetch(',
13
+ 'exec(', 'spawn(', 'execSync', 'spawnSync',
14
+ 'child_process', 'shell: true', 'detached: true',
15
+ ];
16
+
17
+ const BUNDLED_BUN_PATTERN = /bun|runtime/;
18
+
19
+ const SIZE_DELTA_THRESHOLD = 400 * 1024;
20
+
21
+ const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
22
+
23
+ export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
24
+ const signals = [];
25
+
26
+ const activationEvents = extensionManifest?.activationEvents || [];
27
+ if (activationEvents.length === 0 && extensionManifest?.main) {
28
+ return { triggered: false, signals: [], riskLevel: null, why: [] };
29
+ }
30
+
31
+ let maxBaseRisk = 0;
32
+ const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
33
+ const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
34
+
35
+ let worstEvent = null;
36
+ const why = [];
37
+
38
+ for (const event of activationEvents) {
39
+ const risk = ACTIVATION_RISK_MATRIX[event];
40
+ if (risk) {
41
+ const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
42
+ if (baseIdx > maxBaseRisk) {
43
+ maxBaseRisk = baseIdx;
44
+ worstEvent = event;
45
+ }
46
+ } else if (event.includes('*') && event !== 'onCommand:*') {
47
+ const baseIdx = riskValues['high'];
48
+ if (baseIdx > maxBaseRisk) {
49
+ maxBaseRisk = baseIdx;
50
+ worstEvent = event;
51
+ }
52
+ }
53
+ }
54
+
55
+ const contributes = extensionManifest?.contributes || {};
56
+ const commands = contributes?.commands || [];
57
+ const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
58
+
59
+ const bundledDeps = extensionManifest?.bundledDependencies || [];
60
+ const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
61
+
62
+ const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
63
+ const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
64
+
65
+ const activationEventsStr = activationEvents.join(' ');
66
+ const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
67
+
68
+ let escalateToCritical = false;
69
+
70
+ if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
71
+ escalateToCritical = true;
72
+ why.push('HIGH activation event + shell/execution keywords');
73
+ }
74
+
75
+ if (versionHistory.length >= 2) {
76
+ const sizes = versionHistory
77
+ .filter(v => v.assetSize)
78
+ .map(v => v.assetSize)
79
+ .sort((a, b) => b - a);
80
+
81
+ if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
82
+ escalateToCritical = true;
83
+ why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
84
+ }
85
+ }
86
+
87
+ const priorActivationEvents = priorVersions
88
+ .filter(v => v.activationEvents)
89
+ .flatMap(v => v.activationEvents);
90
+
91
+ if (priorActivationEvents.length > 0) {
92
+ const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
93
+ if (newEvents.length > 0) {
94
+ why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
95
+ if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
96
+ escalateToCritical = true;
97
+ }
98
+ }
99
+ }
100
+
101
+ let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
102
+ if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
103
+ riskLevel = 'critical';
104
+ }
105
+
106
+ if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
107
+
108
+ signals.push({
109
+ type: 'ACTIVATION_EVENT_RISK',
110
+ activationEvents,
111
+ riskLevel,
112
+ why,
113
+ });
114
+
115
+ return { triggered: true, signals, riskLevel, why };
116
+ }
@@ -0,0 +1,52 @@
1
+ export async function checkBurstPublish(versionHistory, config = {}) {
2
+ const windowMinutes = config.burstWindowMinutes ?? 30;
3
+ const threshold = config.burstVersionThreshold ?? 2;
4
+ const hotPullMinutes = config.hotPullMinutes ?? 20;
5
+
6
+ const entries = versionHistory
7
+ .filter(v => v.publishedAt)
8
+ .map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
+ .filter(e => !Number.isNaN(e.time))
10
+ .sort((a, b) => a.time - b.time);
11
+
12
+ if (entries.length < threshold) return { triggered: false };
13
+
14
+ const windowMs = windowMinutes * 60 * 1000;
15
+ let burstFound = false;
16
+ let burstWindowStart = null;
17
+ let burstWindowEnd = null;
18
+ let burstVersionCount = 0;
19
+ let burstVersions = [];
20
+
21
+ for (let i = 0; i < entries.length; i++) {
22
+ const start = entries[i].time;
23
+ const end = start + windowMs;
24
+ const inWindow = entries.filter(e => e.time >= start && e.time <= end);
25
+
26
+ if (inWindow.length >= threshold) {
27
+ burstFound = true;
28
+ burstWindowStart = new Date(start).toISOString();
29
+ burstWindowEnd = new Date(end).toISOString();
30
+ burstVersionCount = inWindow.length;
31
+ burstVersions = inWindow.map(e => e.version);
32
+ break;
33
+ }
34
+ }
35
+
36
+ let hotPullDetected = false;
37
+ for (let i = 1; i < entries.length; i++) {
38
+ const gapMinutes = (entries[i].time - entries[i - 1].time) / (1000 * 60);
39
+ if (gapMinutes > 0 && gapMinutes < hotPullMinutes) {
40
+ hotPullDetected = true;
41
+ break;
42
+ }
43
+ }
44
+
45
+ return {
46
+ triggered: burstFound || hotPullDetected,
47
+ burstWindow: burstFound
48
+ ? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
49
+ : null,
50
+ hotPullDetected,
51
+ };
52
+ }
@@ -0,0 +1,88 @@
1
+ const CREDENTIAL_FILE_PATTERNS = [
2
+ /~\/\.npmrc/,
3
+ /~\/\.gitconfig/,
4
+ /~\/\.aws\/credentials/,
5
+ /~\/\.ssh\/id_\w+/,
6
+ /~\/\.vault-token/,
7
+ /~\/\.claude\/settings\.json/,
8
+ /~\/Library\/Application\s+Support\/1Password\//,
9
+ /\/etc\/vault\/token/,
10
+ /\/proc\/\*\/mem/,
11
+ /\$GITHUB_ENV/,
12
+ /\$GITHUB_TOKEN/,
13
+ /\$NPM_TOKEN/,
14
+ /\$NODE_AUTH_TOKEN/,
15
+ /GH_TOKEN/,
16
+ ];
17
+
18
+ const EXFIL_CHANNEL_PATTERNS = [
19
+ /(?:[a-z0-9_-]{40,})\.[a-z0-9_-]+\.(?:com|io|org|net|app|dev|xyz)(?:\/[^\s"')\]]{0,50})?/i,
20
+ /\/gists\b.*authorization/i,
21
+ /\/repos\/[^/]+\/[^/]+\/git\/refs/i,
22
+ /AES-256-GCM/,
23
+ /RSA\/(?:PKCS|OAEP)/,
24
+ ];
25
+
26
+ const ANTI_ANALYSIS_PATTERNS = [
27
+ { pattern: /os\.cpus\(\)\.length\s*<\s*4/, label: 'CPU core count check (< 4)' },
28
+ { pattern: /Intl\.DateTimeFormat.*(?:timeZone|locale)/, label: 'Timezone/locale check' },
29
+ { pattern: /Intl\.DateTimeFormat.*\b(?:ru|rus|kz|by|cn|cns)\b/i, label: 'CIS/locale filtering' },
30
+ { pattern: /\bspawn\(\s*[^,]+,\s*\{[^}]*detached:\s*true\s*\}/, label: 'Detached process spawn' },
31
+ { pattern: /\bBUN_INSTALL\b/, label: 'BUN_INSTALL env reference' },
32
+ { pattern: /~\/\.bun\/bin\/bun/, label: 'Bun binary path' },
33
+ { pattern: /\bBun\.file\(/, label: 'Bun.file() API' },
34
+ { pattern: /\bBun\.serve\(/, label: 'Bun.serve() API' },
35
+ ];
36
+
37
+ function truncateSnippet(str, maxLen = 200) {
38
+ if (!str || str.length <= maxLen) return str || '';
39
+ return str.slice(0, maxLen) + '...';
40
+ }
41
+
42
+ export async function checkExfilPattern(extensionFiles = []) {
43
+ const signals = [];
44
+ const exfilPatterns = [];
45
+ const antiAnalysisTechniques = [];
46
+
47
+ for (const file of extensionFiles) {
48
+ const content = typeof file.content === 'string' ? file.content : '';
49
+ if (!content) continue;
50
+ const path = file.path || '';
51
+
52
+ for (const cp of CREDENTIAL_FILE_PATTERNS) {
53
+ const match = content.match(cp);
54
+ if (match) {
55
+ const snippet = truncateSnippet(match[0]);
56
+ if (!exfilPatterns.some(e => e.includes(snippet))) {
57
+ exfilPatterns.push(`${path}: ${snippet}`);
58
+ signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
59
+ }
60
+ }
61
+ }
62
+
63
+ for (const ep of EXFIL_CHANNEL_PATTERNS) {
64
+ const match = content.match(ep);
65
+ if (match) {
66
+ const snippet = truncateSnippet(match[0]);
67
+ exfilPatterns.push(`${path}: ${snippet}`);
68
+ signals.push({ type: 'EXFIL_CHANNEL', pattern: ep.source, file: path });
69
+ }
70
+ }
71
+
72
+ for (const ap of ANTI_ANALYSIS_PATTERNS) {
73
+ if (ap.pattern.test(content)) {
74
+ if (!antiAnalysisTechniques.includes(ap.label)) {
75
+ antiAnalysisTechniques.push(ap.label);
76
+ signals.push({ type: 'ANTI_ANALYSIS', technique: ap.label, file: path });
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ return {
83
+ triggered: signals.length > 0,
84
+ signals,
85
+ exfilPatterns,
86
+ antiAnalysisTechniques,
87
+ };
88
+ }
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ let iocsData = null;
6
+ let iocsLoaded = false;
7
+ let iocLoadError = null;
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const IOC_PATH = join(__dirname, '..', 'vsix-iocs.json');
12
+
13
+ function loadIOCData() {
14
+ if (iocsLoaded) return iocsData;
15
+ iocsLoaded = true;
16
+ try {
17
+ iocsData = JSON.parse(readFileSync(IOC_PATH, 'utf8'));
18
+ } catch (err) {
19
+ iocLoadError = err;
20
+ iocsData = null;
21
+ }
22
+ return iocsData;
23
+ }
24
+
25
+ export function getIOCLoadError() {
26
+ return iocLoadError;
27
+ }
28
+
29
+ export function reloadIOCData() {
30
+ iocsLoaded = false;
31
+ iocLoadError = null;
32
+ return loadIOCData();
33
+ }
34
+
35
+ export async function checkKnownIOC(extensionId, version, publisherAccount, orphanCommits = [], versionHistory = []) {
36
+ const data = loadIOCData();
37
+ if (!data) return { triggered: false, matches: [] };
38
+
39
+ const matches = [];
40
+ const iocs = data.iocs || [];
41
+
42
+ for (const ioc of iocs) {
43
+ switch (ioc.type) {
44
+ case 'extensionId': {
45
+ if (ioc.value === extensionId) {
46
+ if (!ioc.maliciousVersions || ioc.maliciousVersions.length === 0 || ioc.maliciousVersions.includes(version)) {
47
+ matches.push({
48
+ type: 'extensionId',
49
+ value: extensionId,
50
+ maliciousVersion: version,
51
+ wave: ioc.wave,
52
+ cve: ioc.cve,
53
+ exposureWindowStart: ioc.exposureWindowStart,
54
+ exposureWindowEnd: ioc.exposureWindowEnd,
55
+ });
56
+ }
57
+ }
58
+ break;
59
+ }
60
+
61
+ case 'publisherAccount': {
62
+ if (ioc.value === publisherAccount) {
63
+ const pubTime = versionHistory.length > 0
64
+ ? new Date(versionHistory[versionHistory.length - 1]?.publishedAt).getTime()
65
+ : null;
66
+
67
+ const windowStart = new Date(ioc.compromiseWindowStart).getTime();
68
+ const windowEnd = ioc.compromiseWindowEnd
69
+ ? new Date(ioc.compromiseWindowEnd).getTime()
70
+ : Infinity;
71
+
72
+ if (pubTime && !Number.isNaN(pubTime) && pubTime >= windowStart && pubTime <= windowEnd) {
73
+ matches.push({
74
+ type: 'publisherAccount',
75
+ value: publisherAccount,
76
+ wave: ioc.wave,
77
+ compromiseWindowStart: ioc.compromiseWindowStart,
78
+ compromiseWindowEnd: ioc.compromiseWindowEnd,
79
+ });
80
+ }
81
+ }
82
+ break;
83
+ }
84
+
85
+ case 'orphanCommitHash': {
86
+ for (const commit of orphanCommits) {
87
+ if (ioc.value === commit || (ioc.value === 'PLACEHOLDER_UPDATE_FROM_THREAT_INTEL')) {
88
+ continue;
89
+ }
90
+ if (ioc.value && commit && ioc.value.toLowerCase() === commit.toLowerCase()) {
91
+ matches.push({
92
+ type: 'orphanCommitHash',
93
+ value: commit,
94
+ repo: ioc.repo,
95
+ wave: ioc.wave,
96
+ });
97
+ }
98
+ }
99
+ break;
100
+ }
101
+ }
102
+ }
103
+
104
+ return { triggered: matches.length > 0, matches };
105
+ }
@@ -0,0 +1,69 @@
1
+ const GITHUB_COMMIT_SHA_PATTERN = /api\.github\.com\/repos\/[^/]+\/[^/]+\/git\/commits\/[a-f0-9]{40}/;
2
+ const NPX_GIT_URL_PATTERN = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
3
+ const MCP_KEYWORDS = ['mcp', 'model-context-protocol', 'claude', 'setup', 'init'];
4
+ const EXTERNAL_FETCH_PATTERN = /(?:https?:\/\/)[^\s"')\]]+(?:\.com|\.io|\.org|\.dev|\.app|\.net)[^\s"')\]]*/;
5
+ const NON_NPMJS_FETCH = /(?:fetch|curl|wget)\s*\(?\s*["']https?:\/\/(?!(?:.*npmjs\.org|.*npm\.js\.org|.*github\.com))[^"']+/;
6
+ const BUN_PATTERNS = [/bun\s+install/, /install\s+.*bun/, /\bbunx\b/, /\.bun\/bin\//];
7
+ const NPX_GIT_SHORT = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
8
+
9
+ export async function checkOrphanCommitFetch(extensionFiles = []) {
10
+ const signals = [];
11
+ const indicators = [];
12
+
13
+ for (const file of extensionFiles) {
14
+ const content = typeof file.content === 'string' ? file.content : '';
15
+ if (!content) continue;
16
+ const path = file.path || '';
17
+
18
+ if (GITHUB_COMMIT_SHA_PATTERN.test(content)) {
19
+ const matches = content.match(GITHUB_COMMIT_SHA_PATTERN);
20
+ if (matches) {
21
+ indicators.push(`${path}: GitHub git commit SHA reference`);
22
+ signals.push({
23
+ type: 'ORPHAN_COMMIT_GITHUB_API',
24
+ indicator: 'GitHub API direct commit SHA resolution',
25
+ file: path,
26
+ });
27
+ }
28
+ }
29
+
30
+ if (NPX_GIT_URL_PATTERN.test(content)) {
31
+ const matches = content.match(NPX_GIT_URL_PATTERN);
32
+ if (matches) {
33
+ indicators.push(`${path}: npx with git URL`);
34
+ signals.push({
35
+ type: 'NPX_GIT_URL',
36
+ indicator: 'npx resolves from git URL (non-registry)',
37
+ file: path,
38
+ });
39
+ }
40
+ }
41
+
42
+ const hasMCPKeywords = MCP_KEYWORDS.some(kw =>
43
+ new RegExp(`\\b${kw}\\b`, 'i').test(content));
44
+ const hasExternalFetch = NON_NPMJS_FETCH.test(content);
45
+
46
+ if (hasMCPKeywords && hasExternalFetch) {
47
+ indicators.push(`${path}: MCP-adjacent keywords + external fetch`);
48
+ signals.push({
49
+ type: 'MCP_DISGUISED_EXFIL',
50
+ indicator: 'Shell command disguised as MCP setup',
51
+ file: path,
52
+ });
53
+ }
54
+
55
+ for (const bp of BUN_PATTERNS) {
56
+ if (bp.test(content)) {
57
+ indicators.push(`${path}: Bun installation pattern`);
58
+ signals.push({
59
+ type: 'BUN_INSTALL',
60
+ indicator: `Bun runtime install pattern: ${bp.source}`,
61
+ file: path,
62
+ });
63
+ break;
64
+ }
65
+ }
66
+ }
67
+
68
+ return { triggered: signals.length > 0, signals, indicators };
69
+ }