@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.
Files changed (125) hide show
  1. package/README.md +864 -861
  2. package/backend/cra.js +113 -21
  3. package/backend/db.js +18 -10
  4. package/backend/detectors/atk-001-lifecycle.js +5 -5
  5. package/backend/detectors/atk-002-obfusc.js +126 -47
  6. package/backend/detectors/atk-003-creds.js +8 -4
  7. package/backend/detectors/atk-004-persist.js +3 -3
  8. package/backend/detectors/atk-005-exfil.js +8 -4
  9. package/backend/detectors/atk-006-depconf.js +3 -3
  10. package/backend/detectors/atk-007-typosquat.js +64 -10
  11. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  12. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  13. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  14. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  15. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  16. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  17. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  18. package/backend/detectors/axios-poisoning/index.js +77 -60
  19. package/backend/detectors/config/thresholds.js +48 -3
  20. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  21. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  22. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  23. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  24. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  25. package/backend/detectors/hf-impersonation/index.js +94 -31
  26. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  27. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  28. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  29. package/backend/detectors/index.js +181 -34
  30. package/backend/detectors/lib/ast-patterns.js +4 -1
  31. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  32. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  33. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  34. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  35. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  36. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  37. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  38. package/backend/detectors/megalodon/index.js +35 -25
  39. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  40. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  41. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  42. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  43. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  44. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  45. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  46. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  47. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  48. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  49. package/backend/detectors/msh-supplement/index.js +78 -63
  50. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  51. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  52. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  53. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  54. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  55. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  56. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  57. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  58. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  59. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  60. package/backend/detectors/tier1-binary-embed.js +109 -41
  61. package/backend/detectors/tier1-cloud-imds.js +57 -37
  62. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  63. package/backend/detectors/tier1-infostealer.js +121 -68
  64. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  65. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  66. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  67. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  68. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  69. package/backend/detectors/tier1-self-propagation.js +115 -0
  70. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  71. package/backend/detectors/tier1-transitive-deps.js +182 -0
  72. package/backend/detectors/tier1-typosquat.js +129 -50
  73. package/backend/detectors/tier1-version-anomaly.js +77 -41
  74. package/backend/detectors/tier1-version-confusion.js +79 -59
  75. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  76. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  77. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  78. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  79. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  80. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  81. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  82. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  83. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  84. package/backend/detectors/trapdoor/index.js +19 -14
  85. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  86. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  87. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  88. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  89. package/backend/detectors.test.js +78 -19
  90. package/backend/fetch.js +37 -29
  91. package/backend/index.js +1 -1
  92. package/backend/license.js +20 -4
  93. package/backend/lockfile.js +60 -36
  94. package/backend/pdf.js +107 -28
  95. package/backend/policy.js +183 -56
  96. package/backend/provenance.js +28 -3
  97. package/backend/report.js +136 -70
  98. package/backend/sbom.js +33 -27
  99. package/backend/scripts/analyze-false-positives.js +14 -8
  100. package/backend/scripts/analyze-validation.js +27 -21
  101. package/backend/scripts/detect-false-positives.js +20 -10
  102. package/backend/scripts/fetch-top-packages.js +197 -49
  103. package/backend/scripts/validate-d10-d13.js +103 -0
  104. package/backend/scripts/validate-detectors.js +26 -17
  105. package/backend/siem/cef.js +23 -21
  106. package/backend/siem/ecs.js +3 -3
  107. package/backend/siem/index.js +1 -1
  108. package/backend/siem/qradar.js +3 -3
  109. package/backend/siem/sentinel.js +2 -2
  110. package/backend/tests-d5-enhanced.test.js +13 -12
  111. package/backend/tests-d6-version-anomaly.test.js +17 -8
  112. package/backend/tests-d6.test.js +24 -14
  113. package/backend/tests-d6c.test.js +27 -14
  114. package/backend/tests-d7-obfuscation.test.js +9 -12
  115. package/backend/tests.test.js +182 -83
  116. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  117. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  118. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  119. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  120. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  121. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  122. package/backend/vsix-scan/index.js +97 -41
  123. package/backend/vsix-scan/marketplace-client.js +29 -13
  124. package/cli/cli.js +154 -64
  125. package/package.json +12 -3
