Workflow Integration

Once the BOMnipotent Server is up and running, you can begin uploading documents. A typical workflow begins with generating an SBOM, an inventory of what components your product contains. This step should be automatically triggered upon creating a new release. Then this inventory is scanned for known vulnerabilities. This should also be an automatic step, but it should be triggered periodically. If a vulnerability is discovered, it is time to analyse it. This task needs expertise, and thus cannot be done automatically. The result of this analysis is a CSAF document, which tells your users if and how they have to react to this new development.

This section contains instructions for an exemplary workflow for generating an SBOM using Syft , scanning it for vulnerabilities with Grype , and creating CSAF documents with secvisogram . Other programs exists for these tasks, and as long as they produce valid CycloneDX and CSAF json documents, they are compatible with BOMnipotent.

Mar 16, 2025

Subsections of Workflow Integration

SMTP Server

Direct SMTP Server Communication

To let BOMnipotent Server directly communicate with your email server, set up the smtp part of your configuration file to look roughly like this:

[smtp]
user = "you@yourdomain.com"
endpoint = "your.smtp.host"
secret = "${SMTP_SECRET}"

The exact form will strongly depend on your email provider. Some may require the full email address as the user, others may not.

Clients like Mozilla Thunderbird are usually very good at deducing the required parameters. If you’re at a loss, look for them there.

Communication via SMTP Relay

If you have more than one service that sends email it can be benficial to locally run an SMTP relay station. It offers a single endpoint for your setup to communicate with the mail server.

There are several docker containers that offer SMTP relay functionality. This tutorial focuses on crazymax/msmtpd , because it has the best security posture among the lightweight solutions.

Running Relay via Docker Compose

Add the following service to your compose.yaml file:

  smtp_relay:
    container_name: smtp_relay
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: "512M"
    environment:
      TZ: Europe/Berlin # Replace with your preferred timezone
      SMTP_HOST: your.smtp.host # Replace with the correct endpoint
      SMTP_PORT: 465
      SMTP_TLS: on
      SMTP_STARTTLS: off
      SMTP_TLS_CHECKCERT: on
      SMTP_AUTH: login
      SMTP_USER: you@yourdomain.com # Replace with your username
      SMTP_FROM: you@yourdomain.com # Replace with your email address
      SMTP_PASSWORD: ${SMTP_PASSWORD}
      SMTP_DOMAIN: localhost
    healthcheck:
      test: ["CMD", "msmtp", "--version"]
      interval: 30s
      timeout: 10s
      retries: 3
    image: crazymax/msmtpd
    logging:
      driver: local
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - smtp_network
    restart: unless-stopped

This will spin up the container, connecting to Port 465 (the default for SMTPS protocol) of the SMTP Host, encrypting with TLS and not STARTTLS. It will listen on port 2500, which is not obvious from the input but the default behaviour of msmtp.

The modification of your compose file is not yet done, though!

Under networks, you have to declare the smtp_network:

networks:
  smtp_network:
    driver: bridge
    name: smtp_network

You also need to add the network to any container that is supposed to contact it. You may also want for these containers to depend on the smtp_relay, so that they don’t start before the relay station is ready:

  bomnipotent_server:
    container_name: bomnipotent_server
    depends_on:
      smtp_relay:
        condition: service_healthy
    ...
    networks:
      - smtp_network

Aside from the modifications to the compose file, your .env file or your environment needs to provide the secret or password for your mail provider:

SMTP_PASSWORD=eHD5B6S8Kze3
export SMTP_PASSWORD=eHD5B6S8Kze3
set SMTP_PASSWORD=eHD5B6S8Kze3
$env:SMTP_PASSWORD = "eHD5B6S8Kze3"

In your BOMnipotent Server config file, you can now modify your smtp section to connect to the relay via the docker network:

[smtp_config]
user = "you@yourdomain.com"
endpoint = "smtp://smtp_relay:2500"

Running Relay in standalone Docker Container

If your setup does not have a compose file, you can instead run the container using Docker directly. Make sure that your environment provides a value for SMTP_PASSWORD, and then run

docker run --detach -p 2500:2500 --name smtp_relay \
    -e TZ=Europe/Berlin \
    -e SMTP_HOST=your.smtp.host \
    -e SMTP_PORT=465 \
    -e SMTP_TLS=on \
    -e SMTP_STARTTLS=off \
    -e SMTP_TLS_CHECKCERT=on \
    -e SMTP_AUTH=login \
    -e SMTP_USER=you@yourdomain.com \
    -e SMTP_FROM=you@yourdomain.com \
    -e SMTP_PASSWORD=${SMTP_PASSWORD} \
    -e SMTP_DOMAIN=localhost \
    crazymax/msmtpd

