Skip to content

Commit c76aea2

Browse files
committed
feat: add check-advisories to check BRSA fields
Signed-off-by: Piyush Jena <[email protected]>
1 parent 3fc5308 commit c76aea2

File tree

11 files changed

+574
-2
lines changed

11 files changed

+574
-2
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ members = [
2121
"tools/testsys-config",
2222
"tools/unplug",
2323
"tools/update-metadata",
24+
"tools/advisory-parser",
2425

2526
"twoliter",
2627
"twoliter/src/tool-crates/*",
@@ -54,6 +55,7 @@ pre-build = [
5455
]
5556

5657
[workspace.dependencies]
58+
advisory-parser = { version = "0.1", path = "tools/advisory-parser", artifact = [ "bin:advisory-parser" ] }
5759
amispec = { version = "0.1", path = "tools/amispec" }
5860
bottlerocket-types = { version = "0.0.16", git = "https://github.com/bottlerocket-os/bottlerocket-test-system", tag = "v0.0.16" }
5961
bottlerocket-variant = { version = "0.1", path = "tools/bottlerocket-variant" }
@@ -76,6 +78,7 @@ testsys-config = { version = "0.1", path = "tools/testsys-config" }
7678
testsys-model = { version = "0.0.16", git = "https://github.com/bottlerocket-os/bottlerocket-test-system", tag = "v0.0.16" }
7779

7880
twoliter = { version = "0.13.0", path = "twoliter", artifact = [ "bin:twoliter" ] }
81+
twoliter-tool-advisory-parser = { version = "0.1", path = "twoliter/src/tool-crates/advisory-parser" }
7982
twoliter-tool-buildsys = { version = "0.1", path = "twoliter/src/tool-crates/buildsys" }
8083
twoliter-tool-embedded-bundle = { version = "0.1", path = "twoliter/src/tool-crates/embedded-bundle" }
8184
twoliter-tool-pipesys = { version = "0.1", path = "twoliter/src/tool-crates/pipesys" }

tools/advisory-parser/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "advisory-parser"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0 OR MIT"
6+
7+
[dependencies]
8+
regex = "1"
9+
serde = { workspace = true, features = ["derive"] }
10+
snafu.workspace = true
11+
toml.workspace = true
12+
13+
[lints]
14+
workspace = true

tools/advisory-parser/src/main.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
//! Advisory Parser parses Bottlerocket Security Advisory TOML files in bottlerocket kits
2+
//! and outputs the package metadata in a pipe-separated format. This tool is used by rpm2kit
3+
//! to validate the BRSA fields and the corresponding rpm included in the kit is higher than
4+
//! the expected version.
5+
6+
mod models;
7+
8+
use models::Advisory;
9+
use snafu::{ensure, ResultExt};
10+
use std::path::PathBuf;
11+
use std::{env, fs, process};
12+
13+
fn run() -> Result<()> {
14+
let args: Vec<String> = env::args().collect();
15+
ensure!(args.len() == 2, error::UsageSnafu { program: &args[0] });
16+
17+
let path = PathBuf::from(&args[1]);
18+
let content = fs::read_to_string(&path).context(error::ReadFileSnafu { path: &path })?;
19+
let advisory: Advisory =
20+
toml::from_str(&content).context(error::ParseAdvisorySnafu { path: &path })?;
21+
22+
// Output product information in pipe-separated format to be parsed in the script in rpm2kit
23+
for product in advisory.advisory.products {
24+
println!(
25+
"{}|{}|{}",
26+
product.package_name, product.patched_version, product.patched_epoch
27+
);
28+
}
29+
30+
Ok(())
31+
}
32+
33+
fn main() {
34+
if let Err(e) = run() {
35+
eprintln!("{e}");
36+
process::exit(1);
37+
}
38+
}
39+
40+
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
41+
42+
mod error {
43+
use snafu::Snafu;
44+
use std::path::PathBuf;
45+
46+
#[derive(Debug, Snafu)]
47+
#[snafu(visibility(pub(super)))]
48+
pub(super) enum Error {
49+
#[snafu(display("Usage: {} <advisory-file>", program))]
50+
Usage { program: String },
51+
52+
#[snafu(display("Failed to read '{}': {}", path.display(), source))]
53+
ReadFile {
54+
path: PathBuf,
55+
source: std::io::Error,
56+
},
57+
58+
#[snafu(display("Failed to parse advisory '{}': {}", path.display(), source))]
59+
ParseAdvisory {
60+
path: PathBuf,
61+
source: toml::de::Error,
62+
},
63+
}
64+
}
65+
66+
type Result<T> = std::result::Result<T, error::Error>;
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
72+
#[test]
73+
fn test_valid_cve_formats() {
74+
let valid_cves = vec![
75+
"CVE-2024-0001",
76+
"CVE-2024-1234",
77+
"CVE-2024-12345",
78+
"CVE-2025-472688",
79+
"CVE-1999-0001",
80+
];
81+
82+
for cve in valid_cves {
83+
let toml = format!(
84+
r#"
85+
[advisory]
86+
id = "TEST-001"
87+
title = "Test Advisory"
88+
severity = "high"
89+
description = "Test description"
90+
cve = "{}"
91+
[[advisory.products]]
92+
package-name = "test"
93+
patched-version = "1.0"
94+
"#,
95+
cve
96+
);
97+
assert!(
98+
toml::from_str::<Advisory>(&toml).is_ok(),
99+
"Failed to parse valid CVE: {}",
100+
cve
101+
);
102+
}
103+
}
104+
105+
#[test]
106+
fn test_invalid_cve_formats() {
107+
let invalid_cves = vec![
108+
"CVE-2024-001", // Too few digits
109+
"CVE-2024-INVALID", // Non-numeric
110+
"CVE-24-1234", // Year too short
111+
"CVE-2024-0", // Too few digits
112+
"CVE–2024–1234", // non-ascii characters
113+
"CVE—2024—1234", // non-ascii characters
114+
"GHSA-xxxx-xxxx-xxxx", // Wrong format
115+
];
116+
117+
for cve in invalid_cves {
118+
let toml = format!(
119+
r#"
120+
[advisory]
121+
id = "TEST-001"
122+
title = "Test Advisory"
123+
severity = "high"
124+
description = "Test description"
125+
cve = "{}"
126+
[[advisory.products]]
127+
package-name = "test"
128+
patched-version = "1.0"
129+
"#,
130+
cve
131+
);
132+
assert!(
133+
toml::from_str::<Advisory>(&toml).is_err(),
134+
"Should reject invalid CVE: {}",
135+
cve
136+
);
137+
}
138+
}
139+
140+
#[test]
141+
fn test_valid_ghsa_formats() {
142+
let valid_ghsas = vec![
143+
"GHSA-23fg-6c23-wxrv",
144+
"GHSA-2222-3333-4444",
145+
"GHSA-cfgh-jmpq-rvwx",
146+
];
147+
148+
for ghsa in valid_ghsas {
149+
let toml = format!(
150+
r#"
151+
[advisory]
152+
id = "TEST-001"
153+
title = "Test Advisory"
154+
severity = "high"
155+
description = "Test description"
156+
ghsa = "{}"
157+
[[advisory.products]]
158+
package-name = "test"
159+
patched-version = "1.0"
160+
"#,
161+
ghsa
162+
);
163+
assert!(
164+
toml::from_str::<Advisory>(&toml).is_ok(),
165+
"Failed to parse valid GHSA: {}",
166+
ghsa
167+
);
168+
}
169+
}
170+
171+
#[test]
172+
fn test_invalid_ghsa_formats() {
173+
let invalid_ghsas = vec![
174+
"GHSA-xxxx-yyyy-zzzz", // Invalid characters
175+
"GHSA-123-456-789", // Too short
176+
"GHSA-12345-67890-12345", // Too long
177+
"CVE-2024-1234", // Wrong format
178+
];
179+
180+
for ghsa in invalid_ghsas {
181+
let toml = format!(
182+
r#"
183+
[advisory]
184+
id = "TEST-001"
185+
title = "Test Advisory"
186+
severity = "high"
187+
description = "Test description"
188+
ghsa = "{}"
189+
[[advisory.products]]
190+
package-name = "test"
191+
patched-version = "1.0"
192+
"#,
193+
ghsa
194+
);
195+
assert!(
196+
toml::from_str::<Advisory>(&toml).is_err(),
197+
"Should reject invalid GHSA: {}",
198+
ghsa
199+
);
200+
}
201+
}
202+
203+
#[test]
204+
fn test_complete_advisory() {
205+
let toml = r#"
206+
[advisory]
207+
id = "BRSA-test123"
208+
title = "Test Advisory"
209+
cve = "CVE-2025-12345"
210+
ghsa = "GHSA-23fg-6c23-wxrv"
211+
severity = "high"
212+
description = "Test description"
213+
214+
[[advisory.products]]
215+
package-name = "test-package"
216+
patched-version = "1.2.3"
217+
patched-epoch = "1"
218+
219+
[[advisory.products]]
220+
package-name = "another-package"
221+
patched-version = "2.0.0"
222+
"#;
223+
224+
let advisory: Advisory = toml::from_str(toml).expect("Failed to parse complete advisory");
225+
assert_eq!(advisory.advisory.id, "BRSA-test123");
226+
assert_eq!(advisory.advisory.cve, Some("CVE-2025-12345".to_string()));
227+
assert_eq!(
228+
advisory.advisory.ghsa,
229+
Some("GHSA-23fg-6c23-wxrv".to_string())
230+
);
231+
assert_eq!(advisory.advisory.products.len(), 2);
232+
assert_eq!(advisory.advisory.products[0].package_name, "test-package");
233+
assert_eq!(advisory.advisory.products[0].patched_epoch, "1");
234+
assert_eq!(advisory.advisory.products[1].patched_epoch, "0");
235+
}
236+
237+
#[test]
238+
fn test_missing_cve_and_ghsa() {
239+
let toml = r#"
240+
[advisory]
241+
id = "TEST-001"
242+
title = "Test Advisory"
243+
severity = "high"
244+
description = "Test description"
245+
[[advisory.products]]
246+
package-name = "test"
247+
patched-version = "1.0"
248+
"#;
249+
250+
let result = toml::from_str::<Advisory>(toml);
251+
assert!(
252+
result.is_err(),
253+
"Should reject advisory without CVE or GHSA"
254+
);
255+
}
256+
}

0 commit comments

Comments
 (0)