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.
@@ -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() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
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
- async function fetchLatest(mod, { offline }) {
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
- try {
40
- const res = await fetch(`${PROXY}/${escapeModule(mod)}/@latest`, { headers: { "User-Agent": "fad-checker-go" } });
41
- if (!res.ok) return { error: `HTTP ${res.status}` };
42
- const j = await res.json();
43
- return { latest: (j.Version || "").replace(/^v/, "") || null };
44
- } catch (e) { return { error: e.message }; }
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 };
@@ -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() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
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 {
@@ -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
- const ORDER = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby"];
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
- if (!DETECT_SKIP.has(e.name)) queue.push(path.join(cur, e.name));
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: name => SKIP_DIRS.has(name) || name.startsWith("."),
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 archives = await findArchivesAsync(rootDir);
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 pomFiles = await core.findPomFilesAsync(dir);
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}` }); }
@@ -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 (skipDirs.has(e.name)) continue;
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: name => skipDirs.has(name),
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.npmjs.org path-encodes a scoped name's slash. */
93
- function packumentUrl(name) {
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
- try {
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 res = await fetch(packumentUrl(name), {
107
- headers: { "User-Agent": "fad-checker-npm-registry", Accept: "application/json" },
108
- signal: AbortSignal.timeout(timeoutMs),
109
- });
110
- if (!res.ok) return { error: `HTTP ${res.status}` };
111
- return await res.json();
112
- } catch (err) {
113
- return { error: err.name === "TimeoutError" ? `timeout after ${timeoutMs}ms` : err.message };
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 };
@@ -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 { scanWithRetire } = require("../retire");
24
- const matches = await scanWithRetire(opts.src, { verbose: opts.verbose, force: !!opts.retireRefresh, offline: !!opts.offline });
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 manifestGroups = await findJsManifestsAsync(dir);
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() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
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
- async function fetchProject(name, { offline }) {
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
- try {
62
- const res = await fetch(`${API}/${name}/json`, { headers: { "User-Agent": "fad-checker-pypi" } });
63
- if (!res.ok) return { error: `HTTP ${res.status}` };
64
- return await res.json();
65
- } catch (e) { return { error: e.message }; }
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 };
@@ -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() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
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]);
@@ -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
- module.exports = { maven, npm, yarn, composer, pypi, nuget, go, ruby, dependencyManagementSnippet, npmOverridesSnippet, yarnResolutionsSnippet, composerRequireSnippet, pipInstallSnippet, dotnetAddSnippet, goGetSnippet, bundleUpdateSnippet };
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
- async function fetchGem(name, { offline }) {
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
- try {
42
- const res = await fetch(`${API}/${encodeURIComponent(name)}.json`, { headers: { "User-Agent": "fad-checker-rubygems" } });
43
- if (!res.ok) return { error: `HTTP ${res.status}` };
44
- return await res.json();
45
- } catch (e) { return { error: e.message }; }
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 };