@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.
- package/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +138 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +147 -0
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- 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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 = [],
|
|
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('-'))
|
|
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'))
|
|
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('<'))
|
|
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)
|
|
45
|
+
if (!rest) {
|
|
46
|
+
return { name: 'starlette', version: null, specifier: null };
|
|
47
|
+
}
|
|
40
48
|
|
|
41
|
-
if (
|
|
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)
|
|
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)
|
|
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)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
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)
|
|
105
|
+
if (!version) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
80
108
|
const parsed = parsePEP440(version);
|
|
81
|
-
if (!parsed)
|
|
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')
|
|
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') {
|
|
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')
|
|
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 :
|
|
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('#'))
|
|
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])
|
|
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)
|
|
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 (
|
|
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)
|
|
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)
|
|
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)
|
|
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) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
else if (
|
|
224
|
-
|
|
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('['))
|
|
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) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
else if (
|
|
257
|
-
|
|
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
|
|
358
|
+
for (const file of allFiles || []) {
|
|
275
359
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
276
|
-
if (!content)
|
|
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)
|
|
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 {
|
|
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
|
|
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
|
|
41
|
+
for (const file of allFiles || []) {
|
|
32
42
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
33
|
-
if (!content)
|
|
43
|
+
if (!content) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
34
46
|
const path = file.path || '';
|
|
35
47
|
|
|
36
|
-
|
|
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('-'))
|
|
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 =
|
|
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 = {
|
|
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
|
|
110
|
+
for (const file of allFiles || []) {
|
|
82
111
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
83
|
-
if (!content)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
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)
|
|
179
|
+
if (!allFiles || allFiles.length === 0) {
|
|
180
|
+
return findings;
|
|
181
|
+
}
|
|
135
182
|
|
|
136
183
|
const packages = findPackagesInManifests(allFiles);
|
|
137
184
|
|
|
138
|
-
if (hasStarlettePin(allFiles))
|
|
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)
|
|
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))
|
|
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
|
|
225
|
+
for (const file of allFiles || []) {
|
|
173
226
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
174
|
-
if (!content)
|
|
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('-'))
|
|
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)
|
|
240
|
+
if (eqIdx >= 0) {
|
|
241
|
+
return trimmed.slice(eqIdx + 2).trim();
|
|
242
|
+
}
|
|
184
243
|
}
|
|
185
244
|
}
|
|
186
245
|
}
|