Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/output/printers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export function printSuggestedFixCommands(findings: Finding[], scanInput: ScanIn
if (!plan) return;
if (plan.sections.length === 0) return;
const sharedDirectTableWidths = computeSharedDirectTableWidths(plan.sections);
const sharedParentUpgradeTableWidths = computeSharedParentUpgradeTableWidths(plan.sections);

console.log("");
console.log(chalk.bold.yellow("🛠 Copy And Run These Fix Commands"));
Expand All @@ -169,6 +170,8 @@ export function printSuggestedFixCommands(findings: Finding[], scanInput: ScanIn
for (const note of remainingNotes) {
console.log(chalk.gray(` Note: ${note}`));
}
} else if (shouldRenderParentUpgradeTable(section.targets)) {
printParentUpgradeTargetsTable(section.targets, sharedParentUpgradeTableWidths);
}
console.log(renderCommandCallout(section.command));
}
Expand Down Expand Up @@ -662,6 +665,38 @@ function printDirectTargetsTable(
console.log(line("└", "┴", "┘"));
}

function printParentUpgradeTargetsTable(
targets: Array<{
package: string;
currentVersion?: string;
targetVersion: string;
kind: "direct" | "parent-upgrade";
reason: string;
}>,
widthsOverride?: number[],
): void {
const headers = ["Package", "Current", "Recommended target", "Context"];
const rows = targets.map(target => [
target.package,
target.currentVersion ?? "-",
chalk.cyan(target.targetVersion),
chalk.gray(target.reason),
]);
if (rows.length === 0) return;

const widths = widthsOverride ?? computeTableWidths(headers, rows);
const line = (left: string, mid: string, right: string) =>
left + widths.map(w => "─".repeat(w + 2)).join(mid) + right;

console.log(line("┌", "┬", "┐"));
console.log(renderRow(headers, widths));
console.log(line("├", "┼", "┤"));
for (const row of rows) {
console.log(renderRow(row.map(value => String(value)), widths));
}
console.log(line("└", "┴", "┘"));
}

function computeTableWidths(headers: string[], rows: string[][]): number[] {
return headers.map((header, index) =>
Math.min(40, Math.max(header.length, ...rows.map(row => stripAnsi(String(row[index])).length))),
Expand Down Expand Up @@ -703,3 +738,40 @@ function computeSharedDirectTableWidths(

return computeTableWidths(headers, rows);
}

function shouldRenderParentUpgradeTable(
targets: Array<{ kind: "direct" | "parent-upgrade" }>,
): boolean {
return targets.length > 0 && targets.every(target => target.kind === "parent-upgrade");
}

function computeSharedParentUpgradeTableWidths(
sections: Array<{
kind: "urgent" | "direct" | "direct-adjusted" | "parent-upgrade";
targets: Array<{
package: string;
currentVersion?: string;
targetVersion: string;
kind: "direct" | "parent-upgrade";
reason: string;
}>;
}>,
): number[] | undefined {
const parentSections = sections.filter(section => shouldRenderParentUpgradeTable(section.targets));
if (parentSections.length === 0) return undefined;

const headers = ["Package", "Current", "Recommended target", "Context"];
const rows: string[][] = [];
for (const section of parentSections) {
for (const target of section.targets) {
rows.push([
target.package,
target.currentVersion ?? "-",
target.targetVersion,
target.reason,
]);
}
}

return computeTableWidths(headers, rows);
}
3 changes: 3 additions & 0 deletions src/remediation/fix-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export function buildSuggestedFixCommandPlan(
) {
upsertTarget(targetsByPackage, {
package: finding.recommendedParentUpgrade.package,
currentVersion: finding.recommendedParentUpgrade.currentVersion,
targetVersion: finding.recommendedParentUpgrade.targetVersion,
scannedVersions: null,
knownVulnerableVersions: null,
Expand Down Expand Up @@ -258,6 +259,7 @@ function upsertTarget(
if (looksLikeVersion(existing.targetVersion) && looksLikeVersion(next.targetVersion)) {
if (compareVersions(next.targetVersion, existing.targetVersion) > 0) {
merged.targetVersion = next.targetVersion;
merged.currentVersion = next.currentVersion ?? merged.currentVersion;
merged.reason = next.reason;
merged.scannedVersions = next.scannedVersions ?? merged.scannedVersions ?? null;
merged.knownVulnerableVersions = next.knownVulnerableVersions ?? merged.knownVulnerableVersions ?? null;
Expand All @@ -267,6 +269,7 @@ function upsertTarget(
}

if (next.kind === "direct" && existing.kind !== "direct") {
merged.currentVersion = next.currentVersion ?? merged.currentVersion;
merged.targetVersion = next.targetVersion;
merged.reason = next.reason;
merged.adjustmentNote = next.adjustmentNote ?? merged.adjustmentNote;
Expand Down
37 changes: 37 additions & 0 deletions tests/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,43 @@ describe("output printers", () => {
expect(lines.join("\n")).toContain("npm install tar@7.5.3");
});

it("prints a parent-upgrade table before the command callout when transitive targets are actionable", () => {
const findings = [
createFinding({
pkg: { name: "diff", version: "7.0.0", ecosystem: "npm", paths: [["project", "mocha", "diff"]] },
relationship: "transitive",
dependencyPaths: [["project", "mocha", "diff"]],
severity: "medium",
firstFixedVersion: "3.5.1",
recommendedParentUpgrade: {
package: "mocha",
currentVersion: "11.7.5",
targetVersion: "12.0.0-beta-4",
viaPath: ["project", "mocha", "diff"],
vulnerablePackage: "diff",
confidence: "exact-direct-child",
reason: "mocha@12.0.0-beta-4 no longer allows diff@7.0.0",
},
}),
];

const lines = captureLogs(() => {
printSuggestedFixCommands(findings, createScanInputForSource("package-lock"));
});
const output = lines.join("\n");

expect(output).toContain("Medium severity parent upgrades");
expect(output).toContain("Package");
expect(output).toContain("Current");
expect(output).toContain("Recommended target");
expect(output).toContain("Context");
expect(output).toContain("mocha");
expect(output).toContain("11.7.5");
expect(output).toContain("12.0.0-beta-4");
expect(output).toContain("Parent upgrade for vulnerable diff@7.0.0");
expect(output.indexOf("Context")).toBeLessThan(output.indexOf("> npm install mocha@12.0.0-beta-4"));
});

it("prints registry-adjusted notes before the adjusted command", () => {
const findings = [
createFinding({
Expand Down
Loading