652 lines
25 KiB
YAML
652 lines
25 KiB
YAML
name: Nightly Build & Package
|
|
on:
|
|
schedule:
|
|
# Daily at 09:00 UTC (4am EST / 5am EDT)
|
|
- cron: '0 9 * * *'
|
|
workflow_dispatch:
|
|
inputs:
|
|
reason:
|
|
description: "Why are you running this manually?"
|
|
required: true
|
|
default: "manual trigger"
|
|
skip_tests:
|
|
description: "Skip test-nightly-image job?"
|
|
required: false
|
|
default: "false"
|
|
|
|
env:
|
|
GO_VERSION: '1.26.2'
|
|
NODE_VERSION: '24.12.0'
|
|
GOTOOLCHAIN: auto
|
|
GHCR_REGISTRY: ghcr.io
|
|
DOCKERHUB_REGISTRY: docker.io
|
|
IMAGE_NAME: wikid82/charon
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
sync-development-to-nightly:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
has_changes: ${{ steps.sync.outputs.has_changes }}
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: nightly
|
|
fetch-depth: 0
|
|
token: ${{ secrets.CHARON_CI_TRIGGER_TOKEN || secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Configure Git
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Sync development to nightly
|
|
id: sync
|
|
env:
|
|
HAS_TRIGGER_TOKEN: ${{ secrets.CHARON_CI_TRIGGER_TOKEN != '' }}
|
|
run: |
|
|
# Fetch both branches to ensure we have the latest remote state
|
|
git fetch origin development
|
|
git fetch origin nightly
|
|
|
|
# Sync local nightly with remote nightly to prevent non-fast-forward errors
|
|
echo "Syncing local nightly with remote nightly..."
|
|
git reset --hard origin/nightly
|
|
|
|
# Check if there are differences between remote branches
|
|
if git diff --quiet origin/nightly origin/development; then
|
|
echo "No changes to sync from development to nightly"
|
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Syncing changes from development to nightly"
|
|
# Fast-forward merge development into nightly
|
|
git merge origin/development --ff-only -m "chore: sync from development branch [skip ci]" || {
|
|
# If fast-forward fails, force reset to development
|
|
echo "Fast-forward not possible, resetting nightly to development"
|
|
git reset --hard origin/development
|
|
}
|
|
if [[ "$HAS_TRIGGER_TOKEN" != "true" ]]; then
|
|
echo "::warning title=Using GITHUB_TOKEN fallback::Set CHARON_CI_TRIGGER_TOKEN to ensure push-triggered workflows run on nightly."
|
|
fi
|
|
# Force push to handle cases where nightly diverged from development
|
|
git push --force origin nightly
|
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
trigger-nightly-validation:
|
|
name: Trigger Nightly Validation Workflows
|
|
needs: sync-development-to-nightly
|
|
if: needs.sync-development-to-nightly.outputs.has_changes == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: write
|
|
contents: read
|
|
steps:
|
|
- name: Dispatch Missing Nightly Validation Workflows
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
|
|
const { data: nightlyBranch } = await github.rest.repos.getBranch({
|
|
owner,
|
|
repo,
|
|
branch: 'nightly',
|
|
});
|
|
const nightlyHeadSha = nightlyBranch.commit.sha;
|
|
core.info(`Current nightly HEAD: ${nightlyHeadSha}`);
|
|
|
|
const workflows = [
|
|
{ id: 'e2e-tests-split.yml' },
|
|
{ id: 'codecov-upload.yml', inputs: { run_backend: 'true', run_frontend: 'true' } },
|
|
{ id: 'supply-chain-verify.yml' },
|
|
{ id: 'codeql.yml' },
|
|
];
|
|
|
|
core.info('Skipping security-pr.yml: PR-only workflow intentionally excluded from nightly non-PR dispatch');
|
|
|
|
for (const workflow of workflows) {
|
|
const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({
|
|
owner,
|
|
repo,
|
|
workflow_id: workflow.id,
|
|
branch: 'nightly',
|
|
per_page: 50,
|
|
});
|
|
|
|
const hasRunForHead = workflowRuns.workflow_runs.some(
|
|
(run) => run.head_sha === nightlyHeadSha,
|
|
);
|
|
|
|
if (hasRunForHead) {
|
|
core.info(`Skipping dispatch for ${workflow.id}; run already exists for nightly HEAD`);
|
|
continue;
|
|
}
|
|
|
|
await github.rest.actions.createWorkflowDispatch({
|
|
owner,
|
|
repo,
|
|
workflow_id: workflow.id,
|
|
ref: 'nightly',
|
|
...(workflow.inputs ? { inputs: workflow.inputs } : {}),
|
|
});
|
|
core.info(`Dispatched ${workflow.id} on nightly (missing run for HEAD)`);
|
|
}
|
|
|
|
build-and-push-nightly:
|
|
needs: sync-development-to-nightly
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
HAS_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN != '' }}
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
id-token: write
|
|
outputs:
|
|
version: ${{ steps.meta.outputs.version }}
|
|
digest: ${{ steps.resolve_digest.outputs.digest }}
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
|
|
fetch-depth: 0
|
|
|
|
- name: Set lowercase image name
|
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
|
|
|
- name: Resolve Alpine base image digest
|
|
id: alpine
|
|
run: |
|
|
ALPINE_IMAGE_REF=$(grep -m1 'ARG ALPINE_IMAGE=' Dockerfile | cut -d'=' -f2-)
|
|
if [[ -z "$ALPINE_IMAGE_REF" ]]; then
|
|
echo "::error::Failed to parse ALPINE_IMAGE from Dockerfile"
|
|
exit 1
|
|
fi
|
|
echo "Resolved Alpine image: ${ALPINE_IMAGE_REF}"
|
|
echo "image=${ALPINE_IMAGE_REF}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
|
with:
|
|
registry: ${{ env.GHCR_REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Log in to Docker Hub
|
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
|
with:
|
|
registry: docker.io
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
|
with:
|
|
images: |
|
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
tags: |
|
|
type=raw,value=nightly
|
|
type=raw,value=nightly-{{date 'YYYY-MM-DD'}}
|
|
type=sha,prefix=nightly-,format=short
|
|
labels: |
|
|
org.opencontainers.image.title=Charon Nightly
|
|
org.opencontainers.image.description=Nightly build of Charon
|
|
|
|
- name: Build and push Docker image
|
|
id: build
|
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
build-args: |
|
|
VERSION=nightly-${{ github.sha }}
|
|
VCS_REF=${{ github.sha }}
|
|
BUILD_DATE=${{ github.event.repository.pushed_at }}
|
|
ALPINE_IMAGE=${{ steps.alpine.outputs.image }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
provenance: true
|
|
sbom: true
|
|
|
|
- name: Resolve and export image digest
|
|
id: resolve_digest
|
|
run: |
|
|
set -euo pipefail
|
|
DIGEST="${{ steps.build.outputs.digest }}"
|
|
|
|
if [[ -z "$DIGEST" ]]; then
|
|
echo "Build action digest empty; querying GHCR registry API..."
|
|
GHCR_TOKEN=$(curl -sf \
|
|
-u "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
|
|
"https://ghcr.io/token?scope=repository:${{ env.IMAGE_NAME }}:pull&service=ghcr.io" \
|
|
| jq -r '.token')
|
|
DIGEST=$(curl -sfI \
|
|
-H "Authorization: Bearer ${GHCR_TOKEN}" \
|
|
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json" \
|
|
"https://ghcr.io/v2/${{ env.IMAGE_NAME }}/manifests/nightly" \
|
|
| grep -i '^docker-content-digest:' | awk '{print $2}' | tr -d '\r' || true)
|
|
[[ -n "$DIGEST" ]] && echo "Resolved from GHCR API: ${DIGEST}"
|
|
fi
|
|
|
|
if [[ -z "$DIGEST" ]]; then
|
|
echo "::error::Could not determine image digest from step output or GHCR registry API"
|
|
exit 1
|
|
fi
|
|
|
|
echo "RESOLVED_DIGEST=${DIGEST}" >> "$GITHUB_ENV"
|
|
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
|
|
echo "Exported digest: ${DIGEST}"
|
|
|
|
- name: Record nightly image digest
|
|
run: |
|
|
echo "## 🧾 Nightly Image Digest" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.resolve_digest.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Generate SBOM
|
|
id: sbom_primary
|
|
continue-on-error: true
|
|
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
|
with:
|
|
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}
|
|
format: cyclonedx-json
|
|
output-file: sbom-nightly.json
|
|
syft-version: v1.42.1
|
|
|
|
- name: Generate SBOM fallback with pinned Syft
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom-nightly.json ]] && jq -e . sbom-nightly.json >/dev/null 2>&1; then
|
|
echo "Primary SBOM generation succeeded with valid JSON; skipping fallback"
|
|
exit 0
|
|
fi
|
|
|
|
echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback"
|
|
|
|
SYFT_VERSION="v1.42.4"
|
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
ARCH="$(uname -m)"
|
|
case "$ARCH" in
|
|
x86_64) ARCH="amd64" ;;
|
|
aarch64|arm64) ARCH="arm64" ;;
|
|
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
esac
|
|
|
|
TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz"
|
|
BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}"
|
|
|
|
curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}"
|
|
curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt"
|
|
|
|
grep " ${TARBALL}$" checksums.txt > checksum_line.txt
|
|
sha256sum -c checksum_line.txt
|
|
|
|
tar -xzf "$TARBALL" syft
|
|
chmod +x syft
|
|
|
|
DIGEST="${{ steps.resolve_digest.outputs.digest }}"
|
|
if [[ -z "$DIGEST" ]]; then
|
|
echo "::error::Digest from resolve_digest step is empty; the digest-resolution step did not complete successfully"
|
|
exit 1
|
|
fi
|
|
./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom-nightly.json
|
|
|
|
- name: Verify SBOM artifact
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
test -s sbom-nightly.json
|
|
jq -e . sbom-nightly.json >/dev/null
|
|
jq -e '
|
|
.bomFormat == "CycloneDX"
|
|
and (.specVersion | type == "string" and length > 0)
|
|
and has("version")
|
|
and has("metadata")
|
|
and (.components | type == "array")
|
|
' sbom-nightly.json >/dev/null
|
|
|
|
- name: Upload SBOM artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: sbom-nightly
|
|
path: sbom-nightly.json
|
|
retention-days: 30
|
|
|
|
# Install Cosign for keyless signing
|
|
- name: Install Cosign
|
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
|
|
|
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
|
- name: Sign GHCR Image
|
|
run: |
|
|
echo "Signing GHCR nightly image with keyless signing..."
|
|
cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
|
|
echo "✅ GHCR nightly image signed successfully"
|
|
|
|
# Sign Docker Hub image with keyless signing (Sigstore/Fulcio)
|
|
- name: Sign Docker Hub Image
|
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
|
run: |
|
|
echo "Signing Docker Hub nightly image with keyless signing..."
|
|
cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
|
|
echo "✅ Docker Hub nightly image signed successfully"
|
|
|
|
# Attach SBOM to Docker Hub image
|
|
- name: Attach SBOM to Docker Hub
|
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
|
run: |
|
|
echo "Attaching SBOM to Docker Hub nightly image..."
|
|
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
|
|
echo "✅ SBOM attached to Docker Hub nightly image"
|
|
|
|
test-nightly-image:
|
|
needs: build-and-push-nightly
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
|
|
|
|
- name: Set lowercase image name
|
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
|
with:
|
|
registry: ${{ env.GHCR_REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Pull nightly image
|
|
run: docker pull "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
|
|
|
|
- name: Run container smoke test
|
|
run: |
|
|
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
|
|
docker run --name charon-nightly -d \
|
|
-p 8080:8080 \
|
|
"${IMAGE_REF}"
|
|
|
|
# Wait for container to become healthy
|
|
echo "⏳ Waiting for Charon to be healthy..."
|
|
MAX_ATTEMPTS=30
|
|
ATTEMPT=0
|
|
while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do
|
|
ATTEMPT=$((ATTEMPT + 1))
|
|
echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}..."
|
|
if docker exec charon-nightly wget -qO- http://127.0.0.1:8080/health > /dev/null 2>&1; then
|
|
echo "✅ Charon is healthy!"
|
|
docker exec charon-nightly wget -qO- http://127.0.0.1:8080/health
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [[ ${ATTEMPT} -ge ${MAX_ATTEMPTS} ]]; then
|
|
echo "❌ Health check failed after ${MAX_ATTEMPTS} attempts"
|
|
docker logs charon-nightly
|
|
docker stop charon-nightly
|
|
docker rm charon-nightly
|
|
exit 1
|
|
fi
|
|
|
|
# Cleanup
|
|
docker stop charon-nightly
|
|
docker rm charon-nightly
|
|
|
|
# NOTE: Standalone binary builds removed - Charon uses Docker-only deployment
|
|
# The build-nightly-release job that ran GoReleaser for Windows/macOS/Linux binaries
|
|
# was removed because:
|
|
# 1. Charon is distributed exclusively via Docker images
|
|
# 2. Cross-compilation was failing due to Unix-specific syscalls
|
|
# 3. No users download standalone binaries (all use Docker)
|
|
# If standalone binaries are needed in the future, re-add the job with Linux-only targets
|
|
|
|
verify-nightly-supply-chain:
|
|
needs: build-and-push-nightly
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
security-events: write
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
|
|
|
|
- name: Set lowercase image name
|
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Download SBOM
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: sbom-nightly
|
|
|
|
- name: Scan with Grype
|
|
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
|
|
with:
|
|
sbom: sbom-nightly.json
|
|
fail-build: false
|
|
severity-cutoff: high
|
|
|
|
- name: Scan with Trivy
|
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
|
with:
|
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}
|
|
format: 'sarif'
|
|
output: 'trivy-nightly.sarif'
|
|
version: 'v0.69.3'
|
|
trivyignores: '.trivyignore'
|
|
|
|
- name: Upload Trivy results
|
|
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
|
with:
|
|
sarif_file: 'trivy-nightly.sarif'
|
|
category: 'trivy-nightly'
|
|
|
|
- name: Security severity policy summary
|
|
run: |
|
|
{
|
|
echo "## 🔐 Nightly Supply Chain Severity Policy"
|
|
echo ""
|
|
echo "- Blocking: Critical, High"
|
|
echo "- Medium: non-blocking by default (report + triage SLA)"
|
|
echo "- Policy file: .github/security-severity-policy.yml"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Check for Critical/High CVEs
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
jq -e . trivy-nightly.sarif >/dev/null
|
|
|
|
CRITICAL_COUNT=$(jq -r '
|
|
[
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex].properties["security-severity"] // empty)
|
|
else
|
|
empty
|
|
end
|
|
)
|
|
// ([
|
|
$rules[]?
|
|
| select((.id // "") == ($result.ruleId // ""))
|
|
| .properties["security-severity"]
|
|
][0] // empty)
|
|
// empty
|
|
) as $securitySeverity
|
|
| (try ($securitySeverity | tonumber) catch empty) as $score
|
|
| select($score != null and $score >= 9.0)
|
|
] | length
|
|
' trivy-nightly.sarif)
|
|
|
|
HIGH_COUNT=$(jq -r '
|
|
[
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex].properties["security-severity"] // empty)
|
|
else
|
|
empty
|
|
end
|
|
)
|
|
// ([
|
|
$rules[]?
|
|
| select((.id // "") == ($result.ruleId // ""))
|
|
| .properties["security-severity"]
|
|
][0] // empty)
|
|
// empty
|
|
) as $securitySeverity
|
|
| (try ($securitySeverity | tonumber) catch empty) as $score
|
|
| select($score != null and $score >= 7.0 and $score < 9.0)
|
|
] | length
|
|
' trivy-nightly.sarif)
|
|
|
|
MEDIUM_COUNT=$(jq -r '
|
|
[
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex].properties["security-severity"] // empty)
|
|
else
|
|
empty
|
|
end
|
|
)
|
|
// ([
|
|
$rules[]?
|
|
| select((.id // "") == ($result.ruleId // ""))
|
|
| .properties["security-severity"]
|
|
][0] // empty)
|
|
// empty
|
|
) as $securitySeverity
|
|
| (try ($securitySeverity | tonumber) catch empty) as $score
|
|
| select($score != null and $score >= 4.0 and $score < 7.0)
|
|
] | length
|
|
' trivy-nightly.sarif)
|
|
|
|
{
|
|
echo "- Structured SARIF counts: CRITICAL=${CRITICAL_COUNT}, HIGH=${HIGH_COUNT}, MEDIUM=${MEDIUM_COUNT}"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# List all Critical/High/Medium findings with details for triage
|
|
# shellcheck disable=SC2016
|
|
LIST_FINDINGS='
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex] // {})
|
|
else
|
|
{}
|
|
end
|
|
) as $ruleByIndex
|
|
| (
|
|
[$rules[]? | select((.id // "") == ($result.ruleId // ""))][0] // {}
|
|
) as $ruleById
|
|
| ($ruleByIndex // $ruleById) as $rule
|
|
| ($rule.properties["security-severity"] // null) as $sev
|
|
| (try ($sev | tonumber) catch null) as $score
|
|
| select($score != null and $score >= 4.0)
|
|
| {
|
|
id: ($result.ruleId // "unknown"),
|
|
score: $score,
|
|
severity: (
|
|
if $score >= 9.0 then "CRITICAL"
|
|
elif $score >= 7.0 then "HIGH"
|
|
else "MEDIUM"
|
|
end
|
|
),
|
|
message: ($result.message.text // $rule.shortDescription.text // "no description")[0:120]
|
|
}
|
|
)
|
|
'
|
|
|
|
echo ""
|
|
echo "=== Vulnerability Details ==="
|
|
jq -r "[ ${LIST_FINDINGS} ] | sort_by(-.score) | .[] | \"\\(.severity) (\\(.score)): \\(.id) — \\(.message)\"" trivy-nightly.sarif || true
|
|
echo "============================="
|
|
echo ""
|
|
|
|
if [ "$CRITICAL_COUNT" -gt 0 ]; then
|
|
echo "❌ Critical vulnerabilities found in nightly build (${CRITICAL_COUNT})"
|
|
{
|
|
echo ""
|
|
echo "### ❌ Critical CVEs blocking nightly"
|
|
echo '```'
|
|
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"CRITICAL\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
|
|
echo '```'
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$HIGH_COUNT" -gt 0 ]; then
|
|
echo "❌ High vulnerabilities found in nightly build (${HIGH_COUNT})"
|
|
{
|
|
echo ""
|
|
echo "### ❌ High CVEs blocking nightly"
|
|
echo '```'
|
|
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"HIGH\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
|
|
echo '```'
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$MEDIUM_COUNT" -gt 0 ]; then
|
|
echo "::warning::Medium vulnerabilities found in nightly build (${MEDIUM_COUNT}). Non-blocking by policy; triage with SLA per .github/security-severity-policy.yml"
|
|
{
|
|
echo ""
|
|
echo "### ⚠️ Medium CVEs (non-blocking)"
|
|
echo '```'
|
|
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"MEDIUM\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
|
|
echo '```'
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
echo "✅ No Critical/High vulnerabilities found"
|