@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
@@ -8,11 +8,14 @@ const REFERENCES = [
8
8
  'https://osv.dev/vulnerability/PYSEC-2026-161',
9
9
  ];
10
10
 
11
- const MITIGATION_NOTE = 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
11
+ const MITIGATION_NOTE =
12
+ 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
12
13
 
13
- const DEPENDENCY_REMEDIATION = 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
14
+ const DEPENDENCY_REMEDIATION =
15
+ 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
14
16
 
15
- const CODE_REMEDIATION = 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
17
+ const CODE_REMEDIATION =
18
+ 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
16
19
 
17
20
  function makeFinding(overrides = {}) {
18
21
  return {
@@ -52,7 +55,8 @@ export function directDependencyUnpinnedFinding() {
52
55
  confidence: 'HIGH',
53
56
  source: 'direct-dependency-unpinned',
54
57
  title: `${NICKNAME}: Starlette unpinned`,
55
- description: 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
58
+ description:
59
+ 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
56
60
  remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
57
61
  file: null,
58
62
  line: null,
@@ -2,7 +2,7 @@ import { scanFiles } from './manifest.js';
2
2
  import { scanTransitive } from './transitive.js';
3
3
  import { scanCodePatterns } from './codePattern.js';
4
4
 
5
- export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
5
+ export async function scan(pkgJson, files = [], _registryMeta = null, allFiles = null) {
6
6
  const targetFiles = allFiles || files;
7
7
 
8
8
  const manifestFindings = scanFiles(targetFiles);
@@ -2,10 +2,14 @@ import { directDependencyFinding, directDependencyUnpinnedFinding } from './find
2
2
 
3
3
  function parseReqTxtLine(line) {
4
4
  const trimmed = line.trim();
5
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) return null;
5
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
6
+ return null;
7
+ }
6
8
  const idx = trimmed.indexOf('#');
7
9
  const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
8
- if (!spec || !spec.startsWith('starlette')) return null;
10
+ if (!spec || !spec.startsWith('starlette')) {
11
+ return null;
12
+ }
9
13
 
10
14
  const eqIdx = spec.indexOf('==');
11
15
  const geIdx = spec.indexOf('>=');
@@ -22,7 +26,9 @@ function parseReqTxtLine(line) {
22
26
  const lower = parts[0]?.trim();
23
27
  const upper = parts[1]?.trim();
24
28
  let specStr = `>=${lower}`;
25
- if (upper && upper.startsWith('<')) specStr += `,${upper}`;
29
+ if (upper && upper.startsWith('<')) {
30
+ specStr += `,${upper}`;
31
+ }
26
32
  return { name: 'starlette', version: lower, specifier: specStr };
27
33
  }
28
34
  if (tildeIdx >= 0) {
@@ -36,9 +42,17 @@ function parseReqTxtLine(line) {
36
42
  }
37
43
 
38
44
  const rest = spec.slice('starlette'.length).trim();
39
- if (!rest) return { name: 'starlette', version: null, specifier: null };
45
+ if (!rest) {
46
+ return { name: 'starlette', version: null, specifier: null };
47
+ }
40
48
 
41
- if (!rest.includes('=') && !rest.includes('<') && !rest.includes('>') && !rest.includes('~') && !rest.includes('!')) {
49
+ if (
50
+ !rest.includes('=') &&
51
+ !rest.includes('<') &&
52
+ !rest.includes('>') &&
53
+ !rest.includes('~') &&
54
+ !rest.includes('!')
55
+ ) {
42
56
  return { name: 'starlette', version: rest, specifier: rest };
43
57
  }
44
58
 
@@ -49,13 +63,17 @@ export function parseRequirementsTxt(content) {
49
63
  const lines = content.split('\n');
50
64
  for (const line of lines) {
51
65
  const result = parseReqTxtLine(line);
52
- if (result) return result;
66
+ if (result) {
67
+ return result;
68
+ }
53
69
  }
54
70
  return null;
55
71
  }
56
72
 
57
73
  function parsePEP440(versionStr) {
58
- if (!versionStr) return null;
74
+ if (!versionStr) {
75
+ return null;
76
+ }
59
77
  const clean = versionStr.trim().replace(/^v/, '');
60
78
  const parts = clean.split('.');
61
79
  return {
@@ -66,24 +84,38 @@ function parsePEP440(versionStr) {
66
84
  }
67
85
 
68
86
  function compareVersions(a, b) {
69
- if (!a) return 1;
70
- if (!b) return -1;
71
- if (a.major !== b.major) return a.major - b.major;
72
- if (a.minor !== b.minor) return a.minor - b.minor;
87
+ if (!a) {
88
+ return 1;
89
+ }
90
+ if (!b) {
91
+ return -1;
92
+ }
93
+ if (a.major !== b.major) {
94
+ return a.major - b.major;
95
+ }
96
+ if (a.minor !== b.minor) {
97
+ return a.minor - b.minor;
98
+ }
73
99
  return a.patch - b.patch;
74
100
  }
75
101
 
76
102
  const STARLETTE_SAFE = parsePEP440('1.0.1');
77
103
 
78
104
  function isVulnerable(version) {
79
- if (!version) return true;
105
+ if (!version) {
106
+ return true;
107
+ }
80
108
  const parsed = parsePEP440(version);
81
- if (!parsed) return true;
109
+ if (!parsed) {
110
+ return true;
111
+ }
82
112
  return compareVersions(parsed, STARLETTE_SAFE) < 0;
83
113
  }
84
114
 
85
115
  function findStarletteInTOML(obj) {
86
- if (!obj || typeof obj !== 'object') return null;
116
+ if (!obj || typeof obj !== 'object') {
117
+ return null;
118
+ }
87
119
 
88
120
  const sectionPaths = ['dependencies', 'project.dependencies', 'tool.poetry.dependencies'];
89
121
  for (const path of sectionPaths) {
@@ -91,13 +123,18 @@ function findStarletteInTOML(obj) {
91
123
  let ptr = obj;
92
124
  let found = true;
93
125
  for (const p of parts) {
94
- if (!ptr || typeof ptr !== 'object') { found = false; break; }
126
+ if (!ptr || typeof ptr !== 'object') {
127
+ found = false;
128
+ break;
129
+ }
95
130
  ptr = ptr[p];
96
131
  }
97
- if (!found || !ptr || typeof ptr !== 'object') continue;
132
+ if (!found || !ptr || typeof ptr !== 'object') {
133
+ continue;
134
+ }
98
135
  for (const [key, val] of Object.entries(ptr)) {
99
136
  if (key === 'starlette' || key === '"starlette"') {
100
- const version = typeof val === 'string' ? val : (val?.version || null);
137
+ const version = typeof val === 'string' ? val : val?.version || null;
101
138
  const specifier = typeof val === 'string' ? val : null;
102
139
  return { name: 'starlette', version, specifier };
103
140
  }
@@ -112,28 +149,41 @@ function parseTomlSimple(content) {
112
149
 
113
150
  for (const line of content.split('\n')) {
114
151
  const trimmed = line.trim();
115
- if (!trimmed || trimmed.startsWith('#')) continue;
152
+ if (!trimmed || trimmed.startsWith('#')) {
153
+ continue;
154
+ }
116
155
  const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
117
156
  if (sectionMatch) {
118
157
  const parts = sectionMatch[1].split('.');
119
158
  let ptr = result;
120
159
  for (const p of parts) {
121
160
  const key = p.replace(/^"(.*)"$/, '$1').trim();
122
- if (!ptr[key]) ptr[key] = {};
161
+ if (!ptr[key]) {
162
+ ptr[key] = {};
163
+ }
123
164
  ptr = ptr[key];
124
165
  }
125
166
  currentSection = ptr;
126
167
  continue;
127
168
  }
128
169
  const kvMatch = trimmed.match(/^([^=]+)=\s*(.+)$/);
129
- if (!kvMatch) continue;
170
+ if (!kvMatch) {
171
+ continue;
172
+ }
130
173
  const key = kvMatch[1].trim().replace(/^"(.*)"$/, '$1');
131
174
  let val = kvMatch[2].trim();
132
175
  if (val.startsWith('"') && val.endsWith('"')) {
133
176
  val = val.slice(1, -1);
134
177
  } else if (val.startsWith("'") && val.endsWith("'")) {
135
178
  val = val.slice(1, -1);
136
- } else if (val.startsWith('^') || val.startsWith('~') || val.startsWith('>') || val.startsWith('<') || val.startsWith('=') || val.startsWith('!')) {
179
+ } else if (
180
+ val.startsWith('^') ||
181
+ val.startsWith('~') ||
182
+ val.startsWith('>') ||
183
+ val.startsWith('<') ||
184
+ val.startsWith('=') ||
185
+ val.startsWith('!')
186
+ ) {
137
187
  val = val.replace(/"/g, '');
138
188
  }
139
189
  currentSection[key] = val;
@@ -166,7 +216,9 @@ function parsePoetryLockEntry(content) {
166
216
  }
167
217
  if (inStarlette && trimmed.startsWith('version = ')) {
168
218
  const match = trimmed.match(/version\s*=\s*["'](.+?)["']/);
169
- if (match) version = match[1];
219
+ if (match) {
220
+ version = match[1];
221
+ }
170
222
  }
171
223
  if (inStarlette && trimmed.startsWith('[[package]]') && trimmed !== '[[package]]') {
172
224
  break;
@@ -204,12 +256,16 @@ export function parsePipfile(content) {
204
256
 
205
257
  function parseSetupPyContent(content) {
206
258
  const match = content.match(/install_requires\s*=\s*\[([^\]]+)\]/s);
207
- if (!match) return null;
259
+ if (!match) {
260
+ return null;
261
+ }
208
262
  const block = match[1];
209
- const lines = block.split(',').map(l => l.trim().replace(/["']/g, ''));
263
+ const lines = block.split(',').map((l) => l.trim().replace(/["']/g, ''));
210
264
  for (const line of lines) {
211
265
  const clean = line.trim();
212
- if (!clean) continue;
266
+ if (!clean) {
267
+ continue;
268
+ }
213
269
  if (clean.startsWith('starlette')) {
214
270
  const eqIdx = clean.indexOf('==');
215
271
  const geIdx = clean.indexOf('>=');
@@ -217,11 +273,24 @@ function parseSetupPyContent(content) {
217
273
  const ltIdx = clean.indexOf('<');
218
274
  let version = null;
219
275
  let specifier = null;
220
- if (eqIdx >= 0) { version = clean.slice(eqIdx + 2).trim(); specifier = `==${version}`; }
221
- else if (geIdx >= 0) { version = clean.slice(geIdx + 2).split(',')[0].trim(); specifier = `>=${version}`; }
222
- else if (tildeIdx >= 0) { version = clean.slice(tildeIdx + 2).trim(); specifier = `~=${version}`; }
223
- else if (ltIdx >= 0) { version = clean.slice(ltIdx + 1).trim(); specifier = `<${version}`; }
224
- else if (clean === 'starlette') { return { name: 'starlette', version: null, specifier: null }; }
276
+ if (eqIdx >= 0) {
277
+ version = clean.slice(eqIdx + 2).trim();
278
+ specifier = `==${version}`;
279
+ } else if (geIdx >= 0) {
280
+ version = clean
281
+ .slice(geIdx + 2)
282
+ .split(',')[0]
283
+ .trim();
284
+ specifier = `>=${version}`;
285
+ } else if (tildeIdx >= 0) {
286
+ version = clean.slice(tildeIdx + 2).trim();
287
+ specifier = `~=${version}`;
288
+ } else if (ltIdx >= 0) {
289
+ version = clean.slice(ltIdx + 1).trim();
290
+ specifier = `<${version}`;
291
+ } else if (clean === 'starlette') {
292
+ return { name: 'starlette', version: null, specifier: null };
293
+ }
225
294
  return { name: 'starlette', version, specifier };
226
295
  }
227
296
  }
@@ -242,7 +311,9 @@ function parseSetupCfgContent(content) {
242
311
  continue;
243
312
  }
244
313
  if (inInstallRequires) {
245
- if (trimmed.startsWith('[')) break;
314
+ if (trimmed.startsWith('[')) {
315
+ break;
316
+ }
246
317
  if (trimmed.startsWith('starlette')) {
247
318
  const eqIdx = trimmed.indexOf('==');
248
319
  const geIdx = trimmed.indexOf('>=');
@@ -250,11 +321,24 @@ function parseSetupCfgContent(content) {
250
321
  const ltIdx = trimmed.indexOf('<');
251
322
  let version = null;
252
323
  let specifier = null;
253
- if (eqIdx >= 0) { version = trimmed.slice(eqIdx + 2).trim(); specifier = `==${version}`; }
254
- else if (geIdx >= 0) { version = trimmed.slice(geIdx + 2).split(',')[0].trim(); specifier = `>=${version}`; }
255
- else if (tildeIdx >= 0) { version = trimmed.slice(tildeIdx + 2).trim(); specifier = `~=${version}`; }
256
- else if (ltIdx >= 0) { version = trimmed.slice(ltIdx + 1).trim(); specifier = `<${version}`; }
257
- else if (trimmed === 'starlette') { return { name: 'starlette', version: null, specifier: null }; }
324
+ if (eqIdx >= 0) {
325
+ version = trimmed.slice(eqIdx + 2).trim();
326
+ specifier = `==${version}`;
327
+ } else if (geIdx >= 0) {
328
+ version = trimmed
329
+ .slice(geIdx + 2)
330
+ .split(',')[0]
331
+ .trim();
332
+ specifier = `>=${version}`;
333
+ } else if (tildeIdx >= 0) {
334
+ version = trimmed.slice(tildeIdx + 2).trim();
335
+ specifier = `~=${version}`;
336
+ } else if (ltIdx >= 0) {
337
+ version = trimmed.slice(ltIdx + 1).trim();
338
+ specifier = `<${version}`;
339
+ } else if (trimmed === 'starlette') {
340
+ return { name: 'starlette', version: null, specifier: null };
341
+ }
258
342
  if (trimmed.startsWith('starlette')) {
259
343
  return { name: 'starlette', version, specifier };
260
344
  }
@@ -271,9 +355,11 @@ export function parseSetupCfg(content) {
271
355
  export function scanFiles(allFiles) {
272
356
  const findings = [];
273
357
 
274
- for (const file of (allFiles || [])) {
358
+ for (const file of allFiles || []) {
275
359
  const content = typeof file.content === 'string' ? file.content : '';
276
- if (!content) continue;
360
+ if (!content) {
361
+ continue;
362
+ }
277
363
  const path = file.path || '';
278
364
 
279
365
  let result = null;
@@ -292,7 +378,9 @@ export function scanFiles(allFiles) {
292
378
  result = parseSetupCfg(content);
293
379
  }
294
380
 
295
- if (!result) continue;
381
+ if (!result) {
382
+ continue;
383
+ }
296
384
 
297
385
  if (result.version === null && result.specifier === null) {
298
386
  findings.push(directDependencyUnpinnedFinding());
@@ -1,5 +1,12 @@
1
1
  import { transitiveDependencyFinding } from './findings.js';
2
- import { parseRequirementsTxt, parsePyprojectToml, parsePoetryLock, parsePipfile, parseSetupPy, parseSetupCfg } from './manifest.js';
2
+ import {
3
+ parseRequirementsTxt,
4
+ parsePyprojectToml,
5
+ parsePoetryLock,
6
+ parsePipfile,
7
+ parseSetupPy,
8
+ parseSetupCfg,
9
+ } from './manifest.js';
3
10
 
4
11
  const TIER_1_PACKAGES = [
5
12
  'fastapi',
@@ -22,29 +29,41 @@ const TIER_2_PACKAGES = [
22
29
  ];
23
30
 
24
31
  function normalizePkgName(name) {
25
- return name.replace(/["'\[\]]/g, '').trim().toLowerCase();
32
+ return name
33
+ .replace(/["'[\]]/g, '')
34
+ .trim()
35
+ .toLowerCase();
26
36
  }
27
37
 
28
38
  function findPackagesInManifests(allFiles) {
29
39
  const packages = new Set();
30
40
 
31
- for (const file of (allFiles || [])) {
41
+ for (const file of allFiles || []) {
32
42
  const content = typeof file.content === 'string' ? file.content : '';
33
- if (!content) continue;
43
+ if (!content) {
44
+ continue;
45
+ }
34
46
  const path = file.path || '';
35
47
 
36
- let deps = [];
48
+ const deps = [];
37
49
 
38
50
  if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
39
51
  const lines = content.split('\n');
40
52
  for (const line of lines) {
41
53
  const trimmed = line.trim();
42
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
54
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
55
+ continue;
56
+ }
43
57
  const idx = trimmed.indexOf('#');
44
58
  const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
45
59
  const eqIdx = spec.indexOf('==');
46
60
  const geIdx = spec.indexOf('>=');
47
- const name = eqIdx >= 0 ? spec.slice(0, eqIdx).trim() : (geIdx >= 0 ? spec.slice(0, geIdx).trim() : spec);
61
+ const name =
62
+ eqIdx >= 0
63
+ ? spec.slice(0, eqIdx).trim()
64
+ : geIdx >= 0
65
+ ? spec.slice(0, geIdx).trim()
66
+ : spec;
48
67
  if (name && !name.includes('=') && !name.includes('<') && !name.includes('>')) {
49
68
  deps.push(normalizePkgName(name));
50
69
  }
@@ -52,11 +71,17 @@ function findPackagesInManifests(allFiles) {
52
71
  } else if (path === 'pyproject.toml') {
53
72
  try {
54
73
  const obj = JSON.parse(content);
55
- const allDeps = { ...(obj?.tool?.poetry?.dependencies || {}), ...(obj?.dependencies || {}), ...(obj?.['dev-dependencies'] || {}) };
74
+ const allDeps = {
75
+ ...(obj?.tool?.poetry?.dependencies || {}),
76
+ ...(obj?.dependencies || {}),
77
+ ...(obj?.['dev-dependencies'] || {}),
78
+ };
56
79
  for (const key of Object.keys(allDeps)) {
57
80
  deps.push(normalizePkgName(key));
58
81
  }
59
- } catch {}
82
+ } catch {
83
+ /* ignore parse errors */
84
+ }
60
85
  } else if (path === 'poetry.lock') {
61
86
  const pattern = /name\s*=\s*["']([^"']+)["']/g;
62
87
  let m;
@@ -69,18 +94,24 @@ function findPackagesInManifests(allFiles) {
69
94
  for (const key of Object.keys(obj?.packages || {})) {
70
95
  deps.push(normalizePkgName(key));
71
96
  }
72
- } catch {}
97
+ } catch {
98
+ /* ignore parse errors */
99
+ }
100
+ }
101
+ for (const dep of deps) {
102
+ packages.add(dep);
73
103
  }
74
- for (const dep of deps) packages.add(dep);
75
104
  }
76
105
 
77
106
  return packages;
78
107
  }
79
108
 
80
109
  function hasStarlettePin(allFiles) {
81
- for (const file of (allFiles || [])) {
110
+ for (const file of allFiles || []) {
82
111
  const content = typeof file.content === 'string' ? file.content : '';
83
- if (!content) continue;
112
+ if (!content) {
113
+ continue;
114
+ }
84
115
  const path = file.path || '';
85
116
 
86
117
  let result = null;
@@ -99,10 +130,14 @@ function hasStarlettePin(allFiles) {
99
130
  }
100
131
 
101
132
  if (result) {
102
- if (result.version === null && result.specifier === null) return false;
133
+ if (result.version === null && result.specifier === null) {
134
+ return false;
135
+ }
103
136
  const parsed = parsePEP440(result.version);
104
137
  const safe = parsePEP440('1.0.1');
105
- if (parsed && compareVersions(parsed, safe) >= 0) return true;
138
+ if (parsed && compareVersions(parsed, safe) >= 0) {
139
+ return true;
140
+ }
106
141
  }
107
142
  }
108
143
 
@@ -110,7 +145,9 @@ function hasStarlettePin(allFiles) {
110
145
  }
111
146
 
112
147
  function parsePEP440(versionStr) {
113
- if (!versionStr) return null;
148
+ if (!versionStr) {
149
+ return null;
150
+ }
114
151
  const clean = versionStr.trim().replace(/^v/, '');
115
152
  const parts = clean.split('.');
116
153
  return {
@@ -121,21 +158,33 @@ function parsePEP440(versionStr) {
121
158
  }
122
159
 
123
160
  function compareVersions(a, b) {
124
- if (!a) return 1;
125
- if (!b) return -1;
126
- if (a.major !== b.major) return a.major - b.major;
127
- if (a.minor !== b.minor) return a.minor - b.minor;
161
+ if (!a) {
162
+ return 1;
163
+ }
164
+ if (!b) {
165
+ return -1;
166
+ }
167
+ if (a.major !== b.major) {
168
+ return a.major - b.major;
169
+ }
170
+ if (a.minor !== b.minor) {
171
+ return a.minor - b.minor;
172
+ }
128
173
  return a.patch - b.patch;
129
174
  }
130
175
 
131
176
  export function scanTransitive(allFiles) {
132
177
  const findings = [];
133
178
 
134
- if (!allFiles || allFiles.length === 0) return findings;
179
+ if (!allFiles || allFiles.length === 0) {
180
+ return findings;
181
+ }
135
182
 
136
183
  const packages = findPackagesInManifests(allFiles);
137
184
 
138
- if (hasStarlettePin(allFiles)) return findings;
185
+ if (hasStarlettePin(allFiles)) {
186
+ return findings;
187
+ }
139
188
 
140
189
  const handled = new Set();
141
190
 
@@ -147,7 +196,9 @@ export function scanTransitive(allFiles) {
147
196
  if (version) {
148
197
  const parsed = parsePEP440(version);
149
198
  const safeFastapi = parsePEP440('0.116.0');
150
- if (parsed && compareVersions(parsed, safeFastapi) >= 0) continue;
199
+ if (parsed && compareVersions(parsed, safeFastapi) >= 0) {
200
+ continue;
201
+ }
151
202
  }
152
203
  }
153
204
  findings.push(transitiveDependencyFinding(pkg, 1));
@@ -157,7 +208,9 @@ export function scanTransitive(allFiles) {
157
208
 
158
209
  if (findings.length === 0) {
159
210
  for (const pkg of packages) {
160
- if (handled.has(pkg)) continue;
211
+ if (handled.has(pkg)) {
212
+ continue;
213
+ }
161
214
  if (TIER_2_PACKAGES.includes(pkg)) {
162
215
  findings.push(transitiveDependencyFinding(pkg, 2));
163
216
  break;
@@ -169,18 +222,24 @@ export function scanTransitive(allFiles) {
169
222
  }
170
223
 
171
224
  function findFastapiVersion(allFiles) {
172
- for (const file of (allFiles || [])) {
225
+ for (const file of allFiles || []) {
173
226
  const content = typeof file.content === 'string' ? file.content : '';
174
- if (!content) continue;
227
+ if (!content) {
228
+ continue;
229
+ }
175
230
  const path = file.path || '';
176
231
  if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
177
232
  const lines = content.split('\n');
178
233
  for (const line of lines) {
179
234
  const trimmed = line.trim();
180
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
235
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
236
+ continue;
237
+ }
181
238
  if (trimmed.startsWith('fastapi')) {
182
239
  const eqIdx = trimmed.indexOf('==');
183
- if (eqIdx >= 0) return trimmed.slice(eqIdx + 2).trim();
240
+ if (eqIdx >= 0) {
241
+ return trimmed.slice(eqIdx + 2).trim();
242
+ }
184
243
  }
185
244
  }
186
245
  }