fad-checker 1.0.4 → 1.0.5

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/CLAUDE.md CHANGED
@@ -15,7 +15,7 @@ Code-level orientation for contributors and Claude Code sessions on this repo.
15
15
  - retire.js (vendored JS signatures),
16
16
  - optionally Snyk (`--snyk`).
17
17
  4. Cross-checks every match's NVD CPE configurations against the dep version (`lib/cpe.js`) to filter false positives.
18
- 5. Reports EOL frameworks (endoflife.date), obsolete libs (curated), outdated libs (Maven Central).
18
+ 5. Reports EOL frameworks (endoflife.date — Maven & npm), obsolete libs (curated Maven + npm-registry per-version `deprecated` field — authoritative, skips nothing), outdated libs (Maven Central + npm registry `dist-tags.latest`). **WebJars** (`org.webjars*`) are reduced to their npm coordinate by `webjarToNpm()` and run through the npm EOL/deprecation/outdated paths — so e.g. `org.webjars:angularjs:1.8.3` is flagged EOL.
19
19
  6. Produces a self-contained HTML report + Word-compatible `.doc`, organised by ecosystem and by defining manifest, with per-tool fix recipes and an executive summary.
20
20
 
21
21
  No build tool (`mvn`, `npm install`, `yarn`) is required on PATH — `pom.xml` / `package-lock.json` / `yarn.lock` are parsed directly.
@@ -67,6 +67,7 @@ lib/retire.js retire.js (vendored-JS scanner) wrapper + cache + n
67
67
  lib/scan-completeness.js Warnings for deps fad-checker couldn't fully resolve.
68
68
  lib/npm/parse.js package.json, package-lock.json (v1/2/3), yarn.lock v1 parsers.
69
69
  lib/npm/collect.js Merge across JS manifests → unified resolvedDeps Map.
70
+ lib/npm/registry.js npm registry packument query → per-version deprecation + dist-tags.latest (npm EOL feeds via lib/outdated.js).
70
71
  lib/cache-archive.js tar.gz / zip export & import of ~/.fad-checker/.
71
72
  lib/config.js Persistent user config in ~/.fad-checker/config.json (mode 0600).
72
73
  data/ known-obsolete.json, eol-mapping.json, cpe-coord-map.json, known-public-namespaces.json
@@ -106,7 +107,7 @@ Test fixtures live in `test/fixtures/`:
106
107
  - CVE bundle from CVEProject is ~500 MB unpacked. Shells out to `curl + unzip` (fallback to `fetch()` + `unzip` / `Expand-Archive`). Extracted JSON deleted after index build. Ships as `cves.zip.zip` (nested zip) — `extractZip()` recurses up to 3 levels.
107
108
  - `endoflife.date` API responses cached 7 days; Maven Central version lookups cached 24 hours. Cache lives in `~/.fad-checker/`.
108
109
  - **Persistent config**: `~/.fad-checker/config.json` (mode 0600). Set NVD key via `fad-checker --set-nvd-key <KEY>` (free, instant from <https://nvd.nist.gov/developers/request-an-api-key> — bumps rate limit from 5/30s to 50/30s).
109
- - **`--offline` umbrella flag**: skips every network call (CVE/OSV/NVD/Maven Central/endoflife/retire). Falls back to whatever is already cached. Per-source variants (`--cve-offline`, `--no-osv`, `--no-nvd`, `--no-retire`, `--no-transitive`) still work independently.
110
+ - **`--offline` umbrella flag**: skips every network call (CVE/OSV/NVD/Maven Central/endoflife/npm-registry/retire). Falls back to whatever is already cached. Per-source variants (`--cve-offline`, `--no-osv`, `--no-nvd`, `--no-retire`, `--no-transitive`, `--no-js`) still work independently. npm registry deprecation always runs when online; npm (and Maven) outdated is gated by `--no-all-libs`.
110
111
  - `snyk` is not a hard dep — shells out via `execFile`. `snyk` exits 1 on findings; the JSON is still on stdout.
111
112
  - The cleaned POM is the union of every profile's deps. Counts will be larger than the source POM. Intentional — don't "fix" that.
112
113
  - Unresolved `${…}` Maven variables stay verbatim in the rewritten POM. `lib/cve-match.js` resolves them lazily via `resolveDepVersion()` when scanning. Deps that *still* can't be resolved (external BOM not in source tree) surface in chapter 0 as `unresolved-versions` warnings.
@@ -122,5 +123,6 @@ Test fixtures live in `test/fixtures/`:
122
123
  | NVD CVE record | `~/.fad-checker/nvd-cache/<cveId>.json` | 7 d |
123
124
  | endoflife.date cycles | `~/.fad-checker/eol-cache.json` | 7 d |
124
125
  | Maven Central latest | `~/.fad-checker/version-cache.json` | 24 h |
126
+ | npm registry (deprecation + latest) | `~/.fad-checker/npm-registry-cache.json` | 24 h |
125
127
  | Transitive POM | `~/.fad-checker/poms-cache/<g>__<a>__<v>.pom` | ∞ (immutable on Maven Central) |
126
128
  | retire.js findings | `~/.fad-checker/retire-cache/<md5(src)>.json` | 24 h |
@@ -30,5 +30,17 @@
30
30
  "org.springframework": { "product": "spring-framework", "label": "Spring Framework" },
31
31
  "org.hibernate": { "product": "hibernate", "label": "Hibernate ORM" },
32
32
  "io.netty": { "product": "netty", "label": "Netty" }
33
+ },
34
+ "by_npm_name": {
35
+ "angular": { "product": "angularjs", "label": "AngularJS" },
36
+ "angularjs": { "product": "angularjs", "label": "AngularJS" },
37
+ "vue": { "product": "vue", "label": "Vue" },
38
+ "react": { "product": "react", "label": "React" },
39
+ "react-dom": { "product": "react", "label": "React" },
40
+ "jquery": { "product": "jquery", "label": "jQuery" },
41
+ "bootstrap": { "product": "bootstrap", "label": "Bootstrap" }
42
+ },
43
+ "by_npm_scope": {
44
+ "@angular/": { "product": "angular", "label": "Angular" }
33
45
  }
