fad-checker 2.0.0 → 2.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 +65 -0
- package/README.md +147 -25
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +14 -1
- package/data/eol-mapping.json +5 -5
- package/data/license-policy.json +97 -0
- package/fad-checker-report/cve-report.doc +630 -0
- package/fad-checker-report/cve-report.html +748 -0
- package/fad-checker.js +556 -245
- package/lib/cache-archive.js +3 -0
- package/lib/codecs/codec.interface.js +3 -0
- package/lib/codecs/composer/parse.js +3 -0
- package/lib/codecs/composer/registry.js +13 -5
- package/lib/codecs/composer.codec.js +3 -0
- package/lib/codecs/go/parse.js +65 -0
- package/lib/codecs/go/registry.js +74 -0
- package/lib/codecs/go.codec.js +76 -0
- package/lib/codecs/index.js +7 -2
- package/lib/codecs/maven/jar-scan.js +199 -0
- package/lib/codecs/maven.codec.js +17 -1
- package/lib/codecs/npm/collect.js +37 -17
- package/lib/codecs/npm/parse.js +117 -10
- package/lib/codecs/npm/registry.js +14 -5
- package/lib/codecs/npm.codec.js +4 -1
- package/lib/codecs/nuget/parse.js +3 -0
- package/lib/codecs/nuget/registry.js +11 -4
- package/lib/codecs/nuget.codec.js +7 -2
- package/lib/codecs/pypi/parse.js +111 -14
- package/lib/codecs/pypi/registry.js +24 -5
- package/lib/codecs/pypi.codec.js +30 -6
- package/lib/codecs/recipes.js +28 -1
- package/lib/codecs/ruby/parse.js +42 -0
- package/lib/codecs/ruby/registry.js +76 -0
- package/lib/codecs/ruby.codec.js +66 -0
- package/lib/codecs/select.js +3 -0
- package/lib/codecs/yarn.codec.js +3 -0
- package/lib/config.js +3 -0
- package/lib/core.js +38 -1
- package/lib/cpe.js +30 -5
- package/lib/csaf-export.js +159 -0
- package/lib/cve-download.js +3 -0
- package/lib/cve-match.js +27 -6
- package/lib/cve-report.js +157 -28
- package/lib/dep-record.js +15 -2
- package/lib/deps-descriptor.js +113 -0
- package/lib/epss.js +115 -0
- package/lib/gate.js +45 -0
- package/lib/json-export.js +110 -0
- package/lib/kev.js +88 -0
- package/lib/license-policy.js +110 -0
- package/lib/maven-license.js +52 -0
- package/lib/maven-repo.js +3 -0
- package/lib/maven-version.js +8 -3
- package/lib/nvd.js +17 -6
- package/lib/osv.js +75 -23
- package/lib/outdated.js +10 -2
- package/lib/priority.js +90 -0
- package/lib/purl.js +77 -0
- package/lib/retire.js +80 -13
- package/lib/sarif-export.js +134 -0
- package/lib/sbom-export.js +153 -0
- package/lib/scan-completeness.js +3 -0
- package/lib/snyk.js +3 -0
- package/lib/suppress.js +113 -0
- package/lib/transitive.js +6 -3
- package/lib/ui.js +90 -0
- package/package.json +49 -2
- package/CLAUDE.md +0 -147
- package/docs/ARCHITECTURE.md +0 -182
- package/docs/USAGE.md +0 -187
- package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
- package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
- package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
- package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
- package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
- package/test/cli-ecosystem.test.js +0 -30
- package/test/codec-edge-cases.test.js +0 -131
- package/test/codec-integration.test.js +0 -119
- package/test/codecs.test.js +0 -92
- package/test/composer.test.js +0 -71
- package/test/core.test.js +0 -153
- package/test/cpe.test.js +0 -166
- package/test/cve-download.test.js +0 -39
- package/test/cve-match.test.js +0 -217
- package/test/cve-report.test.js +0 -180
- package/test/dep-record.test.js +0 -31
- package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
- package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
- package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
- package/test/fixtures/complex-enterprise/pom.xml +0 -66
- package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
- package/test/fixtures/csharp-config/packages.config +0 -4
- package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
- package/test/fixtures/csharp-csproj/app.csproj +0 -7
- package/test/fixtures/csharp-lock/packages.lock.json +0 -6
- package/test/fixtures/cve-samples/cve-non-java.json +0 -19
- package/test/fixtures/cve-samples/cve-product-only.json +0 -31
- package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
- package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
- package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
- package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
- package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
- package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
- package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
- package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
- package/test/fixtures/monorepo-mixed/pom.xml +0 -29
- package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
- package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
- package/test/fixtures/php-app/composer.json +0 -5
- package/test/fixtures/php-app/composer.lock +0 -10
- package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
- package/test/fixtures/polyglot/js/package-lock.json +0 -1
- package/test/fixtures/polyglot/js/package.json +0 -1
- package/test/fixtures/polyglot/mvn/pom.xml +0 -7
- package/test/fixtures/polyglot/php/composer.lock +0 -1
- package/test/fixtures/polyglot/py/poetry.lock +0 -3
- package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
- package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
- package/test/fixtures/private-lib-detection/pom.xml +0 -35
- package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
- package/test/fixtures/python-poetry/poetry.lock +0 -7
- package/test/fixtures/python-reqs/requirements.txt +0 -5
- package/test/fixtures/python-uv/uv.lock +0 -3
- package/test/fixtures/simple/app/pom.xml +0 -28
- package/test/fixtures/simple/lib/pom.xml +0 -18
- package/test/fixtures/simple/pom.xml +0 -24
- package/test/maven-repo.test.js +0 -111
- package/test/maven-version.test.js +0 -57
- package/test/monorepo.test.js +0 -132
- package/test/npm-registry.test.js +0 -64
- package/test/npm.test.js +0 -150
- package/test/nuget.test.js +0 -66
- package/test/osv.test.js +0 -62
- package/test/outdated.test.js +0 -101
- package/test/pypi.test.js +0 -72
- package/test/snyk.test.js +0 -64
- package/test/transitive.test.js +0 -305
- package/test/webjar.test.js +0 -33
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
* - lockfile entries WIN over package.json range specs (concrete version)
|
|
14
14
|
* - if two lockfiles disagree, keep the highest semver-comparable version
|
|
15
15
|
* - dev/optional/peer downgrade to prod if any manifest puts the dep in prod
|
|
16
|
+
*
|
|
17
|
+
* @author: N.BRAUN
|
|
18
|
+
* @email: pp9ping@gmail.com
|
|
16
19
|
*/
|
|
17
20
|
const path = require("path");
|
|
18
21
|
const fs = require("fs");
|
|
19
|
-
const { parsePackageJson, parsePackageLock, parseYarnLockV1, findJsManifests } = require("./parse");
|
|
22
|
+
const { parsePackageJson, parsePackageLock, parseYarnLockV1, parsePnpmLock, findJsManifests } = require("./parse");
|
|
20
23
|
const { makeDepRecord } = require("../../dep-record");
|
|
21
24
|
|
|
22
25
|
const SCOPE_RANK = { prod: 4, peer: 3, optional: 2, dev: 1, transitive: 0 };
|
|
@@ -91,6 +94,14 @@ function upsert(out, dep, manifestPath, lockType) {
|
|
|
91
94
|
} else if (!have && incoming) {
|
|
92
95
|
existing.version = incoming;
|
|
93
96
|
}
|
|
97
|
+
// Every DISTINCT concrete version is scanned, not just the highest: a JS tree
|
|
98
|
+
// routinely pins the same package at several versions (nested node_modules),
|
|
99
|
+
// and a CVE may affect only a lower one. cve-match.js / osv.js iterate
|
|
100
|
+
// dep.versions, so we union them here (makeDepRecord seeds `versions` with the
|
|
101
|
+
// record's own concrete version).
|
|
102
|
+
for (const v of record.versions || []) {
|
|
103
|
+
if (!existing.versions.includes(v)) existing.versions.push(v);
|
|
104
|
+
}
|
|
94
105
|
// Stronger scope wins (prod > peer > optional > dev)
|
|
95
106
|
if (rankScope(record.scope) > rankScope(existing.scope)) existing.scope = record.scope;
|
|
96
107
|
// A dep is "dev" overall only if every occurrence is dev. Any prod use → not dev.
|
|
@@ -133,7 +144,8 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
133
144
|
const pj = group.packageJson ? safeParse(parsePackageJson, group.packageJson, verbose) : null;
|
|
134
145
|
const pl = group.packageLock ? safeParse(parsePackageLock, group.packageLock, verbose) : null;
|
|
135
146
|
const yl = group.yarnLock ? safeParse(parseYarnLockV1, group.yarnLock, verbose) : null;
|
|
136
|
-
const
|
|
147
|
+
const pn = group.pnpmLock ? safeParse(parsePnpmLock, group.pnpmLock, verbose) : null;
|
|
148
|
+
const hasLock = !!(pl || yl || pn);
|
|
137
149
|
|
|
138
150
|
// package.json without lockfile → best-effort fallback (changed contract):
|
|
139
151
|
// scan pinned exact versions ("1.2.3"), skip ranges ("^1.0.0"), and warn
|
|
@@ -159,14 +171,14 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
159
171
|
if (verbose) console.warn(`⚠️ ${group.packageJson}: no lockfile — best-effort (${pinned} pinned, ${ranges} ranges skipped)`);
|
|
160
172
|
}
|
|
161
173
|
|
|
162
|
-
// Yarn-Berry lockfile
|
|
163
|
-
if (yl?.
|
|
174
|
+
// Yarn-Berry lockfile that failed to parse → warning (deps still empty).
|
|
175
|
+
if (yl?.parseError) {
|
|
164
176
|
warnings.push({
|
|
165
|
-
type: "
|
|
177
|
+
type: "parse-error",
|
|
166
178
|
manifestPath: group.yarnLock,
|
|
167
|
-
message: `yarn 2+/Berry lockfile
|
|
179
|
+
message: `yarn 2+/Berry lockfile parse failed: ${yl.parseError}`,
|
|
168
180
|
});
|
|
169
|
-
if (verbose) console.warn(`⚠️ ${group.yarnLock}: yarn-berry
|
|
181
|
+
if (verbose) console.warn(`⚠️ ${group.yarnLock}: yarn-berry parse failed — ${yl.parseError}`);
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
// Used only as a source of scope info — never emits deps on its own.
|
|
@@ -185,15 +197,23 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
185
197
|
}
|
|
186
198
|
if (yl) {
|
|
187
199
|
parsedCount++;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
const lockType = yl.lockfileVersion === "berry" ? "yarn-berry" : "yarn-v1";
|
|
201
|
+
for (const d of yl.deps) {
|
|
202
|
+
const explicit = directScopes?.get(d.name);
|
|
203
|
+
const scope = explicit || d.scope || "prod";
|
|
204
|
+
if (ignoreTest && (scope === "dev" || scope === "optional")) continue;
|
|
205
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
206
|
+
upsert(out, { ...d, scope }, group.yarnLock, lockType);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (pn) {
|
|
210
|
+
parsedCount++;
|
|
211
|
+
for (const d of pn.deps) {
|
|
212
|
+
const explicit = directScopes?.get(d.name);
|
|
213
|
+
const scope = explicit || d.scope || "prod";
|
|
214
|
+
if (ignoreTest && (scope === "dev" || scope === "optional")) continue;
|
|
215
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
216
|
+
upsert(out, { ...d, scope }, group.pnpmLock, "pnpm");
|
|
197
217
|
}
|
|
198
218
|
}
|
|
199
219
|
}
|
|
@@ -251,7 +271,7 @@ function hasJsManifests(rootDir) {
|
|
|
251
271
|
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
252
272
|
catch { continue; }
|
|
253
273
|
for (const e of entries) {
|
|
254
|
-
if (e.isFile() && (e.name === "package.json" || e.name === "package-lock.json" || e.name === "yarn.lock")) return true;
|
|
274
|
+
if (e.isFile() && (e.name === "package.json" || e.name === "package-lock.json" || e.name === "yarn.lock" || e.name === "pnpm-lock.yaml")) return true;
|
|
255
275
|
if (e.isDirectory() && !skip.has(e.name)) stack.push(path.join(cur, e.name));
|
|
256
276
|
}
|
|
257
277
|
}
|
package/lib/codecs/npm/parse.js
CHANGED
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
*
|
|
22
22
|
* The collector (lib/npm/collect.js) is responsible for merging across
|
|
23
23
|
* files and applying exclusion rules.
|
|
24
|
+
*
|
|
25
|
+
* @author: N.BRAUN
|
|
26
|
+
* @email: pp9ping@gmail.com
|
|
24
27
|
*/
|
|
25
28
|
const fs = require("fs");
|
|
26
29
|
const path = require("path");
|
|
30
|
+
const yaml = require("js-yaml");
|
|
27
31
|
|
|
28
32
|
function isWorkspaceVersion(v) {
|
|
29
33
|
// npm v9+ uses "*" / "" for workspace-local refs; yarn uses "workspace:*"
|
|
@@ -189,14 +193,8 @@ function parsePackageLock(filePath) {
|
|
|
189
193
|
function parseYarnLockV1(filePath) {
|
|
190
194
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
191
195
|
if (raw.includes("__metadata:")) {
|
|
192
|
-
// Berry / yarn 2+ uses YAML
|
|
193
|
-
return
|
|
194
|
-
manifestPath: filePath,
|
|
195
|
-
manifestType: "yarn.lock",
|
|
196
|
-
lockfileVersion: "berry",
|
|
197
|
-
deps: [],
|
|
198
|
-
unsupported: "yarn-berry",
|
|
199
|
-
};
|
|
196
|
+
// Berry / yarn 2+ uses YAML — parse it with js-yaml.
|
|
197
|
+
return parseYarnBerry(raw, filePath);
|
|
200
198
|
}
|
|
201
199
|
const out = {
|
|
202
200
|
manifestPath: filePath,
|
|
@@ -247,6 +245,112 @@ function parseYarnLockV1(filePath) {
|
|
|
247
245
|
return out;
|
|
248
246
|
}
|
|
249
247
|
|
|
248
|
+
/* -------- yarn.lock v2+ (Berry) ----------
|
|
249
|
+
Berry lockfiles are YAML. Top-level keys are comma-separated descriptor lists
|
|
250
|
+
("lodash@npm:^4.17.0, lodash@npm:^4.17.21:") whose value carries `version` and
|
|
251
|
+
`resolution`. The workspace's own packages resolve to "@workspace:" — skipped.
|
|
252
|
+
*/
|
|
253
|
+
function identFromDescriptor(d) {
|
|
254
|
+
const s = String(d || "").trim().replace(/^"|"$/g, "");
|
|
255
|
+
const scoped = s.startsWith("@");
|
|
256
|
+
const at = s.indexOf("@", scoped ? 1 : 0); // the @ before the range/protocol
|
|
257
|
+
if (at <= 0) return null;
|
|
258
|
+
return s.slice(0, at);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseYarnBerry(raw, filePath) {
|
|
262
|
+
const out = { manifestPath: filePath, manifestType: "yarn.lock", lockfileVersion: "berry", deps: [] };
|
|
263
|
+
let doc;
|
|
264
|
+
try { doc = yaml.load(raw) || {}; }
|
|
265
|
+
catch (e) { out.parseError = e.message; return out; }
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
for (const [key, val] of Object.entries(doc)) {
|
|
268
|
+
if (key === "__metadata") continue;
|
|
269
|
+
if (!val || typeof val !== "object" || !val.version) continue;
|
|
270
|
+
if (/@workspace:/.test(val.resolution || "")) continue; // the local package itself
|
|
271
|
+
const version = String(val.version);
|
|
272
|
+
const names = new Set();
|
|
273
|
+
for (const desc of String(key).split(",")) {
|
|
274
|
+
const name = identFromDescriptor(desc);
|
|
275
|
+
if (name) names.add(name);
|
|
276
|
+
}
|
|
277
|
+
for (const name of names) {
|
|
278
|
+
const k = `${name}@${version}`;
|
|
279
|
+
if (seen.has(k)) continue; seen.add(k);
|
|
280
|
+
out.deps.push({ name, version, scope: "prod", depth: 0, resolved: val.resolution || null });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* -------- pnpm-lock.yaml ----------
|
|
287
|
+
YAML. The full resolved set lives in `packages` (v5/v6) and `snapshots` (v9);
|
|
288
|
+
`importers.*` carry the per-workspace direct deps with dev classification.
|
|
289
|
+
Package keys vary by lockfileVersion:
|
|
290
|
+
v9: "name@1.2.3" / "@scope/name@1.2.3"
|
|
291
|
+
v6: "/name@1.2.3(peers)" / "/@scope/name@1.2.3(peers)"
|
|
292
|
+
v5: "/name/1.2.3" / "/@scope/name/1.2.3"
|
|
293
|
+
*/
|
|
294
|
+
function pnpmNameVersion(key) {
|
|
295
|
+
let k = String(key || "");
|
|
296
|
+
if (k.startsWith("/")) k = k.slice(1);
|
|
297
|
+
const paren = k.indexOf("("); // strip peer-deps suffix
|
|
298
|
+
if (paren !== -1) k = k.slice(0, paren);
|
|
299
|
+
const at = k.lastIndexOf("@"); // @-form (v6/v9)
|
|
300
|
+
if (at > 0 && /^\d/.test(k.slice(at + 1))) return { name: k.slice(0, at), version: k.slice(at + 1) };
|
|
301
|
+
const slash = k.lastIndexOf("/"); // slash-form (v5)
|
|
302
|
+
if (slash > 0 && /^\d/.test(k.slice(slash + 1))) return { name: k.slice(0, slash), version: k.slice(slash + 1) };
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parsePnpmLock(filePath) {
|
|
307
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
308
|
+
let doc;
|
|
309
|
+
try { doc = yaml.load(raw) || {}; }
|
|
310
|
+
catch (e) { throw new Error(`pnpm-lock parse failed (${filePath}): ${e.message}`); }
|
|
311
|
+
const out = { manifestPath: filePath, manifestType: "pnpm-lock", lockfileVersion: doc.lockfileVersion || null, deps: [] };
|
|
312
|
+
const seen = new Set();
|
|
313
|
+
const push = (name, version, scope, isDev) => {
|
|
314
|
+
if (!name || !version) return;
|
|
315
|
+
const v = String(version).split("(")[0]; // peer-resolved suffix
|
|
316
|
+
if (!/^\d/.test(v)) return;
|
|
317
|
+
const k = `${name}@${v}`;
|
|
318
|
+
if (seen.has(k)) return; seen.add(k);
|
|
319
|
+
out.deps.push({ name, version: v, scope: scope || "prod", isDev: !!isDev, depth: 0 });
|
|
320
|
+
};
|
|
321
|
+
// Direct deps from importers FIRST (v6/v9) — these carry the authoritative
|
|
322
|
+
// dev/optional classification. In v9 the `packages` section has no dev flag,
|
|
323
|
+
// so seeding dev/optional here (dedup by name@version) keeps the right scope.
|
|
324
|
+
const importers = doc.importers || ((doc.dependencies || doc.devDependencies) ? { ".": doc } : null);
|
|
325
|
+
if (importers && typeof importers === "object") {
|
|
326
|
+
const readDir = (obj, scope, isDev) => {
|
|
327
|
+
for (const [name, spec] of Object.entries(obj || {})) {
|
|
328
|
+
const version = typeof spec === "string" ? spec : (spec && (spec.version || spec.specifier));
|
|
329
|
+
push(name, version, scope, isDev);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
for (const imp of Object.values(importers)) {
|
|
333
|
+
if (!imp || typeof imp !== "object") continue;
|
|
334
|
+
readDir(imp.devDependencies, "dev", true);
|
|
335
|
+
readDir(imp.optionalDependencies, "optional", false);
|
|
336
|
+
readDir(imp.dependencies, "prod", false);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Full resolved set (direct + transitive) fills in anything not already seen.
|
|
340
|
+
for (const sec of [doc.packages, doc.snapshots]) {
|
|
341
|
+
if (!sec || typeof sec !== "object") continue;
|
|
342
|
+
for (const [key, entry] of Object.entries(sec)) {
|
|
343
|
+
const nv = pnpmNameVersion(key);
|
|
344
|
+
if (!nv) continue;
|
|
345
|
+
const name = (entry && entry.name) || nv.name;
|
|
346
|
+
const version = (entry && entry.version) || nv.version;
|
|
347
|
+
const isDev = !!(entry && entry.dev); // v6 carries a dev flag
|
|
348
|
+
push(name, version, isDev ? "dev" : "prod", isDev);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
250
354
|
/* -------- discovery ---------- */
|
|
251
355
|
// Dirs that hold packaged / generated content — never our own source.
|
|
252
356
|
// Conservative list: only well-known build-output / package-cache dirs.
|
|
@@ -266,7 +370,7 @@ function findJsManifests(rootDir, opts = {}) {
|
|
|
266
370
|
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
267
371
|
catch { continue; }
|
|
268
372
|
// Group lockfile per directory so we can prefer lock > package.json
|
|
269
|
-
const here = { dir: cur, packageJson: null, packageLock: null, yarnLock: null };
|
|
373
|
+
const here = { dir: cur, packageJson: null, packageLock: null, yarnLock: null, pnpmLock: null };
|
|
270
374
|
for (const e of entries) {
|
|
271
375
|
const p = path.join(cur, e.name);
|
|
272
376
|
if (e.isDirectory()) {
|
|
@@ -276,9 +380,10 @@ function findJsManifests(rootDir, opts = {}) {
|
|
|
276
380
|
if (e.name === "package.json") here.packageJson = p;
|
|
277
381
|
else if (e.name === "package-lock.json") here.packageLock = p;
|
|
278
382
|
else if (e.name === "yarn.lock") here.yarnLock = p;
|
|
383
|
+
else if (e.name === "pnpm-lock.yaml") here.pnpmLock = p;
|
|
279
384
|
}
|
|
280
385
|
}
|
|
281
|
-
if (here.packageJson || here.packageLock || here.yarnLock) found.push(here);
|
|
386
|
+
if (here.packageJson || here.packageLock || here.yarnLock || here.pnpmLock) found.push(here);
|
|
282
387
|
}
|
|
283
388
|
return found;
|
|
284
389
|
}
|
|
@@ -287,5 +392,7 @@ module.exports = {
|
|
|
287
392
|
parsePackageJson,
|
|
288
393
|
parsePackageLock,
|
|
289
394
|
parseYarnLockV1,
|
|
395
|
+
parseYarnBerry,
|
|
396
|
+
parsePnpmLock,
|
|
290
397
|
findJsManifests,
|
|
291
398
|
};
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* `packumentToFindings` is the pure extractor (unit-tested without network);
|
|
14
14
|
* `checkNpmRegistryDeps` is the cached, concurrent driver.
|
|
15
|
+
*
|
|
16
|
+
* @author: N.BRAUN
|
|
17
|
+
* @email: pp9ping@gmail.com
|
|
15
18
|
*/
|
|
16
19
|
const fs = require("fs");
|
|
17
20
|
const path = require("path");
|
|
@@ -49,10 +52,13 @@ function replacementFromMessage(msg) {
|
|
|
49
52
|
* Pure — no network, no cache. Either field is null when not applicable.
|
|
50
53
|
*/
|
|
51
54
|
function packumentToFindings(packument, dep) {
|
|
52
|
-
const out = { deprecated: null, outdated: null };
|
|
55
|
+
const out = { deprecated: null, outdated: null, license: null };
|
|
53
56
|
if (!packument || typeof packument !== "object") return out;
|
|
54
57
|
|
|
55
58
|
const versionEntry = packument.versions?.[dep.version];
|
|
59
|
+
// License: prefer the resolved version's field, fall back to the packument root.
|
|
60
|
+
const lic = versionEntry?.license ?? versionEntry?.licenses ?? packument.license ?? packument.licenses;
|
|
61
|
+
if (lic) out.license = lic;
|
|
56
62
|
const depMsg = versionEntry && typeof versionEntry.deprecated === "string"
|
|
57
63
|
? versionEntry.deprecated.trim()
|
|
58
64
|
: "";
|
|
@@ -118,7 +124,7 @@ async function fetchPackument(name, opts = {}) {
|
|
|
118
124
|
* opts: { verbose, offline, allLibs, concurrency = 8 }
|
|
119
125
|
*/
|
|
120
126
|
async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
121
|
-
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
127
|
+
const { verbose, offline, allLibs = true, concurrency = 8, onProgress } = opts;
|
|
122
128
|
// Targets = npm deps (queried by their own name) + WebJars (queried by their
|
|
123
129
|
// derived npm-equivalent name; version matches upstream). The original dep
|
|
124
130
|
// is kept for display/results so the report shows e.g. org.webjars:angularjs.
|
|
@@ -129,7 +135,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
129
135
|
const wj = webjarToNpm(d);
|
|
130
136
|
if (wj?.name) targets.push({ dep: d, npmName: wj.name, version: d.version });
|
|
131
137
|
}
|
|
132
|
-
const result = { deprecated: [], outdated: [] };
|
|
138
|
+
const result = { deprecated: [], outdated: [], licensed: [] };
|
|
133
139
|
if (!targets.length) return result;
|
|
134
140
|
|
|
135
141
|
const cache = loadCache();
|
|
@@ -138,7 +144,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
138
144
|
|
|
139
145
|
const cacheKey = t => `${t.npmName}@${t.version}`;
|
|
140
146
|
const liveCount = offline ? 0 : targets.filter(t => !cache.entries[cacheKey(t)]).length;
|
|
141
|
-
if (liveCount && !offline) {
|
|
147
|
+
if (liveCount && !offline && !onProgress) {
|
|
142
148
|
console.log(`📦 npm registry: checking ${targets.length} packages for deprecation${allLibs ? " + outdated" : ""} (${liveCount} live, ${targets.length - liveCount} cached)…`);
|
|
143
149
|
}
|
|
144
150
|
|
|
@@ -147,6 +153,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
147
153
|
let processed = 0;
|
|
148
154
|
const startedAt = Date.now();
|
|
149
155
|
const printProgress = (final = false) => {
|
|
156
|
+
if (onProgress) { onProgress(processed, targets.length); return; }
|
|
150
157
|
if (!liveCount) return;
|
|
151
158
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
152
159
|
const pct = Math.round((processed / targets.length) * 100);
|
|
@@ -168,6 +175,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
168
175
|
deprecated: f.deprecated ? { reason: f.deprecated.reason, replacement: f.deprecated.replacement } : null,
|
|
169
176
|
latest: f.outdated ? f.outdated.latest : null,
|
|
170
177
|
latestDate: f.outdated ? f.outdated.releaseDate : null,
|
|
178
|
+
license: f.license || null,
|
|
171
179
|
};
|
|
172
180
|
cache.entries[key] = extracted;
|
|
173
181
|
} else {
|
|
@@ -191,6 +199,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
191
199
|
result.outdated.push({ dep, latest: extracted.latest, releaseDate: extracted.latestDate || null });
|
|
192
200
|
if (verbose) process.stdout.write(` outdated: ${npmName} ${version} → ${extracted.latest}\n`);
|
|
193
201
|
}
|
|
202
|
+
if (extracted.license) result.licensed.push({ dep, licenses: extracted.license, source: "npm" });
|
|
194
203
|
})));
|
|
195
204
|
if (liveCount) printProgress(true);
|
|
196
205
|
|
|
@@ -199,7 +208,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
199
208
|
|
|
200
209
|
result.deprecated.sort((a, b) => a.dep.artifactId.localeCompare(b.dep.artifactId));
|
|
201
210
|
result.outdated.sort((a, b) => a.dep.artifactId.localeCompare(b.dep.artifactId));
|
|
202
|
-
if (liveCount && !offline) console.log(` npm registry: ${result.deprecated.length} deprecated, ${result.outdated.length} outdated`);
|
|
211
|
+
if (liveCount && !offline && !onProgress) console.log(` npm registry: ${result.deprecated.length} deprecated, ${result.outdated.length} outdated`);
|
|
203
212
|
return result;
|
|
204
213
|
}
|
|
205
214
|
|
package/lib/codecs/npm.codec.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* npm.collect ramasse package-lock ET yarn.lock — chaque dep porte son
|
|
9
9
|
* `ecosystemType` ("npm" | "yarn"). Le codec yarn (yarn.codec.js) n'existe que
|
|
10
10
|
* pour fournir son label/recette au report ; il ne re-scanne pas.
|
|
11
|
+
*
|
|
12
|
+
* @author: N.BRAUN
|
|
13
|
+
* @email: pp9ping@gmail.com
|
|
11
14
|
*/
|
|
12
15
|
const { collectNpmDeps, hasJsManifests } = require("./npm/collect");
|
|
13
16
|
const { coordKeyFor } = require("../dep-record");
|
|
@@ -25,7 +28,7 @@ const retireScanner = {
|
|
|
25
28
|
|
|
26
29
|
const base = {
|
|
27
30
|
osvEcosystem: "npm",
|
|
28
|
-
manifestNames: ["package.json", "package-lock.json", "yarn.lock"],
|
|
31
|
+
manifestNames: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
29
32
|
detect(dir) { return hasJsManifests(dir); },
|
|
30
33
|
coordKey(d) { return coordKeyFor("npm", "", d.name || d.artifactId); },
|
|
31
34
|
formatCoord(d) { return d.name || d.artifactId; },
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* packages.config — XML legacy, <package id version targetFramework />.
|
|
10
10
|
*
|
|
11
11
|
* NuGet ids are case-insensitive (lowercased for the key; original case kept for display).
|
|
12
|
+
*
|
|
13
|
+
* @author: N.BRAUN
|
|
14
|
+
* @email: pp9ping@gmail.com
|
|
12
15
|
*/
|
|
13
16
|
const fs = require("fs");
|
|
14
17
|
const xml2js = require("xml2js");
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* → { items: [ { items: [ { catalogEntry: { version, deprecation } } ] } ] }
|
|
7
7
|
*
|
|
8
8
|
* Cached in ~/.fad-checker/nuget-cache.json for 24h.
|
|
9
|
+
*
|
|
10
|
+
* @author: N.BRAUN
|
|
11
|
+
* @email: pp9ping@gmail.com
|
|
9
12
|
*/
|
|
10
13
|
const fs = require("fs");
|
|
11
14
|
const path = require("path");
|
|
@@ -31,11 +34,14 @@ function cmp(a, b) {
|
|
|
31
34
|
function nugetRegistrationToFindings(reg, { version }) {
|
|
32
35
|
const entries = [];
|
|
33
36
|
for (const page of reg.items || []) for (const leaf of page.items || []) if (leaf.catalogEntry) entries.push(leaf.catalogEntry);
|
|
34
|
-
const out = { outdated: null, deprecated: null };
|
|
37
|
+
const out = { outdated: null, deprecated: null, license: null };
|
|
35
38
|
const stable = entries.map(e => e.version).filter(isStable);
|
|
36
39
|
if (stable.length) { const latest = stable.sort(cmp).at(-1); if (latest && cmp(latest, version) > 0) out.outdated = { latest }; }
|
|
37
40
|
const mine = entries.find(e => String(e.version) === String(version));
|
|
38
41
|
if (mine?.deprecation) out.deprecated = { reason: (mine.deprecation.reasons || []).join(", ") || "deprecated", replacement: mine.deprecation.alternatePackage?.id || null };
|
|
42
|
+
// Modern packages expose an SPDX licenseExpression; older ones only a licenseUrl (→ unknown).
|
|
43
|
+
const licEntry = mine || entries.at(-1);
|
|
44
|
+
if (licEntry?.licenseExpression) out.license = licEntry.licenseExpression;
|
|
39
45
|
return out;
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -52,7 +58,7 @@ async function fetchRegistration(name, { offline }) {
|
|
|
52
58
|
async function checkNugetRegistryDeps(deps, opts = {}) {
|
|
53
59
|
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
54
60
|
const targets = [...deps.values()].filter(d => d.ecosystem === "nuget" && d.version);
|
|
55
|
-
const result = { deprecated: [], outdated: [] };
|
|
61
|
+
const result = { deprecated: [], outdated: [], licensed: [] };
|
|
56
62
|
if (!targets.length) return result;
|
|
57
63
|
const cache = loadCache();
|
|
58
64
|
const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < CACHE_MAX_AGE_MS;
|
|
@@ -65,12 +71,13 @@ async function checkNugetRegistryDeps(deps, opts = {}) {
|
|
|
65
71
|
const reg = await fetchRegistration(t.name, { offline });
|
|
66
72
|
if (reg && !reg.error) {
|
|
67
73
|
const f = nugetRegistrationToFindings(reg, { version: t.version });
|
|
68
|
-
ex = { deprecated: f.deprecated, latest: f.outdated?.latest || null };
|
|
74
|
+
ex = { deprecated: f.deprecated, latest: f.outdated?.latest || null, license: f.license || null };
|
|
69
75
|
cache.entries[key] = ex;
|
|
70
76
|
} else {
|
|
71
|
-
ex = { deprecated: null, latest: null };
|
|
77
|
+
ex = { deprecated: null, latest: null, license: null };
|
|
72
78
|
}
|
|
73
79
|
}
|
|
80
|
+
if (ex.license) result.licensed.push({ dep: t, licenses: ex.license, source: "nuget" });
|
|
74
81
|
if (ex.deprecated) {
|
|
75
82
|
result.deprecated.push({ dep: t, severity: "MEDIUM", replacement: ex.deprecated.replacement, reason: ex.deprecated.reason, source: "nuget" });
|
|
76
83
|
if (verbose) process.stdout.write(` deprecated: ${t.name}@${t.version}\n`);
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* else packages.config), NuGet registration registry (deprecation + outdated),
|
|
7
7
|
* and EOL. NuGet ids are case-insensitive: the key is lowercased, dep.name keeps
|
|
8
8
|
* the original casing for display / OSV.
|
|
9
|
+
*
|
|
10
|
+
* @author: N.BRAUN
|
|
11
|
+
* @email: pp9ping@gmail.com
|
|
9
12
|
*/
|
|
10
13
|
const fs = require("fs");
|
|
11
14
|
const path = require("path");
|
|
@@ -13,6 +16,8 @@ const { makeDepRecord, coordKeyFor } = require("../dep-record");
|
|
|
13
16
|
const N = require("./nuget/parse");
|
|
14
17
|
|
|
15
18
|
const SKIP = new Set([".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "bin", "obj", "target", "packages"]);
|
|
19
|
+
// MSBuild project files share the same <PackageReference> schema — C#, F#, VB.
|
|
20
|
+
const PROJ_RE = /\.(csproj|fsproj|vbproj)$/i;
|
|
16
21
|
|
|
17
22
|
function findNugetDirs(dir) {
|
|
18
23
|
const groups = [];
|
|
@@ -21,7 +26,7 @@ function findNugetDirs(dir) {
|
|
|
21
26
|
const cur = stack.pop();
|
|
22
27
|
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
23
28
|
const files = entries.filter(e => e.isFile()).map(e => e.name);
|
|
24
|
-
const csprojs = files.filter(f =>
|
|
29
|
+
const csprojs = files.filter(f => PROJ_RE.test(f));
|
|
25
30
|
if (files.includes("packages.lock.json") || files.includes("packages.config") || csprojs.length) {
|
|
26
31
|
groups.push({ dir: cur, files, csprojs });
|
|
27
32
|
}
|
|
@@ -34,7 +39,7 @@ module.exports = {
|
|
|
34
39
|
id: "nuget",
|
|
35
40
|
label: "NuGet",
|
|
36
41
|
osvEcosystem: "NuGet",
|
|
37
|
-
manifestNames: ["packages.lock.json", "*.csproj", "packages.config"],
|
|
42
|
+
manifestNames: ["packages.lock.json", "*.csproj", "*.fsproj", "*.vbproj", "packages.config"],
|
|
38
43
|
|
|
39
44
|
detect(dir) { return findNugetDirs(dir).length > 0; },
|
|
40
45
|
|
package/lib/codecs/pypi/parse.js
CHANGED
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* poetry.lock / uv.lock / pdm.lock — TOML, [[package]] name/version arrays.
|
|
5
5
|
* Pipfile.lock — JSON, { default:{}, develop:{} }, version "==x".
|
|
6
|
+
* pyproject.toml — TOML, PEP 621 [project] + poetry table (fallback).
|
|
6
7
|
* requirements.txt — text; only "==" pins are queryable (fallback).
|
|
8
|
+
* -r/-c includes are followed recursively;
|
|
9
|
+
* a -c constraint pin upgrades a range to scannable.
|
|
7
10
|
*
|
|
8
11
|
* All names are PEP 503 normalized (lowercase, runs of -_. → single -) so they
|
|
9
12
|
* match OSV / PyPI / the resolved-deps key.
|
|
13
|
+
*
|
|
14
|
+
* @author: N.BRAUN
|
|
15
|
+
* @email: pp9ping@gmail.com
|
|
10
16
|
*/
|
|
11
17
|
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
12
19
|
const TOML = require("smol-toml");
|
|
13
20
|
|
|
14
21
|
function pep503(name) { return String(name || "").toLowerCase().replace(/[-_.]+/g, "-"); }
|
|
@@ -17,6 +24,15 @@ function stripOp(v) { return String(v || "").replace(/^[=~!<>]+/, "").trim(); }
|
|
|
17
24
|
function isPinned(spec) { return /^==\s*\d[\w.\-+!]*$/.test(String(spec || "").trim()); }
|
|
18
25
|
function isConcrete(v) { return /^\d+(\.\d+)*([.\-+]\S+)?$/.test(String(v || "")); }
|
|
19
26
|
|
|
27
|
+
// Split a PEP 508 requirement string ("requests[extra]==2.31.0 ; marker") into
|
|
28
|
+
// { name, spec } — env markers (after ";") and extras ("[...]") are dropped.
|
|
29
|
+
function splitPep508(req) {
|
|
30
|
+
const s = String(req || "").split(";")[0].trim();
|
|
31
|
+
const m = s.match(/^([A-Za-z0-9._-]+)\s*(\[[^\]]*\])?\s*(.*)$/);
|
|
32
|
+
if (!m) return { name: null, spec: "" };
|
|
33
|
+
return { name: m[1], spec: m[3].trim() };
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
// poetry.lock / uv.lock / pdm.lock all use [[package]] name/version arrays.
|
|
21
37
|
function parseTomlPackages(filePath, type) {
|
|
22
38
|
const data = TOML.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -24,9 +40,11 @@ function parseTomlPackages(filePath, type) {
|
|
|
24
40
|
const deps = [];
|
|
25
41
|
for (const p of pkgs) {
|
|
26
42
|
if (!p.name || !p.version) continue;
|
|
27
|
-
// pdm
|
|
43
|
+
// pdm/poetry≥1.5 use `groups`; classic poetry.lock (≤1.4) uses `category = "dev"`.
|
|
28
44
|
const groups = Array.isArray(p.groups) ? p.groups : null;
|
|
29
|
-
const isDev = groups
|
|
45
|
+
const isDev = groups
|
|
46
|
+
? groups.every(g => g === "dev")
|
|
47
|
+
: (p.category ? String(p.category).toLowerCase() === "dev" : false);
|
|
30
48
|
deps.push({ name: pep503(p.name), version: String(p.version), scope: isDev ? "dev" : "prod", isDev });
|
|
31
49
|
}
|
|
32
50
|
return { manifestPath: filePath, manifestType: type, deps };
|
|
@@ -50,22 +68,101 @@ function parsePipfileLock(filePath) {
|
|
|
50
68
|
return { manifestPath: filePath, manifestType: "Pipfile.lock", deps };
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
// pyproject.toml — fallback when no lockfile. Reads PEP 621 [project] and the
|
|
72
|
+
// poetry tool table. Only exact pins are scannable (PEP 621 "==x"; poetry exact
|
|
73
|
+
// "x"); ranges (^/~/>=/wildcards) are counted in `skipped` like requirements.txt.
|
|
74
|
+
function parsePyprojectToml(filePath) {
|
|
75
|
+
const data = TOML.parse(fs.readFileSync(filePath, "utf8"));
|
|
55
76
|
const deps = [];
|
|
56
77
|
let skipped = 0;
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const push = (rawName, version, scope) => {
|
|
80
|
+
const name = pep503(rawName);
|
|
81
|
+
if (!name || name === "python") return;
|
|
82
|
+
const k = `${name}@${version}`;
|
|
83
|
+
if (seen.has(k)) return; seen.add(k);
|
|
84
|
+
deps.push({ name, version, scope, isDev: scope === "dev" });
|
|
85
|
+
};
|
|
86
|
+
// PEP 508 specs ("requests==2.31.0") — exact "==" pins only.
|
|
87
|
+
const addSpec = (req, scope) => {
|
|
88
|
+
const { name, spec } = splitPep508(req);
|
|
89
|
+
if (!name) return;
|
|
90
|
+
if (isPinned(spec)) push(name, stripOp(spec), scope);
|
|
91
|
+
else skipped++;
|
|
92
|
+
};
|
|
93
|
+
// Poetry constraints ("^2.0" / "==4.2" / bare exact "4.2.0" / { version }).
|
|
94
|
+
const addPoetry = (name, val, scope) => {
|
|
95
|
+
if (pep503(name) === "python") return; // the interpreter, not a dep
|
|
96
|
+
const spec = typeof val === "string" ? val : (val && typeof val === "object" ? (val.version || "") : "");
|
|
97
|
+
const s = String(spec || "").trim();
|
|
98
|
+
if (isConcrete(s)) push(name, s, scope); // poetry: bare version == exact
|
|
99
|
+
else if (isPinned(s)) push(name, stripOp(s), scope);
|
|
100
|
+
else skipped++;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// PEP 621 [project]
|
|
104
|
+
const proj = data.project || {};
|
|
105
|
+
for (const req of (Array.isArray(proj.dependencies) ? proj.dependencies : [])) addSpec(req, "prod");
|
|
106
|
+
for (const [group, arr] of Object.entries(proj["optional-dependencies"] || {})) {
|
|
107
|
+
const isDev = /^(dev|test|tests|lint|docs|typing|type|check)$/i.test(group);
|
|
108
|
+
for (const req of (Array.isArray(arr) ? arr : [])) addSpec(req, isDev ? "dev" : "prod");
|
|
109
|
+
}
|
|
110
|
+
// Poetry [tool.poetry]
|
|
111
|
+
const poetry = (data.tool && data.tool.poetry) || {};
|
|
112
|
+
for (const [name, v] of Object.entries(poetry.dependencies || {})) addPoetry(name, v, "prod");
|
|
113
|
+
for (const [name, v] of Object.entries(poetry["dev-dependencies"] || {})) addPoetry(name, v, "dev");
|
|
114
|
+
for (const grp of Object.values(poetry.group || {})) {
|
|
115
|
+
for (const [name, v] of Object.entries((grp && grp.dependencies) || {})) addPoetry(name, v, "dev");
|
|
116
|
+
}
|
|
117
|
+
return { manifestPath: filePath, manifestType: "pyproject.toml", deps, skipped };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Walk a requirements.txt graph, following -r/--requirement (deps) and
|
|
121
|
+
// -c/--constraint (version pins) includes recursively. `kind` is "req" for the
|
|
122
|
+
// dependency graph and "constraint" for files reached via -c.
|
|
123
|
+
function collectReqGraph(filePath, seen, kind, acc) {
|
|
124
|
+
const real = path.resolve(filePath);
|
|
125
|
+
if (seen.has(real)) return; // cycle / re-include guard
|
|
126
|
+
seen.add(real);
|
|
127
|
+
if (seen.size > 200) return; // sanity bound
|
|
128
|
+
let lines;
|
|
129
|
+
try { lines = fs.readFileSync(real, "utf8").split(/\r?\n/); }
|
|
130
|
+
catch { acc.missing.push(real); return; }
|
|
131
|
+
const dir = path.dirname(real);
|
|
57
132
|
for (const raw of lines) {
|
|
58
|
-
const line = raw.replace(
|
|
133
|
+
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
59
134
|
if (!line) continue;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
135
|
+
const rMatch = line.match(/^(?:-r|--requirement)[=\s]+(.+)$/);
|
|
136
|
+
if (rMatch) { collectReqGraph(path.resolve(dir, rMatch[1].trim()), seen, kind, acc); continue; }
|
|
137
|
+
const cMatch = line.match(/^(?:-c|--constraint)[=\s]+(.+)$/);
|
|
138
|
+
if (cMatch) { collectReqGraph(path.resolve(dir, cMatch[1].trim()), seen, "constraint", acc); continue; }
|
|
139
|
+
if (line.startsWith("-")) continue; // -e ., --hash, other flags
|
|
140
|
+
const { name, spec } = splitPep508(line);
|
|
141
|
+
if (!name) continue;
|
|
142
|
+
const n = pep503(name);
|
|
143
|
+
if (kind === "constraint") acc.constraints.set(n, spec);
|
|
144
|
+
else acc.reqs.push({ name: n, spec });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseRequirementsTxt(filePath) {
|
|
149
|
+
const acc = { reqs: [], constraints: new Map(), missing: [] };
|
|
150
|
+
collectReqGraph(filePath, new Set(), "req", acc);
|
|
151
|
+
const deps = [];
|
|
152
|
+
let skipped = 0;
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
for (const r of acc.reqs) {
|
|
155
|
+
let version = null;
|
|
156
|
+
if (isPinned(r.spec)) version = stripOp(r.spec);
|
|
157
|
+
else if (acc.constraints.has(r.name) && isPinned(acc.constraints.get(r.name))) {
|
|
158
|
+
version = stripOp(acc.constraints.get(r.name)); // range pinned by -c constraint
|
|
159
|
+
}
|
|
160
|
+
if (!version) { skipped++; continue; }
|
|
161
|
+
const k = `${r.name}@${version}`;
|
|
162
|
+
if (seen.has(k)) continue; seen.add(k);
|
|
163
|
+
deps.push({ name: r.name, version, scope: "prod", isDev: false });
|
|
67
164
|
}
|
|
68
|
-
return { manifestPath: filePath, manifestType: "requirements.txt", deps, skipped };
|
|
165
|
+
return { manifestPath: filePath, manifestType: "requirements.txt", deps, skipped, missing: acc.missing };
|
|
69
166
|
}
|
|
70
167
|
|
|
71
|
-
module.exports = { pep503, isConcrete, parsePoetryLock, parseUvLock, parsePdmLock, parsePipfileLock, parseRequirementsTxt };
|
|
168
|
+
module.exports = { pep503, isConcrete, splitPep508, parsePoetryLock, parseUvLock, parsePdmLock, parsePipfileLock, parsePyprojectToml, parseRequirementsTxt };
|