@lateos/npm-scan 0.17.1 → 0.18.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 (89) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +199 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -708
  6. package/README.fr.md +707 -707
  7. package/README.ja.md +704 -704
  8. package/README.md +826 -826
  9. package/README.zh.md +708 -708
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  26. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  27. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  28. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  29. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  30. package/backend/detectors/hf-impersonation/index.js +396 -396
  31. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  32. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  33. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  34. package/backend/detectors/index.js +75 -75
  35. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  36. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  37. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  38. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  39. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  40. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  41. package/backend/detectors/megalodon/index.js +80 -80
  42. package/backend/detectors/megalodon/types.js +9 -9
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  49. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  50. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  51. package/backend/fetch.js +175 -175
  52. package/backend/index.js +4 -4
  53. package/backend/license.js +89 -89
  54. package/backend/lockfile.js +379 -379
  55. package/backend/pdf.js +245 -245
  56. package/backend/policy.js +193 -193
  57. package/backend/report.js +254 -254
  58. package/backend/sbom.js +66 -66
  59. package/backend/siem/cef.js +32 -32
  60. package/backend/siem/ecs.js +40 -40
  61. package/backend/siem/index.js +18 -18
  62. package/backend/siem/qradar.js +56 -56
  63. package/backend/siem/sentinel.js +27 -27
  64. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  65. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  66. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  67. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  68. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  69. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  70. package/backend/vsix-scan/index.js +183 -183
  71. package/backend/vsix-scan/marketplace-client.js +145 -145
  72. package/backend/vsix-scan/vsix-iocs.json +31 -31
  73. package/cli/cli.js +458 -458
  74. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  75. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  76. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  77. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  78. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  79. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  80. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  81. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  82. package/deploy/helm/npm-scan/values.yaml +102 -102
  83. package/package.json +57 -57
  84. package/scripts/download-corpus.js +30 -30
  85. package/scripts/gen-mal-corpus.js +34 -34
  86. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  87. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  88. package/test/fixtures/lockfiles/yarn.lock +103 -103
  89. package/test/fixtures/mock-data.js +69 -69
