@4da/mcp-server 6.2.4 → 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 +484 -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 +258 -9
- package/dist/live/fix-paths.js +790 -45
- 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/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- 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 +49 -0
- package/dist/live/version-compare.js +131 -0
- 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 +162 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +59 -4
- 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
|
@@ -23,29 +23,42 @@
|
|
|
23
23
|
* says so (`installedVersion`) and a reinstall is the step when nothing
|
|
24
24
|
* else about the lockfile's version needs changing.
|
|
25
25
|
*/
|
|
26
|
-
import { isMaintenanceNotice } from "../live/maintenance.js";
|
|
26
|
+
import { isActionableVulnerability, isMaintenanceNotice } from "../live/maintenance.js";
|
|
27
27
|
import { computeSemverDistance } from "../live/semver-utils.js";
|
|
28
|
-
import { compareVersions, maxVersion, namesPackage } from "../live/version-compare.js";
|
|
29
|
-
import { formatAppPlan, readAppPlan } from "./app-plan.js";
|
|
28
|
+
import { compareVersions, inEcosystemForm, maxVersion, namesPackage } from "../live/version-compare.js";
|
|
29
|
+
import { formatAppPlan, readAppPlan, scopeAppPlanSteps } from "./app-plan.js";
|
|
30
30
|
import { dirsLabel, driftFor } from "./install-drift-notes.js";
|
|
31
31
|
import { emptyAnswerNote } from "./package-presence.js";
|
|
32
32
|
import { relativeDir } from "./vulnerability-scan-format.js";
|
|
33
|
-
import { liveIntelFor, resolveProjectScope } from "./project-scope.js";
|
|
34
|
-
import { verifyFixPaths } from "./upgrade-planner-fixpaths.js";
|
|
35
|
-
import {
|
|
33
|
+
import { appTracksProject, liveIntelFor, resolveProjectScope } from "./project-scope.js";
|
|
34
|
+
import { reconcileDirectTargets, verifyFixPaths } from "./upgrade-planner-fixpaths.js";
|
|
35
|
+
import { groupAdvisories } from "../live/advisory-groups.js";
|
|
36
|
+
import { isDevOnlyCopy, rankedSeverity } from "../live/severity-scope.js";
|
|
37
|
+
import { discoveryNote, exclusionNotes, unreadableNotes } from "../project-tree.js";
|
|
38
|
+
import { vulnerabilityCoverage } from "../live/scan-coverage.js";
|
|
39
|
+
import { uncheckedOfferNote, vetOfferedReleases } from "../live/offered-release.js";
|
|
40
|
+
/** Time the plan spends checking the newer releases its rows offer against their advisories. */
|
|
41
|
+
const OFFER_CHECK_BUDGET_MS = 10_000;
|
|
42
|
+
/** The newer release a registry row would offer (its latest stable), or null when it offers none. */
|
|
43
|
+
function offeredUpgrade(dep) {
|
|
44
|
+
const latest = dep.latestStableVersion || dep.latestVersion;
|
|
45
|
+
if (!latest || !dep.currentVersion)
|
|
46
|
+
return null;
|
|
47
|
+
return compareVersions(latest, dep.currentVersion, dep.ecosystem) > 0 ? latest : null;
|
|
48
|
+
}
|
|
36
49
|
/** How long a plan waits for its own OSV scan before answering CVE-blind (the scan keeps running). */
|
|
37
50
|
const PLANNER_SCAN_TIMEOUT_MS = 60_000;
|
|
38
51
|
const PROVENANCE_NOTE = "Point-in-time heuristic from local lockfiles plus live registry/OSV lookups. " +
|
|
39
52
|
"Not the 4DA desktop app's cross-project, version-confirmed plan (none was available).";
|
|
40
53
|
export const upgradePlannerTool = {
|
|
41
54
|
name: "upgrade_planner",
|
|
42
|
-
description: "Ranked dependency upgrade plan for one project — call before upgrading, adding, or auditing any dependency to pick the safest order. `project_path` picks the project (default: the one the server was started in; \"*\" for every project). When the 4DA desktop app has computed its plan, returns that plan's work order (provenance app_plan): per package the ecosystem, each installed version with its minimum clean target, upgrade type (patch/minor/major), the projects that hold it (direct/dev) and the mechanism (manifest_bump, lockfile_or_parent_update, mixed, no_fix); flagged stale past its freshness horizon. Otherwise falls back to a standalone plan from local lockfiles (provenance standalone_heuristic): the smallest version that fixes each vulnerability (direct and transitive), checked against every known advisory of the package, whether a lockfile refresh already reaches it (lockfile_refresh with the command) or a parent blocks it (
|
|
55
|
+
description: "Ranked dependency upgrade plan for one project — call before upgrading, adding, or auditing any dependency to pick the safest order. `project_path` picks the project (default: the one the server was started in; \"*\" for every project). When the 4DA desktop app has computed its plan, returns that plan's work order (provenance app_plan): per package the ecosystem, each installed version with its minimum clean target, upgrade type (patch/minor/major), the projects that hold it (direct/dev) and the mechanism (manifest_bump, lockfile_or_parent_update, mixed, no_fix); flagged stale past its freshness horizon. Otherwise falls back to a standalone plan from local lockfiles (provenance standalone_heuristic): the smallest version that fixes each vulnerability (direct and transitive), checked against every known advisory of the package, whether a lockfile refresh already reaches it (lockfile_refresh with the command) or a parent blocks it: upgrade_parent with `parentUpgrades` naming the smallest parent release whose requirement admits a clean version (through the direct dependency it arrives by), override_or_replace with the override syntax when no parent release will (deprecated, already latest, or none admits one), waiting_on_upstream when nothing could be read; direct steps carry `upgradeCommands` and are raised to the release that also clears the transitives they pin; a copy bundled inside its owner waits on the owner; dev-only rows are included and labelled `isDev`, as in vulnerability_scan; deprecation and version distance; it runs the OSV scan itself when none has run. When OSV.dev could not answer for every dependency, `vulnerabilityDataAvailable` is false, `vulnerabilityCoverage` counts the unchecked ones and the summary starts with \"NOT CHECKED\": the plan then misses whatever those dependencies carry. `package` narrows either plan to one package. Privacy: the fallback may query public registries and OSV with package names and versions; it never sends source code.",
|
|
43
56
|
inputSchema: {
|
|
44
57
|
type: "object",
|
|
45
58
|
properties: {
|
|
46
59
|
include_dev: {
|
|
47
60
|
type: "boolean",
|
|
48
|
-
description: "
|
|
61
|
+
description: "Dev-only packages (labelled isDev). Default (unset): vulnerable devDependencies and dev-only transitive packages are planned, as in vulnerability_scan; true also checks every devDependency for newer releases and deprecation; false leaves dev-only packages out and the summary counts them.",
|
|
49
62
|
},
|
|
50
63
|
max_recommendations: {
|
|
51
64
|
type: "number",
|
|
@@ -68,23 +81,32 @@ export const upgradePlannerTool = {
|
|
|
68
81
|
},
|
|
69
82
|
};
|
|
70
83
|
const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
71
|
-
/** critical > high > medium > low, then fixable now (direct) before transitive, then non-breaking first (quick wins). */
|
|
84
|
+
/** critical > high > medium > low, then runtime before dev-only, then fixable now (direct) before transitive, then non-breaking first (quick wins). */
|
|
72
85
|
function planOrder(a, b) {
|
|
73
86
|
const riskDiff = RISK_LEVELS[b.risk] - RISK_LEVELS[a.risk];
|
|
74
87
|
if (riskDiff !== 0)
|
|
75
88
|
return riskDiff;
|
|
89
|
+
if (a.isDev !== b.isDev)
|
|
90
|
+
return a.isDev ? 1 : -1;
|
|
76
91
|
if (a.scope !== b.scope)
|
|
77
92
|
return a.scope === "direct" ? -1 : 1;
|
|
78
93
|
if (a.breaking !== b.breaking)
|
|
79
94
|
return a.breaking ? 1 : -1;
|
|
80
95
|
return 0;
|
|
81
96
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The grade a row ranks by: the scope-adjusted severity vulnerability_scan presents (transitive
|
|
99
|
+
* caps critical at high; dev-only drops one level), over the 4DA app's base urgency (an ungraded
|
|
100
|
+
* advisory medium, a MAL- record high: severity-scope.ts rankedSeverity). A dev-only medium is low,
|
|
101
|
+
* as the scan presents it; it used to stay medium. Ranking on the raw grade let dev-only criticals
|
|
102
|
+
* push a runtime high out of max_recommendations (readiness review 2026-10-17, finding 6).
|
|
103
|
+
*/
|
|
104
|
+
function riskOf(entries) {
|
|
105
|
+
const tiers = entries.map((v) => rankedSeverity(v));
|
|
106
|
+
for (const t of ["critical", "high", "medium"])
|
|
107
|
+
if (tiers.includes(t))
|
|
108
|
+
return t;
|
|
109
|
+
return "low";
|
|
88
110
|
}
|
|
89
111
|
/** Distance between the installed and target versions (patch/minor/major), or unknown. */
|
|
90
112
|
function upgradeTypeBetween(current, target) {
|
|
@@ -104,8 +126,15 @@ export async function executeUpgradePlanner(db, params, liveIntel) {
|
|
|
104
126
|
// One brain: the app's plan wins whenever there is one, stale or not (a
|
|
105
127
|
// stale plan is returned flagged, never silently replaced by a heuristic).
|
|
106
128
|
// It covers every project the app tracks, so it is narrowed to this one.
|
|
129
|
+
// A project the app does not track is not in its plan at all; narrowing the
|
|
130
|
+
// plan to it gave "0 steps" for a project with 83 vulnerable packages
|
|
131
|
+
// (audit 6.2.5 D-1). Plan it from its own lockfiles instead.
|
|
107
132
|
const appPlan = readAppPlan(db);
|
|
108
|
-
|
|
133
|
+
const untracked = appPlan.kind === "plan" &&
|
|
134
|
+
scopePath !== null &&
|
|
135
|
+
scopeAppPlanSteps(appPlan.snapshot.steps, scopePath).length === 0 &&
|
|
136
|
+
!appTracksProject(db, scopePath);
|
|
137
|
+
if (appPlan.kind === "plan" && !untracked)
|
|
109
138
|
return formatAppPlan(appPlan, params, scopePath);
|
|
110
139
|
// The live layer already covers the server's own project; only an explicit
|
|
111
140
|
// project_path elsewhere resolves another tree.
|
|
@@ -113,7 +142,10 @@ export async function executeUpgradePlanner(db, params, liveIntel) {
|
|
|
113
142
|
? liveIntelFor(liveIntel, db, scope)
|
|
114
143
|
: liveIntel;
|
|
115
144
|
const standalone = await executeStandalonePlanner(params, scoped);
|
|
116
|
-
|
|
145
|
+
const appPlanUnavailable = appPlan.kind === "plan"
|
|
146
|
+
? `the 4DA app does not track ${scopePath}, so its plan does not cover it; this plan is computed from the project's own lockfiles`
|
|
147
|
+
: appPlan.reason;
|
|
148
|
+
return { ...standalone, appPlanUnavailable, projectScope: scopePath ?? standalone.projectPath };
|
|
117
149
|
}
|
|
118
150
|
async function executeStandalonePlanner(params, liveIntel) {
|
|
119
151
|
if (!liveIntel || !liveIntel.isInitialized()) {
|
|
@@ -127,6 +159,7 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
127
159
|
breakingChanges: 0,
|
|
128
160
|
waitingOnUpstream: 0,
|
|
129
161
|
lockfileRefreshes: 0,
|
|
162
|
+
parentUpgradeSteps: 0,
|
|
130
163
|
vulnerabilityDataAvailable: false,
|
|
131
164
|
provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
|
|
132
165
|
};
|
|
@@ -134,16 +167,17 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
134
167
|
// A long-lived server must not plan from the versions it read at startup:
|
|
135
168
|
// re-resolve if a lockfile (or node_modules) changed. Stat calls only.
|
|
136
169
|
liveIntel.refreshIfLockfilesChanged();
|
|
137
|
-
|
|
170
|
+
// Dev dependencies by default, as vulnerability_scan (A8): leaving them out hid 30 of navcal's
|
|
171
|
+
// 34 vulnerable versions, vitest's critical among them (readiness audit H4).
|
|
172
|
+
const includeDev = params.include_dev ?? true;
|
|
138
173
|
const only = params.package?.trim();
|
|
139
174
|
const named = (name, ecosystem) => !only || namesPackage(only, name, ecosystem);
|
|
140
175
|
let deps = liveIntel.getResolvedDeps();
|
|
141
|
-
if (!includeDev)
|
|
142
|
-
deps = deps.filter((d) => !d.isDev);
|
|
143
176
|
if (only)
|
|
144
177
|
deps = deps.filter((d) => named(d.name, d.ecosystem));
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
const devDirect = includeDev ? [] : deps.filter((d) => d.isDev);
|
|
179
|
+
if (!includeDev)
|
|
180
|
+
deps = deps.filter((d) => !d.isDev);
|
|
147
181
|
const drift = liveIntel.getInstallDrift();
|
|
148
182
|
const root = liveIntel.getProjectRoot() ?? process.cwd();
|
|
149
183
|
// Vulnerability data at the plan's scope, loaded here (bounded) when no scan
|
|
@@ -152,7 +186,40 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
152
186
|
// includes dev dependencies must not read a scan that left them out, so
|
|
153
187
|
// ensureVulnerabilities rescans at the wider scope when needed.
|
|
154
188
|
const vulnResult = (await liveIntel.ensureVulnerabilities(root, PLANNER_SCAN_TIMEOUT_MS, { includeDev })) ?? liveIntel.getVulnerabilities();
|
|
155
|
-
|
|
189
|
+
// include_dev: false: the summary counts the vulnerable devDependencies it left out, when the
|
|
190
|
+
// scan read them; otherwise how many it did not check (readiness review 2026-10-17, finding 10).
|
|
191
|
+
const devScanned = devDirect.length > 0 && liveIntel.lastScanIncludesDev();
|
|
192
|
+
const devVulnerable = new Set((vulnResult?.vulnerabilities ?? []).filter((v) => v.isDirect && v.isDev && !isMaintenanceNotice(v)).map((v) => `${v.ecosystem}\0${v.package}\0${v.currentVersion}`));
|
|
193
|
+
const devDirectLeftOut = devScanned ? devDirect.filter((d) => devVulnerable.has(`${d.ecosystem}\0${d.name}\0${d.version}`)).length : 0;
|
|
194
|
+
const devDirectUnchecked = devScanned ? 0 : devDirect.length;
|
|
195
|
+
// Registry rows (freshness, deprecation) for direct deps. By default a devDependency gets one
|
|
196
|
+
// only when it is vulnerable: fetching every dev tool's registry document (typescript,
|
|
197
|
+
// @types/node, vite...) more than doubled navcal's lookups for rows no advisory asks for.
|
|
198
|
+
// include_dev: true checks every devDependency's freshness too.
|
|
199
|
+
const vulnerableNames = new Set((vulnResult?.vulnerabilities ?? []).filter((v) => !isMaintenanceNotice(v)).map((v) => `${v.ecosystem}\0${v.package}`));
|
|
200
|
+
const registryDeps = params.include_dev === undefined ? deps.filter((d) => !d.isDev || vulnerableNames.has(`${d.ecosystem}\0${d.name}`)) : deps;
|
|
201
|
+
const devFreshnessSkipped = deps.length - registryDeps.length;
|
|
202
|
+
// Fetch registry data (live lookups for direct deps; cached where fresh)
|
|
203
|
+
const registryData = await liveIntel.fetchRegistryHealth(registryDeps);
|
|
204
|
+
// Every newer release a row offers is checked as dependency_check checks one (offered-release.ts):
|
|
205
|
+
// crossenv 0.0.2-security was sent to 1.0.1, the malware itself (final release audit of 6.2.5).
|
|
206
|
+
const offerSources = liveIntel.getFixPathSources();
|
|
207
|
+
const offers = await vetOfferedReleases(offerSources, registryData
|
|
208
|
+
.filter((d) => offeredUpgrade(d) !== null)
|
|
209
|
+
.map((d) => ({ key: `${d.ecosystem}\0${d.name}\0${d.currentVersion ?? "unknown"}`, ecosystem: d.ecosystem, name: d.name, installed: d.currentVersion, offered: offeredUpgrade(d) })), OFFER_CHECK_BUDGET_MS);
|
|
210
|
+
// Complete vulnerability data only: a partial scan (OSV.dev unreachable or
|
|
211
|
+
// rate limited for some dependencies) ranks what it found and says the rest
|
|
212
|
+
// is unchecked; it never claims the plan is CVE-aware (6.2.4 said
|
|
213
|
+
// vulnerabilityDataAvailable: true over 10,568 unchecked dependencies).
|
|
214
|
+
const scopeTotal = liveIntel.getAuditDeps().filter((d) => includeDev || !isDevOnlyCopy(d)).length;
|
|
215
|
+
const coverage = vulnerabilityCoverage(vulnResult, {
|
|
216
|
+
enabled: liveIntel.isEnabled(),
|
|
217
|
+
expectedTotal: scopeTotal,
|
|
218
|
+
notLoaded: `the OSV.dev scan did not finish within ${PLANNER_SCAN_TIMEOUT_MS / 1000} s or OSV.dev was unreachable`,
|
|
219
|
+
});
|
|
220
|
+
// A project whose only manifests are ones this server does not read (pom.xml) has no data at all (audit 6.2.5 A-6).
|
|
221
|
+
const nothingReadable = (vulnResult?.totalScanned ?? 0) === 0 && (liveIntel.getDiscovery()?.notCoveredCount ?? 0) > 0;
|
|
222
|
+
const vulnerabilityDataAvailable = vulnResult !== null && coverage === null && !nothingReadable;
|
|
156
223
|
// Keyed by ecosystem + name + INSTANCE version. A name-only key attached every
|
|
157
224
|
// instance's CVEs to whichever row asked first: the direct anyhow 1.0.104 was
|
|
158
225
|
// charged with RUSTSEC-2026-0190 (fixed in 1.0.103 — it isn't affected) while
|
|
@@ -176,13 +243,7 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
176
243
|
const key = instanceKey(v.ecosystem, v.package, v.currentVersion);
|
|
177
244
|
if (!vulnsByPackage.has(key))
|
|
178
245
|
vulnsByPackage.set(key, []);
|
|
179
|
-
vulnsByPackage.get(key).push(
|
|
180
|
-
severity: v.severity,
|
|
181
|
-
vulnId: v.vulnId,
|
|
182
|
-
summary: v.summary,
|
|
183
|
-
fixedVersion: v.fixedVersion,
|
|
184
|
-
platformActive: v.platformActive,
|
|
185
|
-
});
|
|
246
|
+
vulnsByPackage.get(key).push(v);
|
|
186
247
|
}
|
|
187
248
|
}
|
|
188
249
|
// Build recommendations — direct deps (registry-informed)
|
|
@@ -194,14 +255,24 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
194
255
|
directPackages.add(instanceKey(dep.ecosystem, dep.name, dep.currentVersion));
|
|
195
256
|
const reasons = [];
|
|
196
257
|
let risk = "low";
|
|
197
|
-
const
|
|
258
|
+
const registryLatest = dep.latestStableVersion || dep.latestVersion;
|
|
259
|
+
// The newest release this row may offer: the registry's latest, unless an advisory affects it or
|
|
260
|
+
// the registry withdrew it; then the newest release above the installed one that is neither.
|
|
261
|
+
const offered = offeredUpgrade(dep);
|
|
262
|
+
const vet = offered ? offers.get(instanceKey(dep.ecosystem, dep.name, dep.currentVersion)) : undefined;
|
|
263
|
+
const offerNote = offered ? (vet ? vet.note : offerSources ? uncheckedOfferNote(dep.name, offered) : null) : null;
|
|
264
|
+
const latestStable = vet?.checked && vet.note ? vet.safe : registryLatest;
|
|
265
|
+
const noSafeUpgrade = Boolean(offered && vet?.checked && vet.note && vet.safe === null);
|
|
198
266
|
let targetVersion = latestStable;
|
|
199
267
|
let platformActive = true;
|
|
200
268
|
let fixable = false;
|
|
201
269
|
// Check vulnerabilities
|
|
202
270
|
const vulns = vulnsByPackage.get(instanceKey(dep.ecosystem, dep.name, dep.currentVersion));
|
|
271
|
+
// One advisory per alias group: GHSA + PYSEC records of one issue counted twice (django 96 for 51, M1).
|
|
272
|
+
const groups = groupAdvisories(vulns);
|
|
273
|
+
const unfixedGroups = groups.filter((g) => !g.members.some((m) => m.fixedVersion));
|
|
203
274
|
if (vulns && vulns.length > 0) {
|
|
204
|
-
reasons.push(
|
|
275
|
+
reasons.push(vulnerabilitiesReason(groups));
|
|
205
276
|
if (vulns.every((v) => !v.platformActive)) {
|
|
206
277
|
// Code this host never compiles is not this host's exposure: listed,
|
|
207
278
|
// never ranked as high (openssl topped the plan on Windows).
|
|
@@ -210,7 +281,7 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
210
281
|
reasons.push("Affected code is gated behind a target not active on this platform");
|
|
211
282
|
}
|
|
212
283
|
else {
|
|
213
|
-
risk =
|
|
284
|
+
risk = riskOf(vulns.filter((v) => v.platformActive));
|
|
214
285
|
}
|
|
215
286
|
// The smallest version that fixes every fixable advisory: each
|
|
216
287
|
// fixedVersion is already the fix on the installed release line.
|
|
@@ -218,9 +289,8 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
218
289
|
if (fixedVersions.length > 0) {
|
|
219
290
|
targetVersion = maxVersion(fixedVersions, dep.ecosystem);
|
|
220
291
|
fixable = true;
|
|
221
|
-
if (
|
|
222
|
-
reasons.push(
|
|
223
|
-
}
|
|
292
|
+
if (unfixedGroups.length > 0)
|
|
293
|
+
reasons.push(noFixReason(unfixedGroups.length));
|
|
224
294
|
}
|
|
225
295
|
}
|
|
226
296
|
// Check deprecation
|
|
@@ -229,9 +299,10 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
229
299
|
risk = "high";
|
|
230
300
|
reasons.push(dep.deprecationMessage ? `Deprecated: ${dep.deprecationMessage}` : "Package is deprecated");
|
|
231
301
|
}
|
|
232
|
-
// Check version distance
|
|
233
|
-
|
|
234
|
-
|
|
302
|
+
// Check version distance: to the release this row may offer, not to a known-affected latest.
|
|
303
|
+
const behind = latestStable === registryLatest ? dep.versionsBehind : latestStable && dep.currentVersion ? computeSemverDistance(dep.currentVersion, latestStable) : null;
|
|
304
|
+
if (behind && behind.label !== "up-to-date") {
|
|
305
|
+
const d = behind;
|
|
235
306
|
if (d.label === "major" && d.major === 0) {
|
|
236
307
|
// A 0.x bump: breaking under caret rules, though the major number is unchanged.
|
|
237
308
|
const n = d.minor > 0 ? d.minor : d.patch;
|
|
@@ -270,13 +341,20 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
270
341
|
const installedVulns = vulnsByPackage.get(instanceKey("npm", dep.name, installed));
|
|
271
342
|
if (!installedVulns || installedVulns.length === 0)
|
|
272
343
|
continue;
|
|
273
|
-
|
|
274
|
-
|
|
344
|
+
const installedGroups = groupAdvisories(installedVulns);
|
|
345
|
+
reasons.push(`The installed ${installed} has ${installedGroups.length} known CVE${installedGroups.length !== 1 ? "s" : ""} (${installedGroups.map((g) => g.best.vulnId).join(", ")})`);
|
|
346
|
+
risk = higherRisk(risk, riskOf(installedVulns));
|
|
275
347
|
}
|
|
276
348
|
}
|
|
349
|
+
// Every newer release is known-affected or withdrawn: the row says so (crossenv: remove it).
|
|
350
|
+
if (noSafeUpgrade && offerNote)
|
|
351
|
+
reasons.push(offerNote);
|
|
277
352
|
// Skip if no upgrade needed (no vuln, not deprecated, not behind, no drift => no reasons)
|
|
278
353
|
if (reasons.length === 0)
|
|
279
354
|
continue;
|
|
355
|
+
// The newer release it would have offered was replaced or could not be checked: said, never silent.
|
|
356
|
+
if (offerNote && !noSafeUpgrade)
|
|
357
|
+
reasons.push(offerNote);
|
|
280
358
|
if (dep.declaredRange) {
|
|
281
359
|
reasons.push(`No lockfile: ${dep.currentVersion} is the floor of the declared range "${dep.declaredRange}", not an installed version`);
|
|
282
360
|
}
|
|
@@ -299,19 +377,26 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
299
377
|
? "not_built_on_this_host"
|
|
300
378
|
: vulnerable && !fixable
|
|
301
379
|
? "no_fix_available"
|
|
302
|
-
:
|
|
303
|
-
?
|
|
304
|
-
|
|
380
|
+
: !vulnerable && noSafeUpgrade
|
|
381
|
+
? vet?.holding
|
|
382
|
+
? "remove_dependency"
|
|
383
|
+
: "hold_current"
|
|
384
|
+
: targetVersion
|
|
385
|
+
? "upgrade_direct"
|
|
386
|
+
: "no_fix_available";
|
|
305
387
|
const rec = {
|
|
306
388
|
package: dep.name,
|
|
307
389
|
ecosystem: dep.ecosystem,
|
|
308
390
|
currentVersion: dep.currentVersion,
|
|
309
|
-
|
|
391
|
+
// A vulnerable row with no fix names no release (its newest is affected too): the parent_release
|
|
392
|
+
// vectors' own_target is null there (pr-006, request 2.88.0), and verifyFixPaths names one only
|
|
393
|
+
// when a release past a last_affected bound is clean.
|
|
394
|
+
targetVersion: reinstallOnly ? dep.currentVersion : action === "no_fix_available" ? null : targetVersion,
|
|
310
395
|
...(latestStable && latestStable !== targetVersion ? { latestVersion: latestStable } : {}),
|
|
311
|
-
upgradeType,
|
|
396
|
+
upgradeType: action === "no_fix_available" ? "unknown" : upgradeType,
|
|
312
397
|
risk,
|
|
313
398
|
reasons,
|
|
314
|
-
breaking: upgradeType === "major",
|
|
399
|
+
breaking: action !== "no_fix_available" && upgradeType === "major",
|
|
315
400
|
isDev: dep.isDev,
|
|
316
401
|
scope: "direct",
|
|
317
402
|
action,
|
|
@@ -321,7 +406,15 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
321
406
|
};
|
|
322
407
|
recommendations.push(rec);
|
|
323
408
|
if (fixable && !reinstallOnly && action === "upgrade_direct" && vulns) {
|
|
324
|
-
|
|
409
|
+
// The manifest directories that declare it: where its upgrade command runs (audit E8).
|
|
410
|
+
fixPathInputs.set(rec, {
|
|
411
|
+
fixes: vulns.map((v) => v.fixedVersion).filter((f) => Boolean(f)),
|
|
412
|
+
sourceDirs: [...new Set(vulns.flatMap((v) => v.sourceDirs ?? []))],
|
|
413
|
+
unfixed: unfixedGroups.map((g) => ({ id: g.best.vulnId, ids: [...g.ids] })),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else if (vulnerable && action === "no_fix_available") {
|
|
417
|
+
fixPathInputs.set(rec, { fixes: [], sourceDirs: [] });
|
|
325
418
|
}
|
|
326
419
|
}
|
|
327
420
|
// Transitive vulnerable packages — previously counted in scan totals but
|
|
@@ -329,8 +422,10 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
329
422
|
// The skip is instance-aware: a transitive instance at a DIFFERENT version
|
|
330
423
|
// than a same-named direct dep is its own finding (anyhow 1.0.102 transitive
|
|
331
424
|
// beside anyhow 1.0.104 direct), not a duplicate of the direct row.
|
|
425
|
+
let devTransitiveLeftOut = 0;
|
|
332
426
|
if (vulnResult) {
|
|
333
427
|
const transitive = new Map();
|
|
428
|
+
const devSkipped = new Set();
|
|
334
429
|
for (const v of vulnResult.vulnerabilities) {
|
|
335
430
|
if (v.isDirect)
|
|
336
431
|
continue;
|
|
@@ -340,19 +435,23 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
340
435
|
continue;
|
|
341
436
|
if (directPackages.has(instanceKey(v.ecosystem, v.package, v.currentVersion)))
|
|
342
437
|
continue;
|
|
343
|
-
if (!includeDev && v.isDev && v.devScopeKnown)
|
|
344
|
-
continue;
|
|
345
438
|
const key = instanceKey(v.ecosystem, v.package, v.currentVersion);
|
|
439
|
+
if (!includeDev && isDevOnlyCopy(v)) {
|
|
440
|
+
devSkipped.add(key);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
346
443
|
if (!transitive.has(key))
|
|
347
444
|
transitive.set(key, []);
|
|
348
445
|
transitive.get(key).push(v);
|
|
349
446
|
}
|
|
447
|
+
devTransitiveLeftOut = [...devSkipped].filter((k) => !transitive.has(k)).length;
|
|
350
448
|
for (const entries of transitive.values()) {
|
|
351
449
|
const first = entries[0];
|
|
352
450
|
const fixedVersions = entries.map((e) => e.fixedVersion).filter((f) => Boolean(f));
|
|
353
451
|
const platformActive = !entries.every((e) => !e.platformActive);
|
|
452
|
+
const groups = groupAdvisories(entries);
|
|
354
453
|
const reasons = [
|
|
355
|
-
|
|
454
|
+
vulnerabilitiesReason(groups),
|
|
356
455
|
"Transitive dependency — not declared in your manifest; the fix arrives via a parent package update or a lockfile refresh",
|
|
357
456
|
];
|
|
358
457
|
if (!platformActive) {
|
|
@@ -365,7 +464,7 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
365
464
|
currentVersion: first.currentVersion,
|
|
366
465
|
targetVersion: target,
|
|
367
466
|
upgradeType: upgradeTypeBetween(first.currentVersion, target),
|
|
368
|
-
risk: platformActive ?
|
|
467
|
+
risk: platformActive ? riskOf(entries.filter((e) => e.platformActive)) : "low",
|
|
369
468
|
reasons,
|
|
370
469
|
breaking: false,
|
|
371
470
|
isDev: entries.every((e) => e.isDev),
|
|
@@ -374,8 +473,13 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
374
473
|
platformActive,
|
|
375
474
|
};
|
|
376
475
|
recommendations.push(rec);
|
|
377
|
-
if (rec.action === "waiting_on_upstream") {
|
|
378
|
-
|
|
476
|
+
if (rec.action === "waiting_on_upstream" || rec.action === "no_fix_available") {
|
|
477
|
+
const unfixed = groups.filter((g) => !g.members.some((m) => m.fixedVersion));
|
|
478
|
+
fixPathInputs.set(rec, {
|
|
479
|
+
fixes: fixedVersions,
|
|
480
|
+
sourceDirs: [...new Set(entries.flatMap((e) => e.sourceDirs ?? []))],
|
|
481
|
+
unfixed: unfixed.map((g) => ({ id: g.best.vulnId, ids: [...g.ids] })),
|
|
482
|
+
});
|
|
379
483
|
}
|
|
380
484
|
}
|
|
381
485
|
}
|
|
@@ -401,20 +505,39 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
401
505
|
const limited = filtered.slice(0, maxRecs);
|
|
402
506
|
// Verify the fix path of every vulnerable step the plan returns: a target
|
|
403
507
|
// no advisory affects, and whether a lockfile refresh already reaches it.
|
|
404
|
-
const
|
|
508
|
+
const verifiedInputs = limited.filter((r) => fixPathInputs.has(r)).map((r) => ({ step: r, ...fixPathInputs.get(r) }));
|
|
509
|
+
const unverified = await verifyFixPaths(verifiedInputs, liveIntel);
|
|
510
|
+
// A direct bump must not stop at a release that still pins a vulnerable transitive a slightly
|
|
511
|
+
// newer release clears (express 4.20.0 still pins path-to-regexp 0.1.10; audit H1).
|
|
512
|
+
await reconcileDirectTargets(limited, verifiedInputs, liveIntel);
|
|
405
513
|
limited.sort(planOrder);
|
|
514
|
+
// Go module versions carry their `v` in every field, as currentVersion does (audit 7e).
|
|
515
|
+
for (const r of limited) {
|
|
516
|
+
r.targetVersion = inEcosystemForm(r.ecosystem, r.targetVersion);
|
|
517
|
+
if (r.latestVersion !== undefined)
|
|
518
|
+
r.latestVersion = inEcosystemForm(r.ecosystem, r.latestVersion);
|
|
519
|
+
}
|
|
406
520
|
const upgrades = limited.filter((r) => r.scope === "direct" && r.action === "upgrade_direct");
|
|
407
521
|
const quickWins = upgrades.filter((r) => !r.breaking).length;
|
|
408
522
|
const breakingChanges = upgrades.filter((r) => r.breaking).length;
|
|
409
523
|
const waitingOnUpstream = limited.filter((r) => r.action === "waiting_on_upstream").length;
|
|
410
524
|
const lockfileRefreshes = limited.filter((r) => r.action === "lockfile_refresh").length;
|
|
525
|
+
const parentUpgradeSteps = limited.filter((r) => r.action === "upgrade_parent").length;
|
|
526
|
+
const devOnly = limited.filter((r) => r.isDev).length;
|
|
527
|
+
const overrides = limited.filter((r) => r.action === "override_or_replace").length;
|
|
411
528
|
const reinstalls = limited.filter((r) => r.action === "reinstall").length;
|
|
412
529
|
const noFix = limited.filter((r) => r.action === "no_fix_available").length;
|
|
413
530
|
const inactive = limited.filter((r) => r.action === "not_built_on_this_host").length;
|
|
531
|
+
const unverifiedParents = limited.filter((r) => r.action === "upgrade_parent_unverified").length;
|
|
532
|
+
const removals = limited.filter((r) => r.action === "remove_dependency").length;
|
|
533
|
+
const holds = limited.filter((r) => r.action === "hold_current").length;
|
|
414
534
|
// Summary
|
|
415
535
|
const parts = [];
|
|
536
|
+
if (coverage)
|
|
537
|
+
parts.push(`${coverage.note} This plan ranks only what the checked part showed`);
|
|
416
538
|
parts.push(filtered.length > limited.length
|
|
417
|
-
?
|
|
539
|
+
? // The counts that follow describe the rows shown, not the whole plan (audit 6.2.5 D-6: 6 waiting shown vs 43 real).
|
|
540
|
+
`Showing ${limited.length} of ${filtered.length} recommendations (raise max_recommendations for the rest); the counts below are for the ${limited.length} shown`
|
|
418
541
|
: `${limited.length} recommendation${limited.length !== 1 ? "s" : ""}`);
|
|
419
542
|
if (upgrades.length > 0)
|
|
420
543
|
parts.push(`${upgrades.length} upgradable directly (${quickWins} non-breaking, ${breakingChanges} crossing a major version)`);
|
|
@@ -422,8 +545,19 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
422
545
|
parts.push(`${reinstalls} need only a reinstall (node_modules behind the lockfile)`);
|
|
423
546
|
if (lockfileRefreshes > 0)
|
|
424
547
|
parts.push(`${lockfileRefreshes} transitive, fixed by a lockfile refresh (every parent already admits a clean version)`);
|
|
548
|
+
if (parentUpgradeSteps > 0)
|
|
549
|
+
parts.push(`${parentUpgradeSteps} transitive, fixed by upgrading the parent that pins it (parentUpgrades names the release)`);
|
|
550
|
+
if (unverifiedParents > 0) {
|
|
551
|
+
parts.push(`${unverifiedParents} transitive, a parent release that admits a clean version exists but no release of the declared dependency that brings it in was verified (parentUpgrades says what to check; not counted as fixed)`);
|
|
552
|
+
}
|
|
425
553
|
if (waitingOnUpstream > 0)
|
|
426
554
|
parts.push(`${waitingOnUpstream} transitive, waiting on upstream`);
|
|
555
|
+
if (removals > 0)
|
|
556
|
+
parts.push(`${removals} to remove or replace: no release is clean of the malware or advisories affecting the installed copy`);
|
|
557
|
+
if (holds > 0)
|
|
558
|
+
parts.push(`${holds} to keep where ${holds === 1 ? "it is" : "they are"}: every newer release is known-affected or withdrawn`);
|
|
559
|
+
if (overrides > 0)
|
|
560
|
+
parts.push(`${overrides} transitive, blocked by a parent that will not move (deprecated, already its latest release, or no release admits a clean version): override the version or replace the parent`);
|
|
427
561
|
if (unverified > 0)
|
|
428
562
|
parts.push(`${unverified} fix target${unverified !== 1 ? "s" : ""} checked only against the installed version's advisories (offline, or OSV or the registry did not answer in time; see fixPathChecked)`);
|
|
429
563
|
if (noFix > 0)
|
|
@@ -433,20 +567,36 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
433
567
|
const criticalCount = limited.filter((r) => r.risk === "critical").length;
|
|
434
568
|
if (criticalCount > 0)
|
|
435
569
|
parts.push(`${criticalCount} critical`);
|
|
570
|
+
if (devOnly > 0)
|
|
571
|
+
parts.push(`${devOnly} dev-only (isDev)`);
|
|
572
|
+
// The population, worded as dependency_health and vulnerability_scan word it (finding 6).
|
|
573
|
+
if (vulnResult)
|
|
574
|
+
parts.push(populationNote(vulnResult.vulnerabilities, liveIntel.getAuditDeps(), includeDev, named));
|
|
575
|
+
if (devDirectLeftOut + devTransitiveLeftOut + devDirectUnchecked > 0) {
|
|
576
|
+
const left = [];
|
|
577
|
+
if (devDirectLeftOut > 0)
|
|
578
|
+
left.push(`${devDirectLeftOut} vulnerable devDependenc${devDirectLeftOut !== 1 ? "ies" : "y"}`);
|
|
579
|
+
if (devDirectUnchecked > 0)
|
|
580
|
+
left.push(`${devDirectUnchecked} devDependenc${devDirectUnchecked !== 1 ? "ies" : "y"} not checked for vulnerabilities`);
|
|
581
|
+
if (devTransitiveLeftOut > 0)
|
|
582
|
+
left.push(`${devTransitiveLeftOut} dev-only vulnerable transitive cop${devTransitiveLeftOut !== 1 ? "ies" : "y"}`);
|
|
583
|
+
parts.push(`include_dev: false left out ${left.join(" and ")} (the default, true, plans them)`);
|
|
584
|
+
}
|
|
585
|
+
if (devFreshnessSkipped > 0) {
|
|
586
|
+
parts.push(`${devFreshnessSkipped} devDependenc${devFreshnessSkipped !== 1 ? "ies" : "y"} with no known vulnerability ${devFreshnessSkipped !== 1 ? "were" : "was"} not checked for newer releases or deprecation (include_dev: true checks them)`);
|
|
587
|
+
}
|
|
436
588
|
if (maintenanceNotices.size > 0) {
|
|
437
589
|
parts.push(`${maintenanceNotices.size} unmaintained-package notice${maintenanceNotices.size !== 1 ? "s" : ""} not ranked (no fix exists; see vulnerability_scan maintenance_notices)`);
|
|
438
590
|
}
|
|
439
591
|
const partial = discoveryNote(liveIntel.getDiscovery());
|
|
440
592
|
if (partial)
|
|
441
593
|
parts.push(partial.replace(/[.]$/, ""));
|
|
594
|
+
// Ecosystems not read, test fixtures left out, lockfiles that could not be read: never silent.
|
|
595
|
+
for (const note of [...exclusionNotes(liveIntel.getDiscovery()), ...unreadableNotes(liveIntel.getLockfileProblems())])
|
|
596
|
+
parts.push(note.replace(/[.]$/, ""));
|
|
442
597
|
const nothing = vulnerabilityDataAvailable ? "nothing to upgrade (no known vulnerability, deprecation, or newer release of a direct dependency)" : "no vulnerability data to plan from";
|
|
443
598
|
if (only && limited.length === 0)
|
|
444
599
|
parts.push(emptyAnswerNote(only, liveIntel, includeDev, nothing));
|
|
445
|
-
if (!vulnerabilityDataAvailable) {
|
|
446
|
-
parts.push(liveIntel.isEnabled()
|
|
447
|
-
? `No vulnerability data: the OSV scan did not finish within ${PLANNER_SCAN_TIMEOUT_MS / 1000}s or OSV was unreachable, so this plan is CVE-blind. Call again to use the scan once it completes`
|
|
448
|
-
: "No vulnerability data: offline mode (FOURDA_OFFLINE), so this plan is CVE-blind");
|
|
449
|
-
}
|
|
450
600
|
return {
|
|
451
601
|
generatedAt: new Date().toISOString(),
|
|
452
602
|
projectPath: liveIntel.getProjectRoot() ?? vulnResult?.projectPath ?? process.cwd(),
|
|
@@ -457,7 +607,33 @@ async function executeStandalonePlanner(params, liveIntel) {
|
|
|
457
607
|
breakingChanges,
|
|
458
608
|
waitingOnUpstream,
|
|
459
609
|
lockfileRefreshes,
|
|
610
|
+
parentUpgradeSteps,
|
|
460
611
|
vulnerabilityDataAvailable,
|
|
612
|
+
...(coverage ? { vulnerabilityCoverage: coverage } : {}),
|
|
461
613
|
provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
|
|
462
614
|
};
|
|
463
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* "Planned over N vulnerable packages (M installed copies) in the full tree of T installed
|
|
618
|
+
* (K dev-only copies left out)": the same population, counted the same way, as
|
|
619
|
+
* dependency_health's vulnerableCount and vulnerability_scan's total_vulnerable_packages.
|
|
620
|
+
*/
|
|
621
|
+
function populationNote(vulns, tree, includeDev, named) {
|
|
622
|
+
const active = vulns.filter((v) => isActionableVulnerability(v) && (includeDev || !isDevOnlyCopy(v)) && named(v.package, v.ecosystem));
|
|
623
|
+
const packages = new Set(active.map((v) => `${v.ecosystem}\0${v.package}`)).size;
|
|
624
|
+
const copies = new Set(active.map((v) => `${v.ecosystem}\0${v.package}\0${v.currentVersion}`)).size;
|
|
625
|
+
const versioned = tree.filter((d) => d.version !== null);
|
|
626
|
+
const devOnly = versioned.filter((d) => isDevOnlyCopy(d)).length;
|
|
627
|
+
const total = includeDev ? versioned.length : versioned.length - devOnly;
|
|
628
|
+
const dev = devOnly > 0 ? (includeDev ? "; dev-only copies included" : `; ${devOnly.toLocaleString("en-US")} dev-only cop${devOnly === 1 ? "y" : "ies"} left out`) : "";
|
|
629
|
+
return `Planned over ${packages} vulnerable package${packages !== 1 ? "s" : ""} (${copies} installed cop${copies !== 1 ? "ies" : "y"}) in the full tree of ${total.toLocaleString("en-US")} installed${dev ? ` (${dev.slice(2)})` : ""}`;
|
|
630
|
+
}
|
|
631
|
+
/** "N known vulnerabilities (ids)", one per alias group. */
|
|
632
|
+
function vulnerabilitiesReason(groups) {
|
|
633
|
+
const n = groups.length;
|
|
634
|
+
return `${n} known vulnerabilit${n !== 1 ? "ies" : "y"} (${groups.map((g) => g.best.vulnId).join(", ")})`;
|
|
635
|
+
}
|
|
636
|
+
/** Before the fix path is checked; verifyFixPaths rewrites it once the target is known (M2). */
|
|
637
|
+
function noFixReason(n) {
|
|
638
|
+
return `${n} of them ${n === 1 ? "has" : "have"} no published fix`;
|
|
639
|
+
}
|
|
@@ -20,11 +20,18 @@ export interface PackageRow {
|
|
|
20
20
|
package: string;
|
|
21
21
|
current_version: string;
|
|
22
22
|
severity: SeverityTier;
|
|
23
|
+
/** Set when the row's grade is a GHSA alias's: the worst record rates nothing itself. */
|
|
24
|
+
severity_from?: string;
|
|
25
|
+
/** A malicious-package record (MAL- id or alias, CWE-506) affects this copy. */
|
|
26
|
+
malware?: true;
|
|
23
27
|
advisories: number;
|
|
24
28
|
ids: string[];
|
|
25
29
|
/** The version to move to: checked against every advisory of the package when fix_checked says so. */
|
|
26
30
|
fixed_version: string | null;
|
|
27
31
|
fix_checked: "all_advisories" | "installed_advisories_only";
|
|
32
|
+
/** Releases the registry withdrew (yanked, deprecated) that `fixed_version` steps over, and why, in plain words (FX-H5). */
|
|
33
|
+
withheld?: string[];
|
|
34
|
+
withheld_note?: string;
|
|
28
35
|
unfixed_advisories?: number;
|
|
29
36
|
is_direct: boolean | null;
|
|
30
37
|
is_dev?: true;
|
|
@@ -34,23 +41,55 @@ export interface PackageRow {
|
|
|
34
41
|
/** One row per package version: worst graded severity, the fix that clears every fixable advisory. */
|
|
35
42
|
export declare function groupByPackage(rows: FormattedVuln[], targets?: FullScan["fix_targets"]): PackageRow[];
|
|
36
43
|
export declare function conciseScanResult(full: FullScan): {
|
|
44
|
+
summary?: string | undefined;
|
|
37
45
|
scan_time: string;
|
|
38
46
|
project_root: string;
|
|
39
47
|
host_platform: string;
|
|
40
48
|
ecosystems_scanned: string[];
|
|
41
49
|
total_dependencies_scanned: number;
|
|
42
|
-
|
|
50
|
+
dev_scope?: string | undefined;
|
|
51
|
+
total_vulnerable_packages: number | null;
|
|
43
52
|
coverage?: {
|
|
44
53
|
complete: false;
|
|
45
54
|
note: string;
|
|
55
|
+
unchecked_dependencies?: number | undefined;
|
|
56
|
+
checked_dependencies?: number | undefined;
|
|
57
|
+
osv_status?: string | undefined;
|
|
46
58
|
lockfiles_not_scanned: number;
|
|
59
|
+
unreadable_lockfiles?: {
|
|
60
|
+
path: string;
|
|
61
|
+
error: string;
|
|
62
|
+
}[] | undefined;
|
|
63
|
+
partial_lockfiles?: {
|
|
64
|
+
path: string;
|
|
65
|
+
skipped_entries: number;
|
|
66
|
+
reason: string;
|
|
67
|
+
}[] | undefined;
|
|
68
|
+
refused_includes?: {
|
|
69
|
+
path: string;
|
|
70
|
+
reason: string;
|
|
71
|
+
}[] | undefined;
|
|
72
|
+
not_covered?: {
|
|
73
|
+
path: string;
|
|
74
|
+
ecosystem: string;
|
|
75
|
+
}[] | undefined;
|
|
76
|
+
not_covered_count?: number | undefined;
|
|
77
|
+
not_scanned?: {
|
|
78
|
+
test_fixture_lockfiles: number;
|
|
79
|
+
dirs: string[];
|
|
80
|
+
} | undefined;
|
|
47
81
|
unresolved_manifests?: {
|
|
48
82
|
manifest: string;
|
|
49
83
|
declared_ranges: number;
|
|
50
84
|
fix: string;
|
|
51
85
|
}[] | undefined;
|
|
52
86
|
} | undefined;
|
|
53
|
-
by_severity: import("../live/severity-scope.js").SeverityCounts;
|
|
87
|
+
by_severity: import("../live/severity-scope.js").SeverityCounts | null;
|
|
88
|
+
found_in_checked_dependencies?: {
|
|
89
|
+
note: string;
|
|
90
|
+
vulnerable_packages: number;
|
|
91
|
+
by_severity: import("../live/severity-scope.js").SeverityCounts;
|
|
92
|
+
} | undefined;
|
|
54
93
|
vulnerable_packages: PackageRow[];
|
|
55
94
|
vulnerable_packages_note?: string | undefined;
|
|
56
95
|
recommendations: string[];
|
|
@@ -79,6 +118,12 @@ export declare function conciseScanResult(full: FullScan): {
|
|
|
79
118
|
lockfiles_read: number;
|
|
80
119
|
resolved_at: string | null;
|
|
81
120
|
privacy: string;
|
|
121
|
+
alias_severity?: {
|
|
122
|
+
rated_from_alias: number;
|
|
123
|
+
unresolved: string[];
|
|
124
|
+
ungraded: string[];
|
|
125
|
+
note?: string | undefined;
|
|
126
|
+
} | undefined;
|
|
82
127
|
};
|
|
83
128
|
};
|
|
84
129
|
export {};
|