34
46
  }
@@ -21,6 +21,7 @@ lib/retire.js retire.js (vendored-JS scanner) wrapper + cache + n
21
21
  lib/scan-completeness.js Warnings for deps we couldn't fully resolve.
22
22
  lib/npm/parse.js package.json, package-lock.json (v1/2/3), yarn.lock v1 parsers.
23
23
  lib/npm/collect.js Merge across JS manifests → unified resolvedDeps Map.
24
+ lib/npm/registry.js npm registry packument query → per-version deprecation + dist-tags.latest.
24
25
  lib/cache-archive.js tar.gz / zip export & import of ~/.fad-checker/.
25
26
  lib/config.js Persistent user config in ~/.fad-checker/config.json (mode 0600).
26
27
  data/ Curated JSON: known-obsolete, eol-mapping, cpe-coord-map, known-public-namespaces.
@@ -77,10 +78,11 @@ The Maven keyspace and npm keyspace never collide — `:lodash` (Maven groupId-l
77
78
  - Confirms the dep version actually falls in the vulnerable range (else `cpeFiltered: true` — likely false positive).
78
79
  - Upgrades match `confidence` from `possible` → `probable` → `exact` when a curated `cpe-coord-map.json` entry confirms vendor:product → dep coord.
79
80
  8. **retire.js** (default on) — shells out to `retire --outputformat json --jspath <src>`. Output normalised to fad-checker match shape, with the vendored file path attached so the report can show where the offending `.js` lives. Cache: `~/.fad-checker/retire-cache/<md5(src)>.json`, 24h TTL.
80
- 9. **EOL / Obsolete / Outdated** — `lib/outdated.js`:
81
- - **EOL**: matches dep coord against `data/eol-mapping.json`, fetches the cycle list from endoflife.date (cached 7d), flags cycles past their EOL date.
82
- - **Obsolete**: lookup in `data/known-obsolete.json` (curated: log4j 1.x, jackson-mapper-asl, joda-time, commons-httpclient 3.x, ).
83
- - **Outdated**: Maven Central Solr query for the latest version. Cache 24h. Concurrency 8.
81
+ 9. **EOL / Obsolete / Outdated** — `lib/outdated.js` (Maven) + `lib/npm/registry.js` (npm):
82
+ - **WebJars** (`org.webjars*` client-side JS shipped as Maven artifacts) are reduced to their npm-equivalent coordinate by `webjarToNpm()` (`lib/npm/collect.js`): `org.webjars.npm` is a deterministic npm mirror (`angular__core` → `@angular/core`); classic `org.webjars`/bower names pass through. They then flow through the **same npm paths** below — no WebJar-specific data.
83
+ - **EOL**: matches dep coord against `data/eol-mapping.json`, fetches the cycle list from endoflife.date (cached 7d), flags cycles past their EOL date. npm packages and WebJars resolve by JS library name via `by_npm_name` / `by_npm_scope` (e.g. npm `angular`/webjar `angularjs` → AngularJS 1.x, `@angular/*` Angular, `react`/`jquery`/`vue`/`bootstrap`).
84
+ - **Obsolete**: Maven via curated `data/known-obsolete.json` (log4j 1.x, jackson-mapper-asl, joda-time, commons-httpclient 3.x, …); npm **and WebJars** via the registry's per-version `deprecated` field (authoritative maintainer data — every dep is checked, nothing curated, nothing skipped).
85
+ - **Outdated**: Maven Central Solr query; npm registry `dist-tags.latest` (npm deps and WebJars). Both gated by `--no-all-libs`. Cache 24h. Concurrency 8.
84
86
  10. **Snyk** (optional, `--snyk`) — runs `snyk test --all-projects --json` against the cleaned target dir. Normalised + merged. Findings in both sources tagged `source: "both"`.
85
87
  11. **Render** — `writeReports()` produces `cve-report.html` (self-contained, inline CSS, no external assets) and `cve-report.doc` (same HTML with Office XML namespace meta tags so Word opens it natively). Default output dir: `./fad-checker-report/`.
86
88
 
@@ -129,9 +131,9 @@ The Maven keyspace and npm keyspace never collide — `:lodash` (Maven groupId-l
129
131
 
130
132
  - The CVE bundle from CVEProject is ~500 MB unpacked. We shell out to `curl + unzip` (Node built-in fallback to `fetch()` + system `unzip` / PowerShell `Expand-Archive`). The extracted JSON is deleted after the index is built.
131
133
  - The bundle ships as `cves.zip.zip` (a zip whose sole content is another zip). `extractZip()` recurses up to 3 levels.
132
- - `endoflife.date` API responses are cached locally for 7 days; Maven Central version lookups for 24 hours.
134
+ - `endoflife.date` API responses are cached locally for 7 days; Maven Central and npm registry version lookups for 24 hours.
133
135
  - **Persistent config**: `~/.fad-checker/config.json` (mode 0600) stores per-user state, currently the NVD API key. Set via `fad-checker --set-nvd-key <KEY>`.
134
- - **`--offline` umbrella flag**: skips every network call (CVE index download, OSV queries, NVD enrichment, endoflife.date lookups, Maven Central version queries, transitive POM fetches, retire.js scans). Falls back to whatever is already cached. Per-source variants (`--cve-offline`, `--no-osv`, `--no-nvd`, `--no-retire`) still work independently.
136
+ - **`--offline` umbrella flag**: skips every network call (CVE index download, OSV queries, NVD enrichment, endoflife.date lookups, Maven Central version queries, npm registry queries, transitive POM fetches, retire.js scans). Falls back to whatever is already cached. Per-source variants (`--cve-offline`, `--no-osv`, `--no-nvd`, `--no-retire`, `--no-js`) still work independently.
135
137
  - `snyk` is not a dependency — we shell out via `execFile`. `snyk` exits 1 when it finds vulnerabilities, which is expected (the JSON is still on stdout).
136
138
  - The cleaned POM is the union of every profile's deps. Counts will therefore be larger than the source POM. This is intentional — verify your reasoning before "reducing" them.
137
139
  - Unresolved `${…}` Maven variables are kept verbatim in the rewritten POM. `lib/cve-match.js` resolves them lazily via `resolveDepVersion()` when collecting deps for the scan. Deps that *still* can't be resolved (external BOM) are surfaced in chapter 0 as `unresolved-versions` warnings.
package/fad-checker.js CHANGED
@@ -464,6 +464,17 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
464
464
  catch (err) { console.warn(chalk.yellow("⚠️ Outdated check skipped:"), err.message); }
465
465
  }
466
466
 
467
+ // 4a. npm registry — deprecation (always, authoritative maintainer data) and
468
+ // outdated (gated by --all-libs like Maven Central). Covers npm deps and
469
+ // WebJars (Maven artifacts wrapping npm/bower libs), so it runs even in
470
+ // Maven-only mode. One fetch per package; no-ops when there are no targets.
471
+ try {
472
+ const { checkNpmRegistryDeps } = require("./lib/npm/registry");
473
+ const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs });
474
+ obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
475
+ outdatedResults = outdatedResults.concat(npmReg.outdated);
476
+ } catch (err) { console.warn(chalk.yellow("⚠️ npm registry check skipped:"), err.message); }
477
+
467
478
  // Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
