fad-checker 2.4.1 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +3 -2
- package/REVIEW-2026-07-02.md +190 -0
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +3 -0
- package/data/cpe-coord-map.json +1 -0
- package/fad-checker.js +36 -4
- package/lib/certs/analyze.js +305 -0
- package/lib/certs/index.js +17 -0
- package/lib/certs/scan.js +67 -0
- package/lib/certs/sniff.js +29 -0
- package/lib/codecs/go/parse.js +50 -8
- package/lib/codecs/go/registry.js +3 -1
- package/lib/codecs/go.codec.js +22 -4
- package/lib/codecs/nuget/parse.js +21 -6
- package/lib/codecs/nuget/registry.js +30 -4
- package/lib/codecs/nuget.codec.js +37 -7
- package/lib/codecs/pypi/parse.js +11 -2
- package/lib/codecs/pypi/registry.js +3 -1
- package/lib/codecs/pypi.codec.js +3 -0
- package/lib/cve-download.js +102 -11
- package/lib/cve-report.js +56 -8
- package/lib/json-export.js +4 -1
- package/lib/maven-version.js +25 -16
- package/lib/osv.js +8 -4
- package/lib/provenance.js +2 -0
- package/lib/sarif-export.js +48 -2
- package/package.json +1 -1
|
@@ -70,9 +70,29 @@ async function resolveRegistrationBase(base, fetcher, headers) {
|
|
|
70
70
|
return resolved;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// Packages with many versions ship a PAGED registration index: pages carry an
|
|
74
|
+
// "@id" URL + [lower, upper] range instead of inline items. Without fetching
|
|
75
|
+
// them, deprecation/outdated came back silently empty for exactly the popular
|
|
76
|
+
// packages (Newtonsoft.Json, Serilog, …). Fetch only what the findings need:
|
|
77
|
+
// the LAST page (latest version) and the page whose range covers `version`.
|
|
78
|
+
async function inlineNeededPages(reg, version, fetcher, headers) {
|
|
79
|
+
const pages = reg.items || [];
|
|
80
|
+
const need = new Set();
|
|
81
|
+
const last = pages[pages.length - 1];
|
|
82
|
+
if (last && !Array.isArray(last.items) && last["@id"]) need.add(last);
|
|
83
|
+
for (const p of pages) {
|
|
84
|
+
if (Array.isArray(p.items) || !p["@id"]) continue;
|
|
85
|
+
if (version && p.lower != null && p.upper != null && cmp(version, p.lower) >= 0 && cmp(version, p.upper) <= 0) need.add(p);
|
|
86
|
+
}
|
|
87
|
+
await Promise.all([...need].map(async p => {
|
|
88
|
+
try { const r = await fetcher(p["@id"], { headers }); if (r.ok) { const j = await r.json(); if (Array.isArray(j.items)) p.items = j.items; } }
|
|
89
|
+
catch { /* page stays un-inlined — findings degrade, don't fail */ }
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
// Custom registries first (NuGet v3 registration API: <regBase>/<lowerid>/index.json),
|
|
74
94
|
// then api.nuget.org. A service-index base is resolved to its RegistrationsBaseUrl.
|
|
75
|
-
async function fetchRegistration(name, { offline, registries = [], fetcher = globalThis.fetch } = {}) {
|
|
95
|
+
async function fetchRegistration(name, { offline, version = null, registries = [], fetcher = globalThis.fetch } = {}) {
|
|
76
96
|
if (offline) return null;
|
|
77
97
|
const id = name.toLowerCase();
|
|
78
98
|
const bases = withPublic("nuget", registries);
|
|
@@ -86,7 +106,11 @@ async function fetchRegistration(name, { offline, registries = [], fetcher = glo
|
|
|
86
106
|
if (!regBase) { lastErr = "no registration base"; continue; }
|
|
87
107
|
try {
|
|
88
108
|
const res = await fetcher(`${regBase}${id}/index.json`, { headers });
|
|
89
|
-
if (res.ok)
|
|
109
|
+
if (res.ok) {
|
|
110
|
+
const reg = await res.json();
|
|
111
|
+
await inlineNeededPages(reg, version, fetcher, headers);
|
|
112
|
+
return reg;
|
|
113
|
+
}
|
|
90
114
|
lastErr = `HTTP ${res.status}`;
|
|
91
115
|
} catch (e) { lastErr = e.message; }
|
|
92
116
|
}
|
|
@@ -107,7 +131,7 @@ async function checkNugetRegistryDeps(deps, opts = {}) {
|
|
|
107
131
|
const key = `${t.name.toLowerCase()}@${t.version}`;
|
|
108
132
|
let ex = cache.entries[key];
|
|
109
133
|
if (!ex) {
|
|
110
|
-
const reg = await fetchRegistration(t.name, { offline, registries, ...(fetcher ? { fetcher } : {}) });
|
|
134
|
+
const reg = await fetchRegistration(t.name, { offline, version: t.version, registries, ...(fetcher ? { fetcher } : {}) });
|
|
111
135
|
if (reg && !reg.error) {
|
|
112
136
|
const f = nugetRegistrationToFindings(reg, { version: t.version });
|
|
113
137
|
ex = { deprecated: f.deprecated, latest: f.outdated?.latest || null, license: f.license || null };
|
|
@@ -126,7 +150,9 @@ async function checkNugetRegistryDeps(deps, opts = {}) {
|
|
|
126
150
|
if (verbose) process.stdout.write(` outdated: ${t.name} ${t.version} → ${ex.latest}\n`);
|
|
127
151
|
}
|
|
128
152
|
})));
|
|
129
|
-
|
|
153
|
+
// Offline reads must not re-stamp freshness — a stale cache would then look
|
|
154
|
+
// fresh to the next ONLINE run and skip its refetch.
|
|
155
|
+
if (!offline) cache.meta = { fetchedAt: Date.now() };
|
|
130
156
|
saveCache(cache);
|
|
131
157
|
return result;
|
|
132
158
|
}
|
|
@@ -54,7 +54,40 @@ module.exports = {
|
|
|
54
54
|
const parsedManifests = [];
|
|
55
55
|
const add = (d, manifestPath) => {
|
|
56
56
|
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
57
|
-
|
|
57
|
+
const key = coordKeyFor("nuget", "", d.name);
|
|
58
|
+
const rec = makeDepRecord({ ecosystem: "nuget", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: d.isDev });
|
|
59
|
+
const existing = out.get(key);
|
|
60
|
+
if (!existing) { out.set(key, rec); return; }
|
|
61
|
+
// Same id across projects/TFMs: merge manifests and scan EVERY distinct
|
|
62
|
+
// resolved version (same convention as Maven) — set() overwrote the first.
|
|
63
|
+
for (const p of rec.manifestPaths) if (!existing.manifestPaths.includes(p)) existing.manifestPaths.push(p);
|
|
64
|
+
if (!existing.version && rec.version) { existing.version = rec.version; if (!existing.versions.includes(rec.version)) existing.versions.push(rec.version); }
|
|
65
|
+
else if (rec.version && !existing.versions.includes(rec.version)) existing.versions.push(rec.version);
|
|
66
|
+
if (existing.isDev && !rec.isDev) { existing.isDev = false; existing.scope = rec.scope || "prod"; }
|
|
67
|
+
};
|
|
68
|
+
// Directory.Packages.props applies to every project BELOW it (MSBuild walks
|
|
69
|
+
// UP from the project dir; nearest file wins). Looking only in the project's
|
|
70
|
+
// own directory missed root-level CPM — i.e. every modern centrally-managed
|
|
71
|
+
// solution collected ZERO deps. Memoised per starting dir.
|
|
72
|
+
const cpmMemo = new Map();
|
|
73
|
+
const cpmFor = async (startDir) => {
|
|
74
|
+
if (cpmMemo.has(startDir)) return cpmMemo.get(startDir);
|
|
75
|
+
let found = { map: {}, path: null };
|
|
76
|
+
let cur = startDir;
|
|
77
|
+
while (true) {
|
|
78
|
+
const fp = path.join(cur, "Directory.Packages.props");
|
|
79
|
+
if (fs.existsSync(fp)) {
|
|
80
|
+
try { found = { map: await N.parseDirectoryPackagesProps(fp), path: fp }; }
|
|
81
|
+
catch { /* unparsable props → best-effort without CPM */ }
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (path.resolve(cur) === path.resolve(dir)) break;
|
|
85
|
+
const parent = path.dirname(cur);
|
|
86
|
+
if (parent === cur) break;
|
|
87
|
+
cur = parent;
|
|
88
|
+
}
|
|
89
|
+
cpmMemo.set(startDir, found);
|
|
90
|
+
return found;
|
|
58
91
|
};
|
|
59
92
|
for (const g of findNugetDirs(dir, dirFilter(dir, opts))) {
|
|
60
93
|
if (g.files.includes("packages.lock.json")) {
|
|
@@ -65,12 +98,9 @@ module.exports = {
|
|
|
65
98
|
continue; // lockfile is authoritative for this directory
|
|
66
99
|
}
|
|
67
100
|
// No lockfile → best-effort from .csproj (+CPM) and packages.config + warning.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
parsedManifests.push(cpmFp);
|
|
72
|
-
try { cpm = await N.parseDirectoryPackagesProps(cpmFp); } catch { /* ignore */ }
|
|
73
|
-
}
|
|
101
|
+
const cpmRes = g.csprojs.length ? await cpmFor(g.dir) : { map: {}, path: null };
|
|
102
|
+
const cpm = cpmRes.map;
|
|
103
|
+
if (cpmRes.path && !parsedManifests.includes(cpmRes.path)) parsedManifests.push(cpmRes.path);
|
|
74
104
|
let pinned = 0, skipped = 0;
|
|
75
105
|
for (const cs of g.csprojs) {
|
|
76
106
|
const fp = path.join(g.dir, cs);
|
package/lib/codecs/pypi/parse.js
CHANGED
|
@@ -40,6 +40,9 @@ function parseTomlPackages(filePath, type) {
|
|
|
40
40
|
const deps = [];
|
|
41
41
|
for (const p of pkgs) {
|
|
42
42
|
if (!p.name || !p.version) continue;
|
|
43
|
+
// uv.lock lists the project ITSELF as a package (source.virtual/editable = ".");
|
|
44
|
+
// it is not a registry dep and its name could shadow a real PyPI package.
|
|
45
|
+
if (p.source && (p.source.virtual != null || p.source.editable != null)) continue;
|
|
43
46
|
// pdm/poetry≥1.5 use `groups`; classic poetry.lock (≤1.4) uses `category = "dev"`.
|
|
44
47
|
const groups = Array.isArray(p.groups) ? p.groups : null;
|
|
45
48
|
const isDev = groups
|
|
@@ -130,14 +133,20 @@ function collectReqGraph(filePath, seen, kind, acc) {
|
|
|
130
133
|
catch { acc.missing.push(real); return; }
|
|
131
134
|
const dir = path.dirname(real);
|
|
132
135
|
for (const raw of lines) {
|
|
133
|
-
|
|
136
|
+
let line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
137
|
+
if (!line) continue;
|
|
138
|
+
// pip-compile --generate-hashes writes "pkg==1.0 \" + indented "--hash=…"
|
|
139
|
+
// continuation lines. The trailing backslash made isPinned() reject the
|
|
140
|
+
// spec, silently skipping EVERY dep of a hash-pinned file.
|
|
141
|
+
line = line.replace(/\\$/, "").trim();
|
|
134
142
|
if (!line) continue;
|
|
135
143
|
const rMatch = line.match(/^(?:-r|--requirement)[=\s]+(.+)$/);
|
|
136
144
|
if (rMatch) { collectReqGraph(path.resolve(dir, rMatch[1].trim()), seen, kind, acc); continue; }
|
|
137
145
|
const cMatch = line.match(/^(?:-c|--constraint)[=\s]+(.+)$/);
|
|
138
146
|
if (cMatch) { collectReqGraph(path.resolve(dir, cMatch[1].trim()), seen, "constraint", acc); continue; }
|
|
139
147
|
if (line.startsWith("-")) continue; // -e ., --hash, other flags
|
|
140
|
-
|
|
148
|
+
// Per-requirement options ("pkg==1.0 --hash=sha256:…") come after the spec.
|
|
149
|
+
const { name, spec } = splitPep508(line.split(/\s+--/)[0].trim());
|
|
141
150
|
if (!name) continue;
|
|
142
151
|
const n = pep503(name);
|
|
143
152
|
if (kind === "constraint") acc.constraints.set(n, spec);
|
|
@@ -106,7 +106,9 @@ async function checkPypiRegistryDeps(deps, opts = {}) {
|
|
|
106
106
|
if (verbose) process.stdout.write(` outdated: ${t.name} ${t.version} → ${ex.latest}\n`);
|
|
107
107
|
}
|
|
108
108
|
})));
|
|
109
|
-
|
|
109
|
+
// Offline reads must not re-stamp freshness — a stale cache would then look
|
|
110
|
+
// fresh to the next ONLINE run and skip its refetch.
|
|
111
|
+
if (!offline) cache.meta = { fetchedAt: Date.now() };
|
|
110
112
|
saveCache(cache);
|
|
111
113
|
return result;
|
|
112
114
|
}
|
package/lib/codecs/pypi.codec.js
CHANGED
|
@@ -72,6 +72,9 @@ module.exports = {
|
|
|
72
72
|
// version, and let prod win over dev (declared prod anywhere ⇒ prod).
|
|
73
73
|
for (const p of rec.manifestPaths) if (!existing.manifestPaths.includes(p)) existing.manifestPaths.push(p);
|
|
74
74
|
if (!existing.version && rec.version) { existing.version = rec.version; if (!existing.versions.includes(rec.version)) existing.versions.push(rec.version); }
|
|
75
|
+
// Two files pinning DIFFERENT versions of the same package: every distinct
|
|
76
|
+
// concrete version is scanned (same convention as Maven), not just the first.
|
|
77
|
+
else if (rec.version && !existing.versions.includes(rec.version)) existing.versions.push(rec.version);
|
|
75
78
|
if (existing.isDev && !rec.isDev) { existing.isDev = false; existing.scope = rec.scope || "prod"; }
|
|
76
79
|
};
|
|
77
80
|
for (const g of findPyDirs(dir, pyDirFilter(dir, opts))) {
|
package/lib/cve-download.js
CHANGED
|
@@ -16,6 +16,8 @@ const { execFile } = require("child_process");
|
|
|
16
16
|
const { promisify } = require("util");
|
|
17
17
|
const execFileP = promisify(execFile);
|
|
18
18
|
|
|
19
|
+
const { compareMavenVersions, versionLikeBound } = require("./maven-version");
|
|
20
|
+
|
|
19
21
|
const CVE_DATA_DIR = path.join(os.homedir(), ".fad-checker", "cve-data");
|
|
20
22
|
const CVE_INDEX_PATH = path.join(CVE_DATA_DIR, "maven-cve-index.json");
|
|
21
23
|
const CVE_META_PATH = path.join(CVE_DATA_DIR, "meta.json");
|
|
@@ -42,6 +44,45 @@ const KNOWN_JAVA_VENDORS = new Set([
|
|
|
42
44
|
"squareup", "okhttp",
|
|
43
45
|
]);
|
|
44
46
|
|
|
47
|
+
// Curated CPE vendor:product → Maven coord map, shared with lib/cpe.js. Used
|
|
48
|
+
// here at index-build time to (1) keep product-only records that name a known
|
|
49
|
+
// Java product and (2) backfill their packageName so tier-1 exact matching works.
|
|
50
|
+
let COORD_MAP_CACHE = null;
|
|
51
|
+
function coordMap() {
|
|
52
|
+
if (!COORD_MAP_CACHE) {
|
|
53
|
+
try { COORD_MAP_CACHE = require("../data/cpe-coord-map.json"); }
|
|
54
|
+
catch { COORD_MAP_CACHE = { byVendorProduct: {} }; }
|
|
55
|
+
}
|
|
56
|
+
return COORD_MAP_CACHE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a CNA vendor/product pair to curated Maven coords. CNAs publish
|
|
61
|
+
* legal-entity vendors ("Apache Software Foundation") and display products
|
|
62
|
+
* ("Apache Log4j2"), so the lookup tries every vendor token against the
|
|
63
|
+
* product as written (spaces → "_", CPE convention) and with the leading
|
|
64
|
+
* vendor word stripped ("apache log4j2" → "log4j2").
|
|
65
|
+
*/
|
|
66
|
+
function curatedCoordsFor(vendor, product) {
|
|
67
|
+
if (!product) return [];
|
|
68
|
+
const map = coordMap().byVendorProduct || {};
|
|
69
|
+
const p = String(product).toLowerCase().trim();
|
|
70
|
+
const vTokens = String(vendor || "").toLowerCase().split(/[^a-z0-9.]+/).filter(Boolean);
|
|
71
|
+
const candidates = new Set([p.replace(/\s+/g, "_")]);
|
|
72
|
+
const words = p.split(/\s+/);
|
|
73
|
+
while (words.length > 1 && vTokens.includes(words[0])) words.shift();
|
|
74
|
+
candidates.add(words.join("_"));
|
|
75
|
+
const out = new Set();
|
|
76
|
+
for (const vt of vTokens) {
|
|
77
|
+
for (const cand of candidates) {
|
|
78
|
+
for (const c of map[`${vt}:${cand}`] || []) out.add(c);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...out];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const PRODUCT_HEURISTIC = /^(spring-|jackson-|commons-|hibernate-|netty-|guava|log4j|slf4j|okhttp|httpclient|jetty-|tomcat-|struts-)/;
|
|
85
|
+
|
|
45
86
|
function isMavenRelevant(affected) {
|
|
46
87
|
if (!affected || !Array.isArray(affected)) return false;
|
|
47
88
|
for (const a of affected) {
|
|
@@ -50,9 +91,16 @@ function isMavenRelevant(affected) {
|
|
|
50
91
|
if (a.versions?.some(v => v.versionType === "maven")) return true;
|
|
51
92
|
const v = String(a.vendor || "").toLowerCase();
|
|
52
93
|
const p = String(a.product || "").toLowerCase();
|
|
94
|
+
// CNAs publish legal-entity vendor strings ("Apache Software Foundation",
|
|
95
|
+
// "QOS.ch Sarl"): any single token matching a known Java vendor counts —
|
|
96
|
+
// exact-set membership alone dropped CVE-2021-44228 from the index.
|
|
53
97
|
if (KNOWN_JAVA_VENDORS.has(v)) return true;
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
if (v.split(/[^a-z0-9.]+/).some(t => KNOWN_JAVA_VENDORS.has(t))) return true;
|
|
99
|
+
// Heuristic: product names that look like Maven artifactIds — also after
|
|
100
|
+
// stripping a leading vendor word ("apache log4j2" → "log4j2").
|
|
101
|
+
const stripped = p.replace(/^[a-z0-9.]+\s+/, "");
|
|
102
|
+
if (PRODUCT_HEURISTIC.test(p) || PRODUCT_HEURISTIC.test(stripped)) return true;
|
|
103
|
+
if (curatedCoordsFor(a.vendor, a.product).length) return true;
|
|
56
104
|
}
|
|
57
105
|
return false;
|
|
58
106
|
}
|
|
@@ -91,31 +139,70 @@ function severityFromScore(s) {
|
|
|
91
139
|
return "NONE";
|
|
92
140
|
}
|
|
93
141
|
|
|
142
|
+
/**
|
|
143
|
+
* CVE 5.x `versions[]` entries may carry a `changes[]` timeline: the range
|
|
144
|
+
* [version, lessThan*) starts in `status` and flips at each `at` boundary.
|
|
145
|
+
* That is how CVE-2021-44228 encodes its affected windows (2.0-beta9→2.3.1,
|
|
146
|
+
* 2.4→2.12.2, 2.13.0→2.15.0). Expand them into plain affected windows the
|
|
147
|
+
* matcher already understands; ignoring `changes` would both miss real hits
|
|
148
|
+
* and flag the patched 2.12.2/2.3.1 backport lines.
|
|
149
|
+
*/
|
|
150
|
+
function expandVersionChanges(v) {
|
|
151
|
+
const changes = Array.isArray(v.changes) ? v.changes.filter(c => c && c.at) : [];
|
|
152
|
+
if (!changes.length) return null;
|
|
153
|
+
const sorted = [...changes].sort((a, b) => compareMavenVersions(a.at, b.at));
|
|
154
|
+
const windows = [];
|
|
155
|
+
let status = String(v.status || "affected").toLowerCase();
|
|
156
|
+
let start = v.version;
|
|
157
|
+
for (const c of sorted) {
|
|
158
|
+
if (status === "affected") {
|
|
159
|
+
windows.push({ version: start, status: "affected", versionType: v.versionType, lessThan: c.at, lessThanOrEqual: undefined });
|
|
160
|
+
}
|
|
161
|
+
status = String(c.status || "").toLowerCase();
|
|
162
|
+
start = c.at;
|
|
163
|
+
}
|
|
164
|
+
if (status === "affected") {
|
|
165
|
+
windows.push({ version: start, status: "affected", versionType: v.versionType, lessThan: v.lessThan, lessThanOrEqual: v.lessThanOrEqual });
|
|
166
|
+
}
|
|
167
|
+
return windows;
|
|
168
|
+
}
|
|
169
|
+
|
|
94
170
|
function extractAffectedRanges(affected) {
|
|
95
171
|
const out = [];
|
|
96
172
|
for (const a of affected || []) {
|
|
97
|
-
const
|
|
173
|
+
const explicit = a.packageName && String(a.packageName).includes(":") ? a.packageName : null;
|
|
174
|
+
// Product-only records (the common shape for pre-2023 CVEs) get their
|
|
175
|
+
// coordinate(s) from the curated map so they land in byPackageName and
|
|
176
|
+
// match at tier-1. One product may map to several artifacts.
|
|
177
|
+
const packageNames = explicit ? [explicit] : curatedCoordsFor(a.vendor, a.product);
|
|
98
178
|
const vendor = a.vendor ? String(a.vendor).toLowerCase() : null;
|
|
99
179
|
const product = a.product ? String(a.product).toLowerCase() : null;
|
|
100
180
|
const versions = Array.isArray(a.versions) ? a.versions : [];
|
|
101
|
-
const ranges = versions.
|
|
181
|
+
const ranges = versions.flatMap(v => expandVersionChanges(v) || [{
|
|
102
182
|
version: v.version,
|
|
103
183
|
status: v.status,
|
|
104
184
|
versionType: v.versionType,
|
|
105
185
|
lessThan: v.lessThan,
|
|
106
186
|
lessThanOrEqual: v.lessThanOrEqual,
|
|
107
|
-
})
|
|
108
|
-
out.push({ packageName, vendor, product, ranges });
|
|
187
|
+
}]);
|
|
188
|
+
out.push({ packageName: packageNames[0] || null, packageNames, vendor, product, ranges });
|
|
109
189
|
}
|
|
110
190
|
return out;
|
|
111
191
|
}
|
|
112
192
|
|
|
193
|
+
// Highest version-like upper bound across the affected ranges — the safe
|
|
194
|
+
// "upgrade to at least" target. (First-match was wrong for multi-branch
|
|
195
|
+
// records: struts 2.3.x fixed in 2.3.32 vs 2.5.x fixed in 2.5.10.1.)
|
|
113
196
|
function fixVersionFromRanges(ranges) {
|
|
197
|
+
let best = null;
|
|
114
198
|
for (const r of ranges) {
|
|
115
|
-
|
|
116
|
-
|
|
199
|
+
for (const u of [r.lessThan, r.lessThanOrEqual]) {
|
|
200
|
+
if (!versionLikeBound(u)) continue;
|
|
201
|
+
const s = String(u).trim();
|
|
202
|
+
if (!best || compareMavenVersions(s, best) > 0) best = s;
|
|
203
|
+
}
|
|
117
204
|
}
|
|
118
|
-
return
|
|
205
|
+
return best;
|
|
119
206
|
}
|
|
120
207
|
|
|
121
208
|
function extractMavenRelevantCve(json) {
|
|
@@ -160,8 +247,9 @@ function addToIndex(index, cve) {
|
|
|
160
247
|
vendor: a.vendor,
|
|
161
248
|
product: a.product,
|
|
162
249
|
};
|
|
163
|
-
|
|
164
|
-
|
|
250
|
+
const names = a.packageNames?.length ? a.packageNames : (a.packageName ? [a.packageName] : []);
|
|
251
|
+
for (const n of names) {
|
|
252
|
+
(index.byPackageName[String(n).toLowerCase()] ||= []).push(entry);
|
|
165
253
|
}
|
|
166
254
|
if (a.product) {
|
|
167
255
|
(index.byProduct[a.product] ||= []).push(entry);
|
|
@@ -369,4 +457,7 @@ module.exports = {
|
|
|
369
457
|
ensureCveIndex,
|
|
370
458
|
extractMavenRelevantCve,
|
|
371
459
|
isMavenRelevant,
|
|
460
|
+
curatedCoordsFor,
|
|
461
|
+
newIndex,
|
|
462
|
+
addToIndex,
|
|
372
463
|
};
|
package/lib/cve-report.js
CHANGED
|
@@ -1502,7 +1502,7 @@ function renderLicenseChapter(licenseResults) {
|
|
|
1502
1502
|
return intro + blocks;
|
|
1503
1503
|
}
|
|
1504
1504
|
|
|
1505
|
-
function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs = [], resolvedDeps, projectInfo, warnings, parsedManifests = [], diff = null, wrapTables = true }) {
|
|
1505
|
+
function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, certFindings, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs = [], resolvedDeps, projectInfo, warnings, parsedManifests = [], diff = null, wrapTables = true }) {
|
|
1506
1506
|
setRenderCtx({ srcRoot: projectInfo?.src || null });
|
|
1507
1507
|
// Dedupe rows so a single (dep, cve) shows once with all via paths merged.
|
|
1508
1508
|
cveMatches = mergeMatches(cveMatches || []);
|
|
@@ -1542,6 +1542,8 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1542
1542
|
const unmanagedContent = renderUnmanagedInventory(unmanagedInventory, projectInfo?.src);
|
|
1543
1543
|
const vendoredJsInv = vendoredJsInventory || [];
|
|
1544
1544
|
const vendoredJsContent = renderVendoredJsInventory(vendoredJsInv, projectInfo?.src);
|
|
1545
|
+
const certInv = certFindings || [];
|
|
1546
|
+
const certContent = renderCertificatesChapter(certInv, projectInfo?.src);
|
|
1545
1547
|
const fpContent = renderFalsePositives([...prodMatchesFP, ...devMatchesFP, ...embMatchesFP]);
|
|
1546
1548
|
|
|
1547
1549
|
const toolbar = `<div class="toolbar">
|
|
@@ -1574,6 +1576,8 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1574
1576
|
const embTotal = embeddedInventory.length;
|
|
1575
1577
|
const unmTotal = unmanagedInventory.length;
|
|
1576
1578
|
const vjsTotal = vendoredJsInv.length;
|
|
1579
|
+
const certTotal = certInv.length;
|
|
1580
|
+
const certPrivTotal = certInv.filter(c => c.kind === "private-key").length;
|
|
1577
1581
|
const excTotal = excludedDirs?.length || 0;
|
|
1578
1582
|
// CVE root header breakdown: direct vs indirect (transitive) production vulns + the dev count.
|
|
1579
1583
|
const prodDirect = prodMatchesActive.filter(m => m.dep?.scope !== "transitive").length;
|
|
@@ -1590,12 +1594,13 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1590
1594
|
|
|
1591
1595
|
// Root 2 — Unmanaged / unversioned components (binaries + vendored JS, no package manager).
|
|
1592
1596
|
const binRoot = majorSection(
|
|
1593
|
-
`2. Unmanaged / unversioned components (${embTotal} embedded, ${unmTotal} native, ${vjsTotal} vendored JS)`,
|
|
1597
|
+
`2. Unmanaged / unversioned components (${embTotal} embedded, ${unmTotal} native, ${vjsTotal} vendored JS, ${certTotal} crypto)`,
|
|
1594
1598
|
(embTotal ? minorSection(`2.1 Embedded binaries — JAR/WAR/EAR (${embTotal})`, embeddedContent, { id: "ch1e", open: embTotal <= 50 }) : "") +
|
|
1595
1599
|
(unmTotal ? minorSection(`2.2 Unmanaged / vendored native binaries (${unmTotal})`, unmanagedContent, { id: "ch1c", open: unmTotal <= 50 }) : "") +
|
|
1596
|
-
(vjsTotal ? minorSection(`2.3 Unmanaged / vendored JavaScript (${vjsTotal})`, vendoredJsContent, { id: "ch1d", open: vjsTotal <= 50 }) : "")
|
|
1597
|
-
|
|
1598
|
-
|
|
1600
|
+
(vjsTotal ? minorSection(`2.3 Unmanaged / vendored JavaScript (${vjsTotal})`, vendoredJsContent, { id: "ch1d", open: vjsTotal <= 50 }) : "") +
|
|
1601
|
+
(certTotal ? minorSection(`2.4 Certificates & key material (${certTotal}${certPrivTotal ? `, ${certPrivTotal} private key${certPrivTotal > 1 ? "s" : ""}` : ""})`, certContent, { id: "ch1f", open: certTotal <= 50 }) : "") ||
|
|
1602
|
+
`<div class="empty">No unmanaged binaries, vendored JavaScript or committed crypto material found.</div>`,
|
|
1603
|
+
{ id: "chbin", open: (embTotal + unmTotal + vjsTotal + certTotal) > 0 });
|
|
1599
1604
|
|
|
1600
1605
|
// Root 3 — Maintenance / lifecycle (EOL + obsolete + outdated).
|
|
1601
1606
|
const maintRoot = majorSection(
|
|
@@ -1627,10 +1632,11 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1627
1632
|
{ id: "ch3", label: `Dev (${devStats.total})` },
|
|
1628
1633
|
...(fpTotal ? [{ id: "ch9", label: `Likely FP (${fpTotal})` }] : []),
|
|
1629
1634
|
] },
|
|
1630
|
-
{ id: "chbin", label: `2. Unmanaged components (${embTotal + unmTotal + vjsTotal})`, sub: [
|
|
1635
|
+
{ id: "chbin", label: `2. Unmanaged components (${embTotal + unmTotal + vjsTotal + certTotal})`, sub: [
|
|
1631
1636
|
...(embTotal ? [{ id: "ch1e", label: `Embedded (${embTotal})` }] : []),
|
|
1632
1637
|
...(unmTotal ? [{ id: "ch1c", label: `Native (${unmTotal})` }] : []),
|
|
1633
1638
|
...(vjsTotal ? [{ id: "ch1d", label: `Vendored JS (${vjsTotal})` }] : []),
|
|
1639
|
+
...(certTotal ? [{ id: "ch1f", label: `Crypto (${certTotal})` }] : []),
|
|
1634
1640
|
] },
|
|
1635
1641
|
{ id: "chmaint", label: `3. Maintenance (${eolTotal + obsTotal + outTotal})`, sub: [
|
|
1636
1642
|
{ id: "ch4", label: `EOL (${eolTotal})` },
|
|
@@ -1822,6 +1828,48 @@ function renderVendoredJsInventory(inventory, srcRoot) {
|
|
|
1822
1828
|
return intro + `<table><thead><tr><th>Library</th><th>Version</th><th>File</th><th>Detection</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
1823
1829
|
}
|
|
1824
1830
|
|
|
1831
|
+
// Certificate / key-material inventory chapter (2.4). Every committed certificate,
|
|
1832
|
+
// private/public key and keystore the scan found, with crypto-hygiene findings.
|
|
1833
|
+
// 100% offline (built-in crypto.X509Certificate + header parsing — no network).
|
|
1834
|
+
const CERT_ISSUE_LABEL = {
|
|
1835
|
+
"private-key-committed": "private key committed",
|
|
1836
|
+
"public-key-committed": "public key",
|
|
1837
|
+
"keystore-committed": "keystore",
|
|
1838
|
+
"cert-expired": "EXPIRED",
|
|
1839
|
+
"cert-expiring": "expiring soon",
|
|
1840
|
+
"cert-weak-key": "weak key",
|
|
1841
|
+
"cert-weak-signature": "weak signature",
|
|
1842
|
+
"cert-self-signed": "self-signed",
|
|
1843
|
+
};
|
|
1844
|
+
const CERT_SEV_BG = { critical: "#7c0008", high: "#b91c1c", medium: "#ea580c", low: "#6b7280", info: "#6b7280" };
|
|
1845
|
+
|
|
1846
|
+
function renderCertificatesChapter(inventory, srcRoot) {
|
|
1847
|
+
if (!inventory || !inventory.length) return `<div class="empty">No committed certificates or key material found.</div>`;
|
|
1848
|
+
const priv = inventory.filter(c => c.kind === "private-key").length;
|
|
1849
|
+
const pub = inventory.filter(c => c.kind === "public-key").length;
|
|
1850
|
+
const expired = inventory.filter(c => c.issues.some(i => i.type === "cert-expired")).length;
|
|
1851
|
+
const intro = `<div class="fp-intro">Cryptographic material committed into the source tree — X.509 certificates, <strong>private & public keys</strong> (PEM / OpenSSH every algorithm / PuTTY / PGP / one-line SSH) and Java/PKCS#12 keystores. Each key is labelled <strong>private</strong> (a committed secret — critical) or <strong>public</strong> (inventory). Detected and parsed entirely offline (built-in X.509 parser; no network, no decryption). <strong>${priv}</strong> private key${priv === 1 ? "" : "s"}, <strong>${pub}</strong> public key${pub === 1 ? "" : "s"}, <strong>${expired}</strong> expired certificate${expired === 1 ? "" : "s"}.</div>`;
|
|
1852
|
+
const typePill = c => c.kind === "private-key" ? pill("🔑 PRIVATE KEY", CERT_SEV_BG.critical)
|
|
1853
|
+
: c.kind === "public-key" ? pill("public key", "#2563eb")
|
|
1854
|
+
: c.kind === "keystore" ? pill("keystore", "#b45309")
|
|
1855
|
+
: pill("certificate", "#475569");
|
|
1856
|
+
const rows = inventory.map(c => {
|
|
1857
|
+
let rel = c.path;
|
|
1858
|
+
if (srcRoot) { try { rel = path.relative(srcRoot, c.path); } catch { /* keep abs */ } }
|
|
1859
|
+
const algo = `${esc(c.algorithm || "?")}${c.bits ? " " + esc(String(c.bits)) : ""}`;
|
|
1860
|
+
let detail;
|
|
1861
|
+
if (c.kind === "certificate") {
|
|
1862
|
+
const exp = c.notAfter ? `exp ${esc(c.notAfter.slice(0, 10))}${c.daysUntilExpiry != null ? ` (${c.daysUntilExpiry}d)` : ""}` : "";
|
|
1863
|
+
detail = `${esc(c.subject || "?")} <span style="color:#6b7280">← ${esc(c.issuer || "?")}</span><br><span style="color:#6b7280">${exp}</span>`;
|
|
1864
|
+
} else {
|
|
1865
|
+
detail = `<span style="color:#6b7280">${esc(c.format || "")}${c.encrypted === true ? " · encrypted" : c.encrypted === false ? " · UNENCRYPTED" : ""}${c.count > 1 ? ` · ${c.count} keys` : ""}</span>`;
|
|
1866
|
+
}
|
|
1867
|
+
const status = (c.issues || []).map(i => pill(CERT_ISSUE_LABEL[i.type] || i.type, CERT_SEV_BG[i.severity] || "#6b7280")).join(" ");
|
|
1868
|
+
return `<tr><td class="dep"><code class="path">${esc(rel)}</code></td><td>${typePill(c)}</td><td>${algo}</td><td>${detail}</td><td>${status}</td><td><code class="path" style="color:#6b7280">${esc((c.sha256 || "").slice(0, 16))}…</code></td></tr>`;
|
|
1869
|
+
}).join("\n");
|
|
1870
|
+
return intro + `<table><thead><tr><th>File</th><th>Type</th><th>Algorithm</th><th>Details</th><th>Findings</th><th>SHA-256</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1825
1873
|
function renderExcludedDirs(excludedDirs) {
|
|
1826
1874
|
if (!excludedDirs || !excludedDirs.length) return `<div class="empty">No directories were excluded from the scan.</div>`;
|
|
1827
1875
|
const byDefault = excludedDirs.filter(e => e.type === "default").length;
|
|
@@ -2353,12 +2401,12 @@ ${WORD_SECTION_PR}
|
|
|
2353
2401
|
// Render the HTML and/or Word report. Callers pass explicit target paths;
|
|
2354
2402
|
// a null/omitted path skips that format (so `--report-html` alone writes only HTML).
|
|
2355
2403
|
// Back-compat: `outputDir` still writes both under the default file names.
|
|
2356
|
-
async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings, parsedManifests, diff }) {
|
|
2404
|
+
async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, certFindings, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings, parsedManifests, diff }) {
|
|
2357
2405
|
if (outputDir) {
|
|
2358
2406
|
if (htmlPath === undefined) htmlPath = path.join(outputDir, "cve-report.html");
|
|
2359
2407
|
if (docPath === undefined) docPath = path.join(outputDir, "cve-report.doc");
|
|
2360
2408
|
}
|
|
2361
|
-
const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps, projectInfo, warnings, parsedManifests, diff };
|
|
2409
|
+
const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, certFindings, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps, projectInfo, warnings, parsedManifests, diff };
|
|
2362
2410
|
const written = { htmlPath: null, docPath: null };
|
|
2363
2411
|
if (htmlPath) {
|
|
2364
2412
|
await fs.promises.mkdir(path.dirname(path.resolve(htmlPath)), { recursive: true });
|
package/lib/json-export.js
CHANGED
|
@@ -69,7 +69,7 @@ const SEV = ["critical", "high", "medium", "low", "none", "unknown"];
|
|
|
69
69
|
*/
|
|
70
70
|
function buildFindings(payload = {}) {
|
|
71
71
|
const {
|
|
72
|
-
cveMatches = [], retireMatches = [], vendoredJsInventory = [], eolResults = [], obsoleteResults = [],
|
|
72
|
+
cveMatches = [], retireMatches = [], vendoredJsInventory = [], certFindings = [], eolResults = [], obsoleteResults = [],
|
|
73
73
|
outdatedResults = [], licenseResults = null, resolvedDeps, projectInfo = {}, toolVersion = "0",
|
|
74
74
|
typosquats = [], excludedDirs = [], diff = null,
|
|
75
75
|
} = payload;
|
|
@@ -108,6 +108,8 @@ function buildFindings(payload = {}) {
|
|
|
108
108
|
unmanaged: unmanaged.length,
|
|
109
109
|
embedded: embedded.length,
|
|
110
110
|
vendoredJs: vendoredJsInventory.length,
|
|
111
|
+
certificates: certFindings.length,
|
|
112
|
+
certPrivateKeys: certFindings.filter(c => c.kind === "private-key").length,
|
|
111
113
|
suppressed: cveMatches.filter(m => m.suppressed).length,
|
|
112
114
|
malicious: cveMatches.filter(m => m.malicious && !m.suppressed).length,
|
|
113
115
|
typosquat: typosquats.length,
|
|
@@ -122,6 +124,7 @@ function buildFindings(payload = {}) {
|
|
|
122
124
|
unmanaged,
|
|
123
125
|
embedded,
|
|
124
126
|
vendoredJs: vendoredJsInventory,
|
|
127
|
+
certificates: certFindings,
|
|
125
128
|
typosquat: typosquats,
|
|
126
129
|
excludedDirs,
|
|
127
130
|
// Differential audit vs a --baseline document (summary + per-category deltas).
|
package/lib/maven-version.js
CHANGED
|
@@ -103,6 +103,14 @@ function compareMavenVersions(aStr, bStr) {
|
|
|
103
103
|
* spec shape: { version, status, lessThan, lessThanOrEqual, versionType }
|
|
104
104
|
* Returns true if depVersion is affected.
|
|
105
105
|
*/
|
|
106
|
+
// A bound participates in comparisons only if it looks like a version. CVE 5.x
|
|
107
|
+
// records carry placeholders in these fields ("log4j-core*", "*", "unspecified")
|
|
108
|
+
// — comparing those as Maven versions is garbage (alpha sorts below numeric), so
|
|
109
|
+
// CVE-2021-44228's `lessThan: "log4j-core*"` used to unmatch every real version.
|
|
110
|
+
function versionLikeBound(s) {
|
|
111
|
+
return s != null && /^[0-9]/.test(String(s).trim()) && String(s).trim() !== "0";
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
function isVersionAffected(depVersion, spec) {
|
|
107
115
|
if (!spec) return false;
|
|
108
116
|
if (spec.status && spec.status !== "affected") return false;
|
|
@@ -110,27 +118,27 @@ function isVersionAffected(depVersion, spec) {
|
|
|
110
118
|
const dep = parseMavenVersion(depVersion);
|
|
111
119
|
if (!dep.segments.length) return false;
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
const lower = versionLikeBound(spec.version) && spec.version !== "*" ? spec.version : null;
|
|
122
|
+
const upperExcl = versionLikeBound(spec.lessThan) ? spec.lessThan : null;
|
|
123
|
+
const upperIncl = versionLikeBound(spec.lessThanOrEqual) ? spec.lessThanOrEqual : null;
|
|
124
|
+
|
|
125
|
+
// Fail-closed: a spec with no usable version constraint carries no information.
|
|
114
126
|
// Without this guard the function falls through to `return true` for every input,
|
|
115
|
-
// which was the H1 cascade described in CRITICAL-REVIEW.md.
|
|
116
|
-
|
|
117
|
-
if (!
|
|
127
|
+
// which was the H1 cascade described in CRITICAL-REVIEW.md. A wildcard/placeholder
|
|
128
|
+
// upper on its own does not count ({version:"*", lessThan:"*"} must stay inert).
|
|
129
|
+
if (!lower && !upperExcl && !upperIncl) return false;
|
|
118
130
|
|
|
119
131
|
// Lower bound (inclusive)
|
|
120
|
-
if (
|
|
121
|
-
if (compareMavenVersions(depVersion, spec.version) < 0) return false;
|
|
122
|
-
}
|
|
132
|
+
if (lower && compareMavenVersions(depVersion, lower) < 0) return false;
|
|
123
133
|
// Upper bound exclusive
|
|
124
|
-
if (
|
|
125
|
-
if (compareMavenVersions(depVersion, spec.lessThan) >= 0) return false;
|
|
126
|
-
}
|
|
134
|
+
if (upperExcl && compareMavenVersions(depVersion, upperExcl) >= 0) return false;
|
|
127
135
|
// Upper bound inclusive
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
//
|
|
132
|
-
if (
|
|
133
|
-
if (compareMavenVersions(depVersion,
|
|
136
|
+
if (upperIncl && compareMavenVersions(depVersion, upperIncl) > 0) return false;
|
|
137
|
+
// Exact match with no upper of ANY kind (not even a wildcard) — only affected if
|
|
138
|
+
// equal. A wildcard upper ({version:"2.0", lessThan:"log4j-core*"}) instead means
|
|
139
|
+
// "from 2.0 onward, unbounded", so it must NOT collapse to an exact match.
|
|
140
|
+
if (lower && spec.lessThan == null && spec.lessThanOrEqual == null) {
|
|
141
|
+
if (compareMavenVersions(depVersion, lower) !== 0) return false;
|
|
134
142
|
}
|
|
135
143
|
return true;
|
|
136
144
|
}
|
|
@@ -160,5 +168,6 @@ module.exports = {
|
|
|
160
168
|
parseMavenVersion,
|
|
161
169
|
compareMavenVersions,
|
|
162
170
|
isVersionAffected,
|
|
171
|
+
versionLikeBound,
|
|
163
172
|
parseRange,
|
|
164
173
|
};
|
package/lib/osv.js
CHANGED
|
@@ -31,12 +31,16 @@ function cacheKey(g, a, v, ecosystem = "maven") {
|
|
|
31
31
|
return `${ecosystem}__${safeG}__${safeA}__${v}.json`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// OFFLINE bypasses the TTL (same rule as lib/nvd.js): the warmed cache is the
|
|
35
|
+
// ONLY source on an air-gapped box, so a >12h-old entry must still be served —
|
|
36
|
+
// expiring it there silently reported "0 OSV vulns" for every ecosystem.
|
|
37
|
+
// Online keeps enforcing the TTL so entries refresh normally.
|
|
38
|
+
function readCache(name, { ignoreTtl = false } = {}) {
|
|
35
39
|
const p = path.join(OSV_CACHE_DIR, name);
|
|
36
40
|
if (!fs.existsSync(p)) return null;
|
|
37
41
|
try {
|
|
38
42
|
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
39
|
-
if (Date.now() - data._fetchedAt < OSV_CACHE_TTL_MS) return data.body;
|
|
43
|
+
if (ignoreTtl || Date.now() - data._fetchedAt < OSV_CACHE_TTL_MS) return data.body;
|
|
40
44
|
} catch { /* ignore */ }
|
|
41
45
|
return null;
|
|
42
46
|
}
|
|
@@ -223,7 +227,7 @@ async function queryBatch(deps, opts = {}) {
|
|
|
223
227
|
for (let i = 0; i < deps.length; i++) {
|
|
224
228
|
const d = deps[i];
|
|
225
229
|
const ck = cacheKey(d.groupId, d.artifactId, d.version, d.ecosystem || "maven");
|
|
226
|
-
const hit = readCache(ck);
|
|
230
|
+
const hit = readCache(ck, { ignoreTtl: !!offline });
|
|
227
231
|
if (hit !== null) {
|
|
228
232
|
indexMap[i] = { cached: hit };
|
|
229
233
|
} else {
|
|
@@ -297,7 +301,7 @@ async function queryBatch(deps, opts = {}) {
|
|
|
297
301
|
const liveIds = [];
|
|
298
302
|
for (const id of allIds) {
|
|
299
303
|
const detailCacheKey = `vuln_${id}.json`;
|
|
300
|
-
const hit = readCache(detailCacheKey);
|
|
304
|
+
const hit = readCache(detailCacheKey, { ignoreTtl: !!offline });
|
|
301
305
|
if (hit) { detailById.set(id, hit); continue; }
|
|
302
306
|
if (offline) continue;
|
|
303
307
|
liveIds.push(id);
|
package/lib/provenance.js
CHANGED
|
@@ -109,6 +109,8 @@ function runConfiguration(options = {}) {
|
|
|
109
109
|
allLibs: options.allLibs !== false,
|
|
110
110
|
licenses: !!options.licenses,
|
|
111
111
|
typosquat: !!options.typosquat,
|
|
112
|
+
certs: options.certs !== false,
|
|
113
|
+
certExpiryDays: options.certExpiryDays != null ? String(options.certExpiryDays) : null,
|
|
112
114
|
failOn: options.failOn || "none",
|
|
113
115
|
ignoreFile: options.ignore || null,
|
|
114
116
|
vexFile: options.vex || null,
|