This does basically the same as the section suggested for the compose file. You again need to replace the values for TZ, SMTP_HOST, SMTP_USER and SMTP_FROM with the ones for your email provider.

The command above exposes the port 2500 to localhost, which is why your BOMnipotent config needs to be as follows:

[smtp_config]
user = "you@yourdomain.com"
endpoint = "smtp://localhost:2500"

To stop the container, run:

docker stop smtp_relay
May 17, 2025

SBOMs with Syft

A Software Bill of Material (SBOM) is a list of all software components used in your product. In the contex of supply chain security, it serves as a machine-readable list of items to compare to whenever a new vulnerability surfaces.

Several tools exist to automatically generate such an SBOM. This tutorial focuses on Anchore’s Syft, an open source command line tool.

Setup

The official Syft GitHub repo contains installation instructions. It is available via a shell script or via various package managers.

On some linux systems you may want to change the install path (the very last argument to the shell command) to “~/.local/bin”, because “/usr/local/bin” requires root permissions to modify.

Usage

The basic usage of Syft is:

syft <target> [options]

Additionally, some configuration can be made using environment variables.

Syft supports lockfiles, directories, container images and more as targets.

Lockfile

An example call looks like this:

SYFT_FORMAT_PRETTY=1 syft Cargo.lock --output cyclonedx-json=./sbom.cdx.json --source-name="BOMnipotent" --source-version="1.0.0"
SYFT_FORMAT_PRETTY=1 syft Cargo.lock -o cyclonedx-json=./sbom.cdx.json --source-name="BOMnipotent" --source-version="1.0.0"
Breakdown:

  • ‘SYFT_FORMAT_PRETTY=1’ is makes this call with an environment variable that tells Syft to use prettified output. This only serves to make the resulting json easier readable for humans. See here for a full list of configurations.
  • ‘syft’ cals the Syft program.
  • ‘Cargo.lock’ tells Syft to analyse the lockfile of the Rust ecosystem.
  • ‘–output cyclonedx-json=./sbom.cdx.json’ specifies that the output is to be stored in the CycloneDx JSON format in the file ‘./sbom.cdx.json’.
  • ‘–source-name=“BOMnipotent”’ explains to Syft that these are the sources for the BOMnipotent component, which it may not automatically detect in all cases.
    • The CycloneDX schema may not require a component name, but BOMnipotent does.
  • Likewise ‘–source-version=“1.0.0”’ tells Syft the current version of your project.
    • If you do not provide a version, BOMnipotent will try to use the timestamp as a version string instead.

Syft supports a wide range of ecosystems, which is listed on their GitHub repo .

Directory

Letting Syft loose on a whole directory is possible, but overdoes it in most situations. It will go through all subdirectories and collect everything that looks remotely like a lockfile, including all your test dependencies, development scripts and GitHub Actions.

syft . --output cyclonedx-json=./dev_env_sbom.cdx.json --source-name="BOMnipotent Development Environment" --source-version=1.2.3
syft . -o cyclonedx-json=./dev_env_sbom.cdx.json --source-name="BOMnipotent Development Environment" --source-version=1.2.3

Container

If you have a docker container exported as a ‘.tar’ file you can also specify that as a target:

syft container.tar --output cyclonedx-json=./container_sbom.cdx.json --source-name="BOMnipotent Container" --source-version=1.2.3
syft container.tar -o cyclonedx-json=./container_sbom.cdx.json --source-name="BOMnipotent Container" --source-version=1.2.3

For compiled languages the results will be vastly different, because most information about the components that went into compilation is lost. On the other hand, this SBOM contains information about the environment that your product may later run in.

Final remarks

When deciding on a target, it is important to think about the scope of your application: What do you ship to the customer? Up to which extend are you responsible for the supply chain of your product? If in doubt, there’s no harm in uploading more than one variant of a BOM, as long as product name or version are different.

Once your SBOM is generated, you can use BOMnipotent Client to upload it to BOMnipotent Server .

After that you can use Grype to periodically scan for vulnerabilities .

May 31, 2025

Vulnerabilities with Grype

Once your SBOM is generated, it is time to continuously scan it for vulnerabilities. Note that some laws, for example the EU’s Cyber Resiliance Act, require that products are released without any known vulnerability. The first scan should therefore happen before a release.

