feat: Enhance supply chain verification by excluding PR builds and add Docker image artifact handling
This commit is contained in:
288
.github/workflows/docker-build.yml
vendored
288
.github/workflows/docker-build.yml
vendored
@@ -29,6 +29,8 @@ concurrency:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
||||
SYFT_VERSION: v1.17.0
|
||||
GRYPE_VERSION: v0.85.0
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -133,6 +135,21 @@ jobs:
|
||||
VCS_REF=${{ github.sha }}
|
||||
CADDY_IMAGE=${{ steps.caddy.outputs.image }}
|
||||
|
||||
- name: Save Docker Image as Artifact
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]')
|
||||
docker save ghcr.io/${IMAGE_NAME}:pr-${{ github.event.pull_request.number }} -o /tmp/charon-pr-image.tar
|
||||
ls -lh /tmp/charon-pr-image.tar
|
||||
|
||||
- name: Upload Image Artifact
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: pr-image-${{ github.event.pull_request.number }}
|
||||
path: /tmp/charon-pr-image.tar
|
||||
retention-days: 1
|
||||
|
||||
- name: Verify Caddy Security Patches (CVE-2025-68156)
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
timeout-minutes: 2
|
||||
@@ -380,3 +397,274 @@ jobs:
|
||||
- name: Run Trivy filesystem scan on `charon` (fail PR on HIGH/CRITICAL)
|
||||
run: |
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy -v $PWD:/workdir aquasec/trivy:latest fs --exit-code 1 --severity CRITICAL,HIGH /workdir/charon_binary
|
||||
|
||||
# ============================================================================
|
||||
# Supply Chain Verification for PR Builds
|
||||
# ============================================================================
|
||||
# This job performs SBOM generation and vulnerability scanning for PR builds.
|
||||
# It depends on the build-and-push job completing successfully and uses the
|
||||
# Docker image artifact uploaded by that job.
|
||||
#
|
||||
# Dependency Chain: build-and-push (builds & uploads) → verify-supply-chain-pr (downloads & scans)
|
||||
# ============================================================================
|
||||
verify-supply-chain-pr:
|
||||
name: Supply Chain Verification (PR)
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
# Critical Fix #2: Enhanced conditional with result check
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.build-and-push.outputs.skip_build != 'true' &&
|
||||
needs.build-and-push.result == 'success'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
# Critical Fix #1: Download image artifact
|
||||
- name: Download Image Artifact
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: pr-image-${{ github.event.pull_request.number }}
|
||||
|
||||
# Critical Fix #1: Load Docker image
|
||||
- name: Load Docker Image
|
||||
run: |
|
||||
docker load -i charon-pr-image.tar
|
||||
docker images
|
||||
echo "✅ Image loaded successfully"
|
||||
|
||||
- name: Normalize image name
|
||||
run: |
|
||||
IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]')
|
||||
echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set PR image reference
|
||||
id: image
|
||||
run: |
|
||||
IMAGE_REF="ghcr.io/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}"
|
||||
echo "ref=${IMAGE_REF}" >> $GITHUB_OUTPUT
|
||||
echo "📦 Will verify: ${IMAGE_REF}"
|
||||
|
||||
- name: Install Verification Tools
|
||||
run: |
|
||||
# Use workflow-level environment variables for versions
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin ${{ env.SYFT_VERSION }}
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin ${{ env.GRYPE_VERSION }}
|
||||
syft version
|
||||
grype version
|
||||
|
||||
- name: Generate SBOM
|
||||
id: sbom
|
||||
run: |
|
||||
echo "🔍 Generating SBOM for ${{ steps.image.outputs.ref }}..."
|
||||
if ! syft ${{ steps.image.outputs.ref }} -o cyclonedx-json > sbom-pr.cyclonedx.json; then
|
||||
echo "❌ SBOM generation failed"
|
||||
exit 1
|
||||
fi
|
||||
COMPONENT_COUNT=$(jq '.components | length' sbom-pr.cyclonedx.json 2>/dev/null || echo "0")
|
||||
echo "📦 SBOM contains ${COMPONENT_COUNT} components"
|
||||
if [[ ${COMPONENT_COUNT} -eq 0 ]]; then
|
||||
echo "⚠️ WARNING: SBOM contains no components"
|
||||
exit 1
|
||||
fi
|
||||
echo "component_count=${COMPONENT_COUNT}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Scan for Vulnerabilities
|
||||
id: scan
|
||||
run: |
|
||||
echo "🔍 Scanning for vulnerabilities..."
|
||||
grype db update
|
||||
if ! grype sbom:./sbom-pr.cyclonedx.json --output json --file vuln-scan.json; then
|
||||
echo "❌ Vulnerability scan failed"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
echo "=== Vulnerability Summary ==="
|
||||
grype sbom:./sbom-pr.cyclonedx.json --output table || true
|
||||
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 Breakdown:"
|
||||
echo " 🔴 Critical: ${CRITICAL}"
|
||||
echo " 🟠 High: ${HIGH}"
|
||||
echo " 🟡 Medium: ${MEDIUM}"
|
||||
echo " 🟢 Low: ${LOW}"
|
||||
echo "critical=${CRITICAL}" >> $GITHUB_OUTPUT
|
||||
echo "high=${HIGH}" >> $GITHUB_OUTPUT
|
||||
echo "medium=${MEDIUM}" >> $GITHUB_OUTPUT
|
||||
echo "low=${LOW}" >> $GITHUB_OUTPUT
|
||||
if [[ ${CRITICAL} -gt 0 ]]; then
|
||||
echo "::error::${CRITICAL} CRITICAL vulnerabilities found - BLOCKING"
|
||||
fi
|
||||
if [[ ${HIGH} -gt 0 ]]; then
|
||||
echo "::warning::${HIGH} HIGH vulnerabilities found"
|
||||
fi
|
||||
|
||||
- name: Generate SARIF Report
|
||||
if: always()
|
||||
run: |
|
||||
echo "📋 Generating SARIF report..."
|
||||
grype sbom:./sbom-pr.cyclonedx.json --output sarif --file grype-results.sarif || true
|
||||
|
||||
# Critical Fix #3: SARIF category includes SHA to prevent conflicts
|
||||
- name: Upload SARIF to GitHub Security
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
with:
|
||||
sarif_file: grype-results.sarif
|
||||
category: supply-chain-pr-${{ github.event.pull_request.number }}-${{ github.sha }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: supply-chain-pr-${{ github.event.pull_request.number }}
|
||||
path: |
|
||||
sbom-pr.cyclonedx.json
|
||||
vuln-scan.json
|
||||
grype-results.sarif
|
||||
retention-days: 30
|
||||
|
||||
# Critical Fix #4: Null checks in PR comment
|
||||
- name: Comment on PR
|
||||
if: always()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const critical = '${{ steps.scan.outputs.critical }}' || '0';
|
||||
const high = '${{ steps.scan.outputs.high }}' || '0';
|
||||
const medium = '${{ steps.scan.outputs.medium }}' || '0';
|
||||
const low = '${{ steps.scan.outputs.low }}' || '0';
|
||||
const components = '${{ steps.sbom.outputs.component_count }}' || 'N/A';
|
||||
const commitSha = '${{ github.sha }}'.substring(0, 7);
|
||||
|
||||
let status = '✅ **PASSED**';
|
||||
let statusEmoji = '✅';
|
||||
|
||||
if (parseInt(critical) > 0) {
|
||||
status = '❌ **BLOCKED** - Critical vulnerabilities found';
|
||||
statusEmoji = '❌';
|
||||
} else if (parseInt(high) > 0) {
|
||||
status = '⚠️ **WARNING** - High vulnerabilities found';
|
||||
statusEmoji = '⚠️';
|
||||
}
|
||||
|
||||
const body = `## ${statusEmoji} Supply Chain Verification (PR Build)
|
||||
|
||||
**Status**: ${status}
|
||||
**Commit**: \`${commitSha}\`
|
||||
**Image**: \`${{ steps.image.outputs.ref }}\`
|
||||
**Components Scanned**: ${components}
|
||||
|
||||
### 📊 Vulnerability Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🔴 Critical | ${critical} |
|
||||
| 🟠 High | ${high} |
|
||||
| 🟡 Medium | ${medium} |
|
||||
| 🟢 Low | ${low} |
|
||||
|
||||
${parseInt(critical) > 0 ? '### ❌ Critical Vulnerabilities Detected\n\n**Action Required**: This PR cannot be merged until critical vulnerabilities are resolved.\n\n' : ''}
|
||||
${parseInt(high) > 0 ? '### ⚠️ High Vulnerabilities Detected\n\n**Recommendation**: Review and address high-severity vulnerabilities before merging.\n\n' : ''}
|
||||
📋 [View Full Report](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
|
||||
📦 [Download Artifacts](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}#artifacts)
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: body
|
||||
});
|
||||
|
||||
- name: Fail on Critical Vulnerabilities
|
||||
if: steps.scan.outputs.critical != '0'
|
||||
run: |
|
||||
echo "❌ CRITICAL: ${{ steps.scan.outputs.critical }} critical vulnerabilities found"
|
||||
echo "This PR is blocked from merging until critical vulnerabilities are resolved."
|
||||
exit 1
|
||||
|
||||
# Critical Fix #4: Null checks in job summary
|
||||
- name: Create Job Summary
|
||||
if: always()
|
||||
run: |
|
||||
# Use default values if outputs are not set
|
||||
COMPONENT_COUNT="${{ steps.sbom.outputs.component_count }}"
|
||||
CRITICAL="${{ steps.scan.outputs.critical }}"
|
||||
HIGH="${{ steps.scan.outputs.high }}"
|
||||
MEDIUM="${{ steps.scan.outputs.medium }}"
|
||||
LOW="${{ steps.scan.outputs.low }}"
|
||||
|
||||
# Apply defaults
|
||||
COMPONENT_COUNT="${COMPONENT_COUNT:-N/A}"
|
||||
CRITICAL="${CRITICAL:-0}"
|
||||
HIGH="${HIGH:-0}"
|
||||
MEDIUM="${MEDIUM:-0}"
|
||||
LOW="${LOW:-0}"
|
||||
|
||||
echo "## 🔒 Supply Chain Verification - PR #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Image**: \`${{ steps.image.outputs.ref }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Components**: ${COMPONENT_COUNT}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Vulnerability Breakdown" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🔴 Critical: ${CRITICAL}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟠 High: ${HIGH}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟡 Medium: ${MEDIUM}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟢 Low: ${LOW}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ ${CRITICAL} -gt 0 ]]; then
|
||||
echo "❌ **BLOCKED**: Critical vulnerabilities must be resolved" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ ${HIGH} -gt 0 ]]; then
|
||||
echo "⚠️ **WARNING**: High vulnerabilities detected" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "✅ **PASSED**: No critical or high vulnerabilities" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Supply Chain Verification - Skipped Feedback
|
||||
# ============================================================================
|
||||
# This job provides user feedback when the build is skipped (e.g., chore commits).
|
||||
# Critical Fix #7: User feedback for skipped builds
|
||||
# ============================================================================
|
||||
verify-supply-chain-pr-skipped:
|
||||
name: Supply Chain Verification (Skipped)
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.build-and-push.outputs.skip_build == 'true'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Comment on PR - Build Skipped
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const commitSha = '${{ github.sha }}'.substring(0, 7);
|
||||
const body = `## ⏭️ Supply Chain Verification (Skipped)
|
||||
|
||||
**Commit**: \`${commitSha}\`
|
||||
**Reason**: Build was skipped (likely a documentation-only or chore commit)
|
||||
|
||||
Supply chain verification is not performed for skipped builds. If this commit should trigger a build, ensure it includes changes to application code or dependencies.
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: body
|
||||
});
|
||||
|
||||
5
.github/workflows/supply-chain-verify.yml
vendored
5
.github/workflows/supply-chain-verify.yml
vendored
@@ -35,9 +35,12 @@ jobs:
|
||||
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_name != 'workflow_run' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request'))
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
Reference in New Issue
Block a user