@@ -24,9 +24,10 @@ function resolveSeverity(signals, d4Evidence) {
24
24
  maxScore = Math.max(maxScore, SIGNAL_SEVERITY[s] || 0);
25
25
  }
26
26
 
27
- const d4Hint = d4Evidence.find(e => e._severityHint);
27
+ const d4Hint = d4Evidence.find((e) => e._severityHint);
28
28
  if (d4Hint) {
29
- const hintScore = d4Hint._severityHint === 'HIGH' ? 4 : d4Hint._severityHint === 'MEDIUM' ? 3 : 0;
29
+ const hintScore =
30
+ d4Hint._severityHint === 'HIGH' ? 4 : d4Hint._severityHint === 'MEDIUM' ? 3 : 0;
30
31
  maxScore = Math.max(maxScore, hintScore);
31
32
  }
32
33
 
@@ -45,36 +46,45 @@ export async function scanAll(pkgJson, allFiles = [], registryMeta = {}) {
45
46
  const d3Ev = await scanD3(registryMeta);
46
47
  allEvidence.push(...d3Ev);
47
48
 
48
- const velocityResult = d3Ev.length > 0 ? {
49
- triggered: true,
50
- windowStartISO: d3Ev[0]._windowStartISO || null,
51
- versionsInWindow: d3Ev[0].excerpt || '',
52
- _allVersions: d3Ev[0]._allVersions || [],
53
- } : { triggered: false, versionsInWindow: [], windowStartISO: null };
49
+ const velocityResult =
50
+ d3Ev.length > 0
51
+ ? {
52
+ triggered: true,
53
+ windowStartISO: d3Ev[0]._windowStartISO || null,
54
+ versionsInWindow: d3Ev[0].excerpt || '',
55
+ _allVersions: d3Ev[0]._allVersions || [],
56
+ }
57
+ : { triggered: false, versionsInWindow: [], windowStartISO: null };
54
58
 
55
59
  const d4Ev = await scanD4(registryMeta, velocityResult);
56
60
  allEvidence.push(...d4Ev);
57
61
 
58
- allEvidence.push(...await scanD5(registryMeta));
59
- allEvidence.push(...await scanD6(pkgJson, registryMeta));
62
+ allEvidence.push(...(await scanD5(registryMeta)));
63
+ allEvidence.push(...(await scanD6(pkgJson, registryMeta)));
60
64
 
61
- const signals = [...new Set(allEvidence.map(e => e.signal).filter(Boolean))];
65
+ const signals = [...new Set(allEvidence.map((e) => e.signal).filter(Boolean))];
62
66
 
63
- if (signals.length === 0) return [];
67
+ if (signals.length === 0) {
68
+ return [];
69
+ }
64
70
 
65
71
  const severity = resolveSeverity(signals, d4Ev);
66
72
 
67
- const cleaned = allEvidence.map(({ _windowStartISO, _allVersions, _severityHint, ...rest }) => rest);
68
-
69
- return [{
70
- id: 'MEGALODON',
71
- severity,
72
- title: 'Megalodon CI/CD attack campaign',
73
- description: `${signals.length} signal(s): ${signals.join(', ')}`,
74
- evidence: JSON.stringify({
75
- campaign: 'MEGALODON',
76
- signals,
77
- evidence: cleaned,
78
- }),
79
- }];
73
+ const cleaned = allEvidence.map(
74
+ ({ _windowStartISO, _allVersions, _severityHint, ...rest }) => rest
75
+ );
76
+
77
+ return [
78
+ {
79
+ id: 'MEGALODON',
80
+ severity,
81
+ title: 'Megalodon CI/CD attack campaign',
82
+ description: `${signals.length} signal(s): ${signals.join(', ')}`,
83
+ evidence: JSON.stringify({
84
+ campaign: 'MEGALODON',
85
+ signals,
86
+ evidence: cleaned,
87
+ }),
88
+ },
89
+ ];
80
90
  }
