This article is a step-by-step tutorial for developers and DevSecOps engineers on how to transform a Software Bill of Materials (SBOM) from a static compliance artifact into an active, automated security tool within a CI/CD pipeline.
## Introduction
In today’s software development landscape, understanding your dependencies is no longer optional. The rise of sophisticated supply chain attacks has made it critical to know exactly what’s inside your application. This is where the Software Bill of Materials (SBOM) comes in. An SBOM is a formal, machine-readable inventory of all the components, libraries, and modules required to build a piece of software.
For many organizations, generating an SBOM is simply a compliance checkbox. It’s a file that gets created, archived, and forgotten. But an SBOM’s true power isn’t in its existence; it’s in its *application*. A well-utilized SBOM can become a proactive defense mechanism that automatically detects and blocks vulnerabilities before they ever reach production.
This guide will show you how to move beyond passive SBOM generation. We will walk through the process of integrating supply chain intelligence directly into your CI/CD pipeline, turning it into an automated security gatekeeper. You’ll learn how to generate an SBOM, scan it for known vulnerabilities, automatically fail a build based on risk, and manage false positives effectively.
## Prerequisites
Before we begin, ensure you have the following:
* A basic understanding of Git, CI/CD principles, and YAML syntax.
* A GitHub account and a repository for your project.
* A sample application to work with. We will use a simple Node.js application for our examples.
* Docker installed on your local machine to run tools like Trivy without needing to install them globally.
## Step-by-Step Guide
### Step 1: Setting Up a Sample Project
First, let’s create a simple Node.js project. This application will include a `package.json` file with a few dependencies, some of which we know have vulnerabilities for demonstration purposes.
Create a new directory for your project, navigate into it, and initialize a Node.js project.
“`bash
mkdir sbom-project
cd sbom-project
npm init -y
“`
Next, install a couple of dependencies. We’ll use an older, vulnerable version of `express` and `axios`.
“`bash
npm install [email protected] [email protected]
“`
Your `package.json` will now contain these dependencies. This file is the source of truth for our project’s direct dependencies.
### Step 2: Generating Your First SBOM
Now that we have a project, our first goal is to generate an SBOM. We will use **Trivy**, a popular open-source security scanner from Aqua Security. It’s an excellent all-in-one tool that can discover vulnerabilities, misconfigurations, and generate SBOMs in various formats.
We’ll use Trivy’s Docker image to run it without a local installation. The following command scans the project’s file system (`fs`) and generates an SBOM in the CycloneDX JSON format, saving it to `sbom.json`.
“`bash
docker run –rm -v $(pwd):/app aquasec/trivy fs –format cyclonedx –output sbom.json /app
“`
**Code Explanation:**
* `docker run –rm -v $(pwd):/app`: This runs the Trivy container, mounts the current directory (`$(pwd)`) into the container’s `/app` directory, and removes the container (`–rm`) after execution.
* `aquasec/trivy`: The official Trivy Docker image.
* `fs`: Specifies that we are scanning a filesystem.
* `–format cyclonedx`: Instructs Trivy to output the results in the CycloneDX SBOM format.
* `–output sbom.json`: Defines the name of the output file.
* `/app`: The target directory inside the container that we want to scan.
After running this command, you will find an `sbom.json` file in your project directory. This file contains a detailed list of all your dependencies and sub-dependencies.
### Step 3: Automating SBOM Generation with GitHub Actions
Manually generating an SBOM is useful, but the real power comes from automation. Let’s create a GitHub Actions workflow to generate our SBOM every time we push code.
In your project root, create a `.github/workflows` directory, and inside it, create a file named `ci.yml`.
“`yaml
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
– name: Checkout code
uses: actions/checkout@v3
– name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
# Scan the file system for dependencies
scan-type: ‘fs’
# Generate SBOM in CycloneDX format
format: ‘cyclonedx’
# Name the output file
output: ‘sbom.json’
“`
**Code Explanation:**
* `on: [push, pull_request]`: This workflow triggers on every push and pull request to the `main` branch.
* `jobs: build-and-scan`: Defines a single job to run our process.
* `actions/checkout@v3`: A standard action to check out your repository’s code into the runner.
* `aquasecurity/trivy-action@master`: The official GitHub Action for Trivy, which simplifies its integration.
* `with:`: This block configures the parameters for the Trivy action, mirroring the command-line flags we used earlier.
Commit this file and push it to your GitHub repository. Now, every time you update your code, an SBOM will be automatically generated. You can even configure the workflow to upload this SBOM as a build artifact for auditing purposes.
### Step 4: Actioning Intelligence – Vulnerability Scanning
An SBOM is a list. Now, let’s use that list to find risks. We’ll modify our workflow to not just generate the SBOM but to also scan for vulnerabilities and fail the build if a critical issue is found.
Update your `ci.yml` file to include a vulnerability scanning step.
“`yaml
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
– name: Checkout code
uses: actions/checkout@v3
# This step remains the same for generating the artifact
– name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
scan-type: ‘fs’
format: ‘cyclonedx’
output: ‘sbom.json’
# NEW: This step actively scans for vulnerabilities
– name: Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: ‘fs’
# Fail the build if a CRITICAL vulnerability is found
exit-code: ‘1’
ignore-unfixed: true
severity: ‘CRITICAL,HIGH’
“`
**Code Explanation:**
* `exit-code: ‘1’`: This is the key instruction. It tells Trivy to exit with a non-zero status code if it finds vulnerabilities matching the criteria, which causes the workflow step (and the entire job) to fail.
* `ignore-unfixed: true`: A practical setting that tells Trivy to only report on vulnerabilities that have a known fix available. This keeps the report focused on actionable items.
* `severity: ‘CRITICAL,HIGH’`: We instruct Trivy to only consider `CRITICAL` and `HIGH` severity vulnerabilities for the purpose of failing the build.
With this change, your CI pipeline is now an active security gate. A pull request containing a dependency with a new, high-severity vulnerability will be automatically blocked, preventing the threat from being merged into your main branch.
### Step 5: Reducing Noise with VEX
Sometimes, a scanner will flag a vulnerability that doesn’t actually affect your project. For example, the vulnerability might exist in a function that your code never calls. This is where the **Vulnerability Exploitability eXchange (VEX)** comes in. VEX is a standard for communicating whether a product is actually affected by a vulnerability.
Let’s say Trivy flags a vulnerability that, after investigation, you determine is not exploitable. You can create a `vex.json` document to programmatically ignore it.
Create a file named `vex.json` in your project root:
“`json
{
“vexSpec”: “1.0.0”,
“author”: “My Security Team”,
“statements”: [
{
“vulnerability”: “CVE-2022-24999”,
“status”: “not_affected”,
“justification”: “code_not_reachable”,
“statement_id”: “vex-123”
}
]
}
“`
**Code Explanation:**
* `vulnerability`: The CVE identifier of the vulnerability you want to suppress.
* `status`: Set to `not_affected` to indicate the vulnerability is not a threat.
* `justification`: Provides a machine-readable reason, such as `code_not_reachable`.
* `statement_id`: A unique identifier for this VEX statement.
Now, update the `Vulnerability Scan` step in your `ci.yml` to use this VEX document.
“`yaml
# In .github/workflows/ci.yml
# … (previous steps) …
– name: Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: ‘fs’
exit-code: ‘1’
ignore-unfixed: true
severity: ‘CRITICAL,HIGH’
# NEW: Use the VEX document to filter findings
vex: ‘./vex.json’
“`
By adding the `vex: ‘./vex.json’` line, you tell Trivy to cross-reference its findings with your VEX document. The `CVE-2022-24999` vulnerability will now be excluded from the scan results, allowing your build to pass while maintaining a clear, documented record of your security decision.
## Conclusion
You have successfully transformed your SBOM from a simple inventory list into the backbone of an automated security system. By integrating SBOM generation, vulnerability scanning, and VEX documentation into your CI/CD pipeline, you’ve created a powerful, proactive defense against software supply chain attacks.
This process—**Generate, Scan, Gate, and Document**—ensures that security is not an afterthought but a continuous, automated part of your development lifecycle. From here, you can expand on this foundation by integrating scan results into security dashboards, creating more sophisticated policies, and further securing every stage of your software supply chain.
0 Comments