Publish Docker images to both Docker Hub (docker.io/wikid82/charon) and GitHub Container Registry (ghcr.io/wikid82/charon) for maximum reach. Add Docker Hub login with secret existence check for graceful fallback Update docker/metadata-action to generate tags for both registries Add Cosign keyless signing for both GHCR and Docker Hub images Attach SBOM to Docker Hub via cosign attach sbom Add Docker Hub signature verification to supply-chain-verify workflow Update README with Docker Hub badges and dual registry examples Update getting-started.md with both registry options Supply chain security maintained: identical tags, signatures, and SBOMs on both registries. PR images remain GHCR-only.
825 lines
32 KiB
YAML
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
|