Automate your Bill of Materials

Use Chalk to fulfill pesky security supply chain compliance requests, and tick pesky compliance checkboxes fast and easy

The US Government, several large corporations, and the Linux foundation with their SBOM Everywhere initiative are all driving the industry to adopt a series of requirements around “software supply chain security,” including:

  • SBOMs (Software Bills of Materials) to show what 3rd party components are in the code.

  • Code provenance data that allows you to tie software in the field to the code commit where it originated from and the build process that created it.

  • Digital signatures on the above.

This information is complex and tedious to generate and manage, but we can easily automate this with Chalkā„¢.

As a big bonus, with no extra effort, you can be SLSA level 2 compliant before people start officially requiring SLSA level 1 compliance.

Let’s see how.

Load Chalk configurations

First, turn on automatic collection and embedding your SBOM by loading our pre-built configuration:

chalk load https://chalkdust.io/run_sbom.c4m
chalk load https://chalkdust.io/embed_sbom.c4m

These configurations augment the default metadata Chalk collects as follows:

  • run_sbom.c4m - Enables the collection of SBOMs (off by default because on large projects, this can add a small delay to the build)
  • embed_sbom.c4m - For simplicity it will embed SBOM findings into the chalk mark which is going to be embedded into the artifact. Note that SBOM findings can be large and can increase the artifact size significantly. If that is a concern, we recommend shipping SBOM data to and external sink such as either S3 or an API.

You can check that the configuration has been loaded by running:

$ chalk dump
use run_sbom from "https://chalkdust.io"
use embed_sbom from "https://chalkdust.io"

Next we’ll turn on digital signing.

Turn on digital signing

Generate a signing key for Chalk:

$ chalk setup

------------------------------------------
CHALK_PASSWORD=kplOibksg6N8Qr7WbWgfeQ==
------------------------------------------
Write this down. In future chalk commands, you will need
to provide it via CHALK_PASSWORD environment variable.

info:  attestation: Configured attestation key. Saving it to chalk binary.
info:  attestation: signing file /home/admin/chalk
info:  Configuration replaced in binary: /home/admin/chalk
info:  /home/admin/.local/chalk/chalk.log: Open (sink conf='default_out')
info:  Full chalk report appended to: ~/.local/chalk/chalk.log

Then export the CHALK_PASSWORD environment variable you get (which will be different than you see here).

To learn more about this process, read the Attestation guide.

Syft

Chalk uses Syft under the hood to scan project and package metadata and pull together an SBOM. Chalk will install it internally if you don’t already have it, but it will be useful to install it ourselves so we can run reports on Chalk-marked artifacts.

curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin

Now let’s take a look how SBOM collection works with and without Docker.

SBOM without Docker

Grab the Caddy sources (a webserver in Go) and build it.

git clone https://github.com/caddyserver/caddy
cd caddy
go build ./cmd/caddy

Now run chalk insert ./caddy to Chalk the binary.

$ chalk insert ./caddy > /dev/null
info:  attestation: signing file /home/admin/caddy/caddy
info:  /home/admin/caddy/caddy: chalk mark successfully added
info:  /home/admin/.local/chalk/chalk.log: Open (sink conf='default_out')
info:  Full chalk report appended to: ~/.local/chalk/chalk.log

The SBOM is included in the report generated by chalk insert. We can query it with jq and store it so we can report on it with Syft.

jq -c -r '.[]|._CHALKS[]?|.SBOM.syft' ~/.local/chalk/chalk.log | tail -n1 > sbom.json

Now ask Syft to pretty-print it.

$ syft convert sbom.json -o table
syft convert sbom.json -o table
[0000]  WARN convert is an experimental feature, run `syft convert -h` for help
NAME                                                               VERSION                                      TYPE
cel.dev/expr                                                       v0.25.1                                      go-module
cloud.google.com/go/auth                                           v0.18.2                                      go-module
cloud.google.com/go/auth/oauth2adapt                               v0.2.8                                       go-module
cloud.google.com/go/compute/metadata                               v0.9.0                                       go-module
dario.cat/mergo                                                    v1.0.2                                       go-module

... and so on ...

google.golang.org/api                                              v0.272.0                                     go-module
google.golang.org/genproto/googleapis/api                          v0.0.0-20260406210006-6f92a3bedf2d           go-module
google.golang.org/genproto/googleapis/rpc                          v0.0.0-20260406210006-6f92a3bedf2d           go-module
google.golang.org/grpc                                             v1.80.0                                      go-module
google.golang.org/protobuf                                         v1.36.11                                     go-module
gopkg.in/yaml.v3                                                   v3.0.1                                       go-module
stdlib                                                             go1.25.0                                     go-module

But it’s not just stored in the Chalk logs, it’s stored in the ./caddy binary itself. Let’s delete the Chalk logs and then run chalk extract to pull out the Chalk marks from the binary.

rm ~/.local/chalk/chalk.log
chalk extract ./caddy

And we can pull out the SBOM again.

jq -c -r '.[]|._CHALKS[]?|.SBOM.syft' ~/.local/chalk/chalk.log | tail -n1 > sbom.json

And report on it again with Syft.

$ syft convert sbom.json -o table
syft convert sbom.json -o table
[0000]  WARN convert is an experimental feature, run `syft convert -h` for help
NAME                                                               VERSION                                      TYPE
cel.dev/expr                                                       v0.25.1                                      go-module
cloud.google.com/go/auth                                           v0.18.2                                      go-module
cloud.google.com/go/auth/oauth2adapt                               v0.2.8                                       go-module
cloud.google.com/go/compute/metadata                               v0.9.0                                       go-module
dario.cat/mergo                                                    v1.0.2                                       go-module

