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