@lateos/npm-scan 0.16.0 → 0.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +199 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -669
  6. package/README.fr.md +707 -668
  7. package/README.ja.md +704 -665
  8. package/README.md +826 -801
  9. package/README.zh.md +708 -669
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +24 -0
  26. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +24 -0
  27. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +90 -0
  28. package/backend/detectors/axios-poisoning/index.js +94 -0
  29. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  30. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  31. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  32. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  33. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  34. package/backend/detectors/hf-impersonation/index.js +396 -396
  35. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  36. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  37. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  38. package/backend/detectors/index.js +75 -38
  39. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  40. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  41. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  42. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  43. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  44. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  45. package/backend/detectors/megalodon/index.js +80 -80
  46. package/backend/detectors/megalodon/types.js +9 -9
  47. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  48. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  49. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  50. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  51. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  52. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  53. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  54. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  55. package/backend/detectors/msh-supplement/d1-obfuscation.js +18 -0
  56. package/backend/detectors/msh-supplement/d2-persistence.js +47 -0
  57. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +35 -0
  58. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +33 -0
  59. package/backend/detectors/msh-supplement/index.js +107 -0
  60. package/backend/detectors/tier1-binary-embed.js +219 -0
  61. package/backend/detectors/tier1-infostealer.js +280 -0
  62. package/backend/detectors/tier1-lifecycle-hook.js +176 -0
  63. package/backend/detectors/tier1-metadata-spoof.js +180 -0
  64. package/backend/detectors/tier1-typosquat.js +219 -0
  65. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +77 -0
  66. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +37 -0
  67. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +66 -0
  68. package/backend/detectors/typosquat-vpmdhaj/index.js +98 -0
  69. package/backend/fetch.js +175 -175
  70. package/backend/index.js +4 -4
  71. package/backend/license.js +89 -89
  72. package/backend/lockfile.js +379 -379
  73. package/backend/pdf.js +245 -245
  74. package/backend/policy.js +193 -176
  75. package/backend/provenance.js +79 -0
  76. package/backend/report.js +254 -254
  77. package/backend/sbom.js +66 -66
  78. package/backend/siem/cef.js +32 -32
  79. package/backend/siem/ecs.js +40 -40
  80. package/backend/siem/index.js +18 -18
  81. package/backend/siem/qradar.js +56 -56
  82. package/backend/siem/sentinel.js +27 -27
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  94. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  95. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  96. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  97. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  98. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  99. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  100. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  101. package/deploy/helm/npm-scan/values.yaml +102 -102
  102. package/package.json +57 -57
  103. package/scripts/download-corpus.js +30 -30
  104. package/scripts/gen-mal-corpus.js +34 -34
  105. package/scripts/generate-campaign-fixtures.js +170 -0
  106. package/src/config/top-5000.json +87 -0
  107. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  108. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  109. package/test/fixtures/lockfiles/yarn.lock +103 -103
  110. package/test/fixtures/mock-data.js +69 -69
