blob: e5709a3f084fcf89f06c811f4cf2a20fb0c8ad9b [file] [log] [blame]
//! Advisory identifiers
use super::date::{YEAR_MAX, YEAR_MIN};
use crate::error::{Error, ErrorKind};
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
use std::{
fmt::{self, Display},
str::FromStr,
};
/// An identifier for an individual advisory
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Id {
/// An autodetected identifier kind
kind: IdKind,
/// Year this vulnerability was published
year: Option<u32>,
/// The actual string representing the identifier
string: String,
}
impl Id {
/// Placeholder advisory name: shouldn't be used until an ID is assigned
pub const PLACEHOLDER: &'static str = "RUSTSEC-0000-0000";
/// Get a string reference to this advisory ID
pub fn as_str(&self) -> &str {
self.string.as_ref()
}
/// Get the advisory kind for this advisory
pub fn kind(&self) -> IdKind {
self.kind
}
/// Is this advisory ID the `RUSTSEC-0000-0000` placeholder ID?
pub fn is_placeholder(&self) -> bool {
self.string == Self::PLACEHOLDER
}
/// Is this advisory ID a RUSTSEC advisory?
pub fn is_rustsec(&self) -> bool {
self.kind == IdKind::RustSec
}
/// Is this advisory ID a CVE?
pub fn is_cve(&self) -> bool {
self.kind == IdKind::Cve
}
/// Is this advisory ID a GHSA?
pub fn is_ghsa(&self) -> bool {
self.kind == IdKind::Ghsa
}
/// Is this an unknown kind of advisory ID?
pub fn is_other(&self) -> bool {
self.kind == IdKind::Other
}
/// Get the year this vulnerability was published (if known)
pub fn year(&self) -> Option<u32> {
self.year
}
/// Get the numerical part of this advisory (if available).
///
/// This corresponds to the numbers on the right side of the ID.
pub fn numerical_part(&self) -> Option<u32> {
if self.is_placeholder() {
return None;
}
self.string
.split('-')
.last()
.and_then(|s| str::parse(s).ok())
}
/// Get a URL to a web page with more information on this advisory
// TODO(tarcieri): look up GHSA URLs via the GraphQL API?
// <https://developer.github.com/v4/object/securityadvisory/>
pub fn url(&self) -> Option<String> {
match self.kind {
IdKind::RustSec => {
if self.is_placeholder() {
None
} else {
Some(format!("https://rustsec.org/advisories/{}", &self.string))
}
}
IdKind::Cve => Some(format!(
"https://cve.mitre.org/cgi-bin/cvename.cgi?name={}",
&self.string
)),
IdKind::Ghsa => Some(format!("https://github.com/advisories/{}", &self.string)),
IdKind::Talos => Some(format!(
"https://www.talosintelligence.com/reports/{}",
&self.string
)),
_ => None,
}
}
}
impl AsRef<str> for Id {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Default for Id {
fn default() -> Id {
Id {
kind: IdKind::RustSec,
year: None,
string: Id::PLACEHOLDER.into(),
}
}
}
impl Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Id {
type Err = Error;
/// Create an `Id` from the given string
fn from_str(advisory_id: &str) -> Result<Self, Error> {
if advisory_id == Id::PLACEHOLDER {
return Ok(Id::default());
}
let kind = IdKind::detect(advisory_id);
// Ensure known advisory types are well-formed
let year = match kind {
IdKind::RustSec | IdKind::Cve | IdKind::Talos => Some(parse_year(advisory_id)?),
_ => None,
};
Ok(Self {
kind,
year,
string: advisory_id.into(),
})
}
}
impl Serialize for Id {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.string)
}
}
impl<'de> Deserialize<'de> for Id {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
/// Known kinds of advisory IDs
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum IdKind {
/// Our advisory namespace
RustSec,
/// Common Vulnerabilities and Exposures
Cve,
/// GitHub Security Advisory
Ghsa,
/// Cisco Talos identifiers
Talos,
/// Other types of advisory identifiers we don't know about
Other,
}
impl IdKind {
/// Detect the identifier kind for the given string
pub fn detect(string: &str) -> Self {
if string.starts_with("RUSTSEC-") {
IdKind::RustSec
} else if string.starts_with("CVE-") {
IdKind::Cve
} else if string.starts_with("TALOS-") {
IdKind::Talos
} else if string.starts_with("GHSA-") {
IdKind::Ghsa
} else {
IdKind::Other
}
}
}
/// Parse the year from an advisory identifier
fn parse_year(advisory_id: &str) -> Result<u32, Error> {
let mut parts = advisory_id.split('-');
parts.next().unwrap();
let year = match parts.next().unwrap().parse::<u32>() {
Ok(n) => match n {
YEAR_MIN..=YEAR_MAX => n,
_ => fail!(
ErrorKind::Parse,
"out-of-range year in advisory ID: {}",
advisory_id
),
},
_ => fail!(
ErrorKind::Parse,
"malformed year in advisory ID: {}",
advisory_id
),
};
if let Some(num) = parts.next() {
if num.parse::<u32>().is_err() {
fail!(ErrorKind::Parse, "malformed advisory ID: {}", advisory_id);
}
} else {
fail!(ErrorKind::Parse, "incomplete advisory ID: {}", advisory_id);
}
if parts.next().is_some() {
fail!(ErrorKind::Parse, "malformed advisory ID: {}", advisory_id);
}
Ok(year)
}
#[cfg(test)]
mod tests {
use super::{Id, IdKind};
const EXAMPLE_RUSTSEC_ID: &str = "RUSTSEC-2018-0001";
const EXAMPLE_CVE_ID: &str = "CVE-2017-1000168";
const EXAMPLE_GHSA_ID: &str = "GHSA-4mmc-49vf-jmcp";
const EXAMPLE_TALOS_ID: &str = "TALOS-2017-0468";
const EXAMPLE_UNKNOWN_ID: &str = "Anonymous-42";
#[test]
fn rustsec_id_test() {
let rustsec_id = EXAMPLE_RUSTSEC_ID.parse::<Id>().unwrap();
assert!(rustsec_id.is_rustsec());
assert_eq!(rustsec_id.year().unwrap(), 2018);
assert_eq!(
rustsec_id.url().unwrap(),
"https://rustsec.org/advisories/RUSTSEC-2018-0001"
);
assert_eq!(rustsec_id.numerical_part().unwrap(), 1);
}
// The RUSTSEC-0000-0000 ID is a placeholder we need to treat as valid
#[test]
fn rustsec_0000_0000_test() {
let rustsec_id = Id::PLACEHOLDER.parse::<Id>().unwrap();
assert!(rustsec_id.is_rustsec());
assert!(rustsec_id.year().is_none());
assert!(rustsec_id.url().is_none());
assert!(rustsec_id.numerical_part().is_none());
}
#[test]
fn cve_id_test() {
let cve_id = EXAMPLE_CVE_ID.parse::<Id>().unwrap();
assert!(cve_id.is_cve());
assert_eq!(cve_id.year().unwrap(), 2017);
assert_eq!(
cve_id.url().unwrap(),
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-1000168"
);
assert_eq!(cve_id.numerical_part().unwrap(), 1000168);
}
#[test]
fn ghsa_id_test() {
let ghsa_id = EXAMPLE_GHSA_ID.parse::<Id>().unwrap();
assert!(ghsa_id.is_ghsa());
assert!(ghsa_id.year().is_none());
assert_eq!(
ghsa_id.url().unwrap(),
"https://github.com/advisories/GHSA-4mmc-49vf-jmcp"
);
assert!(ghsa_id.numerical_part().is_none());
}
#[test]
fn talos_id_test() {
let talos_id = EXAMPLE_TALOS_ID.parse::<Id>().unwrap();
assert_eq!(talos_id.kind(), IdKind::Talos);
assert_eq!(talos_id.year().unwrap(), 2017);
assert_eq!(
talos_id.url().unwrap(),
"https://www.talosintelligence.com/reports/TALOS-2017-0468"
);
assert_eq!(talos_id.numerical_part().unwrap(), 468);
}
#[test]
fn other_id_test() {
let other_id = EXAMPLE_UNKNOWN_ID.parse::<Id>().unwrap();
assert!(other_id.is_other());
assert!(other_id.year().is_none());
assert!(other_id.url().is_none());
assert_eq!(other_id.numerical_part().unwrap(), 42);
}
}