... and so on ...

google.golang.org/api                                              v0.272.0                                     go-module
google.golang.org/genproto/googleapis/api                          v0.0.0-20260406210006-6f92a3bedf2d           go-module
google.golang.org/genproto/googleapis/rpc                          v0.0.0-20260406210006-6f92a3bedf2d           go-module
google.golang.org/grpc                                             v1.80.0                                      go-module
google.golang.org/protobuf                                         v1.36.11                                     go-module
gopkg.in/yaml.v3                                                   v3.0.1                                       go-module
stdlib                                                             go1.25.0                                     go-module

Now let’s take a look at the same thing in Docker.

SBOM with Docker

Things are a little different in Docker world. First off we cannot use chalk insert to inject a container because containers are immutable. Instead, Chalk wraps Docker build commands and injects Chalk marks into the container during the build process.

Second, we collect two categories of SBOMs. Keys like "SBOM" are for the repository-level SBOM as above. Keys like "IMAGE_SBOM" are for image-level SBOMs.

Repository-level SBOMs are embedded into the Docker image if you use the embed_sbom config. Image-level SBOMs are collected after an image is built, thus cannot be embedded into the image. They are either stored in your Docker registry when you build with buildx --sbom=true and push the image to a repository (we’ll do this later). These image-level SBOMs can be recovered when you chalk extract $registry/$image.

(We have a fallback where if you do not build the image that way, the image-level SBOMs are captured and emitted to your Chalk logs on build. They will not be recovered when you chalk extract.)

Let’s set up a registry locally to test.

A local Docker registry

First off, in this local playground, we must set up the link-local address as an insecure registry in Docker settings. And when we push the image to the registry we’ll refer to the link-local address, not localhost.

Grab a link-local address (you may have multiple) and export it in an environment variable.

export LOCAL_IP=$(hostname -I | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)

Add that IP address as an insecure registry in /etc/docker/daemon.json:

printf "
{
    \"insecure-registries\": [
        \"$LOCAL_IP:5000\"
    ]
}" | sudo tee /etc/docker/daemon.json

And restart the Docker service.

sudo systemctl restart docker

Now run a Docker registry.

docker run -d -p 5000:5000 --name registry --rm registry:2

Now let’s build a Postgres image.

Postgres image source

First, grab the sources for the official image packaging for Postgres.

cd ~
git clone https://github.com/docker-library/postgres
cd postgres

Now build the Alpine Postgres 18 image with Chalk.

cd 18/alpine3.23
chalk docker buildx build --sbom=true -t $LOCAL_IP:5000/postgres:18-alpine3.23 . --push

Chalk will generate a build report when we do this, reporting on both repository-level bill of materials and image-level bill of materials. i.e. what’s installed by your language package manager and what’s installed by your distro package manager.

We can find the repository-level bill-of-materials with the following jq filter.

jq -c -r '.[]|.SBOM.syft' ~/.local/chalk/chalk.log > sbom.json

And we can analyze it with the Syft CLI.

$ syft convert sbom.json -o table
NAME                     VERSION  TYPE
actions/checkout         v4       github-action  (+1 duplicate)
docker-library/bashbrew  HEAD     github-action

This isn’t very interesting because Postgres has no language-level dependencies. Instead we must check the image-level bill-of-materials.

jq -c -r '.[]|._CHALKS[]|._IMAGE_SBOM' ~/.local/chalk/chalk.log > sbom.json

Let’s analyze it with the Syft CLI.

$ syft convert sbom.json -o table
[0000]  WARN convert is an experimental feature, run `syft convert -h` for help
NAME                      VERSION           TYPE
.postgresql-rundeps       20260406.192256   apk
alpine-baselayout         3.7.1-r8          apk
alpine-baselayout-data    3.7.1-r8          apk
alpine-keys               2.6-r0            apk
alpine-release            3.23.3-r0         apk
apk-tools                 3.0.3-r1          apk
bash                      5.3.3-r1          apk
brotli-libs               1.2.0-r0          apk

... omitted for brevity ...

readline                  8.3.1-r0          apk
scanelf                   1.3.8-r2          apk
ssl_client                1.37.0-r30        apk
stdlib                    go1.24.6          go-module
tzdata                    2026a-r0          apk
xz-libs                   5.8.2-r0          apk
zlib                      1.3.2-r0          apk
zstd                      1.5.7-r2          apk
zstd-libs                 1.5.7-r2          apk

We get a nice list of image-level dependencies that constitue the image.

If we delete the Chalk log and run chalk extract against the image we can recover the SBOM.

rm ~/.local/chalk/chalk.log
chalk extract $LOCAL_IP:5000/postgres:18-alpine3.23

The repository-level SBOM is at a different location in the extract report for a Docker image.

jq -c -r '.[]|._CHALKS[]|.SBOM.syft' ~/.local/chalk/chalk.log > sbom.json

And the image-level SBOM is in the same place as it was in the chalk docker buildx build report.

jq -c -r '.[]|._CHALKS[]|._IMAGE_SBOM' ~/.local/chalk/chalk.log > sbom.json

And you can analyze the JSON with Syft just as above.

Bulk extraction

To extract Chalk marks from all images, we can run chalk extract images; similarly, chalk extract containers will extract Chalk marks from all running containers. (Warning: running extract on all images or containers will take a long time, and is not generally recommended.)

To extract on a specific artifact, you can pass a list of locations (or image/container names) into the extract command as demonstrated above.

The extract operation will pull all the metadata that Chalk saved during the insert or docker build operations and log it, showing only a short summary report to your console.