Skip to content

Commit f064191

Browse files
committed
feat: add check-advisories to check BRSA fields
Signed-off-by: Piyush Jena <jepiyush@amazon.com>
1 parent 3fc5308 commit f064191

File tree

10 files changed

+465
-0
lines changed

10 files changed

+465
-0
lines changed

Cargo.lock

Lines changed: 83 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
lazy_static.workspace = true
9+
test-case.workspace = true
10+
nutype = { version = "0.6.2", features = ["regex", "serde"] }
11+
regex.workspace = true
12+
serde = { workspace = true, features = ["derive"] }
13+
snafu.workspace = true
14+
toml.workspace = true
15+
16+
[lints]
17+
workspace = true

tools/advisory-parser/src/main.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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+
16+
// Enforce the requirement of 2 arguments because the binary expects the BRSA file to parse.
17+
ensure!(args.len() == 2, error::UsageSnafu { program: &args[0] });
18+
19+
let path = PathBuf::from(&args[1]);
20+
let content = fs::read_to_string(&path).context(error::ReadFileSnafu { path: &path })?;
21+
let advisory: Advisory =
22+
toml::from_str(&content).context(error::ParseAdvisorySnafu { path: &path })?;
23+
24+
// Output product information in pipe-separated format to be parsed in the script in rpm2kit
25+
for product in advisory.advisory.products {
26+
println!(
27+
"{}|{}|{}",
28+
product.package_name, product.patched_version, product.patched_epoch
29+
);
30+
}
31+
32+
Ok(())
33+
}
34+
35+
fn main() {
36+
if let Err(e) = run() {
37+
eprintln!("{e}");
38+
process::exit(1);
39+
}
40+
}
41+
42+
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
43+
44+
mod error {
45+
use snafu::Snafu;
46+
use std::path::PathBuf;
47+
48+
#[derive(Debug, Snafu)]
49+
#[snafu(visibility(pub(super)))]
50+
pub(super) enum Error {
51+
#[snafu(display("Usage: {} <advisory-file>", program))]
52+
Usage { program: String },
53+
54+
#[snafu(display("Failed to read '{}': {}", path.display(), source))]
55+
ReadFile {
56+
path: PathBuf,
57+
source: std::io::Error,
58+
},
59+
60+
#[snafu(display("Failed to parse advisory '{}': {}", path.display(), source))]
61+
ParseAdvisory {
62+
path: PathBuf,
63+
source: toml::de::Error,
64+
},
65+
}
66+
}
67+
68+
type Result<T> = std::result::Result<T, error::Error>;
69+
70+
#[cfg(test)]
71+
mod tests {
72+
use super::*;
73+
use models::{CveId, GhsaId};
74+
use test_case::test_case;
75+
76+
fn make_advisory_toml(id_field: &str, id_value: &str) -> String {
77+
format!(
78+
r#"
79+
[advisory]
80+
id = "TEST-001"
81+
title = "Test Advisory"
82+
severity = "high"
83+
description = "Test description"
84+
{id_field} = "{id_value}"
85+
[[advisory.products]]
86+
package-name = "test"
87+
patched-version = "1.0"
88+
"#
89+
)
90+
}
91+
92+
#[test_case("CVE-2024-1234"; "basic 4 digit")]
93+
#[test_case("CVE-2024-12345"; "5 digit")]
94+
#[test_case("CVE-2025-472688"; "6 digit")]
95+
#[test_case("CVE-1999-0001"; "old year")]
96+
fn valid_cve(cve: &str) {
97+
let toml = make_advisory_toml("cve", cve);
98+
assert!(
99+
toml::from_str::<Advisory>(&toml).is_ok(),
100+
"Failed to parse valid CVE: {cve}"
101+
);
102+
}
103+
104+
#[test_case("CVE-2024-001"; "too few digits")]
105+
#[test_case("CVE-2024-INVALID"; "non numeric")]
106+
#[test_case("CVE-24-1234"; "year too short")]
107+
#[test_case("CVE-2024-0"; "single digit")]
108+
#[test_case("CVE–2024–1234"; "en dash")]
109+
#[test_case("CVE—2024—1234"; "em dash")]
110+
#[test_case("GHSA-xxxx-xxxx-xxxx"; "wrong format")]
111+
fn invalid_cve(cve: &str) {
112+
let toml = make_advisory_toml("cve", cve);
113+
assert!(
114+
toml::from_str::<Advisory>(&toml).is_err(),
115+
"Should reject invalid CVE: {cve}"
116+
);
117+
}
118+
119+
#[test_case("GHSA-23fg-6c23-wxrv"; "standard")]
120+
#[test_case("GHSA-2222-3333-4444"; "all numeric")]
121+
#[test_case("GHSA-cfgh-jmpq-rvwx"; "all alpha")]
122+
fn valid_ghsa(ghsa: &str) {
123+
let toml = make_advisory_toml("ghsa", ghsa);
124+
assert!(
125+
toml::from_str::<Advisory>(&toml).is_ok(),
126+
"Failed to parse valid GHSA: {ghsa}"
127+
);
128+
}
129+
130+
#[test_case("GHSA-123-456-789"; "too short")]
131+
#[test_case("GHSA-12345-67890-12345"; "too long")]
132+
#[test_case("CVE-2024-1234"; "wrong format")]
133+
fn invalid_ghsa(ghsa: &str) {
134+
let toml = make_advisory_toml("ghsa", ghsa);
135+
assert!(
136+
toml::from_str::<Advisory>(&toml).is_err(),
137+
"Should reject invalid GHSA: {ghsa}"
138+
);
139+
}
140+
141+
#[test]
142+
fn complete_advisory() {
143+
let toml = r#"
144+
[advisory]
145+
id = "BRSA-test123"
146+
title = "Test Advisory"
147+
cve = "CVE-2025-12345"
148+
ghsa = "GHSA-23fg-6c23-wxrv"
149+
severity = "high"
150+
description = "Test description"
151+
152+
[[advisory.products]]
153+
package-name = "test-package"
154+
patched-version = "1.2.3"
155+
patched-epoch = "1"
156+
157+
[[advisory.products]]
158+
package-name = "another-package"
159+
patched-version = "2.0.0"
160+
"#;
161+
162+
let advisory: Advisory = toml::from_str(toml).expect("Failed to parse complete advisory");
163+
assert_eq!(advisory.advisory.id, "BRSA-test123");
164+
assert_eq!(advisory.advisory.cve, CveId::try_new("CVE-2025-12345").ok());
165+
assert_eq!(
166+
advisory.advisory.ghsa,
167+
GhsaId::try_new("GHSA-23fg-6c23-wxrv").ok()
168+
);
169+
assert_eq!(advisory.advisory.products.len(), 2);
170+
assert_eq!(advisory.advisory.products[0].package_name, "test-package");
171+
assert_eq!(advisory.advisory.products[0].patched_epoch, "1");
172+
assert_eq!(advisory.advisory.products[1].patched_epoch, "0");
173+
}
174+
}

0 commit comments

Comments
 (0)