468
479
  const eolKeys = new Set(eolResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
469
480
  const obsKeys = new Set(obsoleteResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
@@ -574,16 +585,19 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
574
585
  if (retireMatches.length > 10) console.log(` ... and ${retireMatches.length - 10} more (see report)`);
575
586
  }
576
587
 
588
+ // npm deps have no groupId; show them as "npm:name" rather than ":name".
589
+ const coordOf = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
590
+
577
591
  console.log(chalk.bold.cyan("\n 2. End-of-Life Frameworks"));
578
- for (const e of eolResults) console.log(` ${e.product.padEnd(20)} ${e.dep.groupId}:${e.dep.artifactId}:${e.dep.version} ${e.eol}`);
592
+ for (const e of eolResults) console.log(` ${e.product.padEnd(20)} ${coordOf(e.dep)}:${e.dep.version} ${e.eol}`);
579
593
  if (!eolResults.length) console.log(chalk.gray(" (none)"));
580
594
 
581
595
  console.log(chalk.bold.cyan("\n 3. Obsolete / Deprecated Libraries"));
582
- for (const o of obsoleteResults) console.log(` ${(o.severity || "info").padEnd(8)} ${o.dep.groupId}:${o.dep.artifactId}:${o.dep.version} → ${o.replacement || "n/a"}`);
596
+ for (const o of obsoleteResults) console.log(` ${(o.severity || "info").padEnd(8)} ${coordOf(o.dep)}:${o.dep.version} → ${o.replacement || "n/a"}`);
583
597
  if (!obsoleteResults.length) console.log(chalk.gray(" (none)"));
584
598
 
585
599
  console.log(chalk.bold.cyan("\n 4. Outdated Libraries"));
586
- for (const o of outdatedResults.slice(0, 20)) console.log(` ${o.dep.groupId}:${o.dep.artifactId} ${o.dep.version} → ${o.latest}`);
600
+ for (const o of outdatedResults.slice(0, 20)) console.log(` ${coordOf(o.dep)} ${o.dep.version} → ${o.latest}`);
587
601
  if (outdatedResults.length > 20) console.log(` ... and ${outdatedResults.length - 20} more`);
588
602
  if (!outdatedResults.length && options.allLibs) console.log(chalk.gray(" (none)"));
589
603
  if (!options.allLibs) console.log(chalk.gray(" (re-run with -a/--allLibs to query Maven Central)"));
@@ -199,6 +199,30 @@ function safeParse(fn, file, verbose) {
199
199
  }
200
200
  }
