Skip to content

Commit

Permalink
Add org.osbuild.dnf4.sbom.spdx stage
Browse files Browse the repository at this point in the history
Add a new stage, which allows analyzing the installed packages in a
given filesystem tree using DNF4 API and generating an SPDX v2.3 SBOM
document for it.

One can provide the filesystem tree to be analyzed as a stage input. If
no input is provided, the stage will analyze the filesystem tree of the
current pipeline.

Add tests cases for both usage variants of the stage, as well as the
unit test for stage schema validation.

Signed-off-by: Tomáš Hozza <[email protected]>
  • Loading branch information
thozza committed Jul 12, 2024
1 parent bf93296 commit 5c7a37e
Show file tree
Hide file tree
Showing 13 changed files with 4,556 additions and 0 deletions.
52 changes: 52 additions & 0 deletions stages/org.osbuild.dnf4.sbom.spdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/python3
import json
import sys
import tempfile

import dnf

import osbuild
from osbuild.util.bom.dnf import dnf_pkgset_to_bom_pkgset
from osbuild.util.bom.spdx import bom_pkgset_to_spdx2_doc


def get_installed_packages(tree):
with tempfile.TemporaryDirectory() as tempdir:
conf = dnf.conf.Conf()
conf.installroot = tree
conf.persistdir = f"{tempdir}{conf.persistdir}"
conf.cachedir = f"{tempdir}{conf.cachedir}"
conf.reposdir = [f"{tree}{d}" for d in conf.reposdir]
conf.pluginconfpath = [f"{tree}{d}" for d in conf.pluginconfpath]
conf.varsdir = [f"{tree}{d}" for d in conf.varsdir]
conf.prepend_installroot("config_file_path")

base = dnf.Base(conf)
base.read_all_repos()
base.fill_sack(load_available_repos=False)
return base.sack.query().installed()


def main(inputs, tree, options):
config = options["config"]
doc_path = config["doc_path"]

tree_to_analyze = tree
if inputs:
tree_to_analyze = inputs["root-tree"]["path"]

installed = get_installed_packages(tree_to_analyze)
bom_pkgset = dnf_pkgset_to_bom_pkgset(installed)
spdx2_doc = bom_pkgset_to_spdx2_doc(bom_pkgset)
spdx2_json = spdx2_doc.to_dict()

with open(f"{tree}{doc_path}", "w", encoding="utf-8") as f:
json.dump(spdx2_json, f)

return 0


if __name__ == '__main__':
args = osbuild.api.arguments()
r = main(args.get("inputs", {}), args["tree"], args["options"])
sys.exit(r)
59 changes: 59 additions & 0 deletions stages/org.osbuild.dnf4.sbom.spdx.meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"summary": "Generate SPDX SBOM document for the installed packages.",
"description": [
"The stage generates a Software Bill of Materials (SBOM) document",
"in SPDX v2 format for the installed RPM packages. DNF4 API is used",
"to retrieve the installed packages and their metadata. The SBOM",
"document is saved in the specified path. If a tree is provided,",
"as an input, the stage will analyze the tree instead of the",
"current pipeline tree."
],
"schema_2": {
"options": {
"additionalProperties": false,
"description": "Options for the SPDX SBOM generator.",
"required": [
"config"
],
"properties": {
"config": {
"type": "object",
"description": "Configuration for the SPDX SBOM generator.",
"additionalProperties": false,
"required": [
"doc_path"
],
"properties": {
"doc_path": {
"type": "string",
"pattern": "^\\/(?!\\.\\.)((?!\\/\\.\\.\\/).)+[\\w]{1,250}\\.spdx.json$",
"description": "Path used to save the SPDX SBOM document."
}
}
}
}
},
"inputs": {
"type": "object",
"additionalProperties": false,
"required": [
"root-tree"
],
"properties": {
"root-tree": {
"type": "object",
"additionalProperties": true,
"description": "The tree containing the installed packages. If the input is not provided, the stage will analyze the tree of the current pipeline.",
"properties": {
"type": {
"type": "string",
"enum": [
"org.osbuild.tree"
]
}
}
}
}
}
}
}
130 changes: 130 additions & 0 deletions stages/test/test_dnf4_sbom_spdx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/python3

import pytest

import osbuild.testutil as testutil

STAGE_NAME = "org.osbuild.dnf4.sbom.spdx"


@pytest.mark.parametrize("test_data,expected_err", [
# good
(
{
"options": {
"config": {
"doc_path": "/image.spdx.json",
}
}
},
"",
),
(
{
"options": {
"config": {
"doc_path": "/root/doc.spdx.json",
}
}
},
"",
),
(
{
"options": {
"config": {
"doc_path": "/image.spdx.json",
}
},
"inputs": {
"root-tree": {
"type": "org.osbuild.tree",
"origin": "org.osbuild.pipeline",
"references": [
"name:root-tree"
]
}
}
},
"",
),
# bad
(
{
"options": {
"config": {
"doc_path": "/image.spdx",
}
}
},
"'/image.spdx' does not match '^\\\\/(?!\\\\.\\\\.)((?!\\\\/\\\\.\\\\.\\\\/).)+[\\\\w]{1,250}\\\\.spdx.json$'",
),
(
{
"options": {
"config": {
"doc_path": "/image.json",
}
}
},
"'/image.json' does not match '^\\\\/(?!\\\\.\\\\.)((?!\\\\/\\\\.\\\\.\\\\/).)+[\\\\w]{1,250}\\\\.spdx.json$'",
),
(
{
"options": {
"config": {
"doc_path": "image.spdx.json",
}
}
},
"'image.spdx.json' does not match '^\\\\/(?!\\\\.\\\\.)((?!\\\\/\\\\.\\\\.\\\\/).)+[\\\\w]{1,250}\\\\.spdx.json$'",
),
(
{
"options": {
"config": {}
}
},
"'doc_path' is a required property",
),
(
{
"options": {}
},
"'config' is a required property",
),
(
{
"options": {
"config": {
"doc_path": "/image.spdx.json",
}
},
"inputs": {
"root-tree": {
"type": "org.osbuild.file",
"origin": "org.osbuild.pipeline",
"references": [
"name:root-tree"
]
}
}
},
"'org.osbuild.file' is not one of ['org.osbuild.tree']",
),
])
@pytest.mark.parametrize("stage_schema", ["2"], indirect=True)
def test_schema_validation(stage_schema, test_data, expected_err):
test_input = {
"type": STAGE_NAME,
"options": test_data["options"],
}
if "inputs" in test_data:
test_input["inputs"] = test_data["inputs"]

res = stage_schema.validate(test_input)
if expected_err == "":
assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}"
else:
assert res.valid is False
testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1)
Loading

0 comments on commit 5c7a37e

Please sign in to comment.