136 lines
5.1 KiB
YAML
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
|