A container image can contain more than your application. It may include operating system packages, runtime libraries, third-party dependencies, and tools installed during the build. Even when the application code is secure, one of these components may contain a known vulnerability.
This becomes difficult to manage when a project uses multiple base images, builds frequently, and deploys the same application across development, staging, and production environments.
A software bill of materials (SBOM) helps solve the visibility problem. It provides an inventory of the software components included in an application or container image. Security scanners can then compare those components against vulnerability databases and report known security issues.
In this article, we will generate an SBOM for a container image, scan the image for known vulnerabilities, and integrate both checks into a GitHub Actions workflow.
What Is an SBOM?
An SBOM is a structured inventory of software components and their relationships. Depending on the format and tooling, it can include package names, versions, package identifiers, licenses, and dependency information.
For a container image, an SBOM may identify components such as:
- Operating system packages installed in the image.
- .NET, Node.js, Python, or other runtime components.
- Application dependencies included in the final image.
- Libraries and packages supplied by the base image.
Two widely used SBOM formats are SPDX and CycloneDX. Both can represent software component information, but their schemas and supported details differ.
An SBOM does not prove that software is secure. It provides information that can be used for vulnerability analysis, license reviews, incident response, and software supply chain management.
Why Scan Container Images?
Consider a .NET application built from an official runtime image. Your application may have no known vulnerable NuGet packages, but the final image may still contain a vulnerable operating system library.
A dependency scan limited to the project file may not identify that operating system package. Scanning the container image provides a broader view of the software included in the deployment artifact.
Image scanning helps answer questions such as:
- Which components are included in the image?
- Which installed packages have known vulnerabilities?
- Which vulnerabilities are classified as critical or high severity?
- Does the image contain an outdated operating system package?
- Did a new vulnerability appear after the image was built?
- Can the image be deployed under the organization's security policy?
A useful container security process combines an SBOM with vulnerability scanning. The SBOM provides component inventory, while the scanner evaluates the available vulnerability information.
Step 1: Build a Container Image
Start with an application that has a Dockerfile. The following example shows a simplified multi-stage build for an ASP.NET Core application.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY MyApp.csproj .
RUN dotnet restore MyApp.csproj
COPY . .
RUN dotnet publish MyApp.csproj \
-c Release \
-o /app/publish \
--no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
This example assumes that the project targets a compatible .NET version and that the project file is named MyApp.csproj. Replace the project name and base image tags with those used by your application.
The first stage restores dependencies and publishes the application. The final stage contains the ASP.NET Core runtime and published application files rather than the complete SDK.
Multi-stage builds can reduce the number of unnecessary tools included in the runtime image. They do not eliminate vulnerabilities in the remaining packages, so the final image still needs to be scanned.
Build the image locally:
docker build -t myapp:local .
For reproducible production builds, use controlled build inputs and consider pinning base images by digest. Image tags can point to updated image contents over time, so a tag alone does not always identify an immutable artifact.
Step 2: Generate an SBOM with Syft
Syft is a tool that can inspect container images and generate software bills of materials in supported formats.
After installing Syft in your development environment or CI runner, generate an SPDX JSON SBOM:
syft myapp:local \
-o spdx-json=sbom.spdx.json
You can also generate a CycloneDX JSON SBOM:
syft myapp:local \
-o cyclonedx-json=sbom.cdx.json
Choose the format that fits your security tooling and reporting requirements. You usually do not need to generate both formats unless different consumers require them.
The generated file can contain information about the components Syft identified in the image. The exact contents depend on the image, package managers, file systems, and scanner capabilities.
For example, an SBOM might identify an operating system package with its installed version and package identifier. A vulnerability scanner can use this information to determine whether a known vulnerability affects that component.
Keep the generated SBOM with the corresponding image digest or build record. An SBOM is most useful when you can identify exactly which artifact it describes.
Step 3: Scan the Container Image with Grype
Grype is a vulnerability scanner that can analyze container images and SBOM files.
To scan the local image, run:
grype myapp:local
The output lists detected vulnerabilities and associated package information when matches are found.
For a structured report, you can produce JSON output:
grype myapp:local \
-o json \
--file grype-report.json
You can also scan the SBOM generated in the previous step:
grype sbom:sbom.spdx.json
Scanning the image and scanning an SBOM serve related but different purposes. Direct image scanning allows the tool to inspect the image using its supported cataloging capabilities. SBOM scanning evaluates the component inventory already recorded in the SBOM.
An SBOM may omit components that a particular cataloger cannot identify. Therefore, generating an SBOM does not guarantee that every file or library in the image has been inventoried.
Step 4: Understand Vulnerability Results
A vulnerability scanner generally matches identified packages against vulnerability data from supported sources.
A report may contain information such as the following:
| Field | Meaning |
|---|
| Package name | The component associated with the finding |
| Installed version | The version identified in the image or SBOM |
| Vulnerability ID | An identifier such as a CVE |
| Severity | The scanner's severity classification |
| Fixed version | A version reported as containing a fix, when known |
| Package type | The ecosystem or package manager associated with the component |
Suppose a report identifies a vulnerable operating system package. The first step is to confirm that the package is present in the final image and that the finding applies to its installed version.
Then determine whether a fixed version is available and whether upgrading it is compatible with the application.
Do not assume that every finding can be fixed by changing an application dependency. Some vulnerabilities originate in the base image or operating system packages and require a base-image update.
Likewise, the absence of findings does not prove that the image is free of vulnerabilities. New vulnerabilities may be disclosed later, some packages may be difficult to identify, and vulnerability databases may not cover every component.
Step 5: Fail the Build When Required Vulnerabilities Are Found
A scanner is more useful when its results can enforce a defined security policy.
For example, Grype supports severity-based filtering. You can configure a scan to fail when critical vulnerabilities are detected:
grype myapp:local \
--fail-on critical
To fail on high-severity vulnerabilities as well:
grype myapp:local \
--fail-on high
The selected threshold should reflect your organization's risk policy. Failing every build on every low-severity finding may create unnecessary noise, while ignoring critical findings can allow dangerous components to reach production.
Severity alone is not always enough to prioritize remediation. Consider whether the vulnerable component is reachable, whether a fix exists, the application's exposure, and any applicable exploit information.
If you temporarily suppress a finding, document the reason, assign an owner, and define a review date. Avoid broad exclusions that hide future vulnerabilities in unrelated packages.
Step 6: Add SBOM Generation and Vulnerability Scanning to GitHub Actions
You can automate these checks in a GitHub Actions workflow so that the image is inspected as part of CI.
The following example illustrates the workflow structure. It assumes that Syft and Grype have been installed on the runner using your organization's approved installation process.
name: Container Security Scan
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Check out source code
uses: actions/checkout@v4
- name: Build container image
run: docker build -t myapp:${{ github.sha }} .
- name: Generate SBOM
run: |
syft myapp:${{ github.sha }} \
-o spdx-json=sbom.spdx.json
- name: Scan container image
run: |
grype myapp:${{ github.sha }} \
--fail-on high \
-o json \
--file grype-report.json
- name: Upload SBOM and scan report
if: always()
uses: actions/upload-artifact@v4
with:
name: container-security-reports
path: |
sbom.spdx.json
grype-report.json
if-no-files-found: warn
This workflow builds the image, generates an SBOM, scans the image, and uploads the reports as workflow artifacts.
The example uses a high-severity failure threshold. Adjust it to match your organization's policy.
The workflow does not install Syft or Grype. Add a controlled installation step or use a maintained, trusted scanner image or action. Pin third-party actions to reviewed versions or full commit SHAs according to your security policy.
Also review the behavior of the report-upload step. If the scan fails before producing a report, the workflow may not have a complete set of artifacts. For reliable reporting, consider separating report generation from the enforcement step so the report is saved even when the policy check fails.
Step 7: Scan the Exact Image You Plan to Deploy
A security scan is only useful if it applies to the artifact that reaches production.
Consider this sequence:
- CI builds and scans an image.
- The image is rebuilt later with a mutable base-image tag.
- The new image is pushed to the registry.
- Production deploys the new image without scanning it.
The deployed image may contain different components from the image that passed the original scan.
A safer process is to build the image once, scan that image, and promote the same artifact through the deployment pipeline. Record the image digest and associate the SBOM and scan report with that digest.
If the image is rebuilt, scan the rebuilt artifact again. Also consider rescanning stored images when vulnerability databases are updated, because a component that had no known vulnerability at build time may be affected by a newly disclosed issue.
Step 8: Manage Reports and Remediation
Generating reports is only the first part of the process. Teams also need to maintain them and respond to findings.
Store the SBOM with the build's metadata or artifact record. Keep enough information to identify the image digest, source revision, build time, and scanner configuration.
When a vulnerability is reported:
- Confirm the affected component and installed version.
- Check whether a fixed version is available.
- Identify whether the component comes from the application dependencies or the base image.
- Update the relevant dependency or base image.
- Rebuild the image and generate a new SBOM.
- Rescan the rebuilt image.
- Record the result and deploy the verified artifact.
For production systems, assign remediation responsibilities and define time limits based on severity and exposure. A report that nobody reviews does not improve security.
Common Mistakes to Avoid
- Generating an SBOM only for application dependencies while ignoring the final container image.
- Treating an SBOM as proof that the image is secure.
- Scanning one image and deploying a different build.
- Ignoring vulnerabilities in the operating system or runtime base image.
- Failing every build on all findings without a clear severity policy.
- Suppressing findings without documenting the reason or review date.
- Saving reports without associating them with the exact image digest.
- Assuming a clean scan remains valid forever as new vulnerabilities are disclosed.
- Using unpinned or unreviewed third-party tools in privileged CI workflows.
Best Practices for Container Security
- Generate an SBOM for the final container image.
- Use a supported format such as SPDX or CycloneDX based on your tooling requirements.
- Scan images against current vulnerability data.
- Define severity thresholds and documented exception procedures.
- Build once and promote the same verified image through environments.
- Record the image digest and keep the SBOM and scan report associated with it.
- Update base images and dependencies when fixes become available.
- Review new vulnerability disclosures against deployed artifacts.
- Protect CI workflows and review the tools and actions they execute.
- Track remediation so critical findings are assigned and resolved.
Summary
An SBOM provides visibility into the software components included in a container image. Vulnerability scanners use that inventory, along with their supported package detection and vulnerability data, to identify known security issues.
With tools such as Syft and Grype, teams can generate an SBOM, scan container images, and enforce vulnerability policies in GitHub Actions. The most important part is to scan the exact image that will be deployed and keep its reports associated with the corresponding artifact.
An SBOM does not guarantee security, and a vulnerability scan cannot identify every possible weakness. Combined with controlled builds, regular updates, documented remediation, and ongoing scanning, however, these tools provide a practical way to improve container supply chain security.
Join the conversation! Your thoughts help the community grow.