fad-checker 2.1.2 → 2.2.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.
- package/CHANGELOG.md +37 -0
- package/README.md +75 -87
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +1 -0
- package/fad-checker.js +211 -49
- package/lib/codecs/binary/scan.js +71 -0
- package/lib/codecs/binary/sniff.js +37 -0
- package/lib/codecs/binary.codec.js +54 -0
- package/lib/codecs/composer.codec.js +7 -3
- package/lib/codecs/go/registry.js +19 -11
- package/lib/codecs/go.codec.js +7 -3
- package/lib/codecs/index.js +9 -4
- package/lib/codecs/maven/jar-scan.js +8 -3
- package/lib/codecs/maven.codec.js +4 -2
- package/lib/codecs/npm/parse.js +5 -2
- package/lib/codecs/npm/registry.js +29 -18
- package/lib/codecs/npm.codec.js +7 -5
- package/lib/codecs/nuget.codec.js +7 -3
- package/lib/codecs/pypi/registry.js +16 -10
- package/lib/codecs/pypi.codec.js +7 -3
- package/lib/codecs/recipes.js +9 -1
- package/lib/codecs/ruby/registry.js +16 -10
- package/lib/codecs/ruby.codec.js +7 -3
- package/lib/config.js +34 -23
- package/lib/core.js +9 -5
- package/lib/cve-match.js +3 -2
- package/lib/cve-report.js +121 -25
- package/lib/dep-record.js +12 -9
- package/lib/embedded.js +57 -0
- package/lib/hash-id.js +78 -0
- package/lib/json-export.js +15 -2
- package/lib/options-env.js +113 -0
- package/lib/outdated.js +19 -7
- package/lib/parallel-walk.js +5 -2
- package/lib/path-filter.js +58 -0
- package/lib/registries.js +112 -0
- package/lib/retire.js +158 -7
- package/lib/scan-completeness.js +22 -7
- package/lib/ui.js +4 -3
- package/lib/unmanaged.js +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/binary.codec.js — codec for committed NATIVE binaries
|
|
3
|
+
* (.dll/.exe/.so/.dylib) that no package manager governs.
|
|
4
|
+
*
|
|
5
|
+
* Plan 1 scope: discover + hash only. The records carry no resolved coordinate
|
|
6
|
+
* (just a filename); Plan 2's hash-id service fills `identity`, Plan 3 builds the
|
|
7
|
+
* unmanaged inventory + report. Until then the records are `provenance:"binary"`
|
|
8
|
+
* and the CVE/OSV/EOL/outdated stages skip them (no coordinate to query).
|
|
9
|
+
*
|
|
10
|
+
* @author: N.BRAUN
|
|
11
|
+
* @email: pp9ping@gmail.com
|
|
12
|
+
*/
|
|
13
|
+
const { makeDepRecord } = require("../dep-record");
|
|
14
|
+
const { scanBinaries } = require("./binary/scan");
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
id: "binary",
|
|
18
|
+
label: "Binaries",
|
|
19
|
+
osvEcosystem: null, // no OSV ecosystem until identified
|
|
20
|
+
manifestNames: ["*.dll", "*.exe", "*.so", "*.dylib"], // lets detectCodecs include us in auto mode
|
|
21
|
+
|
|
22
|
+
detect(dir) {
|
|
23
|
+
try { return scanBinaries(dir, { onProgress: null }).length > 0; }
|
|
24
|
+
catch { return false; }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async collect(dir, opts = {}) {
|
|
28
|
+
const out = new Map();
|
|
29
|
+
const warnings = [];
|
|
30
|
+
let records;
|
|
31
|
+
try { records = scanBinaries(dir, { onProgress: opts.onBinaryProgress, srcRoot: opts.srcRoot || dir, excludePath: opts.excludePath, defaultExcludes: opts.defaultExcludes }); }
|
|
32
|
+
catch (e) { return { deps: out, warnings: [{ type: "scan-error", message: `binary scan failed: ${e.message}` }] }; }
|
|
33
|
+
for (const r of records) {
|
|
34
|
+
out.set(`binary:${r.path}`, makeDepRecord({
|
|
35
|
+
ecosystem: "binary",
|
|
36
|
+
name: r.declaredName,
|
|
37
|
+
version: null,
|
|
38
|
+
manifestPath: r.path,
|
|
39
|
+
provenance: "binary",
|
|
40
|
+
hashes: { sha1: r.sha1, sha256: r.sha256 },
|
|
41
|
+
declaredName: r.declaredName,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
return { deps: out, warnings };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
coordKey(d) { return `binary:${d.manifestPaths?.[0] || d.name}`; },
|
|
48
|
+
formatCoord(d) { return d.declaredName || d.name; },
|
|
49
|
+
osvPackageName() { return null; },
|
|
50
|
+
async checkRegistry() { return { deprecated: [], outdated: [], licensed: [] }; },
|
|
51
|
+
resolveEolProduct() { return null; },
|
|
52
|
+
recipe: require("./recipes").binary,
|
|
53
|
+
nativeScanners: [],
|
|
54
|
+
};
|
|
@@ -15,7 +15,7 @@ const { parseComposerLock, parseComposerJson, isConcrete } = require("./composer
|
|
|
15
15
|
|
|
16
16
|
const SKIP = new Set(["vendor", ".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "target"]);
|
|
17
17
|
|
|
18
|
-
function findComposerManifests(dir) {
|
|
18
|
+
function findComposerManifests(dir, skipDir = (child, name) => SKIP.has(name)) {
|
|
19
19
|
const groups = [];
|
|
20
20
|
const stack = [dir];
|
|
21
21
|
while (stack.length) {
|
|
@@ -29,11 +29,15 @@ function findComposerManifests(dir) {
|
|
|
29
29
|
composerLock: names.has("composer.lock") ? path.join(cur, "composer.lock") : null,
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
for (const e of entries) if (e.isDirectory() && !
|
|
32
|
+
for (const e of entries) if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
33
33
|
}
|
|
34
34
|
return groups;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function dirFilter(dir, opts) {
|
|
38
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
module.exports = {
|
|
38
42
|
id: "composer",
|
|
39
43
|
label: "Composer",
|
|
@@ -46,7 +50,7 @@ module.exports = {
|
|
|
46
50
|
const { ignoreTest, deps2Exclude } = opts;
|
|
47
51
|
const out = new Map();
|
|
48
52
|
const warnings = [];
|
|
49
|
-
for (const g of findComposerManifests(dir)) {
|
|
53
|
+
for (const g of findComposerManifests(dir, dirFilter(dir, opts))) {
|
|
50
54
|
if (g.composerLock) {
|
|
51
55
|
let parsed;
|
|
52
56
|
try { parsed = parseComposerLock(g.composerLock); }
|
|
@@ -13,11 +13,11 @@ const fs = require("fs");
|
|
|
13
13
|
const path = require("path");
|
|
14
14
|
const os = require("os");
|
|
15
15
|
let pLimit; try { pLimit = require("p-limit"); } catch { pLimit = () => (fn) => fn(); }
|
|
16
|
+
const { withPublic, authHeaderFor, normalise } = require("../../registries");
|
|
16
17
|
|
|
17
18
|
const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
18
19
|
const CACHE_PATH = path.join(CACHE_DIR, "go-proxy-cache.json");
|
|
19
20
|
const CACHE_MAX_AGE_MS = 24 * 3600 * 1000;
|
|
20
|
-
const PROXY = "https://proxy.golang.org";
|
|
21
21
|
|
|
22
22
|
function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); } catch { return { entries: {}, meta: {} }; } }
|
|
23
23
|
function saveCache(d) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify(d)); } catch { /* ignore */ } }
|
|
@@ -34,18 +34,26 @@ function cmp(a, b) {
|
|
|
34
34
|
return 0;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Custom GOPROXY first (same protocol: <base>/<escaped-module>/@latest), then proxy.golang.org.
|
|
38
|
+
async function fetchLatest(mod, { offline, registries = [], fetcher = globalThis.fetch } = {}) {
|
|
38
39
|
if (offline) return null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const bases = withPublic("go", registries);
|
|
41
|
+
let lastErr = null;
|
|
42
|
+
for (const base of bases) {
|
|
43
|
+
const url = normalise(base.url) + `${escapeModule(mod)}/@latest`;
|
|
44
|
+
const headers = { "User-Agent": "fad-checker-go", Accept: "application/json" };
|
|
45
|
+
const ah = authHeaderFor(base); if (ah) headers.Authorization = ah;
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetcher(url, { headers });
|
|
48
|
+
if (res.ok) { const j = await res.json(); return { latest: (j.Version || "").replace(/^v/, "") || null }; }
|
|
49
|
+
lastErr = `HTTP ${res.status}`;
|
|
50
|
+
} catch (e) { lastErr = e.message; }
|
|
51
|
+
}
|
|
52
|
+
return { error: lastErr || "no data" };
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
async function checkGoRegistryDeps(deps, opts = {}) {
|
|
48
|
-
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
56
|
+
const { verbose, offline, allLibs = true, concurrency = 8, registries = [] } = opts;
|
|
49
57
|
const targets = [...deps.values()].filter(d => d.ecosystem === "go" && d.version);
|
|
50
58
|
const result = { deprecated: [], outdated: [], licensed: [] };
|
|
51
59
|
if (!targets.length || !allLibs) return result;
|
|
@@ -57,7 +65,7 @@ async function checkGoRegistryDeps(deps, opts = {}) {
|
|
|
57
65
|
const key = `${t.name}@${t.version}`;
|
|
58
66
|
let ex = cache.entries[key];
|
|
59
67
|
if (!ex) {
|
|
60
|
-
const r = await fetchLatest(t.name, { offline });
|
|
68
|
+
const r = await fetchLatest(t.name, { offline, registries });
|
|
61
69
|
ex = (r && !r.error) ? { latest: r.latest } : { latest: null };
|
|
62
70
|
if (r && !r.error) cache.entries[key] = ex;
|
|
63
71
|
}
|
|
@@ -71,4 +79,4 @@ async function checkGoRegistryDeps(deps, opts = {}) {
|
|
|
71
79
|
return result;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
module.exports = { checkGoRegistryDeps, escapeModule };
|
|
82
|
+
module.exports = { checkGoRegistryDeps, escapeModule, fetchLatest };
|
package/lib/codecs/go.codec.js
CHANGED
|
@@ -15,7 +15,7 @@ const G = require("./go/parse");
|
|
|
15
15
|
|
|
16
16
|
const SKIP = new Set([".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "target", "vendor", "testdata"]);
|
|
17
17
|
|
|
18
|
-
function findGoDirs(dir) {
|
|
18
|
+
function findGoDirs(dir, skipDir = (child, name) => SKIP.has(name)) {
|
|
19
19
|
const groups = [];
|
|
20
20
|
const stack = [dir];
|
|
21
21
|
while (stack.length) {
|
|
@@ -23,11 +23,15 @@ function findGoDirs(dir) {
|
|
|
23
23
|
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
24
24
|
const names = new Set(entries.filter(e => e.isFile()).map(e => e.name));
|
|
25
25
|
if (names.has("go.mod") || names.has("go.sum")) groups.push({ dir: cur, names });
|
|
26
|
-
for (const e of entries) if (e.isDirectory() && !
|
|
26
|
+
for (const e of entries) if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
27
27
|
}
|
|
28
28
|
return groups;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function dirFilter(dir, opts) {
|
|
32
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
module.exports = {
|
|
32
36
|
id: "go",
|
|
33
37
|
label: "Go",
|
|
@@ -44,7 +48,7 @@ module.exports = {
|
|
|
44
48
|
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
45
49
|
out.set(coordKeyFor("go", "", d.name), makeDepRecord({ ecosystem: "go", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: false }));
|
|
46
50
|
};
|
|
47
|
-
for (const g of findGoDirs(dir)) {
|
|
51
|
+
for (const g of findGoDirs(dir, dirFilter(dir, opts))) {
|
|
48
52
|
if (g.names.has("go.mod")) {
|
|
49
53
|
const fp = path.join(g.dir, "go.mod");
|
|
50
54
|
try {
|
package/lib/codecs/index.js
CHANGED
|
@@ -19,12 +19,14 @@ const pypi = require("./pypi.codec");
|
|
|
19
19
|
const nuget = require("./nuget.codec");
|
|
20
20
|
const go = require("./go.codec");
|
|
21
21
|
const ruby = require("./ruby.codec");
|
|
22
|
+
const binary = require("./binary.codec");
|
|
22
23
|
|
|
23
24
|
// Ordre stable pour le report (maven, npm, yarn, puis les nouveaux écosystèmes).
|
|
24
|
-
|
|
25
|
+
// "binary" (committed native libs, no manifest) reste en dernier.
|
|
26
|
+
const ORDER = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby", "binary"];
|
|
25
27
|
|
|
26
28
|
const REGISTRY = new Map();
|
|
27
|
-
for (const c of [maven, npm, yarn, composer, pypi, nuget, go, ruby]) {
|
|
29
|
+
for (const c of [maven, npm, yarn, composer, pypi, nuget, go, ruby, binary]) {
|
|
28
30
|
assertCodecShape(c);
|
|
29
31
|
REGISTRY.set(c.id, c);
|
|
30
32
|
}
|
|
@@ -57,7 +59,9 @@ const DETECT_CONCURRENCY = 48;
|
|
|
57
59
|
// globs). Short-circuits as soon as every codec has matched. readdir runs with bounded
|
|
58
60
|
// concurrency — on a high-latency filesystem (WSL/9p, network mounts) the walk is
|
|
59
61
|
// latency-bound, so issuing many readdirs at once is far faster than a serial walk.
|
|
60
|
-
async function detectCodecs(dir) {
|
|
62
|
+
async function detectCodecs(dir, opts = {}) {
|
|
63
|
+
const { makeDirFilter } = require("../path-filter");
|
|
64
|
+
const skipDir = makeDirFilter({ srcRoot: dir, defaultSkip: DETECT_SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
61
65
|
const codecs = allCodecs().filter(c => c.id !== "yarn");
|
|
62
66
|
const matchers = codecs.map(c => {
|
|
63
67
|
const exact = new Set();
|
|
@@ -85,7 +89,8 @@ async function detectCodecs(dir) {
|
|
|
85
89
|
fs.promises.readdir(cur, { withFileTypes: true }).then(entries => {
|
|
86
90
|
for (const e of entries) {
|
|
87
91
|
if (e.isDirectory()) {
|
|
88
|
-
|
|
92
|
+
const child = path.join(cur, e.name);
|
|
93
|
+
if (!skipDir(child, e.name)) queue.push(child);
|
|
89
94
|
continue;
|
|
90
95
|
}
|
|
91
96
|
if (!e.isFile()) continue;
|
|
@@ -289,11 +289,12 @@ function findArchives(dir, acc = []) {
|
|
|
289
289
|
|
|
290
290
|
// Parallel equivalent of findArchives — concurrent readdir so a deep tree on a
|
|
291
291
|
// high-latency filesystem isn't walked one round-trip at a time.
|
|
292
|
-
async function findArchivesAsync(dir) {
|
|
292
|
+
async function findArchivesAsync(dir, skipDir) {
|
|
293
293
|
const { walkDirs } = require("../../parallel-walk");
|
|
294
|
+
const skip = skipDir || ((child, name) => SKIP_DIRS.has(name) || name.startsWith("."));
|
|
294
295
|
const acc = [];
|
|
295
296
|
await walkDirs(dir, {
|
|
296
|
-
skipDir:
|
|
297
|
+
skipDir: skip,
|
|
297
298
|
onDir: (cur, entries) => {
|
|
298
299
|
for (const e of entries) if (e.isFile() && ARCHIVE_RE.test(e.name)) acc.push(path.join(cur, e.name));
|
|
299
300
|
},
|
|
@@ -315,7 +316,11 @@ async function findArchivesAsync(dir) {
|
|
|
315
316
|
* @returns { deps: depRecord[], warnings: [{type,message}] }
|
|
316
317
|
*/
|
|
317
318
|
async function scanEmbeddedJars(rootDir, opts = {}) {
|
|
318
|
-
const
|
|
319
|
+
const { makeDirFilter } = require("../../path-filter");
|
|
320
|
+
const useDefaults = opts.defaultExcludes !== false;
|
|
321
|
+
const base = makeDirFilter({ srcRoot: opts.srcRoot || rootDir, defaultSkip: SKIP_DIRS, excludePath: opts.excludePath, useDefaults });
|
|
322
|
+
const skipDir = (child, name) => base(child, name) || (useDefaults && name.startsWith("."));
|
|
323
|
+
const archives = await findArchivesAsync(rootDir, skipDir);
|
|
319
324
|
const ctx = {
|
|
320
325
|
out: [],
|
|
321
326
|
warnings: [],
|
|
@@ -40,7 +40,9 @@ module.exports = {
|
|
|
40
40
|
// Expose _maven (store/propsByPom/pomFiles) pour que l'orchestrateur garde la
|
|
41
41
|
// phase de réécriture des POM nettoyés (lib/core.rewritePoms).
|
|
42
42
|
async collect(dir, opts = {}) {
|
|
43
|
-
const
|
|
43
|
+
const { makeDirFilter } = require("../path-filter");
|
|
44
|
+
const skipDir = makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: core.SKIP_DIRS, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
45
|
+
const pomFiles = await core.findPomFilesAsync(dir, skipDir);
|
|
44
46
|
const store = core.newMetadataStore();
|
|
45
47
|
const propsByPom = {};
|
|
46
48
|
for (const pom of pomFiles) {
|
|
@@ -58,7 +60,7 @@ module.exports = {
|
|
|
58
60
|
if (opts.scanJars !== false) {
|
|
59
61
|
try {
|
|
60
62
|
const { scanEmbeddedJars } = require("./maven/jar-scan");
|
|
61
|
-
const { deps: embedded, warnings: jarWarnings } = await scanEmbeddedJars(dir, { srcRoot: opts.srcRoot || dir, onProgress: opts.onJarProgress });
|
|
63
|
+
const { deps: embedded, warnings: jarWarnings } = await scanEmbeddedJars(dir, { srcRoot: opts.srcRoot || dir, excludePath: opts.excludePath, defaultExcludes: opts.defaultExcludes, onProgress: opts.onJarProgress });
|
|
62
64
|
for (const rec of embedded) deps.set(rec.coordKey, rec);
|
|
63
65
|
warnings.push(...jarWarnings);
|
|
64
66
|
} catch (e) { warnings.push({ type: "embedded-jar", message: `embedded-jar scan failed: ${e.message}` }); }
|
package/lib/codecs/npm/parse.js
CHANGED
|
@@ -362,6 +362,7 @@ const DEFAULT_JS_SKIP_DIRS = new Set([
|
|
|
362
362
|
|
|
363
363
|
function findJsManifests(rootDir, opts = {}) {
|
|
364
364
|
const { skipDirs = DEFAULT_JS_SKIP_DIRS } = opts;
|
|
365
|
+
const skipDir = opts.skipDir || ((child, name) => skipDirs.has(name));
|
|
365
366
|
const found = [];
|
|
366
367
|
const stack = [rootDir];
|
|
367
368
|
while (stack.length) {
|
|
@@ -374,7 +375,7 @@ function findJsManifests(rootDir, opts = {}) {
|
|
|
374
375
|
for (const e of entries) {
|
|
375
376
|
const p = path.join(cur, e.name);
|
|
376
377
|
if (e.isDirectory()) {
|
|
377
|
-
if (
|
|
378
|
+
if (skipDir(p, e.name)) continue;
|
|
378
379
|
stack.push(p);
|
|
379
380
|
} else if (e.isFile()) {
|
|
380
381
|
if (e.name === "package.json") here.packageJson = p;
|
|
@@ -392,10 +393,11 @@ function findJsManifests(rootDir, opts = {}) {
|
|
|
392
393
|
// serialized one round-trip at a time on a high-latency filesystem.
|
|
393
394
|
async function findJsManifestsAsync(rootDir, opts = {}) {
|
|
394
395
|
const { skipDirs = DEFAULT_JS_SKIP_DIRS } = opts;
|
|
396
|
+
const skipDir = opts.skipDir || ((child, name) => skipDirs.has(name));
|
|
395
397
|
const { walkDirs } = require("../../parallel-walk");
|
|
396
398
|
const found = [];
|
|
397
399
|
await walkDirs(rootDir, {
|
|
398
|
-
skipDir
|
|
400
|
+
skipDir,
|
|
399
401
|
onDir: (cur, entries) => {
|
|
400
402
|
const here = { dir: cur, packageJson: null, packageLock: null, yarnLock: null, pnpmLock: null };
|
|
401
403
|
for (const e of entries) {
|
|
@@ -420,4 +422,5 @@ module.exports = {
|
|
|
420
422
|
parsePnpmLock,
|
|
421
423
|
findJsManifests,
|
|
422
424
|
findJsManifestsAsync,
|
|
425
|
+
DEFAULT_JS_SKIP_DIRS,
|
|
423
426
|
};
|
|
@@ -21,11 +21,11 @@ const path = require("path");
|
|
|
21
21
|
const os = require("os");
|
|
22
22
|
const pLimit = require("p-limit");
|
|
23
23
|
const { semverCompare, webjarToNpm } = require("./collect");
|
|
24
|
+
const { withPublic, authHeaderFor, normalise } = require("../../registries");
|
|
24
25
|
|
|
25
26
|
const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
26
27
|
const CACHE_PATH = path.join(CACHE_DIR, "npm-registry-cache.json");
|
|
27
28
|
const CACHE_MAX_AGE_MS = 24 * 3600 * 1000; // 1 day, aligned with Maven Central
|
|
28
|
-
const REGISTRY = "https://registry.npmjs.org";
|
|
29
29
|
|
|
30
30
|
function loadCache() {
|
|
31
31
|
try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); }
|
|
@@ -89,29 +89,40 @@ function packumentToFindings(packument, dep) {
|
|
|
89
89
|
return out;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/** registry
|
|
93
|
-
function
|
|
94
|
-
return name.startsWith("@")
|
|
95
|
-
? `${REGISTRY}/${name.replace("/", "%2F")}`
|
|
96
|
-
: `${REGISTRY}/${encodeURIComponent(name)}`;
|
|
92
|
+
/** registry path-encodes a scoped name's slash. */
|
|
93
|
+
function encodeName(name) {
|
|
94
|
+
return name.startsWith("@") ? name.replace("/", "%2F") : encodeURIComponent(name);
|
|
97
95
|
}
|
|
98
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Fetch a packument, trying each custom registry first (with per-registry auth),
|
|
99
|
+
* then the public registry.npmjs.org as the final fallback. Returns the parsed
|
|
100
|
+
* packument, or { error } if every base failed.
|
|
101
|
+
*/
|
|
99
102
|
async function fetchPackument(name, opts = {}) {
|
|
100
103
|
if (opts.offline) return null;
|
|
101
104
|
const timeoutMs = opts.timeoutMs || 15000;
|
|
102
|
-
|
|
105
|
+
const fetcher = opts.fetcher || globalThis.fetch;
|
|
106
|
+
const bases = withPublic("npm", opts.registries || []);
|
|
107
|
+
const enc = encodeName(name);
|
|
108
|
+
let lastErr = null;
|
|
109
|
+
for (const base of bases) {
|
|
103
110
|
// Per-request timeout: a single stalled connection must never hang the
|
|
104
111
|
// whole run (one slow package would otherwise occupy a concurrency slot
|
|
105
112
|
// forever and starve the pool).
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
const url = normalise(base.url) + enc;
|
|
114
|
+
const headers = { "User-Agent": "fad-checker-npm-registry", Accept: "application/json" };
|
|
115
|
+
const ah = authHeaderFor(base);
|
|
116
|
+
if (ah) headers.Authorization = ah;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetcher(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
119
|
+
if (res.ok) return await res.json();
|
|
120
|
+
lastErr = `HTTP ${res.status}`;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
lastErr = err.name === "TimeoutError" ? `timeout after ${timeoutMs}ms` : err.message;
|
|
123
|
+
}
|
|
114
124
|
}
|
|
125
|
+
return { error: lastErr || "no data" };
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
/**
|
|
@@ -124,7 +135,7 @@ async function fetchPackument(name, opts = {}) {
|
|
|
124
135
|
* opts: { verbose, offline, allLibs, concurrency = 8 }
|
|
125
136
|
*/
|
|
126
137
|
async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
127
|
-
const { verbose, offline, allLibs = true, concurrency = 8, onProgress } = opts;
|
|
138
|
+
const { verbose, offline, allLibs = true, concurrency = 8, onProgress, registries = [] } = opts;
|
|
128
139
|
// Targets = npm deps (queried by their own name) + WebJars (queried by their
|
|
129
140
|
// derived npm-equivalent name; version matches upstream). The original dep
|
|
130
141
|
// is kept for display/results so the report shows e.g. org.webjars:angularjs.
|
|
@@ -168,7 +179,7 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
168
179
|
const key = cacheKey(t);
|
|
169
180
|
let extracted = cache.entries[key];
|
|
170
181
|
if (!extracted) {
|
|
171
|
-
const packument = await fetchPackument(npmName, { offline });
|
|
182
|
+
const packument = await fetchPackument(npmName, { offline, registries });
|
|
172
183
|
if (packument && !packument.error) {
|
|
173
184
|
const f = packumentToFindings(packument, { version });
|
|
174
185
|
extracted = {
|
|
@@ -212,4 +223,4 @@ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
|
|
|
212
223
|
return result;
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
module.exports = { packumentToFindings, checkNpmRegistryDeps, replacementFromMessage };
|
|
226
|
+
module.exports = { packumentToFindings, checkNpmRegistryDeps, replacementFromMessage, fetchPackument };
|
package/lib/codecs/npm.codec.js
CHANGED
|
@@ -20,9 +20,9 @@ const retireScanner = {
|
|
|
20
20
|
id: "retire",
|
|
21
21
|
kind: "vendored", // résultats → chapitre vendored-JS (séparé des CVE)
|
|
22
22
|
async scan(_deps, opts = {}) {
|
|
23
|
-
const {
|
|
24
|
-
const matches = await
|
|
25
|
-
return { matches, meta: {} };
|
|
23
|
+
const { scanWithRetireFull } = require("../retire");
|
|
24
|
+
const { matches, inventory, error } = await scanWithRetireFull(opts.src, { verbose: opts.verbose, force: !!opts.retireRefresh, offline: !!opts.offline });
|
|
25
|
+
return { matches, meta: { inventory, error } };
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -51,8 +51,10 @@ module.exports = {
|
|
|
51
51
|
async collect(dir, opts = {}) {
|
|
52
52
|
// Parallel manifest discovery (concurrent readdir) — much faster than the
|
|
53
53
|
// serial walk on a high-latency filesystem; parsing then runs synchronously.
|
|
54
|
-
const { findJsManifestsAsync } = require("./npm/parse");
|
|
55
|
-
const
|
|
54
|
+
const { findJsManifestsAsync, DEFAULT_JS_SKIP_DIRS } = require("./npm/parse");
|
|
55
|
+
const { makeDirFilter } = require("../path-filter");
|
|
56
|
+
const skipDir = makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: DEFAULT_JS_SKIP_DIRS, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
57
|
+
const manifestGroups = await findJsManifestsAsync(dir, { skipDir });
|
|
56
58
|
const deps = collectNpmDeps(dir, { ...opts, manifestGroups });
|
|
57
59
|
return { deps, warnings: deps.warnings || [] };
|
|
58
60
|
},
|
|
@@ -19,7 +19,7 @@ const SKIP = new Set([".git", ".idea", ".vscode", "node_modules", "dist", "build
|
|
|
19
19
|
// MSBuild project files share the same <PackageReference> schema — C#, F#, VB.
|
|
20
20
|
const PROJ_RE = /\.(csproj|fsproj|vbproj)$/i;
|
|
21
21
|
|
|
22
|
-
function findNugetDirs(dir) {
|
|
22
|
+
function findNugetDirs(dir, skipDir = (child, name) => SKIP.has(name)) {
|
|
23
23
|
const groups = [];
|
|
24
24
|
const stack = [dir];
|
|
25
25
|
while (stack.length) {
|
|
@@ -30,11 +30,15 @@ function findNugetDirs(dir) {
|
|
|
30
30
|
if (files.includes("packages.lock.json") || files.includes("packages.config") || csprojs.length) {
|
|
31
31
|
groups.push({ dir: cur, files, csprojs });
|
|
32
32
|
}
|
|
33
|
-
for (const e of entries) if (e.isDirectory() && !
|
|
33
|
+
for (const e of entries) if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
34
34
|
}
|
|
35
35
|
return groups;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function dirFilter(dir, opts) {
|
|
39
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
module.exports = {
|
|
39
43
|
id: "nuget",
|
|
40
44
|
label: "NuGet",
|
|
@@ -51,7 +55,7 @@ module.exports = {
|
|
|
51
55
|
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
52
56
|
out.set(coordKeyFor("nuget", "", d.name), makeDepRecord({ ecosystem: "nuget", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: d.isDev }));
|
|
53
57
|
};
|
|
54
|
-
for (const g of findNugetDirs(dir)) {
|
|
58
|
+
for (const g of findNugetDirs(dir, dirFilter(dir, opts))) {
|
|
55
59
|
if (g.files.includes("packages.lock.json")) {
|
|
56
60
|
const fp = path.join(g.dir, "packages.lock.json");
|
|
57
61
|
try { const { deps } = await N.parsePackagesLockJson(fp); for (const d of deps) add(d, fp); }
|
|
@@ -14,11 +14,11 @@ const fs = require("fs");
|
|
|
14
14
|
const path = require("path");
|
|
15
15
|
const os = require("os");
|
|
16
16
|
let pLimit; try { pLimit = require("p-limit"); } catch { pLimit = () => (fn) => fn(); }
|
|
17
|
+
const { withPublic, authHeaderFor, normalise } = require("../../registries");
|
|
17
18
|
|
|
18
19
|
const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
19
20
|
const CACHE_PATH = path.join(CACHE_DIR, "pypi-cache.json");
|
|
20
21
|
const CACHE_MAX_AGE_MS = 24 * 3600 * 1000;
|
|
21
|
-
const API = "https://pypi.org/pypi";
|
|
22
22
|
|
|
23
23
|
function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); } catch { return { entries: {}, meta: {} }; } }
|
|
24
24
|
function saveCache(d) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify(d)); } catch { /* ignore */ } }
|
|
@@ -56,18 +56,24 @@ function pypiToFindings(data, { version }) {
|
|
|
56
56
|
return out;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// Custom registries first (same JSON API: <base>/<name>/json), then pypi.org.
|
|
60
|
+
async function fetchProject(name, { offline, registries = [], fetcher = globalThis.fetch } = {}) {
|
|
60
61
|
if (offline) return null;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
const bases = withPublic("pypi", registries);
|
|
63
|
+
let lastErr = null;
|
|
64
|
+
for (const base of bases) {
|
|
65
|
+
const url = normalise(base.url) + `${name}/json`;
|
|
66
|
+
const headers = { "User-Agent": "fad-checker-pypi", Accept: "application/json" };
|
|
67
|
+
const ah = authHeaderFor(base); if (ah) headers.Authorization = ah;
|
|
68
|
+
try { const res = await fetcher(url, { headers }); if (res.ok) return await res.json(); lastErr = `HTTP ${res.status}`; }
|
|
69
|
+
catch (e) { lastErr = e.message; }
|
|
70
|
+
}
|
|
71
|
+
return { error: lastErr || "no data" };
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// Mirror of checkNpmRegistryDeps: returns { deprecated:[], outdated:[] }.
|
|
69
75
|
async function checkPypiRegistryDeps(deps, opts = {}) {
|
|
70
|
-
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
76
|
+
const { verbose, offline, allLibs = true, concurrency = 8, registries = [] } = opts;
|
|
71
77
|
const targets = [...deps.values()].filter(d => d.ecosystem === "pypi" && d.version);
|
|
72
78
|
const result = { deprecated: [], outdated: [], licensed: [] };
|
|
73
79
|
if (!targets.length) return result;
|
|
@@ -79,7 +85,7 @@ async function checkPypiRegistryDeps(deps, opts = {}) {
|
|
|
79
85
|
const key = `${t.name}@${t.version}`;
|
|
80
86
|
let ex = cache.entries[key];
|
|
81
87
|
if (!ex) {
|
|
82
|
-
const data = await fetchProject(t.name, { offline });
|
|
88
|
+
const data = await fetchProject(t.name, { offline, registries });
|
|
83
89
|
if (data && !data.error) {
|
|
84
90
|
const f = pypiToFindings(data, { version: t.version });
|
|
85
91
|
ex = { yanked: f.yanked, inactive: f.inactive, latest: f.outdated?.latest || null, license: f.license || null };
|
|
@@ -105,4 +111,4 @@ async function checkPypiRegistryDeps(deps, opts = {}) {
|
|
|
105
111
|
return result;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
module.exports = { pypiToFindings, pypiLicense, checkPypiRegistryDeps };
|
|
114
|
+
module.exports = { pypiToFindings, pypiLicense, checkPypiRegistryDeps, fetchProject };
|
package/lib/codecs/pypi.codec.js
CHANGED
|
@@ -24,7 +24,7 @@ const LOCKS = [
|
|
|
24
24
|
|
|
25
25
|
const FALLBACK_MANIFESTS = ["pyproject.toml", "requirements.txt"];
|
|
26
26
|
|
|
27
|
-
function findPyDirs(dir) {
|
|
27
|
+
function findPyDirs(dir, skipDir = (child, name) => SKIP.has(name)) {
|
|
28
28
|
const groups = [];
|
|
29
29
|
const stack = [dir];
|
|
30
30
|
while (stack.length) {
|
|
@@ -32,11 +32,15 @@ function findPyDirs(dir) {
|
|
|
32
32
|
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
33
33
|
const names = new Set(entries.filter(e => e.isFile()).map(e => e.name));
|
|
34
34
|
if ([...LOCKS.map(l => l[0]), ...FALLBACK_MANIFESTS].some(n => names.has(n))) groups.push({ dir: cur, names });
|
|
35
|
-
for (const e of entries) if (e.isDirectory() && !
|
|
35
|
+
for (const e of entries) if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
36
36
|
}
|
|
37
37
|
return groups;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function pyDirFilter(dir, opts) {
|
|
41
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
module.exports = {
|
|
41
45
|
id: "pypi",
|
|
42
46
|
label: "PyPI",
|
|
@@ -54,7 +58,7 @@ module.exports = {
|
|
|
54
58
|
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
55
59
|
out.set(coordKeyFor("pypi", "", d.name), makeDepRecord({ ecosystem: "pypi", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: d.isDev }));
|
|
56
60
|
};
|
|
57
|
-
for (const g of findPyDirs(dir)) {
|
|
61
|
+
for (const g of findPyDirs(dir, pyDirFilter(dir, opts))) {
|
|
58
62
|
const lock = LOCKS.find(([n]) => g.names.has(n));
|
|
59
63
|
if (lock) {
|
|
60
64
|
const fp = path.join(g.dir, lock[0]);
|
package/lib/codecs/recipes.js
CHANGED
|
@@ -132,4 +132,12 @@ const ruby = {
|
|
|
132
132
|
directSection: "B. Or pin them in the Gemfile and run bundle update",
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
const binary = {
|
|
136
|
+
label: "Binaries",
|
|
137
|
+
pinSection: "A. Replace or remove the vendored binary",
|
|
138
|
+
pinIntro: cnt => `Replace ${cnt} committed binar${cnt > 1 ? "ies" : "y"} with a managed dependency, or verify their provenance/checksum:`,
|
|
139
|
+
snippet: (items) => ((items || []).map(it => `# ${it.artifactId || "binary"}: replace with a managed dependency, or record its origin and verify its checksum`).join("\n")) || "# verify the provenance/checksum of each vendored binary, or replace it with a managed dependency",
|
|
140
|
+
directSection: "B. Prefer declaring these through a package manager (Maven/npm/NuGet/…)",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
module.exports = { maven, npm, yarn, composer, pypi, nuget, go, ruby, binary, dependencyManagementSnippet, npmOverridesSnippet, yarnResolutionsSnippet, composerRequireSnippet, pipInstallSnippet, dotnetAddSnippet, goGetSnippet, bundleUpdateSnippet };
|
|
@@ -11,11 +11,11 @@ const fs = require("fs");
|
|
|
11
11
|
const path = require("path");
|
|
12
12
|
const os = require("os");
|
|
13
13
|
let pLimit; try { pLimit = require("p-limit"); } catch { pLimit = () => (fn) => fn(); }
|
|
14
|
+
const { withPublic, authHeaderFor, normalise } = require("../../registries");
|
|
14
15
|
|
|
15
16
|
const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
16
17
|
const CACHE_PATH = path.join(CACHE_DIR, "rubygems-cache.json");
|
|
17
18
|
const CACHE_MAX_AGE_MS = 24 * 3600 * 1000;
|
|
18
|
-
const API = "https://rubygems.org/api/v1/gems";
|
|
19
19
|
|
|
20
20
|
function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); } catch { return { entries: {}, meta: {} }; } }
|
|
21
21
|
function saveCache(d) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify(d)); } catch { /* ignore */ } }
|
|
@@ -36,17 +36,23 @@ function gemToFindings(data) {
|
|
|
36
36
|
return out;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Custom gem servers first (same JSON API: <base>/<gem>.json), then rubygems.org.
|
|
40
|
+
async function fetchGem(name, { offline, registries = [], fetcher = globalThis.fetch } = {}) {
|
|
40
41
|
if (offline) return null;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const bases = withPublic("ruby", registries);
|
|
43
|
+
let lastErr = null;
|
|
44
|
+
for (const base of bases) {
|
|
45
|
+
const url = normalise(base.url) + `${encodeURIComponent(name)}.json`;
|
|
46
|
+
const headers = { "User-Agent": "fad-checker-rubygems", Accept: "application/json" };
|
|
47
|
+
const ah = authHeaderFor(base); if (ah) headers.Authorization = ah;
|
|
48
|
+
try { const res = await fetcher(url, { headers }); if (res.ok) return await res.json(); lastErr = `HTTP ${res.status}`; }
|
|
49
|
+
catch (e) { lastErr = e.message; }
|
|
50
|
+
}
|
|
51
|
+
return { error: lastErr || "no data" };
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
async function checkRubyRegistryDeps(deps, opts = {}) {
|
|
49
|
-
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
55
|
+
const { verbose, offline, allLibs = true, concurrency = 8, registries = [] } = opts;
|
|
50
56
|
const targets = [...deps.values()].filter(d => d.ecosystem === "ruby" && d.version);
|
|
51
57
|
const result = { deprecated: [], outdated: [], licensed: [] };
|
|
52
58
|
if (!targets.length) return result;
|
|
@@ -58,7 +64,7 @@ async function checkRubyRegistryDeps(deps, opts = {}) {
|
|
|
58
64
|
const key = `${t.name}@${t.version}`;
|
|
59
65
|
let ex = cache.entries[key];
|
|
60
66
|
if (!ex) {
|
|
61
|
-
const data = await fetchGem(t.name, { offline });
|
|
67
|
+
const data = await fetchGem(t.name, { offline, registries });
|
|
62
68
|
ex = (data && !data.error) ? gemToFindings(data) : { latest: null, license: null };
|
|
63
69
|
if (data && !data.error) cache.entries[key] = ex;
|
|
64
70
|
}
|
|
@@ -73,4 +79,4 @@ async function checkRubyRegistryDeps(deps, opts = {}) {
|
|
|
73
79
|
return result;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
module.exports = { gemToFindings, checkRubyRegistryDeps };
|
|
82
|
+
module.exports = { gemToFindings, checkRubyRegistryDeps, fetchGem };
|