@4da/mcp-server 5.0.4 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +698 -0
- package/README.md +70 -39
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -58
- package/dist/db.js +226 -299
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -136
- package/package.json +35 -35
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vulnerability_scan output: alias collapsing, relevance splits,
|
|
3
|
+
* recommendations, install drift, the shared severity grade, and resolution
|
|
4
|
+
* provenance. Split out of vulnerability-scan.ts, which keeps execution.
|
|
5
|
+
*/
|
|
6
|
+
import type { InstallDriftRecord, ResolutionProvenance, VulnerabilityScanResult } from "../live/types.js";
|
|
7
|
+
/** What the formatter needs beyond the scan itself. */
|
|
8
|
+
export interface ScanContext {
|
|
9
|
+
resolution: ResolutionProvenance | null;
|
|
10
|
+
reResolvedThisCall: boolean;
|
|
11
|
+
drift: InstallDriftRecord[];
|
|
12
|
+
includeDev: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Collapse advisories that are the same finding under different identifiers.
|
|
16
|
+
*
|
|
17
|
+
* OSV returns GHSA and RUSTSEC records for one vulnerability, each naming the
|
|
18
|
+
* other in `aliases`. Reported separately they double the count and read as two
|
|
19
|
+
* problems: `quinn-proto` showed as "2 high" for a single memory-exhaustion
|
|
20
|
+
* bug. Keeps the entry with the most usable detail (a CVSS score, then a fix
|
|
21
|
+
* version) and folds the rest into its alias list.
|
|
22
|
+
*/
|
|
23
|
+
export declare function collapseAliases(vulns: VulnerabilityScanResult["vulnerabilities"]): import("../live/types.js").VulnerabilityEntry[];
|
|
24
|
+
/**
|
|
25
|
+
* Describe the platform filter truthfully, based on whether it actually ran.
|
|
26
|
+
*
|
|
27
|
+
* A crate is only knowable as platform-inactive when cargo could resolve the
|
|
28
|
+
* graph for this host. When it could not (cargo missing, offline resolution
|
|
29
|
+
* impossible, a non-Rust tree), every crate stays active — and saying
|
|
30
|
+
* "advisories are filtered to the host platform" would then be a claim about
|
|
31
|
+
* work that never happened.
|
|
32
|
+
*/
|
|
33
|
+
export declare function platformFilterNote(vulns: VulnerabilityScanResult["vulnerabilities"]): string;
|
|
34
|
+
/** Shortest readable label for a manifest directory, relative to the scan root. */
|
|
35
|
+
export declare function relativeDir(dir: string, root: string): string;
|
|
36
|
+
export declare function formatScanResult(result: VulnerabilityScanResult, severityFilter: string | undefined, ctx: ScanContext): {
|
|
37
|
+
scan_time: string;
|
|
38
|
+
project: string;
|
|
39
|
+
project_root: string;
|
|
40
|
+
workspaces_with_findings: string[];
|
|
41
|
+
host_platform: string;
|
|
42
|
+
ecosystems_scanned: string[];
|
|
43
|
+
total_dependencies_scanned: number;
|
|
44
|
+
total_vulnerable_packages: number;
|
|
45
|
+
platform_inactive_packages: number;
|
|
46
|
+
by_severity: import("../live/severity-scope.js").SeverityCounts;
|
|
47
|
+
advisory_by_severity: import("../live/severity-scope.js").SeverityCounts;
|
|
48
|
+
vulnerabilities: {
|
|
49
|
+
package: string;
|
|
50
|
+
current_version: string;
|
|
51
|
+
/** Manifest directories pinning this version — where to apply the fix. */
|
|
52
|
+
source_dirs: string[];
|
|
53
|
+
ecosystem: import("../live/types.js").OsvEcosystem;
|
|
54
|
+
is_dev: boolean;
|
|
55
|
+
is_direct: boolean;
|
|
56
|
+
dev_scope_known: boolean;
|
|
57
|
+
target: string | null;
|
|
58
|
+
platform_active: boolean;
|
|
59
|
+
vuln_id: string;
|
|
60
|
+
aliases: string[];
|
|
61
|
+
severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
62
|
+
advisory_severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
63
|
+
severity_note?: string | undefined;
|
|
64
|
+
cvss_score: number | null;
|
|
65
|
+
summary: string;
|
|
66
|
+
fixed_version: string | null;
|
|
67
|
+
published: string;
|
|
68
|
+
references: string[];
|
|
69
|
+
}[];
|
|
70
|
+
platform_inactive_vulnerabilities: {
|
|
71
|
+
package: string;
|
|
72
|
+
current_version: string;
|
|
73
|
+
/** Manifest directories pinning this version — where to apply the fix. */
|
|
74
|
+
source_dirs: string[];
|
|
75
|
+
ecosystem: import("../live/types.js").OsvEcosystem;
|
|
76
|
+
is_dev: boolean;
|
|
77
|
+
is_direct: boolean;
|
|
78
|
+
dev_scope_known: boolean;
|
|
79
|
+
target: string | null;
|
|
80
|
+
platform_active: boolean;
|
|
81
|
+
vuln_id: string;
|
|
82
|
+
aliases: string[];
|
|
83
|
+
severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
84
|
+
advisory_severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
85
|
+
severity_note?: string | undefined;
|
|
86
|
+
cvss_score: number | null;
|
|
87
|
+
summary: string;
|
|
88
|
+
fixed_version: string | null;
|
|
89
|
+
published: string;
|
|
90
|
+
references: string[];
|
|
91
|
+
}[];
|
|
92
|
+
maintenance_notices: {
|
|
93
|
+
package: string;
|
|
94
|
+
current_version: string;
|
|
95
|
+
/** Manifest directories pinning this version — where to apply the fix. */
|
|
96
|
+
source_dirs: string[];
|
|
97
|
+
ecosystem: import("../live/types.js").OsvEcosystem;
|
|
98
|
+
is_dev: boolean;
|
|
99
|
+
is_direct: boolean;
|
|
100
|
+
dev_scope_known: boolean;
|
|
101
|
+
target: string | null;
|
|
102
|
+
platform_active: boolean;
|
|
103
|
+
vuln_id: string;
|
|
104
|
+
aliases: string[];
|
|
105
|
+
severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
106
|
+
advisory_severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
107
|
+
severity_note?: string | undefined;
|
|
108
|
+
cvss_score: number | null;
|
|
109
|
+
summary: string;
|
|
110
|
+
fixed_version: string | null;
|
|
111
|
+
published: string;
|
|
112
|
+
references: string[];
|
|
113
|
+
}[];
|
|
114
|
+
install_drift: Record<string, unknown>[];
|
|
115
|
+
recommendations: string[];
|
|
116
|
+
clean_dependencies: number;
|
|
117
|
+
scan_duration_ms: number;
|
|
118
|
+
_meta: {
|
|
119
|
+
source: string;
|
|
120
|
+
cached: boolean;
|
|
121
|
+
osv_cached: boolean;
|
|
122
|
+
offline: boolean;
|
|
123
|
+
privacy: string;
|
|
124
|
+
relevance: string;
|
|
125
|
+
severity_rule: string;
|
|
126
|
+
resolution: {
|
|
127
|
+
resolved_at: string | null;
|
|
128
|
+
lockfiles: {
|
|
129
|
+
path: string;
|
|
130
|
+
kind: "lockfile" | "manifest";
|
|
131
|
+
mtime: string | null;
|
|
132
|
+
}[];
|
|
133
|
+
re_resolved_this_call: boolean;
|
|
134
|
+
note: string;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* vulnerability_scan output: alias collapsing, relevance splits,
|
|
4
|
+
* recommendations, install drift, the shared severity grade, and resolution
|
|
5
|
+
* provenance. Split out of vulnerability-scan.ts, which keeps execution.
|
|
6
|
+
*/
|
|
7
|
+
import { hostTarget } from "../live/platform.js";
|
|
8
|
+
import { maxSemver } from "../live/semver-utils.js";
|
|
9
|
+
import { isMaintenanceNotice } from "../live/maintenance.js";
|
|
10
|
+
import { countBySeverity, presentedSeverity, scopeAdjustmentReason, SEVERITY_RANK, } from "../live/severity-scope.js";
|
|
11
|
+
import { buildDriftView, groupDriftRows, lockfileAlsoAffected } from "./install-drift-notes.js";
|
|
12
|
+
const SEVERITY_RULE = "`severity` is the grade every 4DA surface presents: a transitive-only dependency caps critical at high, and a dev-only dependency drops one level (critical to high, high to medium, medium to low). Unknown dev scope gets no discount. `advisory_severity` is the advisory's own tier (counted in `advisory_by_severity`); `by_severity` counts the presented grade.";
|
|
13
|
+
const RESOLUTION_NOTE = "Dependency versions were read from these files at resolved_at. Every call re-checks them (stat only) and re-resolves when one changes. `cached` and `osv_cached` describe the OSV advisory lookup, not dependency resolution.";
|
|
14
|
+
/**
|
|
15
|
+
* Collapse advisories that are the same finding under different identifiers.
|
|
16
|
+
*
|
|
17
|
+
* OSV returns GHSA and RUSTSEC records for one vulnerability, each naming the
|
|
18
|
+
* other in `aliases`. Reported separately they double the count and read as two
|
|
19
|
+
* problems: `quinn-proto` showed as "2 high" for a single memory-exhaustion
|
|
20
|
+
* bug. Keeps the entry with the most usable detail (a CVSS score, then a fix
|
|
21
|
+
* version) and folds the rest into its alias list.
|
|
22
|
+
*/
|
|
23
|
+
export function collapseAliases(vulns) {
|
|
24
|
+
const kept = [];
|
|
25
|
+
const claimed = new Map(); // advisory id -> index in `kept`
|
|
26
|
+
const richness = (v) => (v.cvssScore !== null ? 2 : 0) + (v.fixedVersion ? 1 : 0);
|
|
27
|
+
for (const v of vulns) {
|
|
28
|
+
const scope = `${v.ecosystem}\0${v.package}\0${v.currentVersion}`;
|
|
29
|
+
const ids = [v.vulnId, ...v.aliases];
|
|
30
|
+
const hitIndex = ids
|
|
31
|
+
.map((id) => claimed.get(`${scope}\0${id}`))
|
|
32
|
+
.find((i) => i !== undefined);
|
|
33
|
+
if (hitIndex === undefined) {
|
|
34
|
+
const index = kept.length;
|
|
35
|
+
kept.push({ ...v, aliases: [...v.aliases] });
|
|
36
|
+
for (const id of ids)
|
|
37
|
+
claimed.set(`${scope}\0${id}`, index);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const incumbent = kept[hitIndex];
|
|
41
|
+
const winner = richness(v) > richness(incumbent) ? v : incumbent;
|
|
42
|
+
const merged = {
|
|
43
|
+
...winner,
|
|
44
|
+
aliases: [
|
|
45
|
+
...new Set([...incumbent.aliases, ...ids, incumbent.vulnId].filter((id) => id !== winner.vulnId)),
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
kept[hitIndex] = merged;
|
|
49
|
+
for (const id of [merged.vulnId, ...merged.aliases])
|
|
50
|
+
claimed.set(`${scope}\0${id}`, hitIndex);
|
|
51
|
+
}
|
|
52
|
+
return kept;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Describe the platform filter truthfully, based on whether it actually ran.
|
|
56
|
+
*
|
|
57
|
+
* A crate is only knowable as platform-inactive when cargo could resolve the
|
|
58
|
+
* graph for this host. When it could not (cargo missing, offline resolution
|
|
59
|
+
* impossible, a non-Rust tree), every crate stays active — and saying
|
|
60
|
+
* "advisories are filtered to the host platform" would then be a claim about
|
|
61
|
+
* work that never happened.
|
|
62
|
+
*/
|
|
63
|
+
export function platformFilterNote(vulns) {
|
|
64
|
+
const host = hostTarget();
|
|
65
|
+
const platform = `${host.os}/${host.arch}`;
|
|
66
|
+
const inactive = vulns.filter((v) => v.platformActive === false).length;
|
|
67
|
+
const anyTargetKnown = vulns.some((v) => v.target);
|
|
68
|
+
if (inactive > 0) {
|
|
69
|
+
return `Filtered to ${platform}: ${inactive} advisor${inactive === 1 ? "y is" : "ies are"} against packages not built on this host, listed separately under platform_inactive_vulnerabilities.`;
|
|
70
|
+
}
|
|
71
|
+
if (anyTargetKnown) {
|
|
72
|
+
return `Filtered to ${platform}: every advisory below is against a package that IS built on this host.`;
|
|
73
|
+
}
|
|
74
|
+
return `Not platform-filtered: no per-package target information was resolvable (cargo metadata unavailable, or a non-Rust tree). Some advisories below may be against packages that never build on ${platform}.`;
|
|
75
|
+
}
|
|
76
|
+
/** Shortest readable label for a manifest directory, relative to the scan root. */
|
|
77
|
+
export function relativeDir(dir, root) {
|
|
78
|
+
const n = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
79
|
+
const d = n(dir);
|
|
80
|
+
const r = n(root);
|
|
81
|
+
if (r && d.toLowerCase().startsWith(`${r.toLowerCase()}/`))
|
|
82
|
+
return d.slice(r.length + 1);
|
|
83
|
+
if (r && d.toLowerCase() === r.toLowerCase())
|
|
84
|
+
return ".";
|
|
85
|
+
return d;
|
|
86
|
+
}
|
|
87
|
+
export function formatScanResult(result, severityFilter, ctx) {
|
|
88
|
+
const root = result.projectPath;
|
|
89
|
+
const relative = (dir) => relativeDir(dir, root);
|
|
90
|
+
let vulns = collapseAliases(result.vulnerabilities);
|
|
91
|
+
// Filter and sort on the PRESENTED grade, the one the `severity` field shows.
|
|
92
|
+
if (severityFilter) {
|
|
93
|
+
const minRank = SEVERITY_RANK[severityFilter] ?? 0;
|
|
94
|
+
vulns = vulns.filter((v) => SEVERITY_RANK[presentedSeverity(v)] >= minRank);
|
|
95
|
+
}
|
|
96
|
+
vulns.sort((a, b) => SEVERITY_RANK[presentedSeverity(b)] - SEVERITY_RANK[presentedSeverity(a)]);
|
|
97
|
+
// Relevance split: advisories on deps NOT active on the host platform are not
|
|
98
|
+
// actionable for this user (e.g. a Linux-only crate's CVE shown to a Windows
|
|
99
|
+
// dev). Keep them, but separated and labeled so they don't pollute the signal.
|
|
100
|
+
const onHost = vulns.filter((v) => v.platformActive !== false);
|
|
101
|
+
const inactive = vulns.filter((v) => v.platformActive === false);
|
|
102
|
+
// Second relevance split: "X is unmaintained" is a maintenance notice, not a
|
|
103
|
+
// vulnerability. Nothing is exploitable and there is nothing to upgrade to.
|
|
104
|
+
// Live scan: 23 of 41 findings were these, generating 27 "Review X" lines
|
|
105
|
+
// that buried the six real upgrades. Kept, labelled, and out of the counts.
|
|
106
|
+
const active = onHost.filter((v) => !isMaintenanceNotice(v));
|
|
107
|
+
const maintenance = onHost.filter((v) => isMaintenanceNotice(v));
|
|
108
|
+
const drift = buildDriftView(result, ctx.drift, ctx.includeDev, relative);
|
|
109
|
+
const host = hostTarget();
|
|
110
|
+
const activePackages = new Set(active.map((v) => v.package)).size;
|
|
111
|
+
// Every distinct manifest directory the scan actually covered. A single
|
|
112
|
+
// "project" label hides the fact that a repo root can span several
|
|
113
|
+
// independently-versioned workspaces.
|
|
114
|
+
const scannedProjects = [...new Set(vulns.flatMap((v) => v.sourceDirs ?? []).map(relative))].sort();
|
|
115
|
+
return {
|
|
116
|
+
scan_time: result.scannedAt,
|
|
117
|
+
project: result.projectPath.split(/[/\\]/).pop() || result.projectPath,
|
|
118
|
+
project_root: result.projectPath,
|
|
119
|
+
// Workspaces under `project_root` that contributed a finding. Advisories
|
|
120
|
+
// below are attributed per entry via `source_dirs`.
|
|
121
|
+
workspaces_with_findings: scannedProjects,
|
|
122
|
+
host_platform: `${host.os}/${host.arch}`,
|
|
123
|
+
ecosystems_scanned: result.ecosystemsScanned,
|
|
124
|
+
total_dependencies_scanned: result.totalScanned,
|
|
125
|
+
total_vulnerable_packages: activePackages,
|
|
126
|
+
platform_inactive_packages: result.platformInactiveVulnerable,
|
|
127
|
+
// Counted over the listed findings: the same rows `vulnerabilities` shows,
|
|
128
|
+
// one per vulnerability (aliases collapsed). Counting every raw OSV row
|
|
129
|
+
// (alias duplicates, maintenance notices, other-platform advisories) gave
|
|
130
|
+
// "medium 3, low 1, unknown 9" beside three listed rows (2026-10-02 eval).
|
|
131
|
+
by_severity: countBySeverity(active, presentedSeverity),
|
|
132
|
+
advisory_by_severity: countBySeverity(active, (v) => v.severity),
|
|
133
|
+
vulnerabilities: active.map((v) => formatVuln(v, root, drift)),
|
|
134
|
+
// Advisories suppressed from the main list because the affected crate is not
|
|
135
|
+
// built on this host (e.g. cfg(not(windows)) deps on Windows). Surfaced for
|
|
136
|
+
// cross-platform builds, clearly labeled with the gating target.
|
|
137
|
+
platform_inactive_vulnerabilities: inactive.map((v) => formatVuln(v, root, drift)),
|
|
138
|
+
// Unmaintained-dependency notices: real information to plan around, but
|
|
139
|
+
// nothing exploitable and nothing to upgrade to. Separated so they cannot
|
|
140
|
+
// outnumber the advisories that need action today.
|
|
141
|
+
maintenance_notices: maintenance.map((v) => formatVuln(v, root, drift)),
|
|
142
|
+
// npm direct dependencies whose node_modules copy is not what the lockfile
|
|
143
|
+
// pins. The lockfile is what every other tool reads; this is what runs.
|
|
144
|
+
install_drift: drift.rows,
|
|
145
|
+
recommendations: buildRecommendations(active, result.vulnerabilities, relative),
|
|
146
|
+
clean_dependencies: result.cleanCount,
|
|
147
|
+
scan_duration_ms: result.scanDurationMs,
|
|
148
|
+
_meta: {
|
|
149
|
+
source: "osv.dev",
|
|
150
|
+
// Kept for compatibility; it has only ever described the OSV lookup.
|
|
151
|
+
cached: result.cached,
|
|
152
|
+
osv_cached: result.cached,
|
|
153
|
+
offline: result.offline,
|
|
154
|
+
privacy: "Only package names and versions were sent (public manifest data)",
|
|
155
|
+
// State what actually happened. This previously asserted that advisories
|
|
156
|
+
// WERE platform-filtered while reporting zero inactive packages and
|
|
157
|
+
// listing nine Linux-only crates on a Windows host — a claim of filtering
|
|
158
|
+
// is worse than no filtering, because it invites the reader to trust it.
|
|
159
|
+
relevance: platformFilterNote(vulns),
|
|
160
|
+
severity_rule: SEVERITY_RULE,
|
|
161
|
+
resolution: {
|
|
162
|
+
resolved_at: ctx.resolution?.resolvedAt ?? null,
|
|
163
|
+
lockfiles: (ctx.resolution?.sources ?? []).map((s) => ({
|
|
164
|
+
path: relative(s.path),
|
|
165
|
+
kind: s.kind,
|
|
166
|
+
mtime: s.mtimeMs === null ? null : new Date(s.mtimeMs).toISOString(),
|
|
167
|
+
})),
|
|
168
|
+
re_resolved_this_call: ctx.reResolvedThisCall,
|
|
169
|
+
note: RESOLUTION_NOTE,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function buildRecommendations(active, all, relative) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
const rankOf = (vulns) => Math.max(...vulns.map((v) => SEVERITY_RANK[presentedSeverity(v)]));
|
|
177
|
+
// Name the manifest directories that actually pin the vulnerable version.
|
|
178
|
+
// Without this a reader cannot tell a finding in the primary crate from one
|
|
179
|
+
// in a sibling workspace, and a patched primary reads as vulnerable.
|
|
180
|
+
const whereOf = (vulns) => {
|
|
181
|
+
const dirs = [...new Set(vulns.flatMap((v) => v.sourceDirs ?? []).map(relative))].sort();
|
|
182
|
+
return dirs.length > 0 ? ` in ${dirs.join(", ")}` : "";
|
|
183
|
+
};
|
|
184
|
+
// Recommendations are derived from platform-active, exploitable vulns only.
|
|
185
|
+
const byPackage = new Map();
|
|
186
|
+
for (const v of active) {
|
|
187
|
+
if (v.installDriftOf)
|
|
188
|
+
continue; // reinstall advice below, not an upgrade
|
|
189
|
+
const list = byPackage.get(v.package);
|
|
190
|
+
if (list)
|
|
191
|
+
list.push(v);
|
|
192
|
+
else
|
|
193
|
+
byPackage.set(v.package, [v]);
|
|
194
|
+
}
|
|
195
|
+
for (const [pkg, pkgVulns] of byPackage) {
|
|
196
|
+
const highest = pkgVulns[0];
|
|
197
|
+
const breakdown = severityBreakdown(pkgVulns);
|
|
198
|
+
// Target the HIGHEST published fix so the upgrade resolves every fixable CVE,
|
|
199
|
+
// not just the first one. (The old code picked the first fix version, which
|
|
200
|
+
// could leave higher-version-gated CVEs unfixed while claiming to fix them.)
|
|
201
|
+
const target = maxSemver(pkgVulns.map((v) => v.fixedVersion).filter((v) => Boolean(v)));
|
|
202
|
+
const total = pkgVulns.length;
|
|
203
|
+
const fixable = pkgVulns.filter((v) => v.fixedVersion).length;
|
|
204
|
+
const where = whereOf(pkgVulns);
|
|
205
|
+
let text;
|
|
206
|
+
if (target) {
|
|
207
|
+
const scope = fixable === total
|
|
208
|
+
? `${total} CVE${total > 1 ? "s" : ""}`
|
|
209
|
+
: `${fixable} of ${total} CVEs (${total - fixable} with no published fix)`;
|
|
210
|
+
text = `Upgrade ${pkg} ${highest.currentVersion} → ${target}${where} — resolves ${scope} [${breakdown}]`;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
text = `Review ${pkg} ${highest.currentVersion}${where} — ${total} known vulnerabilit${total > 1 ? "ies" : "y"} [${breakdown}], no fix version published`;
|
|
214
|
+
}
|
|
215
|
+
lines.push({ rank: rankOf(pkgVulns), text });
|
|
216
|
+
}
|
|
217
|
+
// Installed copies the lockfile does not pin. The lockfile already says what
|
|
218
|
+
// should be installed, so the advice is the reinstall, not a version bump.
|
|
219
|
+
for (const entries of groupDriftRows(active)) {
|
|
220
|
+
const first = entries[0];
|
|
221
|
+
const count = entries.length;
|
|
222
|
+
const dirs = [...new Set(entries.flatMap((v) => v.sourceDirs ?? []).map(relative))]
|
|
223
|
+
.map((d) => (d === "." ? "the project root" : d))
|
|
224
|
+
.sort();
|
|
225
|
+
const where = dirs.length > 0 ? ` in ${dirs.join(", ")}` : "";
|
|
226
|
+
const command = first.installFix ?? "a reinstall";
|
|
227
|
+
const tail = entries.some((e) => lockfileAlsoAffected(e, all))
|
|
228
|
+
? `the lockfile's ${first.installDriftOf} is also affected — upgrade it, then run \`${command}\`${where}`
|
|
229
|
+
: `the lockfile already pins ${first.installDriftOf}, which is not affected — run \`${command}\`${where}`;
|
|
230
|
+
lines.push({
|
|
231
|
+
rank: rankOf(entries),
|
|
232
|
+
text: `Reinstall ${first.package}${where}: node_modules has ${first.currentVersion} with ${count} known vulnerabilit${count > 1 ? "ies" : "y"} [${severityBreakdown(entries)}]; ${tail}`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return lines
|
|
236
|
+
.map((line, index) => ({ ...line, index }))
|
|
237
|
+
.sort((a, b) => b.rank - a.rank || a.index - b.index)
|
|
238
|
+
.map((line) => line.text);
|
|
239
|
+
}
|
|
240
|
+
/** "1 critical, 3 high, 2 medium" (presented grades), omitting empty buckets. */
|
|
241
|
+
function severityBreakdown(vulns) {
|
|
242
|
+
const counts = countBySeverity(vulns, presentedSeverity);
|
|
243
|
+
return ["critical", "high", "medium", "low", "unknown"]
|
|
244
|
+
.filter((s) => counts[s])
|
|
245
|
+
.map((s) => `${counts[s]} ${s}`)
|
|
246
|
+
.join(", ");
|
|
247
|
+
}
|
|
248
|
+
function formatVuln(v, root, drift) {
|
|
249
|
+
const presented = presentedSeverity(v);
|
|
250
|
+
const reason = scopeAdjustmentReason(v);
|
|
251
|
+
return {
|
|
252
|
+
package: v.package,
|
|
253
|
+
current_version: v.currentVersion,
|
|
254
|
+
/** Manifest directories pinning this version — where to apply the fix. */
|
|
255
|
+
source_dirs: [...new Set((v.sourceDirs ?? []).map((d) => relativeDir(d, root)))].sort(),
|
|
256
|
+
ecosystem: v.ecosystem,
|
|
257
|
+
is_dev: v.isDev,
|
|
258
|
+
is_direct: v.isDirect ?? null,
|
|
259
|
+
dev_scope_known: v.devScopeKnown ?? false,
|
|
260
|
+
target: v.target,
|
|
261
|
+
platform_active: v.platformActive,
|
|
262
|
+
vuln_id: v.vulnId,
|
|
263
|
+
aliases: v.aliases,
|
|
264
|
+
severity: presented,
|
|
265
|
+
advisory_severity: v.severity,
|
|
266
|
+
...(reason
|
|
267
|
+
? { severity_note: `Graded ${presented}: the advisory rates it ${v.severity}, and this is a ${reason}.` }
|
|
268
|
+
: {}),
|
|
269
|
+
cvss_score: v.cvssScore,
|
|
270
|
+
summary: v.summary,
|
|
271
|
+
fixed_version: v.fixedVersion,
|
|
272
|
+
published: v.published,
|
|
273
|
+
references: v.references,
|
|
274
|
+
...drift.annotate(v),
|
|
275
|
+
};
|
|
276
|
+
}
|