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
17 changes: 8 additions & 9 deletions src/output/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { chalk } from "../utils/chalk.js";
import { severityOrder } from "../constants.js";
import { loadCache } from "../osv/cache.js";
import { inferSeverity } from "../osv/severity.js";
import { getPrimaryParent } from "../utils/finding.js";

export function formatSeverityLabel(severity: string): string {
const lower = severity.toLowerCase();
Expand All @@ -20,12 +21,6 @@ export function formatRelationshipLabel(value: string): string {
return chalk.gray(value);
}

export function getPrimaryParent(finding: Finding): string | null {
const firstPath = finding.dependencyPaths?.[0];
if (!firstPath || firstPath.length < 3) return null;
return firstPath[1] ?? null;
}

export function getRecommendedAction(finding: Finding): string {
if (finding.relationship === "direct" && finding.firstFixedVersion) {
return `Upgrade ${finding.pkg.name} to ${finding.firstFixedVersion}+ in this project.`;
Expand All @@ -40,13 +35,13 @@ export function getRecommendedAction(finding: Finding): string {

const parent = getPrimaryParent(finding);
if (parent && finding.firstFixedVersion) {
return `Review ${parent}; aim for a version that resolves ${finding.pkg.name} to ${finding.firstFixedVersion}+`;
return `Upgrade ${parent} — no safe version was identified automatically. Check for a release that resolves ${finding.pkg.name} to ${finding.firstFixedVersion}+.`;
}
if (parent) {
return `Review ${parent}; it currently pulls in vulnerable ${finding.pkg.name}.`;
}
if (finding.firstFixedVersion) {
return `Upgrade the parent dependency chain so ${finding.pkg.name} resolves to ${finding.firstFixedVersion}+`;
return `No dependency path found for ${finding.pkg.name}. Inspect your lockfile to identify which package pulls it in.`;
Comment thread
sonukapoor marked this conversation as resolved.
}
return `Inspect the dependency path for ${finding.pkg.name} and choose the safest upgrade path.`;
}
Expand Down Expand Up @@ -89,8 +84,12 @@ export function summarizeNextAction(finding: Finding): string {
if (finding.recommendedParentUpgrade) {
return `Upgrade ${finding.recommendedParentUpgrade.package} ${finding.recommendedParentUpgrade.currentVersion} -> ${finding.recommendedParentUpgrade.targetVersion}.`;
}
const parent = getPrimaryParent(finding);
if (parent && finding.firstFixedVersion) {
return `Upgrade ${parent} — no safe version identified. Find a release resolving ${finding.pkg.name} to ${finding.firstFixedVersion}+.`;
}
if (finding.firstFixedVersion) {
return `Upgrade the parent dependency chain so it resolves ${finding.pkg.name} to ${finding.firstFixedVersion}+`;
return `No dependency path found for ${finding.pkg.name}. Check your lockfile for which package pulls it in.`;
Comment thread
sonukapoor marked this conversation as resolved.
}
return `Inspect the parent dependency chain for ${finding.pkg.name} and choose the safest available upgrade.`;
}
Expand Down
68 changes: 48 additions & 20 deletions src/output/printers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { chalk, stripAnsi } from "../utils/chalk.js";
import { severityOrder } from "../constants.js";
import { buildSuggestedFixCommandPlan } from "../remediation/fix-commands.js";
import { isMajorVersionBump } from "../utils/version.js";
import { getPrimaryParent } from "../utils/finding.js";
import {
formatSeverityLabel,
formatRelationshipLabel,
getPrimaryParent,
getRecommendedAction,
summarizeRisk,
summarizeNextAction,
Expand Down Expand Up @@ -168,20 +168,34 @@ export function printSuggestedFixCommands(findings: Finding[], scanInput: ScanIn
for (const section of plan.sections) {
console.log("");
console.log(colorFixSectionTitle(section.severity, section.title));
const hasDirectTargets = section.targets.some(t => t.kind === "direct");
if (section.kind === "direct" || section.kind === "direct-adjusted" || (section.kind === "urgent" && hasDirectTargets)) {

if (section.kind === "direct" || section.kind === "direct-adjusted") {
const validationSummary = summarizeAdjustedValidation(section.targets);
const remainingNotes: string[] = [];
// Urgent sections compute their own widths so the Breaking? column
// is not truncated by widths derived from smaller non-urgent sections.
const tableWidths = section.kind === "urgent" ? undefined : sharedDirectTableWidths;
printDirectTargetsTable(section.targets, remainingNotes, validationSummary, tableWidths);
printDirectTargetsTable(section.targets, remainingNotes, validationSummary, sharedDirectTableWidths);
for (const note of remainingNotes) {
console.log(chalk.gray(` Note: ${note}`));
}
} else if (section.kind === "urgent") {
const directTargets = section.targets.filter(t => t.kind === "direct");
const parentUpgradeTargets = section.targets.filter(t => t.kind === "parent-upgrade");
if (directTargets.length > 0) {
const validationSummary = summarizeAdjustedValidation(directTargets);
const remainingNotes: string[] = [];
// Urgent sections compute their own widths so the Breaking? column
// is not truncated by widths derived from smaller non-urgent sections.
printDirectTargetsTable(directTargets, remainingNotes, validationSummary, undefined);
for (const note of remainingNotes) {
console.log(chalk.gray(` Note: ${note}`));
}
}
if (parentUpgradeTargets.length > 0) {
printParentUpgradeTargetsTable(parentUpgradeTargets, undefined);
}
Comment thread
sonukapoor marked this conversation as resolved.
} else if (shouldRenderParentUpgradeTable(section.targets)) {
printParentUpgradeTargetsTable(section.targets, sharedParentUpgradeTableWidths);
}

console.log(renderCommandCallout(section.command));
}
}
Expand Down Expand Up @@ -419,14 +433,26 @@ export function printCompactOutput(findings: Finding[], scanInput?: ScanInput) {
` ${chalk.gray(`Fix: upgrade ${finding.recommendedParentUpgrade.package} to ${finding.recommendedParentUpgrade.targetVersion}`)}`,
);
} else if (finding.firstFixedVersion) {
const action = finding.relationship === "direct"
? `upgrade to ${finding.firstFixedVersion}`
: `upgrade parent chain to resolve ${finding.firstFixedVersion}+`;
let action: string;
if (finding.relationship === "direct") {
action = `upgrade to ${finding.firstFixedVersion}`;
} else {
const parent = getPrimaryParent(finding);
action = parent
? `Upgrade ${parent} — check for release resolving ${finding.pkg.name} to ${finding.firstFixedVersion}+`
: `No dependency path found — inspect lockfile to identify which package pulls in ${finding.pkg.name}`;
}
console.log(` ${chalk.gray(`Fix: ${action}`)}`);
} else {
const action = finding.relationship === "direct"
? "review and upgrade directly"
: "upgrade parent chain to resolve";
let action: string;
if (finding.relationship === "direct") {
action = "review and upgrade directly";
} else {
const parent = getPrimaryParent(finding);
action = parent
? `Upgrade ${parent} to resolve ${finding.pkg.name}`
: `No dependency path found — inspect lockfile to identify which package pulls in ${finding.pkg.name}`;
}
console.log(` ${chalk.gray(`Fix: ${action}`)}`);
}
console.log("");
Expand Down Expand Up @@ -484,7 +510,7 @@ export function printCompactOutput(findings: Finding[], scanInput?: ScanInput) {
if (parent) {
console.log(`Upgrade ${chalk.whiteBright(parent)} to resolve ${topFinding.pkg.name}`);
} else {
console.log(`Upgrade parent chain to resolve ${chalk.whiteBright(topFinding.pkg.name)}`);
console.log(`No dependency path found — inspect lockfile to identify which package pulls in ${chalk.whiteBright(topFinding.pkg.name)}`);
}
}
if (plan) {
Expand Down Expand Up @@ -740,7 +766,8 @@ function printParentUpgradeTargetsTable(
});
if (rows.length === 0) return;

const widths = widthsOverride ?? computeTableWidths(headers, rows);
// Allow the Context column (index 4) up to 60 chars so reason text is not truncated.
Comment thread
sonukapoor marked this conversation as resolved.
const widths = widthsOverride ?? computeTableWidths(headers, rows, [40, 40, 40, 40, 60]);
const line = (left: string, mid: string, right: string) =>
left + widths.map(w => "─".repeat(w + 2)).join(mid) + right;

Expand All @@ -753,10 +780,11 @@ function printParentUpgradeTargetsTable(
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))),
);
function computeTableWidths(headers: string[], rows: string[][], columnMaxWidths?: number[]): number[] {
return headers.map((header, index) => {
const maxWidth = columnMaxWidths?.[index] ?? 40;
return Math.min(maxWidth, Math.max(header.length, ...rows.map(row => stripAnsi(String(row[index])).length)));
});
}

function computeSharedDirectTableWidths(
Expand Down Expand Up @@ -843,5 +871,5 @@ function computeSharedParentUpgradeTableWidths(
}
}

return computeTableWidths(headers, rows);
return computeTableWidths(headers, rows, [40, 40, 40, 40, 60]);
}
17 changes: 16 additions & 1 deletion src/remediation/fix-commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Finding, ScanInput, SeverityLabel } from "../types.js";
import { severityOrder } from "../constants.js";
import { compareVersions, looksLikeVersion } from "../utils/version.js";
import { getPrimaryParent } from "../utils/finding.js";

