UNPKG

8.04 kBPlain TextView Raw
1import chalk from "chalk";
2import * as os from "os";
3
4import { createFromJSON, DepGraph } from "@snyk/dep-graph";
5import {
6 BaseImageRemediationAdvice,
7 ContainerTarget,
8 Issue,
9 Options,
10 ScanResult,
11 TestResult,
12} from "./types";
13
14const BREAK_LINE = os.EOL;
15const SECTION_PADDING_TO_FORMAT_METADATA = 19;
16
17export async function display(
18 scanResults: ScanResult[],
19 testResults: TestResult[],
20 errors: string[],
21 options?: Options,
22): Promise<string> {
23 const result: string[] = [];
24 let index = 0;
25 for (const testResult of testResults) {
26 const formattedIssue: string[] = [];
27 for (const issue of testResult.issues) {
28 formattedIssue.push(formatIssue(testResult, issue));
29 }
30 result.push(formattedIssue.join(BREAK_LINE));
31 result.push(includeSectionSeparator());
32
33 const scanResult: ScanResult = scanResults[index];
34 const metadata = formatMetadataSection(scanResult, testResult);
35 result.push(metadata);
36 result.push(includeSectionSeparator());
37
38 const summary = formatSummary(testResult);
39 result.push(summary);
40 result.push(includeSectionSeparator());
41
42 const remediations = formatRemediations(testResult);
43 if (remediations) {
44 result.push(remediations);
45 result.push(includeSectionSeparator());
46 }
47
48 const suggestions = formatSuggestions(options);
49 if (suggestions) {
50 result.push(suggestions);
51 result.push(includeSectionSeparator());
52 }
53
54 const userCTA = formatUserCTA(options);
55 if (userCTA) {
56 result.push(userCTA);
57 }
58
59 index += 1;
60 }
61
62 return result.join(BREAK_LINE);
63}
64
65function includeSectionSeparator(): string {
66 return BREAK_LINE;
67}
68
69function formatIssue(testResult: TestResult, issue: Issue): string {
70 const result: string[] = [];
71 const issueData = testResult.issuesData[issue.issueId];
72 const severity = capitalize(issueData.severity);
73 const pkg = issue.pkgName;
74 const color = getColor(issueData.severity);
75
76 const header = color(`✗ ${severity} severity vulnerability found in ${pkg}`);
77 const description = ` Description: ${issueData.title}`;
78 const info = ` Info: https://snyk.io/vuln/${issue.issueId}`;
79 const introduced = ` Introduced through: ${formatIntroduced(
80 issueData.from,
81 )}`;
82 const from = formatFrom(issueData.from);
83 const fixedIn = formatFixedIn(issue);
84
85 result.push(header);
86 result.push(description);
87 result.push(info);
88 result.push(introduced);
89 result.push(from);
90 if (fixedIn) {
91 result.push(fixedIn);
92 }
93 result.push("");
94
95 return result.join(BREAK_LINE);
96}
97
98function capitalize(word: string): string {
99 return word[0].toUpperCase() + word.slice(1);
100}
101
102function formatIntroduced(fromList: string[][]) {
103 const result: string[] = [];
104
105 for (const from of fromList) {
106 result.push(from[0]);
107 }
108
109 return result.join(", ");
110}
111function formatFrom(fromList: string[][]): string {
112 const result: string[] = [];
113 let counter = 0;
114 const max = 3;
115 for (const localFrom of fromList) {
116 if (counter >= max) {
117 break;
118 }
119 counter += 1;
120
121 result.push(` From: ${localFrom.join(" > ")}`);
122 }
123 if (fromList.length > max) {
124 result.push(` and ${(fromList.length = max)} more...`);
125 }
126
127 return result.join(BREAK_LINE);
128}
129
130function formatFixedIn(issue: Issue): string | undefined {
131 if (!issue.fixInfo || !issue.fixInfo.nearestFixedInVersion) {
132 return undefined;
133 }
134
135 return chalk.bold.green(` Fixed in: ${issue.fixInfo.nearestFixedInVersion}`);
136}
137
138function formatMetadataSection(
139 scanResult: ScanResult,
140 testResult: TestResult,
141): string {
142 const result: string[] = [];
143 result.push(formatMetadataLine("Organization:", testResult.org));
144
145 const packageManager = scanResult.identity.type;
146 result.push(formatMetadataLine("Package manager:", packageManager));
147
148 const target: ContainerTarget = scanResult.target as ContainerTarget;
149 const projectName = target.image;
150 const image = target.image.replace("docker-image|", "");
151 result.push(formatMetadataLine("Project name:", projectName));
152 result.push(formatMetadataLine("Docker image:", image));
153 if (testResult.docker && testResult.docker.baseImage) {
154 result.push(formatMetadataLine("Base image:", testResult.docker.baseImage));
155 }
156 if (testResult.licensesPolicy) {
157 result.push(formatMetadataLine("Licenses:", chalk.green("enabled")));
158 }
159 const platform = scanResult.identity.args?.platform;
160 if (platform) {
161 result.push(formatMetadataLine("Platform:", platform));
162 }
163
164 return result.join(BREAK_LINE);
165}
166
167function formatMetadataLine(header: string, info: string = ""): string {
168 return `${chalk.green(
169 padding(header, SECTION_PADDING_TO_FORMAT_METADATA),
170 )} ${info}`;
171}
172
173function formatSummary(testResult: TestResult): string {
174 const depGraph: DepGraph = createFromJSON(testResult.depGraphData);
175 const pkgCount = depGraph?.getDepPkgs()?.length || 0;
176 const pathOrDepsText = `${pkgCount} dependencies`;
177 const testedInfoText = `Tested ${pathOrDepsText} for known issues`;
178 const vulnPathsText = formatVulnSummaryText(testResult.issues);
179 let summaryText = `${testedInfoText}, ${vulnPathsText}`;
180 if (testResult.issues.length === 0) {
181 summaryText = chalk.green(`✓ ${summaryText}`);
182 }
183
184 return summaryText;
185}
186
187function formatVulnSummaryText(issues: Issue[]): string {
188 if (issues.length > 0) {
189 return chalk.bold.red(`found ${issues.length} issues.`);
190 }
191
192 return "no vulnerable paths found.";
193}
194
195function getColor(severity: string): (text: string) => string {
196 let color: (text: string) => string;
197 switch (severity) {
198 case "low":
199 color = chalk.bold.blue;
200 break;
201 case "medium":
202 color = chalk.bold.yellow;
203 break;
204 case "high":
205 color = chalk.bold.red;
206 break;
207 default:
208 color = chalk.whiteBright;
209 break;
210 }
211
212 return color;
213}
214
215export function formatRemediations(res: TestResult) {
216 if (!res.docker || !res.docker.baseImageRemediation) {
217 return "";
218 }
219 const { advice, message } = res.docker.baseImageRemediation;
220 const out = [] as any[];
221
222 if (advice) {
223 for (const item of advice) {
224 out.push(formatString(item)(item.message));
225 }
226 } else if (message) {
227 out.push(message);
228 } else {
229 return "";
230 }
231 return `${out.join(BREAK_LINE)}`;
232}
233
234function formatString({ color, bold }: BaseImageRemediationAdvice) {
235 let formatter = chalk;
236 if (color && formatter[color]) {
237 formatter = formatter[color];
238 }
239 if (bold) {
240 formatter = formatter.bold;
241 }
242 return formatter;
243}
244
245function formatSuggestions(options): string {
246 if (options.isDockerUser) {
247 return "";
248 }
249
250 const dockerSuggestion: string[] = [];
251 if (options.config && options.config.disableSuggestions !== "true") {
252 const optOutSuggestions =
253 "To remove this message in the future, please run `snyk config set disableSuggestions=true`";
254 if (!options.file) {
255 dockerSuggestion.push(
256 chalk.bold.white(
257 "Pro tip: use `--file` option to get base image remediation advice.",
258 ),
259 );
260 dockerSuggestion.push(
261 chalk.bold.white(
262 `Example: $ snyk container test ${options.path} --file=path/to/Dockerfile`,
263 ),
264 );
265 dockerSuggestion.push(BREAK_LINE);
266 dockerSuggestion.push(optOutSuggestions);
267 } else if (!options["exclude-base-image-vulns"]) {
268 dockerSuggestion.push(
269 chalk.bold.white(
270 "Pro tip: use `--exclude-base-image-vulns` to exclude from display Docker base image vulnerabilities.",
271 ),
272 );
273 dockerSuggestion.push(BREAK_LINE);
274 dockerSuggestion.push(optOutSuggestions);
275 }
276 }
277 return dockerSuggestion.join(BREAK_LINE);
278}
279
280function formatUserCTA(options): string {
281 if (options.isDockerUser) {
282 return "For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp";
283 }
284 return "";
285}
286
287function padding(s: string, padding: number) {
288 const padLength = padding - s.length;
289 if (padLength <= 0) {
290 return s;
291 }
292
293 return s + " ".repeat(padLength);
294}