There are several tools for scanning a product for supply chain vulnerabilities. This tutorial uses Anchore’s Grype, because it integrates well with Anchore’s Syft from the SBOM tutorial . Like Syft, Grype is an open source command line utility.

Setup

The official Grype GitHub repo contains installation instructions. Like for Syft, you may want to change the install path (the very last argument to the shell command) to ‘~/.local/bin’, because ‘/usr/local/bin’ requires root permissions to modify.

Usage

With an SBOM at hand, scanning for vulnerabilities is very easy:

grype sbom:./sbom.cdx.json --fail-on low
grype sbom:./sbom.cdx.json -f low
 ✔ Scanned for vulnerabilities     [2 vulnerability matches]  
   ├── by severity: 0 critical, 0 high, 2 medium, 0 low, 0 negligible
   └── by status:   2 fixed, 0 not-fixed, 0 ignored 
NAME    INSTALLED  FIXED-IN  TYPE        VULNERABILITY        SEVERITY 
ring    0.17.8     0.17.12   rust-crate  GHSA-4p46-pwfr-66x6  Medium    
rustls  0.23.15    0.23.18   rust-crate  GHSA-qg5g-gv98-5ffh  Medium
[0000] ERROR discovered vulnerabilities at or above the severity threshold

When running this command, Grype checks several vulnerability databases for matches with the components provided in the sbom. The ‘fail-on’ option specifies that it exits with a non-zero error code if any with severity ’low’ or higher is discovered.

The syntax to export a vulnerability report consumable by BOMnipotent is similar to Syft:

grype sbom:./sbom.cdx.json --output cyclonedx-json=./vuln.cdx.json
grype sbom:./sbom.cdx.json -o cyclonedx-json=./vuln.cdx.json

Grype integrates well with BOMnipotent. You can use the “bom get” command of BOMnipotent Client to directly print the contents of a BOM to the console output, and then pipe it to grype:

bomnipotent_client bom get <BOM-NAME> <BOM-VERSION> | grype --output cyclonedx-json=./vuln.cdx.json
bomnipotent_client bom get <BOM-NAME> <BOM-VERSION> | grype -o cyclonedx-json=./vuln.cdx.json

May 31, 2025

CI/CD

BOMnipotent is built with automation in mind. By design, it never prompts for an interactive user input and is thus fully scriptable.

This page describes how to use BOMnipotent Client inside your CI/CD pipeline to upload BOMs with every release, and to check for vulnerabilities on a daily basis.

Weichwerke Heidrich Software has created several ready-to-use GitHub A to integrate BOMnipotent into your pipeline. Most of them are intentionally based on bash scripts, to make the transfer to other pipeline environments as easy and seamless as possible.

Prerequisites

This setup assumes that you have a BOMnipotent Server instance up and running. Consult the setup instructions if that is not yet the case.

The pipeline further requires that the server contains an approved robot user account with the BOM_MANAGEMENT and VULN_MANAGEMENT permissions. The sections on account creation and access management contain more information on how to creat one.

Setup BOMnipotent Client

The task of making executables available is where various pipeline environments show the most differences. Luckily, it is also the task that DevOps engineers figure out very early on. This task is therefore currently only described for GitHub Actions, where a ready-to-use step exists.

GitHub Action

The action to setup BOMnipotent Client can be found on the GitHub Marketplace .

A typical snippet from you workflow yaml file looks like this (except for the indentation, because yaml…):

- name: Install BOMnipotent Client
  uses: Weichwerke-Heidrich-Software/setup-bomnipotent-action@v1
  with:
    domain: '<your-server-domain>'
    user: '<your-robot-user>'
    secret-key: ${{ secrets.CLIENT_SECRET_KEY }} 

A more complete example can be found on GitHub .

The three parameters are optional, but recommended:

  • domain: Provide the full domain (including subdomain) to your BOMnipotent Server instance. It will be stored in a session, so that you do not have to provide in subsequent calls.
  • user: Store the username of your robot user, which you have set up during the prerequisites section.
  • secret-key: Provide a reference to the secret key used to authenticate the robot user. You can make it available to your pipeline via <your repo> → Settings → Secrets and variables → Actions → New repository secret. In the example above, it is named “CLIENT_SECRET_KEY”.

Caution: Do not directly store your secret key inside the pipeline, or any other versioned file. This is what the GitHub secret mechanism is designed to handle.

After running this action, the command “bomnipotent_client.exe” (Windows) or “bomnipotent_client” (UNIX) is available for the rest of the job. Because different jobs run inside different containers, it may be necessary to call the setup action several times throughout the workflow.

