@lateos/npm-scan 0.16.0 → 0.16.4

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,90 @@
1
+ const SUSPICIOUS_HOOKS = ['postinstall', 'install', 'preinstall'];
2
+ const TEMP_DIR_RE = /(?:os\.tmpdir|tmpdir|temp|%TEMP%|\/tmp|\/var\/tmp)/;
3
+ const POWERSHELL_RE = /powershell|pwsh|cmd\.exe|Invoke-Expression|IEX\s*\(/;
4
+ const LAUNCHD_RE = /\/Library\/Launch(Daemons|Agents)\/|launchctl\s+(load|start|submit)/;
5
+ const SYSTEMD_SERVICE_RE = /\/etc\/systemd\/system\/|systemctl\s+(enable|start|daemon-reload)/;
6
+ const CRON_PERSIST_RE = /crontab\s+-[ei]|@reboot\s+|@daily\s+|@hourly\s+/;
7
+ const DLL_LOAD_RE = /LoadLibrary|dlopen|LoadLibraryEx|lib\.(?:LoadLibrary|dlopen)/;
8
+ const PROCESS_INJECT_RE = /CreateRemoteThread|VirtualAllocEx|WriteProcessMemory|NtCreateThreadEx/;
9
+ const NET_CALLBACK_RE = /(?:https?:\/\/|wss?:\/\/|ws:\/\/)(?:[^\s'"]*\.[^\s'"]{2,})/;
10
+ const BINARY_DROP_RE = /(?:fs\.writeFileSync|writeFile|writeFileSync)\s*\([^)]*(?:\.exe|\.dll|\.bin|\.bat|\.ps1)/;
11
+
12
+ const SUSPICIOUS_HOOK_PATTERNS = [
13
+ /curl|wget|fetch|https?:\/\//,
14
+ /powershell|cmd\.exe|bash\b|sh\b/,
15
+ /process\.exit|fs\.chmod|exec(?:Sync)?\s*\(/,
16
+ /spawn|fork|detached/,
17
+ /systemd|launchctl|crontab|schtasks/,
18
+ /LoadLibrary|dlopen/,
19
+ /eval|Function\s*\(/,
20
+ /__dirname|__filename/,
21
+ ];
22
+
23
+ export function scanPostinstallRAT(pkgJson, files = []) {
24
+ const scripts = pkgJson?.scripts || {};
25
+ const code = files.map(f => f.content || '').join('\n');
26
+
27
+ const activeHooks = [];
28
+ for (const hook of SUSPICIOUS_HOOKS) {
29
+ if (scripts[hook]) {
30
+ activeHooks.push({ hook, command: scripts[hook] });
31
+ }
32
+ }
33
+
34
+ if (activeHooks.length === 0) {
35
+ return { triggered: false, platforms: [], c2Indicators: [], payloadType: null, hooks: [], hasBinaryDrop: false };
36
+ }
37
+
38
+ const combined = code + '\n' + activeHooks.map(h => h.command).join('\n');
39
+
40
+ const hasSuspiciousCode = SUSPICIOUS_HOOK_PATTERNS.some(p => p.test(combined));
41
+
42
+ if (activeHooks.length > 0 && !hasSuspiciousCode) {
43
+ return { triggered: false, platforms: [], c2Indicators: [], payloadType: null, hooks: [], hasBinaryDrop: false };
44
+ }
45
+
46
+ const platforms = [];
47
+ let c2Indicators = [];
48
+ let hasBinaryDrop = false;
49
+
50
+ if (POWERSHELL_RE.test(combined)) platforms.push('windows');
51
+ if (LAUNCHD_RE.test(combined)) platforms.push('macos');
52
+ if (SYSTEMD_SERVICE_RE.test(combined) || CRON_PERSIST_RE.test(combined)) platforms.push('linux');
53
+ if (TEMP_DIR_RE.test(combined) && (POWERSHELL_RE.test(combined) || BINARY_DROP_RE.test(combined))) {
54
+ if (!platforms.includes('windows')) platforms.push('windows');
55
+ if (!platforms.includes('linux')) platforms.push('linux');
56
+ if (!platforms.includes('macos')) platforms.push('macos');
57
+ }
58
+
59
+ if (DLL_LOAD_RE.test(combined)) platforms.push('windows');
60
+ if (PROCESS_INJECT_RE.test(combined)) platforms.push('windows');
61
+
62
+ if (NET_CALLBACK_RE.test(combined)) {
63
+ const urls = combined.match(NET_CALLBACK_RE);
64
+ c2Indicators = urls ? [...new Set(urls.map(u => u.replace(/['")]/g, '')))] : ['Network callback to external server'];
65
+ }
66
+
67
+ if (BINARY_DROP_RE.test(combined)) hasBinaryDrop = true;
68
+
69
+ let payloadType = null;
70
+ if (platforms.length >= 2 && c2Indicators.length > 0 && hasBinaryDrop) {
71
+ payloadType = 'cross_platform_RAT';
72
+ } else if (hasBinaryDrop && c2Indicators.length > 0) {
73
+ payloadType = 'network_backdoor';
74
+ } else if (hasBinaryDrop && platforms.length > 0) {
75
+ payloadType = 'platform_persistence';
76
+ }
77
+
78
+ if (payloadType) {
79
+ return {
80
+ triggered: true,
81
+ payloadType,
82
+ platforms: [...new Set(platforms)],
83
+ c2Indicators,
84
+ hooks: activeHooks.map(h => h.hook),
85
+ hasBinaryDrop,
86
+ };
87
+ }
88
+
89
+ return { triggered: false, platforms: [], c2Indicators: [], payloadType: null, hooks: [], hasBinaryDrop: false };
90
+ }
@@ -0,0 +1,94 @@
1
+ import { scanVersionBlocklist } from './d1-version-fingerprint.js';
2
+ import { scanDecoyDependency } from './d2-decoy-dep.js';
3
+ import { scanPostinstallRAT } from './d3-postinstall-rat.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 = scanVersionBlocklist(pkgJson);
20
+ if (d1Result.stopCondition) {
21
+ const evidence = attachProvenance({
22
+ rule: 'AXS-VER-001',
23
+ campaign: 'AXIOS_POISONING',
24
+ triggeredChecks: ['D1'],
25
+ matchedVersion: d1Result.matchedVersion,
26
+ action: 'BLOCK_IMMEDIATELY',
27
+ remediation: `Upgrade to axios@1.14.2 or later, or use pinned safe version`,
28
+ }, {
29
+ ruleId: 'AXS-VER-001',
30
+ ruleName: 'Compromised Axios Version Fingerprinting',
31
+ severity: 'CRITICAL',
32
+ campaignName: 'Axios Registry Poisoning',
33
+ pkgName,
34
+ pkgVersion,
35
+ triggered: true,
36
+ severity: 'critical',
37
+ indicators: [{ type: 'known_malicious_version', value: `${pkgName}@${pkgVersion}` }],
38
+ ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/axios-poisoning/d1-version-fingerprint.js',
39
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
40
+ });
41
+
42
+ return [{
43
+ id: 'AXIOS_POISONING',
44
+ severity: 'critical',
45
+ title: 'Axios Registry Poisoning campaign',
46
+ description: `HALT: ${pkgName}@${pkgVersion} is a known compromised version in the Axios registry poisoning campaign. Block install immediately.`,
47
+ evidence: JSON.stringify(evidence),
48
+ mitigation: d1Result.reason ? `BLOCK IMMEDIATELY. ${d1Result.reason}. Upgrade to axios@1.14.2 or later, or use pinned safe version.` : 'BLOCK IMMEDIATELY. Upgrade to a safe version.',
49
+ stopCondition: true,
50
+ }];
51
+ }
52
+
53
+ const d2Result = scanDecoyDependency(pkgJson);
54
+ const d3Result = scanPostinstallRAT(pkgJson, fileList);
55
+
56
+ const results = { D1: d1Result, D2: d2Result, D3: d3Result };
57
+
58
+ const triggered = Object.entries(results)
59
+ .filter(([_, r]) => r.triggered)
60
+ .map(([id]) => id);
61
+
62
+ if (triggered.length === 0) return [];
63
+
64
+ const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
65
+
66
+ const evidence = attachProvenance({
67
+ campaign: 'AXIOS_POISONING',
68
+ triggeredChecks: triggered,
69
+ details: Object.fromEntries(
70
+ Object.entries(results).filter(([_, r]) => r.triggered)
71
+ ),
72
+ }, {
73
+ ruleId: 'AXIOS_POISONING',
74
+ ruleName: 'Axios Registry Poisoning Detection',
75
+ severity: severity.toUpperCase(),
76
+ campaignName: 'Axios Registry Poisoning',
77
+ pkgName,
78
+ pkgVersion,
79
+ triggered: true,
80
+ severity,
81
+ indicators: triggered.map(id => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
82
+ ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/axios-poisoning/',
83
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
84
+ });
85
+
86
+ return [{
87
+ id: 'AXIOS_POISONING',
88
+ severity,
89
+ title: 'Axios Registry Poisoning campaign',
90
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
91
+ evidence: JSON.stringify(evidence),
92
+ mitigation: 'If decoy dependency detected: verify all axios dependencies are legitimate. If RAT payload detected: run full malware scan on the system, rotate all credentials, check for unauthorized network connections. Upgrade to axios@1.14.2+ or pin to a known safe version.',
93
+ }];
94
+ }
@@ -15,6 +15,9 @@ import { scan as miniShaiHuludScan } from './mini-shai-hulud/index.js';
15
15
  import { scan as badhostScan } from './cve-2026-48710-badhost/index.js';
16
16
  import { scan as trapdoorScan } from './trapdoor/index.js';
17
17
  import { scan as nodeIpcScan } from './node-ipc-compromise/index.js';
18
+ import { scan as mshSupplementScan } from './msh-supplement/index.js';
19
+ import { scan as typosquatScan } from './typosquat-vpmdhaj/index.js';
20
+ import { scan as axiosPoisoningScan } from './axios-poisoning/index.js';
18
21
 
19
22
  export async function runAll(pkgJson, files = [], registryMeta = null, allFiles = null) {
20
23
  const findings = [];
@@ -35,5 +38,8 @@ export async function runAll(pkgJson, files = [], registryMeta = null, allFiles
35
38
  findings.push(...await badhostScan(pkgJson, files, registryMeta, allFiles || files));
36
39
  findings.push(...await trapdoorScan(pkgJson, files, registryMeta, allFiles || files));
37
40
  findings.push(...await nodeIpcScan(pkgJson, files, registryMeta, allFiles || files));
41
+ findings.push(...await mshSupplementScan(pkgJson, files, registryMeta, allFiles || files));
42
+ findings.push(...await typosquatScan(pkgJson, files, registryMeta, allFiles || files));
43
+ findings.push(...await axiosPoisoningScan(pkgJson, files, registryMeta, allFiles || files));
38
44
  return findings.sort((a, b) => b.severity.localeCompare(a.severity));
39
45
  }
@@ -0,0 +1,18 @@
1
+ const CTF_SCRAMBLE_RE = /require\(['"](ctf-scramble-v2|ctf-scramble-v\d+)['"]\)/;
2
+ const CTF_SCRAMBLE_ESM_RE = /(?:from|import)\s+['"](ctf-scramble-v2|ctf-scramble-v\d+)['"]/;
3
+
4
+ export function scanCtfScramble(files = []) {
5
+ for (const file of files) {
6
+ const content = file.content || '';
7
+ if (CTF_SCRAMBLE_RE.test(content) || CTF_SCRAMBLE_ESM_RE.test(content)) {
8
+ const match = content.match(CTF_SCRAMBLE_RE) || content.match(CTF_SCRAMBLE_ESM_RE);
9
+ return {
10
+ triggered: true,
11
+ stopCondition: true,
12
+ filePath: file.path,
13
+ patternMatched: match ? match[1] : 'ctf-scramble-v2',
14
+ };
15
+ }
16
+ }
17
+ return { triggered: false, stopCondition: false };
18
+ }
@@ -0,0 +1,47 @@
1
+ const DAEMON_RE = /\b(daemon|fork)\s*\(/;
2
+ const SPAWN_DETACHED_RE = /spawn\s*\([^)]*detached\s*:\s*true/;
3
+ const SYSTEMD_RE = /\/etc\/systemd\/system\/|systemctl\s+(enable|start)/;
4
+ const CRON_RE = /crontab\s+-e|\/etc\/cron\b/;
5
+ const LAUNCHD_RE = /\/Library\/LaunchDaemons\/|launchctl\s+(load|start)/;
6
+ const TASK_SCHED_RE = /schtasks\.exe|New-ScheduledTask|Register-ScheduledJob/;
7
+ const CI_GUARD_RE = /!process\.env\.CI|process\.env\.CI\s*===?\s*undefined/;
8
+
9
+ export function scanPersistence(pkgJson, files = []) {
10
+ const allScripts = [];
11
+ const hooks = ['preinstall', 'install', 'postinstall', 'preuninstall', 'postuninstall'];
12
+ for (const hook of hooks) {
13
+ const script = pkgJson?.scripts?.[hook];
14
+ if (script) {
15
+ allScripts.push({ hook, content: script });
16
+ }
17
+ }
18
+
19
+ const code = files.map(f => f.content || '').join('\n');
20
+ const codeWithScripts = code + '\n' + allScripts.map(s => s.content).join('\n');
21
+
22
+ const detectedApis = [];
23
+ let hasCiGuard = false;
24
+ let hasDaemon = false;
25
+
26
+ if (DAEMON_RE.test(codeWithScripts)) detectedApis.push('daemon');
27
+ if (SPAWN_DETACHED_RE.test(codeWithScripts)) detectedApis.push('spawn_detached');
28
+ if (SYSTEMD_RE.test(codeWithScripts)) detectedApis.push('systemd');
29
+ if (CRON_RE.test(codeWithScripts)) detectedApis.push('cron');
30
+ if (LAUNCHD_RE.test(codeWithScripts)) detectedApis.push('launchd');
31
+ if (TASK_SCHED_RE.test(codeWithScripts)) detectedApis.push('task_scheduler');
32
+ if (CI_GUARD_RE.test(codeWithScripts)) hasCiGuard = true;
33
+
34
+ if (DAEMON_RE.test(codeWithScripts) || SPAWN_DETACHED_RE.test(codeWithScripts)) hasDaemon = true;
35
+
36
+ if (hasDaemon || detectedApis.length > 0) {
37
+ return {
38
+ triggered: true,
39
+ detectedApis,
40
+ hasCiGuard,
41
+ hooks: allScripts.map(s => s.hook),
42
+ context: hasCiGuard ? 'Spawns background process when CI env var absent' : 'Suspicious persistence/detached process detected',
43
+ };
44
+ }
45
+
46
+ return { triggered: false, detectedApis: [], hasCiGuard: false, hooks: [] };
47
+ }
@@ -0,0 +1,35 @@
1
+ const LOCALE_CHECKS = [
2
+ /process\.env\.LANG/,
3
+ /process\.env\.LC_ALL/,
4
+ /process\.env\.LC_MESSAGES/,
5
+ /Intl\.DateTimeFormat\(\)\.resolvedOptions\(\)\.timeZone/,
6
+ /Intl\.DateTimeFormat\.resolvedOptions\b/,
7
+ ];
8
+ const TARGET_LOCALES = /ru_RU|be_BY|uk_UA/;
9
+ const SILENT_EXIT_RE = /process\.exit\s*\(\s*0\s*\)/;
10
+
11
+ export function scanGeoKillswitch(files = []) {
12
+ const code = files.map(f => f.content || '').join('\n');
13
+ if (!code) return { triggered: false, targetedLocales: [], triggerBehavior: null };
14
+
15
+ const hasLocaleCheck = LOCALE_CHECKS.some(re => re.test(code));
16
+ if (!hasLocaleCheck) return { triggered: false, targetedLocales: [], triggerBehavior: null };
17
+
18
+ const hasTargetLocale = TARGET_LOCALES.test(code);
19
+ const hasSilentExit = SILENT_EXIT_RE.test(code);
20
+
21
+ if (hasTargetLocale || hasSilentExit) {
22
+ const matchedLocales = [];
23
+ if (/ru_RU/.test(code)) matchedLocales.push('ru_RU');
24
+ if (/be_BY/.test(code)) matchedLocales.push('be_BY');
25
+ if (/uk_UA/.test(code)) matchedLocales.push('uk_UA');
26
+
27
+ return {
28
+ triggered: true,
29
+ targetedLocales: matchedLocales.length > 0 ? matchedLocales : ['ru_RU', 'be_BY'],
30
+ triggerBehavior: hasSilentExit ? 'Silent exit' : 'Locale/timezone match with conditional behavior',
31
+ };
32
+ }
33
+
34
+ return { triggered: false, targetedLocales: [], triggerBehavior: null };
35
+ }
@@ -0,0 +1,33 @@
1
+ const OHNO_WHATS_GOING_ON_RE = /OhNoWhatsGoingOnWithGitHub/;
2
+ const GITHUB_COMMIT_SCRAPE_RE = /api\.github\.com\/repos\/[^/]+\/[^/]+\/commits/;
3
+ const GITHUB_GRAPHQL_RE = /api\.github\.com\/graphql/;
4
+ const GITHUB_TOKEN_ACCESS_RE = /process\.env\.(?:GH_TOKEN|GITHUB_TOKEN|GITHUB_ACTOR)/;
5
+ const COMMIT_PARSE_LOOP_RE = /commits?\s*\.\s*(?:map|filter|forEach|for\s*\(|while\s*\()/;
6
+
7
+ export function scanC2DeadDrop(files = []) {
8
+ const code = files.map(f => f.content || '').join('\n');
9
+ if (!code) return { triggered: false, matches: [] };
10
+
11
+ const matches = [];
12
+
13
+ if (OHNO_WHATS_GOING_ON_RE.test(code)) {
14
+ matches.push({ type: 'ioc_keyword', value: 'OhNoWhatsGoingOnWithGitHub', attackVector: 'GitHub commit scraping for token recovery' });
15
+ }
16
+
17
+ const hasTokenAccess = GITHUB_TOKEN_ACCESS_RE.test(code);
18
+ const hasGithubApi = GITHUB_COMMIT_SCRAPE_RE.test(code) || GITHUB_GRAPHQL_RE.test(code);
19
+ const hasCommitParseLoop = COMMIT_PARSE_LOOP_RE.test(code);
20
+
21
+ if (hasTokenAccess && hasGithubApi) {
22
+ matches.push({ type: 'token_exfil_github_api', value: 'Credential access followed by GitHub API call', attackVector: 'Credential/token extraction followed by GitHub API calls' });
23
+ }
24
+
25
+ if (hasCommitParseLoop && hasGithubApi) {
26
+ matches.push({ type: 'commit_scraping', value: 'Commit message parsing with GitHub API', attackVector: 'Commit scraping for secret detection' });
27
+ }
28
+
29
+ return {
30
+ triggered: matches.length > 0,
31
+ matches,
32
+ };
33
+ }
@@ -0,0 +1,107 @@
1
+ import { scanCtfScramble } from './d1-obfuscation.js';
2
+ import { scanPersistence } from './d2-persistence.js';
3
+ import { scanGeoKillswitch } from './d3-geo-killswitch.js';
4
+ import { scanC2DeadDrop } from './d4-c2-deaddrop.js';
5
+ import { attachProvenance } from '../../provenance.js';
6
+
7
+ const RULE_SEVERITY = {
8
+ D1: 'critical',
9
+ D2: 'critical',
10
+ D3: 'high',
11
+ D4: 'critical',
12
+ };
13
+
14
+ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
15
+
16
+ function highestSeverity(severities) {
17
+ for (const s of SEVERITY_ORDER) {
18
+ if (severities.includes(s)) return s;
19
+ }
20
+ return 'none';
21
+ }
22
+
23
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
24
+ const fileList = allFiles || files || [];
25
+ const pkgName = pkgJson?.name || 'unknown';
26
+ const pkgVersion = pkgJson?.version || '0.0.0';
27
+
28
+ const d1Results = scanCtfScramble(fileList);
29
+ if (d1Results.stopCondition) {
30
+ const evidence = attachProvenance({
31
+ rule: 'MSH-OBF-001',
32
+ campaign: 'MINI_SHAI_HULUD',
33
+ triggeredChecks: ['D1'],
34
+ filePath: d1Results.filePath,
35
+ patternMatched: d1Results.patternMatched,
36
+ action: 'BLOCK_IMMEDIATELY',
37
+ }, {
38
+ ruleId: 'MSH-OBF-001',
39
+ ruleName: 'ctf-scramble-v2 Obfuscation Detection',
40
+ severity: 'CRITICAL',
41
+ campaignName: 'Mini Shai-Hulud',
42
+ pkgName,
43
+ pkgVersion,
44
+ triggered: true,
45
+ severity: 'critical',
46
+ indicators: [{ type: 'obfuscation_found', value: d1Results.patternMatched }],
47
+ ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/msh-supplement/d1-obfuscation.js',
48
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
49
+ });
50
+
51
+ return [{
52
+ id: 'MINI_SHAI_HULUD',
53
+ severity: 'critical',
54
+ title: 'Mini Shai-Hulud worm campaign — ctf-scramble-v2 malware obfuscation detected',
55
+ description: 'HALT: ctf-scramble-v2 obfuscation layer detected. Package is compromised. Block install immediately.',
56
+ evidence: JSON.stringify(evidence),
57
+ mitigation: 'BLOCK IMMEDIATELY. Do not install this package version. Revoke any npm tokens exposed to this package. Rotate all CI/CD secrets. Run full malware scan on any system that processed this package.',
58
+ stopCondition: true,
59
+ }];
60
+ }
61
+
62
+ const results = {
63
+ D2: scanPersistence(pkgJson, fileList),
64
+ D3: scanGeoKillswitch(fileList),
65
+ D4: scanC2DeadDrop(fileList),
66
+ };
67
+
68
+ const triggered = Object.entries(results)
69
+ .filter(([_, r]) => r.triggered)
70
+ .map(([id]) => id);
71
+
72
+ if (triggered.length === 0) return [];
73
+
74
+ const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
75
+
76
+ const evidence = attachProvenance({
77
+ campaign: 'MINI_SHAI_HULUD',
78
+ triggeredChecks: triggered,
79
+ details: Object.fromEntries(
80
+ Object.entries(results).filter(([_, r]) => r.triggered)
81
+ ),
82
+ }, {
83
+ ruleId: 'MSH-SUPPLEMENT',
84
+ ruleName: 'Mini Shai-Hulud Supplement Detection',
85
+ severity: severity.toUpperCase(),
86
+ campaignName: 'Mini Shai-Hulud',
87
+ pkgName,
88
+ pkgVersion,
89
+ triggered: true,
90
+ severity,
91
+ indicators: triggered.map(id => ({
92
+ type: `rule_${id}`,
93
+ value: RULE_SEVERITY[id],
94
+ })),
95
+ ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/msh-supplement/',
96
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
97
+ });
98
+
99
+ return [{
100
+ id: 'MINI_SHAI_HULUD',
101
+ severity,
102
+ title: 'Mini Shai-Hulud worm campaign — supplement indicators',
103
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
104
+ evidence: JSON.stringify(evidence),
105
+ mitigation: 'If daemonization detected: revoke npm tokens and rotate CI/CD secrets. If geographic killswitch detected: verify running in expected region; attacker may be avoiding certain locales. If C2 dead-drop detected: check for unauthorized GitHub API access and token exfiltration. Review recent version publish history for anomalous bursts.',
106
+ }];
107
+ }
@@ -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
+ }