-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
13 changed files
with
4,556 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.