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.