Skip to content

Container Security Scan #200

Container Security Scan

Container Security Scan #200

# Copyright AGNTCY Contributors (https://github.com/agntcy)
# SPDX-License-Identifier: Apache-2.0
name: Container Security Scan
on:
# Nightly scan on main branch only; manual dispatch allowed.
schedule:
- cron: "0 3 * * *" # Daily at 03:00 UTC (runs on default branch context: main)
workflow_dispatch:
inputs:
image-tag:
required: false
type: string
description: "Override tag for repo images (latest for latest release version)"
permissions:
contents: read
security-events: write # for uploading SARIF
actions: read
issues: write # create issues for critical CVEs
jobs:
resolve-latest-tag:
if: ${{ github.event.inputs.image-tag == 'latest' }}
name: Resolve latest release version tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Resolve image tag
id: resolve
run: |
set -euo pipefail
if [ -n "${{ inputs.image-tag }}" ]; then
echo "version=${{ inputs.image-tag }}" >> $GITHUB_OUTPUT
else
OWNER=${GITHUB_REPOSITORY%%/*}
REPO_NAME=${GITHUB_REPOSITORY#*/}
TAG_VERSION=$(gh api graphql \
-f query='
query($owner: String!, $repo: String!, $tagCount: Int!) {
repository(owner: $owner, name: $repo) {
refs(refPrefix: "refs/tags/", first: $tagCount, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {
nodes { name }
}
}
}
' \
-F owner=$OWNER -F repo=$REPO_NAME -F tagCount=100 \
--jq 'first(.data.repository.refs.nodes[] | select(.name | contains("/") | not).name)'
)
echo "version=${TAG_VERSION}" >> $GITHUB_OUTPUT
fi
build-from-main:
if: ${{ github.event.inputs.image-tag == 'latest' && github.event.inputs.image-tag == '' }}
name: Build main branch
runs-on: ubuntu-latest
outputs:
main_sha: ${{ steps.get-commit-sha.outputs.main_sha }}
steps:
- name: Get main branch SHA
id: get-commit-sha
run: |
MAIN_SHA=gh api repos/${{ github.repository }}/git/refs/heads/${{ github.event.repository.default_branch }} | jq -r .object.sha
echo "main_sha=${MAIN_SHA}" >> $GITHUB_OUTPUT
- uses: ./.github/workflows/reusable-build.yaml
with:
image_repo: ghcr.io/agntcy
image_tag: ${{ steps.get-commit-sha.outputs.main_sha }}
image-list:
name: Resolve image list
runs-on: ubuntu-latest
needs: [resolve-latest-tag, build-from-main]
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Taskfile
shell: bash
run: sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
- name: Get image list from task
id: matrix
env:
IMAGE_TAG: ${{ needs.resolve-latest-tag.outputs.version || needs.build-from-main.outputs.main_sha || github.event.inputs.image-tag }}
IMAGE_REPO: ghcr.io/${{ github.repository_owner }}
run: |
matrix=$(task --silent deps:vuln:images:list | jq -R -s -c 'split("\n") | map(select(length > 0)) | {image: .}')
echo "matrix<<EOF" >> $GITHUB_OUTPUT
echo "$matrix" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
trivy-scan:
name: Trivy Scan
runs-on: ubuntu-latest
needs: [image-list]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.image-list.outputs.matrix) }}
steps:
- name: Set image name
id: image-name
run: |
# Extract image name from full reference (e.g., ghcr.io/owner/image:tag -> image)
IMAGE_NAME=$(echo "${{ matrix.image }}" | sed -E 's|.*/||; s|[:@].*||')
echo "name=$IMAGE_NAME" >> $GITHUB_OUTPUT
- name: Scan image
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: ${{ matrix.image }}
github-pat: ${{ secrets.GITHUB_TOKEN }}
format: sarif
output: trivy-${{ steps.image-name.outputs.name }}.sarif
vuln-type: "os,library"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
- name: Export image metadata
run: echo "${{ matrix.image }}" > trivy-${{ steps.image-name.outputs.name }}.meta
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: trivy-${{ steps.image-name.outputs.name }}.sarif
category: trivy-${{ steps.image-name.outputs.name }}
- name: Upload report artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: trivy-report-${{ steps.image-name.outputs.name }}
path: |
trivy-${{ steps.image-name.outputs.name }}.sarif
trivy-${{ steps.image-name.outputs.name }}.meta
retention-days: 7
summarize:
name: Summarize Results
needs: [trivy-scan]
runs-on: ubuntu-latest
if: always()
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
path: trivy-artifacts
- name: Generate summary
run: |
chmod +x .github/workflows/scripts/security/generate_trivy_summary.sh
.github/workflows/scripts/security/generate_trivy_summary.sh
- name: Fail if critical vulns found (optional gate)
if: ${{ github.event_name != 'pull_request' }}
run: |
set -e
found=$(grep -R "CRITICAL" -c trivy-artifacts || true)
if [ "${found}" != "0" ]; then
echo "Critical vulnerabilities detected. (Gate currently informational.)" >&2
fi
- name: Create GitHub issues for critical CVEs
if: ${{ github.event_name != 'pull_request' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
echo "Installing dependencies for issue creation script";
npm init -y >/dev/null 2>&1 || true
npm install @octokit/rest@21 glob >/dev/null 2>&1
node .github/workflows/scripts/security/create_critical_cve_issues.js