rustsec/fixer.rs
1//! Automatically attempt to fix vulnerable dependencies
2//!
3//! This module is **experimental**, and its behavior may change in the future.
4
5use crate::vulnerability::Vulnerability;
6use cargo_lock::{Lockfile, Package};
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10/// Auto-fixer for vulnerable dependencies
11#[cfg_attr(docsrs, doc(cfg(feature = "fix")))]
12pub struct Fixer {
13 lockfile: Lockfile,
14 manifest_path: Option<PathBuf>,
15 path_to_cargo: Option<PathBuf>,
16}
17
18impl Fixer {
19 /// Create a new [`Fixer`] for the given `Cargo.lock` file
20 ///
21 /// `path_to_cargo` defaults to `cargo`, resolved in your `$PATH`.
22 ///
23 /// If the path to `Cargo.toml` is not specified, the `cargo update` command
24 /// will be run in the directory with the `Cargo.lock` file.
25 /// Leaving it blank will fix the entire workspace.
26 pub fn new(
27 cargo_lock: Lockfile,
28 cargo_toml: Option<PathBuf>,
29 path_to_cargo: Option<PathBuf>,
30 ) -> Self {
31 Self {
32 lockfile: cargo_lock,
33 manifest_path: cargo_toml,
34 path_to_cargo,
35 }
36 }
37
38 /// Returns a command that calls `cargo update` with the right arguments
39 /// to attempt to fix this vulnerability.
40 ///
41 /// Note that the success of the command does not mean
42 /// the vulnerability was actually fixed!
43 /// It may remain if no semver-compatible fix was available.
44 pub fn get_fix_command(&self, vulnerability: &Vulnerability, dry_run: bool) -> Command {
45 let cargo_path: &Path = self.path_to_cargo.as_deref().unwrap_or(Path::new("cargo"));
46 let pkg_name = &vulnerability.package.name;
47 let mut command = Command::new(cargo_path);
48 command.arg("update");
49 if let Some(path) = self.manifest_path.as_ref() {
50 command.arg("--manifest-path").arg(path);
51 }
52 if dry_run {
53 command.arg("--dry-run");
54 }
55 // there can be more than one version of a given package in the lockfile, so we need to iterate over all of them
56 for pkg in self.lockfile.packages.iter().filter(|pkg| {
57 &pkg.name == pkg_name && vulnerability.versions.is_vulnerable(&pkg.version)
58 }) {
59 let pkgid = pkgid(pkg);
60 command.arg(&pkgid);
61 }
62
63 command
64 }
65}
66
67/// Returns a Cargo unique identifier for a package.
68/// See `cargo help pkgid` for more info.
69///
70/// We need to pass these to `cargo update` because otherwise
71/// the package specification will be ambiguous, and it will refuse to do anything.
72fn pkgid(pkg: &Package) -> String {
73 match pkg.source.as_ref() {
74 Some(source) => format!("{}#{}@{}", source, pkg.name, pkg.version),
75 None => format!("{}@{}", pkg.name, pkg.version),
76 }
77}