Improve test coverage (#823) #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# =============================================================== | |
# 📦 Secure Docker Build & Scan Workflow | |
# =============================================================== | |
# | |
# This workflow: | |
# - Builds and tags the container image (`latest` + timestamp) | |
# - Re-uses a BuildKit layer cache for faster rebuilds | |
# - Lints the Dockerfile with **Hadolint** (CLI) → SARIF | |
# - Lints the finished image with **Dockle** (CLI) → SARIF | |
# - Generates an SPDX SBOM with **Syft** | |
# - Scans the image for CRITICAL CVEs with **Trivy/Grype** | |
# - Uploads Hadolint, Dockle and Trivy results as SARIF files | |
# - Pushes the image to **GitHub Container Registry (GHCR)** | |
# - Signs & attests the image with **Cosign (key-less OIDC)** | |
# | |
# Triggers: | |
# - Every push / PR to `main` | |
# - Weekly scheduled run (Tue 18:17 UTC) to catch new CVEs | |
# --------------------------------------------------------------- | |
name: Secure Docker Build | |
on: | |
push: | |
branches: ["main"] | |
pull_request: | |
branches: ["main"] | |
schedule: | |
- cron: "17 18 * * 2" # Tuesday @ 18:17 UTC | |
# ----------------------------------------------------------------- | |
# Minimal permissions - keep the principle of least privilege | |
# ----------------------------------------------------------------- | |
permissions: | |
contents: read | |
packages: write # push to ghcr.io via GITHUB_TOKEN | |
security-events: write # upload SARIF to "Code scanning" | |
actions: read # needed by upload-sarif in private repos | |
id-token: write # required for OIDC token generation | |
jobs: | |
build-scan-sign: | |
runs-on: ubuntu-latest | |
env: | |
CACHE_DIR: /tmp/.buildx-cache # BuildKit layer cache dir | |
steps: | |
# ------------------------------------------------------------- | |
# 0️⃣ Checkout source | |
# ------------------------------------------------------------- | |
- name: ⬇️ Checkout code | |
uses: actions/checkout@v4 | |
# ------------------------------------------------------------- | |
# 0️⃣.5️⃣ Derive lower-case IMAGE_NAME for Docker tag | |
# ------------------------------------------------------------- | |
- name: 🏷️ Set IMAGE_NAME (lower-case repo path) | |
run: | | |
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" | |
echo "IMAGE_NAME=$IMAGE" >> "$GITHUB_ENV" | |
echo "Will build & push: $IMAGE_NAME" | |
# ------------------------------------------------------------- | |
# 1️⃣ Dockerfile lint (Hadolint CLI → SARIF) | |
# ------------------------------------------------------------- | |
- name: 🔍 Dockerfile lint (Hadolint) | |
id: hadolint | |
continue-on-error: true | |
run: | | |
curl -sSL https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint | |
chmod +x /usr/local/bin/hadolint | |
hadolint -f sarif Containerfile.lite > hadolint-results.sarif | |
echo "HADOLINT_EXIT=$?" >> "$GITHUB_ENV" | |
exit 0 | |
- name: ☁️ Upload Hadolint SARIF | |
if: always() | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: hadolint-results.sarif | |
# ------------------------------------------------------------- | |
# 2️⃣ Set up Buildx & restore cache | |
# ------------------------------------------------------------- | |
- name: 🛠️ Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: 🔄 Restore BuildKit layer cache | |
uses: actions/cache@v4 | |
with: | |
path: ${{ env.CACHE_DIR }} | |
key: ${{ runner.os }}-buildx-${{ github.sha }} | |
restore-keys: ${{ runner.os }}-buildx- | |
# ------------------------------------------------------------- | |
# 3️⃣ Build & tag image (timestamp + latest) | |
# ------------------------------------------------------------- | |
- name: 🏗️ Build Docker image | |
env: | |
DOCKER_CONTENT_TRUST: "1" | |
run: | | |
TAG=$(date +%s) | |
echo "TAG=$TAG" >> "$GITHUB_ENV" | |
docker buildx build \ | |
--file Containerfile.lite \ | |
--tag $IMAGE_NAME:$TAG \ | |
--tag $IMAGE_NAME:latest \ | |
--cache-from type=local,src=${{ env.CACHE_DIR }} \ | |
--cache-to type=local,dest=${{ env.CACHE_DIR }},mode=max \ | |
--load \ | |
. # build context is mandatory | |
# ------------------------------------------------------------- | |
# 4️⃣ Image lint (Dockle CLI → SARIF) | |
# ------------------------------------------------------------- | |
- name: 🔍 Image lint (Dockle) | |
id: dockle | |
continue-on-error: true | |
env: | |
DOCKLE_VERSION: 0.4.15 | |
run: | | |
curl -sSL "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.tar.gz" \ | |
| tar -xz -C /usr/local/bin dockle | |
dockle --exit-code 1 --format sarif \ | |
--output dockle-results.sarif \ | |
$IMAGE_NAME:latest | |
echo "DOCKLE_EXIT=$?" >> "$GITHUB_ENV" | |
exit 0 | |
- name: ☁️ Upload Dockle SARIF | |
if: always() | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: dockle-results.sarif | |
# ------------------------------------------------------------- | |
# 5️⃣ Generate SPDX SBOM with Syft | |
# ------------------------------------------------------------- | |
- name: 📄 Generate SBOM (Syft) | |
uses: anchore/sbom-action@v0 | |
with: | |
image: ${{ env.IMAGE_NAME }}:latest | |
output-file: sbom.spdx.json | |
# ------------------------------------------------------------- | |
# 6️⃣ Trivy, Grype CVE scan → SARIF | |
# ------------------------------------------------------------- | |
- name: 🛡️ Trivy vulnerability scan | |
id: trivy | |
continue-on-error: true | |
uses: aquasecurity/[email protected] | |
with: | |
image-ref: ${{ env.IMAGE_NAME }}:latest | |
format: sarif | |
output: trivy-results.sarif | |
severity: CRITICAL | |
exit-code: 0 | |
- name: ☁️ Upload Trivy SARIF | |
if: always() | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: trivy-results.sarif | |
- name: 📥 Installing Grype CLI | |
run: | | |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin | |
- name: 🔍 Grype vulnerability scan | |
run: | | |
grype ${{ env.IMAGE_NAME }}:latest --scope all-layers --only-fixed | |
- name: 📄 Generating Grype SARIF report | |
run: | | |
grype ${{ env.IMAGE_NAME }}:latest --scope all-layers --output sarif --file grype-results.sarif | |
- name: ☁️ Upload Grype SARIF | |
if: always() | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: grype-results.sarif | |
# ------------------------------------------------------------- | |
# 7️⃣ Push both tags to GHCR | |
# ------------------------------------------------------------- | |
- name: 🔑 Log in to GHCR | |
uses: docker/login-action@v3 | |
with: | |
registry: ghcr.io | |
username: ${{ github.actor }} | |
password: ${{ secrets.GITHUB_TOKEN }} | |
- name: 🚀 Push image to GHCR | |
if: github.ref == 'refs/heads/main' | |
run: | | |
docker push $IMAGE_NAME:${{ env.TAG }} | |
docker push $IMAGE_NAME:latest | |
# ------------------------------------------------------------- | |
# 8️⃣ Key-less Cosign sign + attest (latest **and** timestamp) | |
# ------------------------------------------------------------- | |
- name: 📥 Install Cosign | |
if: github.ref == 'refs/heads/main' | |
uses: sigstore/cosign-installer@v3 # provides the matching CLI | |
- name: 🔏 Sign & attest images (latest + timestamp) | |
if: github.ref == 'refs/heads/main' | |
env: | |
COSIGN_EXPERIMENTAL: "1" | |
run: | | |
for REF in $IMAGE_NAME:latest $IMAGE_NAME:${{ env.TAG }}; do | |
echo "🔑 Signing $REF" | |
cosign sign --yes "$REF" | |
echo "📝 Attesting SBOM for $REF" | |
cosign attest --yes \ | |
--predicate sbom.spdx.json \ | |
--type spdxjson \ | |
"$REF" | |
done | |
# ------------------------------------------------------------- | |
# 9️⃣ Single gate - fail job on any scanner error | |
# ------------------------------------------------------------- | |
- name: ⛔ Enforce lint & vuln gates | |
if: | | |
env.HADOLINT_EXIT != '0' || | |
env.DOCKLE_EXIT != '0' || | |
steps.trivy.outcome == 'failure' | |
run: | | |
echo "Hadolint exit: $HADOLINT_EXIT" | |
echo "Dockle exit: $DOCKLE_EXIT" | |
echo "Trivy status: ${{ steps.trivy.outcome }}" | |
exit 1 |