Skip to content

Commit c4da0c4

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

File tree

15 files changed

+2089
-9
lines changed

15 files changed

+2089
-9
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 5 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-checker",
2425

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

5657
[workspace.dependencies]
58+
advisory-checker = { version = "0.1", path = "tools/advisory-checker", artifact = [ "bin:advisory-checker" ] }
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" }
@@ -77,6 +79,7 @@ testsys-config = { version = "0.1", path = "tools/testsys-config" }
7779
testsys-model = { version = "0.0.16", git = "https://github.com/bottlerocket-os/bottlerocket-test-system", tag = "v0.0.16" }
7880

7981
twoliter = { version = "0.16.0", path = "twoliter", artifact = [ "bin:twoliter" ] }
82+
twoliter-tool-advisory-checker = { version = "0.1", path = "twoliter/src/tool-crates/advisory-checker" }
8083
twoliter-tool-buildsys = { version = "0.1", path = "twoliter/src/tool-crates/buildsys" }
8184
twoliter-tool-embedded-bundle = { version = "0.1", path = "twoliter/src/tool-crates/embedded-bundle" }
8285
twoliter-tool-pipesys = { version = "0.1", path = "twoliter/src/tool-crates/pipesys" }
@@ -136,13 +139,15 @@ nix = "0.30"
136139
nonzero_ext = "0.3"
137140
num_cpus = "1"
138141
num-traits = "0.2"
142+
nutype = "0.6"
139143
once_cell = "1.21"
140144
olpc-cjson = "0.1"
141145
proc-macro2 = "1"
142146
quote = "1"
143147
rand = { version = "0.9", default-features = false }
144148
regex = "1"
145149
reqwest = { version = "0.12", default-features = false }
150+
rpm = "0.18"
146151
seccompiler = "0.5"
147152
semver = "1"
148153
serde = "1"

