Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/scan-summary.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Turns a Grype SARIF report into a markdown table for the job summary: one row per
# vulnerability with a CVSS score of 7.0 or more, highest first. Used by images.yml.
.runs[0] as $r
| ($r.tool.driver.rules | map({key: .id, value: (.properties["security-severity"] | tonumber? // 0)}) | from_entries) as $score
| [$r.results[] | {id: .ruleId, score: $score[.ruleId],
pkg: ((.message.text | capture("package: (?<p>[^ ,]+), version (?<v>[^ ]+)") | "\(.p) \(.v)") // ""),
at: ((.message.text | capture(" at: (?<a>.+)$").a) // "")}]
| map(select(.score >= 7)) | unique_by(.id) | sort_by(-.score)
| if length == 0 then "None." else
("| Vulnerability | Score | Package | Where |", "|---|---|---|---|",
(.[] | "| \(.id) | \(.score) | \(.pkg) | \(.at) |"))
end
77 changes: 76 additions & 1 deletion .github/workflows/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ name: images
# zoneminder/pyzm-serve
# Every tag is also published to GitHub's registry as ghcr.io/zoneminder/zoneminder and
# ghcr.io/zoneminder/pyzm-serve, pushed with the workflow's GITHUB_TOKEN.
#
# Supply chain: each image carries an SBOM and full build provenance as attestations, and the
# merge job signs every multi-arch index with cosign (keyless, through GitHub's OIDC token).
# The amd64 build job scans the smoke-tested images with Grype and reports to the repo's
# Security tab. Scan results never block a push: holding back an image would also hold back
# the Debian security fixes in it. Third-party actions are pinned to a commit.

on:
push:
Expand Down Expand Up @@ -101,6 +107,9 @@ jobs:
needs: prep
runs-on: ${{ matrix.runner }}
timeout-minutes: 90
permissions:
contents: read
security-events: write # upload scan results to the Security tab
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -129,6 +138,55 @@ jobs:
ZM_VERSION: ${{ steps.meta.outputs.pin }}
run: tests/smoke.sh

# Scan the images the smoke test just built and ran, before any registry login, so the
# scanner never runs with Docker Hub credentials available. amd64 only: the arm64 images
# hold the same packages, and scanning both would report every finding twice.
- name: Scan zm-es
id: scan_zm
if: matrix.arch == 'amd64'
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
with:
image: zmsmoke:es
grype-version: v0.121.0
only-fixed: true
fail-build: false
output-format: sarif
output-file: scan-zm-es.sarif
- name: Scan pyzm-serve
id: scan_pyzm
if: matrix.arch == 'amd64' && matrix.channel == 'release'
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
with:
image: zmsmoke-pyzm-serve:latest
grype-version: v0.121.0
only-fixed: true
fail-build: false
output-format: sarif
output-file: scan-pyzm-serve.sarif
- name: Scan summary
if: matrix.arch == 'amd64'
run: |
{
echo "### Fixable vulnerabilities scoring 7.0 or more (${{ matrix.channel }})"
for f in scan-*.sarif; do
name=${f#scan-}; echo; echo "**${name%.sarif}**"; echo
jq -r -f .github/scan-summary.jq "$f"
done
} >> "$GITHUB_STEP_SUMMARY"
# Pull requests from forks get a read-only token, so they only get the summary above.
- name: Upload zm-es scan
if: matrix.arch == 'amd64' && github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
sarif_file: scan-zm-es.sarif
category: grype-zm-es-${{ matrix.channel }}
- name: Upload pyzm-serve scan
if: matrix.arch == 'amd64' && matrix.channel == 'release' && github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
sarif_file: scan-pyzm-serve.sarif
category: grype-pyzm-serve

- uses: docker/setup-buildx-action@v3
if: steps.meta.outputs.push == 'true'
- uses: docker/login-action@v3
Expand Down Expand Up @@ -165,6 +223,8 @@ jobs:
org.opencontainers.image.licenses=GPL-2.0-or-later
tags: ${{ steps.meta.outputs.zm_repo }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
sbom: true
provenance: mode=max
cache-from: type=gha,scope=${{ matrix.channel }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.channel }}-${{ matrix.arch }}

Expand Down Expand Up @@ -194,6 +254,8 @@ jobs:
org.opencontainers.image.licenses=GPL-2.0-or-later
tags: ${{ steps.meta.outputs.zm_repo }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
sbom: true
provenance: mode=max
cache-from: type=gha,scope=${{ matrix.channel }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.channel }}-${{ matrix.arch }}

Expand All @@ -214,6 +276,8 @@ jobs:
org.opencontainers.image.revision=${{ github.sha }}
tags: ${{ steps.meta.outputs.pyzm_repo }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
sbom: true
provenance: mode=max
cache-from: type=gha,scope=pyzm-serve-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=pyzm-serve-${{ matrix.arch }}

Expand Down Expand Up @@ -243,6 +307,7 @@ jobs:
timeout-minutes: 15
permissions:
packages: write # push to ghcr.io
id-token: write # cosign keyless signing
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -276,8 +341,12 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
if: steps.meta.outputs.push == 'true'
with:
cosign-release: v3.1.4

- name: Tag multi-arch images
- name: Tag and sign multi-arch images
if: steps.meta.outputs.push == 'true'
env:
CHANNEL: ${{ matrix.channel }}
Expand All @@ -291,6 +360,7 @@ jobs:
# merge <image> <repo> <tags> <description>: one index from both architectures' digests,
# under every tag. imagetools copies the images into the other repos and registries.
# GHCR reads the source annotation on the index to link the package to this repo.
# Then cosign signs the index in each repo, since signatures are stored per repo.
merge() {
local sources=() tags=() arch f
for arch in amd64 arm64; do
Expand All @@ -304,6 +374,11 @@ jobs:
--annotation "index:org.opencontainers.image.description=$4" \
"${sources[@]}"
docker buildx imagetools inspect "${3%% *}"
local digest repo
digest=$(docker buildx imagetools inspect "${3%% *}" --format '{{json .Manifest}}' | jq -r .digest)
for repo in $(for t in $3; do echo "${t%:*}"; done | sort -u); do
cosign sign --yes "$repo@$digest"
done
}
merge zm "$ZM_REPO" "$ZM_TAGS" "ZoneMinder video surveillance on Debian"
merge zm-es "$ZM_REPO" "$ES_TAGS" "ZoneMinder video surveillance on Debian, with zmeventnotificationNg"
Expand Down
39 changes: 35 additions & 4 deletions .github/workflows/pyzm-cuda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
permissions:
contents: read
packages: write # push to ghcr.io
id-token: write # cosign keyless signing
steps:
- uses: actions/checkout@v4

Expand All @@ -59,7 +60,7 @@ jobs:
{
echo "push=$push"
echo "pyzm=$pyzm"
echo "tags=$(for r in $PYZM_REPOS; do printf '%s ' "$r:cuda" "$r:$pyzm-cuda"; done)"
echo "tags=$(for r in $PYZM_REPOS; do printf '%s,' "$r:cuda" "$r:$pyzm-cuda"; done)"
} | tee -a "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@v3
Expand Down Expand Up @@ -101,10 +102,40 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Same inputs as Build, so every layer comes from the cache and the pushed image is the one
# just tested. This step adds the SBOM and provenance attestations, which a docker push of
# the loaded image can't carry.
- name: Push
id: push
if: steps.meta.outputs.push == 'true'
uses: docker/build-push-action@v6
with:
context: release/pyzm-serve
file: release/pyzm-serve/Dockerfile.cuda
platforms: linux/amd64
push: true
sbom: true
provenance: mode=max
build-args: BUILD_JOBS=4
labels: |
org.opencontainers.image.title=pyzm-serve (CUDA)
org.opencontainers.image.description=Object detection server for the ZoneMinder event server, on NVIDIA GPUs
org.opencontainers.image.version=${{ steps.meta.outputs.pyzm }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha,scope=pyzm-cuda

- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
if: steps.meta.outputs.push == 'true'
with:
cosign-release: v3.1.4
- name: Sign
if: steps.meta.outputs.push == 'true'
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.push.outputs.digest }}
run: |
for tag in ${{ steps.meta.outputs.tags }}; do
docker tag "$TEST_IMAGE" "$tag"
docker push "$tag"
for repo in $(echo "$TAGS" | tr , '\n' | sed '/^$/d; s/:[^:]*$//' | sort -u); do
cosign sign --yes "$repo@$DIGEST"
done
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,33 @@ docker compose up -d
live in the same place (`/var/cache/zoneminder/events`), so point the `events` volume at the old
directory, or bind mount it.

## Verifying images

CI signs every published image with [cosign](https://docs.sigstore.dev/), using GitHub's OIDC
identity, so there is no key to distribute. To check that an image came from this repo's master
branch:

```bash
cosign verify zoneminder/zoneminder:latest \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/ZoneMinder/zmdockerfiles/\.github/workflows/(images|pyzm-cuda)\.yml@refs/heads/master$'
```

The same works for `zoneminderhq/zoneminder`, `zoneminder/pyzm-serve` and the `ghcr.io` names.

Each image also carries an SBOM, a list of every package inside it with versions, and a record of
how it was built: the commit, workflow and build arguments. Read them without pulling the image:

```bash
docker buildx imagetools inspect zoneminder/zoneminder:latest --format '{{json .SBOM}}'
docker buildx imagetools inspect zoneminder/zoneminder:latest --format '{{json .Provenance}}'
```

CI scans the images with [Grype](https://github.com/anchore/grype) on every build. Results for
master are in the repo's Security tab, and each run's summary lists fixable findings scoring 7.0
or more. Findings don't block publishing: an image held back for one CVE would also hold back the
Debian security fixes in the same rebuild.

## Building

```bash
Expand Down
Loading