From a2e8f3d863a3139d9e408d17ec2a2a82fc9b0ef3 Mon Sep 17 00:00:00 2001 From: Tom Fay Date: Mon, 22 Apr 2024 10:05:05 +0100 Subject: [PATCH] add /etc/os-release to images by default --- .gitignore | 1 + README.md | 10 ++++-- src/config.rs | 12 ++++++- src/lockfile/mod.rs | 26 +++++++++++----- src/lockfile/resolve.rs | 10 +++++- .../no_auto_etc_os_release/rpmoci.toml | 13 ++++++++ tests/it.rs | 31 +++++++++++++++++++ 7 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/no_auto_etc_os_release/rpmoci.toml diff --git a/.gitignore b/.gitignore index a31bbd7..035dd75 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ tests/**/out out/ vendor/ +tests/**/rpmoci.lock diff --git a/README.md b/README.md index 1642d7a..5cde57f 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,14 @@ id = "foo" Whether or not documentation files are included in the produced containers can be specified via the `content.docs` boolean field. By default documentation files are not included, optimizing for image size. +#### /etc/os-release + +Whether `/etc/os-release` is automatically included as a dependency during resolution, hence installed in the produced image, can be specified via the `content.os_release` boolean field. +This enables SBOM and vulnerability scanning tools to better determine the provenance of packages within the image. +By default this field is enabled. + +*The /etc/os-release file can also be included by adding the distro's `-release` package to the packages array: this field exists to ensure the /etc/os-release file is included by default.* + ### Image building Running `rpmoci build --image foo --tag bar` will build a container image in OCI format. @@ -243,8 +251,6 @@ $ rpmoci build --image foo --tag bar --vendor-dir vendor ### SBOM support rpmoci doesn't have native SBOM support, but because it just uses standard OS package functionality SBOM generators like trivy and syft can be used to generate SBOMs for the produced images. -*For these tools to detect the Linux distribution correctly you may need to install the `-release` package in the image.* - ## Developing rpmoci is written in Rust and currently resolves RPMs using DNF via an embedded Python module. diff --git a/src/config.rs b/src/config.rs index 387df7a..71c3025 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,16 +58,26 @@ pub(crate) struct PackageConfig { pub(crate) packages: Vec, #[serde(default)] pub(crate) gpgkeys: Vec, - /// Whether to install documentation files + /// Whether to install documentation files. /// Defaults to false, to produce smaller container images. #[serde(default = "docs_default")] pub(crate) docs: bool, + /// Whether to include /etc/os-release as a dependency. + /// Defaults to true, so that scanning tools can detect + /// the distro of images produced by rpmoci without users + /// needing to add the -release package. + #[serde(default = "os_release_default")] + pub(crate) os_release: bool, } fn docs_default() -> bool { false } +fn os_release_default() -> bool { + true +} + /// Configuration file for rpmoci #[derive(Debug, Serialize, Default, Deserialize, Clone)] #[serde(deny_unknown_fields)] diff --git a/src/lockfile/mod.rs b/src/lockfile/mod.rs index 306e41c..4298bf0 100644 --- a/src/lockfile/mod.rs +++ b/src/lockfile/mod.rs @@ -78,20 +78,20 @@ struct RepoKeyInfo { /// A resolved package #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)] -struct Package { +pub struct Package { /// The package name - name: String, + pub name: String, /// The package epoch-version-release - evr: String, + pub evr: String, /// The package checksum - checksum: Checksum, + pub checksum: Checksum, /// The id of the package's repository - repoid: String, + pub repoid: String, } /// Checksum of RPM package #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)] -struct Checksum { +pub struct Checksum { /// The algorithm of the checksum algorithm: Algorithm, /// The checksum value @@ -101,11 +101,16 @@ struct Checksum { /// Algorithms supported by RPM for checksums #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)] #[serde(rename_all = "lowercase")] -enum Algorithm { - MD5, //Devskim: ignore DS126858 +pub enum Algorithm { + /// The MD5 algorithm + MD5, //Devskim: ignore DS126858 + /// The SHA1 algorithm SHA1, //Devskim: ignore DS126858 + /// The SHA256 algorithm SHA256, + /// The SHA384 algorithm SHA384, + /// The SHA512 algorithm SHA512, } @@ -179,4 +184,9 @@ impl Lockfile { Ok(()) } + + /// Returns an iterator over the packages in the Lockfile + pub fn iter_packages(&self) -> impl Iterator { + self.packages.iter() + } } diff --git a/src/lockfile/resolve.rs b/src/lockfile/resolve.rs index bdb4234..6f82551 100644 --- a/src/lockfile/resolve.rs +++ b/src/lockfile/resolve.rs @@ -31,9 +31,10 @@ use crate::config::Repository; impl Lockfile { /// Perform dependency resolution on the given package specs pub(crate) fn resolve( - pkg_specs: Vec, + mut pkg_specs: Vec, repositories: &[Repository], gpgkeys: Vec, + include_etc_os_release: bool, ) -> Result { let output = Python::with_gil(|py| { // Resolve is a compiled in python module for resolving dependencies @@ -41,6 +42,11 @@ impl Lockfile { PyModule::from_code(py, include_str!("resolve.py"), "resolve", "resolve")?; let base = setup_base(py, repositories, &gpgkeys)?; + let etc_os_release = "/etc/os-release".to_string(); + if include_etc_os_release && !pkg_specs.contains(&etc_os_release) { + pkg_specs.push(etc_os_release); + } + let args = PyTuple::new(py, &[base.to_object(py), pkg_specs.to_object(py)]); // Run the resolve function, returning a json string, which we shall deserialize. let val: String = resolve.getattr("resolve")?.call1(args)?.extract()?; @@ -64,6 +70,7 @@ impl Lockfile { cfg.contents.packages.clone(), &cfg.contents.repositories, cfg.contents.gpgkeys.clone(), + cfg.contents.os_release, ) } @@ -137,6 +144,7 @@ impl Lockfile { requires, &cfg.contents.repositories, cfg.contents.gpgkeys.clone(), + cfg.contents.os_release, )?; lockfile.local_packages = self.local_packages.clone(); lockfile.pkg_specs = cfg.contents.packages.clone(); diff --git a/tests/fixtures/no_auto_etc_os_release/rpmoci.toml b/tests/fixtures/no_auto_etc_os_release/rpmoci.toml new file mode 100644 index 0000000..b6a4d70 --- /dev/null +++ b/tests/fixtures/no_auto_etc_os_release/rpmoci.toml @@ -0,0 +1,13 @@ +[contents] +gpgkeys = [ + "https://raw.githubusercontent.com/microsoft/CBL-Mariner/2.0/SPECS/mariner-repos/MICROSOFT-RPM-GPG-KEY", + "https://packages.microsoft.com/keys/microsoft.asc", +] +packages = ["tini-static"] +os_release = false + +[[contents.repositories]] +url = "https://packages.microsoft.com/cbl-mariner/2.0/prod/base/x86_64" + +[image] +cmd = ["tini-static"] diff --git a/tests/it.rs b/tests/it.rs index c22bf8f..2ba8032 100644 --- a/tests/it.rs +++ b/tests/it.rs @@ -6,6 +6,8 @@ use std::{ process::Command, }; +use rpmoci::lockfile::Lockfile; + // Path to rpmoci binary under test const EXE: &str = env!("CARGO_BIN_EXE_rpmoci"); @@ -158,6 +160,14 @@ fn test_simple_build() { .status() .unwrap(); + // Open the lockfile and verify /etc/os-release was included as a dependency + let lockfile_path = root.join("rpmoci.lock"); + eprintln!("lockfile_path: {}", lockfile_path.display()); + let lockfile: Lockfile = toml::from_str(&fs::read_to_string(lockfile_path).unwrap()).unwrap(); + assert!(lockfile + .iter_packages() + .any(|p| p.name == "mariner-release")); + // Cleanup using sudo let _ = Command::new("sudo") .arg("rm") @@ -234,3 +244,24 @@ fn test_capabilities() { .contains("cap_net_admin=ep")); assert!(status.success()); } + +#[test] +fn test_no_auto_etc_os_release() { + // Test that `contents.os_release = false` works + let root = setup_test("no_auto_etc_os_release"); + let output = Command::new(EXE) + .arg("update") + .current_dir(&root) + .output() + .unwrap(); + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + eprintln!("stderr: {}. {}. {}", stderr, root.display(), EXE); + assert!(output.status.success()); + // Open the lockfile and verify /etc/os-release was not added as a dependency + let lockfile_path = root.join("rpmoci.lock"); + eprintln!("lockfile_path: {}", lockfile_path.display()); + let lockfile: Lockfile = toml::from_str(&fs::read_to_string(lockfile_path).unwrap()).unwrap(); + assert!(!lockfile + .iter_packages() + .any(|p| p.name == "mariner-release")); +}