1use crate::check::Outcome;
26use crate::pushrefs::PushRef;
27
28use super::common;
29
30#[derive(Debug, PartialEq, Eq)]
32enum Report {
33 Clean,
34 Advisories(Vec<String>),
37 Vulnerabilities(Vec<String>),
39 CouldNotCheck,
41}
42
43fn releasing(refs: &[PushRef]) -> bool {
47 refs.iter().any(|r| {
48 r.remote_ref
49 .strip_prefix("refs/tags/")
50 .and_then(|t| t.strip_prefix('v'))
51 .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit()))
52 })
53}
54
55fn conclude(
59 settings: &crate::config::Settings,
60 tool: &str,
61 report: Report,
62 releasing: bool,
63 full: &str,
64) -> Outcome {
65 match report {
66 Report::Clean => {
67 common::ok(settings, &format!("{tool}: no known vulnerabilities"));
68 Outcome::Passed
69 }
70 Report::Advisories(ids) => {
71 common::warn(&format!(
72 "{tool}: advisories against the dependency tree (warnings — unmaintained/unsound): {}",
73 ids.join(", ")
74 ));
75 Outcome::Warned
76 }
77 Report::Vulnerabilities(what) => {
78 if releasing {
79 for line in full.lines() {
80 crate::say!("{line}");
81 }
82 common::fail(&format!(
83 "{tool}: known vulnerabilities in the dependency tree — a v* tag \
84 does not ship with these: {}",
85 what.join(", ")
86 ));
87 Outcome::Failed
88 } else {
89 common::warn(&format!(
90 "{tool}: known vulnerabilities in the dependency tree ({}) — \
91 this will BLOCK a v* tag push",
92 what.join(", ")
93 ));
94 Outcome::Warned
95 }
96 }
97 Report::CouldNotCheck => {
98 common::warn(&format!(
99 "{tool} could not complete — the dependency tree was NOT checked. \
100 This is not a clean result."
101 ));
102 Outcome::Unavailable
103 }
104 }
105}
106
107fn is_advisory_id(w: &str) -> bool {
109 w.len() == 17
110 && w.starts_with("RUSTSEC-")
111 && w[8..12].bytes().all(|b| b.is_ascii_digit())
112 && w.as_bytes()[12] == b'-'
113 && w[13..17].bytes().all(|b| b.is_ascii_digit())
114}
115
116fn ids_with_crates(out: &str) -> Vec<(String, Option<String>)> {
127 let mut pairs: Vec<(String, Option<String>)> = Vec::new();
128 let mut pending: Option<String> = None;
129 for line in out.lines() {
130 let t = line.trim();
131 if let Some(rest) = t.strip_prefix("Crate:") {
132 pending = rest.split_whitespace().next().map(str::to_string);
133 continue;
134 }
135 if let Some(rest) = t.strip_prefix("ID:") {
136 if let Some(id) = rest.split_whitespace().next().filter(|w| is_advisory_id(w)) {
137 pairs.push((id.to_string(), pending.take()));
138 continue;
139 }
140 }
141 for w in t.split_whitespace().filter(|w| is_advisory_id(w)) {
144 pairs.push((w.to_string(), None));
145 }
146 }
147 pairs
148}
149
150fn local_dependents(tree_out: &str) -> Vec<String> {
162 let mut names: Vec<String> = tree_out
163 .lines()
164 .filter(|l| l.contains(" (/"))
165 .filter_map(|l| {
166 l.split_whitespace()
167 .find(|w| !w.is_empty() && w.chars().next().is_some_and(|c| c.is_alphanumeric()))
168 .map(str::to_string)
169 })
170 .collect();
171 names.sort();
172 names.dedup();
173 names
174}
175
176fn describe(id: &str, krate: Option<&str>, reaches: &[String]) -> String {
179 match (krate, reaches.is_empty()) {
180 (None, _) => id.to_string(),
181 (Some(k), true) => format!("{id} ({k}, not in the build graph)"),
182 (Some(k), false) => format!("{id} ({k} → {})", reaches.join(", ")),
183 }
184}
185
186fn read_cargo_audit(exit_ok: bool, out: &str) -> Report {
189 let mut ids: Vec<String> = out
190 .split_whitespace()
191 .filter(|w| {
192 w.len() == 17
193 && w.starts_with("RUSTSEC-")
194 && w[8..12].bytes().all(|b| b.is_ascii_digit())
195 && w.as_bytes()[12] == b'-'
196 && w[13..17].bytes().all(|b| b.is_ascii_digit())
197 })
198 .map(|w| w.to_string())
199 .collect();
200 ids.sort();
201 ids.dedup();
202 match (ids.is_empty(), exit_ok) {
203 (true, true) => Report::Clean,
204 (true, false) => Report::CouldNotCheck,
205 (false, true) => Report::Advisories(ids),
206 (false, false) => Report::Vulnerabilities(ids),
207 }
208}
209
210fn read_npm_audit(exit_ok: bool, out: &str) -> Report {
217 let summary = out.lines().rev().map(str::trim).find(|l| {
218 l.contains("vulnerabilit")
219 && (l.starts_with("found ")
220 || l.split_whitespace()
221 .next()
222 .is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())))
223 });
224 match summary {
225 Some(l) if l.starts_with("found 0 ") || l.starts_with("0 ") => Report::Clean,
226 Some(l) => Report::Vulnerabilities(vec![l.to_string()]),
227 None if exit_ok => Report::Clean,
228 None => Report::CouldNotCheck,
229 }
230}
231
232fn read_govulncheck(exit_ok: bool, out: &str) -> Report {
239 let mut ids: Vec<String> = out
240 .split_whitespace()
241 .map(|w| w.trim_matches(|c: char| !c.is_ascii_alphanumeric() && c != '-'))
242 .filter(|w| {
243 w.len() >= 12
244 && w.starts_with("GO-")
245 && w[3..7].bytes().all(|b| b.is_ascii_digit())
246 && w.as_bytes()[7] == b'-'
247 && w[8..].bytes().all(|b| b.is_ascii_digit())
248 })
249 .map(|w| w.to_string())
250 .collect();
251 ids.sort();
252 ids.dedup();
253 match (ids.is_empty(), exit_ok) {
254 (true, true) => Report::Clean,
255 (true, false) => Report::CouldNotCheck,
256 (false, true) => Report::Advisories(ids),
257 (false, false) => Report::Vulnerabilities(ids),
258 }
259}
260
261fn read_pip_audit(exit_ok: bool, out: &str) -> Report {
263 if out.contains("No known vulnerabilities found") {
264 return Report::Clean;
265 }
266 if let Some(line) = out
267 .lines()
268 .map(str::trim)
269 .find(|l| l.starts_with("Found ") && l.contains("known vulnerabilit"))
270 {
271 return Report::Vulnerabilities(vec![line.to_string()]);
272 }
273 if exit_ok {
274 Report::Clean
275 } else {
276 Report::CouldNotCheck
277 }
278}
279
280fn venv_site_packages(root: &str) -> Option<String> {
289 let candidates = std::env::var_os("VIRTUAL_ENV")
290 .map(std::path::PathBuf::from)
291 .into_iter()
292 .chain(std::iter::once(std::path::Path::new(root).join(".venv")));
293 for venv in candidates {
294 let windows = venv.join("Lib").join("site-packages");
295 if windows.is_dir() {
296 return Some(windows.to_string_lossy().into_owned());
297 }
298 let Ok(entries) = std::fs::read_dir(venv.join("lib")) else {
299 continue;
300 };
301 for e in entries.flatten() {
302 if !e.file_name().to_string_lossy().starts_with("python") {
303 continue;
304 }
305 let sp = e.path().join("site-packages");
306 if sp.is_dir() {
307 return Some(sp.to_string_lossy().into_owned());
308 }
309 }
310 }
311 None
312}
313
314fn audited(settings: &crate::config::Settings, argv: &[String]) -> Option<(bool, String)> {
316 audited_in(settings, argv, std::path::Path::new(&common::repo_root()))
317}
318
319fn audited_in(
321 settings: &crate::config::Settings,
322 argv: &[String],
323 dir: &std::path::Path,
324) -> Option<(bool, String)> {
325 let mut cmd = std::process::Command::new(&argv[0]);
326 cmd.args(&argv[1..])
327 .current_dir(dir)
328 .stdin(std::process::Stdio::null());
329 common::strip_git_env(&mut cmd);
330 let (ran, out) = common::capture_within(settings, &mut cmd)?;
331 match ran {
332 common::Ran::Status(s) => Some((s.success(), out)),
333 common::Ran::TimedOut(budget) => {
334 common::say_timed_out(&argv[0], budget);
335 None
336 }
337 }
338}
339
340pub fn rust(settings: &crate::config::Settings, refs: &[PushRef]) -> Outcome {
341 if !has_lockfile("Cargo.lock") {
342 return Outcome::Inert;
343 }
344 if common::which("cargo-audit").is_none() {
345 common::warn(
346 "audit-rust: cargo-audit is not installed (cargo install cargo-audit) — \
347 the audit did NOT run",
348 );
349 return Outcome::Unavailable;
350 }
351 let argv = vec![
352 common::program("cargo"),
353 "audit".into(),
354 "--color".into(),
355 "never".into(),
356 ];
357 let Some((exit_ok, out)) = audited(settings, &argv) else {
358 return Outcome::Unavailable;
359 };
360 conclude(
361 settings,
362 "audit-rust",
363 attribute(read_cargo_audit(exit_ok, &out), &out, |krate| {
364 cargo_tree_inverse(settings, krate)
365 }),
366 releasing(refs),
367 &out,
368 )
369}
370
371fn cargo_tree_inverse(settings: &crate::config::Settings, krate: &str) -> Option<String> {
375 let argv = vec![
376 common::program("cargo"),
377 "tree".into(),
378 "--invert".into(),
379 krate.into(),
380 "--edges".into(),
381 "normal".into(),
382 "--color".into(),
383 "never".into(),
384 ];
385 audited(settings, &argv).map(|(_, out)| out)
386}
387
388fn attribute(report: Report, out: &str, tree: impl Fn(&str) -> Option<String>) -> Report {
396 match report {
397 Report::Advisories(ids) => Report::Advisories(described(ids, out, tree)),
398 Report::Vulnerabilities(ids) => Report::Vulnerabilities(described(ids, out, tree)),
399 other => other,
402 }
403}
404
405fn described(ids: Vec<String>, out: &str, tree: impl Fn(&str) -> Option<String>) -> Vec<String> {
409 let pairs = ids_with_crates(out);
410 let mut seen: Vec<(String, Vec<String>)> = Vec::new();
411 ids.iter()
412 .map(|id| {
413 let krate = pairs
414 .iter()
415 .find(|(pid, k)| pid == id && k.is_some())
416 .and_then(|(_, k)| k.clone());
417 let Some(k) = krate else {
418 return id.clone();
419 };
420 if let Some((_, reaches)) = seen.iter().find(|(name, _)| *name == k) {
421 return describe(id, Some(&k), reaches);
422 }
423 let reaches = tree(&k).map(|t| local_dependents(&t)).unwrap_or_default();
424 seen.push((k.clone(), reaches.clone()));
425 describe(id, Some(&k), &reaches)
426 })
427 .collect()
428}
429
430fn has_lockfile(lockfile: &str) -> bool {
437 crate::tracked_paths()
438 .iter()
439 .any(|p| p.rsplit('/').next().unwrap_or(p) == lockfile)
440}
441
442fn lockfile_dirs(lockfile: &str) -> Vec<String> {
447 let mut dirs: Vec<String> = crate::tracked_paths()
448 .iter()
449 .filter(|p| p.rsplit('/').next().unwrap_or(p) == lockfile)
450 .map(|p| {
451 p.rsplit_once('/')
452 .map(|(d, _)| d.to_string())
453 .unwrap_or_default()
454 })
455 .collect();
456 dirs.sort();
457 dirs.dedup();
458 dirs
459}
460
461pub fn js(settings: &crate::config::Settings, refs: &[PushRef]) -> Outcome {
471 let dirs = lockfile_dirs("package-lock.json");
472 if dirs.is_empty() {
473 return Outcome::Inert;
474 }
475 let root = std::path::PathBuf::from(common::repo_root());
476 let argv = vec![common::program("npm"), "audit".into()];
477 let mut found = Vec::new();
478 let mut unchecked = Vec::new();
479 let mut full = String::new();
480 for dir in &dirs {
481 let label = if dir.is_empty() {
482 ".".to_string()
483 } else {
484 dir.clone()
485 };
486 let Some((exit_ok, out)) = audited_in(settings, &argv, &root.join(dir)) else {
487 common::warn(&format!(
488 "audit-js: npm could not run in {label} — the audit did NOT run"
489 ));
490 return Outcome::Unavailable;
491 };
492 match read_npm_audit(exit_ok, &out) {
493 Report::Vulnerabilities(what) => {
494 found.extend(what.into_iter().map(|w| format!("{label}: {w}")));
495 full.push_str(&format!("── npm audit in {label}\n{out}\n"));
496 }
497 Report::CouldNotCheck => unchecked.push(label),
498 Report::Clean | Report::Advisories(_) => {}
499 }
500 }
501 if !unchecked.is_empty() {
502 common::warn(&format!(
503 "audit-js: npm audit could not answer in {} — those projects were NOT checked",
504 unchecked.join(", ")
505 ));
506 }
507 let report = if !found.is_empty() {
508 Report::Vulnerabilities(found)
509 } else if !unchecked.is_empty() {
510 Report::CouldNotCheck
511 } else {
512 Report::Clean
513 };
514 conclude(settings, "audit-js", report, releasing(refs), &full)
515}
516
517pub fn go(settings: &crate::config::Settings, refs: &[PushRef]) -> Outcome {
518 if !has_lockfile("go.sum") {
519 return Outcome::Inert;
520 }
521 if common::which("govulncheck").is_none() {
522 common::warn(
523 "audit-go: govulncheck is not installed \
524 (go install golang.org/x/vuln/cmd/govulncheck@latest) — the audit did NOT run",
525 );
526 return Outcome::Unavailable;
527 }
528 let argv = vec![common::program("govulncheck"), "./...".into()];
529 let Some((exit_ok, out)) = audited(settings, &argv) else {
530 return Outcome::Unavailable;
531 };
532 conclude(
533 settings,
534 "audit-go",
535 read_govulncheck(exit_ok, &out),
536 releasing(refs),
537 &out,
538 )
539}
540
541pub fn python(settings: &crate::config::Settings, refs: &[PushRef]) -> Outcome {
542 if !has_lockfile("requirements.txt") && !has_lockfile("pyproject.toml") {
543 return Outcome::Inert;
544 }
545 if common::which("pip-audit").is_none() {
546 common::warn(
547 "audit-python: pip-audit is not installed (pip install pip-audit) — \
548 the audit did NOT run",
549 );
550 return Outcome::Unavailable;
551 }
552 let root = common::repo_root();
553 let argv = if std::path::Path::new(&root)
554 .join("requirements.txt")
555 .exists()
556 {
557 vec![
558 common::program("pip-audit"),
559 "-r".into(),
560 "requirements.txt".into(),
561 ]
562 } else if let Some(site_packages) = venv_site_packages(&root) {
563 vec![
571 common::program("pip-audit"),
572 "--path".into(),
573 site_packages,
574 "--skip-editable".into(),
577 ]
578 } else {
579 common::warn(
580 "audit-python: no requirements.txt, and no virtualenv to audit \
581 (looked at $VIRTUAL_ENV and .venv) — the audit did NOT run",
582 );
583 return Outcome::Unavailable;
584 };
585 let Some((exit_ok, out)) = audited(settings, &argv) else {
586 return Outcome::Unavailable;
587 };
588 conclude(
589 settings,
590 "audit-python",
591 read_pip_audit(exit_ok, &out),
592 releasing(refs),
593 &out,
594 )
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
607 fn a_uv_project_is_audited_through_its_venv() {
608 let root = std::env::temp_dir().join(format!("audit-venv-{}", std::process::id()));
609 let _ = std::fs::remove_dir_all(&root);
610
611 std::fs::create_dir_all(&root).unwrap();
614 assert_eq!(venv_site_packages(root.to_str().unwrap()), None);
615
616 let sp = root
623 .join(".venv")
624 .join("lib")
625 .join("python3.13")
626 .join("site-packages");
627 std::fs::create_dir_all(&sp).unwrap();
628 assert_eq!(
629 venv_site_packages(root.to_str().unwrap()),
630 Some(sp.to_string_lossy().into_owned())
631 );
632
633 let win = std::env::temp_dir().join(format!("audit-venv-win-{}", std::process::id()));
635 let _ = std::fs::remove_dir_all(&win);
636 let wsp = win.join(".venv").join("Lib").join("site-packages");
637 std::fs::create_dir_all(&wsp).unwrap();
638 assert_eq!(
639 venv_site_packages(win.to_str().unwrap()),
640 Some(wsp.to_string_lossy().into_owned())
641 );
642
643 let _ = std::fs::remove_dir_all(&root);
644 let _ = std::fs::remove_dir_all(&win);
645 }
646
647 fn tag(name: &str) -> PushRef {
648 PushRef {
649 local_ref: name.to_string(),
650 local_oid: "a".repeat(40),
651 remote_ref: name.to_string(),
652 remote_oid: "0".repeat(40),
653 }
654 }
655
656 #[test]
659 fn a_release_is_a_v_number_tag() {
660 assert!(releasing(&[tag("refs/tags/v1.6.6")]));
661 assert!(releasing(&[tag("refs/tags/v2")]));
662 assert!(!releasing(&[tag("refs/tags/vendor-drop")]));
663 assert!(!releasing(&[tag("refs/tags/release")]));
664 assert!(!releasing(&[tag("refs/heads/v1-styles")]));
665 assert!(!releasing(&[tag("refs/heads/main")]));
666 assert!(releasing(&[tag("refs/heads/main"), tag("refs/tags/v1.0")]));
668 }
669
670 #[test]
673 fn cargo_audit_ids_decide_not_the_exit_code() {
674 assert_eq!(
675 read_cargo_audit(true, "ok, 312 crates checked"),
676 Report::Clean
677 );
678 assert_eq!(
679 read_cargo_audit(false, "error: couldn't fetch advisory database"),
680 Report::CouldNotCheck
681 );
682 let warn = "warning: unmaintained RUSTSEC-2024-0436 paste";
683 assert_eq!(
684 read_cargo_audit(true, warn),
685 Report::Advisories(vec!["RUSTSEC-2024-0436".into()])
686 );
687 let vuln = "Crate: foo\nID: RUSTSEC-2025-0001\nerror: 1 vulnerability found\nRUSTSEC-2025-0001 again";
688 assert_eq!(
689 read_cargo_audit(false, vuln),
690 Report::Vulnerabilities(vec!["RUSTSEC-2025-0001".into()])
691 );
692 assert_eq!(read_cargo_audit(true, "RUSTSEC-20XX-0001"), Report::Clean);
694 }
695
696 fn real_report() -> String {
699 [
700 "Crate: paste",
701 "Version: 1.0.15",
702 "Warning: unmaintained",
703 "ID: RUSTSEC-2024-0436",
704 "URL: https://rustsec.org/advisories/RUSTSEC-2024-0436",
705 "",
706 "Crate: lru",
707 "Version: 0.12.5",
708 "Warning: unsound",
709 "ID: RUSTSEC-2026-0253",
710 "",
711 "Crate: lru",
712 "Version: 0.12.5",
713 "ID: RUSTSEC-2026-0002",
714 "",
715 "warning: 3 allowed warnings found",
716 ]
717 .join("\n")
718 }
719
720 #[test]
721 fn an_advisory_is_paired_with_its_crate() {
722 let pairs = ids_with_crates(&real_report());
723 assert_eq!(
724 pairs,
725 vec![
726 ("RUSTSEC-2024-0436".into(), Some("paste".into())),
727 ("RUSTSEC-2026-0253".into(), Some("lru".into())),
728 ("RUSTSEC-2026-0002".into(), Some("lru".into())),
729 ]
730 );
731 }
732
733 #[test]
736 fn a_loose_id_borrows_no_crate() {
737 let out = "Crate: paste\nID: RUSTSEC-2024-0436\nsee also RUSTSEC-2025-0001";
738 assert_eq!(
739 ids_with_crates(out),
740 vec![
741 ("RUSTSEC-2024-0436".into(), Some("paste".into())),
742 ("RUSTSEC-2025-0001".into(), None),
743 ]
744 );
745 }
746
747 #[test]
750 fn only_local_crates_are_named_as_reached() {
751 let tree =
752 "lru v0.12.5\n└── ratatui v0.29.0\n └── amont-fleet v1.32.0 (/w/crates/amont-fleet)";
753 assert_eq!(local_dependents(tree), vec!["amont-fleet".to_string()]);
754 assert!(local_dependents("lru v0.12.5\n└── ratatui v0.29.0").is_empty());
755 }
756
757 #[test]
761 fn the_warning_names_the_crate_and_what_reaches_it() {
762 let calls = std::cell::RefCell::new(Vec::new());
763 let fake = |k: &str| {
764 calls.borrow_mut().push(k.to_string());
765 Some(format!(
766 "{k} v1\n└── ratatui v0.29.0\n └── amont-fleet v1.32.0 (/w/crates/amont-fleet)"
767 ))
768 };
769 let out = real_report();
770 let got = attribute(read_cargo_audit(true, &out), &out, fake);
771 assert_eq!(
772 got,
773 Report::Advisories(vec![
774 "RUSTSEC-2024-0436 (paste → amont-fleet)".into(),
775 "RUSTSEC-2026-0002 (lru → amont-fleet)".into(),
776 "RUSTSEC-2026-0253 (lru → amont-fleet)".into(),
777 ])
778 );
779 assert_eq!(
780 calls.into_inner(),
781 vec!["paste", "lru"],
782 "one call per crate"
783 );
784 }
785
786 #[test]
790 fn a_crate_outside_the_build_graph_says_so() {
791 let out = "Crate: wezterm-input-types\nID: RUSTSEC-2025-0001";
792 let got = attribute(read_cargo_audit(true, out), out, |_| {
793 Some("warning: nothing to print.".into())
794 });
795 assert_eq!(
796 got,
797 Report::Advisories(vec![
798 "RUSTSEC-2025-0001 (wezterm-input-types, not in the build graph)".into()
799 ])
800 );
801 }
802
803 #[test]
806 fn attribution_never_changes_the_verdict() {
807 let spawned = std::cell::Cell::new(false);
808 let clean = attribute(read_cargo_audit(true, "0 vulnerabilities"), "", |_| {
809 spawned.set(true);
810 None
811 });
812 assert_eq!(clean, Report::Clean);
813 assert!(!spawned.get(), "a clean report must spawn nothing");
814
815 let out = "Crate: paste\nID: RUSTSEC-2024-0436";
816 let blind = attribute(read_cargo_audit(false, out), out, |_| None);
817 assert_eq!(
818 blind,
819 Report::Vulnerabilities(vec![
820 "RUSTSEC-2024-0436 (paste, not in the build graph)".into()
821 ])
822 );
823 }
824
825 #[test]
826 fn npm_audit_summary_decides() {
827 assert_eq!(
828 read_npm_audit(true, "found 0 vulnerabilities\n"),
829 Report::Clean
830 );
831 assert_eq!(
832 read_npm_audit(false, "found 3 vulnerabilities (1 moderate, 2 high)\n"),
833 Report::Vulnerabilities(vec!["found 3 vulnerabilities (1 moderate, 2 high)".into()])
834 );
835 assert_eq!(
838 read_npm_audit(
839 false,
840 "# npm audit report\n\nvite 6.0.0 - 6.1.5\nSeverity: high\n\n\
841 17 vulnerabilities (8 moderate, 9 high)\n\nTo address all issues, run:\n npm audit fix\n"
842 ),
843 Report::Vulnerabilities(vec!["17 vulnerabilities (8 moderate, 9 high)".into()])
844 );
845 assert_eq!(
846 read_npm_audit(false, "1 vulnerability (1 high)\n"),
847 Report::Vulnerabilities(vec!["1 vulnerability (1 high)".into()])
848 );
849 assert_eq!(
850 read_npm_audit(true, "up to date, audited 100 packages\n"),
851 Report::Clean
852 );
853 assert_eq!(
854 read_npm_audit(false, "npm ERR! network ENOTFOUND\n"),
855 Report::CouldNotCheck
856 );
857 }
858
859 #[test]
862 fn govulncheck_ids_decide_not_the_exit_code() {
863 assert_eq!(
864 read_govulncheck(true, "No vulnerabilities found.\n"),
865 Report::Clean
866 );
867 assert_eq!(
868 read_govulncheck(false, "vulncheck: fetching vulnerability database: dial tcp: lookup vuln.go.dev: no such host\n"),
869 Report::CouldNotCheck
870 );
871 assert_eq!(
874 read_govulncheck(
875 true,
876 "=== Informational ===\nVulnerability #1: GO-2023-1840\n More info: https://pkg.go.dev/vuln/GO-2023-1840\n"
877 ),
878 Report::Advisories(vec!["GO-2023-1840".into()])
879 );
880 assert_eq!(
881 read_govulncheck(
882 false,
883 "Vulnerability #1: GO-2022-0969\n Your code calls it.\nGO-2022-0969 again\n"
884 ),
885 Report::Vulnerabilities(vec!["GO-2022-0969".into()])
886 );
887 assert_eq!(
889 read_govulncheck(true, "GO-20XX-0001 GO-2023-1"),
890 Report::Clean
891 );
892 }
893
894 #[test]
895 fn pip_audit_sentence_decides() {
896 assert_eq!(
897 read_pip_audit(true, "No known vulnerabilities found\n"),
898 Report::Clean
899 );
900 assert_eq!(
901 read_pip_audit(
902 false,
903 "Found 2 known vulnerabilities in 1 package\nrequests 2.0 PYSEC-2023-74\n"
904 ),
905 Report::Vulnerabilities(vec!["Found 2 known vulnerabilities in 1 package".into()])
906 );
907 assert_eq!(
908 read_pip_audit(false, "ERROR: could not resolve\n"),
909 Report::CouldNotCheck
910 );
911 }
912}