Files
git.stella-ops.org/.gitea/workflows/templates/sbom-canonicalization-check.yml
2026-02-19 22:10:54 +02:00

136 lines
5.1 KiB
YAML

# =============================================================================
# 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:<hex>)'
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