Skip to content

Commit 0547fb0

Browse files
committed
feat: add parent-upgrade tables to verbose fix output
1 parent 4af66d0 commit 0547fb0

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

src/output/printers.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export function printSuggestedFixCommands(findings: Finding[], scanInput: ScanIn
153153
if (!plan) return;
154154
if (plan.sections.length === 0) return;
155155
const sharedDirectTableWidths = computeSharedDirectTableWidths(plan.sections);
156+
const sharedParentUpgradeTableWidths = computeSharedParentUpgradeTableWidths(plan.sections);
156157

157158
console.log("");
158159
console.log(chalk.bold.yellow("🛠 Copy And Run These Fix Commands"));
@@ -169,6 +170,8 @@ export function printSuggestedFixCommands(findings: Finding[], scanInput: ScanIn
169170
for (const note of remainingNotes) {
170171
console.log(chalk.gray(` Note: ${note}`));
171172
}
173+
} else if (shouldRenderParentUpgradeTable(section.targets)) {
174+
printParentUpgradeTargetsTable(section.targets, sharedParentUpgradeTableWidths);
172175
}
173176
console.log(renderCommandCallout(section.command));
174177
}
@@ -662,6 +665,38 @@ function printDirectTargetsTable(
662665
console.log(line("└", "┴", "┘"));
663666
}
664667