export type SuggestedFixPackageManager = "npm" | "pnpm" | "yarn" | "bun";

Expand Down Expand Up @@ -186,12 +187,26 @@ export function buildSuggestedFixCommandPlan(
continue;
}

const primaryParent = getPrimaryParent(finding);
if (finding.relationship === "transitive" && primaryParent) {
const fixClause = finding.firstFixedVersion
? ` — check for a release that resolves ${finding.pkg.name} to ${finding.firstFixedVersion}+`
: "";
skippedByKey.set(`${finding.relationship}:${finding.pkg.name}@${finding.pkg.version}`, {
package: finding.pkg.name,
version: finding.pkg.version,
relationship: finding.relationship,
reason: `${finding.pkg.name}@${finding.pkg.version} is pulled in by ${primaryParent}. No safe upgrade version for ${primaryParent} was identified automatically${fixClause}.`,
});
continue;
}

skippedByKey.set(`${finding.relationship}:${finding.pkg.name}@${finding.pkg.version}`, {
package: finding.pkg.name,
version: finding.pkg.version,
relationship: finding.relationship,
reason: finding.relationship === "transitive"
? "No specific parent upgrade target was found for this transitive issue."
? `No dependency path available for ${finding.pkg.name}@${finding.pkg.version}. Inspect your lockfile to find which package pulls it in.`
: "No confident automatic fix command could be generated for this issue.",
});
}
Expand Down
7 changes: 7 additions & 0 deletions src/utils/finding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Finding } from "../types.js";

export function getPrimaryParent(finding: Finding): string | null {
const firstPath = finding.dependencyPaths?.[0];
if (!firstPath || firstPath.length < 3) return null;
return firstPath[1] ?? null;
}
Loading
Loading