@@ -1,305 +1,305 @@
1
- import { directDependencyFinding, directDependencyUnpinnedFinding } from './findings.js';
2
-
3
- function parseReqTxtLine(line) {
4
- const trimmed = line.trim();
5
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) return null;
6
- const idx = trimmed.indexOf('#');
7
- const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
8
- if (!spec || !spec.startsWith('starlette')) return null;
9
-
10
- const eqIdx = spec.indexOf('==');
11
- const geIdx = spec.indexOf('>=');
12
- const tildeIdx = spec.indexOf('~=');
13
- const ltIdx = spec.indexOf('<');
14
-
15
- if (eqIdx >= 0) {
16
- const ver = spec.slice(eqIdx + 2).trim();
17
- return { name: 'starlette', version: ver, specifier: `==${ver}` };
18
- }
19
- if (geIdx >= 0) {
20
- const rest = spec.slice(geIdx + 2).trim();
21
- const parts = rest.split(',');
22
- const lower = parts[0]?.trim();
23
- const upper = parts[1]?.trim();
24
- let specStr = `>=${lower}`;
25
- if (upper && upper.startsWith('<')) specStr += `,${upper}`;
26
- return { name: 'starlette', version: lower, specifier: specStr };
27
- }
28
- if (tildeIdx >= 0) {
29
- const ver = spec.slice(tildeIdx + 2).trim();
30
- return { name: 'starlette', version: ver, specifier: `~=${ver}` };
31
- }
32
- if (ltIdx >= 0) {
33
- const ver = spec.slice(ltIdx + 1).trim();
34
- const name = 'starlette';
35
- return { name, version: ver, specifier: `<${ver}` };
36
- }
37
-
38
- const rest = spec.slice('starlette'.length).trim();
39
- if (!rest) return { name: 'starlette', version: null, specifier: null };
40
-
41
- if (!rest.includes('=') && !rest.includes('<') && !rest.includes('>') && !rest.includes('~') && !rest.includes('!')) {
42
- return { name: 'starlette', version: rest, specifier: rest };
43
- }
44
-
45
- return null;
46
- }
47
-
48
- export function parseRequirementsTxt(content) {
49
- const lines = content.split('\n');
50
- for (const line of lines) {
51
- const result = parseReqTxtLine(line);
52
- if (result) return result;
53
- }
54
- return null;
55
- }
56
-
57
- function parsePEP440(versionStr) {
58
- if (!versionStr) return null;
59
- const clean = versionStr.trim().replace(/^v/, '');
60
- const parts = clean.split('.');
61
- return {
62
- major: parseInt(parts[0], 10) || 0,
63
- minor: parseInt(parts[1], 10) || 0,
64
- patch: parseInt(parts[2], 10) || 0,
65
- };
66
- }
67
-
68
- 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;
73
- return a.patch - b.patch;
74
- }
75
-
76
- const STARLETTE_SAFE = parsePEP440('1.0.1');
77
-
78
- function isVulnerable(version) {
79
- if (!version) return true;
80
- const parsed = parsePEP440(version);
81
- if (!parsed) return true;
82
- return compareVersions(parsed, STARLETTE_SAFE) < 0;
83
- }
84
-
85
- function findStarletteInTOML(obj) {
86
- if (!obj || typeof obj !== 'object') return null;
87
-
88
- const sectionPaths = ['dependencies', 'project.dependencies', 'tool.poetry.dependencies'];
89
- for (const path of sectionPaths) {
90
- const parts = path.split('.');
91
- let ptr = obj;
92
- let found = true;
93
- for (const p of parts) {
94
- if (!ptr || typeof ptr !== 'object') { found = false; break; }
95
- ptr = ptr[p];
96
- }
97
- if (!found || !ptr || typeof ptr !== 'object') continue;
98
- for (const [key, val] of Object.entries(ptr)) {
99
- if (key === 'starlette' || key === '"starlette"') {
100
- const version = typeof val === 'string' ? val : (val?.version || null);
101
- const specifier = typeof val === 'string' ? val : null;
102
- return { name: 'starlette', version, specifier };
103
- }
104
- }
105
- }
106
- return null;
107
- }
108
-
109
- function parseTomlSimple(content) {
110
- const result = {};
111
- let currentSection = result;
112
-
113
- for (const line of content.split('\n')) {
114
- const trimmed = line.trim();
115
- if (!trimmed || trimmed.startsWith('#')) continue;
116
- const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
117
- if (sectionMatch) {
118
- const parts = sectionMatch[1].split('.');
119
- let ptr = result;
120
- for (const p of parts) {
121
- const key = p.replace(/^"(.*)"$/, '$1').trim();
122
- if (!ptr[key]) ptr[key] = {};
123
- ptr = ptr[key];
124
- }
125
- currentSection = ptr;
126
- continue;
127
- }
128
- const kvMatch = trimmed.match(/^([^=]+)=\s*(.+)$/);
129
- if (!kvMatch) continue;
130
- const key = kvMatch[1].trim().replace(/^"(.*)"$/, '$1');
131
- let val = kvMatch[2].trim();
132
- if (val.startsWith('"') && val.endsWith('"')) {
133
- val = val.slice(1, -1);
134
- } else if (val.startsWith("'") && val.endsWith("'")) {
135
- val = val.slice(1, -1);
136
- } else if (val.startsWith('^') || val.startsWith('~') || val.startsWith('>') || val.startsWith('<') || val.startsWith('=') || val.startsWith('!')) {
137
- val = val.replace(/"/g, '');
138
- }
139
- currentSection[key] = val;
140
- }
141
-
142
- return result;
143
- }
144
-
145
- export function parsePyprojectToml(content) {
146
- let obj;
147
- try {
148
- obj = JSON.parse(content);
149
- } catch {
150
- obj = parseTomlSimple(content);
151
- }
152
- return findStarletteInTOML(obj);
153
- }
154
-
155
- function parsePoetryLockEntry(content) {
156
- const lines = content.split('\n');
157
- let inStarlette = false;
158
- let version = null;
159
- for (const line of lines) {
160
- const trimmed = line.trim();
161
- if (trimmed.startsWith('[[package]]')) {
162
- inStarlette = false;
163
- }
164
- if (trimmed.startsWith('name = "starlette"') || trimmed.startsWith("name = 'starlette'")) {
165
- inStarlette = true;
166
- }
167
- if (inStarlette && trimmed.startsWith('version = ')) {
168
- const match = trimmed.match(/version\s*=\s*["'](.+?)["']/);
169
- if (match) version = match[1];
170
- }
171
- if (inStarlette && trimmed.startsWith('[[package]]') && trimmed !== '[[package]]') {
172
- break;
173
- }
174
- }
175
- if (version) {
176
- return { name: 'starlette', version, specifier: `==${version}` };
177
- }
178
- return null;
179
- }
180
-
181
- export function parsePoetryLock(content) {
182
- return parsePoetryLockEntry(content);
183
- }
184
-
185
- function parsePipfileEntry(content) {
186
- try {
187
- const obj = JSON.parse(content);
188
- const packages = obj?.packages || {};
189
- for (const [key, val] of Object.entries(packages)) {
190
- if (key === 'starlette' || key === '"starlette"') {
191
- const version = typeof val === 'string' ? val : null;
192
- return { name: 'starlette', version, specifier: version || null };
193
- }
194
- }
195
- return null;
196
- } catch {
197
- return null;
198
- }
199
- }
200
-
201
- export function parsePipfile(content) {
202
- return parsePipfileEntry(content);
203
- }
204
-
205
- function parseSetupPyContent(content) {
206
- const match = content.match(/install_requires\s*=\s*\[([^\]]+)\]/s);
207
- if (!match) return null;
208
- const block = match[1];
209
- const lines = block.split(',').map(l => l.trim().replace(/["']/g, ''));
210
- for (const line of lines) {
211
- const clean = line.trim();
212
- if (!clean) continue;
213
- if (clean.startsWith('starlette')) {
214
- const eqIdx = clean.indexOf('==');
215
- const geIdx = clean.indexOf('>=');
216
- const tildeIdx = clean.indexOf('~=');
217
- const ltIdx = clean.indexOf('<');
218
- let version = null;
219
- 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 }; }
225
- return { name: 'starlette', version, specifier };
226
- }
227
- }
228
- return null;
229
- }
230
-
231
- export function parseSetupPy(content) {
232
- return parseSetupPyContent(content);
233
- }
234
-
235
- function parseSetupCfgContent(content) {
236
- const lines = content.split('\n');
237
- let inInstallRequires = false;
238
- for (const line of lines) {
239
- const trimmed = line.trim();
240
- if (trimmed.startsWith('install_requires')) {
241
- inInstallRequires = true;
242
- continue;
243
- }
244
- if (inInstallRequires) {
245
- if (trimmed.startsWith('[')) break;
246
- if (trimmed.startsWith('starlette')) {
247
- const eqIdx = trimmed.indexOf('==');
248
- const geIdx = trimmed.indexOf('>=');
249
- const tildeIdx = trimmed.indexOf('~=');
250
- const ltIdx = trimmed.indexOf('<');
251
- let version = null;
252
- 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 }; }
258
- if (trimmed.startsWith('starlette')) {
259
- return { name: 'starlette', version, specifier };
260
- }
261
- }
262
- }
263
- }
264
- return null;
265
- }
266
-
267
- export function parseSetupCfg(content) {
268
- return parseSetupCfgContent(content);
269
- }
270
-
271
- export function scanFiles(allFiles) {
272
- const findings = [];
273
-
274
- for (const file of (allFiles || [])) {
275
- const content = typeof file.content === 'string' ? file.content : '';
276
- if (!content) continue;
277
- const path = file.path || '';
278
-
279
- let result = null;
280
-
281
- if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
282
- result = parseRequirementsTxt(content);
283
- } else if (path === 'pyproject.toml') {
284
- result = parsePyprojectToml(content);
285
- } else if (path === 'poetry.lock') {
286
- result = parsePoetryLock(content);
287
- } else if (path === 'Pipfile' || path === 'Pipfile.lock') {
288
- result = parsePipfile(content);
289
- } else if (path === 'setup.py') {
290
- result = parseSetupPy(content);
291
- } else if (path === 'setup.cfg') {
292
- result = parseSetupCfg(content);
293
- }
294
-
295
- if (!result) continue;
296
-
297
- if (result.version === null && result.specifier === null) {
298
- findings.push(directDependencyUnpinnedFinding());
299
- } else if (isVulnerable(result.version)) {
300
- findings.push(directDependencyFinding(result.version, result.specifier || 'unknown'));
301
- }
302
- }
303
-
304
- return findings;
305
- }
1
+ import { directDependencyFinding, directDependencyUnpinnedFinding } from './findings.js';
2
+
3
+ function parseReqTxtLine(line) {
4
+ const trimmed = line.trim();
5
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) return null;
6
+ const idx = trimmed.indexOf('#');
7
+ const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
8
+ if (!spec || !spec.startsWith('starlette')) return null;
9
+
10
+ const eqIdx = spec.indexOf('==');
11
+ const geIdx = spec.indexOf('>=');
12
+ const tildeIdx = spec.indexOf('~=');
13
+ const ltIdx = spec.indexOf('<');
14
+
15
+ if (eqIdx >= 0) {
16
+ const ver = spec.slice(eqIdx + 2).trim();
17
+ return { name: 'starlette', version: ver, specifier: `==${ver}` };
18
+ }
19
+ if (geIdx >= 0) {
20
+ const rest = spec.slice(geIdx + 2).trim();
21
+ const parts = rest.split(',');
22
+ const lower = parts[0]?.trim();
23
+ const upper = parts[1]?.trim();
24
+ let specStr = `>=${lower}`;
25
+ if (upper && upper.startsWith('<')) specStr += `,${upper}`;
26
+ return { name: 'starlette', version: lower, specifier: specStr };
27
+ }
28
+ if (tildeIdx >= 0) {
29
+ const ver = spec.slice(tildeIdx + 2).trim();
30
+ return { name: 'starlette', version: ver, specifier: `~=${ver}` };
31
+ }
32
+ if (ltIdx >= 0) {
33
+ const ver = spec.slice(ltIdx + 1).trim();
34
+ const name = 'starlette';
35
+ return { name, version: ver, specifier: `<${ver}` };
36
+ }
37
+
38
+ const rest = spec.slice('starlette'.length).trim();
39
+ if (!rest) return { name: 'starlette', version: null, specifier: null };
40
+
41
+ if (!rest.includes('=') && !rest.includes('<') && !rest.includes('>') && !rest.includes('~') && !rest.includes('!')) {
42
+ return { name: 'starlette', version: rest, specifier: rest };
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ export function parseRequirementsTxt(content) {
49
+ const lines = content.split('\n');
50
+ for (const line of lines) {
51
+ const result = parseReqTxtLine(line);
52
+ if (result) return result;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function parsePEP440(versionStr) {
58
+ if (!versionStr) return null;
59
+ const clean = versionStr.trim().replace(/^v/, '');
60
+ const parts = clean.split('.');
61
+ return {
62
+ major: parseInt(parts[0], 10) || 0,
63
+ minor: parseInt(parts[1], 10) || 0,
64
+ patch: parseInt(parts[2], 10) || 0,
65
+ };
66
+ }
67
+
68
+ 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;
73
+ return a.patch - b.patch;
74
+ }
75
+
76
+ const STARLETTE_SAFE = parsePEP440('1.0.1');
77
+
78
+ function isVulnerable(version) {
79
+ if (!version) return true;
80
+ const parsed = parsePEP440(version);
81
+ if (!parsed) return true;
82
+ return compareVersions(parsed, STARLETTE_SAFE) < 0;
83
+ }
84
+
85
+ function findStarletteInTOML(obj) {
86
+ if (!obj || typeof obj !== 'object') return null;
87
+
88
+ const sectionPaths = ['dependencies', 'project.dependencies', 'tool.poetry.dependencies'];
89
+ for (const path of sectionPaths) {
90
+ const parts = path.split('.');
91
+ let ptr = obj;
92
+ let found = true;
93
+ for (const p of parts) {
94
+ if (!ptr || typeof ptr !== 'object') { found = false; break; }
95
+ ptr = ptr[p];
96
+ }
97
+ if (!found || !ptr || typeof ptr !== 'object') continue;
98
+ for (const [key, val] of Object.entries(ptr)) {
99
+ if (key === 'starlette' || key === '"starlette"') {
100
+ const version = typeof val === 'string' ? val : (val?.version || null);
101
+ const specifier = typeof val === 'string' ? val : null;
102
+ return { name: 'starlette', version, specifier };
103
+ }
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function parseTomlSimple(content) {
110
+ const result = {};
111
+ let currentSection = result;
112
+
113
+ for (const line of content.split('\n')) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed || trimmed.startsWith('#')) continue;
116
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
117
+ if (sectionMatch) {
118
+ const parts = sectionMatch[1].split('.');
119
+ let ptr = result;
120
+ for (const p of parts) {
121
+ const key = p.replace(/^"(.*)"$/, '$1').trim();
122
+ if (!ptr[key]) ptr[key] = {};
123
+ ptr = ptr[key];
124
+ }
125
+ currentSection = ptr;
126
+ continue;
127
+ }
128
+ const kvMatch = trimmed.match(/^([^=]+)=\s*(.+)$/);
129
+ if (!kvMatch) continue;
130
+ const key = kvMatch[1].trim().replace(/^"(.*)"$/, '$1');
131
+ let val = kvMatch[2].trim();
132
+ if (val.startsWith('"') && val.endsWith('"')) {
133
+ val = val.slice(1, -1);
134
+ } else if (val.startsWith("'") && val.endsWith("'")) {
135
+ val = val.slice(1, -1);
136
+ } else if (val.startsWith('^') || val.startsWith('~') || val.startsWith('>') || val.startsWith('<') || val.startsWith('=') || val.startsWith('!')) {
137
+ val = val.replace(/"/g, '');
138
+ }
139
+ currentSection[key] = val;
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ export function parsePyprojectToml(content) {
146
+ let obj;
147
+ try {
148
+ obj = JSON.parse(content);
149
+ } catch {
150
+ obj = parseTomlSimple(content);
151
+ }
152
+ return findStarletteInTOML(obj);
153
+ }
154
+
155
+ function parsePoetryLockEntry(content) {
156
+ const lines = content.split('\n');
157
+ let inStarlette = false;
158
+ let version = null;
159
+ for (const line of lines) {
160
+ const trimmed = line.trim();
161
+ if (trimmed.startsWith('[[package]]')) {
162
+ inStarlette = false;
163
+ }
164
+ if (trimmed.startsWith('name = "starlette"') || trimmed.startsWith("name = 'starlette'")) {
165
+ inStarlette = true;
166
+ }
167
+ if (inStarlette && trimmed.startsWith('version = ')) {
168
+ const match = trimmed.match(/version\s*=\s*["'](.+?)["']/);
169
+ if (match) version = match[1];
170
+ }
171
+ if (inStarlette && trimmed.startsWith('[[package]]') && trimmed !== '[[package]]') {
172
+ break;
173
+ }
174
+ }
175
+ if (version) {
176
+ return { name: 'starlette', version, specifier: `==${version}` };
177
+ }
178
+ return null;
179
+ }
180
+
181
+ export function parsePoetryLock(content) {
182
+ return parsePoetryLockEntry(content);
183
+ }
184
+
185
+ function parsePipfileEntry(content) {
186
+ try {
187
+ const obj = JSON.parse(content);
188
+ const packages = obj?.packages || {};
189
+ for (const [key, val] of Object.entries(packages)) {
190
+ if (key === 'starlette' || key === '"starlette"') {
191
+ const version = typeof val === 'string' ? val : null;
192
+ return { name: 'starlette', version, specifier: version || null };
193
+ }
194
+ }
195
+ return null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ export function parsePipfile(content) {
202
+ return parsePipfileEntry(content);
203
+ }
204
+
205
+ function parseSetupPyContent(content) {
206
+ const match = content.match(/install_requires\s*=\s*\[([^\]]+)\]/s);
207
+ if (!match) return null;
208
+ const block = match[1];
209
+ const lines = block.split(',').map(l => l.trim().replace(/["']/g, ''));
210
+ for (const line of lines) {
211
+ const clean = line.trim();
212
+ if (!clean) continue;
213
+ if (clean.startsWith('starlette')) {
214
+ const eqIdx = clean.indexOf('==');
215
+ const geIdx = clean.indexOf('>=');
216
+ const tildeIdx = clean.indexOf('~=');
217
+ const ltIdx = clean.indexOf('<');
218
+ let version = null;
219
+ 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 }; }
225
+ return { name: 'starlette', version, specifier };
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+
231
+ export function parseSetupPy(content) {
232
+ return parseSetupPyContent(content);
233
+ }
234
+
235
+ function parseSetupCfgContent(content) {
236
+ const lines = content.split('\n');
237
+ let inInstallRequires = false;
238
+ for (const line of lines) {
239
+ const trimmed = line.trim();
240
+ if (trimmed.startsWith('install_requires')) {
241
+ inInstallRequires = true;
242
+ continue;
243
+ }
244
+ if (inInstallRequires) {
245
+ if (trimmed.startsWith('[')) break;
246
+ if (trimmed.startsWith('starlette')) {
247
+ const eqIdx = trimmed.indexOf('==');
248
+ const geIdx = trimmed.indexOf('>=');
249
+ const tildeIdx = trimmed.indexOf('~=');
250
+ const ltIdx = trimmed.indexOf('<');
251
+ let version = null;
252
+ 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 }; }
258
+ if (trimmed.startsWith('starlette')) {
259
+ return { name: 'starlette', version, specifier };
260
+ }
261
+ }
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+
267
+ export function parseSetupCfg(content) {
268
+ return parseSetupCfgContent(content);
269
+ }
270
+
271
+ export function scanFiles(allFiles) {
272
+ const findings = [];
273
+
274
+ for (const file of (allFiles || [])) {
275
+ const content = typeof file.content === 'string' ? file.content : '';
276
+ if (!content) continue;
277
+ const path = file.path || '';
278
+
279
+ let result = null;
280
+
281
+ if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
282
+ result = parseRequirementsTxt(content);
283
+ } else if (path === 'pyproject.toml') {
284
+ result = parsePyprojectToml(content);
285
+ } else if (path === 'poetry.lock') {
286
+ result = parsePoetryLock(content);
287
+ } else if (path === 'Pipfile' || path === 'Pipfile.lock') {
288
+ result = parsePipfile(content);
289
+ } else if (path === 'setup.py') {
290
+ result = parseSetupPy(content);
291
+ } else if (path === 'setup.cfg') {
292
+ result = parseSetupCfg(content);
293
+ }
294
+
295
+ if (!result) continue;
296
+
297
+ if (result.version === null && result.specifier === null) {
298
+ findings.push(directDependencyUnpinnedFinding());
299
+ } else if (isVulnerable(result.version)) {
300
+ findings.push(directDependencyFinding(result.version, result.specifier || 'unknown'));
301
+ }
302
+ }
303
+
304
+ return findings;
305
+ }