diff --git a/src/provenance/main.py b/src/provenance/main.py index e4bf35a..ee6e0dd 100755 --- a/src/provenance/main.py +++ b/src/provenance/main.py @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: Apache-2.0 -""" Python script that generates SLSA v1.0 provenance file for a nix target """ +"""Python script that generates SLSA v1.0 provenance file for a nix target""" import argparse import json @@ -42,7 +42,7 @@ def get_env_metadata(): "PROVENANCE_INTERNAL_PARAMS", ] - values = [os.environ.get(name) for name in env_vars] + values = [os.environ.get(name, "") for name in env_vars] LOG.info("Reading metadata from environment:") for name, value in zip(env_vars, values): @@ -128,10 +128,10 @@ def get_external_parameters(drv_path: str, metadata: BuildMeta) -> dict: return {k: v for k, v in params.items() if v} -def timestamp(unix_time: int | str | None) -> str | None: +def timestamp(unix_time: int | str | None) -> str: """Turn unix timestamp into RFC 3339 format""" - if unix_time is None: - return None + if not unix_time: + return "" dtime = datetime.fromtimestamp( int(unix_time), @@ -222,6 +222,7 @@ def main(): if args.out: with open(args.out, "w", encoding="utf-8") as filepath: + LOG.info("Writing provenance file into '%s'", args.out) filepath.write(json.dumps(schema, indent=2)) else: print(json.dumps(schema, indent=2)) diff --git a/tests/resources/README.md b/tests/resources/README.md index be95558..4a3662a 100644 --- a/tests/resources/README.md +++ b/tests/resources/README.md @@ -5,11 +5,18 @@ SPDX-License-Identifier: CC-BY-SA-4.0 --> # Test resources + ## CycloneDX 1.3 json schema -- cdx_bom-1.3.schema.json -- https://github.com/CycloneDX/specification/blob/9b04a94474dfcabafe7d3a9f8db6c7e5eb868adb/schema/bom-1.3.schema.json +- cdx_bom-1.3.schema.json +- ## SPDX 2.3 json schema + - spdx_bom-2.3.schema.json -- https://github.com/spdx/spdx-spec/blob/214f23d34ee287cb1db5b31c3d571af291e836f3/schemas/spdx-schema.json \ No newline at end of file +- + +## SLSA v1.0 provenance schema + +- provenance-1.0.schema.json +- translated and rewritten into jsonschema format. diff --git a/tests/resources/provenance-1.0.schema.json b/tests/resources/provenance-1.0.schema.json new file mode 100644 index 0000000..30fb71d --- /dev/null +++ b/tests/resources/provenance-1.0.schema.json @@ -0,0 +1,170 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://in-toto.io/Statement/v1", + "title": "SLSA Provenance v1.0", + "type": "object", + "additionalProperties": false, + "required": [ + "_type", + "subject", + "predicateType", + "predicate" + ], + "properties": { + "_type": { + "description": "Identifier for the schema of the Statement. Always https://in-toto.io/Statement/v1 for this version of the spec.", + "type": "string" + }, + "subject": { + "description": "Set of software artifacts that the attestation applies to. Each element represents a single software artifact.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "Identifier to distinguish this artifact from others within the subject.", + "type": "string" + }, + "digest": { + "description": "Collection of cryptographic digests for the contents of this artifact.", + "type": "object" + } + } + } + }, + "predicateType": { + "description": "URI identifying the type of the Predicate.", + "type": "string" + }, + "predicate": { + "type": "object", + "additionalProperties": false, + "required": [ + "buildDefinition", + "runDetails" + ], + "properties": { + "buildDefinition": { + "type": "object", + "additionalProperties": false, + "minProperties": 4, + "properties": { + "buildType": { + "description": "Identifies the template for how to perform the build and interpret the parameters and dependencies.", + "type": "string" + }, + "externalParameters": { + "description": "The parameters that are under external control, such as those set by a user or tenant of the build platform.", + "type": "object" + }, + "internalParameters": { + "description": "The parameters that are under the control of the entity represented by builder.id.", + "type": "object" + }, + "resolvedDependencies": { + "description": "Unordered collection of artifacts needed at build time.", + "type": "array", + "items": { + "$ref": "#/$defs/ResourceDescriptor" + } + } + } + }, + "runDetails": { + "type": "object", + "additionalProperties": false, + "required": [ + "builder", + "metadata", + "byproducts" + ], + "properties": { + "builder": { + "description": "dentifies the build platform that executed the invocation.", + "type": "object", + "properties": { + "id": { + "description": "URI indicating the transitive closure of the trusted build platform.", + "type": "string" + }, + "builderDependencies": { + "description": "Dependencies used by the orchestrator that are not run within the workload and that do not affect the build", + "type": "array", + "items": { + "$ref": "#/$defs/ResourceDescriptor" + } + }, + "version": { + "description": "Map of names of components of the build platform to their version.", + "type": "object" + } + } + }, + "metadata": { + "description": "Metadata about this particular execution of the build.", + "type": "object", + "properties": { + "invocationId": { + "description": "Identifies this particular build invocation", + "type": "string" + }, + "startedOn": { + "description": "The timestamp of when the build started.", + "type": "string" + }, + "finishedOn": { + "description": "The timestamp of when the build completed.", + "type": "string" + } + } + }, + "byproducts": { + "description": "Additional artifacts generated during the build that are not considered the “output” of the build", + "type": "array", + "items": { + "$ref": "#/$defs/ResourceDescriptor" + } + } + } + } + } + } + }, + "$defs": { + "ResourceDescriptor": { + "$id": "/schema/ResourceDescriptor", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "description": "Machine-readable identifier for distinguishing between descriptors.", + "type": "string" + }, + "uri": { + "description": "A URI used to identify the resource or artifact globally.", + "type": "string" + }, + "digest": { + "description": "A set of cryptographic digests of the contents of the resource or artifact.", + "type": "object" + }, + "content": { + "description": "The contents of the resource or artifact.", + "type": "string" + }, + "downloadLocation": { + "description": "The location of the described resource or artifact, if different from the uri.", + "type": "string" + }, + "mediaType": { + "description": "The MIME Type (i.e., media type) of the described resource or artifact.", + "type": "string" + }, + "annotations": { + "description": "This field MAY be used to provide additional information or metadata about the resource or artifact that may be useful to the consumer when evaluating the attestation against a policy.", + "type": "object" + } + } + } + } +} diff --git a/tests/test_sbomnix.py b/tests/test_sbomnix.py index eca1d66..d7d0048 100644 --- a/tests/test_sbomnix.py +++ b/tests/test_sbomnix.py @@ -6,7 +6,7 @@ # pylint: disable=invalid-name, global-statement, redefined-outer-name # pylint: disable=too-few-public-methods -""" Tests for sbomnix """ +"""Tests for sbomnix""" import os import subprocess @@ -46,6 +46,7 @@ SBOMNIX = SRCDIR / "sbomnix" / "main.py" NIXGRAPH = SRCDIR / "nixgraph" / "main.py" NIXMETA = SRCDIR / "nixmeta" / "main.py" +PROVENANCE = SRCDIR / "provenance" / "main.py" NIX_OUTDATED = SRCDIR / "nixupdate" / "nix_outdated.py" VULNXSCAN = SRCDIR / "vulnxscan" / "vulnxscan_cli.py" REPOLOGY_CLI = SRCDIR / "repology" / "repology_cli.py" @@ -657,6 +658,49 @@ def test_whitelist(): ################################################################################ +def test_provenance_help(): + """Test provenance command line argument: '-h'""" + _run_python_script([PROVENANCE, "-h"]) + + +def test_provenance_schema(): + """Test provenance generates valid schema""" + out_path = TEST_WORK_DIR / "provenance_test.json" + _run_python_script( + [ + PROVENANCE, + TEST_NIX_RESULT, + "--out", + out_path.as_posix(), + ] + ) + assert out_path.exists() + schema_path = MYDIR / "resources" / "provenance-1.0.schema.json" + assert schema_path.exists() + validate_json(out_path.as_posix(), schema_path) + + +def test_provenance_schema_recursive(): + """Test provenance generates valid schema with recursive option""" + out_path = TEST_WORK_DIR / "recursive_provenance_test.json" + _run_python_script( + [ + PROVENANCE, + TEST_NIX_RESULT, + "--recursive", + "--out", + out_path.as_posix(), + ] + ) + assert out_path.exists() + schema_path = MYDIR / "resources" / "provenance-1.0.schema.json" + assert schema_path.exists() + validate_json(out_path.as_posix(), schema_path) + + +################################################################################ + + class JSONSchemaRetrieve: """Cached retriever that can be used with jsonschema.validate"""