use super::ranges_for_advisory;
use crate::{
advisory::{affected::FunctionPath, Affected, Category, Id, Informational},
repository::git::{GitModificationTimes, GitPath},
Advisory,
};
use serde::Serialize;
use std::ops::Add;
use url::Url;
const ECOSYSTEM: &str = "crates.io";
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(docsrs, doc(cfg(feature = "osv-export")))]
pub struct OsvAdvisory {
id: Id,
modified: String, published: String, #[serde(skip_serializing_if = "Option::is_none")]
withdrawn: Option<String>, aliases: Vec<Id>,
related: Vec<Id>,
summary: String,
details: String,
affected: Vec<OsvAffected>,
references: Vec<OsvReference>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvPackage {
ecosystem: &'static str,
name: String,
purl: String,
}
impl From<&cargo_lock::Name> for OsvPackage {
fn from(package: &cargo_lock::Name) -> Self {
OsvPackage {
ecosystem: ECOSYSTEM,
name: package.to_string(),
purl: "pkg:cargo/".to_string() + package.as_str(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvAffected {
package: OsvPackage,
ecosystem_specific: OsvEcosystemSpecific,
database_specific: OsvDatabaseSpecific,
ranges: Vec<OsvJsonRange>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvJsonRange {
#[serde(rename = "type")]
kind: &'static str,
events: Vec<OsvTimelineEvent>,
}
#[derive(Debug, Clone, Serialize)]
pub enum OsvTimelineEvent {
#[serde(rename = "introduced")]
Introduced(semver::Version),
#[serde(rename = "fixed")]
Fixed(semver::Version),
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvReference {
#[serde(rename = "type")]
kind: OsvReferenceKind,
url: Url,
}
impl From<Url> for OsvReference {
fn from(url: Url) -> Self {
OsvReference {
kind: guess_url_kind(&url),
url,
}
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Serialize)]
pub enum OsvReferenceKind {
ADVISORY,
#[allow(dead_code)]
ARTICLE,
REPORT,
#[allow(dead_code)]
FIX,
PACKAGE,
WEB,
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvEcosystemSpecific {
affects: OsvEcosystemSpecificAffected,
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvEcosystemSpecificAffected {
arch: Vec<platforms::target::Arch>,
os: Vec<platforms::target::OS>,
functions: Vec<FunctionPath>,
}
impl From<Affected> for OsvEcosystemSpecificAffected {
fn from(a: Affected) -> Self {
OsvEcosystemSpecificAffected {
arch: a.arch,
os: a.os,
functions: a.functions.into_iter().map(|(f, _v)| f).collect(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct OsvDatabaseSpecific {
categories: Vec<Category>,
cvss: Option<cvss::v3::Base>,
informational: Option<Informational>,
}
impl OsvAdvisory {
pub fn from_rustsec(
advisory: Advisory,
mod_times: &GitModificationTimes,
path: GitPath<'_>,
) -> Self {
let metadata = advisory.metadata;
let mut reference_urls: Vec<Url> = Vec::new();
let package_url = "https://crates.io/crates/".to_owned() + metadata.package.as_str();
reference_urls.push(Url::parse(&package_url).unwrap());
let advisory_url = format!(
"https://rustsec.org/advisories/{}.html",
metadata.id.as_str()
);
reference_urls.push(Url::parse(&advisory_url).unwrap());
if let Some(url) = metadata.url {
reference_urls.push(url);
}
reference_urls.extend(metadata.references.into_iter());
OsvAdvisory {
id: metadata.id,
modified: git2_time_to_rfc3339(mod_times.for_path(path)),
published: rustsec_date_to_rfc3339(&metadata.date),
affected: vec![OsvAffected {
package: (&metadata.package).into(),
ranges: vec![timeline_for_advisory(&advisory.versions)],
ecosystem_specific: OsvEcosystemSpecific {
affects: advisory.affected.unwrap_or_default().into(),
},
database_specific: OsvDatabaseSpecific {
categories: metadata.categories,
cvss: metadata.cvss,
informational: metadata.informational,
},
}],
withdrawn: metadata.withdrawn.map(|d| rustsec_date_to_rfc3339(&d)),
aliases: metadata.aliases,
related: metadata.related,
summary: metadata.title,
details: metadata.description,
references: osv_references(reference_urls),
}
}
}
fn osv_references(references: Vec<Url>) -> Vec<OsvReference> {
references.into_iter().map(|u| u.into()).collect()
}
fn guess_url_kind(url: &Url) -> OsvReferenceKind {
let str = url.as_str();
if (str.contains("://github.com/") || str.contains("://gitlab.")) && str.contains("/issues/") {
OsvReferenceKind::REPORT
} else if str.contains("/advisories/") || str.contains("://cve.mitre.org/") {
OsvReferenceKind::ADVISORY
} else if str.contains("://crates.io/crates/") {
OsvReferenceKind::PACKAGE
} else {
OsvReferenceKind::WEB
}
}
fn timeline_for_advisory(versions: &crate::advisory::Versions) -> OsvJsonRange {
let ranges = ranges_for_advisory(versions);
assert!(!ranges.is_empty()); let mut timeline = Vec::new();
for range in ranges {
match range.introduced {
Some(ver) => timeline.push(OsvTimelineEvent::Introduced(ver)),
None => timeline.push(OsvTimelineEvent::Introduced(
semver::Version::parse("0.0.0-0").unwrap(),
)),
}
#[allow(clippy::single_match)]
match range.fixed {
Some(ver) => timeline.push(OsvTimelineEvent::Fixed(ver)),
None => (), }
}
OsvJsonRange {
kind: "SEMVER",
events: timeline,
}
}
fn git2_time_to_rfc3339(git_timestamp: &git2::Time) -> String {
let unix_timestamp: u64 = git_timestamp.seconds().try_into().unwrap();
let duration_from_epoch = std::time::Duration::from_secs(unix_timestamp);
humantime::format_rfc3339(std::time::UNIX_EPOCH.add(duration_from_epoch)).to_string()
}
fn rustsec_date_to_rfc3339(d: &crate::advisory::Date) -> String {
format!("{}-{:02}-{:02}T12:00:00Z", d.year(), d.month(), d.day())
}