A container image may look like a single deployable package, but it can contain dozens or hundreds of software components. These include operating system packages, application libraries, runtime dependencies, and tools installed during the image build.
If one of those components contains a known security vulnerability, your application may be exposed even when your own code has no obvious security issue.
This is why development teams need to know what is inside their container images and whether any included components have reported vulnerabilities.
A Software Bill of Materials (SBOM) helps solve the first problem by listing the software components included in an application or image. Vulnerability scanners use component information to identify packages that match known security advisories.
In this article, you will learn how to generate an SBOM for a Docker image, scan the image for known vulnerabilities, add these checks to a CI pipeline, and handle the results without blocking releases unnecessarily.
What Is an SBOM?
A Software Bill of Materials is a structured inventory of the software components included in an application or software package.
Depending on the tool and format, an SBOM can contain information such as:
- Package names and versions.
- Package identifiers and suppliers.
- Dependency relationships.
- Licenses.
- Package checksums.
- Component metadata.
An SBOM helps security teams understand which components are present when a new vulnerability is announced.
For example, suppose a security advisory affects a particular version of an OpenSSL package. If you have an SBOM for your container image, you can search the inventory to determine whether that package and version are included.
However, an SBOM is an inventory, not a security guarantee. It does not automatically prove that every component is safe, that every dependency has been discovered, or that a vulnerable component is exploitable in your application.
Why Scan Container Images?
Container images include more than application source code.
A typical image might contain:
- A base operating system.
- A language runtime such as .NET or Node.js.
- Application dependencies.
- Native libraries.
- Additional utilities installed during the build.
Each component introduces its own maintenance and security considerations.
A vulnerability scanner compares the components it can identify against vulnerability databases and security advisories.
Scanning can help you:
- Find vulnerable operating system packages.
- Identify outdated application dependencies.
- Detect known vulnerabilities before deployment.
- Investigate the impact of newly disclosed security issues.
- Enforce consistent security checks in CI/CD pipelines.
The results depend on the scanner's package detection capabilities, vulnerability data, and the image being inspected. A clean scan does not prove that an image has no vulnerabilities.
Step 1: Build a Docker Image
Start with a Docker image that represents the application you intend to deploy.
For example, a simple ASP.NET Core application might use a multi-stage Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
The first stage uses the .NET SDK to build the application. The final stage uses the ASP.NET Core runtime image and copies the published application into it.
This approach keeps the SDK and build tools out of the final image, reducing its size and potentially reducing the number of installed components that need to be maintained.
Replace MyApp.dll with the actual application assembly name.
Build the image:
docker build -t myapp:local .
You now have a local image that can be inspected and scanned.
For production pipelines, use deliberate base-image versioning and update policies. A floating tag can change over time, while a digest identifies a specific image artifact. The right choice depends on how your team handles updates and reproducibility.
Step 2: Generate an SBOM with Syft
Syft is a tool that can inspect container images and generate a Software Bill of Materials.
It supports common SBOM formats, including SPDX and CycloneDX. These formats allow teams to exchange software inventory data with different security tools.
After installing Syft in your development or CI environment, generate an SBOM for the image:
syft myapp:local -o cyclonedx-json > sbom.cdx.json
This command inspects myapp:local, generates an SBOM in CycloneDX JSON format, and writes the result to sbom.cdx.json.
You can also generate an SPDX document:
syft myapp:local -o spdx-json > sbom.spdx.json
Choose a format that your security tooling and downstream systems support.
What Should You Check in the SBOM?
Open the generated file and inspect the component list.
Look for:
- Expected application dependencies.
- Operating system packages.
- Runtime and native libraries.
- Correct package versions.
- Missing or unexpected components.
If a package is absent from the inventory, do not immediately conclude that the package is not present in the image. Detection coverage depends on the package manager, file layout, metadata, and scanner capabilities.
Keep the SBOM associated with the exact image that was scanned. An SBOM generated from a local build is not necessarily valid for a different image tag or a later rebuild.
Step 3: Scan the Image for Vulnerabilities with Grype
Grype is a vulnerability scanner that can inspect container images and SBOMs.
After installing Grype, scan the local image:
grype myapp:local
The scanner identifies components that match known vulnerability records and reports information such as package name, installed version, vulnerability identifier, severity, and available fix information.
You can also scan the generated SBOM:
grype sbom:sbom.cdx.json
Scanning the SBOM can be useful when the inventory has already been generated and stored as a CI artifact.
However, the image scan and SBOM scan are not always identical in scope. A scanner may discover additional information directly from the image, depending on its detection capabilities.
Understand the Results
A vulnerability report commonly includes severity categories such as Critical, High, Medium, and Low.
Do not treat severity as the only measure of risk.
When reviewing a finding, consider:
- Whether the affected package is actually present in the deployed image.
- Whether the installed version is affected.
- Whether a fixed version is available.
- Whether the vulnerable functionality is reachable in your application.
- Whether the issue affects a runtime dependency or an unused build-stage component.
- Whether the vulnerability is relevant to your deployment environment.
A Critical finding deserves prompt investigation, but the right remediation depends on the actual exposure and available fixes.
Likewise, a vulnerability classified as Low should not automatically be ignored if it affects a sensitive component or violates a required security policy.
Step 4: Scan the Exact Image You Plan to Deploy
One common mistake is scanning an image during development and then deploying a different artifact.
For example, a developer may scan myapp:local, while the deployment pipeline rebuilds the application from a different base image or dependency state.
The scan results no longer necessarily represent the deployed image.
A better approach is to build the image once, assign it a unique identifier, scan that image, and promote the same artifact through the deployment pipeline.
For example:
docker build -t myapp:release-candidate .
syft myapp:release-candidate \
-o cyclonedx-json > sbom.cdx.json
grype myapp:release-candidate
These commands illustrate the sequence. A production pipeline should also record the image digest and associate the SBOM and scan report with that digest.
If the image is rebuilt, even from the same Dockerfile, the resulting artifact can differ because of changed dependencies, base images, or build inputs.
Scanning the final image helps ensure that the results apply to the artifact being released.
Step 5: Add SBOM Generation and Vulnerability Scanning to GitHub Actions
You can automate these checks in a GitHub Actions workflow.
The following example builds a container image, generates an SBOM, scans the image, and uploads the SBOM as a workflow artifact.
name: Container Security Check
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Build container image
run: docker build -t myapp:ci .
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Install Grype
uses: anchore/scan-action/download-grype@v0
- name: Generate SBOM
run: |
syft myapp:ci \
-o cyclonedx-json > sbom.cdx.json
- name: Scan container image
run: grype myapp:ci
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: container-sbom
path: sbom.cdx.json
if-no-files-found: error
This example illustrates the workflow structure. Before using it in production, verify the current installation-action interfaces and pin third-party actions to reviewed commit SHAs. A production pipeline should not depend on an unreviewed action revision.
By default, the workflow fails if a command returns a nonzero exit code. However, the scanner's default exit behavior may not match your vulnerability policy.
Configure explicit severity and failure thresholds so the pipeline behaves predictably.
For example, a team may decide that new Critical findings must fail a release build, while High findings require review. The exact policy should reflect the application's risk, compliance requirements, and remediation process.
Step 6: Define a Practical Vulnerability Policy
Scanning becomes useful when teams know what to do with the results.
A reasonable policy should define which findings block deployment, which require review, and how exceptions are handled.
For example:
| Finding | Possible response |
|---|
| Critical vulnerability with an available fix | Block release until fixed or formally approved |
| High vulnerability in an exposed runtime component | Prioritize remediation and assess release risk |
| Vulnerability without an available fix | Document the risk and apply mitigations where possible |
| Finding in a build-only component | Verify whether the component exists in the final image |
| False positive | Record evidence and create a reviewed exception |
Avoid automatically ignoring every finding that has no available fix. A vulnerability can still be exploitable even when an upstream patch does not exist.
Exceptions should include a reason, an owner, and a review date. They should not become permanent exclusions without reassessment.
Best Practices for SBOM and Container Security
1. Generate an SBOM for Every Release
Create an SBOM for each release artifact and store it with the associated image digest.
This helps you investigate future security advisories without guessing which package versions were deployed.
2. Keep Base Images Updated
Use maintained base images and establish a process for rebuilding applications when security updates become available.
A vulnerability scan is only a snapshot. A clean image today can contain a known vulnerability after a new advisory is published.
3. Use Multi-Stage Docker Builds
Keep compilers, SDKs, package managers, and development tools out of the final image when they are not needed at runtime.
Smaller images can reduce attack surface and simplify maintenance, although image size alone does not determine security.
4. Scan Both Application and Operating System Dependencies
Some tools focus on language dependencies, while others inspect operating system packages and container layers.
Understand the coverage of your selected tools and combine them when necessary.
5. Track Vulnerability Results Over Time
Keep scan reports and SBOMs so your team can compare findings between releases.
A growing number of findings may indicate an outdated base image, newly disclosed vulnerabilities, or a dependency update that introduced additional components.
6. Protect the Build Pipeline
Restrict access to release workflows, review third-party actions, and avoid exposing deployment credentials to untrusted pull-request code.
A secure container is not enough if an attacker can modify the pipeline that builds or publishes it.
Common Mistakes to Avoid
- Treating an SBOM as proof of security: It lists components but does not prove they are vulnerability-free.
- Scanning a different image from the one deployed: Results may no longer represent the release artifact.
- Ignoring the final image: Vulnerabilities in runtime dependencies matter even when application source code is clean.
- Blocking every finding without review: Some findings need risk assessment, prioritization, or a documented exception.
- Ignoring findings without available fixes: Lack of a patch does not mean lack of risk.
- Keeping the SBOM without identifying its image: An inventory must be tied to the exact artifact it describes.
- Running scans only once: New vulnerability disclosures can affect existing images after deployment.
Conclusion
An SBOM gives your team visibility into the software components inside a container image, while vulnerability scanning helps identify components that match known security issues.
Tools such as Syft and Grype can automate much of this work. By generating an SBOM, scanning the exact image intended for deployment, and enforcing a clear vulnerability policy in CI/CD, teams can identify security issues earlier and respond more consistently.
The important part is to treat scanning as an ongoing process. Keep base images updated, preserve release inventories, review findings based on risk, and ensure that the image you deploy is the same artifact that passed your security checks.
Join the conversation! Your thoughts help the community grow.