@@ -10,7 +10,9 @@ export async function checkBurstPublish(registryMeta, config = {}) {
10
10
  .filter(([, ts]) => !Number.isNaN(ts))
11
11
  .sort((a, b) => a[1] - b[1]);
12
12
 
13
- if (entries.length === 0) return { triggered: false };
13
+ if (entries.length === 0) {
14
+ return { triggered: false };
15
+ }
14
16
 
15
17
  const windowMs = windowMinutes * 60 * 1000;
16
18
 
@@ -12,7 +12,9 @@ function checkBurstOnTimeMap(timeMap, windowMinutes, threshold) {
12
12
  .filter(([, ts]) => !Number.isNaN(ts))
13
13
  .sort((a, b) => a[1] - b[1]);
14
14
 
15
- if (entries.length === 0) return null;
15
+ if (entries.length === 0) {
16
+ return null;
17
+ }
16
18
 
17
19
  const windowMs = windowMinutes * 60 * 1000;
18
20
 
@@ -55,17 +57,23 @@ export async function checkSiblingCompromise(pkgJson, config = {}) {
55
57
  for (const name of Object.keys(deps)) {
56
58
  if (name.startsWith('@')) {
57
59
  const scope = name.split('/')[0];
58
- if (!scopedDeps[scope]) scopedDeps[scope] = [];
60
+ if (!scopedDeps[scope]) {
61
+ scopedDeps[scope] = [];
62
+ }
59
63
  scopedDeps[scope].push(name);
60
64
  }
61
65
  }
62
66
 
63
- if (Object.keys(scopedDeps).length === 0) return { triggered: false };
67
+ if (Object.keys(scopedDeps).length === 0) {
68
+ return { triggered: false };
69
+ }
64
70
 
65
71
  const results = [];
66
72
 
67
73
  for (const [scope, packages] of Object.entries(scopedDeps)) {
68
- if (packages.length < 2) continue;
74
+ if (packages.length < 2) {
75
+ continue;
76
+ }
69
77
 
70
78
  const burstSiblings = [];
71
79
 
@@ -75,7 +83,9 @@ export async function checkSiblingCompromise(pkgJson, config = {}) {
75
83
  try {
76
84
  const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}`;
77
85
  const res = await fetch(url);
78
- if (!res.ok) continue;
86
+ if (!res.ok) {
87
+ continue;
88
+ }
79
89
  const data = await res.json();
80
90
  timeData = data.time || {};
81
91
  siblingCache.set(pkg, timeData);
@@ -91,19 +101,19 @@ export async function checkSiblingCompromise(pkgJson, config = {}) {
91
101
  }
92
102
 
93
103
  if (burstSiblings.length >= 2) {
94
- const windows = burstSiblings.map(s => ({
104
+ const windows = burstSiblings.map((s) => ({
95
105
  start: new Date(s.windowStart).getTime(),
96
106
  end: new Date(s.windowEnd).getTime(),
97
107
  }));
98
108
 
99
- const overlapStart = Math.max(...windows.map(w => w.start));
100
- const overlapEnd = Math.min(...windows.map(w => w.end));
109
+ const overlapStart = Math.max(...windows.map((w) => w.start));
110
+ const overlapEnd = Math.min(...windows.map((w) => w.end));
101
111
 
102
112
  if (overlapStart < overlapEnd) {
103
113
  results.push({
104
114
  triggered: true,
105
115
  scope,
106
- siblingPackages: burstSiblings.map(s => s.name),
116
+ siblingPackages: burstSiblings.map((s) => s.name),
107
117
  windowStart: new Date(overlapStart).toISOString(),
108
118
  windowEnd: new Date(overlapEnd).toISOString(),
109
119
  });
@@ -111,6 +121,8 @@ export async function checkSiblingCompromise(pkgJson, config = {}) {
111
121
  }
112
122
  }
113
123
 
114
- if (results.length === 0) return { triggered: false };
124
+ if (results.length === 0) {
125
+ return { triggered: false };
126
+ }
115
127
  return { triggered: true, results };
116
128
  }
@@ -1,24 +1,40 @@
1
- export async function checkSlsaMismatch(packageName, version, burstWindow, timeMap = {}, config = {}) {
2
- if (!burstWindow?.triggered) return { triggered: false };
1
+ export async function checkSlsaMismatch(
2
+ packageName,
3
+ version,
4
+ burstWindow,
5
+ timeMap = {},
6
+ _config = {}
7
+ ) {
8
+ if (!burstWindow?.triggered) {
9
+ return { triggered: false };
10
+ }
3
11
 
4
12
  const anomalies = [];
5
13
  const publishTime = timeMap?.[version];
6
- if (!publishTime) return { triggered: false };
14
+ if (!publishTime) {
15
+ return { triggered: false };
16
+ }
7
17
 
8
18
  try {
9
19
  const url = `https://registry.npmjs.org/-/npm/v1/attestations/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`;
10
20
  const res = await fetch(url);
11
- if (!res.ok) return { triggered: false };
21
+ if (!res.ok) {
22
+ return { triggered: false };
23
+ }
12
24
 
13
25
  const data = await res.json();
14
26
  const attestations = data?.attestations || [];
15
- if (attestations.length === 0) return { triggered: false };
27
+ if (attestations.length === 0) {
28
+ return { triggered: false };
29
+ }
16
30
 
17
31
  const publishMs = new Date(publishTime).getTime();
18
- if (Number.isNaN(publishMs)) return { triggered: false };
32
+ if (Number.isNaN(publishMs)) {
33
+ return { triggered: false };
34
+ }
19
35
 
20
36
  // Check if this is the first-ever attested version for this package
21
- const allVersions = Object.keys(timeMap).filter(v => v !== 'created' && v !== 'modified');
37
+ const allVersions = Object.keys(timeMap).filter((v) => v !== 'created' && v !== 'modified');
22
38
  const currentIdx = allVersions.indexOf(version);
23
39
  let prevHadAttestation = false;
24
40
 
@@ -49,7 +65,7 @@ export async function checkSlsaMismatch(packageName, version, burstWindow, timeM
49
65
  const ts = att?.timestamp;
50
66
  if (ts) {
51
67
  const attMs = new Date(ts).getTime();
52
- if (!Number.isNaN(attMs) && attMs >= publishMs && (attMs - publishMs) < 60000) {
68
+ if (!Number.isNaN(attMs) && attMs >= publishMs && attMs - publishMs < 60000) {
53
69
  const gapMs = attMs - publishMs;
54
70
  anomalies.push(`Sub-60s attestation gap for ${version}: ${gapMs}ms`);
55
71
  }
@@ -57,8 +73,12 @@ export async function checkSlsaMismatch(packageName, version, burstWindow, timeM
57
73
 
58
74
  const builderId = att?.predicate?.runDetails?.builder?.id;
59
75
  if (builderId) {
60
- const knownPrefixes = ['https://github.com/', 'https://gitlab.com/', 'https://circleci.com/'];
61
- const isKnown = knownPrefixes.some(p => builderId.startsWith(p));
76
+ const knownPrefixes = [
77
+ 'https://github.com/',
78
+ 'https://gitlab.com/',
79
+ 'https://circleci.com/',
80
+ ];
81
+ const isKnown = knownPrefixes.some((p) => builderId.startsWith(p));
62
82
  if (!isKnown) {
63
83
  anomalies.push(`Unrecognized builder ID for ${version}: ${builderId}`);
64
84
  }
@@ -1,4 +1,4 @@
1
- export async function checkMaintainerAnomaly(registryMeta, config = {}) {
1
+ export async function checkMaintainerAnomaly(registryMeta, _config = {}) {
2
2
  const versions = registryMeta?.versions || {};
3
3
  const timeMap = registryMeta?.time || {};
4
4
 
@@ -10,10 +10,12 @@ export async function checkMaintainerAnomaly(registryMeta, config = {}) {
10
10
  time: new Date(t).getTime(),
11
11
  user: versions[v]?._npmUser?.name,
12
12
  }))
13
- .filter(e => !Number.isNaN(e.time) && e.user)
13
+ .filter((e) => !Number.isNaN(e.time) && e.user)
14
14
  .sort((a, b) => a.time - b.time);
15
15
 
16
- if (sorted.length < 2) return { triggered: false };
16
+ if (sorted.length < 2) {
17
+ return { triggered: false };
18
+ }
17
19
 
18
20
  for (let i = 1; i < sorted.length; i++) {
19
21
  const prev = sorted[i - 1];
@@ -22,19 +24,21 @@ export async function checkMaintainerAnomaly(registryMeta, config = {}) {
22
24
  if (curr.user !== prev.user) {
23
25
  const gapMinutes = (curr.time - prev.time) / (1000 * 60);
24
26
  if (gapMinutes <= 10) {
25
- const newUserVersions = sorted.filter(e => e.user === curr.user);
27
+ const newUserVersions = sorted.filter((e) => e.user === curr.user);
26
28
  if (newUserVersions.length >= 2) {
27
29
  return {
28
30
  triggered: true,
29
- signals: [{
30
- type: 'PUBLISHER_DRIFT_RAPID',
31
- previousPublisher: prev.user,
32
- newPublisher: curr.user,
33
- gapMinutes,
34
- newUserVersionCount: newUserVersions.length,
35
- driftVersion: curr.version,
36
- driftWindowStart: new Date(curr.time).toISOString(),
37
- }],
31
+ signals: [
32
+ {
33
+ type: 'PUBLISHER_DRIFT_RAPID',
34
+ previousPublisher: prev.user,
35
+ newPublisher: curr.user,
36
+ gapMinutes,
37
+ newUserVersionCount: newUserVersions.length,
38
+ driftVersion: curr.version,
39
+ driftWindowStart: new Date(curr.time).toISOString(),
40
+ },
41
+ ],
38
42
  };
39
43
  }
40
44
  }
@@ -11,7 +11,9 @@ const __dirname = dirname(__filename);
11
11
  const IOC_PATH = join(__dirname, 'iocs.json');
12
12
 
13
13
  function loadIOCData() {
14
- if (iocsLoaded) return iocsData;
14
+ if (iocsLoaded) {
15
+ return iocsData;
16
+ }
15
17
  iocsLoaded = true;
16
18
  try {
17
19
  iocsData = JSON.parse(readFileSync(IOC_PATH, 'utf8'));
@@ -34,7 +36,9 @@ export function reloadIOCData() {
34
36
 
35
37
  export async function checkIOC(pkgName, pkgVersion, sha512, publisherAccount, timeMap = {}) {
36
38
  const data = loadIOCData();
37
- if (!data) return { triggered: false, matches: [] };
39
+ if (!data) {
40
+ return { triggered: false, matches: [] };
41
+ }
38
42
 
39
43
  const matches = [];
40
44
  const allIOCs = [];
@@ -44,7 +48,7 @@ export async function checkIOC(pkgName, pkgVersion, sha512, publisherAccount, ti
44
48
  for (const waveKey of Object.keys(data.waves || {})) {
45
49
  const wave = data.waves[waveKey];
46
50
  const waveNum = waveKey === 'wave1' ? 1 : waveKey === 'wave2' ? 2 : 3;
47
- for (const ioc of (wave.iocs || [])) {
51
+ for (const ioc of wave.iocs || []) {
48
52
  allIOCs.push({ ...ioc, wave: waveNum });
49
53
  }
50
54
  }
@@ -53,7 +57,11 @@ export async function checkIOC(pkgName, pkgVersion, sha512, publisherAccount, ti
53
57
  switch (ioc.type) {
54
58
  case 'packageName': {
55
59
  if (ioc.value === pkgName) {
56
- if (!ioc.maliciousVersions || ioc.maliciousVersions.length === 0 || ioc.maliciousVersions.includes(pkgVersion)) {
60
+ if (
61
+ !ioc.maliciousVersions ||
62
+ ioc.maliciousVersions.length === 0 ||
63
+ ioc.maliciousVersions.includes(pkgVersion)
64
+ ) {
57
65
  matches.push({ type: 'packageName', value: pkgName, wave: ioc.wave });
58
66
  }
59
67
  }
@@ -14,7 +14,9 @@ const SUSPICIOUS_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
14
14
  const MAX_SNIPPET_LENGTH = 200;
15
15
 
16
16
  function truncateSnippet(text) {
17
- if (text.length <= MAX_SNIPPET_LENGTH) return text;
17
+ if (text.length <= MAX_SNIPPET_LENGTH) {
18
+ return text;
19
+ }
18
20
  return text.slice(0, MAX_SNIPPET_LENGTH - 3) + '...';
19
21
  }
20
22
 
@@ -24,7 +26,9 @@ export function checkTokenExfil(allFiles, pkgJson) {
24
26
 
25
27
  for (const hook of SUSPICIOUS_SCRIPTS) {
26
28
  const scriptContent = scripts[hook];
27
- if (!scriptContent) continue;
29
+ if (!scriptContent) {
30
+ continue;
31
+ }
28
32
 
29
33
  for (const pattern of EXFIL_PATTERNS) {
30
34
  if (pattern.test(scriptContent)) {
@@ -1,5 +1,8 @@
1
1
  import { checkBurstPublish } from './d1-burst-publish.js';
2
- import { checkSiblingCompromise, clearSiblingCache } from './d2-sibling-compromise.js';
2
+ import {
3
+ checkSiblingCompromise,
4
+ clearSiblingCache as _clearSiblingCache,
5
+ } from './d2-sibling-compromise.js';
3
6
  import { checkSlsaMismatch } from './d3-slsa-mismatch.js';
4
7
  import { checkMaintainerAnomaly } from './d4-maintainer-anomaly.js';
5
8
  import { checkIOC } from './d5-ioc-check.js';
@@ -31,15 +34,31 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
31
34
  const nxDownstreamResult = checkNxConsoleDownstream(pkgJson, allFiles || files);
32
35
 
33
36
  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');
37
+ if (burstResult.triggered) {
38
+ triggeredChecks.push('D1_BURST');
39
+ }
40
+ if (siblingResult.triggered) {
41
+ triggeredChecks.push('D2_SIBLING');
42
+ }
43
+ if (slsaResult.triggered) {
44
+ triggeredChecks.push('D3_SLSA');
45
+ }
46
+ if (maintainerResult.triggered) {
47
+ triggeredChecks.push('D4_MAINTAINER');
48
+ }
49
+ if (iocResult.triggered) {
50
+ triggeredChecks.push('D5_IOC');
51
+ }
52
+ if (exfilResult.triggered) {
53
+ triggeredChecks.push('D6_EXFIL');
54
+ }
55
+ if (nxDownstreamResult.triggered) {
56
+ triggeredChecks.push('D7_NX_CONSOLE');
57
+ }
41
58
 
42
- if (triggeredChecks.length === 0) return [];
59
+ if (triggeredChecks.length === 0) {
60
+ return [];
61
+ }
43
62
 
44
63
  let waveAttribution = 'unknown';
45
64
  if (pkgName.startsWith('@tanstack')) {
@@ -49,9 +68,10 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
49
68
  } else if (nxDownstreamResult.triggered) {
50
69
  waveAttribution = 'wave3-nx-console';
51
70
  } else if (iocResult.matches && iocResult.matches.length > 0) {
52
- const waves = [...new Set(iocResult.matches.map(m => m.wave))];
71
+ const waves = [...new Set(iocResult.matches.map((m) => m.wave))];
53
72
  if (waves.length === 1) {
54
- waveAttribution = waves[0] === 1 ? 'wave1-tanstack' : waves[0] === 2 ? 'wave2-antv' : 'wave3-nx-console';
73
+ waveAttribution =
74
+ waves[0] === 1 ? 'wave1-tanstack' : waves[0] === 2 ? 'wave2-antv' : 'wave3-nx-console';
55
75
  }
56
76
  }
57
77
 
@@ -62,10 +82,14 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
62
82
  waveAttribution,
63
83
  triggeredChecks,
64
84
  burstWindow: burstResult.triggered
65
- ? { start: burstResult.windowStart, end: burstResult.windowEnd, versionCount: burstResult.versionCount }
85
+ ? {
86
+ start: burstResult.windowStart,
87
+ end: burstResult.windowEnd,
88
+ versionCount: burstResult.versionCount,
89
+ }
66
90
  : null,
67
91
  siblingPackages: siblingResult.triggered
68
- ? siblingResult.results.flatMap(r => r.siblingPackages)
92
+ ? siblingResult.results.flatMap((r) => r.siblingPackages)
69
93
  : null,
70
94
  attestationAnomalies: slsaResult.triggered ? slsaResult.anomalies : null,
71
95
  iocMatches: iocResult.triggered ? iocResult.matches : null,
@@ -75,25 +99,38 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
75
99
  : null,
76
100
  };
77
101
 
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
- }];
102
+ return [
103
+ {
104
+ id: 'MINI_SHAI_HULUD',
105
+ severity: isCritical ? 'critical' : 'high',
106
+ title: 'Mini Shai-Hulud worm campaign',
107
+ description: `${triggeredChecks.length} signal(s): ${triggeredChecks.join(', ')}`,
108
+ evidence: JSON.stringify(evidence),
109
+ mitigation:
110
+ '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.',
111
+ },
112
+ ];
86
113
  }
87
114
 
88
115
  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: [] };
116
+ const deps = {
117
+ ...pkgJson?.dependencies,
118
+ ...pkgJson?.devDependencies,
119
+ ...pkgJson?.peerDependencies,
120
+ };
121
+ const nxDeps = Object.keys(deps).filter((d) => d.startsWith('@nx/') || d.startsWith('nrwl/'));
122
+ if (nxDeps.length === 0) {
123
+ return { triggered: false, nxDeps: [], vsCodeExtensions: [] };
124
+ }
92
125
 
93
126
  let vsCodeExtensions = [];
94
127
  if (allFiles && Array.isArray(allFiles)) {
95
128
  for (const file of allFiles) {
96
- if (file.path && (file.path.endsWith('.vscode/extensions.json') || file.path.endsWith('.vscode/extensions.json'))) {
129
+ if (
130
+ file.path &&
131
+ (file.path.endsWith('.vscode/extensions.json') ||
132
+ file.path.endsWith('.vscode/extensions.json'))
133
+ ) {
97
134
  try {
98
135
  const content = typeof file.content === 'string' ? file.content : '';
99
136
  const parsed = JSON.parse(content);
@@ -101,7 +138,7 @@ function checkNxConsoleDownstream(pkgJson, allFiles) {
101
138
  ...(parsed.recommendations || []),
102
139
  ...(parsed.unwantedRecommendations || []),
103
140
  ];
104
- const matched = allExts.filter(e => e.includes('nrwl.angular-console'));
141
+ const matched = allExts.filter((e) => e.includes('nrwl.angular-console'));
105
142
  if (matched.length > 0) {
106
143
  vsCodeExtensions = matched;
107
144
  }
@@ -16,30 +16,48 @@ export function scanPersistence(pkgJson, files = []) {
16
16
  }
17
17
  }
18
18
 
19
- const code = files.map(f => f.content || '').join('\n');
20
- const codeWithScripts = code + '\n' + allScripts.map(s => s.content).join('\n');
19
+ const code = files.map((f) => f.content || '').join('\n');
20
+ const codeWithScripts = code + '\n' + allScripts.map((s) => s.content).join('\n');
21
21
 
22
22
  const detectedApis = [];
23
23
  let hasCiGuard = false;
24
24
  let hasDaemon = false;
25
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;
26
+ if (DAEMON_RE.test(codeWithScripts)) {
27
+ detectedApis.push('daemon');
28
+ }
29
+ if (SPAWN_DETACHED_RE.test(codeWithScripts)) {
30
+ detectedApis.push('spawn_detached');
31
+ }
32
+ if (SYSTEMD_RE.test(codeWithScripts)) {
33
+ detectedApis.push('systemd');
34
+ }
35
+ if (CRON_RE.test(codeWithScripts)) {
36
+ detectedApis.push('cron');
37
+ }
38
+ if (LAUNCHD_RE.test(codeWithScripts)) {
39
+ detectedApis.push('launchd');
40
+ }
41
+ if (TASK_SCHED_RE.test(codeWithScripts)) {
42
+ detectedApis.push('task_scheduler');
43
+ }
44
+ if (CI_GUARD_RE.test(codeWithScripts)) {
45
+ hasCiGuard = true;
46
+ }
33
47
 
34
- if (DAEMON_RE.test(codeWithScripts) || SPAWN_DETACHED_RE.test(codeWithScripts)) hasDaemon = true;
48
+ if (DAEMON_RE.test(codeWithScripts) || SPAWN_DETACHED_RE.test(codeWithScripts)) {
49
+ hasDaemon = true;
50
+ }
35
51
 
36
52
  if (hasDaemon || detectedApis.length > 0) {
37
53
  return {
38
54
  triggered: true,
39
55
  detectedApis,
40
56
  hasCiGuard,
41
- hooks: allScripts.map(s => s.hook),
42
- context: hasCiGuard ? 'Spawns background process when CI env var absent' : 'Suspicious persistence/detached process detected',
57
+ hooks: allScripts.map((s) => s.hook),
58
+ context: hasCiGuard
59
+ ? 'Spawns background process when CI env var absent'
60
+ : 'Suspicious persistence/detached process detected',
43
61
  };
44
62
  }
45
63
 
@@ -9,25 +9,37 @@ const TARGET_LOCALES = /ru_RU|be_BY|uk_UA/;
9
9
  const SILENT_EXIT_RE = /process\.exit\s*\(\s*0\s*\)/;
10
10
 
11
11
  export function scanGeoKillswitch(files = []) {
12
- const code = files.map(f => f.content || '').join('\n');
13
- if (!code) return { triggered: false, targetedLocales: [], triggerBehavior: null };
12
+ const code = files.map((f) => f.content || '').join('\n');
13
+ if (!code) {
14
+ return { triggered: false, targetedLocales: [], triggerBehavior: null };
15
+ }
14
16
 
15
- const hasLocaleCheck = LOCALE_CHECKS.some(re => re.test(code));
16
- if (!hasLocaleCheck) return { triggered: false, targetedLocales: [], triggerBehavior: null };
17
+ const hasLocaleCheck = LOCALE_CHECKS.some((re) => re.test(code));
18
+ if (!hasLocaleCheck) {
19
+ return { triggered: false, targetedLocales: [], triggerBehavior: null };
20
+ }
17
21
 
18
22
  const hasTargetLocale = TARGET_LOCALES.test(code);
19
23
  const hasSilentExit = SILENT_EXIT_RE.test(code);
20
24
 
21
25
  if (hasTargetLocale || hasSilentExit) {
22
26
  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');
27
+ if (/ru_RU/.test(code)) {
28
+ matchedLocales.push('ru_RU');
29
+ }
30
+ if (/be_BY/.test(code)) {
31
+ matchedLocales.push('be_BY');
32
+ }
33
+ if (/uk_UA/.test(code)) {
34
+ matchedLocales.push('uk_UA');
35
+ }
26
36
 
27
37
  return {
28
38
  triggered: true,
29
39
  targetedLocales: matchedLocales.length > 0 ? matchedLocales : ['ru_RU', 'be_BY'],
30
- triggerBehavior: hasSilentExit ? 'Silent exit' : 'Locale/timezone match with conditional behavior',
40
+ triggerBehavior: hasSilentExit
41
+ ? 'Silent exit'
42
+ : 'Locale/timezone match with conditional behavior',
31
43
  };
32
44
  }
33
45
 
@@ -5,13 +5,19 @@ const GITHUB_TOKEN_ACCESS_RE = /process\.env\.(?:GH_TOKEN|GITHUB_TOKEN|GITHUB_AC
5
5
  const COMMIT_PARSE_LOOP_RE = /commits?\s*\.\s*(?:map|filter|forEach|for\s*\(|while\s*\()/;
6
6
 
7
7
  export function scanC2DeadDrop(files = []) {
8
- const code = files.map(f => f.content || '').join('\n');
9
- if (!code) return { triggered: false, matches: [] };
8
+ const code = files.map((f) => f.content || '').join('\n');
9
+ if (!code) {
10
+ return { triggered: false, matches: [] };
11
+ }
10
12
 
11
13
  const matches = [];
12
14
 
13
15
  if (OHNO_WHATS_GOING_ON_RE.test(code)) {
14
- matches.push({ type: 'ioc_keyword', value: 'OhNoWhatsGoingOnWithGitHub', attackVector: 'GitHub commit scraping for token recovery' });
16
+ matches.push({
17
+ type: 'ioc_keyword',
18
+ value: 'OhNoWhatsGoingOnWithGitHub',
19
+ attackVector: 'GitHub commit scraping for token recovery',
20
+ });
15
21
  }
16
22
 
17
23
  const hasTokenAccess = GITHUB_TOKEN_ACCESS_RE.test(code);
@@ -19,11 +25,19 @@ export function scanC2DeadDrop(files = []) {
19
25
  const hasCommitParseLoop = COMMIT_PARSE_LOOP_RE.test(code);
20
26
 
21
27
  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' });
28
+ matches.push({
29
+ type: 'token_exfil_github_api',
30
+ value: 'Credential access followed by GitHub API call',
31
+ attackVector: 'Credential/token extraction followed by GitHub API calls',
32
+ });
23
33
  }
24
34
 
25
35
  if (hasCommitParseLoop && hasGithubApi) {
26
- matches.push({ type: 'commit_scraping', value: 'Commit message parsing with GitHub API', attackVector: 'Commit scraping for secret detection' });
36
+ matches.push({
37
+ type: 'commit_scraping',
38
+ value: 'Commit message parsing with GitHub API',
39
+ attackVector: 'Commit scraping for secret detection',
40
+ });
27
41
  }
28
42
 
29
43
  return {