rustsec_admin/
osv_export.rs

1//! Backend for the `osv` subcommand.
2
3use std::path::{Path, PathBuf};
4
5use rustsec::{
6    advisory::Informational,
7    fs,
8    osv::OsvAdvisory,
9    repository::git::{GitModificationTimes, GitPath, Repository},
10    Advisory, Collection,
11};
12
13use crate::{
14    error::{Error, ErrorKind},
15    prelude::*,
16};
17
18/// Lists all versions for a crate and prints info on which ones are affected
19pub struct OsvExporter {
20    /// Loaded git repository
21    repository: Repository,
22
23    /// Loaded modification times for files in Git
24    mod_times: GitModificationTimes,
25}
26
27impl OsvExporter {
28    /// Load the the database at the given path
29    pub fn new(repo_path: Option<&Path>) -> Result<Self, Error> {
30        let repository = match repo_path {
31            Some(path) => Repository::open(path)?,
32            None => Repository::fetch_default_repo()?,
33        };
34        let mod_times = GitModificationTimes::new(&repository)?;
35        Ok(Self {
36            repository,
37            mod_times,
38        })
39    }
40
41    /// Exports all advisories to OSV JSON format to the specified directory.
42    pub fn export_all(&self, destination_folder: &Path) -> Result<(), Error> {
43        let repo_path = self.repository.path();
44        let collection_path = repo_path.join(Collection::Crates.as_str());
45        let mut found_at_least_one_advisory = false;
46
47        if let Ok(collection_entry) = fs::read_dir(collection_path) {
48            for dir_entry in collection_entry {
49                for advisory_entry in fs::read_dir(dir_entry?.path())? {
50                    found_at_least_one_advisory = true;
51
52                    // Load the RustSec advisory
53                    let advisory_path = advisory_entry?.path();
54                    let advisory = Advisory::load_file(&advisory_path)?;
55                    let id = advisory.id().clone();
56
57                    if let Some(kind) = &advisory.metadata.informational {
58                        match kind {
59                            // If not `Unmaintained` or `Unsound` or `Notice`, don't export it to OSV
60                            // to make the output format stable.
61                            // Adding new types should be accompanied by a version bump.
62                            Informational::Unmaintained => (),
63                            Informational::Unsound => (),
64                            Informational::Notice => (),
65                            _ => continue,
66                        }
67                    }
68
69                    // Transform the advisory to OSV format
70                    // We've been simply pushing things to the end of the path, so in theory
71                    // it *should* reverse cleanly, hence the `.unwrap()`
72                    let relative_path = advisory_path.strip_prefix(repo_path).unwrap();
73                    let gitpath = GitPath::new(&self.repository, relative_path)?;
74                    let osv = OsvAdvisory::from_rustsec(advisory, &self.mod_times, gitpath);
75
76                    // Serialize the OSV advisory to JSON and write it to file
77                    let mut output_path: PathBuf = destination_folder.join(id.as_str());
78                    output_path.set_extension("json");
79                    let output_file = fs::File::create(output_path)?;
80                    let writer = std::io::BufWriter::new(output_file);
81                    serde_json::to_writer_pretty(writer, &osv)
82                        .map_err(|err| format_err!(ErrorKind::Io, "{}", err))?
83                }
84            }
85        }
86        if found_at_least_one_advisory {
87            Ok(())
88        } else {
89            Err(format_err!(
90                ErrorKind::Io,
91                format!("Could not find any advisories in {:?}", repo_path)
92            )
93            .into())
94        }
95    }
96}