201
201
 
202
+ /**
203
+ * WebJars are client-side JS libraries shipped as Maven artifacts. Derive the
204
+ * upstream npm coordinate from a WebJar dep so it can flow through the npm
205
+ * pipeline (registry deprecation/outdated + endoflife EOL) instead of being
206
+ * special-cased.
207
+ *
208
+ * org.webjars.npm — deterministic npm mirror. artifactId == npm name;
209
+ * a scoped "@scope/name" is encoded as "scope__name".
210
+ * org.webjars — hand-curated catalogue; artifactId is the JS lib name
211
+ * (mostly aligned with npm, e.g. jquery/bootstrap).
212
+ * org.webjars.bower* — Bower mirrors; best-effort name match.
213
+ *
214
+ * Versions match the upstream package. Returns { name, version } or null for a
215
+ * non-WebJar coordinate.
216
+ */
217
+ function webjarToNpm(dep) {
218
+ const g = dep.groupId || "";
219
+ if (g !== "org.webjars" && !g.startsWith("org.webjars.")) return null;
220
+ let name = dep.artifactId || "";
221
+ if (!name) return null;
222
+ if (name.includes("__")) name = "@" + name.replace(/__/g, "/");
223
+ return { name, version: dep.version || null };
224
+ }
225
+
202
226
  function hasJsManifests(rootDir) {
203
227
  try {
204
228
  const stack = [rootDir];
@@ -221,4 +245,4 @@ function hasJsManifests(rootDir) {
221
245
  return false;
222
246
  }
223
247
 
224
- module.exports = { collectNpmDeps, hasJsManifests };
248
+ module.exports = { collectNpmDeps, hasJsManifests, semverCompare, webjarToNpm };
@@ -0,0 +1,206 @@
1
+ /**
2
+ * lib/npm/registry.js — npm registry queries for the npm half of the
3
+ * EOL/obsolete/outdated story.
4
+ *
5
+ * Two authoritative, online signals come from one packument fetch:
6
+ * - deprecated: the maintainer's `deprecated` string on the *resolved*
7
+ * version (the same data behind `npm WARN deprecated …`).
8
+ * - outdated: `dist-tags.latest` vs. the resolved version.
9
+ *
10
+ * The point of querying the registry rather than a curated list is to skip
11
+ * nothing: every npm dep is checked against the source of truth.
12
+ *
13
+ * `packumentToFindings` is the pure extractor (unit-tested without network);
14
+ * `checkNpmRegistryDeps` is the cached, concurrent driver.
15
+ */
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const os = require("os");
19
+ const pLimit = require("p-limit");
20
+ const { semverCompare, webjarToNpm } = require("./collect");
21
+
22
+ const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
23
+ const CACHE_PATH = path.join(CACHE_DIR, "npm-registry-cache.json");
24
+ const CACHE_MAX_AGE_MS = 24 * 3600 * 1000; // 1 day, aligned with Maven Central
25
+ const REGISTRY = "https://registry.npmjs.org";
26
+
27
+ function loadCache() {
28
+ try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); }
29
+ catch { return { meta: { fetchedAt: 0 }, entries: {} }; }
30
+ }
31
+
32
+ function saveCache(data) {
33
+ try {
34
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
35
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(data));
36
+ } catch { /* ignore */ }
37
+ }
38
+
39
+ /** Pull a recommended replacement out of a free-form deprecation message. */
40
+ function replacementFromMessage(msg) {
41
+ if (!msg) return null;
42
+ const url = msg.match(/https?:\/\/\S+/);
43
+ if (url) return url[0].replace(/[).,]+$/, "");
44
+ return "see deprecation notice";
45
+ }
46
+
47
+ /**
48
+ * Extract { deprecated, outdated } findings for one dep from its packument.
49
+ * Pure — no network, no cache. Either field is null when not applicable.
50
+ */
51
+ function packumentToFindings(packument, dep) {
52
+ const out = { deprecated: null, outdated: null };
53
+ if (!packument || typeof packument !== "object") return out;
54
+
55
+ const versionEntry = packument.versions?.[dep.version];
56
+ const depMsg = versionEntry && typeof versionEntry.deprecated === "string"
57
+ ? versionEntry.deprecated.trim()
58
+ : "";
59
+ if (depMsg) {
60
+ out.deprecated = {
61
+ dep,
62
+ severity: "MEDIUM",
63
+ replacement: replacementFromMessage(depMsg),
64
+ reason: depMsg,
65
+ source: "npm",
66
+ };
67
+ }
68
+
69
+ const latest = packument["dist-tags"]?.latest;
70
+ if (latest && dep.version) {
71
+ let behind = false;
72
+ try { behind = semverCompare(dep.version, latest) < 0; }
73
+ catch { behind = false; }
74
+ if (behind) {
75
+ const t = packument.time?.[latest];
76
+ out.outdated = {
77
+ dep,
78
+ latest,
79
+ releaseDate: typeof t === "string" ? t.slice(0, 10) : null,
80
+ };
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** registry.npmjs.org path-encodes a scoped name's slash. */
87
+ function packumentUrl(name) {
88
+ return name.startsWith("@")
89
+ ? `${REGISTRY}/${name.replace("/", "%2F")}`
90
+ : `${REGISTRY}/${encodeURIComponent(name)}`;
91
+ }
92
+
93
+ async function fetchPackument(name, opts = {}) {
94
+ if (opts.offline) return null;
95
+ const timeoutMs = opts.timeoutMs || 15000;
96
+ try {
97
+ // Per-request timeout: a single stalled connection must never hang the
98
+ // whole run (one slow package would otherwise occupy a concurrency slot
99
+ // forever and starve the pool).
100
+ const res = await fetch(packumentUrl(name), {
101
+ headers: { "User-Agent": "fad-checker-npm-registry", Accept: "application/json" },
102
+ signal: AbortSignal.timeout(timeoutMs),
103
+ });
104
+ if (!res.ok) return { error: `HTTP ${res.status}` };
105
+ return await res.json();
106
+ } catch (err) {
107
+ return { error: err.name === "TimeoutError" ? `timeout after ${timeoutMs}ms` : err.message };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Check every npm dep against the registry.
113
+ *
114
+ * Returns { deprecated: [obsolete-shaped], outdated: [outdated-shaped] }.
115
+ * Deprecation always runs (online); outdated is only collected when allLibs
116
+ * is set, mirroring the Maven Central outdated gate (--no-all-libs).
117
+ *
118
+ * opts: { verbose, offline, allLibs, concurrency = 8 }
119
+ */
120
+ async function checkNpmRegistryDeps(resolvedDeps, opts = {}) {
121
+ const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
122
+ // Targets = npm deps (queried by their own name) + WebJars (queried by their
123
+ // derived npm-equivalent name; version matches upstream). The original dep
124
+ // is kept for display/results so the report shows e.g. org.webjars:angularjs.
125
+ const targets = [];
126
+ for (const d of resolvedDeps.values()) {
127
+ if (!d.version) continue;
128
+ if (d.ecosystem === "npm") { targets.push({ dep: d, npmName: d.artifactId, version: d.version }); continue; }
129
+ const wj = webjarToNpm(d);
130
+ if (wj?.name) targets.push({ dep: d, npmName: wj.name, version: d.version });
131
+ }
132
+ const result = { deprecated: [], outdated: [] };
133
+ if (!targets.length) return result;
134
+
135
+ const cache = loadCache();
136
+ const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < CACHE_MAX_AGE_MS;
137
+ if (!fresh && !offline) cache.entries = {};
138
+
139
+ const cacheKey = t => `${t.npmName}@${t.version}`;
140
+ const liveCount = offline ? 0 : targets.filter(t => !cache.entries[cacheKey(t)]).length;
141
+ if (liveCount && !offline) {
142
+ console.log(`📦 npm registry: checking ${targets.length} packages for deprecation${allLibs ? " + outdated" : ""} (${liveCount} live, ${targets.length - liveCount} cached)…`);
143
+ }
144
+
145
+ // Incremental progress — fetching ~hundreds/thousands of packuments at
146
+ // concurrency N would otherwise be total silence for a minute or more.
147
+ let processed = 0;
148
+ const startedAt = Date.now();
149
+ const printProgress = (final = false) => {
150
+ if (!liveCount) return;
151
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
152
+ const pct = Math.round((processed / targets.length) * 100);
153
+ const line = ` npm registry: ${processed}/${targets.length} (${pct}%) — ${elapsed}s`;
154
+ if (process.stdout.isTTY) process.stdout.write(`\r${line}${final ? "\n" : " "}`);
155
+ else if (final) console.log(line);
156
+ };
157
+
158
+ const limit = pLimit(concurrency);
159
+ await Promise.all(targets.map(t => limit(async () => {
160
+ const { dep, npmName, version } = t;
161
+ const key = cacheKey(t);
162
+ let extracted = cache.entries[key];
163
+ if (!extracted) {
164
+ const packument = await fetchPackument(npmName, { offline });
165
+ if (packument && !packument.error) {
166
+ const f = packumentToFindings(packument, { version });
167
+ extracted = {
168
+ deprecated: f.deprecated ? { reason: f.deprecated.reason, replacement: f.deprecated.replacement } : null,
169
+ latest: f.outdated ? f.outdated.latest : null,
170
+ latestDate: f.outdated ? f.outdated.releaseDate : null,
171
+ };
172
+ cache.entries[key] = extracted;
173
+ } else {
174
+ extracted = { deprecated: null, latest: null, latestDate: null, error: packument?.error || "no data" };
175
+ if (!offline) cache.entries[key] = extracted;
176
+ }
177
+ }
178
+ processed++;
179
+ if (processed % 25 === 0 || processed === targets.length) printProgress();
180
+ if (extracted.deprecated) {
181
+ result.deprecated.push({
182
+ dep,
183
+ severity: "MEDIUM",
184
+ replacement: extracted.deprecated.replacement,
185
+ reason: extracted.deprecated.reason,
186
+ source: "npm",
187
+ });
188
+ if (verbose) process.stdout.write(` deprecated: ${npmName}@${version}\n`);
189
+ }
190
+ if (allLibs && extracted.latest) {
191
+ result.outdated.push({ dep, latest: extracted.latest, releaseDate: extracted.latestDate || null });
192
+ if (verbose) process.stdout.write(` outdated: ${npmName} ${version} → ${extracted.latest}\n`);
193
+ }
194
+ })));
195
+ if (liveCount) printProgress(true);
196
+
197
+ cache.meta = { fetchedAt: Date.now() };
198
+ saveCache(cache);
199
+
200
+ result.deprecated.sort((a, b) => a.dep.artifactId.localeCompare(b.dep.artifactId));
201
+ 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`);
203
+ return result;
204
+ }
205
+
206
+ module.exports = { packumentToFindings, checkNpmRegistryDeps, replacementFromMessage };
package/lib/outdated.js CHANGED
@@ -9,6 +9,7 @@ const fs = require("fs");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
11
  const { compareMavenVersions } = require("./maven-version");
12
+ const { webjarToNpm } = require("./npm/collect");
12
13
 
13
14
  const KNOWN_OBSOLETE = require("../data/known-obsolete.json");
14
15
  const EOL_MAPPING = require("../data/eol-mapping.json");
@@ -39,6 +40,23 @@ function isEolCacheFresh(maxAge = EOL_CACHE_MAX_AGE_MS) {
39
40
  // -------- EOL via endoflife.date --------
40
41
 
41
42
  function findEolProduct(dep) {
43
+ // npm packages — and WebJars, which are client-side JS shipped as Maven
44
+ // artifacts — resolve by JS library name, not Maven coordinate. npm deps
45
+ // use their name directly; WebJars are reduced to their npm-equivalent name
46
+ // first (org.webjars:angularjs → "angularjs", org.webjars.npm:angular__core
47
+ // → "@angular/core"). The npm package literally named "angular" is AngularJS
48
+ // 1.x; modern Angular is the @angular/* scope — hence the name vs. scope maps.
49
+ const npmName = dep.ecosystem === "npm" ? (dep.artifactId || "") : webjarToNpm(dep)?.name;
50
+ if (npmName != null) {
51
+ const byName = EOL_MAPPING.by_npm_name?.[npmName];
52
+ if (byName) return byName;
53
+ const scopes = Object.keys(EOL_MAPPING.by_npm_scope || {})
54
+ .sort((a, b) => b.length - a.length);
55
+ for (const s of scopes) {
56
+ if (npmName.startsWith(s)) return EOL_MAPPING.by_npm_scope[s];
57
+ }
58
+ return null;
59
+ }
42
60
  const key = `${dep.groupId}:${dep.artifactId}`;
43
61
  const direct = EOL_MAPPING.by_group_artifact?.[key];
44
62
  if (direct) return direct;
@@ -104,7 +122,6 @@ async function checkEolDeps(resolvedDeps, opts = {}) {
104
122
  const seen = new Set();
105
123
 
106
124
  for (const dep of resolvedDeps.values()) {
107
- if (dep.ecosystem === "npm") continue; // EOL mapping is Maven-only for now
108
125
  const product = findEolProduct(dep);
109
126
  if (!product) continue;
110
127
  const dedupeKey = `${dep.groupId}:${dep.artifactId}|${product.product}`;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "1.0.4",
4
- "description": "Fucking Autonomous Dependency Checker — multi-ecosystem CVE / EOL / outdated / vendored-JS scanner for Maven, npm and Yarn monorepos. Self-contained HTML + Word report.",
3
+ "version": "1.0.5",
4
+ "description": "Fucking Autonomous Dependency Checker — Scans ALL pom/bom, npm/Yarn stuff and vendored JavaScript in a source tree, deal with private deps, multi modules & produces 1 HTML/DOC report with CVE, EOL, obsolete, outdated deps & fix recos",
5
5
  "license": "MIT",
6
+ "repository": "https://github.com/n8tz/fad-checker",
6
7
  "author": "pp9Ping <pp9Ping@gmail.com>",
7
8
  "maintainers": [
8
9
  "pp9Ping <pp9Ping@gmail.com>"
@@ -0,0 +1,64 @@
1
+ const { test } = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { packumentToFindings } = require("../lib/npm/registry");
4
+
5
+ const dep = (name, version) => ({ ecosystem: "npm", groupId: "", artifactId: name, version });
6
+
7
+ test("packumentToFindings flags a deprecated resolved version", () => {
8
+ const packument = {
9
+ name: "request",
10
+ "dist-tags": { latest: "2.88.2" },
11
+ versions: {
12
+ "2.88.2": { deprecated: "request has been deprecated, see https://github.com/request/request/issues/3142" },
13
+ },
14
+ time: { "2.88.2": "2020-02-11T00:00:00.000Z" },
15
+ };
16
+ const { deprecated, outdated } = packumentToFindings(packument, dep("request", "2.88.2"));
17
+ assert.ok(deprecated, "should return a deprecated finding");
18
+ assert.match(deprecated.reason, /has been deprecated/);
19
+ assert.equal(deprecated.source, "npm");
20
+ assert.equal(deprecated.dep.artifactId, "request");
21
+ // Latest === current, so not outdated.
22
+ assert.equal(outdated, null);
23
+ });
24
+
25
+ test("packumentToFindings extracts a replacement URL from the deprecation message", () => {
26
+ const packument = {
27
+ "dist-tags": { latest: "1.0.0" },
28
+ versions: { "1.0.0": { deprecated: "use the foo package instead, see https://example.com/why" } },
29
+ };
30
+ const { deprecated } = packumentToFindings(packument, dep("bar", "1.0.0"));
31
+ assert.equal(deprecated.replacement, "https://example.com/why");
32
+ });
33
+
34
+ test("packumentToFindings reports outdated when latest is newer", () => {
35
+ const packument = {
36
+ "dist-tags": { latest: "3.7.1" },
37
+ versions: { "3.6.0": {} },
38
+ time: { "3.7.1": "2023-08-28T00:00:00.000Z" },
39
+ };
40
+ const { deprecated, outdated } = packumentToFindings(packument, dep("jquery", "3.6.0"));
41
+ assert.equal(deprecated, null, "not deprecated");
42
+ assert.ok(outdated, "should be outdated");
43
+ assert.equal(outdated.latest, "3.7.1");
44
+ assert.equal(outdated.releaseDate, "2023-08-28");
45
+ });
46
+
47
+ test("packumentToFindings returns nothing for an up-to-date, non-deprecated dep", () => {
48
+ const packument = {
49
+ "dist-tags": { latest: "4.18.2" },
50
+ versions: { "4.18.2": {} },
51
+ };
52
+ const { deprecated, outdated } = packumentToFindings(packument, dep("express", "4.18.2"));
53
+ assert.equal(deprecated, null);
54
+ assert.equal(outdated, null);
55
+ });
56
+
57
+ test("packumentToFindings tolerates a missing version entry", () => {
58
+ // Resolved version not present in the registry (e.g. unpublished) — must not throw.
59
+ const packument = { "dist-tags": { latest: "2.0.0" }, versions: { "2.0.0": {} } };
60
+ const { deprecated, outdated } = packumentToFindings(packument, dep("ghost", "1.5.0"));
61
+ assert.equal(deprecated, null);
62
+ assert.ok(outdated, "1.5.0 < 2.0.0 so still outdated");
63
+ assert.equal(outdated.latest, "2.0.0");
64
+ });
@@ -54,3 +54,48 @@ test("findEolProduct picks longest prefix match", () => {
54
54
  assert.equal(sec.product, "spring-framework");
55
55
  assert.equal(sec.label, "Spring Security");
56
56
  });
57
+
58
+ test("findEolProduct maps the npm 'angular' package to AngularJS 1.x", () => {
59
+ // The literal npm package named "angular" IS AngularJS (1.x), EOL since 2022.
60
+ const a = findEolProduct({ ecosystem: "npm", groupId: "", artifactId: "angular" });
61
+ assert.equal(a.product, "angularjs");
62
+ assert.equal(a.label, "AngularJS");
63
+ });
64
+
65
+ test("findEolProduct maps @angular/* scoped packages to modern Angular", () => {
66
+ const core = findEolProduct({ ecosystem: "npm", groupId: "", artifactId: "@angular/core" });
67
+ assert.equal(core.product, "angular");
68
+ const router = findEolProduct({ ecosystem: "npm", groupId: "", artifactId: "@angular/router" });
69
+ assert.equal(router.product, "angular");
70
+ });
71
+
72
+ test("findEolProduct maps react / react-dom / jquery / vue / bootstrap", () => {
73
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "react" }).product, "react");
74
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "react-dom" }).product, "react");
75
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "jquery" }).product, "jquery");
76
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "vue" }).product, "vue");
77
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "bootstrap" }).product, "bootstrap");
78
+ });
79
+
80
+ test("findEolProduct returns null for an unmapped npm package", () => {
81
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "left-pad" }), null);
82
+ // A Maven groupId must never leak into the npm lookup.
83
+ assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "org.springframework" }), null);
84
+ });
85
+
86
+ test("findEolProduct maps WebJars (client-side JS shipped as Maven artifacts)", () => {
87
+ // org.webjars:angularjs:1.8.3 — AngularJS 1.x, EOL since 2021.
88
+ const ajs = findEolProduct({ groupId: "org.webjars", artifactId: "angularjs", version: "1.8.3" });
89
+ assert.ok(ajs, "org.webjars:angularjs must map");
90
+ assert.equal(ajs.product, "angularjs");
91
+
92
+ assert.equal(findEolProduct({ groupId: "org.webjars", artifactId: "jquery" }).product, "jquery");
93
+ assert.equal(findEolProduct({ groupId: "org.webjars", artifactId: "bootstrap" }).product, "bootstrap");
94
+ // org.webjars.npm mirrors npm names; scope slash is encoded as "__".
95
+ assert.equal(findEolProduct({ groupId: "org.webjars.npm", artifactId: "vue" }).product, "vue");
96
+ assert.equal(findEolProduct({ groupId: "org.webjars.npm", artifactId: "angular__core" }).product, "angular");
97
+ });
98
+
99
+ test("findEolProduct returns null for an unmapped WebJar artifact", () => {
100
+ assert.equal(findEolProduct({ groupId: "org.webjars", artifactId: "datatables" }), null);
101
+ });
@@ -0,0 +1,33 @@
1
+ const { test } = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { webjarToNpm } = require("../lib/npm/collect");
4
+
5
+ test("webjarToNpm derives the npm name from org.webjars.npm (deterministic mirror)", () => {
6
+ assert.deepEqual(
7
+ webjarToNpm({ groupId: "org.webjars.npm", artifactId: "react", version: "18.2.0" }),
8
+ { name: "react", version: "18.2.0" },
9
+ );
10
+ // Scoped packages encode "@scope/name" as "scope__name".
11
+ assert.deepEqual(
12
+ webjarToNpm({ groupId: "org.webjars.npm", artifactId: "angular__core", version: "17.2.3" }),
13
+ { name: "@angular/core", version: "17.2.3" },
14
+ );
15
+ });
16
+
17
+ test("webjarToNpm passes classic org.webjars artifactIds through as-is", () => {
18
+ // Classic WebJars are hand-curated; the artifactId is the JS lib name.
19
+ assert.deepEqual(
20
+ webjarToNpm({ groupId: "org.webjars", artifactId: "angularjs", version: "1.8.3" }),
21
+ { name: "angularjs", version: "1.8.3" },
22
+ );
23
+ assert.equal(webjarToNpm({ groupId: "org.webjars", artifactId: "jquery", version: "3.7.1" }).name, "jquery");
24
+ });
25
+
26
+ test("webjarToNpm handles bower webjars too", () => {
27
+ assert.equal(webjarToNpm({ groupId: "org.webjars.bowergithub.foo", artifactId: "bar", version: "1.0.0" }).name, "bar");
28
+ });
29
+
30
+ test("webjarToNpm returns null for non-webjar coordinates", () => {
31
+ assert.equal(webjarToNpm({ groupId: "org.springframework", artifactId: "spring-core", version: "6.0.0" }), null);
32
+ assert.equal(webjarToNpm({ ecosystem: "npm", groupId: "", artifactId: "react", version: "18.0.0" }), null);
33
+ });