Gaps fill up, fixes, ui restructuring
This commit is contained in:
167
.gitea/workflows/templates/vex-mapping-check.yml
Normal file
167
.gitea/workflows/templates/vex-mapping-check.yml
Normal file
@@ -0,0 +1,167 @@
|
||||
# =============================================================================
|
||||
# vex-mapping-check.yml
|
||||
# Sprint: SPRINT_20260219_011 (CIAP-03)
|
||||
# Description: Validates VEX documents and verifies target artifact matching
|
||||
# =============================================================================
|
||||
#
|
||||
# This workflow validates OpenVEX or CycloneDX VEX documents against their
|
||||
# schemas, asserts required fields are present and valid, and optionally
|
||||
# verifies target artifact matches a known canonical_id.
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
name: VEX Mapping Check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
vex_path:
|
||||
description: 'Path to VEX document (JSON)'
|
||||
required: true
|
||||
type: string
|
||||
vex_format:
|
||||
description: 'VEX format: openvex or cyclonedx'
|
||||
required: false
|
||||
type: string
|
||||
default: 'openvex'
|
||||
canonical_id:
|
||||
description: 'Expected canonical_id of the target artifact (optional)'
|
||||
required: false
|
||||
type: string
|
||||
schema_path:
|
||||
description: 'Path to VEX JSON schema (optional, uses bundled schemas by default)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
validate-vex:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate VEX schema
|
||||
id: validate
|
||||
run: |
|
||||
VEX_FILE="${{ inputs.vex_path }}"
|
||||
FORMAT="${{ inputs.vex_format }}"
|
||||
|
||||
# Select schema
|
||||
if [ -n "${{ inputs.schema_path }}" ]; then
|
||||
SCHEMA="${{ inputs.schema_path }}"
|
||||
elif [ "${FORMAT}" = "openvex" ]; then
|
||||
SCHEMA="docs/schemas/openvex-0.2.0.schema.json"
|
||||
else
|
||||
SCHEMA="docs/schemas/cyclonedx-bom-1.7.schema.json"
|
||||
fi
|
||||
|
||||
# Validate
|
||||
if [ -f "${SCHEMA}" ]; then
|
||||
npx ajv-cli validate -s "${SCHEMA}" -d "${VEX_FILE}" && {
|
||||
echo "Schema validation: PASS" >> $GITHUB_STEP_SUMMARY
|
||||
} || {
|
||||
echo "Schema validation: FAIL" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "Schema file not found: ${SCHEMA}, skipping schema validation" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Assert required VEX fields
|
||||
run: |
|
||||
FORMAT="${{ inputs.vex_format }}"
|
||||
VEX_FILE="${{ inputs.vex_path }}"
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
with open('${VEX_FILE}') as f:
|
||||
vex = json.load(f)
|
||||
|
||||
errors = []
|
||||
format_name = '${FORMAT}'
|
||||
|
||||
if format_name == 'openvex':
|
||||
# OpenVEX validation
|
||||
if 'statements' not in vex:
|
||||
errors.append('Missing required field: statements')
|
||||
else:
|
||||
for i, stmt in enumerate(vex['statements']):
|
||||
if 'status' not in stmt:
|
||||
errors.append(f'Statement [{i}]: missing status')
|
||||
elif stmt['status'] not in ('affected', 'not_affected', 'fixed', 'under_investigation'):
|
||||
errors.append(f'Statement [{i}]: invalid status: {stmt[\"status\"]}')
|
||||
if 'vulnerability' not in stmt:
|
||||
errors.append(f'Statement [{i}]: missing vulnerability')
|
||||
if 'product' not in stmt and 'products' not in stmt:
|
||||
errors.append(f'Statement [{i}]: missing product or products')
|
||||
else:
|
||||
# CycloneDX VEX (embedded in SBOM vulnerabilities)
|
||||
vulns = vex.get('vulnerabilities', [])
|
||||
if not vulns:
|
||||
errors.append('No vulnerabilities found in CycloneDX VEX')
|
||||
for i, vuln in enumerate(vulns):
|
||||
analysis = vuln.get('analysis', {})
|
||||
state = analysis.get('state')
|
||||
if not state:
|
||||
errors.append(f'Vulnerability [{i}] ({vuln.get(\"id\",\"?\")}): missing analysis.state')
|
||||
elif state not in ('resolved', 'resolved_with_pedigree', 'exploitable', 'in_triage', 'false_positive', 'not_affected'):
|
||||
errors.append(f'Vulnerability [{i}]: invalid analysis.state: {state}')
|
||||
|
||||
if errors:
|
||||
print('VEX field validation FAILED:', file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f' - {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f'VEX field validation: PASS ({format_name})')
|
||||
"
|
||||
|
||||
echo "VEX field assertions: PASS" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Verify target canonical_id match
|
||||
if: inputs.canonical_id != ''
|
||||
run: |
|
||||
FORMAT="${{ inputs.vex_format }}"
|
||||
VEX_FILE="${{ inputs.vex_path }}"
|
||||
EXPECTED_ID="${{ inputs.canonical_id }}"
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
with open('${VEX_FILE}') as f:
|
||||
vex = json.load(f)
|
||||
|
||||
expected = '${EXPECTED_ID}'
|
||||
format_name = '${FORMAT}'
|
||||
found_match = False
|
||||
|
||||
if format_name == 'openvex':
|
||||
for stmt in vex.get('statements', []):
|
||||
product = stmt.get('product', '')
|
||||
products = stmt.get('products', [])
|
||||
targets = [product] if product else products
|
||||
for t in targets:
|
||||
pid = t if isinstance(t, str) else t.get('@id', '')
|
||||
if expected.replace('sha256:', '') in pid or pid == expected:
|
||||
found_match = True
|
||||
break
|
||||
else:
|
||||
# CycloneDX: check affects refs
|
||||
for vuln in vex.get('vulnerabilities', []):
|
||||
for affects in vuln.get('affects', []):
|
||||
ref = affects.get('ref', '')
|
||||
if expected.replace('sha256:', '') in ref:
|
||||
found_match = True
|
||||
break
|
||||
|
||||
if not found_match:
|
||||
print(f'WARNING: canonical_id {expected} not found in VEX targets', file=sys.stderr)
|
||||
print('This may indicate the VEX document does not apply to the expected artifact')
|
||||
# Warning only, not a hard failure
|
||||
else:
|
||||
print(f'Target canonical_id match: PASS')
|
||||
"
|
||||
|
||||
echo "Target artifact check: completed" >> $GITHUB_STEP_SUMMARY
|
||||
Reference in New Issue
Block a user