Add org.osbuild.dnf4.sbom.spdx stage

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 <thozza@redhat.com>
This commit is contained in:
Tomáš Hozza 2024-07-02 17:49:44 +02:00 committed by Simon de Vlieger
parent 1d8bd0f8a6
commit ba70909975
13 changed files with 4556 additions and 0 deletions

View file

@ -0,0 +1,52 @@
#!/usr/bin/python3
import json
import sys
import tempfile
import dnf
import osbuild
from osbuild.util.sbom.dnf import dnf_pkgset_to_sbom_pkgset
from osbuild.util.sbom.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_sbom_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)

View file

@ -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"
]
}
}
}
}
}
}
}

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
version: '2'
pipelines:
- mpp-import-pipelines:
path: ../manifests/fedora-vars.ipp.yaml
- mpp-import-pipeline:
path: ../manifests/fedora-build-v2.ipp.yaml
id: build
runner:
mpp-format-string: org.osbuild.fedora{release}
- name: os-tree
build: name:build
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: $module_platform_id
repos:
mpp-eval: repos
packages:
- tmux
- name: tree
build: name:build
stages:
- type: org.osbuild.noop

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
version: '2'
pipelines:
- mpp-import-pipelines:
path: ../manifests/fedora-vars.ipp.yaml
- mpp-import-pipeline:
path: ../manifests/fedora-build-v2.ipp.yaml
id: build
runner:
mpp-format-string: org.osbuild.fedora{release}
- name: os-tree
build: name:build
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: $module_platform_id
repos:
mpp-eval: repos
packages:
- tmux
- name: tree
build: name:build
stages:
- type: org.osbuild.dnf4.sbom.spdx
inputs:
root-tree:
type: org.osbuild.tree
origin: org.osbuild.pipeline
references:
- name:os-tree
options:
config:
doc_path: "/image.spdx.json"

View file

@ -0,0 +1,7 @@
{
"added_files": [
"/image.spdx.json"
],
"deleted_files": [],
"differences": {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
version: '2'
pipelines:
- mpp-import-pipelines:
path: ../manifests/fedora-vars.ipp.yaml
- mpp-import-pipeline:
path: ../manifests/fedora-build-v2.ipp.yaml
id: build
runner:
mpp-format-string: org.osbuild.fedora{release}
- name: tree
build: name:build
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: $module_platform_id
repos:
mpp-eval: repos
packages:
- tmux

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
version: '2'
pipelines:
- mpp-import-pipelines:
path: ../manifests/fedora-vars.ipp.yaml
- mpp-import-pipeline:
path: ../manifests/fedora-build-v2.ipp.yaml
id: build
runner:
mpp-format-string: org.osbuild.fedora{release}
- name: tree
build: name:build
stages:
- type: org.osbuild.rpm
inputs:
packages:
type: org.osbuild.files
origin: org.osbuild.source
mpp-depsolve:
architecture: $arch
module-platform-id: $module_platform_id
repos:
mpp-eval: repos
packages:
- tmux
- type: org.osbuild.dnf4.sbom.spdx
options:
config:
doc_path: "/root/image.spdx.json"

View file

@ -0,0 +1,7 @@
{
"added_files": [
"/root/image.spdx.json"
],
"deleted_files": [],
"differences": {}
}