use crate::{
error::{Error, ErrorKind},
lock::acquire_cargo_package_lock,
prelude::*,
};
use rustsec::advisory::{Id, IdKind, Parts};
use rustsec::osv::OsvAdvisory;
use rustsec::{Advisory, Collection};
use std::fs::read_to_string;
use std::iter::FromIterator;
use std::{
fs, iter,
path::{Path, PathBuf},
};
use tame_index::{index::RemoteGitIndex, KrateName};
use toml_edit::{value, Document};
#[allow(dead_code)]
pub struct Synchronizer {
repo_path: PathBuf,
crates_index: RemoteGitIndex,
advisory_db: rustsec::Database,
osv: Vec<OsvAdvisory>,
updated_advisories: usize,
missing_advisories: Vec<OsvAdvisory>,
}
impl Synchronizer {
pub fn new(repo_path: impl Into<PathBuf>, osv_path: impl Into<PathBuf>) -> Result<Self, Error> {
let repo_path = repo_path.into();
let cargo_package_lock = acquire_cargo_package_lock()?;
let mut crates_index = RemoteGitIndex::new(
tame_index::GitIndex::new(tame_index::IndexLocation::new(
tame_index::IndexUrl::CratesIoGit,
))?,
&cargo_package_lock,
)?;
crates_index.fetch(&cargo_package_lock)?;
let advisory_db = rustsec::Database::open(&repo_path)?;
let osv = Self::load_osv_export(&osv_path.into())?;
status_info!(
"Info",
"Loaded {} advisories from {}",
osv.len(),
repo_path.display()
);
Ok(Self {
repo_path,
crates_index,
advisory_db,
osv,
updated_advisories: 0,
missing_advisories: vec![],
})
}
pub fn advisory_db(&self) -> &rustsec::Database {
&self.advisory_db
}
pub fn sync(&mut self) -> Result<(usize, Vec<OsvAdvisory>), Error> {
for osv in self.osv.clone() {
if osv.withdrawn() {
continue;
}
let rustsec_ids_in_osv = osv.rustsec_refs_imported();
let affected_crates = osv.crates();
let rustsec_ids_alias: Vec<Id> = self
.advisory_db
.iter()
.filter_map(|a| {
if a.metadata.aliases.contains(osv.id()) {
Some(a.id().clone())
} else {
None
}
})
.collect();
let mut rs_aliases = rustsec_ids_in_osv.clone();
rs_aliases.extend(rustsec_ids_alias.clone());
rs_aliases.sort();
rs_aliases.dedup();
if rs_aliases.is_empty() {
for c in affected_crates {
let crate_name: KrateName = match c.as_str().try_into() {
Ok(k) => k,
Err(_e) => {
status_info!(
"Info",
"Crate name {} in {} advisory is invalid, skipping",
c,
osv.id(),
);
continue;
}
};
if let Ok(Some(_)) = self.crates_index.krate(
crate_name,
true,
&acquire_cargo_package_lock().unwrap(),
) {
self.missing_advisories.push(osv.clone());
} else {
status_info!(
"Info",
"Unknown crate {} in {} advisory, skipping",
c,
osv.id()
);
continue;
}
}
} else {
for rs_id in rs_aliases {
let rs_advisory = self
.advisory_db
.get(&rs_id)
.expect("Referenced advisory not in rustsec")
.clone();
if !affected_crates
.iter()
.any(|c| c == rs_advisory.metadata.package.as_str())
{
status_info!(
"Info",
"Crate names {:?} in {} advisory not matching existing advisory {}, skipping",
affected_crates,
osv.id(),
rs_advisory.id()
);
continue;
}
self.update_advisory_from_alias(&rs_advisory, &osv)?;
}
}
}
Ok((self.updated_advisories, self.missing_advisories.clone()))
}
fn update_advisory_from_alias(
&mut self,
advisory: &Advisory,
external: &OsvAdvisory,
) -> Result<(), Error> {
let mut missing_aliases = vec![];
let missing_related = vec![];
for external_id in external.aliases().iter().chain(iter::once(external.id())) {
match external_id.kind() {
IdKind::Cve | IdKind::Ghsa => {
if external_id != advisory.id()
&& !advisory.metadata.aliases.contains(external_id)
{
missing_aliases.push(external_id.clone());
status_info!(
"Info",
"Adding missing alias {} for {}",
external_id,
advisory.id()
);
}
}
_ => continue,
}
}
if !missing_aliases.is_empty() || !missing_related.is_empty() {
self.update_aliases(
&self
.repo_path
.join(Collection::Crates.to_string())
.join(advisory.metadata.package.as_str())
.join(format!("{}.md", advisory.id())),
&missing_aliases,
&missing_related,
)?;
}
Ok(())
}
fn update_aliases(
&mut self,
advisory_path: &Path,
missing_aliases: &[Id],
missing_related: &[Id],
) -> Result<(), Error> {
let content = read_to_string(advisory_path)?;
let parts = Parts::parse(&content)?;
let mut metadata = parts
.front_matter
.parse::<Document>()
.expect("invalid TOML front matter");
let mut aliases: Vec<String> = metadata["advisory"]
.get("aliases")
.map(|i| {
i.as_array()
.unwrap()
.into_iter()
.map(|v| v.as_str().unwrap().to_string())
.collect()
})
.unwrap_or_else(Vec::new);
aliases.extend(missing_aliases.iter().map(|a| a.to_string()));
aliases.sort();
aliases.dedup();
if !aliases.is_empty() {
metadata["advisory"]["aliases"] = value(toml_edit::Array::from_iter(aliases.iter()));
}
let mut related: Vec<String> = metadata["advisory"]
.get("related")
.map(|i| {
i.as_array()
.unwrap()
.into_iter()
.map(|v| v.as_str().unwrap().to_string())
.collect()
})
.unwrap_or_else(Vec::new);
related.extend(missing_related.iter().map(|a| a.to_string()));
related.sort();
related.dedup();
if !related.is_empty() {
metadata["advisory"]["related"] = value(toml_edit::Array::from_iter(related.iter()));
}
let updated = format!("```toml\n{}```\n\n{}", metadata, parts.markdown);
fs::write(advisory_path, updated)?;
status_info!("Info", "Written {}", advisory_path.display());
self.updated_advisories += 1;
Ok(())
}
fn load_osv_file(path: impl AsRef<Path>) -> Result<OsvAdvisory, Error> {
let path = path.as_ref();
let advisory_data = read_to_string(path)
.map_err(|e| format_err!(ErrorKind::Io, "couldn't open {}: {}", path.display(), e))?;
let advisory: OsvAdvisory = serde_json::from_str(&advisory_data).map_err(|e| {
format_err!(ErrorKind::Parse, "error parsing {}: {}", path.display(), e)
})?;
Ok(advisory)
}
fn load_osv_export(path: &Path) -> Result<Vec<OsvAdvisory>, Error> {
let mut result = vec![];
for advisory_entry in fs::read_dir(path).unwrap() {
let advisory_path = advisory_entry.unwrap().path();
if advisory_path.extension() != Some("json".as_ref()) {
continue;
}
if advisory_path.to_string_lossy().contains("RUSTSEC-") {
continue;
}
let advisory = Self::load_osv_file(advisory_path)?;
result.push(advisory)
}
Ok(result)
}
}