Gaps fill up, fixes, ui restructuring
This commit is contained in:
135
.gitea/workflows/templates/sbom-canonicalization-check.yml
Normal file
135
.gitea/workflows/templates/sbom-canonicalization-check.yml
Normal file
@@ -0,0 +1,135 @@
|
||||
# =============================================================================
|
||||
# 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
|
||||
Reference in New Issue
Block a user