Upload BOMs

A Bill of Materials (BOM) is a list of the components of a product. It is a static document, and thus each BOM is closely associated with a specific release of the product. Whenever the components of the (released) product change, it should be tagged with a new version, and associated with a new BOM.

This is why the step of uploading a BOM to the server should be run as part of the release pipeline.

Following the single responsibility principle, the action to upload BOMs has some prerequisites:

  • The BOMnipotent Client command has to be available. There is a separate action that ensures just that.
  • The action requires a BOM in CycloneDX format as input, which has to be generated in an earlier step.

The task of generating a BOM is highly specific to the product. The ideal tool depends on the ecosystem and how it is used. Syft is one tool that can assist, and that offers a ready-to-use GitHub action . It is by far not the only option: The CycloneDX Tool Center offers many alternatives.

GitHub Action

An often used pattern is to trigger the release pipeline upon pushing a tag that corresponds to a semantic version:

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

After setting up BOMnipotent Client and generating the BOM, it can be uploaded with the following snippet:

- name: Upload BOM
  uses: Weichwerke-Heidrich-Software/upload-bom-action@v0
  with:
    bom: './bom.cdx.json'
    name: '${{ github.event.repository.name }}'
    version: '${{ github.ref_name }}'
    tlp: 'amber'

The action accepts several arguments:

  • bom: This is the only mandatory argument, it needs to point to an existing file containing a BOM in CycloneDX format. In this example, the bom was stored under “./bom.cdx.json”.
  • name / version: The CycloneDX standard does not require a BOM to have a name or version, but BOMnipotent does. If the tool used to create the BOM does not offer to set the name and/or version, you can overwrite them via arguments. In the example above, the repository name is used as the product name, and the triggering tag as the version. You may instead also specify a path to a file: The line version: './version.txt' for example would tell the action to read the file “./version.txt” and use the contents as the version string.
  • tlp: You can specify a TLP label to classify the uploaded BOM. If not provided, the server’s default TLP applies.

A full list of arguments can be found on the GitHub marketplace .

Other Pipeline

The GitHub action is merely a wrapper for a bash script. To upload a BOM using your pipeline infrastructure you can download and use the script from the repo.

if [ ! -f ./upload_bom.sh ]; then
    curl https://raw.githubusercontent.com/Weichwerke-Heidrich-Software/upload-bom-action/refs/heads/main/upload_bom.sh > ./upload_bom.sh
    chmod +x ./upload_bom.sh
fi
./upload_bom.sh <bom.cdx.json> <optional args...>

The script takes the same arguments as the action does, except that the bom argument is positional, and that optional arguments need to be prefixed with a double hyphen:

./upload_bom.sh ./bom.cdx.json --name <product-name> --version <product-version> --tlp amber

Check for Vulnerabilities

While BOMs are static objects documenting the composition of a product, vulnerabilities associated with a BOM need to be checked regularly. Most vulnerability databases are updated on a daily basis, and vulnerability checks should match that frequency.

The BOMnipotent vulnerability action has two substeps:

  • Update the known vulnerabilities associated with all BOMs on the server.
  • Check the server for new, unassesed vulnerabilities.

Both steps can be skipped individually, in case they do not fit your use-case.

GitHub Action

To run the vulnerability check every day at 3:00 am (for example), add the following trigger to your workflow yaml:

on:
  schedule:
    - cron: '0 3 * * *' # Runs the workflow every day at 03:00 UTC

Once you have set up BOMnipotent Client on the agent, the snippet for handling the vulnerability checks is very simple:

- name: Update Vulnerabilities
  uses: Weichwerke-Heidrich-Software/vulnerability-action@v0

A complete example can be found in the GitHub marketplace .

The script downloads all BOMs accessible to the robot user, checks it against several databases of known vulnerabilities, and updates them on the server.

It then checks if there are any unassesed vulnerabilities. A vulnerability is unassessed if BOMnipotent Server does not contain a CSAF document associated with it.

Other Pipeline

Analogously to the upload step , the action is mostly a wrapper for a bash script. You can reference the script in the repo, or download and use it directly:

if [ ! -f ./upload_bom.sh ]; then
    curl https://raw.githubusercontent.com/Weichwerke-Heidrich-Software/vulnerability-action/refs/heads/main/update_vulns.sh > ./update_vulns.sh
    chmod +x ./update_vulns.sh
fi
./update_vulns.sh

The script internally uses grype to check for vulnerabilities. If you use the script directly, you will need to ensure that the program is installed.