chore: git cache cleanup
This commit is contained in:
827
.github/workflows/supply-chain-verify.yml
vendored
Normal file
827
.github/workflows/supply-chain-verify.yml
vendored
Normal file
@@ -0,0 +1,827 @@
|
||||
name: Supply Chain Verification
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1' # Mondays 00:00 UTC
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Docker Build, Publish & Test
|
||||
types:
|
||||
- completed
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- prereleased
|
||||
|
||||
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
|
||||
outputs:
|
||||
image_exists: ${{ steps.image-check.outputs.exists }}
|
||||
# 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.event != 'pull_request' &&
|
||||
(github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success')))
|
||||
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: 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
|
||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
if [[ "${BRANCH}" == "main" ]]; then
|
||||
TAG="latest"
|
||||
elif [[ "${BRANCH}" == "development" ]]; then
|
||||
TAG="dev"
|
||||
elif [[ "${BRANCH}" == "nightly" ]]; then
|
||||
TAG="nightly"
|
||||
else
|
||||
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
|
||||
|
||||
# Generate SBOM using official Anchore action (auto-updated by Renovate)
|
||||
- name: Generate and Verify SBOM
|
||||
if: steps.image-check.outputs.exists == 'true'
|
||||
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
|
||||
with:
|
||||
image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
|
||||
format: cyclonedx-json
|
||||
output-file: sbom-verify.cyclonedx.json
|
||||
|
||||
- name: Verify SBOM Completeness
|
||||
if: steps.image-check.outputs.exists == 'true'
|
||||
run: |
|
||||
echo "Verifying SBOM completeness..."
|
||||
echo ""
|
||||
|
||||
# Count components
|
||||
COMPONENT_COUNT=$(jq '.components | length' sbom-verify.cyclonedx.json 2>/dev/null || echo "0")
|
||||
|
||||
echo "SBOM components: ${COMPONENT_COUNT}"
|
||||
|
||||
if [[ ${COMPONENT_COUNT} -eq 0 ]]; then
|
||||
echo "⚠️ SBOM contains no components - may indicate an issue"
|
||||
else
|
||||
echo "✅ SBOM contains ${COMPONENT_COUNT} components"
|
||||
fi
|
||||
|
||||
- name: Upload SBOM Artifact
|
||||
if: steps.image-check.outputs.exists == 'true' && always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: sbom-${{ steps.tag.outputs.tag }}
|
||||
path: sbom-verify.cyclonedx.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-verify.cyclonedx.json ]]; then
|
||||
echo "❌ SBOM file does not exist"
|
||||
echo "valid=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check file is non-empty
|
||||
if [[ ! -s sbom-verify.cyclonedx.json ]]; then
|
||||
echo "❌ SBOM file is empty"
|
||||
echo "valid=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate JSON structure
|
||||
if ! jq empty sbom-verify.cyclonedx.json 2>/dev/null; then
|
||||
echo "❌ SBOM file contains invalid JSON"
|
||||
echo "SBOM content:"
|
||||
cat sbom-verify.cyclonedx.json
|
||||
echo "valid=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate CycloneDX structure
|
||||
BOMFORMAT=$(jq -r '.bomFormat // "missing"' sbom-verify.cyclonedx.json)
|
||||
SPECVERSION=$(jq -r '.specVersion // "missing"' sbom-verify.cyclonedx.json)
|
||||
COMPONENTS=$(jq '.components // [] | length' sbom-verify.cyclonedx.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
|
||||
|
||||
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
|
||||
|
||||
# Scan for vulnerabilities using official Anchore action (auto-updated by Renovate)
|
||||
- name: Scan for Vulnerabilities
|
||||
if: steps.validate-sbom.outputs.valid == 'true'
|
||||
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
|
||||
id: scan
|
||||
with:
|
||||
sbom: sbom-verify.cyclonedx.json
|
||||
fail-build: false
|
||||
output-format: json
|
||||
|
||||
- name: Process Vulnerability Results
|
||||
if: steps.validate-sbom.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "Processing vulnerability results..."
|
||||
|
||||
# The scan-action outputs results.json and results.sarif
|
||||
# Rename for consistency
|
||||
if [[ -f results.json ]]; then
|
||||
mv results.json vuln-scan.json
|
||||
fi
|
||||
if [[ -f results.sarif ]]; then
|
||||
mv results.sarif vuln-scan.sarif
|
||||
fi
|
||||
|
||||
# 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}"
|
||||
echo "HIGH_VULNS=${HIGH}"
|
||||
echo "MEDIUM_VULNS=${MEDIUM}"
|
||||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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"
|
||||
echo ""
|
||||
|
||||
if [[ "${{ steps.image-check.outputs.exists }}" != "true" ]]; then
|
||||
echo "**Reason**: Docker image not available yet"
|
||||
echo ""
|
||||
echo "This is expected for PR workflows. The image will be scanned"
|
||||
echo "after it's built by the docker-build workflow."
|
||||
elif [[ "${{ steps.validate-sbom.outputs.valid }}" != "true" ]]; then
|
||||
echo "**Reason**: SBOM validation failed"
|
||||
echo ""
|
||||
echo "Check the 'Validate SBOM File' step for details."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
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
|
||||
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: Find Existing PR Comment
|
||||
id: find-comment
|
||||
if: steps.pr-number.outputs.result != ''
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr-number.outputs.result }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '<!-- supply-chain-security-comment -->'
|
||||
|
||||
- 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-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
|
||||
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: needs.verify-sbom.outputs.image_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
|
||||
Reference in New Issue
Block a user