-
-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathformatters.ts
More file actions
167 lines (148 loc) · 7.62 KB
/
formatters.ts
File metadata and controls
167 lines (148 loc) · 7.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import type { Finding } from "../types.js";
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();
if (lower === "critical") return chalk.redBright(severity);
if (lower === "high") return chalk.red(severity);
if (lower === "medium") return chalk.yellow(severity);
if (lower === "low") return chalk.blueBright(severity);
if (lower === "unknown") return chalk.magenta(severity);
return severity;
}
export function formatRelationshipLabel(value: string): string {
if (value.startsWith("direct")) return chalk.green(value);
if (value.startsWith("transitive")) return chalk.yellow(value);
return chalk.gray(value);
}
export function getRecommendedAction(finding: Finding): string {
if (finding.relationship === "direct" && finding.firstFixedVersion) {
return `Upgrade ${finding.pkg.name} to ${finding.firstFixedVersion}+ in this project.`;
}
if (finding.relationship === "direct") {
return `Review and upgrade ${finding.pkg.name} directly in this project.`;
}
if (finding.recommendedParentUpgrade) {
return `Upgrade ${finding.recommendedParentUpgrade.package} from ${finding.recommendedParentUpgrade.currentVersion} to ${finding.recommendedParentUpgrade.targetVersion} to stop pulling in vulnerable ${finding.pkg.name}.`;
}
const parent = getPrimaryParent(finding);
if (parent && 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 `No dependency path found for ${finding.pkg.name}. Inspect your lockfile to identify which package pulls it in.`;
}
return `Inspect the dependency path for ${finding.pkg.name} and choose the safest upgrade path.`;
}
export function summarizeRisk(finding: Finding): string {
let risk = "";
if (finding.severity === "critical" && finding.relationship === "direct") {
risk = "Critical direct dependency. Prioritize this first because the project controls it directly.";
} else if (finding.severity === "high" && finding.relationship === "direct") {
risk = "High-severity direct dependency. A direct upgrade is likely the fastest path.";
} else if (finding.relationship === "transitive" && finding.recommendedParentUpgrade) {
risk = `Transitive issue. A specific parent upgrade target was found for ${finding.recommendedParentUpgrade.package}.`;
} else if (finding.relationship === "transitive" && finding.firstFixedVersion) {
risk = `Transitive issue. Look for a parent dependency upgrade that pulls in ${finding.firstFixedVersion}+`;
} else if (finding.relationship === "transitive") {
risk = "Transitive issue. Review the parent path and check whether an upstream package can be upgraded.";
} else if (finding.firstFixedVersion) {
risk = `A fixed-version hint exists. Aim for at least ${finding.firstFixedVersion}.`;
} else {
risk = "Review this finding and inspect the dependency path for the safest upgrade path.";
}
if (finding.usage && !finding.usage.imported) {
if (finding.relationship === "transitive") {
risk = risk.replace("Transitive issue.", "Transitive issue. No direct imports found in source code, so practical reachability may be low.");
} else if (finding.relationship === "direct") {
risk = risk.replace("direct dependency.", "direct dependency. No direct imports found in source code, so practical reachability may be low.");
}
}
return risk;
}
export function summarizeNextAction(finding: Finding): string {
if (finding.relationship === "direct" && finding.firstFixedVersion) {
return `Upgrade ${finding.pkg.name} toward ${finding.firstFixedVersion}.`;
}
if (finding.relationship === "direct") {
return `Review and upgrade ${finding.pkg.name} directly in this project.`;
}
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 `No dependency path found for ${finding.pkg.name}. Check your lockfile for which package pulls it in.`;
}
return `Inspect the parent dependency chain for ${finding.pkg.name} and choose the safest available upgrade.`;
}
export function serializeFinding(finding: Finding) {
return {
package: finding.pkg.name,
version: finding.pkg.version,
severity: finding.severity,
relationship: finding.relationship,
firstFixedVersion: finding.firstFixedVersion,
validatedFirstFixedVersion: finding.validatedFirstFixedVersion ?? null,
fixVersionValidationNote: finding.fixVersionValidationNote ?? null,
validatedTargetScannedVersions: finding.validatedTargetScannedVersions ?? null,
validatedTargetKnownVulnerableVersions: finding.validatedTargetKnownVulnerableVersions ?? null,
recommendedAction: getRecommendedAction(finding),
primaryParent: getPrimaryParent(finding),
recommendedParentUpgrade: finding.recommendedParentUpgrade,
cves: finding.cveAliases,
dependencyPaths: finding.dependencyPaths,
usage: finding.usage ?? null,
vulnerabilities: finding.vulnerabilities.map(v => ({
id: v.id,
aliases: v.aliases ?? [],
summary: v.summary ?? "",
severity: inferSeverity(v),
})),
};
}
export function printCacheSummary(cacheDirOverride?: string, options?: { json?: boolean }) {
if (options?.json) return;
const cache = loadCache(cacheDirOverride);
const advisoryCount = Object.entries(cache.entries).filter(([, value]) => Boolean(value)).length;
const emptyCount = Object.entries(cache.entries).filter(([, value]) => value === null).length;
const packageQueryCount = Object.keys(cache.queryEntries).length;
const totalCount = advisoryCount + emptyCount;
if (totalCount === 0 && packageQueryCount === 0) return;
console.log(
chalk.gray(`Cache: ${packageQueryCount} package match record${packageQueryCount === 1 ? "" : "s"}, ${advisoryCount} advisory detail record${advisoryCount === 1 ? "" : "s"}`) +
(emptyCount > 0
? chalk.gray(`, ${emptyCount} empty lookup${emptyCount === 1 ? "" : "s"}`)
: ""),
);
}
export function logInfo(message: string, options?: { json?: boolean }) {
if (options?.json) return;
console.log(chalk.gray(message));
}
export function logWarn(message: string, options?: { json?: boolean }) {
if (options?.json) return;
console.log(chalk.yellow(message));
}
export function sortFindingsForOutput(findings: Finding[]): Finding[] {
return [...findings].sort((a, b) => {
const sevDelta = severityOrder[b.severity] - severityOrder[a.severity];
if (sevDelta !== 0) return sevDelta;
const usageScore = (f: Finding) => f.usage?.imported ? 1 : 0;
const usageDelta = usageScore(b) - usageScore(a);
if (usageDelta !== 0) return usageDelta;
const relScore = (f: Finding) => f.relationship === "direct" ? 1 : 0;
const relDelta = relScore(b) - relScore(a);
if (relDelta !== 0) return relDelta;
return a.pkg.name.localeCompare(b.pkg.name);
});
}