diff --git a/.gitea/workflows/build-scan-push.yml b/.gitea/workflows/build-scan-push.yml new file mode 100644 index 0000000..a7ba358 --- /dev/null +++ b/.gitea/workflows/build-scan-push.yml @@ -0,0 +1,169 @@ +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'