fad-checker 1.0.3 → 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 +4 -2
- package/data/cwe-names.json +192 -0
- package/data/eol-mapping.json +12 -0
- package/docs/ARCHITECTURE.md +8 -6
- package/fad-checker.js +17 -3
- package/lib/cve-report.js +329 -20
- package/lib/npm/collect.js +25 -1
- package/lib/npm/registry.js +206 -0
- package/lib/nvd.js +7 -1
- package/lib/outdated.js +18 -1
- package/package.json +6 -5
- package/test/cve-report.test.js +92 -0
- package/test/npm-registry.test.js +64 -0
- package/test/outdated.test.js +45 -0
- package/test/webjar.test.js +33 -0
|
@@ -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/nvd.js
CHANGED
|
@@ -18,6 +18,10 @@ const { getNvdApiKey } = require("./config");
|
|
|
18
18
|
|
|
19
19
|
const NVD_CACHE_DIR = path.join(os.homedir(), ".fad-checker", "nvd-cache");
|
|
20
20
|
const NVD_CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
21
|
+
// Bump whenever `extractFromNvdRecord` adds a new field — pre-bump cache
|
|
22
|
+
// entries are treated as cache-miss so we re-fetch and pick up the new field.
|
|
23
|
+
// 2: added `cwes` (Common Weakness Enumeration list).
|
|
24
|
+
const NVD_CACHE_SCHEMA = 2;
|
|
21
25
|
const NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0";
|
|
22
26
|
|
|
23
27
|
function getRateDelay() {
|
|
@@ -43,6 +47,8 @@ function readCache(cveId) {
|
|
|
43
47
|
if (!fs.existsSync(p)) return null;
|
|
44
48
|
try {
|
|
45
49
|
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
50
|
+
// Schema mismatch → force re-fetch so newly-added fields (e.g. cwes) get populated.
|
|
51
|
+
if ((data._schema || 1) < NVD_CACHE_SCHEMA) return null;
|
|
46
52
|
if (Date.now() - data._fetchedAt < NVD_CACHE_TTL_MS) return data.body;
|
|
47
53
|
} catch { /* ignore */ }
|
|
48
54
|
return null;
|
|
@@ -50,7 +56,7 @@ function readCache(cveId) {
|
|
|
50
56
|
|
|
51
57
|
function writeCache(cveId, body) {
|
|
52
58
|
fs.mkdirSync(NVD_CACHE_DIR, { recursive: true });
|
|
53
|
-
fs.writeFileSync(cachePath(cveId), JSON.stringify({ _fetchedAt: Date.now(), body }));
|
|
59
|
+
fs.writeFileSync(cachePath(cveId), JSON.stringify({ _schema: NVD_CACHE_SCHEMA, _fetchedAt: Date.now(), body }));
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
function bestMetric(metrics) {
|
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,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Fucking Autonomous Dependency Checker —
|
|
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
|
-
"
|
|
6
|
+
"repository": "https://github.com/n8tz/fad-checker",
|
|
7
|
+
"author": "pp9Ping <pp9Ping@gmail.com>",
|
|
7
8
|
"maintainers": [
|
|
8
|
-
"
|
|
9
|
+
"pp9Ping <pp9Ping@gmail.com>"
|
|
9
10
|
],
|
|
10
11
|
"contributors": [
|
|
11
|
-
"
|
|
12
|
+
"pp9Ping <pp9Ping@gmail.com>"
|
|
12
13
|
],
|
|
13
14
|
"bin": {
|
|
14
15
|
"fad-checker": "fad-checker.js",
|
package/test/cve-report.test.js
CHANGED
|
@@ -60,6 +60,98 @@ test("generateWordReport contains Word XML namespaces", () => {
|
|
|
60
60
|
assert.ok(doc.includes("Word.Document"));
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("generateHtmlReport wraps tables with copy buttons", () => {
|
|
64
|
+
const html = generateHtmlReport({
|
|
65
|
+
cveMatches: sampleMatches,
|
|
66
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
67
|
+
projectInfo,
|
|
68
|
+
});
|
|
69
|
+
// Wrapper div + copy button injected around every table
|
|
70
|
+
assert.ok(html.includes('class="table-wrap"'), "should wrap tables");
|
|
71
|
+
assert.ok(html.includes('class="btn-copy"'), "should have copy buttons");
|
|
72
|
+
assert.ok(html.includes("📋 Copy table"), "button label");
|
|
73
|
+
// Copy script wired up
|
|
74
|
+
assert.ok(html.includes("navigator.clipboard"), "should include clipboard script");
|
|
75
|
+
assert.ok(html.includes("inlineStylesForWord"), "should inline styles for Word paste");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("generateWordReport does NOT wrap tables (no copy buttons in .doc)", () => {
|
|
79
|
+
const doc = generateWordReport({
|
|
80
|
+
cveMatches: sampleMatches,
|
|
81
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
82
|
+
projectInfo,
|
|
83
|
+
});
|
|
84
|
+
assert.ok(!doc.includes('class="table-wrap"'), "no wrapper in Word output");
|
|
85
|
+
assert.ok(!doc.includes('class="btn-copy"'), "no copy buttons in Word output");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("CWE human names are shown in the CVE table and detail panel", () => {
|
|
89
|
+
const html = generateHtmlReport({
|
|
90
|
+
cveMatches: sampleMatches.map(m => ({
|
|
91
|
+
...m,
|
|
92
|
+
cve: { ...m.cve, cwes: ["CWE-502", "CWE-917"] },
|
|
93
|
+
})),
|
|
94
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
95
|
+
projectInfo,
|
|
96
|
+
});
|
|
97
|
+
// Detail panel: full name inline
|
|
98
|
+
assert.ok(html.includes("Deserialization of Untrusted Data"), "CWE-502 name in panel");
|
|
99
|
+
assert.ok(html.includes("Expression Language Injection"), "CWE-917 name in panel");
|
|
100
|
+
// Tooltip on the column link
|
|
101
|
+
assert.ok(html.includes('title="CWE-502: Deserialization of Untrusted Data"'), "tooltip");
|
|
102
|
+
// Weaknesses section in detail panel
|
|
103
|
+
assert.ok(html.includes("Weaknesses (CWE)"), "weaknesses section heading");
|
|
104
|
+
assert.ok(html.includes('class="cwe-list"'), "CWE list rendered");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("Executive summary surfaces primary CWE name on each preview row", () => {
|
|
108
|
+
const html = generateHtmlReport({
|
|
109
|
+
cveMatches: [{
|
|
110
|
+
dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", scope: "compile" },
|
|
111
|
+
cve: { id: "CVE-2021-44228", severity: "CRITICAL", score: 10, description: "Log4Shell", fixVersion: "2.15.0", cwes: ["CWE-502", "CWE-917"] },
|
|
112
|
+
confidence: "exact",
|
|
113
|
+
}],
|
|
114
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
115
|
+
projectInfo,
|
|
116
|
+
});
|
|
117
|
+
assert.ok(html.includes('class="exec-cwe"'), "exec-cwe chip in summary");
|
|
118
|
+
assert.ok(html.includes('class="exec-cwe-id">CWE-502'), "id in chip");
|
|
119
|
+
assert.ok(html.includes("Deserialization of Untrusted Data"), "human name in chip");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("Unknown CWE falls back to id-only display", () => {
|
|
123
|
+
const html = generateHtmlReport({
|
|
124
|
+
cveMatches: [{
|
|
125
|
+
dep: { groupId: "x", artifactId: "y", version: "1", scope: "compile" },
|
|
126
|
+
cve: { id: "CVE-2099-9999", severity: "HIGH", description: "n/a", cwes: ["CWE-99999"] },
|
|
127
|
+
}],
|
|
128
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
129
|
+
projectInfo,
|
|
130
|
+
});
|
|
131
|
+
assert.ok(html.includes("CWE-99999"), "unknown CWE id present");
|
|
132
|
+
assert.ok(html.includes("(unknown weakness)"), "fallback label for unknown CWE in detail panel");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("generateWordReport applies width:100% to every table for Word page-fit", () => {
|
|
136
|
+
const doc = generateWordReport({
|
|
137
|
+
cveMatches: sampleMatches,
|
|
138
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
139
|
+
projectInfo,
|
|
140
|
+
});
|
|
141
|
+
// Every <table> rewritten with width="100%" + inline style (incl. fixed table-layout)
|
|
142
|
+
assert.ok(doc.includes('width="100%"'), "tables get width=100% attribute");
|
|
143
|
+
assert.ok(doc.includes("table-layout:fixed"), "tables get fixed layout");
|
|
144
|
+
// Word section + landscape page setup
|
|
145
|
+
assert.ok(doc.includes('class="WordSection1"'), "WordSection1 wrapper");
|
|
146
|
+
assert.ok(doc.includes("mso-page-orientation: landscape"), "landscape declared via mso-* property");
|
|
147
|
+
assert.ok(doc.includes("29.7cm 21cm"), "explicit A4 landscape dimensions");
|
|
148
|
+
// MSO conditional comment block with twips-based <w:sectPr> equivalent
|
|
149
|
+
assert.ok(doc.includes("w:WordDocument"), "Word document XML block");
|
|
150
|
+
assert.ok(doc.includes("16838twips"), "twips-based page size in mso conditional");
|
|
151
|
+
// Colgroups injected so fixed-layout tables have deterministic column widths
|
|
152
|
+
assert.ok(doc.includes('<colgroup>'), "colgroup injected on tables");
|
|
153
|
+
});
|
|
154
|
+
|
|
63
155
|
test("writeReports writes both files to disk", async () => {
|
|
64
156
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-rep-"));
|
|
65
157
|
const r = await writeReports({
|
|
@@ -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
|
+
});
|
package/test/outdated.test.js
CHANGED
|
@@ -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
|
+
});
|