@@ -1,79 +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
- }
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,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,219 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const BINARY_DIRS = ['bin/', 'native/'];
4
+ const BINARY_EXTS = ['.exe', '.dll', '.so', '.dylib', '.wasm', '.node', '.o', '.a'];
5
+ const BINARY_FILENAMES = ['bun', 'deno', 'go', 'rustc', 'python', 'python3', 'ruby', 'php'];
6
+
7
+ const CHILD_PROC_RE = /\b(?:spawn|exec|execSync|spawnSync|fork)\s*\(/g;
8
+ const FS_CHMOD_RE = /fs\.chmod\s*\(/g;
9
+
10
+ function detectMagicBytes(content) {
11
+ if (!content || content.length < 4) return null;
12
+
13
+ const c0 = content.charCodeAt(0);
14
+ const c1 = content.charCodeAt(1);
15
+ const c2 = content.charCodeAt(2);
16
+ const c3 = content.charCodeAt(3);
17
+
18
+ if (c0 === 0x7f && content.slice(1, 4) === 'ELF') return 'elf_embedded';
19
+ if (c0 === 0x4d && c1 === 0x5a) return 'pe_embedded';
20
+ if (c0 === 0x00 && content.slice(1, 4) === 'asm') return 'wasm_embedded';
21
+
22
+ const machO = (c0 === 0xfe && c1 === 0xed && c2 === 0xfa && (c3 === 0xce || c3 === 0xcf)) ||
23
+ (c0 === 0xce && c1 === 0xfa && c2 === 0xed && (c3 === 0xfe || c3 === 0xcf)) ||
24
+ (c0 === 0xcf && c1 === 0xfa && c2 === 0xed && c3 === 0xfe);
25
+ if (machO) return 'macho_embedded';
26
+
27
+ const universal = c0 === 0xca && c1 === 0xfe && c2 === 0xba && c3 === 0xbe;
28
+ if (universal) return 'macho_embedded';
29
+
30
+ return null;
31
+ }
32
+
33
+ function isInBinaryDir(filePath) {
34
+ const normalized = filePath.replace(/\\/g, '/');
35
+ return BINARY_DIRS.some(dir => normalized.includes(`/${dir}`) || normalized.startsWith(dir));
36
+ }
37
+
38
+ function hasBinaryExt(filePath) {
39
+ const lower = filePath.toLowerCase();
40
+ return BINARY_EXTS.some(ext => lower.endsWith(ext));
41
+ }
42
+
43
+ function isKnownBinaryName(fileName) {
44
+ const base = fileName.replace(/\.\w+$/, '').toLowerCase();
45
+ return BINARY_FILENAMES.includes(base);
46
+ }
47
+
48
+ function isDeclared(pkgJson, fileName) {
49
+ if (!pkgJson) return false;
50
+ const baseName = fileName.split(/[/\\]/).pop();
51
+
52
+ if (pkgJson.bin) {
53
+ if (typeof pkgJson.bin === 'string' && pkgJson.bin === baseName) return true;
54
+ if (typeof pkgJson.bin === 'object' && Object.values(pkgJson.bin).some(v => v === baseName || v.endsWith(`/${baseName}`))) return true;
55
+ }
56
+
57
+ if (pkgJson.optionalDependencies) {
58
+ for (const [name, val] of Object.entries(pkgJson.optionalDependencies)) {
59
+ if (name === baseName) return true;
60
+ }
61
+ }
62
+
63
+ if (pkgJson.gypfile === true || pkgJson.scripts?.install?.includes('node-gyp') || pkgJson.scripts?.install?.includes('node-pre-gyp')) {
64
+ if (baseName.endsWith('.node')) return true;
65
+ }
66
+
67
+ return false;
68
+ }
69
+
70
+ export const name = 'tier1-binary-embed';
71
+
72
+ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
73
+ const pkgName = pkgJson?.name;
74
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
75
+
76
+ if (!allFiles || allFiles.length === 0) return [];
77
+
78
+ if (pkgName && (
79
+ pkgName === 'electron' || pkgName === 'puppeteer' || pkgName === 'sharp' ||
80
+ pkgName === 'esbuild' || pkgName === 'node-gyp' || pkgName === 'node-pre-gyp' ||
81
+ pkgName === '@mapbox/node-pre-gyp'
82
+ )) return [];
83
+
84
+ const binaries = [];
85
+
86
+ for (const f of allFiles) {
87
+ const content = f.content || '';
88
+ const filePath = f.path || f.name || '';
89
+ const fileName = filePath.split(/[/\\]/).pop();
90
+ const fileSize = content.length;
91
+
92
+ const magic = detectMagicBytes(content);
93
+ const inBinDir = isInBinaryDir(filePath);
94
+ const hasExt = hasBinaryExt(filePath);
95
+ const knownName = isKnownBinaryName(fileName);
96
+ const largeFile = fileSize > 100000000;
97
+
98
+ if (magic || inBinDir || hasExt || knownName) {
99
+ const declared = isDeclared(pkgJson, filePath);
100
+
101
+ binaries.push({
102
+ file: filePath,
103
+ size: fileSize,
104
+ magic,
105
+ inBinDir,
106
+ hasExt,
107
+ knownName,
108
+ declared,
109
+ largeFile,
110
+ });
111
+ }
112
+ }
113
+
114
+ if (binaries.length === 0) return [];
115
+
116
+ const jsCode = (jsFiles || []).map(f => f.content || '').join('\n');
117
+ const invoked = CHILD_PROC_RE.test(jsCode) || FS_CHMOD_RE.test(jsCode);
118
+
119
+ const invokedFiles = [];
120
+ if (jsFiles && invoked) {
121
+ for (const f of jsFiles) {
122
+ const c = f.content || '';
123
+ CHILD_PROC_RE.lastIndex = 0;
124
+ FS_CHMOD_RE.lastIndex = 0;
125
+ if (CHILD_PROC_RE.test(c) || FS_CHMOD_RE.test(c)) {
126
+ invokedFiles.push(f.path || f.name || 'unknown.js');
127
+ }
128
+ }
129
+ }
130
+
131
+ const findings = [];
132
+
133
+ for (const bin of binaries) {
134
+ let baseScore;
135
+ let subtype;
136
+
137
+ if (bin.magic === 'elf_embedded') {
138
+ baseScore = 95;
139
+ subtype = 'elf_embedded';
140
+ } else if (bin.magic === 'pe_embedded') {
141
+ baseScore = 95;
142
+ subtype = 'pe_embedded';
143
+ } else if (bin.magic === 'macho_embedded') {
144
+ baseScore = 95;
145
+ subtype = 'macho_embedded';
146
+ } else if (bin.magic === 'wasm_embedded') {
147
+ baseScore = 60;
148
+ subtype = 'wasm_embedded';
149
+ } else {
150
+ baseScore = 60;
151
+ subtype = 'magic_byte_unknown';
152
+ }
153
+
154
+ let score = baseScore;
155
+
156
+ if (bin.inBinDir) score += 15;
157
+
158
+ if (!bin.declared) score += 50;
159
+
160
+ if (invoked && invokedFiles.length > 0) score += 25;
161
+
162
+ const confidenceScore = Math.max(50, Math.min(100, score));
163
+
164
+ function severityLabel(sc) {
165
+ if (sc >= 90) return 'critical';
166
+ if (sc >= 70) return 'high';
167
+ return 'medium';
168
+ }
169
+
170
+ function confidenceLabel(sc) {
171
+ if (sc >= 95) return 'CRITICAL';
172
+ if (sc >= 80) return 'HIGH';
173
+ if (sc >= 60) return 'MEDIUM';
174
+ return 'LOW';
175
+ }
176
+
177
+ const evidence = [
178
+ `binary: ${bin.file.split(/[/\\]/).pop()}${bin.magic ? ` (${bin.magic.toUpperCase().replace('_EMBEDDED', '')})` : ''}`,
179
+ `path: ${bin.file}`,
180
+ `declared: ${bin.declared}`,
181
+ ];
182
+ if (invoked && invokedFiles.length > 0) {
183
+ evidence.push(`invoked: child_process usage in ${invokedFiles.length} file(s)`);
184
+ evidence.push(`invoked_file: ${invokedFiles[0]}`);
185
+ }
186
+
187
+ const locations = [
188
+ { file: bin.file, size: bin.size },
189
+ ];
190
+
191
+ if (invokedFiles.length > 0) {
192
+ locations.push({ file: invokedFiles[0], line: 0 });
193
+ }
194
+
195
+ let message;
196
+ if (!bin.declared) {
197
+ message = `Undeclared binary detected: ${bin.file.split(/[/\\]/).pop()}`;
198
+ } else if (invoked) {
199
+ message = `Binary ${bin.file.split(/[/\\]/).pop()} invoked from JavaScript`;
200
+ } else {
201
+ message = `Binary embedded in package: ${bin.file.split(/[/\\]/).pop()}`;
202
+ }
203
+
204
+ findings.push({
205
+ detector: 'tier1-binary-embed',
206
+ id: 'TIER1-BINARY-EMBED',
207
+ severity: severityLabel(confidenceScore),
208
+ confidence: confidenceLabel(confidenceScore),
209
+ confidenceScore,
210
+ subtype,
211
+ message,
212
+ evidence,
213
+ locations,
214
+ reference: 'Campaign 2',
215
+ });
216
+ }
217
+
218
+ return findings;
219
+ }