@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
@@ -12,16 +12,18 @@ async function extractTarball(tarPath) {
12
12
  execSync(`tar xzf "${tarPath}" -C "${tmpDir}"`, { stdio: 'pipe' });
13
13
  const globPath = tmpDir.replace(/\\/g, '/') + '/**/package.json';
14
14
  const pkgPath = globSync(globPath, { nodir: true })[0];
15
- if (!pkgPath) throw new Error(`No package.json in ${tarPath}`);
15
+ if (!pkgPath) {
16
+ throw new Error(`No package.json in ${tarPath}`);
17
+ }
16
18
  const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf8'));
17
19
  const pkgDir = path.join(pkgPath, '..');
18
20
  const allGlobPath = pkgDir.replace(/\\/g, '/') + '/**/*';
19
- const allFiles = globSync(allGlobPath, { nodir: true }).map(p => ({
21
+ const allFiles = globSync(allGlobPath, { nodir: true }).map((p) => ({
20
22
  path: p,
21
23
  name: p,
22
24
  content: readFileSync(p, 'utf8'),
23
25
  }));
24
- const jsFiles = allFiles.filter(f => f.path.endsWith('.js'));
26
+ const jsFiles = allFiles.filter((f) => f.path.endsWith('.js'));
25
27
  return { pkgJson, jsFiles, allFiles, registryMeta: {} };
26
28
  }
27
29
 
@@ -32,8 +34,21 @@ const MOCK_SCANS = [
32
34
  package_name: 'lodash',
33
35
  version: '4.17.21',
34
36
  findings: [
35
- { id: 'ATK-003', atk_id: 'ATK-003', severity: 'high', title: 'Credential harvest', description: 'Scrapes env vars', evidence: 'process.env.NPM_TOKEN' },
36
- { id: 'ATK-009', severity: 'medium', title: 'Time trigger', description: 'Conditional trigger (time-based)', evidence: 'time-based trigger detected' },
37
+ {
38
+ id: 'ATK-003',
39
+ atk_id: 'ATK-003',
40
+ severity: 'high',
41
+ title: 'Credential harvest',
42
+ description: 'Scrapes env vars',
43
+ evidence: 'process.env.NPM_TOKEN',
44
+ },
45
+ {
46
+ id: 'ATK-009',
47
+ severity: 'medium',
48
+ title: 'Time trigger',
49
+ description: 'Conditional trigger (time-based)',
50
+ evidence: 'time-based trigger detected',
51
+ },
37
52
  ],
38
53
  },
39
54
  ];
@@ -67,8 +82,8 @@ test('SIEM ECS output format', async () => {
67
82
  assert.equal(e.observer.vendor, 'Lateos', 'ECS observer');
68
83
  assert(['high', 'medium'].includes(e.log.level), 'ECS log level');
69
84
  assert(e.vulnerability.enumeration === 'ATK', 'ECS enumeration');
70
- assert(e['@timestamp'], 'ECS timestamp');
71
- assert(['high', 'medium'].includes(e.log.level), 'ECS log level');
85
+ assert(e['@timestamp'], 'ECS timestamp');
86
+ assert(['high', 'medium'].includes(e.log.level), 'ECS log level');
72
87
  }
73
88
  });
74
89
 
@@ -100,12 +115,19 @@ test('SIEM QRadar output format', async () => {
100
115
  test('SIEM QRadar severity QID mapping', async () => {
101
116
  const { generateQRadar } = await import('./siem/qradar.js');
102
117
  const scans = [
103
- { package_name: 't', version: '1', findings: [
104
- { id: 'ATK-001', severity: 'critical', title: 'c' },
105
- { id: 'ATK-002', severity: 'low', title: 'l' },
106
- ]},
118
+ {
119
+ package_name: 't',
120
+ version: '1',
121
+ findings: [
122
+ { id: 'ATK-001', severity: 'critical', title: 'c' },
123
+ { id: 'ATK-002', severity: 'low', title: 'l' },
124
+ ],
125
+ },
107
126
  ];
108
- const out = generateQRadar(scans).split('\n').filter(Boolean).map(l => JSON.parse(l));
127
+ const out = generateQRadar(scans)
128
+ .split('\n')
129
+ .filter(Boolean)
130
+ .map((l) => JSON.parse(l));
109
131
  assert.equal(out[0].qid, 90050001, 'critical QID');
110
132
  assert.equal(out[1].qid, 90050004, 'low QID');
111
133
  });
@@ -159,7 +181,13 @@ test('SBOM CycloneDX output', async () => {
159
181
  const { generateSBOM } = await import('./sbom.js');
160
182
  const pkg = { name: 'test-pkg', version: '1.0.0' };
161
183
  const findings = [
162
- { id: 'ATK-001', atk_id: 'ATK-001', severity: 'high', title: 'Lifecycle script', description: 'preinstall hook' },
184
+ {
185
+ id: 'ATK-001',
186
+ atk_id: 'ATK-001',
187
+ severity: 'high',
188
+ title: 'Lifecycle script',
189
+ description: 'preinstall hook',
190
+ },
163
191
  ];
164
192
  const out = JSON.parse(generateSBOM(pkg, findings, 'json'));
165
193
  assert.equal(out.bomFormat, 'CycloneDX');
@@ -173,7 +201,13 @@ test('SBOM SPDX output', async () => {
173
201
  const { generateSBOM } = await import('./sbom.js');
174
202
  const pkg = { name: 'spdx-pkg', version: '2.0.0' };
175
203
  const findings = [
176
- { id: 'ATK-002', atk_id: 'ATK-002', severity: 'medium', title: 'Obfuscation', description: 'eval detected' },
204
+ {
205
+ id: 'ATK-002',
206
+ atk_id: 'ATK-002',
207
+ severity: 'medium',
208
+ title: 'Obfuscation',
209
+ description: 'eval detected',
210
+ },
177
211
  ];
178
212
  const out = JSON.parse(generateSBOM(pkg, findings, 'spdx'));
179
213
  assert.equal(out.spdxVersion, 'SPDX-2.3');
@@ -262,8 +296,11 @@ test('license isFeatureEnabled returns true for valid community scan', async ()
262
296
  const prev = process.env.NPM_SCAN_LICENSE_KEY;
263
297
  process.env.NPM_SCAN_LICENSE_KEY = '';
264
298
  const result = m.isFeatureEnabled('scan', '');
265
- if (prev) process.env.NPM_SCAN_LICENSE_KEY = prev;
266
- else delete process.env.NPM_SCAN_LICENSE_KEY;
299
+ if (prev) {
300
+ process.env.NPM_SCAN_LICENSE_KEY = prev;
301
+ } else {
302
+ delete process.env.NPM_SCAN_LICENSE_KEY;
303
+ }
267
304
  assert.equal(result, true);
268
305
  });
269
306
 
@@ -273,8 +310,11 @@ test('license validateLicense community features via isFeatureEnabled', async ()
273
310
  process.env.NPM_SCAN_LICENSE_KEY = '';
274
311
  assert.equal(m.isFeatureEnabled('scan', ''), true);
275
312
  assert.equal(m.isFeatureEnabled('nist-html', ''), true);
276
- if (prev) process.env.NPM_SCAN_LICENSE_KEY = prev;
277
- else delete process.env.NPM_SCAN_LICENSE_KEY;
313
+ if (prev) {
314
+ process.env.NPM_SCAN_LICENSE_KEY = prev;
315
+ } else {
316
+ delete process.env.NPM_SCAN_LICENSE_KEY;
317
+ }
278
318
  });
279
319
 
280
320
  test('license isFeatureEnabled returns false for missing premium key', async () => {
@@ -282,7 +322,9 @@ test('license isFeatureEnabled returns false for missing premium key', async ()
282
322
  const prev = process.env.NPM_SCAN_LICENSE_KEY;
283
323
  delete process.env.NPM_SCAN_LICENSE_KEY;
284
324
  assert.equal(m.isFeatureEnabled('siem', null), false);
285
- if (prev) process.env.NPM_SCAN_LICENSE_KEY = prev;
325
+ if (prev) {
326
+ process.env.NPM_SCAN_LICENSE_KEY = prev;
327
+ }
286
328
  });
287
329
 
288
330
  test('license reject tampered key', async () => {
@@ -487,14 +529,18 @@ test('report with no findings shows clean', async () => {
487
529
  test('NIST table maps all ATK-001 through ATK-011', async () => {
488
530
  const { generateHTML } = await import('./report.js');
489
531
  const allAtkScans = [
490
- { package_name: 'p', version: '1', findings: [
491
- ...Array.from({ length: 11 }, (_, i) => ({
492
- id: `ATK-${String(i + 1).padStart(3, '0')}`,
493
- atk_id: `ATK-${String(i + 1).padStart(3, '0')}`,
494
- severity: 'medium',
495
- title: `ATK-${i + 1}`,
496
- })),
497
- ]},
532
+ {
533
+ package_name: 'p',
534
+ version: '1',
535
+ findings: [
536
+ ...Array.from({ length: 11 }, (_, i) => ({
537
+ id: `ATK-${String(i + 1).padStart(3, '0')}`,
538
+ atk_id: `ATK-${String(i + 1).padStart(3, '0')}`,
539
+ severity: 'medium',
540
+ title: `ATK-${i + 1}`,
541
+ })),
542
+ ],
543
+ },
498
544
  ];
499
545
  const html = generateHTML(allAtkScans);
500
546
  for (let i = 1; i <= 11; i++) {
@@ -537,7 +583,12 @@ test('policy loadPolicy loads JSON', async () => {
537
583
 
538
584
  test('policy isAllowed matches package name', async () => {
539
585
  const { isAllowed } = await import('./policy.js');
540
- const policy = { allow: { packages: ['lodash', 'chalk@5.0.0'] }, severity_overrides: {}, fail_on: 'none', suppress: [] };
586
+ const policy = {
587
+ allow: { packages: ['lodash', 'chalk@5.0.0'] },
588
+ severity_overrides: {},
589
+ fail_on: 'none',
590
+ suppress: [],
591
+ };
541
592
  assert.equal(isAllowed('lodash', policy), true);
542
593
  assert.equal(isAllowed('lodash@4.17.21', policy), true);
543
594
  assert.equal(isAllowed('chalk@5.0.0', policy), true);
@@ -556,7 +607,12 @@ test('policy applyPolicy suppresses findings by atk_id', async () => {
556
607
  { id: 'ATK-003', atk_id: 'ATK-003', severity: 'high', title: 'Creds' },
557
608
  { id: 'ATK-009', atk_id: 'ATK-009', severity: 'medium', title: 'Trigger' },
558
609
  ];
559
- const policy = { allow: { packages: [] }, severity_overrides: {}, fail_on: 'none', suppress: [{ atk_id: 'ATK-003', package: '*', reason: 'FP' }] };
610
+ const policy = {
611
+ allow: { packages: [] },
612
+ severity_overrides: {},
613
+ fail_on: 'none',
614
+ suppress: [{ atk_id: 'ATK-003', package: '*', reason: 'FP' }],
615
+ };
560
616
  const { findings: filtered } = applyPolicy(findings, 'lodash', policy);
561
617
  assert.equal(filtered.length, 1);
562
618
  assert.equal(filtered[0].id, 'ATK-009');
@@ -564,30 +620,39 @@ test('policy applyPolicy suppresses findings by atk_id', async () => {
564
620
 
565
621
  test('policy applyPolicy suppresses findings by package name', async () => {
566
622
  const { applyPolicy } = await import('./policy.js');
567
- const findings = [
568
- { id: 'ATK-001', atk_id: 'ATK-001', severity: 'low', title: 'Lifecycle' },
569
- ];
570
- const policy = { allow: { packages: [] }, severity_overrides: {}, fail_on: 'none', suppress: [{ atk_id: 'ATK-001', package: 'lodash', reason: 'Fixture' }] };
623
+ const findings = [{ id: 'ATK-001', atk_id: 'ATK-001', severity: 'low', title: 'Lifecycle' }];
624
+ const policy = {
625
+ allow: { packages: [] },
626
+ severity_overrides: {},
627
+ fail_on: 'none',
628
+ suppress: [{ atk_id: 'ATK-001', package: 'lodash', reason: 'Fixture' }],
629
+ };
571
630
  const { findings: filtered } = applyPolicy(findings, 'lodash', policy);
572
631
  assert.equal(filtered.length, 0);
573
632
  });
574
633
 
575
634
  test('policy applyPolicy preserves findings when suppress rule targets different package', async () => {
576
635
  const { applyPolicy } = await import('./policy.js');
577
- const findings = [
578
- { id: 'ATK-001', atk_id: 'ATK-001', severity: 'low', title: 'Lifecycle' },
579
- ];
580
- const policy = { allow: { packages: [] }, severity_overrides: {}, fail_on: 'none', suppress: [{ atk_id: 'ATK-001', package: 'express', reason: 'Fixture' }] };
636
+ const findings = [{ id: 'ATK-001', atk_id: 'ATK-001', severity: 'low', title: 'Lifecycle' }];
637
+ const policy = {
638
+ allow: { packages: [] },
639
+ severity_overrides: {},
640
+ fail_on: 'none',
641
+ suppress: [{ atk_id: 'ATK-001', package: 'express', reason: 'Fixture' }],
642
+ };
581
643
  const { findings: filtered } = applyPolicy(findings, 'lodash', policy);
582
644
  assert.equal(filtered.length, 1);
583
645
  });
584
646
 
585
647
  test('policy applyPolicy overrides severity', async () => {
586
648
  const { applyPolicy } = await import('./policy.js');
587
- const findings = [
588
- { id: 'ATK-003', atk_id: 'ATK-003', severity: 'high', title: 'Creds' },
589
- ];
590
- const policy = { allow: { packages: [] }, severity_overrides: { 'ATK-003': 'low' }, fail_on: 'none', suppress: [] };
649
+ const findings = [{ id: 'ATK-003', atk_id: 'ATK-003', severity: 'high', title: 'Creds' }];
650
+ const policy = {
651
+ allow: { packages: [] },
652
+ severity_overrides: { 'ATK-003': 'low' },
653
+ fail_on: 'none',
654
+ suppress: [],
655
+ };
591
656
  const { findings: filtered } = applyPolicy(findings, 'lodash', policy);
592
657
  assert.equal(filtered[0].severity, 'low');
593
658
  assert.equal(filtered[0]._severityOverridden, true);
@@ -595,22 +660,23 @@ test('policy applyPolicy overrides severity', async () => {
595
660
 
596
661
  test('policy checkFailOn blocks at threshold', async () => {
597
662
  const { applyPolicy } = await import('./policy.js');
598
- const findings = [
599
- { id: 'ATK-001', atk_id: 'ATK-001', severity: 'medium', title: 'Test' },
600
- ];
663
+ const findings = [{ id: 'ATK-001', atk_id: 'ATK-001', severity: 'medium', title: 'Test' }];
601
664
  const policy = { allow: { packages: [] }, severity_overrides: {}, fail_on: 'high', suppress: [] };
602
665
  const { blocked } = applyPolicy(findings, 'test', policy);
603
666
  assert.equal(blocked, false);
604
- const policyLow = { allow: { packages: [] }, severity_overrides: {}, fail_on: 'medium', suppress: [] };
667
+ const policyLow = {
668
+ allow: { packages: [] },
669
+ severity_overrides: {},
670
+ fail_on: 'medium',
671
+ suppress: [],
672
+ };
605
673
  const { blocked: b2 } = applyPolicy(findings, 'test', policyLow);
606
674
  assert.equal(b2, true);
607
675
  });
608
676
 
609
677
  test('policy checkFailOn none never blocks', async () => {
610
678
  const { applyPolicy } = await import('./policy.js');
611
- const findings = [
612
- { id: 'ATK-001', atk_id: 'ATK-001', severity: 'critical', title: 'Critical' },
613
- ];
679
+ const findings = [{ id: 'ATK-001', atk_id: 'ATK-001', severity: 'critical', title: 'Critical' }];
614
680
  const policy = { allow: { packages: [] }, severity_overrides: {}, fail_on: 'none', suppress: [] };
615
681
  const { blocked } = applyPolicy(findings, 'test', policy);
616
682
  assert.equal(blocked, false);
@@ -696,14 +762,18 @@ test('text report clean package', async () => {
696
762
 
697
763
  test('text report severity counts', async () => {
698
764
  const { generateText } = await import('./report.js');
699
- const scans = [{
700
- package_name: 'multi-sev', version: '1.0.0', findings: [
701
- { id: 'ATK-001', severity: 'critical', title: 'C' },
702
- { id: 'ATK-002', severity: 'high', title: 'H' },
703
- { id: 'ATK-003', severity: 'medium', title: 'M' },
704
- { id: 'ATK-004', severity: 'low', title: 'L' },
705
- ],
706
- }];
765
+ const scans = [
766
+ {
767
+ package_name: 'multi-sev',
768
+ version: '1.0.0',
769
+ findings: [
770
+ { id: 'ATK-001', severity: 'critical', title: 'C' },
771
+ { id: 'ATK-002', severity: 'high', title: 'H' },
772
+ { id: 'ATK-003', severity: 'medium', title: 'M' },
773
+ { id: 'ATK-004', severity: 'low', title: 'L' },
774
+ ],
775
+ },
776
+ ];
707
777
  const out = generateText(scans);
708
778
  assert(out.includes('critical: 1'), 'critical count');
709
779
  assert(out.includes('high: 1'), 'high count');
@@ -751,19 +821,28 @@ test('PDF report with no findings still valid', async () => {
751
821
  describe('Tier 1 Detectors: Campaign Detection', () => {
752
822
  test('Campaign 1: 95%+ of 33 dependency confusion packages detected', async () => {
753
823
  const { runAll } = await import('./detectors/index.js');
754
- const campaignTarballs = fs.readdirSync('tests/corpus/malicious/')
755
- .filter(f => f.startsWith('campaign-1-'))
824
+ const campaignTarballs = fs
825
+ .readdirSync('tests/corpus/malicious/')
826
+ .filter((f) => f.startsWith('campaign-1-'))
756
827
  .slice(0, 33);
757
828
 
758
829
  let detected = 0;
759
830
  for (const tarball of campaignTarballs) {
760
- const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(`tests/corpus/malicious/${tarball}`);
831
+ const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(
832
+ `tests/corpus/malicious/${tarball}`
833
+ );
761
834
  const result = await runAll(pkgJson, jsFiles, registryMeta, allFiles);
762
835
 
763
- const lifecycleHookFinding = result.find(f => f.detector === 'tier1-lifecycle-hook' && f.confidenceScore >= 80);
764
- const metadataSpoofFinding = result.find(f => f.detector === 'tier1-metadata-spoof' && f.confidenceScore >= 70);
836
+ const lifecycleHookFinding = result.find(
837
+ (f) => f.detector === 'tier1-lifecycle-hook' && f.confidenceScore >= 80
838
+ );
839
+ const metadataSpoofFinding = result.find(
840
+ (f) => f.detector === 'tier1-metadata-spoof' && f.confidenceScore >= 70
841
+ );
765
842
 
766
- if (lifecycleHookFinding || metadataSpoofFinding) detected++;
843
+ if (lifecycleHookFinding || metadataSpoofFinding) {
844
+ detected++;
845
+ }
767
846
  }
768
847
 
769
848
  assert(detected / campaignTarballs.length >= 0.95);
@@ -771,20 +850,31 @@ describe('Tier 1 Detectors: Campaign Detection', () => {
771
850
 
772
851
  test('Campaign 2: 85%+ of 14 typosquatting packages detected', async () => {
773
852
  const { runAll } = await import('./detectors/index.js');
774
- const campaignTarballs = fs.readdirSync('tests/corpus/malicious/')
775
- .filter(f => f.startsWith('campaign-2-'))
853
+ const campaignTarballs = fs
854
+ .readdirSync('tests/corpus/malicious/')
855
+ .filter((f) => f.startsWith('campaign-2-'))
776
856
  .slice(0, 14);
777
857
 
778
858
  let detected = 0;
779
859
  for (const tarball of campaignTarballs) {
780
- const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(`tests/corpus/malicious/${tarball}`);
860
+ const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(
861
+ `tests/corpus/malicious/${tarball}`
862
+ );
781
863
  const result = await runAll(pkgJson, jsFiles, registryMeta, allFiles);
782
864
 
783
- const typosquatFinding = result.find(f => f.detector === 'tier1-typosquat' && f.confidenceScore >= 70);
784
- const infostealerFinding = result.find(f => f.detector === 'tier1-infostealer' && f.confidenceScore >= 75);
785
- const binaryFinding = result.find(f => f.detector === 'tier1-binary-embed' && f.confidenceScore >= 70);
865
+ const typosquatFinding = result.find(
866
+ (f) => f.detector === 'tier1-typosquat' && f.confidenceScore >= 70
867
+ );
868
+ const infostealerFinding = result.find(
869
+ (f) => f.detector === 'tier1-infostealer' && f.confidenceScore >= 75
870
+ );
871
+ const binaryFinding = result.find(
872
+ (f) => f.detector === 'tier1-binary-embed' && f.confidenceScore >= 70
873
+ );
786
874
 
787
- if (typosquatFinding || infostealerFinding || binaryFinding) detected++;
875
+ if (typosquatFinding || infostealerFinding || binaryFinding) {
876
+ detected++;
877
+ }
788
878
  }
789
879
 
790
880
  assert(detected / campaignTarballs.length >= 0.85);
@@ -796,7 +886,7 @@ describe('Tier 1 Detectors: Campaign Detection', () => {
796
886
  const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(tarball);
797
887
  const result = await runAll(pkgJson, jsFiles, registryMeta, allFiles);
798
888
 
799
- const infostealerFinding = result.find(f => f.detector === 'tier1-infostealer');
889
+ const infostealerFinding = result.find((f) => f.detector === 'tier1-infostealer');
800
890
  assert(infostealerFinding !== undefined);
801
891
  assert(infostealerFinding.confidenceScore >= 95);
802
892
  });
@@ -807,21 +897,28 @@ describe('Tier 1 Detectors: Campaign Detection', () => {
807
897
  describe('Tier 1 Detectors: False Positive Regression', () => {
808
898
  test('FP rate <5% on clean corpus (high/critical only)', async () => {
809
899
  const { runAll } = await import('./detectors/index.js');
810
- const cleanTarballs = fs.readdirSync('tests/corpus/clean/')
811
- .filter(f => f.endsWith('.tgz'))
900
+ const cleanTarballs = fs
901
+ .readdirSync('tests/corpus/clean/')
902
+ .filter((f) => f.endsWith('.tgz'))
812
903
  .slice(0, 1000);
813
904
 
814
905
  let fpCount = 0;
815
906
  for (const tarball of cleanTarballs) {
816
- const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(`tests/corpus/clean/${tarball}`);
907
+ const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(
908
+ `tests/corpus/clean/${tarball}`
909
+ );
817
910
  const result = await runAll(pkgJson, jsFiles, registryMeta, allFiles);
818
911
 
819
- const tier1HighSeverity = result.filter(f =>
820
- f.detector && f.detector.startsWith('tier1-') &&
821
- (f.severity === 'high' || f.severity === 'critical')
912
+ const tier1HighSeverity = result.filter(
913
+ (f) =>
914
+ f.detector &&
915
+ f.detector.startsWith('tier1-') &&
916
+ (f.severity === 'high' || f.severity === 'critical')
822
917
  );
823
918
 
824
- if (tier1HighSeverity.length > 0) fpCount++;
919
+ if (tier1HighSeverity.length > 0) {
920
+ fpCount++;
921
+ }
825
922
  }
826
923
 
827
924
  const fpRate = fpCount / (cleanTarballs.length || 1);
@@ -837,9 +934,11 @@ describe('Tier 1 Detectors: False Positive Regression', () => {
837
934
  const { pkgJson, jsFiles, allFiles, registryMeta } = await extractTarball(tarball);
838
935
  const result = await runAll(pkgJson, jsFiles, registryMeta, allFiles);
839
936
 
840
- const tier1HighSeverity = result.filter(f =>
841
- f.detector && f.detector.startsWith('tier1-') &&
842
- (f.severity === 'high' || f.severity === 'critical')
937
+ const tier1HighSeverity = result.filter(
938
+ (f) =>
939
+ f.detector &&
940
+ f.detector.startsWith('tier1-') &&
941
+ (f.severity === 'high' || f.severity === 'critical')
843
942
  );
844
943
 
845
944
  assert.equal(tier1HighSeverity.length, 0);
@@ -860,7 +959,7 @@ describe('Tier 1 Detectors: Confidence Scoring', () => {
860
959
  const registryMeta = { age: 5, weeklyDownloads: 100 };
861
960
 
862
961
  const result = await runAll(pkgJson, jsFiles, registryMeta, []);
863
- const typosquatFinding = result.find(f => f.detector === 'tier1-typosquat');
962
+ const typosquatFinding = result.find((f) => f.detector === 'tier1-typosquat');
864
963
 
865
964
  assert(typosquatFinding !== undefined);
866
965
  assert(typosquatFinding.confidenceScore >= 90);
@@ -876,7 +975,7 @@ describe('Tier 1 Detectors: Confidence Scoring', () => {
876
975
  const pkgJson = {};
877
976
 
878
977
  const result = await runAll(pkgJson, jsFiles, {}, jsFiles);
879
- const infostealerFinding = result.find(f => f.detector === 'tier1-infostealer');
978
+ const infostealerFinding = result.find((f) => f.detector === 'tier1-infostealer');
880
979
 
881
980
  assert(infostealerFinding !== undefined);
882
981
  assert(infostealerFinding.confidenceScore >= 80);
@@ -891,7 +990,7 @@ describe('Tier 1 Detectors: Confidence Scoring', () => {
891
990
  }));
892
991
 
893
992
  const result = await runAll({}, jsFiles, jsFiles, []);
894
- const lifecycleHookFindings = result.filter(f => f.detector === 'tier1-lifecycle-hook');
993
+ const lifecycleHookFindings = result.filter((f) => f.detector === 'tier1-lifecycle-hook');
895
994
 
896
995
  assert.equal(lifecycleHookFindings.length, 0);
897
996
  });
@@ -1,17 +1,26 @@
1
1
  const ACTIVATION_RISK_MATRIX = {
2
2
  '*': { base: 'critical', label: 'Wildcard (all files)' },
3
- 'onStartupFinished': { base: 'high', label: 'Startup finished' },
3
+ onStartupFinished: { base: 'high', label: 'Startup finished' },
4
4
  'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
5
- 'workspaceContains': { base: 'high', label: 'Workspace contains' },
5
+ workspaceContains: { base: 'high', label: 'Workspace contains' },
6
6
  'onCommand:*': { base: 'low', label: 'Any command' },
7
7
  };
8
8
 
9
9
  const DEFAULT_BASE_RISK = 'medium';
10
10
 
11
11
  const ESCALATION_KEYWORDS = [
12
- 'npx', 'bun', 'curl', 'wget', 'fetch(',
13
- 'exec(', 'spawn(', 'execSync', 'spawnSync',
14
- 'child_process', 'shell: true', 'detached: true',
12
+ 'npx',
13
+ 'bun',
14
+ 'curl',
15
+ 'wget',
16
+ 'fetch(',
17
+ 'exec(',
18
+ 'spawn(',
19
+ 'execSync',
20
+ 'spawnSync',
21
+ 'child_process',
22
+ 'shell: true',
23
+ 'detached: true',
15
24
  ];
16
25
 
17
26
  const BUNDLED_BUN_PATTERN = /bun|runtime/;
@@ -20,7 +29,11 @@ const SIZE_DELTA_THRESHOLD = 400 * 1024;
20
29
 
21
30
  const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
22
31
 
23
- export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
32
+ export async function checkActivationEventRisk(
33
+ extensionManifest,
34
+ versionHistory = [],
35
+ priorVersions = []
36
+ ) {
24
37
  const signals = [];
25
38
 
26
39
  const activationEvents = extensionManifest?.activationEvents || [];
@@ -32,7 +45,7 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
32
45
  const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
33
46
  const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
34
47
 
35
- let worstEvent = null;
48
+ let _worstEvent = null;
36
49
  const why = [];
37
50
 
38
51
  for (const event of activationEvents) {
@@ -41,29 +54,31 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
41
54
  const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
42
55
  if (baseIdx > maxBaseRisk) {
43
56
  maxBaseRisk = baseIdx;
44
- worstEvent = event;
57
+ _worstEvent = event;
45
58
  }
46
59
  } else if (event.includes('*') && event !== 'onCommand:*') {
47
60
  const baseIdx = riskValues['high'];
48
61
  if (baseIdx > maxBaseRisk) {
49
62
  maxBaseRisk = baseIdx;
50
- worstEvent = event;
63
+ _worstEvent = event;
51
64
  }
52
65
  }
53
66
  }
54
67
 
55
68
  const contributes = extensionManifest?.contributes || {};
56
69
  const commands = contributes?.commands || [];
57
- const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
70
+ const cmdTitles = commands.map((c) => (c.title || '').toLowerCase()).join(' ');
58
71
 
59
72
  const bundledDeps = extensionManifest?.bundledDependencies || [];
60
73
  const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
61
74
 
62
- const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
75
+ const hasShellKeyword = SHELL_CMDS.some((cmd) => cmdTitles.includes(cmd));
63
76
  const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
64
77
 
65
78
  const activationEventsStr = activationEvents.join(' ');
66
- const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
79
+ const hasShellInActivationContext = ESCALATION_KEYWORDS.some((kw) =>
80
+ activationEventsStr.toLowerCase().includes(kw.toLowerCase())
81
+ );
67
82
 
68
83
  let escalateToCritical = false;
69
84
 
@@ -74,22 +89,22 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
74
89
 
75
90
  if (versionHistory.length >= 2) {
76
91
  const sizes = versionHistory
77
- .filter(v => v.assetSize)
78
- .map(v => v.assetSize)
92
+ .filter((v) => v.assetSize)
93
+ .map((v) => v.assetSize)
79
94
  .sort((a, b) => b - a);
80
95
 
81
- if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
96
+ if (sizes.length >= 2 && sizes[0] - sizes[sizes.length - 1] > SIZE_DELTA_THRESHOLD) {
82
97
  escalateToCritical = true;
83
98
  why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
84
99
  }
85
100
  }
86
101
 
87
102
  const priorActivationEvents = priorVersions
88
- .filter(v => v.activationEvents)
89
- .flatMap(v => v.activationEvents);
103
+ .filter((v) => v.activationEvents)
104
+ .flatMap((v) => v.activationEvents);
90
105
 
91
106
  if (priorActivationEvents.length > 0) {
92
- const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
107
+ const newEvents = activationEvents.filter((e) => !priorActivationEvents.includes(e));
93
108
  if (newEvents.length > 0) {
94
109
  why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
95
110
  if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
@@ -103,7 +118,9 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
103
118
  riskLevel = 'critical';
104
119
  }
105
120
 
106
- if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
121
+ if (!riskLevel) {
122
+ return { triggered: false, signals: [], riskLevel: null, why: [] };
123
+ }
107
124
 
108
125
  signals.push({
109
126
  type: 'ACTIVATION_EVENT_RISK',
@@ -4,12 +4,14 @@ export async function checkBurstPublish(versionHistory, config = {}) {
4
4
  const hotPullMinutes = config.hotPullMinutes ?? 20;
5
5
 
6
6
  const entries = versionHistory
7
- .filter(v => v.publishedAt)
8
- .map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
- .filter(e => !Number.isNaN(e.time))
7
+ .filter((v) => v.publishedAt)
8
+ .map((v) => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
+ .filter((e) => !Number.isNaN(e.time))
10
10
  .sort((a, b) => a.time - b.time);
11
11
 
12
- if (entries.length < threshold) return { triggered: false };
12
+ if (entries.length < threshold) {
13
+ return { triggered: false };
14
+ }
13
15
 
14
16
  const windowMs = windowMinutes * 60 * 1000;
15
17
  let burstFound = false;
@@ -21,14 +23,14 @@ export async function checkBurstPublish(versionHistory, config = {}) {
21
23
  for (let i = 0; i < entries.length; i++) {
22
24
  const start = entries[i].time;
23
25
  const end = start + windowMs;
24
- const inWindow = entries.filter(e => e.time >= start && e.time <= end);
26
+ const inWindow = entries.filter((e) => e.time >= start && e.time <= end);
25
27
 
26
28
  if (inWindow.length >= threshold) {
27
29
  burstFound = true;
28
30
  burstWindowStart = new Date(start).toISOString();
29
31
  burstWindowEnd = new Date(end).toISOString();
30
32
  burstVersionCount = inWindow.length;
31
- burstVersions = inWindow.map(e => e.version);
33
+ burstVersions = inWindow.map((e) => e.version);
32
34
  break;
33
35
  }
34
36
  }
@@ -45,7 +47,12 @@ export async function checkBurstPublish(versionHistory, config = {}) {
45
47
  return {
46
48
  triggered: burstFound || hotPullDetected,
47
49
  burstWindow: burstFound
48
- ? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
50
+ ? {
51
+ start: burstWindowStart,
52
+ end: burstWindowEnd,
53
+ versionCount: burstVersionCount,
54
+ versions: burstVersions,
55
+ }
49
56
  : null,
50
57
  hotPullDetected,
51
58
  };