Files
Charon/.github/workflows/supply-chain-verify.yml
2026-01-26 19:22:05 +00:00

825 lines
32 KiB
YAML

name: Supply Chain Verification
on:
release:
types: [published]
# Triggered after docker-build workflow completes
# Note: workflow_run can only chain 3 levels deep; we're at level 2 (safe)
#
# IMPORTANT: No branches filter here by design
# GitHub Actions limitation: branches filter in workflow_run only matches the default branch.
# Without a filter, this workflow triggers for ALL branches where docker-build completes,
# providing proper supply chain verification coverage for feature branches and PRs.
# Security: The workflow file must exist on the branch to execute, preventing untrusted code.
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
schedule:
# Run weekly on Mondays at 00:00 UTC
- cron: '0 0 * * 1'
workflow_dispatch:
permissions:
contents: read
packages: read
id-token: write # OIDC token for keyless verification
attestations: write # Create/verify attestations
security-events: write
pull-requests: write # Comment on PRs
jobs:
verify-sbom:
name: Verify SBOM
runs-on: ubuntu-latest
# Only run on scheduled scans for main branch, or if workflow_run completed successfully
# Critical Fix #5: Exclude PR builds to prevent duplicate verification (now handled inline in docker-build.yml)
if: |
(github.event_name != 'schedule' || github.ref == 'refs/heads/main') &&
(github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request'))
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Debug: Log workflow_run context for initial validation (can be removed after confidence)
- name: Debug Workflow Run Context
if: github.event_name == 'workflow_run'
run: |
echo "Workflow Run Event Details:"
echo " Workflow: ${{ github.event.workflow_run.name }}"
echo " Conclusion: ${{ github.event.workflow_run.conclusion }}"
echo " Head Branch: ${{ github.event.workflow_run.head_branch }}"
echo " Head SHA: ${{ github.event.workflow_run.head_sha }}"
echo " Event: ${{ github.event.workflow_run.event }}"
echo " PR Count: ${{ toJson(github.event.workflow_run.pull_requests) }}"
- name: Install Verification Tools
run: |
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Determine Image Tag
id: tag
run: |
if [[ "${{ github.event_name }}" == "release" ]]; then
TAG="${{ github.event.release.tag_name }}"
elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then
BRANCH="${{ github.event.workflow_run.head_branch }}"
# Extract tag from the workflow that triggered us
if [[ "${BRANCH}" == "main" ]]; then
TAG="latest"
elif [[ "${BRANCH}" == "development" ]]; then
TAG="dev"
elif [[ "${BRANCH}" == "nightly" ]]; then
TAG="nightly"
elif [[ "${{ github.event.workflow_run.event }}" == "pull_request" ]]; then
# Extract PR number from workflow_run context with null handling
PR_NUMBER=$(jq -r '.pull_requests[0].number // empty' <<< '${{ toJson(github.event.workflow_run.pull_requests) }}')
if [[ -n "${PR_NUMBER}" ]]; then
TAG="pr-${PR_NUMBER}"
else
# Fallback to SHA-based tag if PR number not available
TAG="sha-$(echo ${{ github.event.workflow_run.head_sha }} | cut -c1-7)"
fi
else
# For feature branches and other pushes, sanitize branch name for Docker tag
# Replace / with - to avoid invalid reference format errors
TAG=$(echo "${BRANCH}" | tr '/' '-')
fi
else
TAG="latest"
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Determined image tag: ${TAG}"
- name: Check Image Availability
id: image-check
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Checking if image exists: ${IMAGE}"
# Authenticate with GHCR using GitHub token
echo "${GH_TOKEN}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
if docker manifest inspect ${IMAGE} >/dev/null 2>&1; then
echo "✅ Image exists and is accessible"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "⚠️ Image not found - likely not built yet"
echo "This is normal for PR workflows before docker-build completes"
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Verify SBOM Completeness
if: steps.image-check.outputs.exists == 'true'
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Verifying SBOM for ${IMAGE}..."
echo ""
# Log Syft version for debugging
echo "Syft version:"
syft version
echo ""
# Generate fresh SBOM in CycloneDX format (aligned with docker-build.yml)
echo "Generating SBOM in CycloneDX JSON format..."
if ! syft ${IMAGE} -o cyclonedx-json > sbom-generated.json; then
echo "❌ Failed to generate SBOM"
echo ""
echo "Debug information:"
echo "Image: ${IMAGE}"
echo "Syft exit code: $?"
exit 1 # Fail on real errors, not silent exit
fi
# Check SBOM content
GENERATED_COUNT=$(jq '.components | length' sbom-generated.json 2>/dev/null || echo "0")
echo "Generated SBOM components: ${GENERATED_COUNT}"
if [[ ${GENERATED_COUNT} -eq 0 ]]; then
echo "⚠️ SBOM contains no components - may indicate an issue"
else
echo "✅ SBOM contains ${GENERATED_COUNT} components"
fi
- name: Upload SBOM Artifact
if: steps.image-check.outputs.exists == 'true' && always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: sbom-${{ steps.tag.outputs.tag }}
path: sbom-generated.json
retention-days: 30
- name: Validate SBOM File
id: validate-sbom
if: steps.image-check.outputs.exists == 'true'
run: |
echo "Validating SBOM file..."
echo ""
# Check jq availability
if ! command -v jq &> /dev/null; then
echo "❌ jq is not available"
echo "valid=false" >> $GITHUB_OUTPUT
exit 1
fi
# Check file exists
if [[ ! -f sbom-generated.json ]]; then
echo "❌ SBOM file does not exist"
echo "valid=false" >> $GITHUB_OUTPUT
exit 0
fi
# Check file is non-empty
if [[ ! -s sbom-generated.json ]]; then
echo "❌ SBOM file is empty"
echo "valid=false" >> $GITHUB_OUTPUT
exit 0
fi
# Validate JSON structure
if ! jq empty sbom-generated.json 2>/dev/null; then
echo "❌ SBOM file contains invalid JSON"
echo "SBOM content:"
cat sbom-generated.json
echo "valid=false" >> $GITHUB_OUTPUT
exit 0
fi
# Validate CycloneDX structure
BOMFORMAT=$(jq -r '.bomFormat // "missing"' sbom-generated.json)
SPECVERSION=$(jq -r '.specVersion // "missing"' sbom-generated.json)
COMPONENTS=$(jq '.components // [] | length' sbom-generated.json)
echo "SBOM Format: ${BOMFORMAT}"
echo "Spec Version: ${SPECVERSION}"
echo "Components: ${COMPONENTS}"
echo ""
if [[ "${BOMFORMAT}" != "CycloneDX" ]]; then
echo "❌ Invalid bomFormat: expected 'CycloneDX', got '${BOMFORMAT}'"
echo "valid=false" >> $GITHUB_OUTPUT
exit 0
fi
if [[ "${COMPONENTS}" == "0" ]]; then
echo "⚠️ SBOM has no components - may indicate incomplete scan"
echo "valid=partial" >> $GITHUB_OUTPUT
else
echo "✅ SBOM is valid with ${COMPONENTS} components"
echo "valid=true" >> $GITHUB_OUTPUT
fi
- name: Scan for Vulnerabilities
if: steps.validate-sbom.outputs.valid == 'true'
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
run: |
echo "Scanning for vulnerabilities with Grype..."
echo "SBOM format: CycloneDX JSON"
echo "SBOM size: $(wc -c < sbom-generated.json) bytes"
echo ""
# Update Grype vulnerability database
echo "Updating Grype vulnerability database..."
grype db update
echo ""
# Run Grype with explicit path and better error handling
if ! grype sbom:./sbom-generated.json --output json --file vuln-scan.json; then
echo ""
echo "❌ Grype scan failed"
echo ""
echo "Debug information:"
echo "Grype version:"
grype version
echo ""
echo "SBOM preview (first 1000 characters):"
head -c 1000 sbom-generated.json
echo ""
exit 1 # Fail the step to surface the issue
fi
echo "✅ Grype scan completed successfully"
echo ""
# Display human-readable results
echo "Vulnerability summary:"
grype sbom:./sbom-generated.json --output table || true
# Parse and categorize results
CRITICAL=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' vuln-scan.json 2>/dev/null || echo "0")
HIGH=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' vuln-scan.json 2>/dev/null || echo "0")
MEDIUM=$(jq '[.matches[] | select(.vulnerability.severity == "Medium")] | length' vuln-scan.json 2>/dev/null || echo "0")
LOW=$(jq '[.matches[] | select(.vulnerability.severity == "Low")] | length' vuln-scan.json 2>/dev/null || echo "0")
echo ""
echo "Vulnerability counts:"
echo " Critical: ${CRITICAL}"
echo " High: ${HIGH}"
echo " Medium: ${MEDIUM}"
echo " Low: ${LOW}"
# Set warnings for critical vulnerabilities
if [[ ${CRITICAL} -gt 0 ]]; then
echo "::warning::${CRITICAL} critical vulnerabilities found"
fi
# Store for PR comment
echo "CRITICAL_VULNS=${CRITICAL}" >> $GITHUB_ENV
echo "HIGH_VULNS=${HIGH}" >> $GITHUB_ENV
echo "MEDIUM_VULNS=${MEDIUM}" >> $GITHUB_ENV
echo "LOW_VULNS=${LOW}" >> $GITHUB_ENV
- name: Parse Vulnerability Details
if: steps.validate-sbom.outputs.valid == 'true'
run: |
echo "Parsing detailed vulnerability information..."
# Generate detailed vulnerability tables grouped by severity
# Limit to first 20 per severity to keep PR comment readable
# Critical vulnerabilities
jq -r '
[.matches[] | select(.vulnerability.severity == "Critical")] |
sort_by(.vulnerability.id) |
limit(20; .[]) |
"| \(.vulnerability.id) | \(.artifact.name) | \(.artifact.version) | \(.vulnerability.fix.versions[0] // "No fix available") | \(.vulnerability.description[0:80] // "N/A") |"
' vuln-scan.json > critical-vulns.txt
# High severity vulnerabilities
jq -r '
[.matches[] | select(.vulnerability.severity == "High")] |
sort_by(.vulnerability.id) |
limit(20; .[]) |
"| \(.vulnerability.id) | \(.artifact.name) | \(.artifact.version) | \(.vulnerability.fix.versions[0] // "No fix available") | \(.vulnerability.description[0:80] // "N/A") |"
' vuln-scan.json > high-vulns.txt
# Medium severity vulnerabilities
jq -r '
[.matches[] | select(.vulnerability.severity == "Medium")] |
sort_by(.vulnerability.id) |
limit(20; .[]) |
"| \(.vulnerability.id) | \(.artifact.name) | \(.artifact.version) | \(.vulnerability.fix.versions[0] // "No fix available") | \(.vulnerability.description[0:80] // "N/A") |"
' vuln-scan.json > medium-vulns.txt
# Low severity vulnerabilities
jq -r '
[.matches[] | select(.vulnerability.severity == "Low")] |
sort_by(.vulnerability.id) |
limit(20; .[]) |
"| \(.vulnerability.id) | \(.artifact.name) | \(.artifact.version) | \(.vulnerability.fix.versions[0] // "No fix available") | \(.vulnerability.description[0:80] // "N/A") |"
' vuln-scan.json > low-vulns.txt
echo "✅ Vulnerability details parsed and saved"
- name: Upload Vulnerability Scan Artifact
if: steps.validate-sbom.outputs.valid == 'true' && always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: vulnerability-scan-${{ steps.tag.outputs.tag }}
path: |
vuln-scan.json
critical-vulns.txt
high-vulns.txt
medium-vulns.txt
low-vulns.txt
retention-days: 30
- name: Report Skipped Scan
if: steps.image-check.outputs.exists != 'true' || steps.validate-sbom.outputs.valid != 'true'
run: |
echo "## ⚠️ Vulnerability Scan Skipped" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ steps.image-check.outputs.exists }}" != "true" ]]; then
echo "**Reason**: Docker image not available yet" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This is expected for PR workflows. The image will be scanned" >> $GITHUB_STEP_SUMMARY
echo "after it's built by the docker-build workflow." >> $GITHUB_STEP_SUMMARY
elif [[ "${{ steps.validate-sbom.outputs.valid }}" != "true" ]]; then
echo "**Reason**: SBOM validation failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check the 'Validate SBOM File' step for details." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ Workflow completed successfully (scan skipped)" >> $GITHUB_STEP_SUMMARY
- name: Determine PR Number
id: pr-number
if: |
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
result-encoding: string
script: |
// Determine PR number from context
let prNumber;
if (context.eventName === 'pull_request') {
prNumber = context.issue.number;
} else if (context.eventName === 'workflow_run') {
const pullRequests = context.payload.workflow_run.pull_requests;
if (pullRequests && pullRequests.length > 0) {
prNumber = pullRequests[0].number;
}
}
if (!prNumber) {
console.log('No PR number found');
return '';
}
console.log(`Found PR number: ${prNumber}`);
return prNumber;
- name: Build PR Comment Body
id: comment-body
if: steps.pr-number.outputs.result != ''
run: |
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
IMAGE_EXISTS="${{ steps.image-check.outputs.exists }}"
SBOM_VALID="${{ steps.validate-sbom.outputs.valid }}"
CRITICAL="${CRITICAL_VULNS:-0}"
HIGH="${HIGH_VULNS:-0}"
MEDIUM="${MEDIUM_VULNS:-0}"
LOW="${LOW_VULNS:-0}"
TOTAL=$((CRITICAL + HIGH + MEDIUM + LOW))
# Build comment body
COMMENT_BODY="## 🔒 Supply Chain Security Scan
**Last Updated**: ${TIMESTAMP}
**Workflow Run**: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
---
"
if [[ "${IMAGE_EXISTS}" != "true" ]]; then
COMMENT_BODY+="### ⏳ Status: Waiting for Image
The Docker image has not been built yet. This scan will run automatically once the docker-build workflow completes.
_This is normal for PR workflows._
"
elif [[ "${SBOM_VALID}" != "true" ]]; then
COMMENT_BODY+="### ⚠️ Status: SBOM Validation Failed
The Software Bill of Materials (SBOM) could not be validated. Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
**Action Required**: Review and resolve SBOM generation issues.
"
else
# Scan completed successfully
if [[ ${TOTAL} -eq 0 ]]; then
COMMENT_BODY+="### ✅ Status: No Vulnerabilities Detected
🎉 Great news! No security vulnerabilities were found in this image.
| Severity | Count |
|----------|-------|
| 🔴 Critical | 0 |
| 🟠 High | 0 |
| 🟡 Medium | 0 |
| 🔵 Low | 0 |
"
else
# Vulnerabilities found
if [[ ${CRITICAL} -gt 0 ]]; then
COMMENT_BODY+="### 🚨 Status: Critical Vulnerabilities Detected
⚠️ **Action Required**: ${CRITICAL} critical vulnerabilities require immediate attention!
"
elif [[ ${HIGH} -gt 0 ]]; then
COMMENT_BODY+="### ⚠️ Status: High-Severity Vulnerabilities Detected
${HIGH} high-severity vulnerabilities found. Please review and address.
"
else
COMMENT_BODY+="### 📊 Status: Vulnerabilities Detected
Security scan found ${TOTAL} vulnerabilities.
"
fi
COMMENT_BODY+="
| Severity | Count |
|----------|-------|
| 🔴 Critical | ${CRITICAL} |
| 🟠 High | ${HIGH} |
| 🟡 Medium | ${MEDIUM} |
| 🔵 Low | ${LOW} |
| **Total** | **${TOTAL}** |
## 🔍 Detailed Findings
"
# Add detailed vulnerability tables by severity
# Critical Vulnerabilities
if [[ ${CRITICAL} -gt 0 ]]; then
COMMENT_BODY+="<details>
<summary>🔴 <b>Critical Vulnerabilities (${CRITICAL})</b></summary>
| CVE | Package | Current Version | Fixed Version | Description |
|-----|---------|----------------|---------------|-------------|
"
if [[ -f critical-vulns.txt && -s critical-vulns.txt ]]; then
# Count lines in the file
CRIT_COUNT=$(wc -l < critical-vulns.txt)
COMMENT_BODY+="$(cat critical-vulns.txt)"
# If more than 20, add truncation message
if [[ ${CRITICAL} -gt 20 ]]; then
REMAINING=$((CRITICAL - 20))
COMMENT_BODY+="
_...and ${REMAINING} more. View the [full scan results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for complete details._
"
fi
else
COMMENT_BODY+="| N/A | N/A | N/A | N/A | Details unavailable |
"
fi
COMMENT_BODY+="
</details>
"
fi
# High Severity Vulnerabilities
if [[ ${HIGH} -gt 0 ]]; then
COMMENT_BODY+="<details>
<summary>🟠 <b>High Severity Vulnerabilities (${HIGH})</b></summary>
| CVE | Package | Current Version | Fixed Version | Description |
|-----|---------|----------------|---------------|-------------|
"
if [[ -f high-vulns.txt && -s high-vulns.txt ]]; then
COMMENT_BODY+="$(cat high-vulns.txt)"
if [[ ${HIGH} -gt 20 ]]; then
REMAINING=$((HIGH - 20))
COMMENT_BODY+="
_...and ${REMAINING} more. View the [full scan results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for complete details._
"
fi
else
COMMENT_BODY+="| N/A | N/A | N/A | N/A | Details unavailable |
"
fi
COMMENT_BODY+="
</details>
"
fi
# Medium Severity Vulnerabilities
if [[ ${MEDIUM} -gt 0 ]]; then
COMMENT_BODY+="<details>
<summary>🟡 <b>Medium Severity Vulnerabilities (${MEDIUM})</b></summary>
| CVE | Package | Current Version | Fixed Version | Description |
|-----|---------|----------------|---------------|-------------|
"
if [[ -f medium-vulns.txt && -s medium-vulns.txt ]]; then
COMMENT_BODY+="$(cat medium-vulns.txt)"
if [[ ${MEDIUM} -gt 20 ]]; then
REMAINING=$((MEDIUM - 20))
COMMENT_BODY+="
_...and ${REMAINING} more. View the [full scan results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for complete details._
"
fi
else
COMMENT_BODY+="| N/A | N/A | N/A | N/A | Details unavailable |
"
fi
COMMENT_BODY+="
</details>
"
fi
# Low Severity Vulnerabilities
if [[ ${LOW} -gt 0 ]]; then
COMMENT_BODY+="<details>
<summary>🔵 <b>Low Severity Vulnerabilities (${LOW})</b></summary>
| CVE | Package | Current Version | Fixed Version | Description |
|-----|---------|----------------|---------------|-------------|
"
if [[ -f low-vulns.txt && -s low-vulns.txt ]]; then
COMMENT_BODY+="$(cat low-vulns.txt)"
if [[ ${LOW} -gt 20 ]]; then
REMAINING=$((LOW - 20))
COMMENT_BODY+="
_...and ${REMAINING} more. View the [full scan results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for complete details._
"
fi
else
COMMENT_BODY+="| N/A | N/A | N/A | N/A | Details unavailable |
"
fi
COMMENT_BODY+="
</details>
"
fi
COMMENT_BODY+="
📋 [View detailed vulnerability report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
"
fi
fi
COMMENT_BODY+="
---
<sub><!-- supply-chain-security-comment --></sub>
"
# Save to file for the next step (handles multi-line)
echo "$COMMENT_BODY" > /tmp/comment-body.txt
# Also output for debugging
echo "Generated comment body:"
cat /tmp/comment-body.txt
- name: Update or Create PR Comment
if: steps.pr-number.outputs.result != ''
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ steps.pr-number.outputs.result }}
body-path: /tmp/comment-body.txt
edit-mode: replace
comment-author: 'github-actions[bot]'
body-includes: '<!-- supply-chain-security-comment -->'
verify-docker-image:
name: Verify Docker Image Supply Chain
runs-on: ubuntu-latest
if: github.event_name == 'release'
needs: verify-sbom
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Verification Tools
run: |
# Install Cosign
curl -sLO https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64
echo "4e84f155f98be2c2d3e63dea0e80b0ca5b4d843f5f4b1d3e8c9b7e4e7c0e0e0e cosign-linux-amd64" | sha256sum -c || {
echo "⚠️ Checksum verification skipped (update with actual hash)"
}
sudo install cosign-linux-amd64 /usr/local/bin/cosign
rm cosign-linux-amd64
# Install SLSA Verifier
curl -sLO https://github.com/slsa-framework/slsa-verifier/releases/download/v2.6.0/slsa-verifier-linux-amd64
sudo install slsa-verifier-linux-amd64 /usr/local/bin/slsa-verifier
rm slsa-verifier-linux-amd64
- name: Determine Image Tag
id: tag
run: |
TAG="${{ github.event.release.tag_name }}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Verify Cosign Signature with Rekor Fallback
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
run: |
echo "Verifying Cosign signature for ${IMAGE}..."
# Try with Rekor
if cosign verify ${IMAGE} \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" 2>&1; then
echo "✅ Cosign signature verified (with Rekor)"
else
echo "⚠️ Rekor verification failed, trying offline verification..."
# Fallback: verify without Rekor
if cosign verify ${IMAGE} \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--insecure-ignore-tlog 2>&1; then
echo "✅ Cosign signature verified (offline mode)"
echo "::warning::Verified without Rekor - transparency log unavailable"
else
echo "❌ Signature verification failed"
exit 1
fi
fi
- name: Verify Docker Hub Image Signature
if: steps.image-check.outputs.exists == 'true'
continue-on-error: true
run: |
echo "Verifying Docker Hub image signature..."
cosign verify docker.io/wikid82/charon:${{ steps.tag.outputs.tag }} \
--certificate-identity-regexp="https://github.com/Wikid82/Charon" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" && \
echo "✅ Docker Hub signature verified" || \
echo "⚠️ Docker Hub signature verification failed (image may not exist or not signed)"
- name: Verify SLSA Provenance
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Verifying SLSA provenance for ${IMAGE}..."
# This will be enabled once provenance generation is added
echo "⚠️ SLSA provenance verification not yet implemented"
echo "Will be enabled after Phase 3 workflow updates"
- name: Create Verification Report
if: always()
run: |
cat << EOF > verification-report.md
# Supply Chain Verification Report
**Image**: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
**Date**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
**Workflow**: ${{ github.workflow }}
**Run**: ${{ github.run_id }}
## Results
- **SBOM Verification**: ${{ needs.verify-sbom.result }}
- **Cosign Signature**: ${{ job.status }}
- **SLSA Provenance**: Not yet implemented (Phase 3)
## Verification Failure Recovery
If verification failed:
1. Check workflow logs for detailed error messages
2. Verify signing steps ran successfully in build workflow
3. Confirm attestations were pushed to registry
4. Check Rekor status: https://status.sigstore.dev
5. For Rekor outages, manual verification may be required
6. Re-run build if signatures/provenance are missing
EOF
cat verification-report.md >> $GITHUB_STEP_SUMMARY
verify-release-artifacts:
name: Verify Release Artifacts
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Verification Tools
run: |
# Install Cosign
curl -sLO https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64
sudo install cosign-linux-amd64 /usr/local/bin/cosign
rm cosign-linux-amd64
- name: Download Release Assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG=${{ github.event.release.tag_name }}
mkdir -p ./release-assets
gh release download ${TAG} --dir ./release-assets || {
echo "⚠️ No release assets found or download failed"
exit 0
}
- name: Verify Artifact Signatures with Fallback
continue-on-error: true
run: |
if [[ ! -d ./release-assets ]] || [[ -z "$(ls -A ./release-assets 2>/dev/null)" ]]; then
echo "⚠️ No release assets to verify"
exit 0
fi
echo "Verifying Cosign signatures for release artifacts..."
VERIFIED_COUNT=0
FAILED_COUNT=0
for artifact in ./release-assets/*; do
# Skip signature and certificate files
if [[ "$artifact" == *.sig || "$artifact" == *.pem || "$artifact" == *provenance* || "$artifact" == *.txt || "$artifact" == *.md ]]; then
continue
fi
if [[ -f "$artifact" ]]; then
echo "Verifying: $(basename $artifact)"
# Check if signature files exist
if [[ ! -f "${artifact}.sig" ]] || [[ ! -f "${artifact}.pem" ]]; then
echo "⚠️ No signature files found for $(basename $artifact)"
FAILED_COUNT=$((FAILED_COUNT + 1))
continue
fi
# Try with Rekor
if cosign verify-blob "$artifact" \
--signature "${artifact}.sig" \
--certificate "${artifact}.pem" \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" 2>&1; then
echo "✅ Verified with Rekor"
VERIFIED_COUNT=$((VERIFIED_COUNT + 1))
else
echo "⚠️ Rekor unavailable, trying offline..."
if cosign verify-blob "$artifact" \
--signature "${artifact}.sig" \
--certificate "${artifact}.pem" \
--certificate-identity-regexp="https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--insecure-ignore-tlog 2>&1; then
echo "✅ Verified offline"
VERIFIED_COUNT=$((VERIFIED_COUNT + 1))
else
echo "❌ Verification failed"
FAILED_COUNT=$((FAILED_COUNT + 1))
fi
fi
fi
done
echo ""
echo "Verification summary: ${VERIFIED_COUNT} verified, ${FAILED_COUNT} failed"
if [[ ${FAILED_COUNT} -gt 0 ]]; then
echo "⚠️ Some artifacts failed verification"
else
echo "✅ All artifacts verified successfully"
fi