Supply-chain monitoring

Software used in this article is locked to specific versions to increase the chance of it working as specified. Make sure you update all of it if you want to actually start using it after experimenting. I’ve had this article lying around for a bit before publishing, so versions are pretty outdated even at time of publish.

Automate building inventory specifications for your applications and monitor all those dependencies for known vulnerabilities and license issues. Set and enforce global policies to break builds. In this post we’ll cover:

You can expand on this to cover all software used and produced in your organization.

Build machine images for VMs? produce inventory specifications for them and add checks to enforce policies. Are you using closed source software? appliances? have your vendors produce specifications. Maybe you’re also a vendor, produce specifications and make them available to your customers, let them know that you care.

Setup inventory monitoring

We’ll be using OWASP DependencyTrack, they supply docker images for the software components and there’s also a helm chart available as a starting point.

To quickly get started trying it out we’ll just start it up locally with docker-compose. Create a docker-compose.yml file with this content:

# docker-compose.yml
version: '3.7'

volumes:
  dependency-track:

services:
  dtrack-apiserver:
    image: dependencytrack/apiserver
    ports:
      - '8081:8080'
    volumes:
      - 'dependency-track:/data'
    restart: unless-stopped

  dtrack-frontend:
    image: dependencytrack/frontend
    depends_on:
      - dtrack-apiserver
    environment:
      - API_BASE_URL=http://localhost:8081
    ports:
      - "8080:8080"
    restart: unless-stopped

Then start it up:

# -d is detached mode, execute 'docker-compose down' to stop
docker-compose up -d
firefox http://localhost:8080

You should now be looking at the login screen for your local instance of DependencyTrack. Log in with admin/admin as credentials and change the password when prompted. Go to administration -> access management -> teams -> automation and make a note of what the api key is, we’ll need it for posting SBOM (software bill of materials) files to it’s api.

When starting up it will create local mirrors for vulnerability sources, so leave it running for a bit to avoid issues. Leaving it running while populating inventory, configuring policies and trying them out is likely enough, but you can check on the progress in the logs:

docker-compose logs

Build and populate inventory

We now need to create SBOM files for everything we want to keep track of in DependencyTrack. We’ll go through a few of them here and if you need more you can take a look at the CycloneDX tool center. These tools also include options for scanning platforms at runtime for particular cases, which might be an alternative if there’s a tool for your platform and you’re not interested in blocking stuff earlier in the process.

An important thing to note is that if you have more than one tool producing SBOM specifications you’ll need to merge them before uploading. DependencyTrack does not support incremental changes, which makes sense, how on earth would it ever know about removed dependencies otherwise. We’ll produce the specification from each tool and then have a later step pick them all up, merge them and upload them to the inventory service.

Containers

Container scanning with syft identifies OS packages from apk, dpkg, nix and rpm and most popular development ecosystems. They do have an incentive to nudge you into using their paid services, but there’s lot’s of alternatives and it’s a great start in populating your inventory.

# !/bin/bash

set -eu

IMAGE=$1

SYFT_VERSION="0.33.0"
REPORTS_DIRECTORY="$(pwd)/sbom"
DEPENDENCY_TRACK_URI="localhost:8081"
DEPENDENCY_TRACK_API_KEY="dependencytrack api key"

if [ ! -d "$REPORTS_DIRECTORY" ]; then
  echo "Creating reports directory: $REPORTS_DIRECTORY"
  mkdir -p "$REPORTS_DIRECTORY"
fi

if ! hash syft 2>/dev/null || ! syft version | grep -q "$SYFT_VERSION"; then
  echo "Install image inventory tool"
  wget https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz
  tar -C ~/bin -zxf syft_${SYFT_VERSION}_linux_amd64.tar.gz
  rm syft_${SYFT_VERSION}_linux_amd64.tar.gz
fi

REPORT_PATH="$REPORTS_DIRECTORY/$IMAGE-syft.json"

echo "Checking image"
syft packages $IMAGE \
  --output cyclonedx-json \
  --file $REPORT_PATH

Nuget packages

