Skip to content

Improve test coverage (#823) #53

Improve test coverage (#823)

Improve test coverage (#823) #53

Workflow file for this run

# ===============================================================
# 📦 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