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