Skip to main content
PathMon

Chapter 15 of 15

Verifying Release Artefacts (SBOM and Provenance)

Updated Read the full guide

Overview

Every PatchMon release publishes a software bill of materials (SBOM) and a signed build provenance attestation. Together these let you answer two questions before you deploy:

  • What is inside this build? The SBOM lists the components and their versions.
  • Where did this build come from? The provenance attestation is a signed statement, produced inside the GitHub Actions run that built the artefact, binding the artefact to the exact source commit and workflow that produced it.

The SBOM is required of us under the EU Cyber Resilience Act (Annex I, Part II). The provenance attestation is not mandated by that regulation, but it is what most supply chain reviews ask for, so we publish both.

You do not need any of this to run PatchMon. It matters if you are subject to supply chain policy, are completing a security questionnaire, or simply want to confirm that the image you pulled is the one we built.

What is published

Artefact Where
Build provenance for the server image Attached to the image in GHCR, and in the GitHub attestation store
SBOM for the server image Attached to the image in GHCR, and in the GitHub attestation store
Build provenance for release binaries GitHub attestation store
SHA256SUMS Release assets on the GitHub release page
sbom-source.cdx.json Release assets on the GitHub release page

SBOMs are CycloneDX JSON.

Prerequisites

A GitHub CLI recent enough to have the gh attestation command set:

gh attestation verify --help

If that command is not recognised, update the CLI.

Verification does not require you to be signed in to anything, and does not require the image to be pulled first.

Verifying the container image

gh attestation verify \
  oci://ghcr.io/patchmon/patchmon-server:2.0.3 \
  --repo PatchMon/PatchMon

Replace 2.0.3 with the version you are deploying. A successful run prints the source commit and the workflow that built the image. A failure means the image was not built by our pipeline from our repository, and you should not deploy it.

To pin by digest instead of tag, which is what we recommend in production because a tag can be moved:

gh attestation verify \
  oci://ghcr.io/patchmon/patchmon-server@sha256:<digest> \
  --repo PatchMon/PatchMon

Verifying release binaries

Download the binary you need plus SHA256SUMS from the release page, then:

# Confirm the download is intact
sha256sum -c SHA256SUMS --ignore-missing

# Confirm it came from our pipeline
gh attestation verify ./patchmon-agent-linux-amd64 --repo PatchMon/PatchMon

The checksum check catches a truncated or corrupted download. The attestation check is the one that proves origin, so run both.

Getting the SBOM

The simplest route is the release page: download sbom-source.cdx.json from the release assets.

To pull the image SBOM out of the registry instead:

gh attestation download \
  oci://ghcr.io/patchmon/patchmon-server:2.0.3 \
  --repo PatchMon/PatchMon

That writes a Sigstore bundle. The SBOM is the predicate field inside the signed payload:

jq -r '.dsseEnvelope.payload' <bundle-file> | base64 -d | jq '.predicate' > sbom.cdx.json

Both SBOMs are also kept as build artefacts on the Actions run that produced the release, under the name sbom-server-<version>, for as long as GitHub retains them.

Why there are two SBOMs

You need both to see the whole picture, because neither one is complete on its own.

  • sbom-image.cdx.json is generated by scanning the built container image. It covers the Alpine base packages and the Go module graph of the server binary and the bundled agent binaries.
  • sbom-source.cdx.json is generated by scanning the source tree. It covers the npm dependency tree.

The npm tree does not appear in the image scan. The web interface is bundled by Vite and then compiled directly into the Go binary, so by the time it reaches the image there is no npm metadata left for a scanner to find. Scanning the source tree is the only way to enumerate those dependencies.

One component is in neither file: the SCAP security policy content used for compliance scanning is downloaded from the ComplianceAsCode project during the image build. It is data rather than a package, so it carries no metadata for a scanner to catalogue. The version is fixed per release by the SSG_VERSION build argument.

Verifying an image you have already pulled

Verification works against a digest, so you can check an image that is already on the host:

docker image inspect ghcr.io/patchmon/patchmon-server:2.0.3 \
  --format '{{index .RepoDigests 0}}'

Feed the resulting name@sha256:... value to gh attestation verify with the oci:// prefix.

Troubleshooting

"no attestations found"

Attestations start with the release in which this chapter first appeared. Earlier releases have none, and this is expected rather than a sign of tampering. Check the release notes for the first version that carries them.

The edge tag is built from the main branch and does carry attestations, but the tag moves with every merge, so verify edge by digest rather than by tag.

Verification fails on an image from a mirror or a private registry

The attestation is bound to the image digest, not to its location, so a straight copy still verifies. A mirror that re-compresses or rebuilds layers changes the digest and verification will correctly fail. Verify against GHCR, then copy by digest.

Verifying a copy held in your own registry

Attestations are recorded in the GitHub attestation store as well as being attached to the image, so verification does not depend on where the image is stored. A copy mirrored into your own registry verifies normally, provided it was copied by digest. Point gh attestation verify at your own reference with the oci:// prefix.

See also

  • Chapter 1: Installing PatchMon Server on Docker
  • Chapter 8: Installing the PatchMon Agent
  • Our security policy and vulnerability reporting process: SECURITY.md in the repository