diff --git a/share/README.md b/share/README.md index 9313c1a..ce14f10 100644 --- a/share/README.md +++ b/share/README.md @@ -25,7 +25,7 @@ into a product at `tools/sbom/`, together with the pin files (`VERSION` and Every front end produces a composition input and a config input and hands them to the driver. -Composition (at least one): +Composition (pick one): - `--srcs-file PATH` — the source files compiled into the artifact (tier E). - `--lib PATH` — the built library to hash (tier R/L/S). diff --git a/share/build/sbom.cmake b/share/build/sbom.cmake index ae9b309..91d0408 100644 --- a/share/build/sbom.cmake +++ b/share/build/sbom.cmake @@ -18,6 +18,9 @@ # LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE # ) # +# Composition, one of: TARGETS ... (hash the source set), LIB +# (hash the built library), or NO_ARTIFACT_HASH alone. +# # Optional arguments: # SBOM_GEN Path to gen-sbom (default: driver auto-discovery). # GEN_SBOM Legacy alias for SBOM_GEN. @@ -117,13 +120,20 @@ function(wolfssl_compliance_tooling_add_sbom) if(NOT SB_NAME) message(FATAL_ERROR "wolfssl_compliance_tooling_add_sbom: NAME is required") endif() + if(SB_LIB MATCHES "-NOTFOUND$") + message(FATAL_ERROR + "wolfssl_compliance_tooling_add_sbom: LIB was not found (${SB_LIB})") + endif() if(SB_NO_ARTIFACT_HASH) - if(SB_TARGETS OR SB_LIB) + if(SB_TARGETS OR NOT "${SB_LIB}" STREQUAL "") message(FATAL_ERROR "wolfssl_compliance_tooling_add_sbom: NO_ARTIFACT_HASH stands " "alone. Do not pass TARGETS or LIB.") endif() - elseif(NOT SB_TARGETS AND NOT SB_LIB) + elseif(SB_TARGETS AND NOT "${SB_LIB}" STREQUAL "") + message(FATAL_ERROR + "wolfssl_compliance_tooling_add_sbom: set TARGETS or LIB, not both") + elseif(NOT SB_TARGETS AND "${SB_LIB}" STREQUAL "") message(FATAL_ERROR "wolfssl_compliance_tooling_add_sbom: set TARGETS or LIB") endif() if(NOT SB_ROOT) @@ -190,7 +200,7 @@ function(wolfssl_compliance_tooling_add_sbom) file(GENERATE OUTPUT ${_srcs_file} CONTENT "${_srcs_nl}\n") list(APPEND _cmd --srcs-file ${_srcs_file}) endif() - if(SB_LIB) + if(NOT "${SB_LIB}" STREQUAL "") list(APPEND _cmd --lib ${SB_LIB}) endif() endif() diff --git a/share/build/sbom.mk b/share/build/sbom.mk index 79882d4..c7d63cb 100644 --- a/share/build/sbom.mk +++ b/share/build/sbom.mk @@ -143,6 +143,7 @@ $(1): $($(2)PREREQS) else \ test -n "$(strip $($(2)SRCS))$($(2)LIB)" || \ { echo "ERROR: set $(2)SRCS or $(2)LIB"; exit 1; }; \ + $(if $(and $(strip $($(2)SRCS)),$($(2)LIB)),{ echo "ERROR: set $(2)SRCS or $(2)LIB but not both"; exit 1; };) \ fi @set -e; \ if [ -n "$(strip $($(2)SRCS))" ]; then \ diff --git a/share/gen-sbom b/share/gen-sbom index 312f074..a94cb9f 100755 --- a/share/gen-sbom +++ b/share/gen-sbom @@ -895,8 +895,7 @@ def parse_options_h(path): with open(path) as f: text = f.read() except OSError as e: - print(f"WARNING: cannot read options.h {path}: {e}", file=sys.stderr) - return [] + sys.exit(f"ERROR: cannot read --options-h {path}: {e}") defines = {} for m in re.finditer(r'^#define[ \t]+(\w+)(?:[ \t]+(.*))?$', text, re.MULTILINE): diff --git a/share/sbom-driver.py b/share/sbom-driver.py index 8837615..e5b2b35 100755 --- a/share/sbom-driver.py +++ b/share/sbom-driver.py @@ -262,7 +262,8 @@ def main(): description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - # Composition (at least one of --lib / --srcs-file; --source-only needs srcs). + # Composition (one of --lib / --srcs-file / --no-artifact-hash; + # --source-only needs srcs). ap.add_argument("--lib", default="", help="Path to the built library to hash (tier R/L/S).") ap.add_argument("--srcs-file", default="", @@ -367,7 +368,8 @@ def main(): elif args.lib and args.srcs_file: sys.exit("ERROR: pass one of --lib or --srcs-file.") elif not args.lib and not args.srcs_file: - sys.exit("ERROR: pass at least one of --lib or --srcs-file.") + sys.exit("ERROR: pass one of --lib, --srcs-file or " + "--no-artifact-hash.") if args.source_only and not args.srcs_file: sys.exit("ERROR: --source-only needs --srcs-file.") if args.srcs_file and not os.path.isfile(args.srcs_file): diff --git a/tests/test_gen_sbom.py b/tests/test_gen_sbom.py index acc08dc..d3e1ab3 100644 --- a/tests/test_gen_sbom.py +++ b/tests/test_gen_sbom.py @@ -605,6 +605,11 @@ def test_dedup_keeps_last_assignment(self): )) self.assertEqual(pairs['HAVE_X'], '2') + def test_missing_file_exits(self): + with self.assertRaises(SystemExit) as cm: + gs.parse_options_h('/no/such/options.h') + self.assertIn('/no/such/options.h', str(cm.exception)) + def test_filters_compiler_internals_from_dm_e_dump(self): # The no-pcpp escape hatch (`$CC -dM -E -include settings.h ...`) # produces a defines file containing hundreds of host/compiler diff --git a/tests/test_sbom.py b/tests/test_sbom.py index 8ac9285..761b98b 100755 --- a/tests/test_sbom.py +++ b/tests/test_sbom.py @@ -20,6 +20,7 @@ import importlib.util import json import os +import shutil import subprocess import sys import tempfile @@ -267,6 +268,22 @@ def unit_tests(): capture_output=True, text=True) check(r.returncode != 0 and "stands alone" in r.stderr, "no-artifact-hash rejects --srcs-file") + r = subprocess.run(base, capture_output=True, text=True) + check(r.returncode != 0 + and "--lib, --srcs-file or --no-artifact-hash" in r.stderr, + "a missing checksum source names all three options") + + make = shutil.which("make") + if make: + with open(os.path.join(d, "Makefile"), "w") as f: + f.write("SBOM_NAME = t\nSBOM_VERSION = 1\n" + "SBOM_DRIVER = false\n" + f"include {os.path.join(SHARE, 'build', 'sbom.mk')}\n") + r = subprocess.run([make, "-C", d, "sbom", "SBOM_SRCS=a.c", + "SBOM_LIB=" + lib], + capture_output=True, text=True) + check(r.returncode != 0 and "but not both" in r.stdout + r.stderr, + "sbom.mk rejects SBOM_SRCS with SBOM_LIB") def find_gen_sbom(explicit): diff --git a/tests/test_sbom_cmake_install.py b/tests/test_sbom_cmake_install.py index 240a7dd..74c8d6e 100644 --- a/tests/test_sbom_cmake_install.py +++ b/tests/test_sbom_cmake_install.py @@ -237,6 +237,36 @@ def test_default_call_records_install_targets(self): self.assertNotIn("--dep-openssl", text) self.assertNotIn("--crypto-only", text) + def test_targets_with_lib_fails_at_configure(self): + for n, (targets, lib, error) in enumerate(( + ("TARGETS x", "${CMAKE_CURRENT_BINARY_DIR}/libx.a", + "set TARGETS or LIB, not both"), + ("TARGETS x", "FOO_LIB-NOTFOUND", "LIB was not found"), + ("", "FOO_LIB-NOTFOUND", "LIB was not found"))): + with self.subTest(targets=targets, lib=lib): + src = self.dir / f"src{n}" + src.mkdir() + (src / "x.c").write_text("int x(void) { return 1; }\n") + (src / "LICENSE").write_text("demo\n") + (src / "CMakeLists.txt").write_text(f""" +cmake_minimum_required(VERSION 3.16) +project(sbomtest C) +add_library(x STATIC x.c) +include("{BUILD / "sbom.cmake"}") +wolfssl_compliance_tooling_add_sbom( + NAME t + VERSION 1 + {targets} + LIB {lib} + LICENSE ${{CMAKE_CURRENT_SOURCE_DIR}}/LICENSE) +""") + result = run([ + CMAKE, "-S", str(src), "-B", str(self.dir / f"build{n}"), + "-G", "Unix Makefiles", + ]) + self.assertNotEqual(result.returncode, 0) + self.assertIn(error, result.stderr) + def test_wolftpm_style_install_depends_on_the_public_target(self): # The helper target is not the public sbom target. install-sbom # must wait for the target that writes the tag-value file.