1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
pub mod affected;
pub mod category;
pub mod date;
pub mod id;
pub mod informational;
pub mod keyword;
pub mod linter;
pub mod metadata;
pub mod versions;
pub use self::{
affected::Affected, category::Category, date::Date, id::Id, informational::Informational,
keyword::Keyword, linter::Linter, metadata::Metadata, versions::Versions,
};
pub use cvss::Severity;
use crate::error::{Error, ErrorKind};
use serde::{Deserialize, Serialize};
use std::{fs, path::Path, str::FromStr};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Advisory {
#[serde(rename = "advisory")]
pub metadata: Metadata,
pub affected: Option<Affected>,
pub versions: Versions,
}
impl Advisory {
pub fn load_file(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref();
match path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => {
fs::read_to_string(path)
.map_err(|e| {
format_err!(ErrorKind::Io, "couldn't open {}: {}", path.display(), e)
})?
.parse()
}
Some("md") => {
Self::parse_v3(path)
}
_ => fail!(
ErrorKind::Repo,
"unexpected file extension: {}",
path.display()
),
}
}
pub fn parse_v3(path: &Path) -> Result<Self, Error> {
let advisory_data = fs::read_to_string(path)
.map_err(|e| format_err!(ErrorKind::Io, "couldn't open {}: {}", path.display(), e))?;
if !advisory_data.starts_with("```toml") {
fail!(
ErrorKind::Parse,
"unexpected start of V3 advisory: {}",
path.display()
)
}
let toml_end = advisory_data.find("\n```").ok_or_else(|| {
format_err!(
ErrorKind::Parse,
"couldn't find end of TOML front matter in advisory: {}",
path.display()
)
})?;
let front_matter = advisory_data[7..toml_end].trim_start().trim_end();
let mut advisory: Self = toml::from_str(front_matter)?;
if advisory.metadata.title != "" || advisory.metadata.description != "" {
fail!(
ErrorKind::Parse,
"Markdown advisories MUST have empty title/description: {}",
path.display()
)
}
let markdown = advisory_data[(toml_end + 4)..].trim_start();
if !markdown.starts_with("# ") {
fail!(
ErrorKind::Parse,
"Expected # header after TOML front matter in: {}",
path.display()
);
}
let next_newline = markdown.find('\n').ok_or_else(|| {
format_err!(
ErrorKind::Parse,
"no Markdown body (i.e. description) found: {}",
path.display()
)
})?;
advisory.metadata.title = markdown[2..next_newline].trim_end().to_owned();
advisory.metadata.description = markdown[(next_newline + 1)..]
.trim_start()
.trim_end()
.to_owned();
Ok(advisory)
}
pub fn severity(&self) -> Option<Severity> {
self.metadata.cvss.as_ref().map(|cvss| cvss.severity())
}
}
impl FromStr for Advisory {
type Err = Error;
fn from_str(toml_string: &str) -> Result<Self, Error> {
let advisory: Self = toml::from_str(toml_string)?;
if advisory.metadata.title == "" || advisory.metadata.description == "" {
fail!(
ErrorKind::Parse,
"missing title and/or description in advisory:\n\n{}",
toml_string
)
}
Ok(advisory)
}
}