1use crate::{
4 config::{DenyOption, OutputConfig, OutputFormat},
5 prelude::*,
6};
7use abscissa_core::terminal::{
8 self,
9 Color::{self, Red, Yellow},
10};
11use rustsec::{
12 advisory::License,
13 cargo_lock::{
14 dependency::{self, graph::EdgeDirection, Dependency},
15 Lockfile, Package,
16 },
17 WarningKind,
18};
19use std::{collections::BTreeSet as Set, io, path::Path};
20use std::{io::Write as _, string::ToString as _};
21
22#[cfg(feature = "binary-scanning")]
23use rustsec::binary_scanning::BinaryReport;
24
25#[derive(Clone, Debug)]
27pub struct Presenter {
28 displayed_packages: Set<Dependency>,
31
32 deny_warning_kinds: Set<WarningKind>,
34
35 config: OutputConfig,
37}
38
39impl Presenter {
40 pub fn new(config: &OutputConfig) -> Self {
42 Self {
43 displayed_packages: Set::new(),
44 deny_warning_kinds: config
45 .deny
46 .iter()
47 .flat_map(|k| k.get_warning_kind())
48 .copied()
49 .collect(),
50 config: config.clone(),
51 }
52 }
53
54 pub fn before_report(&mut self, path: &Path, lockfile: &Lockfile) {
56 if !self.config.is_quiet() {
57 status_ok!(
58 "Scanning",
59 "{} for vulnerabilities ({} crate dependencies)",
60 path.display(),
61 lockfile.packages.len(),
62 );
63 }
64 }
65
66 #[cfg(feature = "binary-scanning")]
67 pub fn binary_scan_report(&mut self, report: &BinaryReport, path: &Path) {
69 use rustsec::binary_scanning::BinaryReport::*;
70 if !self.config.is_quiet() {
71 match report {
72 Complete(lockfile) => status_ok!(
73 "Found",
74 "'cargo auditable' data in {} ({} dependencies)",
75 path.display(),
76 lockfile.packages.len()
77 ),
78 Incomplete(lockfile) => {
79 status_warn!(
80 "{} was not built with 'cargo auditable', the report will be incomplete ({} dependencies recovered)",
81 path.display(), lockfile.packages.len());
82 }
83 None => status_err!(
84 "No dependency information found in {}! Is it a Rust program built with cargo?",
85 path.display(),
86 ),
87 }
88 }
89 }
90
91 fn warning_word(&self, count: u64) -> &str {
92 if count != 1 {
93 "warnings"
94 } else {
95 "warning"
96 }
97 }
98
99 pub fn print_report(
101 &mut self,
102 report: &rustsec::Report,
103 lockfile: &Lockfile,
104 path: Option<&Path>,
105 ) {
106 if self.config.format == OutputFormat::Json {
107 serde_json::to_writer(io::stdout(), &report).unwrap();
108 io::stdout().flush().unwrap();
109 return;
110 }
111
112 let tree = lockfile
113 .dependency_tree()
114 .expect("invalid Cargo.lock dependency tree");
115
116 for vulnerability in &report.vulnerabilities.list {
120 self.print_vulnerability(vulnerability, &tree);
121 }
122
123 for warnings in report.warnings.values() {
124 for warning in warnings.iter() {
125 self.print_warning(warning, &tree)
126 }
127 }
128
129 if report.vulnerabilities.found {
130 if report.vulnerabilities.count == 1 {
131 match path {
132 Some(path) => status_err!("1 vulnerability found in {}", path.display()),
133 None => status_err!("1 vulnerability found!"),
134 }
135 } else {
136 match path {
137 Some(path) => status_err!(
138 "{} vulnerabilities found in {}",
139 report.vulnerabilities.count,
140 path.display()
141 ),
142 None => status_err!("{} vulnerabilities found!", report.vulnerabilities.count),
143 }
144 }
145 }
146
147 let (num_denied, num_not_denied) = self.count_warnings(report);
148
149 if num_denied > 0 || num_not_denied > 0 {
150 if num_denied > 0 {
151 match path {
152 Some(path) => status_err!(
153 "{} denied {} found in {}",
154 num_denied,
155 self.warning_word(num_denied),
156 path.display(),
157 ),
158 None => status_err!(
159 "{} denied {} found!",
160 num_denied,
161 self.warning_word(num_denied)
162 ),
163 }
164 }
165 if num_not_denied > 0 {
166 match path {
167 Some(path) => status_warn!(
168 "{} allowed {} found in {}",
169 num_not_denied,
170 self.warning_word(num_not_denied),
171 path.display(),
172 ),
173 None => status_warn!(
174 "{} allowed {} found",
175 num_not_denied,
176 self.warning_word(num_not_denied)
177 ),
178 }
179 }
180 }
181 }
182
183 pub fn print_self_report(&mut self, self_advisories: &[rustsec::Advisory]) {
185 if self_advisories.is_empty() {
186 return;
187 }
188 let msg = "This copy of cargo-audit has known advisories! Upgrade cargo-audit to the \
190 latest version: cargo install --force cargo-audit --locked";
191
192 if self.config.deny.contains(&DenyOption::Warnings) {
193 status_err!(msg);
194 } else {
195 status_warn!(msg);
196 }
197
198 for advisory in self_advisories {
199 self.print_metadata(
200 &advisory.metadata,
201 self.warning_color(self.config.deny.contains(&DenyOption::Warnings)),
202 );
203 }
204 println!();
205 }
206
207 #[must_use]
210 pub fn should_exit_with_failure(&self, report: &rustsec::Report) -> bool {
211 if report.vulnerabilities.found {
212 return true;
213 }
214 let (denied, _allowed) = self.count_warnings(report);
215 if denied != 0 {
216 return true;
217 }
218 false
219 }
220
221 #[must_use]
224 pub fn should_exit_with_failure_due_to_self(
225 &self,
226 self_advisories: &[rustsec::Advisory],
227 ) -> bool {
228 !self_advisories.is_empty() && self.config.deny.contains(&DenyOption::Warnings)
229 }
230
231 fn count_warnings(&self, report: &rustsec::Report) -> (u64, u64) {
234 let mut num_denied: u64 = 0;
235 let mut num_not_denied: u64 = 0;
236
237 for (kind, warnings) in report.warnings.iter() {
238 if self.deny_warning_kinds.contains(kind) {
239 num_denied += warnings.len() as u64;
240 } else {
241 num_not_denied += warnings.len() as u64;
242 }
243 }
244 (num_denied, num_not_denied)
245 }
246
247 fn print_vulnerability(
249 &mut self,
250 vulnerability: &rustsec::Vulnerability,
251 tree: &dependency::Tree,
252 ) {
253 self.print_attr(Red, "Crate: ", &vulnerability.package.name);
254 self.print_attr(Red, "Version: ", vulnerability.package.version.to_string());
255 self.print_metadata(&vulnerability.advisory, Red);
256
257 if vulnerability.versions.patched().is_empty() {
258 self.print_attr(Red, "Solution: ", "No fixed upgrade is available!");
259 } else {
260 self.print_attr(
261 Red,
262 "Solution: ",
263 format!(
264 "Upgrade to {}",
265 vulnerability
266 .versions
267 .patched()
268 .iter()
269 .map(ToString::to_string)
270 .collect::<Vec<_>>()
271 .as_slice()
272 .join(" OR ")
273 ),
274 );
275 }
276
277 self.print_tree(Red, &vulnerability.package, tree);
278 println!();
279 }
280
281 fn print_warning(&mut self, warning: &rustsec::Warning, tree: &dependency::Tree) {
283 let color = self.warning_color(self.deny_warning_kinds.contains(&warning.kind));
284
285 self.print_attr(color, "Crate: ", &warning.package.name);
286 self.print_attr(color, "Version: ", warning.package.version.to_string());
287 self.print_attr(color, "Warning: ", warning.kind.as_str());
288
289 if let Some(metadata) = &warning.advisory {
290 self.print_metadata(metadata, color)
291 }
292
293 self.print_tree(color, &warning.package, tree);
294 println!();
295 }
296
297 fn warning_color(&self, deny_warning: bool) -> Color {
299 if deny_warning {
300 Red
301 } else {
302 Yellow
303 }
304 }
305
306 fn print_metadata(&self, metadata: &rustsec::advisory::Metadata, color: Color) {
308 self.print_attr(color, "Title: ", &metadata.title);
309 self.print_attr(color, "Date: ", &metadata.date);
310 self.print_attr(color, "ID: ", &metadata.id);
311
312 if metadata.license == License::CcBy40 {
313 if let Some(url) = &metadata.url {
315 self.print_attr(color, "URL: ", url);
316 } else if let Some(url) = &metadata.id.url() {
317 self.print_attr(color, "URL: ", url);
318 }
319 } else {
320 if let Some(url) = &metadata.id.url() {
323 self.print_attr(color, "URL: ", url);
324 } else if let Some(url) = &metadata.url {
325 self.print_attr(color, "URL: ", url);
326 }
327 }
328
329 if let Some(cvss) = &metadata.cvss {
330 self.print_attr(
331 color,
332 "Severity: ",
333 format!("{} ({})", cvss.score().value(), cvss.score().severity()),
334 );
335 }
336 }
337
338 fn print_attr(&self, color: Color, attr: &str, content: impl AsRef<str>) {
340 terminal::status::Status::new()
341 .bold()
342 .color(color)
343 .status(attr)
344 .print_stdout(content.as_ref())
345 .unwrap();
346 }
347
348 fn print_tree(&mut self, color: Color, package: &Package, tree: &dependency::Tree) {
350 if !self.displayed_packages.insert(Dependency::from(package)) {
352 return;
353 }
354
355 if !self.config.show_tree.unwrap_or(true) {
356 return;
357 }
358
359 terminal::status::Status::new()
360 .bold()
361 .color(color)
362 .status("Dependency tree:\n")
363 .print_stdout("")
364 .unwrap();
365
366 let package_node = tree.nodes()[&Dependency::from(package)];
367 tree.render(
368 &mut io::stdout(),
369 package_node,
370 EdgeDirection::Incoming,
371 false,
372 )
373 .unwrap();
374 }
375}