tests/integration-tests/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ libc.workspace = true
1111
lzma-rs.workspace = true
1212
once_cell.workspace = true
1313
rand.workspace = true
14+
regex.workspace = true
1415
reqwest.workspace = true
1516
serde.workspace = true
1617
serde_json.workspace = true
@@ -20,4 +21,5 @@ semver.workspace = true
2021
tokio = { workspace = true, features = ["fs", "process", "rt-multi-thread"] }
2122
toml.workspace = true
2223
twoliter = { workspace = true }
24+
advisory-checker = { workspace = true }
2325
which.workspace = true
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
use super::run_command;
2+
use regex::Regex;
3+
use std::path::PathBuf;
4+
use tempfile::TempDir;
5+
6+
pub const ADVISORY_CHECKER_PATH: &str = env!("CARGO_BIN_FILE_ADVISORY_CHECKER");
7+
8+
const SPEC_TEMPLATE: &str = r#"Name: {name}
9+
Version: {version}
10+
Release: 1
11+
Summary: Test package
12+
License: MIT
13+
14+
%description
15+
Test package
16+
"#;
17+
18+
fn create_spec_file(dir: &TempDir, name: &str, version: &str) -> PathBuf {
19+
let content = SPEC_TEMPLATE
20+
.replace("{name}", name)
21+
.replace("{version}", version);
22+
let path = dir.path().join("test.spec");
23+
std::fs::write(&path, content).unwrap();
24+
path
25+
}
26+
27+
fn create_advisory_file(dir: &TempDir, filename: &str, content: &str) -> PathBuf {
28+
let path = dir.path().join(filename);
29+
if let Some(parent) = path.parent() {
30+
std::fs::create_dir_all(parent).unwrap();
31+
}
32+
std::fs::write(&path, content).unwrap();
33+
path
34+
}
35+
36+
#[test]
37+
#[ignore]
38+
fn test_missing_advisories_dir_succeeds() {
39+
// Users of kits may want to do away with the advisories directory.
40+
let temp_dir = TempDir::new().unwrap();
41+
let spec_file = create_spec_file(&temp_dir, "testpkg", "1.0.0");
42+
43+
// We provide an arbitrary advisories directory that isn't present.
44+
let advisories_dir_name = "nonexistent";
45+
let nonexistent_dir = temp_dir.path().join(advisories_dir_name);
46+
47+
// We run the advisory-checker here
48+
let output = run_command(
49+
ADVISORY_CHECKER_PATH,
50+
[
51+
"--spec-file",
52+
spec_file.to_str().unwrap(),
53+
"--advisories-dir",
54+
nonexistent_dir.to_str().unwrap(),
55+
],
56+
[],
57+
);
58+
59+
// The step passes becauses we ignore advisory checks in this case.
60+
assert!(output.status.success());
61+
62+
// We also test the expected output to make sure that the code path is as expected.
63+
let stdout = String::from_utf8_lossy(&output.stdout);
64+
assert!(stdout.contains("skipping"));
65+
}
66+
67+
#[test]
68+
#[ignore]
69+
fn test_empty_advisories_dir_succeeds() {
70+
// Given a scenario when the advisory directory is empty
71+
let temp_dir = TempDir::new().unwrap();
72+
let spec_file = create_spec_file(&temp_dir, "testpkg", "1.0.0");
73+
74+
let advisories_dir = temp_dir.path().join("advisories");
75+
std::fs::create_dir(&advisories_dir).unwrap();
76+
77+
// We run the advisory-checker here
78+
let output = run_command(
79+
ADVISORY_CHECKER_PATH,
80+
[
81+
"--spec-file",
82+
spec_file.to_str().unwrap(),
83+
"--advisories-dir",
84+
advisories_dir.to_str().unwrap(),
85+
],
86+
[],
87+
);
88+
89+
// Then the test will pass because the logic will search for toml
90+
// files and won't find any and exit without errors.
91+
assert!(output.status.success());
92+
}
93+
94+
#[test]
95+
#[ignore]
96+
fn test_ignores_non_toml_files() {
97+
// Given a scenario when the advisory directory has non-toml files
98+
let temp_dir = TempDir::new().unwrap();
99+
let spec_file = create_spec_file(&temp_dir, "testpkg", "1.0.0");
100+
let advisories_dir = temp_dir.path().join("advisories");
101+
std::fs::create_dir(&advisories_dir).unwrap();
102+
std::fs::write(advisories_dir.join(".gitkeep"), "").unwrap();
103+
104+
// We run the advisory-checker here
105+
let output = run_command(
106+
ADVISORY_CHECKER_PATH,
107+
[
108+
"--spec-file",
109+
spec_file.to_str().unwrap(),
110+
"--advisories-dir",
111+
advisories_dir.to_str().unwrap(),
112+
],
113+
[],
114+
);
115+
116+
// Then the test will pass because the logic will search for toml
117+
// files and won't find any and exit without errors.
118+
assert!(output.status.success());
119+
}
120+
121+
#[test]
122+
#[ignore]
123+
fn test_advisory_violation_fails() {
124+
// Given a scenario when we have a software package that is at a lower version
125+
// than a published advisory.
126+
let temp_dir = TempDir::new().unwrap();
127+
let advisories_dir = temp_dir.path().join("advisories");
128+
std::fs::create_dir(&advisories_dir).unwrap();
129+
130+
let spec_file = create_spec_file(&temp_dir, "testpkg", "1.0.0");
131+
132+
let advisory = r#"[advisory]
133+
id = "BRSA-test123"
134+
title = "Test Advisory"
135+
cve = "CVE-2024-12345"
136+
severity = "high"
137+
description = "Test vulnerability"
138+
139+
[[advisory.products]]
140+
package-name = "testpkg"
141+
patched-version = "2.0.0"
142+
patched-epoch = "0"
143+
"#;
144+
create_advisory_file(&temp_dir, "advisories/BRSA-test.toml", advisory);
145+
146+
// We run the advisory-checker here
147+
let output = run_command(
148+
ADVISORY_CHECKER_PATH,
149+
[
150+
"--spec-file",
151+
spec_file.to_str().unwrap(),
152+
"--advisories-dir",
153+
advisories_dir.to_str().unwrap(),
154+
],
155+
[],
156+
);
157+
158+
// Then the code runs successfully but it finds advisory violations
159+
// because the package version is lower than the advisory
160+
assert!(!output.status.success());
161+
let stdout = String::from_utf8_lossy(&output.stdout);
162+
assert!(stdout.contains("Advisory violations found"));
163+
}
164+
165+
#[test]
166+
#[ignore]
167+
fn test_advisory_satisfied_succeeds() {
168+
// Given a scenario when we have a software package that is at a higher
169+
// version than its published advisory.
170+
let temp_dir = TempDir::new().unwrap();
171+
let advisories_dir = temp_dir.path().join("advisories");
172+
std::fs::create_dir(&advisories_dir).unwrap();
173+
174+
let spec_file = create_spec_file(&temp_dir, "testpkg", "3.0.0");
175+
176+
let advisory = r#"[advisory]
177+
id = "BRSA-test456"
178+
title = "Test Advisory"
179+
cve = "CVE-2024-67890"
180+
severity = "moderate"
181+
description = "Test vulnerability"
182+
183+
[[advisory.products]]
184+
package-name = "testpkg"
185+
patched-version = "2.0.0"
186+
patched-epoch = "0"
187+
"#;
188+
create_advisory_file(&temp_dir, "advisories/BRSA-test.toml", advisory);
189+
190+
// We run the advisory-checker here
191+
let output = run_command(
192+
ADVISORY_CHECKER_PATH,
193+
[
194+
"--spec-file",
195+
spec_file.to_str().unwrap(),
196+
"--advisories-dir",
197+
advisories_dir.to_str().unwrap(),
198+
],
199+
[],
200+
);
201+
202+
// Then the code runs successfully and doesn't find advisory violations.
203+
assert!(output.status.success());
204+
let stdout = String::from_utf8_lossy(&output.stdout);
205+
assert!(!stdout.contains("Advisory violations found"));
206+
}
207+
208+
#[test]
209+
#[ignore]
210+
fn test_advisory_for_removed_package_ignored() {
211+
// Given a scenario when we have a kit and advisories for removed packages
212+
let temp_dir = TempDir::new().unwrap();
213+
let advisories_dir = temp_dir.path().join("advisories");
214+
std::fs::create_dir(&advisories_dir).unwrap();
215+
216+
let spec_file = create_spec_file(&temp_dir, "otherpkg", "1.0.0");
217+
218+
let advisory = r#"[advisory]
219+
id = "BRSA-test789"
220+
title = "Test Advisory"
221+
cve = "CVE-2024-11111"
222+
severity = "critical"
223+
description = "Test vulnerability"
224+
225+
[[advisory.products]]
226+
package-name = "testpkg"
227+
patched-version = "2.0.0"
228+
patched-epoch = "0"
229+
"#;
230+
create_advisory_file(&temp_dir, "advisories/BRSA-test.toml", advisory);
231+
232+
// We run the advisory-checker here
233+
let output = run_command(
234+
ADVISORY_CHECKER_PATH,
235+
[
236+
"--spec-file",
237+
spec_file.to_str().unwrap(),
238+
"--advisories-dir",
239+
advisories_dir.to_str().unwrap(),
240+
],
241+
[],
242+
);
243+
244+
// Then the code runs successfully and doesn't test on the advisories
245+
// not meant for this package
246+
assert!(output.status.success());
247+
}
248+
249+
#[test]
250+
#[ignore]
251+
fn test_parse_advisory_error_invalid_toml() {
252+
// Given a spec file and an advisory file with invalid TOML
253+
let temp_dir = TempDir::new().unwrap();
254+
let advisories_dir = temp_dir.path().join("advisories");
255+
std::fs::create_dir(&advisories_dir).unwrap();
256+
257+
let spec_file = create_spec_file(&temp_dir, "testpkg", "1.0.0");
258+
create_advisory_file(
259+
&temp_dir,
260+
"advisories/BRSA-invalid.toml",
261+
"not valid toml {{{",
262+
);
263+
264+
// We run the advisory-checker here
265+
let output = run_command(
266+
ADVISORY_CHECKER_PATH,
267+
[
268+
"--spec-file",
269+
spec_file.to_str().unwrap(),
270+
"--advisories-dir",
271+
advisories_dir.to_str().unwrap(),
272+
],
273+
[],
274+
);
275+
276+
// The command fails because of a failure in advisory parsing and
277+
// we want the user to fix/remove the advisory.
278+
assert!(!output.status.success());
279+
let stderr = String::from_utf8_lossy(&output.stderr);
280+
assert!(stderr.contains("Failed to parse advisory"));
281+
}

tests/integration-tests/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::path::PathBuf;
66
use std::process::Command;
77
use tempfile::TempDir;
88

9+
mod advisory_checker;
910
mod appinventory;
1011
mod imghelper;
1112
mod twoliter_build;

tools/advisory-checker/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "advisory-checker"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0 OR MIT"
6+
publish = false
7+
exclude = ["README.md"]
8+
9+
[dependencies]
10+
nutype = { workspace = true, features = ["regex", "serde"] }
11+
regex.workspace = true
12+
serde = { workspace = true, features = ["derive"] }
13+
clap = { workspace = true, features = ["derive"] }
14+
snafu.workspace = true
15+
toml.workspace = true
16+
rpm.workspace = true
17+
18+
[dev-dependencies]
19+
test-case.workspace = true
20+
21+
[lints]
22+
workspace = true

0 commit comments

Comments
 (0)