Software Bills of Materials (SBOM) are something I rarely think about actively. They’re useful, yes, but for most projects I just make it part of my CI/CD pipeline (mostly because it’s in my templates) along with some sanity checks, and forget about it.
This year, however, is different. I’ve been living through this weird phase of frequency illusion where the topic seems to come up in every project I touch. A lot of the due diligence (DD) projects I’ve been involved this year have had some major or minor hiccups related to the checks, and I also have had to think about them in my role as Security Consultant for the CIVITAS/Core project, so I decided to document some best practices and fundamentals while they’re fresh in my mind.
SBOMs are rarely useful day-to-day, even though they provide a comprehensive insight into your dependency chain. But truth be told, if you don’t have any automated checks in place, you likely won’t derive any personal value from having one.
All is not lost, however. Many code hosting platforms have some built in facilities for working with SBOMs. GitLab, for instance, has some dependency scanning capabilities, including helping you find vulnerabilities in your dependencies. GitHub can even generate SBOMs for you. You should probably invest some time into finding out what’s there for your platform if you want to get the most out of them for you and your project. At the very least it can help you perform a license check every once in a while to help you sleep at night.
Apart from that, though, it’s also often required for compliance reasons, and is often asked for in DD processes (for instance by yours truly). It’s just good to have them at hand when someone asks for them, even if you don’t really use them yourself (even though you really should).
And no, it’s probably not a good idea to produce this list manually, or to jumble something together ad-hoc when someone asks for it. They will be able to tell, and your list will probably be incomplete. It’s neither useful nor a good look.
SBOMs are usually pretty straightforward, but there are some general rules I usually abide by when dealing with them, and I want to share them with you.
Firstly, there are two major formats for them: CycloneDX and SPDX. I personally prefer CycloneDX, but you likely don’t have to care. Pick the one that has better support across your ecosystems and stay consistent.
What should be included? While this depends on the project, I’d say by default only what makes it to production. If you have build and test dependencies, you can include them with a special flag, but it likely will only produce unwanted noise and be a nuisance (which will lower the value provided by the tool). If you ship containers or infrastructure, include the base images and their package lists as well. That will ensure that you get nudged when it’s time to update.
Finally, you’ll probably run into Vulnerability Exploitability eXchange (VEX) sooner or later. You’ll first use it to squash those CVEs that are somewhere in your infrastructure, but definitely not exploitable. In an ideal world, they wouldn’t be there, but you made sure they’re not an issue. VEX helps you mark them.
There are other things you can think about when you are really truly bored, like serial numbers, Package URLs (purls), SLSA provenance attestations, general signatures and in-toto attestations in particular, and many more things, but I’d hope that your tool of choice does most of this for you and ships defaults that let you sleep at night. I certainly don’t want to have to think about any of it.
If you do sign, though, make sure I can verify the signatures, please and thank you.
To keep it brief and actionable, here’s a minimum I would expect you to have if you provide a SBOM at all, both as a security person and as part of the documentation provided during a DD or audit:
- It gets autogenerated as part of your CI/CD, or is at least reproducible. It matches the reality of your system and captures all of its parts that reach production.
- If you track vulnerabilities, you track which ones are relevant using VEX (and why) and do not expect me to wade through a massive backlog of issues manually every time.
- I can diff it somehow. Either the format you produce is inherently diffable and I can use a tool of choice, or your provider (code hosting or CI/CD) automatically tracks diffs somehow.
- If you sign any parts of it (you probably should), make sure that I can verify the signature.
And really, that’s all there is to it. Sure, you can go fancy with it if you’d like, but in my opinion most of the value is captured here.
I said above that how you set things up will depend on your technologies, ecosystems, and platforms, but I don’t like to keep things fully theoretical, so I thought I’d provide at least one minimum example.
Let’s say you run a web application using Django/Python on the backend, React/TS on the frontend, and you use Gitlab for code hosting and CI/CD. You have a monorepo and the code lives in the frontend
and backend
directories, respectively. A simple addition to your .gitlab-ci.yml
would be:
stages:
- sbom
sbom_backend:
stage: sbom
image: python:3.12-slim
before_script:
- python -m pip install --upgrade pip
- pip install pip-audit
script:
# Generate CycloneDX SBOM from locked requirements (no app execution)
- pip-audit -r backend/requirements.txt -f cyclonedx -o gl-sbom-backend.cdx.json
artifacts:
reports:
cyclonedx: gl-sbom-backend.cdx.json
paths:
- gl-sbom-backend.cdx.json
when: always
expire_in: 1 week
sbom_frontend:
stage: sbom
image: node:20-alpine
before_script:
- cd frontend
- npm ci --omit=dev
script:
# Generate CycloneDX SBOM from npm metadata (prod deps only)
- npx @cyclonedx/cyclonedx-npm --omit dev --output-format json --output-file ../gl-sbom-frontend.cdx.json
artifacts:
reports:
cyclonedx: gl-sbom-frontend.cdx.json
paths:
- gl-sbom-frontend.cdx.json
when: always
expire_in: 1 week
You can then also add validation, like so:
sbom_validate:
stage: sbom
image: cyclonedx/cyclonedx-cli:latest
needs: ["sbom_backend", "sbom_frontend"]
script:
- cyclonedx validate --input-file gl-sbom-backend.cdx.json --input-format json
- cyclonedx validate --input-file gl-sbom-frontend.cdx.json --input-format json
And if you want to include your containers as well, you can add a step using syft
:
sbom_image:
stage: sbom
image: anchore/syft:latest
script:
- syft <your-image>:tag -o cyclonedx-json > gl-sbom-image.cdx.json
artifacts:
reports:
cyclonedx: gl-sbom-image.cdx.json
paths:
- gl-sbom-image.cdx.json
It’s a non-negligible amount of YAML, but it’s pretty trivial, and by setting artifacts
above, you will also be able to use GitLab’s security reports and general SBOM UI.
I really don’t know how I ended up here. SBOMs seem to be having a moment, at least in my neck of the woods. I hope this was helpful, and next time I review an SBOM I can just do a quick pass and move on with my life.