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:
parent
1d8bd0f8a6
commit
ba70909975
13 changed files with 4556 additions and 0 deletions
52
stages/org.osbuild.dnf4.sbom.spdx
Executable file
52
stages/org.osbuild.dnf4.sbom.spdx
Executable 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)
|
||||
59
stages/org.osbuild.dnf4.sbom.spdx.meta.json
Normal file
59
stages/org.osbuild.dnf4.sbom.spdx.meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
stages/test/test_dnf4_sbom_spdx.py
Normal file
130
stages/test/test_dnf4_sbom_spdx.py
Normal 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)
|
||||
1045
test/data/stages/dnf4.sbom.spdx-input/a.json
Normal file
1045
test/data/stages/dnf4.sbom.spdx-input/a.json
Normal file
File diff suppressed because it is too large
Load diff
28
test/data/stages/dnf4.sbom.spdx-input/a.mpp.yaml
Normal file
28
test/data/stages/dnf4.sbom.spdx-input/a.mpp.yaml
Normal 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
|
||||
1059
test/data/stages/dnf4.sbom.spdx-input/b.json
Normal file
1059
test/data/stages/dnf4.sbom.spdx-input/b.json
Normal file
File diff suppressed because it is too large
Load diff
37
test/data/stages/dnf4.sbom.spdx-input/b.mpp.yaml
Normal file
37
test/data/stages/dnf4.sbom.spdx-input/b.mpp.yaml
Normal 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"
|
||||
7
test/data/stages/dnf4.sbom.spdx-input/diff.json
Normal file
7
test/data/stages/dnf4.sbom.spdx-input/diff.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"added_files": [
|
||||
"/image.spdx.json"
|
||||
],
|
||||
"deleted_files": [],
|
||||
"differences": {}
|
||||
}
|
||||
1036
test/data/stages/dnf4.sbom.spdx/a.json
Normal file
1036
test/data/stages/dnf4.sbom.spdx/a.json
Normal file
File diff suppressed because it is too large
Load diff
24
test/data/stages/dnf4.sbom.spdx/a.mpp.yaml
Normal file
24
test/data/stages/dnf4.sbom.spdx/a.mpp.yaml
Normal 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
|
||||
1044
test/data/stages/dnf4.sbom.spdx/b.json
Normal file
1044
test/data/stages/dnf4.sbom.spdx/b.json
Normal file
File diff suppressed because it is too large
Load diff
28
test/data/stages/dnf4.sbom.spdx/b.mpp.yaml
Normal file
28
test/data/stages/dnf4.sbom.spdx/b.mpp.yaml
Normal 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"
|
||||
7
test/data/stages/dnf4.sbom.spdx/diff.json
Normal file
7
test/data/stages/dnf4.sbom.spdx/diff.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"added_files": [
|
||||
"/root/image.spdx.json"
|
||||
],
|
||||
"deleted_files": [],
|
||||
"differences": {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue