cargo-audit-build 0.3.2

Audit cargo build.rs files
Documentation
//! Audit build.rs files in a dependency tree.
#![doc = include_str!("../README.md")]
use anyhow::{Result, bail};
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;

use cargo_audit_build::{
    BuildTrustStore, JSON_CACHE, PackageMetadata,
    cargo::{fetch_dependencies, find_build_rs, package_metadata},
    git::{add_file, commit_file, init_repo, is_clean},
    repository,
};

fn prompt(msg: &str) -> Result<String> {
    print!("{}", msg);
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    Ok(input.trim().to_string())
}

fn prompt_bool(msg: &str) -> Result<bool> {
    let res = prompt(msg)?;
    Ok(matches!(res.trim().to_lowercase().as_str(), "y" | "yes"))
}

fn commit_reviewed_build_script(
    repo_root: &Path,
    build_script: &Path,
    pkg: &PackageMetadata,
) -> Result<()> {
    let cache = repo_root.join(format!("{}@{}.rs", pkg.name, pkg.version));
    let mut input = File::open(build_script)?;
    let mut output = File::create(&cache)?;
    std::io::copy(&mut input, &mut output)?;

    let commit_msg = format!("audit-build: add build.rs for {}@{}", pkg.name, pkg.version,);
    add_file(repo_root, &cache)?;
    if !is_clean(repo_root)? {
        commit_file(repo_root, &cache, &commit_msg)?;
    }
    Ok(())
}

fn read_trust_store(repo_root: &Path) -> Result<BuildTrustStore> {
    let path = repo_root.join(JSON_CACHE);
    log::debug!("read trust store: {}", path.display());
    let file = File::open(&path)?;
    Ok(serde_json::from_reader::<_, BuildTrustStore>(file)?)
}

fn save_trust_store(repo_root: &Path, ts: &BuildTrustStore) -> Result<()> {
    let path = repo_root.join(JSON_CACHE);
    log::debug!("save trust store: {}", path.display());
    let file = File::create(&path)?;
    serde_json::to_writer::<_, BuildTrustStore>(file, ts)?;
    let commit_msg = "audit-build: update trust store";
    add_file(repo_root, &path)?;
    commit_file(repo_root, &path, commit_msg)?;
    Ok(())
}

fn audit_build_rs(
    repo_root: &Path,
    build_scripts: Vec<&PackageMetadata>,
    editor: &str,
) -> Result<(u32, BuildTrustStore)> {
    log::debug!("start audit: {}", repo_root.display());
    let mut num_changes = 0;
    if !repo_root.join(".git").exists() {
        log::debug!("init repository: {}", repo_root.display());
        init_repo(repo_root)?;
    }
    let mut trust_store = read_trust_store(repo_root)?;
    for pkg in build_scripts {
        let pkg_id = format!("{}@{}", pkg.name, pkg.version);
        let build_script = pkg.build_script();
        log::debug!("build script for {} is {}", pkg_id, build_script.display());
        let checksum = Sha256::digest(&std::fs::read(&build_script)?);
        let digest = format!("{:0x}", &checksum);

        let is_trusted = trust_store.0.get(&digest).map(|(v, _)| *v).unwrap_or(false);
        if is_trusted {
            let entry = trust_store.0.entry(digest).or_default();
            if !entry.1.contains(&pkg_id) {
                entry.1.insert(pkg_id.clone());
                num_changes += 1;
            }
            log::info!("build.rs for {} is already trusted, skipping", pkg_id);
            continue;
        }

        let mut ps = Command::new(editor)
            .arg(build_script.to_string_lossy().as_ref())
            .spawn()?;
        let status = ps.wait()?;
        if !status.success() {
            bail!(
                "the EDITOR ({}) exited with code {}",
                editor,
                status.code().unwrap_or(i32::MIN)
            );
        } else {
            let msg = format!("Do you trust the build.rs file in {}? [Y/n] ", &pkg_id);
            let trusted = prompt_bool(&msg)?;
            commit_reviewed_build_script(repo_root, &build_script, pkg)?;

            let entry = trust_store.0.entry(digest).or_default();
            entry.0 = trusted;
            entry.1.insert(pkg_id.clone());
            if trusted != is_trusted {
                num_changes += 1;
            }
        }
    }
    Ok((num_changes, trust_store))
}

fn run() -> Result<()> {
    let Ok(editor) = std::env::var("EDITOR") else {
        bail!("the EDITOR environment variable must be set");
    };

    let repo_root = crate::repository()?;
    std::fs::create_dir_all(&repo_root)?;

    fetch_dependencies(&repo_root)?;
    let meta = package_metadata(&repo_root)?;
    let build_scripts = find_build_rs(&meta);
    let (num_changes, trust_store) = audit_build_rs(&repo_root, build_scripts, &editor)?;
    if num_changes > 0 {
        save_trust_store(&repo_root, &trust_store)?;
    }
    Ok(())
}

fn main() {
    use env_logger::Env;
    env_logger::Builder::from_env(Env::default().default_filter_or("info"))
        .format_timestamp(None)
        .format_target(false)
        .init();
    if let Err(e) = run() {
        log::error!("{}", e);
        std::process::exit(1);
    }
}