Container inventory scanner does not fully support dotnet, so we’ll need to set that up individually. CycloneDX-dotnet can build dotnet inventory specifications based on .csproj or .sln files.

# !/bin/bash

set -eu

SCANNER_VERSION="latest"
SCAN_DIRECTORY="$(pwd)"
REPORTS_DIRECTORY="$(pwd)/sbom"
DEPENDENCY_TRACK_URI="localhost:8081"
DEPENDENCY_TRACK_API_KEY="dependencytrack api key"

if [ ! -d "$REPORTS_DIRECTORY" ]; then
  echo "Creating reports directory: $REPORTS_DIRECTORY"
  mkdir -p "$REPORTS_DIRECTORY"
fi

echo "Retrieve dotnet inventory image"
docker pull cyclonedx/cyclonedx-dotnet:$SCANNER_VERSION

echo "Build dotnet application inventory"
docker run --rm \
  --volume "$SCAN_DIRECTORY":/path \
  --volume "$REPORTS_DIRECTORY":/reports \
  cyclonedx/cyclonedx-dotnet:$SCANNER_VERSION \
    /path \
    --out "/reports" \
    --disable-package-restore \
    --json

Merge and upload inventory specifications

A single specification is required per version of a project. If multiple tools were required to build the specifications the results will need to be merged, otherwise that step can be skipped. Operations on specification files can be done with cyclonedx-cli.

The merged result should then be uploaded to DependencyTrack.

#!/bin/bash

set -eu

PROJECT_NAME="some-project"
PROJECT_VERSION="some-version"
DEPENDENCY_TRACK_URI="localhost:8081"
DEPENDENCY_TRACK_API_KEY=""

CYCLONEDX_VERSION="0.21.0"
REPORTS_DIRECTORY="$(pwd)/sbom"
INVENTORY_SPECIFICATIONS=$(find $REPORTS_DIRECTORY -type f | awk '{printf "%s ", $0}')

if ! hash cyclonedx 2>/dev/null || ! cyclonedx --version | grep -q "$CYCLONEDX_VERSION"; then
  echo "Install specifications utility"
  wget https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.21.0/cyclonedx-linux-x64
  mv ./cyclonedx-linux-x64 ~/bin/cyclonedx
  chmod +x ~/bin/cyclonedx
fi

echo "Merge inventory specifications"
cyclonedx merge \
  --input-files $INVENTORY_SPECIFICATIONS \
  --output-file $REPORTS_DIRECTORY/full-inventory.json

echo "Upload results to DependencyTrack"
curl -X "POST" "http://${DEPENDENCY_TRACK_URI}/api/v1/bom" \
     -H "Content-Type: multipart/form-data" \
     -H "X-API-Key: $DEPENDENCY_TRACK_API_KEY" \
     -F "bom=@$REPORTS_DIRECTORY/full-inventory.json" \
     -F "projectName=$PROJECT_NAME" \
     -F "projectVersion=$PROJECT_VERSION" \
     -F "autoCreate=true" \
     -v

echo "Remove merged result to allow reruns"
rm -f $REPORTS_DIRECTORY/full-inventory.json

Utilize the information

You now have a source of information for vulnerabilities and license issues over time and when there’s a particularly nasty vulnerability found, you can select that vulnerability and get a list of all your affected software. It’s also a great indicator of where to spend some extra effort updating, I suggest adding reviewing this information as an action in one of your weekly or monthly meetings, including picking a target for improvement until the next meeting.

Configure a policy for the worst violations (no copyleft licenses or critical vulnerabilities for example), you can also tag services as public or sensitive for more advanced settings. Use notifications or a blocking build step in pull requests as a countermeasure (be very careful with the noisiness of these).

Ideally you’ve been careful enough about bringing in dependencies that you can evaluate each vulnerability finding to suppress findings that are false positives or does not affect you and just patch the ones that are left. But I’m guessing you are now looking at hundreds to thousands of instances of known vulnerabilities and can’t investigate them individually yet, here are some suggested approaches to start moving to a point where that is an option: