#!/bin/sh # Sanity checks on the example auditor packet. # # Mandatory: JSON parse, SPDX externalDocumentRef checksum, CycloneDX bom hash (if pinned). # Best-effort: CycloneDX 1.6 schema (cyclonedx-cli) and SPDX 2.3 schema (pyspdxtools) # validation, when those tools are installed locally. set -eu SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) KIT_DIR=$(dirname "$SCRIPT_DIR") AP="$KIT_DIR/auditor-packet" PRODUCT_CDX="$AP/product-acme-connect-gateway.cdx.json" PRODUCT_SPDX="$AP/product-acme-connect-gateway.spdx.json" fail() { echo "FAIL: $*" >&2; exit 1; } ok() { echo "OK: $*"; } command -v python3 >/dev/null 2>&1 || fail "python3 required" # shellcheck disable=SC1090,SC1091 . "$KIT_DIR/VERSION" 2>/dev/null || WOLFSSL_VERSION=5.9.1 WOLF_CDX="$AP/wolfssl-component/wolfssl-${WOLFSSL_VERSION}.cdx.json" WOLF_SPDX="$AP/wolfssl-component/wolfssl-${WOLFSSL_VERSION}.spdx.json" for f in "$PRODUCT_CDX" "$PRODUCT_SPDX" "$WOLF_CDX" "$WOLF_SPDX"; do [ -f "$f" ] || fail "missing $f" F="$f" python3 -c "import json, os; json.load(open(os.environ['F']))" || fail "invalid JSON: $f" ok "$(basename "$f") parses" done # CycloneDX 1.6 serialNumber must be a well-formed urn:uuid; auditors with strict # validators (cyclonedx-cli) reject anything else. We accept RFC 4122 versions 1-5: # the product SBOM uses a random v4, while the wolfSSL component SBOMs use a # deterministic name-based v5 so regenerated samples keep a stable serialNumber. # Catch malformed values even when the tool isn't installed. PRODUCT_CDX="$PRODUCT_CDX" WOLF_CDX="$WOLF_CDX" python3 <<'PY' import json, os, re, sys UUID = re.compile(r"^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.I) errors = [] for env in ("PRODUCT_CDX", "WOLF_CDX"): path = os.environ[env] sn = json.load(open(path)).get("serialNumber", "") if not UUID.match(sn): errors.append(f"{os.path.basename(path)}: serialNumber {sn!r} is not a urn:uuid:") if errors: sys.exit("CycloneDX serialNumber violation(s):\n " + "\n ".join(errors)) print("OK: CycloneDX serialNumbers are valid urn:uuid (v1-5)") PY PRODUCT_SPDX="$PRODUCT_SPDX" WOLF_SPDX="$WOLF_SPDX" python3 <<'PY' import hashlib, json, os, sys product = json.load(open(os.environ["PRODUCT_SPDX"])) wolf = open(os.environ["WOLF_SPDX"], "rb").read() digest = hashlib.sha256(wolf).hexdigest() refs = product.get("externalDocumentRefs") or [] if not refs: sys.exit("product SPDX has no externalDocumentRefs") chk = refs[0].get("checksum", {}).get("checksumValue", "") if chk.lower() != digest.lower(): sys.exit( f"SPDX checksum mismatch:\n embedded={chk}\n actual ={digest}\n" "Run scripts/refresh-samples.sh after regenerating wolfSSL SBOM." ) print("OK: product SPDX checksum matches wolfssl-component SBOM") PY PRODUCT_CDX="$PRODUCT_CDX" WOLF_CDX="$WOLF_CDX" python3 <<'PY' import hashlib, json, os, sys prod = json.load(open(os.environ["PRODUCT_CDX"])) wolf_bytes = open(os.environ["WOLF_CDX"], "rb").read() digest = hashlib.sha256(wolf_bytes).hexdigest() comps = prod.get("components") or [] wolf = next((c for c in comps if c.get("name") == "wolfssl"), None) if not wolf: sys.exit("product CDX has no wolfssl component") if not wolf.get("supplier", {}).get("name"): sys.exit("product CDX wolfssl component has no supplier (NTIA min-elements gap)") refs = wolf.get("externalReferences") or [] bom = next((r for r in refs if r.get("type") == "bom"), None) if not bom: sys.exit("wolfssl component has no bom externalReference") hashes = bom.get("hashes") or [] if not hashes: sys.exit("wolfssl component bom externalReference has no hashes (run refresh-samples.sh)") got = hashes[0].get("content", "").lower() if got == "to_be_pinned_by_refresh_samples": sys.exit("wolfssl component bom hash is the unpinned placeholder; run refresh-samples.sh") if got != digest.lower(): sys.exit( f"CycloneDX bom hash mismatch:\n embedded={got}\n actual ={digest}\n" "Run scripts/refresh-samples.sh after regenerating wolfSSL SBOM." ) print("OK: product CycloneDX bom hash matches wolfssl-component CDX") print("OK: product CycloneDX wolfssl component has supplier") PY # ---- Optional: embedded component SBOMs (if generated) -------------------- # scripts/generate-embedded-sbom.sh writes here; outputs are .gitignored so the # dir is usually empty in a fresh checkout. When present, parse them and confirm # the demo watermark so an embedded demo SBOM can't be mistaken for production. EMBEDDED_DIR="$AP/wolfssl-component-embedded" EMBEDDED_CDX="$EMBEDDED_DIR/wolfssl-${WOLFSSL_VERSION}.cdx.json" EMBEDDED_SPDX="$EMBEDDED_DIR/wolfssl-${WOLFSSL_VERSION}.spdx.json" if [ -f "$EMBEDDED_CDX" ] || [ -f "$EMBEDDED_SPDX" ]; then for f in "$EMBEDDED_CDX" "$EMBEDDED_SPDX"; do [ -f "$f" ] || continue F="$f" python3 -c "import json, os; json.load(open(os.environ['F']))" \ || fail "invalid JSON: $f" ok "$(basename "$f") (embedded) parses" done # The embedded dir is .gitignored scratch output, so a missing watermark is # a warning (the file may be a leftover or a direct gen-sbom run), not a hard # failure. generate-embedded-sbom.sh always watermarks its own output. if [ -f "$EMBEDDED_CDX" ]; then EMBEDDED_CDX="$EMBEDDED_CDX" python3 <<'PY' import json, os d = json.load(open(os.environ["EMBEDDED_CDX"])) props = d.get("metadata", {}).get("component", {}).get("properties", []) if any(p.get("name") == "wolfssl:sbom:demo" for p in props): print("OK: embedded CycloneDX SBOM carries the demo watermark") else: print("WARNING: embedded CycloneDX SBOM lacks the wolfssl:sbom:demo " "watermark; regenerate via scripts/generate-embedded-sbom.sh so it " "cannot be mistaken for a production-complete SBOM.") PY fi else echo "NOTE: no embedded SBOMs in $EMBEDDED_DIR; skipping embedded checks." echo " Generate with: ./scripts/generate-embedded-sbom.sh" fi # ---- Optional: cyclonedx-cli schema validation ---------------------------- CDX_TOOL= if command -v cyclonedx-cli >/dev/null 2>&1; then CDX_TOOL=cyclonedx-cli elif command -v cyclonedx >/dev/null 2>&1; then CDX_TOOL=cyclonedx fi if [ -n "$CDX_TOOL" ]; then for cdx in "$PRODUCT_CDX" "$WOLF_CDX"; do if "$CDX_TOOL" validate \ --input-file "$cdx" \ --input-format json \ --input-version v1_6 \ --fail-on-errors >/dev/null 2>&1; then ok "$(basename "$cdx") passes CycloneDX 1.6 schema validation ($CDX_TOOL)" else fail "$(basename "$cdx") fails CycloneDX 1.6 schema validation ($CDX_TOOL)" fi done else echo "NOTE: cyclonedx-cli not installed; skipping CycloneDX 1.6 schema validation." echo " Install: https://github.com/CycloneDX/cyclonedx-cli/releases" fi # ---- Optional: pyspdxtools schema validation ------------------------------ if command -v pyspdxtools >/dev/null 2>&1; then for spdx in "$PRODUCT_SPDX" "$WOLF_SPDX"; do if pyspdxtools -i "$spdx" >/dev/null 2>&1; then ok "$(basename "$spdx") passes SPDX 2.3 schema validation (pyspdxtools)" else fail "$(basename "$spdx") fails SPDX 2.3 schema validation (pyspdxtools)" fi done else echo "NOTE: pyspdxtools not installed; skipping SPDX 2.3 schema validation." echo " Install: pip install spdx-tools" fi ok "auditor packet validation passed"