668+
function printParentUpgradeTargetsTable(
669+
targets: Array<{
670+
package: string;
671+
currentVersion?: string;
672+
targetVersion: string;
673+
kind: "direct" | "parent-upgrade";
674+
reason: string;
675+
}>,
676+
widthsOverride?: number[],
677+
): void {
678+
const headers = ["Package", "Current", "Recommended target", "Context"];
679+
const rows = targets.map(target => [
680+
target.package,
681+
target.currentVersion ?? "-",
682+
chalk.cyan(target.targetVersion),
683+
chalk.gray(target.reason),
684+
]);
685+
if (rows.length === 0) return;
686+
687+
const widths = widthsOverride ?? computeTableWidths(headers, rows);
688+
const line = (left: string, mid: string, right: string) =>
689+
left + widths.map(w => "─".repeat(w + 2)).join(mid) + right;
690+
691+
console.log(line("┌", "┬", "┐"));
692+
console.log(renderRow(headers, widths));
693+
console.log(line("├", "┼", "┤"));
694+
for (const row of rows) {
695+
console.log(renderRow(row.map(value => String(value)), widths));
696+
}
697+
console.log(line("└", "┴", "┘"));
698+
}
699+
665700
function computeTableWidths(headers: string[], rows: string[][]): number[] {
666701
return headers.map((header, index) =>
667702
Math.min(40, Math.max(header.length, ...rows.map(row => stripAnsi(String(row[index])).length))),
@@ -703,3 +738,40 @@ function computeSharedDirectTableWidths(
703738

704739
return computeTableWidths(headers, rows);
705740
}
741+
742+
function shouldRenderParentUpgradeTable(
743+
targets: Array<{ kind: "direct" | "parent-upgrade" }>,
744+
): boolean {
745+
return targets.length > 0 && targets.every(target => target.kind === "parent-upgrade");
746+
}
747+
748+
function computeSharedParentUpgradeTableWidths(
749+
sections: Array<{
750+
kind: "urgent" | "direct" | "direct-adjusted" | "parent-upgrade";
751+
targets: Array<{
752+
package: string;
753+
currentVersion?: string;
754+
targetVersion: string;
755+
kind: "direct" | "parent-upgrade";
756+
reason: string;
757+
}>;
758+
}>,
759+
): number[] | undefined {
760+
const parentSections = sections.filter(section => shouldRenderParentUpgradeTable(section.targets));
761+
if (parentSections.length === 0) return undefined;
762+
763+
const headers = ["Package", "Current", "Recommended target", "Context"];
764+
const rows: string[][] = [];
765+
for (const section of parentSections) {
766+
for (const target of section.targets) {
767+
rows.push([
768+
target.package,
769+
target.currentVersion ?? "-",
770+
target.targetVersion,
771+
target.reason,
772+
]);
773+
}
774+
}
775+
776+
return computeTableWidths(headers, rows);
777+
}

src/remediation/fix-commands.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export function buildSuggestedFixCommandPlan(
150150
) {
151151
upsertTarget(targetsByPackage, {
152152
package: finding.recommendedParentUpgrade.package,
153+
currentVersion: finding.recommendedParentUpgrade.currentVersion,
153154
targetVersion: finding.recommendedParentUpgrade.targetVersion,
154155
scannedVersions: null,
155156
knownVulnerableVersions: null,
@@ -258,6 +259,7 @@ function upsertTarget(
258259
if (looksLikeVersion(existing.targetVersion) && looksLikeVersion(next.targetVersion)) {
259260
if (compareVersions(next.targetVersion, existing.targetVersion) > 0) {
260261
merged.targetVersion = next.targetVersion;
262+
merged.currentVersion = next.currentVersion ?? merged.currentVersion;
261263
merged.reason = next.reason;
262264
merged.scannedVersions = next.scannedVersions ?? merged.scannedVersions ?? null;
263265
merged.knownVulnerableVersions = next.knownVulnerableVersions ?? merged.knownVulnerableVersions ?? null;
@@ -267,6 +269,7 @@ function upsertTarget(
267269
}
268270

269271
if (next.kind === "direct" && existing.kind !== "direct") {
272+
merged.currentVersion = next.currentVersion ?? merged.currentVersion;
270273
merged.targetVersion = next.targetVersion;
271274
merged.reason = next.reason;
272275
merged.adjustmentNote = next.adjustmentNote ?? merged.adjustmentNote;

tests/output.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,43 @@ describe("output printers", () => {
553553
expect(lines.join("\n")).toContain("npm install tar@7.5.3");
554554
});
555555

556+
it("prints a parent-upgrade table before the command callout when transitive targets are actionable", () => {
557+
const findings = [
558+
createFinding({
559+
pkg: { name: "diff", version: "7.0.0", ecosystem: "npm", paths: [["project", "mocha", "diff"]] },
560+
relationship: "transitive",
561+
dependencyPaths: [["project", "mocha", "diff"]],
562+
severity: "medium",
563+
firstFixedVersion: "3.5.1",
564+
recommendedParentUpgrade: {
565+
package: "mocha",
566+
currentVersion: "11.7.5",
567+
targetVersion: "12.0.0-beta-4",
568+
viaPath: ["project", "mocha", "diff"],
569+
vulnerablePackage: "diff",
570+
confidence: "exact-direct-child",
571+
reason: "mocha@12.0.0-beta-4 no longer allows diff@7.0.0",
572+
},
573+
}),
574+
];
575+
576+
const lines = captureLogs(() => {
577+
printSuggestedFixCommands(findings, createScanInputForSource("package-lock"));
578+
});
579+
const output = lines.join("\n");
580+
581+
expect(output).toContain("Medium severity parent upgrades");
582+
expect(output).toContain("Package");
583+
expect(output).toContain("Current");
584+
expect(output).toContain("Recommended target");
585+
expect(output).toContain("Context");
586+
expect(output).toContain("mocha");
587+
expect(output).toContain("11.7.5");
588+
expect(output).toContain("12.0.0-beta-4");
589+
expect(output).toContain("Parent upgrade for vulnerable diff@7.0.0");
590+
expect(output.indexOf("Context")).toBeLessThan(output.indexOf("> npm install mocha@12.0.0-beta-4"));
591+
});
592+
556593
it("prints registry-adjusted notes before the adjusted command", () => {
557594
const findings = [
558595
createFinding({

0 commit comments

Comments
 (0)