@4da/mcp-server 6.2.3 → 6.2.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/CHANGELOG.md +520 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +289 -16
- package/dist/live/fix-paths.js +876 -93
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/range-satisfies.d.ts +4 -2
- package/dist/live/range-satisfies.js +7 -5
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/refresh-rule.d.ts +36 -0
- package/dist/live/refresh-rule.js +49 -0
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +51 -0
- package/dist/live/version-compare.js +152 -1
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +164 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +70 -7
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- package/package.json +2 -2
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* verdict fall through to `proceed`.
|
|
11
11
|
*/
|
|
12
12
|
import { compareIn, isPrereleaseIn, versionParts } from "./dependency-check-versions.js";
|
|
13
|
+
import { excludedReleases } from "../live/withdrawn-releases.js";
|
|
14
|
+
import { heldSeverity, heldText } from "../live/held-dependencies.js";
|
|
13
15
|
/** Verdict precedence, strongest first. */
|
|
14
16
|
const PRECEDENCE = ["avoid", "review", "unknown", "wait", "proceed"];
|
|
15
17
|
export const WAIT_DAYS = 3;
|
|
@@ -57,40 +59,82 @@ export function introducedDependencies(to, base) {
|
|
|
57
59
|
const before = new Set(base.dependencies);
|
|
58
60
|
return to.dependencies.filter((d) => !before.has(d)).sort();
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The advisories signal, with what `to` holds (npm, audit 6.2.5 D-4): a dependency it exact-pins
|
|
64
|
+
* or narrowly ranges that an advisory affects is installed with it, so it counts as one of `to`'s
|
|
65
|
+
* own, graded the same way (critical/high: avoid, else review). A held check that could not run
|
|
66
|
+
* never lets the signal say proceed.
|
|
67
|
+
*/
|
|
68
|
+
export function advisoriesSignal(item, facts, held) {
|
|
69
|
+
const own = ownAdvisoriesSignal(item, facts);
|
|
70
|
+
if (!held || (held.status === "checked" && held.affected.length === 0))
|
|
71
|
+
return own;
|
|
72
|
+
if (held.status === "unchecked") {
|
|
73
|
+
const note = `Whether the dependencies ${item.to} exact-pins or narrowly ranges have known advisories was not checked (${held.reason}).`;
|
|
74
|
+
return {
|
|
75
|
+
...own,
|
|
76
|
+
value: own.value === null ? { held_dependencies_unchecked: held.reason } : { ...own.value, held_dependencies_unchecked: held.reason },
|
|
77
|
+
effect: own.effect === "avoid" || own.effect === "review" ? own.effect : "unknown",
|
|
78
|
+
evidence: `${own.evidence} ${note}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const severity = heldSeverity(held.affected);
|
|
82
|
+
const heldEffect = severity === "critical" || severity === "high" ? "avoid" : "review";
|
|
83
|
+
const effect = own.effect === "avoid" || heldEffect === "avoid" ? "avoid" : "review";
|
|
84
|
+
const affected = held.affected.map((d) => ({ package: d.name, spec: d.spec, installs: d.version, advisories: d.ids, severity: d.severity }));
|
|
85
|
+
const heldNote = `${heldText(item.to, held.affected)}: installing ${item.to} installs ${held.affected.length === 1 ? "it" : "them"}.`;
|
|
86
|
+
return {
|
|
87
|
+
id: "advisories",
|
|
88
|
+
value: own.value === null ? { held_dependencies_affected: affected } : { ...own.value, held_dependencies_affected: affected },
|
|
89
|
+
effect,
|
|
90
|
+
// "No known vulnerability affects X." is about X's own advisories; say what it holds instead.
|
|
91
|
+
evidence: own.effect === "proceed"
|
|
92
|
+
? `${own.evidence.replace(`No known vulnerability affects ${item.to}.`, `No advisory of ${item.package} itself affects ${item.to}, but ${heldNote}`)}`
|
|
93
|
+
: `${own.evidence} ${heldNote}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function ownAdvisoriesSignal(item, facts) {
|
|
61
97
|
if (!facts.available) {
|
|
62
98
|
return { id: "advisories", value: null, evidence: `OSV.dev could not be reached: ${facts.detail}`, effect: "unknown" };
|
|
63
99
|
}
|
|
64
100
|
const fromIds = new Set(facts.from.map((a) => a.id));
|
|
65
101
|
const toIds = new Set(facts.to.map((a) => a.id));
|
|
66
102
|
const fixed = facts.from.filter((a) => !toIds.has(a.id) && !a.maintenance);
|
|
67
|
-
|
|
103
|
+
// An unmaintained-package notice names no exploitable vulnerability: it is
|
|
104
|
+
// reported in its own field and never moves the verdict (audit L3, the
|
|
105
|
+
// same split as vulnerability_scan's `maintenance_notices`).
|
|
106
|
+
const notices = facts.to.filter((a) => a.maintenance);
|
|
107
|
+
const value = {
|
|
108
|
+
affecting_to: facts.to.filter((a) => !a.maintenance).map((a) => a.id),
|
|
109
|
+
fixed_by_this_change: fixed.map((a) => a.id),
|
|
110
|
+
...(notices.length > 0 ? { maintenance_notices: notices.map((a) => a.id) } : {}),
|
|
111
|
+
};
|
|
112
|
+
const noticeNote = notices.length > 0
|
|
113
|
+
? ` Unmaintained-package notice${notices.length > 1 ? "s" : ""} ${notices.map((a) => a.id).join(", ")} ${notices.length > 1 ? "apply" : "applies"} to ${item.to}${notices.every((a) => fromIds.has(a.id)) ? ` (already on ${item.from})` : ""}: no exploitable vulnerability, listed in maintenance_notices.`
|
|
114
|
+
: "";
|
|
68
115
|
const fixedNote = fixed.length > 0 ? ` This change fixes ${list(fixed)} affecting ${item.from}.` : "";
|
|
69
116
|
const serious = facts.to.filter((a) => !a.maintenance && (a.severity === "critical" || a.severity === "high"));
|
|
70
117
|
if (serious.length > 0) {
|
|
71
|
-
return { id: "advisories", value, effect: "avoid", evidence: `${item.to} is affected by ${list(serious)}.${fixedNote}` };
|
|
118
|
+
return { id: "advisories", value, effect: "avoid", evidence: `${item.to} is affected by ${list(serious)}.${fixedNote}${noticeNote}` };
|
|
72
119
|
}
|
|
73
120
|
const other = facts.to.filter((a) => !a.maintenance);
|
|
74
|
-
|
|
75
|
-
if (other.length > 0 || newNotices.length > 0) {
|
|
121
|
+
if (other.length > 0) {
|
|
76
122
|
return {
|
|
77
123
|
id: "advisories",
|
|
78
124
|
value,
|
|
79
125
|
effect: "review",
|
|
80
|
-
evidence: `${item.to} is affected by ${list(
|
|
126
|
+
evidence: `${item.to} is affected by ${list(other)}.${fixedNote}${noticeNote}`,
|
|
81
127
|
};
|
|
82
128
|
}
|
|
83
|
-
// Only maintenance notices that `from` already carries can remain here.
|
|
84
|
-
const carried = facts.to.length > 0 ? ` (maintenance notice${facts.to.length > 1 ? "s" : ""} ${facts.to.map((a) => a.id).join(", ")} already apply to ${item.from})` : "";
|
|
85
129
|
return {
|
|
86
130
|
id: "advisories",
|
|
87
131
|
value,
|
|
88
132
|
effect: "proceed",
|
|
89
|
-
evidence: `No known
|
|
133
|
+
evidence: `No known vulnerability affects ${item.to}.${fixedNote}${noticeNote}`,
|
|
90
134
|
};
|
|
91
135
|
}
|
|
92
136
|
function fmt(a) {
|
|
93
|
-
return `${a.id} (${a.maintenance ? "unmaintained notice" : a.severity})`;
|
|
137
|
+
return `${a.id} (${a.maintenance ? "unmaintained notice" : a.severityFrom ? `${a.severity}, per ${a.severityFrom}` : a.severity})`;
|
|
94
138
|
}
|
|
95
139
|
/** At most three named in prose; the full id list is in the signal's `value`. */
|
|
96
140
|
function list(advisories) {
|
|
@@ -241,12 +285,32 @@ export function newDependenciesSignal(item, to, base, introduced, created) {
|
|
|
241
285
|
evidence: `${to.version} adds ${list} since ${base.record.version}.`,
|
|
242
286
|
};
|
|
243
287
|
}
|
|
244
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Yanked, unpublished, retracted: avoid. npm deprecated: avoid when the 4DA rule withdraws it (a
|
|
290
|
+
* later, non-deprecated stable release exists on its caret line, lodash 4.18.0 "Bad release"),
|
|
291
|
+
* and when the notice is this release's alone ("Critical bug in 1.0.3"); review when the rule
|
|
292
|
+
* keeps it and the same notice is on other releases (glob 10.5.0, one of 160 "Old versions of
|
|
293
|
+
* glob are not supported"): a "please upgrade" notice, not a withdrawn release, and possibly
|
|
294
|
+
* the only fix on its line (FX-H5, withdrawn-releases.ts). Without the index: every deprecation avoids.
|
|
295
|
+
*/
|
|
296
|
+
export function withdrawnSignal(item, to, index) {
|
|
245
297
|
if (to.withdrawn) {
|
|
246
298
|
return { id: "yanked_or_deprecated", value: { [to.withdrawn]: true }, effect: "avoid", evidence: `${item.to} was ${to.withdrawn} from the registry.` };
|
|
247
299
|
}
|
|
248
300
|
if (to.deprecated) {
|
|
249
|
-
|
|
301
|
+
const value = { deprecated: to.deprecated };
|
|
302
|
+
// The same notice on other releases too: a "please upgrade" notice about the line or the package.
|
|
303
|
+
const shared = index ? Object.values(index.releases).filter((r) => r.version !== to.version && r.deprecated === to.deprecated).length : 0;
|
|
304
|
+
if (index && item.ecosystem === "npm" && shared > 0 && !excludedReleases(index).has(to.version)) {
|
|
305
|
+
return {
|
|
306
|
+
id: "yanked_or_deprecated",
|
|
307
|
+
value: { ...value, withdrawn: false },
|
|
308
|
+
effect: "review",
|
|
309
|
+
evidence: `${item.to} is deprecated: "${to.deprecated}". The same notice is on ${shared} other release${shared === 1 ? "" : "s"} and no later release on its line is free of one, ` +
|
|
310
|
+
`so it is a notice about the line, not a withdrawn release; tell the user it is deprecated.`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { id: "yanked_or_deprecated", value, effect: "avoid", evidence: `${item.to} is deprecated: "${to.deprecated}".` };
|
|
250
314
|
}
|
|
251
315
|
return { id: "yanked_or_deprecated", value: null, effect: "proceed", evidence: `${item.to} is neither yanked nor deprecated.` };
|
|
252
316
|
}
|
|
@@ -301,7 +365,14 @@ export function decide(signals) {
|
|
|
301
365
|
const fixes = signals.find((s) => s.id === "release_age" && /security fix/.test(s.evidence));
|
|
302
366
|
return { verdict, reason: fixes ? fixes.evidence : "No advisory, age, trust, script or new-dependency concern found." };
|
|
303
367
|
}
|
|
304
|
-
|
|
368
|
+
// A stronger verdict must not hide what could not be checked: crossenv `latest` (the malware
|
|
369
|
+
// 1.0.1) read `review` for "major" alone while its advisory lookup had failed (audit 6.2.5 D-10).
|
|
370
|
+
const unchecked = verdict === "unknown" ? [] : signals.filter((s) => s.effect === "unknown");
|
|
371
|
+
const reason = hits.map((s) => `${s.id}: ${s.evidence}`).join(" | ");
|
|
372
|
+
return {
|
|
373
|
+
verdict,
|
|
374
|
+
reason: unchecked.length > 0 ? `${reason} | NOT CHECKED: ${unchecked.map((s) => `${s.id}: ${s.evidence}`).join(" | ")}` : reason,
|
|
375
|
+
};
|
|
305
376
|
}
|
|
306
377
|
return { verdict: "unknown", reason: "No signal could be evaluated." };
|
|
307
378
|
}
|
|
@@ -76,13 +76,38 @@ export interface RangeCheck {
|
|
|
76
76
|
newest_safe_outside_range?: string | null;
|
|
77
77
|
/** When `from` is given: the smallest safe release above it (the minimal fixing upgrade). */
|
|
78
78
|
smallest_safe_upgrade?: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Releases in range that no advisory affects but the registry withdrew (yanked; npm
|
|
81
|
+
* deprecated with a later clean release on its line), below the smallest safe pick (all of
|
|
82
|
+
* them when none is safe), and why in plain words (FX-H5).
|
|
83
|
+
*/
|
|
84
|
+
withheld?: string[];
|
|
85
|
+
withheld_note?: string;
|
|
86
|
+
/**
|
|
87
|
+
* npm (audit 6.2.5 D-4): releases left out of the safe ones because a dependency they exact-pin
|
|
88
|
+
* or narrowly range is affected, or because that could not be checked; and why, in words.
|
|
89
|
+
*/
|
|
90
|
+
held_excluded?: number;
|
|
91
|
+
held_note?: string;
|
|
79
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The releases in range that the package's own advisories leave safe, ascending: what checkRange
|
|
95
|
+
* would call safe before held dependencies are judged. dependency_check judges the held
|
|
96
|
+
* dependencies of the smallest and newest of these (audit 6.2.5 D-4).
|
|
97
|
+
*/
|
|
98
|
+
export declare function ownSafeInRange(input: {
|
|
99
|
+
ecosystem: CheckEcosystem;
|
|
100
|
+
index: PackageReleaseIndex;
|
|
101
|
+
range: string;
|
|
102
|
+
advisories: AdvisoryRanges[];
|
|
103
|
+
}): string[];
|
|
80
104
|
/**
|
|
81
105
|
* The releases a constraint admits, judged against every advisory OSV.dev
|
|
82
|
-
* lists for the package. Withdrawn (yanked / unpublished /
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
106
|
+
* lists for the package. Withdrawn releases (yanked / unpublished /
|
|
107
|
+
* retracted, and npm deprecations the 4DA rule withdraws: withdrawn-releases.ts)
|
|
108
|
+
* are not candidates; neither are prereleases, unless the constraint admits
|
|
109
|
+
* nothing else. A whole-line npm deprecation notice (glob 10.x, tar 6.x)
|
|
110
|
+
* withdraws nothing. Maintenance notices do not make a release unsafe.
|
|
86
111
|
*/
|
|
87
112
|
export declare function checkRange(input: {
|
|
88
113
|
ecosystem: CheckEcosystem;
|
|
@@ -91,4 +116,12 @@ export declare function checkRange(input: {
|
|
|
91
116
|
basis: RangeCheck["basis"];
|
|
92
117
|
advisories: AdvisoryRanges[];
|
|
93
118
|
from?: string;
|
|
119
|
+
/**
|
|
120
|
+
* npm: a release whose held dependencies (exact pins, narrow ranges) are affected or could not
|
|
121
|
+
* be judged is not safe either (audit 6.2.5 D-4), and `heldNote` says why when any is left out.
|
|
122
|
+
*/
|
|
123
|
+
held?: {
|
|
124
|
+
unsafe: (version: string) => boolean;
|
|
125
|
+
note: string;
|
|
126
|
+
};
|
|
94
127
|
}): RangeCheck;
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* "v1" for a Go major. Pure functions, no I/O.
|
|
16
16
|
*/
|
|
17
17
|
import { cargoReqAdmits, npmRangeAdmits } from "../live/range-satisfies.js";
|
|
18
|
-
import {
|
|
18
|
+
import { advisoriesAffecting, isPrereleaseVersion } from "../live/osv-ranges.js";
|
|
19
19
|
import { compareVersions, parsePep440 } from "../live/version-compare.js";
|
|
20
20
|
import { parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
21
21
|
import { GO_VERSION, goVersion } from "../live/go-releases.js";
|
|
22
|
+
import { excludedReleases, withheldNote } from "../live/withdrawn-releases.js";
|
|
22
23
|
const FULL_SEMVER = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
23
24
|
/** Read a `to` value; null when it is neither a version nor a constraint this ecosystem understands. */
|
|
24
25
|
export function parseTarget(ecosystem, raw) {
|
|
@@ -174,22 +175,39 @@ export function lineOf(ecosystem, version) {
|
|
|
174
175
|
return `v${p.major}`;
|
|
175
176
|
return p.major === 0 ? `0.${p.minor}.x` : `${p.major}.x`;
|
|
176
177
|
}
|
|
178
|
+
/** The installable releases a constraint admits, ascending; prereleases only when it admits nothing else. */
|
|
179
|
+
function rangeCandidates(ecosystem, installable, range) {
|
|
180
|
+
const admitted = installable.filter((v) => rangeAdmits(ecosystem, range, v) === true);
|
|
181
|
+
const stable = admitted.filter((v) => !isPrereleaseIn(ecosystem, v));
|
|
182
|
+
return { candidates: (stable.length > 0 ? stable : admitted).sort((a, b) => compareIn(ecosystem, a, b)), stable };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The releases in range that the package's own advisories leave safe, ascending: what checkRange
|
|
186
|
+
* would call safe before held dependencies are judged. dependency_check judges the held
|
|
187
|
+
* dependencies of the smallest and newest of these (audit 6.2.5 D-4).
|
|
188
|
+
*/
|
|
189
|
+
export function ownSafeInRange(input) {
|
|
190
|
+
const excluded = excludedReleases(input.index);
|
|
191
|
+
const installable = Object.values(input.index.releases).filter((r) => !excluded.has(r.version)).map((r) => r.version);
|
|
192
|
+
return rangeCandidates(input.ecosystem, installable, input.range).candidates.filter((v) => advisoriesAffecting(input.advisories, v, input.ecosystem).length === 0);
|
|
193
|
+
}
|
|
177
194
|
/**
|
|
178
195
|
* The releases a constraint admits, judged against every advisory OSV.dev
|
|
179
|
-
* lists for the package. Withdrawn (yanked / unpublished /
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
196
|
+
* lists for the package. Withdrawn releases (yanked / unpublished /
|
|
197
|
+
* retracted, and npm deprecations the 4DA rule withdraws: withdrawn-releases.ts)
|
|
198
|
+
* are not candidates; neither are prereleases, unless the constraint admits
|
|
199
|
+
* nothing else. A whole-line npm deprecation notice (glob 10.x, tar 6.x)
|
|
200
|
+
* withdraws nothing. Maintenance notices do not make a release unsafe.
|
|
183
201
|
*/
|
|
184
202
|
export function checkRange(input) {
|
|
185
203
|
const { ecosystem, index, range, basis } = input;
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const stable =
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const safe =
|
|
204
|
+
const excluded = excludedReleases(index);
|
|
205
|
+
const installable = Object.values(index.releases).filter((r) => !excluded.has(r.version)).map((r) => r.version);
|
|
206
|
+
const { candidates, stable } = rangeCandidates(ecosystem, installable, range);
|
|
207
|
+
// The same predicate every tool runs on a release it offers (osv-ranges.ts advisoriesAffecting).
|
|
208
|
+
const affecting = (v) => advisoriesAffecting(input.advisories, v, ecosystem);
|
|
209
|
+
const ownSafe = candidates.filter((v) => affecting(v).length === 0);
|
|
210
|
+
const safe = input.held ? ownSafe.filter((v) => !input.held.unsafe(v)) : ownSafe;
|
|
193
211
|
const out = {
|
|
194
212
|
range,
|
|
195
213
|
basis,
|
|
@@ -202,12 +220,29 @@ export function checkRange(input) {
|
|
|
202
220
|
if (candidates.length > 0 && safe.length === 0) {
|
|
203
221
|
out.affecting_all = [...new Set(candidates.flatMap(affecting))].filter((id) => candidates.every((v) => affecting(v).includes(id))).slice(0, 10);
|
|
204
222
|
const outside = installable
|
|
205
|
-
.filter((v) => !isPrereleaseIn(ecosystem, v) && affecting(v).length === 0)
|
|
223
|
+
.filter((v) => !isPrereleaseIn(ecosystem, v) && affecting(v).length === 0 && !input.held?.unsafe(v))
|
|
206
224
|
.sort((a, b) => compareIn(ecosystem, a, b));
|
|
207
225
|
out.newest_safe_outside_range = outside[outside.length - 1] ?? null;
|
|
208
226
|
}
|
|
209
227
|
if (input.from) {
|
|
210
228
|
out.smallest_safe_upgrade = safe.find((v) => compareIn(ecosystem, v, input.from) > 0) ?? null;
|
|
211
229
|
}
|
|
230
|
+
if (input.held && safe.length < ownSafe.length) {
|
|
231
|
+
out.held_excluded = ownSafe.length - safe.length;
|
|
232
|
+
out.held_note = input.held.note;
|
|
233
|
+
}
|
|
234
|
+
// The withdrawn releases that would otherwise have been the smallest safe pick (FX-H5).
|
|
235
|
+
const pick = input.from ? out.smallest_safe_upgrade : out.smallest_safe_in_range;
|
|
236
|
+
const withheld = [...excluded.keys()]
|
|
237
|
+
.filter((v) => rangeAdmits(ecosystem, range, v) === true &&
|
|
238
|
+
(stable.length === 0 || !isPrereleaseIn(ecosystem, v)) &&
|
|
239
|
+
(!input.from || compareIn(ecosystem, v, input.from) > 0) &&
|
|
240
|
+
(!pick || compareIn(ecosystem, v, pick) < 0) &&
|
|
241
|
+
affecting(v).length === 0)
|
|
242
|
+
.sort((a, b) => compareIn(ecosystem, a, b));
|
|
243
|
+
if (withheld.length > 0) {
|
|
244
|
+
out.withheld = withheld;
|
|
245
|
+
out.withheld_note = withheldNote(withheld, pick ?? null, Object.fromEntries(excluded), ecosystem);
|
|
246
|
+
}
|
|
212
247
|
return out;
|
|
213
248
|
}
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
*/
|
|
38
38
|
import type { FourDADatabase } from "../db.js";
|
|
39
39
|
import type { LiveIntelligence } from "../live/index.js";
|
|
40
|
-
import
|
|
40
|
+
import type { VulnerabilityEntry } from "../live/types.js";
|
|
41
|
+
import { type AdvisoryRef, type Signal, type Verdict } from "./dependency-check-signals.js";
|
|
41
42
|
import { type RangeCheck } from "./dependency-check-versions.js";
|
|
42
43
|
export interface DependencyCheckParams {
|
|
43
44
|
items?: unknown;
|
|
@@ -92,6 +93,8 @@ interface ItemResult {
|
|
|
92
93
|
reason: string;
|
|
93
94
|
signals: Signal[];
|
|
94
95
|
range_check?: RangeCheck;
|
|
96
|
+
/** Unmaintained-package notices on `to`: reported, never part of the verdict (audit L3). */
|
|
97
|
+
maintenance_notices?: string[];
|
|
95
98
|
}
|
|
96
99
|
export declare function executeDependencyCheck(_db: FourDADatabase | null, params: DependencyCheckParams, liveIntel: LiveIntelligence | null, opts?: {
|
|
97
100
|
now?: Date;
|
|
@@ -111,4 +114,11 @@ export declare function executeDependencyCheck(_db: FourDADatabase | null, param
|
|
|
111
114
|
};
|
|
112
115
|
};
|
|
113
116
|
}>;
|
|
117
|
+
/**
|
|
118
|
+
* One reference per vulnerability: OSV lists GHSA and RUSTSEC/PYSEC records
|
|
119
|
+
* for the same issue, each naming the other in `aliases`. They were counted
|
|
120
|
+
* twice ("6 advisories" for tokio 1.18.0's three, audit A9). The kept record
|
|
121
|
+
* of an alias group is its most severe, so merging never softens a verdict.
|
|
122
|
+
*/
|
|
123
|
+
export declare function refs(entries: VulnerabilityEntry[] | undefined): AdvisoryRef[];
|
|
114
124
|
export {};
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
* `.info` is read from it. OSV receives name + version, as vulnerability_scan
|
|
37
37
|
* already does, and the package name for its advisory list.
|
|
38
38
|
*/
|
|
39
|
+
import { groupAdvisories } from "../live/advisory-groups.js";
|
|
39
40
|
import { isMaintenanceNotice } from "../live/maintenance.js";
|
|
40
41
|
import { goPathMajor, goVersionMajor } from "../live/upgrade-go.js";
|
|
41
42
|
import { advisoriesSignal, decide, installScriptSignal, introducedDependencies, newDependenciesSignal, publishTrustSignal, releaseAgeSignal, selectBaseline, upgradeTypeSignal, withdrawnSignal, WAIT_DAYS, } from "./dependency-check-signals.js";
|
|
42
|
-
import { checkRange, compareIn, isPrereleaseIn, lineOf, parseTarget, rangeAdmits } from "./dependency-check-versions.js";
|
|
43
|
+
import { checkRange, compareIn, isPrereleaseIn, lineOf, ownSafeInRange, parseTarget, rangeAdmits } from "./dependency-check-versions.js";
|
|
44
|
+
import { HeldJudge, heldRangeCandidates } from "./dependency-check-held.js";
|
|
43
45
|
const MAX_ITEMS = 25;
|
|
44
46
|
/** First-publish lookups for newly introduced dependencies, per call (each is a registry read). */
|
|
45
47
|
const MAX_NEW_DEP_LOOKUPS = 40;
|
|
@@ -57,7 +59,7 @@ export const dependencyCheckTool = {
|
|
|
57
59
|
"Is this dependency version safe to write? Call BEFORE you write any version into a manifest or lockfile: adding, pinning, upgrading, bumping or downgrading a package, or choosing a version (npm, crates.io, PyPI, Go). Pass the exact version you intend to write, or the constraint you were given (\"7.x\", \"^1.20\", \"~=3.1\", \"v1\") to get the smallest and newest safe release in it. Write only a version that came back `proceed`. Your memory of which release is safe is out of date; this reads the registry and OSV.dev now.",
|
|
58
60
|
"",
|
|
59
61
|
"Per item: `verdict` (proceed | wait | review | avoid | unknown), a one-line `reason`, `signals` (each with `id`, `value`, `evidence`, `effect`), and `range_check` when `to` is a constraint or an exact `to` has an advisory: `smallest_safe_in_range`, `newest_safe_in_range`, `safe_versions`, `smallest_safe_upgrade` (above `from`). For a constraint the verdict is for the version it picks: the smallest safe upgrade above `from`, else the newest safe release.",
|
|
60
|
-
"- advisories: OSV advisories affecting `to` (high/critical -> avoid, other -> review) and those this change fixes on `from`.",
|
|
62
|
+
"- advisories: OSV advisories affecting `to` (high/critical -> avoid, other -> review) and those this change fixes on `from`. npm: a dependency `to` exact-pins, or narrowly ranges with no clean release in reach, is installed with it, so its advisories count the same (`held_dependencies_affected`), and such releases are never among the safe ones.",
|
|
61
63
|
"- release_age: under 3 days -> wait, unless `to` fixes an advisory affecting `from`, or every release on its line past the hold has an advisory and no safe one is past it (then proceed; tell the user its age).",
|
|
62
64
|
"- yanked_or_deprecated: yanked, unpublished, retracted (Go) or deprecated -> avoid.",
|
|
63
65
|
"- publish_trust (npm, crates.io): trusted publisher / provenance dropped -> review. install_script_added (npm): new install script -> review. new_dependencies (npm, crates.io): a new runtime dependency under 30 days old -> review. Not applicable elsewhere, and said so.",
|
|
@@ -109,7 +111,7 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
109
111
|
const indexes = await fetchIndexes(requested.map(provisional), sources, new Set(requested.filter(needsFullList).map(lookupKey)));
|
|
110
112
|
// Constraints are judged against the package's whole advisory list.
|
|
111
113
|
const advisoryLists = await packageAdvisoryLists(fixSources, requested.filter((r) => r.target.kind === "range" || r.target.kind === "exact_or_series"));
|
|
112
|
-
|
|
114
|
+
let items = requested.map((r) => resolveTarget(r, indexes.get(lookupKey(r)), advisoryLists.get(lookupKey(r)), now));
|
|
113
115
|
// Go dates come from per-version .info: read them for versions chosen from the list
|
|
114
116
|
// (a constraint, "latest", or `v2.0.0` listed as `v2.0.0+incompatible`).
|
|
115
117
|
const goRedo = items.filter((i) => {
|
|
@@ -121,34 +123,72 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
121
123
|
indexes.set(key, lookup);
|
|
122
124
|
await settleGoPicks(goRedo, indexes, sources.Go, now);
|
|
123
125
|
}
|
|
126
|
+
// npm: what a release installs at a fixed version comes with it (audit 6.2.5 D-4: express 4.20.0
|
|
127
|
+
// exact-pins an affected qs). Judge `to` and, for a constraint, the smallest and newest safe
|
|
128
|
+
// releases in it, then pick again among the releases that hold nothing affected.
|
|
129
|
+
const held = new HeldJudge(fixSources, (deps) => liveIntel.queryAdvisories(deps));
|
|
130
|
+
const isNpm = (i) => i.ecosystem === "npm" && !i.unresolved;
|
|
131
|
+
await held.judge(items.filter(isNpm).map((item) => {
|
|
132
|
+
const lookup = indexes.get(lookupKey(item));
|
|
133
|
+
const advisories = advisoryLists.get(lookupKey(item));
|
|
134
|
+
const ownSafe = item.rangeCheck && lookup?.status === "ok" && advisories
|
|
135
|
+
? ownSafeInRange({ ecosystem: "npm", index: lookup.data, range: item.rangeCheck.range, advisories })
|
|
136
|
+
: [];
|
|
137
|
+
return { pkg: item.package, versions: [item.to, ...heldRangeCandidates(ownSafe, (v) => !item.from || compareIn("npm", v, item.from) > 0)] };
|
|
138
|
+
}));
|
|
139
|
+
items = items.map((item, i) => {
|
|
140
|
+
if (!isNpm(item) || item.rangeCheck?.basis !== "requested")
|
|
141
|
+
return item;
|
|
142
|
+
const filter = held.rangeFilter(item.package);
|
|
143
|
+
const r = requested[i];
|
|
144
|
+
return filter ? resolveTarget(r, indexes.get(lookupKey(r)), advisoryLists.get(lookupKey(r)), now, filter) : item;
|
|
145
|
+
});
|
|
146
|
+
// The pick may have moved to a release judged only through its range: judge it too (normally a no-op).
|
|
147
|
+
await held.judge(items.filter(isNpm).map((item) => ({ pkg: item.package, versions: [item.to] })));
|
|
148
|
+
const heldAt = (item) => (item.ecosystem === "npm" ? held.at(item.package, item.to) : undefined);
|
|
124
149
|
const osvDeps = items
|
|
125
150
|
.filter((i) => !i.unresolved)
|
|
126
151
|
.flatMap((item) => [item.to, item.from].filter((v) => Boolean(v)).map((version) => osvDep(item, version)));
|
|
127
152
|
const scan = await liveIntel.queryAdvisories(osvDeps);
|
|
153
|
+
// Unrated (Go) records carry their GHSA alias's grade from the scanner, as in every tool (severityFrom).
|
|
128
154
|
const byInstance = new Map();
|
|
129
155
|
for (const v of scan.vulnerabilities) {
|
|
130
156
|
const key = instanceKey(v.ecosystem, v.package, v.currentVersion);
|
|
131
157
|
byInstance.set(key, [...(byInstance.get(key) ?? []), v]);
|
|
132
158
|
}
|
|
159
|
+
// A pre-install check needs a current answer: an expired cached one (OSV failed) can predate
|
|
160
|
+
// the advisory that matters, and read as `proceed` (audit 6.2.5 B-2).
|
|
133
161
|
const advisoriesFor = (item) => scan.offline
|
|
134
162
|
? { available: false, detail: "lookup failed or was rate limited" }
|
|
135
|
-
:
|
|
136
|
-
available:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
: (scan.staleAnswers ?? 0) > 0
|
|
164
|
+
? { available: false, detail: `no current answer, only an expired cached one (${scan.staleReason ?? "the fresh query failed"})` }
|
|
165
|
+
: {
|
|
166
|
+
available: true,
|
|
167
|
+
to: refs(byInstance.get(instanceKey(item.ecosystem, item.package, item.to))),
|
|
168
|
+
from: item.from ? refs(byInstance.get(instanceKey(item.ecosystem, item.package, item.from))) : [],
|
|
169
|
+
};
|
|
170
|
+
// An exact `to` an advisory affects (its own, or one it holds): the safe releases on its own line.
|
|
171
|
+
const affectedExact = items.filter((i) => !i.unresolved &&
|
|
172
|
+
!i.rangeCheck &&
|
|
173
|
+
((advisoriesFor(i).available && hasRealAdvisory(advisoriesFor(i))) || (i.ecosystem === "npm" && held.affected(i.package, i.to))));
|
|
142
174
|
const moreLists = await packageAdvisoryLists(fixSources, affectedExact.filter((i) => !advisoryLists.has(lookupKey(i))));
|
|
143
175
|
for (const [k, v] of moreLists)
|
|
144
176
|
advisoryLists.set(k, v);
|
|
145
|
-
|
|
177
|
+
const lines = affectedExact.flatMap((item) => {
|
|
146
178
|
const lookup = indexes.get(lookupKey(item));
|
|
147
179
|
const advisories = advisoryLists.get(lookupKey(item));
|
|
148
180
|
const line = lineOf(item.ecosystem, item.to);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
return lookup.status === "ok" && advisories && line ? [{ item, index: lookup.data, advisories, line }] : [];
|
|
182
|
+
});
|
|
183
|
+
await held.judge(lines
|
|
184
|
+
.filter((l) => l.item.ecosystem === "npm")
|
|
185
|
+
.map((l) => ({
|
|
186
|
+
pkg: l.item.package,
|
|
187
|
+
versions: heldRangeCandidates(ownSafeInRange({ ecosystem: "npm", index: l.index, range: l.line, advisories: l.advisories }), (v) => !l.item.from || compareIn("npm", v, l.item.from) > 0),
|
|
188
|
+
})));
|
|
189
|
+
for (const { item, index, advisories, line } of lines) {
|
|
190
|
+
const filter = item.ecosystem === "npm" ? held.rangeFilter(item.package) : undefined;
|
|
191
|
+
item.rangeCheck = checkRange({ ecosystem: item.ecosystem, index, range: line, basis: "same_line", advisories, from: item.from, ...(filter ? { held: filter } : {}) });
|
|
152
192
|
}
|
|
153
193
|
const holdWaivers = await findHoldWaivers(items, indexes, advisoryLists, fixSources, sources.Go, advisoriesFor, now);
|
|
154
194
|
// Phase 2: first-publish dates of newly introduced dependencies.
|
|
@@ -168,12 +208,16 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
168
208
|
});
|
|
169
209
|
const results = plans.map((p) => {
|
|
170
210
|
const extra = p.item.rangeCheck ? { range_check: p.item.rangeCheck } : {};
|
|
211
|
+
const noticesOf = (s) => {
|
|
212
|
+
const ids = s.value?.maintenance_notices;
|
|
213
|
+
return ids && ids.length > 0 ? { maintenance_notices: ids } : {};
|
|
214
|
+
};
|
|
171
215
|
if (p.blocked) {
|
|
172
216
|
// No release metadata: only the advisory lookup can still speak, and
|
|
173
217
|
// only a negative finding is worth more than "unknown".
|
|
174
218
|
if (p.item.unresolved)
|
|
175
219
|
return { ...head(p.item), verdict: "unknown", reason: p.blocked, signals: [], ...extra };
|
|
176
|
-
const advisories = advisoriesSignal(p.item, advisoriesFor(p.item));
|
|
220
|
+
const advisories = advisoriesSignal(p.item, advisoriesFor(p.item), heldAt(p.item));
|
|
177
221
|
const negative = advisories.effect === "avoid" || advisories.effect === "review";
|
|
178
222
|
return {
|
|
179
223
|
...head(p.item),
|
|
@@ -181,12 +225,13 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
181
225
|
reason: withAlternatives(negative ? `advisories: ${advisories.evidence} | ${p.blocked}` : p.blocked, p.item, negative),
|
|
182
226
|
signals: [advisories],
|
|
183
227
|
...extra,
|
|
228
|
+
...noticesOf(advisories),
|
|
184
229
|
};
|
|
185
230
|
}
|
|
186
231
|
const { item, index } = p;
|
|
187
232
|
const to = index.releases[item.to];
|
|
188
233
|
const base = selectBaseline(index, item);
|
|
189
|
-
const advisories = advisoriesSignal(item, advisoriesFor(item));
|
|
234
|
+
const advisories = advisoriesSignal(item, advisoriesFor(item), heldAt(item));
|
|
190
235
|
const createdForItem = new Map();
|
|
191
236
|
for (const name of p.introduced ?? []) {
|
|
192
237
|
const fact = created.get(`${item.ecosystem}\0${name}`);
|
|
@@ -199,12 +244,12 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
199
244
|
publishTrustSignal(item, to, base),
|
|
200
245
|
installScriptSignal(item, to, item.from ? index.releases[item.from] ?? null : null),
|
|
201
246
|
newDependenciesSignal(item, to, base, p.introduced, createdForItem),
|
|
202
|
-
withdrawnSignal(item, to),
|
|
247
|
+
withdrawnSignal(item, to, index),
|
|
203
248
|
upgradeTypeSignal(item),
|
|
204
249
|
];
|
|
205
250
|
const decided = decide(signals);
|
|
206
251
|
const unsafe = decided.verdict === "avoid" || decided.verdict === "review";
|
|
207
|
-
return { ...head(item), verdict: decided.verdict, reason: withAlternatives(decided.reason, item, unsafe), signals, ...extra };
|
|
252
|
+
return { ...head(item), verdict: decided.verdict, reason: withAlternatives(decided.reason, item, unsafe), signals, ...extra, ...noticesOf(advisories) };
|
|
208
253
|
});
|
|
209
254
|
return finish(checkedAt, results, { offline: scan.offline, osvCached: scan.cached });
|
|
210
255
|
}
|
|
@@ -218,10 +263,15 @@ function withAlternatives(reason, item, unsafe) {
|
|
|
218
263
|
const ids = rc.affecting_all ?? [];
|
|
219
264
|
const all = ids.length ? `: ${ids.slice(0, 3).join(", ")}${ids.length > 3 ? ` and ${ids.length - 3} more` : ""} affect every one` : "";
|
|
220
265
|
const outside = rc.newest_safe_outside_range ? ` Newest safe release outside it: ${rc.newest_safe_outside_range}.` : "";
|
|
221
|
-
|
|
266
|
+
const withheld = rc.withheld_note ? ` ${rc.withheld_note}.` : "";
|
|
267
|
+
return `${reason} | No release ${where} is free of known advisories${all}.${withheld}${outside}`;
|
|
222
268
|
}
|
|
223
269
|
const upgrade = rc.smallest_safe_upgrade && rc.smallest_safe_upgrade !== rc.smallest_safe_in_range ? `, smallest above ${item.from} ${rc.smallest_safe_upgrade}` : "";
|
|
224
|
-
|
|
270
|
+
// FX-H5: a withdrawn release the pick steps over is named, in the registry's words.
|
|
271
|
+
const withheld = rc.withheld_note ? ` ${rc.withheld_note}.` : "";
|
|
272
|
+
// audit 6.2.5 D-4: releases passed over for what they hold are counted, never dropped silently.
|
|
273
|
+
const heldOut = rc.held_excluded ? ` ${rc.held_excluded} release${rc.held_excluded === 1 ? "" : "s"} clear of ${item.package}'s own advisories left out: ${rc.held_note}.` : "";
|
|
274
|
+
const safe = `Safe ${where}: smallest ${rc.smallest_safe_in_range}${upgrade}, newest ${rc.newest_safe_in_range}.${withheld}${heldOut}`;
|
|
225
275
|
if (rc.basis === "same_line" && !unsafe)
|
|
226
276
|
return reason;
|
|
227
277
|
return `${reason} | ${safe}`;
|
|
@@ -242,7 +292,7 @@ function needsFullList(r) {
|
|
|
242
292
|
* the proxy lists it), "latest" (newest stable, not withdrawn), or the
|
|
243
293
|
* version a constraint recommends.
|
|
244
294
|
*/
|
|
245
|
-
function resolveTarget(r, lookup, advisories, now) {
|
|
295
|
+
function resolveTarget(r, lookup, advisories, now, held) {
|
|
246
296
|
const base = provisional(r);
|
|
247
297
|
if (lookup.status !== "ok")
|
|
248
298
|
return base;
|
|
@@ -275,7 +325,7 @@ function resolveTarget(r, lookup, advisories, now) {
|
|
|
275
325
|
// Without the advisory list no release can be called safe; judge the newest one on its own advisories.
|
|
276
326
|
return { ...base, to: newestOf(r.ecosystem, admitted), requested: r.raw };
|
|
277
327
|
}
|
|
278
|
-
const rangeCheck = checkRange({ ecosystem: r.ecosystem, index, range, basis: "requested", advisories, from: r.from });
|
|
328
|
+
const rangeCheck = checkRange({ ecosystem: r.ecosystem, index, range, basis: "requested", advisories, from: r.from, ...(held ? { held } : {}) });
|
|
279
329
|
const safe = rangeCheck.safe_versions;
|
|
280
330
|
const settled = (v) => {
|
|
281
331
|
const at = index.releases[v]?.publishedAt;
|
|
@@ -490,13 +540,15 @@ function osvDep(item, version) {
|
|
|
490
540
|
sourceDirs: [],
|
|
491
541
|
};
|
|
492
542
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
543
|
+
/**
|
|
544
|
+
* One reference per vulnerability: OSV lists GHSA and RUSTSEC/PYSEC records
|
|
545
|
+
* for the same issue, each naming the other in `aliases`. They were counted
|
|
546
|
+
* twice ("6 advisories" for tokio 1.18.0's three, audit A9). The kept record
|
|
547
|
+
* of an alias group is its most severe, so merging never softens a verdict.
|
|
548
|
+
*/
|
|
549
|
+
export function refs(entries) {
|
|
550
|
+
return groupAdvisories(entries).map(({ best: v }) => {
|
|
551
|
+
return { id: v.vulnId, severity: v.severity, summary: v.summary, maintenance: isMaintenanceNotice(v), ...(v.severityFrom ? { severityFrom: v.severityFrom } : {}) };
|
|
500
552
|
});
|
|
501
553
|
}
|
|
502
554
|
const instanceKey = (eco, name, version) => `${eco}\0${eco === "PyPI" ? normalizePypi(name) : name}\0${version}`;
|