name: Build Docker Image & Security on: push: branches: - main workflow_dispatch: jobs: build-scan-push: runs-on: running-man env: # The image is under "homelab" namespace so it can stay private IMAGE: git.sysmd.uk/homelab/sysmd-games:latest COSIGN_VERSION: v3.0.5 GRYPE_VERSION: v0.110.0 steps: - name: Checkout uses: actions/checkout@v4 - name: Login to registry run: | echo "$REGISTRY_TOKEN" | docker login git.sysmd.uk -u "$REGISTRY_USER" --password-stdin env: REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Build image run: | docker build --no-cache -t ${IMAGE} . - name: Install Cosign run: | set -euo pipefail curl -fLO https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64 chmod +x cosign-linux-amd64 mv cosign-linux-amd64 /usr/local/bin/cosign - name: Install Grype run: | set -euo pipefail VERSION_NO_V="${GRYPE_VERSION#v}" FILE="grype_${VERSION_NO_V}_linux_amd64.tar.gz" BASE="https://github.com/anchore/grype/releases/download/${GRYPE_VERSION}" curl -fLO ${BASE}/${FILE} curl -fLO ${BASE}/grype_${VERSION_NO_V}_checksums.txt curl -fLO ${BASE}/grype_${VERSION_NO_V}_checksums.txt.sig curl -fLO ${BASE}/grype_${VERSION_NO_V}_checksums.txt.pem cosign verify-blob \ --signature grype_${VERSION_NO_V}_checksums.txt.sig \ --certificate grype_${VERSION_NO_V}_checksums.txt.pem \ --certificate-identity-regexp "https://github.com/anchore/grype" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ grype_${VERSION_NO_V}_checksums.txt CHECK=$(grep " ${FILE}$" grype_${VERSION_NO_V}_checksums.txt) [ -n "$CHECK" ] || { echo "Missing checksum"; exit 1; } echo "$CHECK" | sha256sum -c - tar -xzf ${FILE} mv grype /usr/local/bin/ - name: Scan image with Grype id: grype continue-on-error: true run: | grype ${IMAGE} -o json > grype.json echo "Fixable HIGH/CRITICAL vulnerabilities:" jq -r ' .matches[] | select( ( .vulnerability.severity == "High" or .vulnerability.severity == "Critical" ) and ( (.vulnerability.fix.versions | length) > 0 ) ) | "\(.artifact.name)@\(.artifact.version) -> \(.vulnerability.id) [\(.vulnerability.severity)] | fixed: \(.vulnerability.fix.versions[0])" ' grype.json # Fail only on fixable HIGH/CRITICAL jq -e ' [ .matches[] | select( ( .vulnerability.severity == "High" or .vulnerability.severity == "Critical" ) and ( (.vulnerability.fix.versions | length) > 0 ) ) ] | length == 0 ' grype.json - name: Notify Node-RED on vulnerabilities if: steps.grype.outcome == 'failure' run: | jq ' { repo: "mdaleo404/games", image: "'${IMAGE}'", summary: ( "Total: " + ( [ .matches[] | select( ( .vulnerability.severity == "High" or .vulnerability.severity == "Critical" ) and ( (.vulnerability.fix.versions | length) > 0 ) ) ] | length | tostring ) ), vulnerabilities: [ .matches[] | select( ( .vulnerability.severity == "High" or .vulnerability.severity == "Critical" ) and ( (.vulnerability.fix.versions | length) > 0 ) ) | { library: .artifact.name, cve: .vulnerability.id, severity: .vulnerability.severity, installed: .artifact.version, fixed: (.vulnerability.fix.versions[0]), title: .vulnerability.description, url: .vulnerability.dataSource } ] } ' grype.json \ | curl -s -X POST https://nodered.sysmd.uk/vulns-alert \ -H "Content-Type: application/json" \ --data-binary @- - name: Fail workflow if vulnerabilities found if: steps.grype.outcome == 'failure' run: exit 1 - name: Push image if: steps.grype.outcome != 'failure' run: | docker push ${IMAGE} - name: Deploy to server if: steps.grype.outcome != 'failure' uses: appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.DEPLOYMENT_HOST }} username: ${{ secrets.DEPLOYMENT_USER }} key: ${{ secrets.DEPLOYMENT_SSH_KEY }} port: ${{ secrets.DEPLOYMENT_SSH_PORT }} script: | sudo docker compose -f /opt/sysmd-games/docker-compose.yml pull && \ sudo docker compose -f /opt/sysmd-games/docker-compose.yml up -d host_key: 'false'