# ============================================================================= # sbom-canonicalization-check.yml # Sprint: SPRINT_20260219_011 (CIAP-01) # Description: Validates CycloneDX SBOM and verifies canonical_id determinism # ============================================================================= # # This workflow validates an SBOM against the CycloneDX schema, computes # the canonical_id (sha256 of JCS-canonicalized JSON), and verifies # that canonicalization is deterministic across runs. # # Usage: # 1. Copy to your project's .gitea/workflows/ directory # 2. Set BOM_PATH to your SBOM output location # 3. Optionally set EXPECTED_CANONICAL_ID for regression testing # # ============================================================================= name: SBOM Canonicalization Check on: workflow_call: inputs: bom_path: description: 'Path to CycloneDX SBOM JSON file' required: true type: string expected_canonical_id: description: 'Expected canonical_id for regression testing (optional)' required: false type: string outputs: canonical_id: description: 'Computed canonical_id (sha256:)' value: ${{ jobs.canonicalize.outputs.canonical_id }} validation_result: description: 'Schema validation result (pass/fail)' value: ${{ jobs.canonicalize.outputs.validation_result }} jobs: canonicalize: runs-on: ubuntu-latest outputs: canonical_id: ${{ steps.compute.outputs.canonical_id }} validation_result: ${{ steps.validate.outputs.result }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Validate CycloneDX schema id: validate run: | # Validate SBOM against CycloneDX 1.7 schema if command -v sbom-utility &> /dev/null; then sbom-utility validate -i "${{ inputs.bom_path }}" --force if [ $? -eq 0 ]; then echo "result=pass" >> $GITHUB_OUTPUT echo "Schema validation: PASS" >> $GITHUB_STEP_SUMMARY else echo "result=fail" >> $GITHUB_OUTPUT echo "Schema validation: FAIL" >> $GITHUB_STEP_SUMMARY exit 1 fi else # Fallback: basic JSON validation with ajv npx ajv-cli validate -s docs/schemas/cyclonedx-bom-1.7.schema.json -d "${{ inputs.bom_path }}" || { echo "result=fail" >> $GITHUB_OUTPUT exit 1 } echo "result=pass" >> $GITHUB_OUTPUT fi - name: Compute canonical_id id: compute run: | # JCS canonicalize and compute SHA-256 # Uses Python for RFC 8785 compliance (json.loads + sorted keys + separators) CANONICAL_ID=$(python3 -c " import json, hashlib, sys with open('${{ inputs.bom_path }}', 'rb') as f: obj = json.load(f) canonical = json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8') digest = hashlib.sha256(canonical).hexdigest() print(f'sha256:{digest}') ") echo "canonical_id=${CANONICAL_ID}" >> $GITHUB_OUTPUT echo "### Canonical SBOM ID" >> $GITHUB_STEP_SUMMARY echo "\`${CANONICAL_ID}\`" >> $GITHUB_STEP_SUMMARY - name: Verify determinism (double-compute) run: | # Canonicalize twice, verify identical output FIRST=$(python3 -c " import json, hashlib with open('${{ inputs.bom_path }}', 'rb') as f: obj = json.load(f) canonical = json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8') print(hashlib.sha256(canonical).hexdigest()) ") SECOND=$(python3 -c " import json, hashlib with open('${{ inputs.bom_path }}', 'rb') as f: obj = json.load(f) canonical = json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8') print(hashlib.sha256(canonical).hexdigest()) ") if [ "${FIRST}" != "${SECOND}" ]; then echo "FATAL: Canonicalization is non-deterministic!" >&2 echo " Run 1: ${FIRST}" >&2 echo " Run 2: ${SECOND}" >&2 exit 1 fi echo "Determinism check: PASS (hash=${FIRST})" >> $GITHUB_STEP_SUMMARY - name: Regression check (if expected_canonical_id provided) if: inputs.expected_canonical_id != '' run: | ACTUAL="${{ steps.compute.outputs.canonical_id }}" EXPECTED="${{ inputs.expected_canonical_id }}" if [ "${ACTUAL}" != "${EXPECTED}" ]; then echo "REGRESSION: canonical_id changed!" >&2 echo " Expected: ${EXPECTED}" >&2 echo " Actual: ${ACTUAL}" >&2 echo "### Regression Detected" >> $GITHUB_STEP_SUMMARY echo "Expected: \`${EXPECTED}\`" >> $GITHUB_STEP_SUMMARY echo "Actual: \`${ACTUAL}\`" >> $GITHUB_STEP_SUMMARY exit 1 fi echo "Regression check: PASS" >> $GITHUB_STEP_SUMMARY