Skip to main content

rustsec/advisory/
id.rs

1//! Advisory identifiers
2
3use super::date::{YEAR_MAX, YEAR_MIN};
4use crate::error::{Error, ErrorKind};
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
6use std::{
7    fmt::{self, Display},
8    str::FromStr,
9};
10
11/// An identifier for an individual advisory
12#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
13pub struct Id {
14    /// An autodetected identifier kind
15    kind: IdKind,
16
17    /// Year this vulnerability was published
18    year: Option<u32>,
19
20    /// The actual string representing the identifier
21    string: String,
22}
23
24impl Id {
25    /// Placeholder advisory name: shouldn't be used until an ID is assigned
26    pub const PLACEHOLDER: &'static str = "RUSTSEC-0000-0000";
27
28    /// Get a string reference to this advisory ID
29    pub fn as_str(&self) -> &str {
30        self.string.as_ref()
31    }
32
33    /// Get the advisory kind for this advisory
34    pub fn kind(&self) -> IdKind {
35        self.kind
36    }
37
38    /// Is this advisory ID the `RUSTSEC-0000-0000` placeholder ID?
39    pub fn is_placeholder(&self) -> bool {
40        self.string == Self::PLACEHOLDER
41    }
42
43    /// Is this advisory ID a RUSTSEC advisory?
44    pub fn is_rustsec(&self) -> bool {
45        self.kind == IdKind::RustSec
46    }
47
48    /// Is this advisory ID a CVE?
49    pub fn is_cve(&self) -> bool {
50        self.kind == IdKind::Cve
51    }
52
53    /// Is this advisory ID a GHSA?
54    pub fn is_ghsa(&self) -> bool {
55        self.kind == IdKind::Ghsa
56    }
57    /// Is this advisory ID a TALOS advisory?
58    pub fn is_talos(&self) -> bool {
59        self.kind == IdKind::Talos
60    }
61
62    /// Is this an unknown kind of advisory ID?
63    pub fn is_other(&self) -> bool {
64        self.kind == IdKind::Other
65    }
66
67    /// Get the year this vulnerability was published (if known)
68    pub fn year(&self) -> Option<u32> {
69        self.year
70    }
71
72    /// Get the numerical part of this advisory (if available).
73    ///
74    /// This corresponds to the numbers on the right side of the ID.
75    pub fn numerical_part(&self) -> Option<u32> {
76        if self.is_placeholder() {
77            return None;
78        }
79
80        self.string
81            .split('-')
82            .next_back()
83            .and_then(|s| str::parse(s).ok())
84    }
85
86    /// Get a URL to a web page with more information on this advisory
87    // TODO(tarcieri): look up GHSA URLs via the GraphQL API?
88    // <https://developer.github.com/v4/object/securityadvisory/>
89    pub fn url(&self) -> Option<String> {
90        match self.kind {
91            IdKind::RustSec => {
92                if self.is_placeholder() {
93                    None
94                } else {
95                    Some(format!("https://rustsec.org/advisories/{}", &self.string))
96                }
97            }
98            IdKind::Cve => Some(format!(
99                "https://cve.mitre.org/cgi-bin/cvename.cgi?name={}",
100                &self.string
101            )),
102            IdKind::Ghsa => Some(format!("https://github.com/advisories/{}", &self.string)),
103            IdKind::Talos => Some(format!(
104                "https://www.talosintelligence.com/reports/{}",
105                &self.string
106            )),
107            _ => None,
108        }
109    }
110}
111
112impl AsRef<str> for Id {
113    fn as_ref(&self) -> &str {
114        self.as_str()
115    }
116}
117
118impl Default for Id {
119    fn default() -> Id {
120        Id {
121            kind: IdKind::RustSec,
122            year: None,
123            string: Id::PLACEHOLDER.into(),
124        }
125    }
126}
127
128impl Display for Id {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str(self.as_str())
131    }
132}
133
134impl FromStr for Id {
135    type Err = Error;
136
137    /// Create an `Id` from the given string
138    fn from_str(advisory_id: &str) -> Result<Self, Error> {
139        if advisory_id == Id::PLACEHOLDER {
140            return Ok(Id::default());
141        }
142
143        let kind = IdKind::detect(advisory_id);
144
145        // Ensure known advisory types are well-formed
146        let year = match kind {
147            IdKind::RustSec | IdKind::Cve | IdKind::Talos => Some(parse_year(advisory_id)?),
148            _ => None,
149        };
150
151        Ok(Self {
152            kind,
153            year,
154            string: advisory_id.into(),
155        })
156    }
157}
158
159impl Serialize for Id {
160    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
161        serializer.serialize_str(&self.string)
162    }
163}
164
165impl<'de> Deserialize<'de> for Id {
166    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
167        Self::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom)
168    }
169}
170
171/// Known kinds of advisory IDs
172#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
173#[non_exhaustive]
174pub enum IdKind {
175    /// Our advisory namespace
176    RustSec,
177
178    /// Common Vulnerabilities and Exposures
179    Cve,
180
181    /// GitHub Security Advisory
182    Ghsa,
183
184    /// Cisco Talos identifiers
185    Talos,
186
187    /// Other types of advisory identifiers we don't know about
188    Other,
189}
190
191impl IdKind {
192    /// Detect the identifier kind for the given string
193    pub fn detect(string: &str) -> Self {
194        if string.starts_with("RUSTSEC-") {
195            IdKind::RustSec
196        } else if string.starts_with("CVE-") {
197            IdKind::Cve
198        } else if string.starts_with("TALOS-") {
199            IdKind::Talos
200        } else if string.starts_with("GHSA-") {
201            IdKind::Ghsa
202        } else {
203            IdKind::Other
204        }
205    }
206}
207
208/// Parse the year from an advisory identifier
209fn parse_year(advisory_id: &str) -> Result<u32, Error> {
210    let mut parts = advisory_id.split('-');
211    parts.next().unwrap();
212
213    let year = match parts.next().unwrap().parse::<u32>() {
214        Ok(n) => match n {
215            YEAR_MIN..=YEAR_MAX => n,
216            _ => fail!(
217                ErrorKind::Parse,
218                "out-of-range year in advisory ID: {}",
219                advisory_id
220            ),
221        },
222        _ => fail!(
223            ErrorKind::Parse,
224            "malformed year in advisory ID: {}",
225            advisory_id
226        ),
227    };
228
229    if let Some(num) = parts.next() {
230        if num.parse::<u32>().is_err() {
231            fail!(ErrorKind::Parse, "malformed advisory ID: {}", advisory_id);
232        }
233    } else {
234        fail!(ErrorKind::Parse, "incomplete advisory ID: {}", advisory_id);
235    }
236
237    if parts.next().is_some() {
238        fail!(ErrorKind::Parse, "malformed advisory ID: {}", advisory_id);
239    }
240
241    Ok(year)
242}
243
244#[cfg(test)]
245mod tests {
246    use super::{Id, IdKind};
247
248    const EXAMPLE_RUSTSEC_ID: &str = "RUSTSEC-2018-0001";
249    const EXAMPLE_CVE_ID: &str = "CVE-2017-1000168";
250    const EXAMPLE_GHSA_ID: &str = "GHSA-4mmc-49vf-jmcp";
251    const EXAMPLE_TALOS_ID: &str = "TALOS-2017-0468";
252    const EXAMPLE_UNKNOWN_ID: &str = "Anonymous-42";
253
254    #[test]
255    fn rustsec_id_test() {
256        let rustsec_id = EXAMPLE_RUSTSEC_ID.parse::<Id>().unwrap();
257        assert!(rustsec_id.is_rustsec());
258        assert_eq!(rustsec_id.year().unwrap(), 2018);
259        assert_eq!(
260            rustsec_id.url().unwrap(),
261            "https://rustsec.org/advisories/RUSTSEC-2018-0001"
262        );
263        assert_eq!(rustsec_id.numerical_part().unwrap(), 1);
264    }
265
266    // The RUSTSEC-0000-0000 ID is a placeholder we need to treat as valid
267    #[test]
268    fn rustsec_0000_0000_test() {
269        let rustsec_id = Id::PLACEHOLDER.parse::<Id>().unwrap();
270        assert!(rustsec_id.is_rustsec());
271        assert!(rustsec_id.year().is_none());
272        assert!(rustsec_id.url().is_none());
273        assert!(rustsec_id.numerical_part().is_none());
274    }
275
276    #[test]
277    fn cve_id_test() {
278        let cve_id = EXAMPLE_CVE_ID.parse::<Id>().unwrap();
279        assert!(cve_id.is_cve());
280        assert_eq!(cve_id.year().unwrap(), 2017);
281        assert_eq!(
282            cve_id.url().unwrap(),
283            "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-1000168"
284        );
285        assert_eq!(cve_id.numerical_part().unwrap(), 1000168);
286    }
287
288    #[test]
289    fn ghsa_id_test() {
290        let ghsa_id = EXAMPLE_GHSA_ID.parse::<Id>().unwrap();
291        assert!(ghsa_id.is_ghsa());
292        assert!(ghsa_id.year().is_none());
293        assert_eq!(
294            ghsa_id.url().unwrap(),
295            "https://github.com/advisories/GHSA-4mmc-49vf-jmcp"
296        );
297        assert!(ghsa_id.numerical_part().is_none());
298    }
299
300    #[test]
301    fn talos_id_test() {
302        let talos_id = EXAMPLE_TALOS_ID.parse::<Id>().unwrap();
303        assert_eq!(talos_id.kind(), IdKind::Talos);
304        assert_eq!(talos_id.year().unwrap(), 2017);
305        assert_eq!(
306            talos_id.url().unwrap(),
307            "https://www.talosintelligence.com/reports/TALOS-2017-0468"
308        );
309        assert_eq!(talos_id.numerical_part().unwrap(), 468);
310    }
311
312    #[test]
313    fn other_id_test() {
314        let other_id = EXAMPLE_UNKNOWN_ID.parse::<Id>().unwrap();
315        assert!(other_id.is_other());
316        assert!(other_id.year().is_none());
317        assert!(other_id.url().is_none());
318        assert_eq!(other_id.numerical_part().unwrap(), 42);
319    }
320}