Source vs Build SBOMs
The .NET ecosystem has evolved significantly in its dependency management. Understanding the landscape is crucial for accurate SBOM generation:
- Legacy:
packages.config(NuGet 2.x style) - Modern:
PackageReferencein.csprojfiles - Recommended:
packages.lock.jsonfor reproducible builds
For the most accurate SBOMs, generate from packages.lock.json which contains resolved transitive dependencies with exact versions.
Lockfile Deep Dive
Generating packages.lock.json
.NET doesn’t create a lockfile by default. Enable it in your .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
</PropertyGroup>
</Project>
Or via CLI:
dotnet restore --use-lock-file
packages.lock.json Structure
The lockfile contains detailed dependency information:
{
"version": 2,
"dependencies": {
"net8.0": {
"Microsoft.Extensions.Logging": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "h1:abc123...",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "def456..."
}
}
}
}
Key fields:
- type: Direct or Transitive dependency
- requested: Version range from your
.csproj - resolved: Actual resolved version
- contentHash: Package integrity hash
- dependencies: Transitive dependencies
PackageReference in .csproj
Without a lockfile, dependencies are declared in .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.*" />
<PackageReference Include="Serilog" Version="[3.0.0,4.0.0)" />
</ItemGroup>
</Project>
Warning: Version ranges (like 13.0.* or [3.0.0,4.0.0)) make reproducible SBOMs impossible without a lockfile.
.NET Framework vs .NET Core vs .NET 5+
Different .NET versions handle dependencies differently:
| Version | Package Format | Recommended SBOM Source |
|---|---|---|
| .NET Framework | packages.config or PackageReference | packages.lock.json |
| .NET Core | PackageReference | packages.lock.json |
| .NET 5+ | PackageReference | packages.lock.json |
Multi-Targeting
.NET projects can target multiple frameworks:
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
Your packages.lock.json will contain separate dependency trees for each target:
{
"dependencies": {
"net6.0": { /* dependencies */ },
"net8.0": { /* dependencies */ },
"netstandard2.1": { /* dependencies */ }
}
}
Consider generating separate SBOMs for each target framework if they differ significantly.
Generating an SBOM
SBOM generation is the first step in the SBOM lifecycle. After generation, you typically need to enrich your SBOM with package metadata and augment it with your organization’s details.
Using sbomify GitHub Action (Recommended)
The sbomify GitHub Action is a swiss army knife for SBOMs that automatically selects the best generation tool for your ecosystem, enriches the output with package metadata, and optionally augments it with your business information—all in one step.
For .NET, sbomify uses cdxgen under the hood with fallback to Trivy and Syft.
Standalone (no account needed):
- uses: sbomify/github-action@master
env:
LOCK_FILE: packages.lock.json
OUTPUT_FILE: sbom.cdx.json
COMPONENT_NAME: my-dotnet-app
COMPONENT_VERSION: ${{ github.ref_name }}
ENRICH: true
UPLOAD: false
Using github.ref_name automatically captures your git tag (e.g., v1.2.3) as the SBOM version. For rolling releases without tags, use github.sha instead. See our SBOM versioning guide for best practices.
With sbomify platform (adds augmentation and upload):
- uses: sbomify/github-action@master
env:
TOKEN: ${{ secrets.SBOMIFY_TOKEN }}
COMPONENT_ID: my-component-id
LOCK_FILE: packages.lock.json
OUTPUT_FILE: sbom.cdx.json
AUGMENT: true
ENRICH: true
Alternative Tools
If you prefer to run SBOM generation tools manually:
Microsoft SBOM Tool (official):
dotnet tool install --global Microsoft.Sbom.DotNetTool
sbom-tool generate -b ./bin/Release -bc . -pn MyApp -pv 1.0.0 -ps MyCompany
CycloneDX .NET Tool:
dotnet tool install --global CycloneDX
dotnet CycloneDX MySolution.sln -o sbom.cdx.json -j
cdxgen:
npm install -g @cyclonedx/cdxgen
cdxgen -t dotnet -o sbom.cdx.json
Trivy:
trivy fs --format cyclonedx --output sbom.cdx.json .
When using these tools directly, you’ll need to handle enrichment and augmentation separately.
GitLab CI
generate-sbom:
image: sbomifyhub/sbomify-action
variables:
LOCK_FILE: packages.lock.json
OUTPUT_FILE: sbom.cdx.json
UPLOAD: "false"
ENRICH: "true"
script:
- /sbomify.sh
artifacts:
paths:
- sbom.cdx.json
Azure DevOps Integration
For Azure Pipelines:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- script: dotnet restore --use-lock-file
displayName: 'Restore dependencies'
- script: |
dotnet tool install --global CycloneDX
dotnet CycloneDX *.sln -o sbom.cdx.json -j
displayName: 'Generate SBOM'
- publish: sbom.cdx.json
artifact: sbom
Handling NuGet Sources
For projects using private NuGet feeds:
<!-- nuget.config -->
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="private-feed" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" />
</packageSources>
<packageSourceCredentials>
<private-feed>
<add key="Username" value="PAT" />
<add key="ClearTextPassword" value="%NUGET_TOKEN%" />
</private-feed>
</packageSourceCredentials>
</configuration>
Ensure your CI environment has access to all NuGet sources for accurate SBOM generation.
Best Practices
- Always use lockfiles - Enable
RestorePackagesWithLockFilefor reproducible builds - Pin exact versions - Avoid floating versions in production
- Use locked mode in CI - Set
RestoreLockedModetotruein CI builds - Separate runtime and dev dependencies - Consider excluding test packages from SBOMs
- Include framework dependencies - Don’t forget the .NET runtime itself
- Document private packages - Ensure metadata is available for internal NuGet packages
Common Issues
Missing Transitive Dependencies
If your SBOM is missing transitive dependencies:
- Ensure you’re using
packages.lock.json - Run
dotnet restore --force-evaluateto regenerate the lockfile
Framework-Specific Dependencies
Different target frameworks may have different dependencies. Choose the target framework most relevant to your deployment:
# Generate for specific framework
dotnet CycloneDX MyProject.csproj -o sbom.cdx.json -j -f net8.0
Self-Contained Deployments
For self-contained deployments that include the .NET runtime:
dotnet publish -c Release -r linux-x64 --self-contained true
Consider generating an SBOM that includes runtime components using Microsoft SBOM Tool on the published output.
Further Resources
For more SBOM tools and resources, see our SBOM Resources page, which includes additional .NET-specific tools like CycloneDX .NET library and Microsoft SBOM Tool.