SBOM Vulnerability Scanning in .NET 10 with CycloneDX and Dependency-Track

Mehdi Hadeli
@mehdihadeli
On this page
Table of contents
Introduction
This guide shows how to stand up a practical vulnerability tracking flow for a .NET 10 application using CycloneDX and Dependency-Track.
Full working sample repository for this article:
https://github.com/mehdihadeli/blog-samples/tree/main/sbom-dependency-track-dotnet10
This article is self-contained on purpose. You can copy and run the snippets directly. The repository link is there if you prefer cloning a working baseline and iterating from it.
Objectives
This article is about one thing: getting Dependency-Track up and running so you can check vulnerabilities in your app from a generated SBOM.
By the end, you will have this working flow:
- Build your .NET app.
- Generate CycloneDX SBOM.
- Run Dependency-Track locally.
- Upload the SBOM.
- See vulnerability results in Dependency-Track.
- Automate the same flow in GitHub Actions.
Architecture Overview
![]()
Prerequisites
- .NET SDK 10
- Docker + Docker Compose
- curl
- A GitHub repository for CI setup
Helpful tools:
- jq (for SBOM inspection and pipeline checks)
- a Slack or Teams webhook for notification routing
- a ticketing integration target (Jira, Azure Boards, or equivalent)
- a clear version value for your builds (tag, build number, or commit)
Step-by-Step Implementation
Step 1: Build a .NET 10 app with meaningful dependencies
You want a dependency graph that has enough shape to test real workflows. The sample API below includes common NuGet packages so the BOM has useful component data.
1.1 Create solution and project
dotnet new sln -n SbomDependencyTrackDemo
dotnet new webapi -n SbomDependencyTrackDemo.Api -o src/SbomDependencyTrackDemo.Api --framework net10.0
dotnet sln add src/SbomDependencyTrackDemo.Api/SbomDependencyTrackDemo.Api.csproj
1.2 Use this project file
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IncludeVulnerablePackage>false</IncludeVulnerablePackage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeVulnerablePackage)' == 'true'">
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.1" />
</ItemGroup>
</Project>
IncludeVulnerablePackage is a test switch. Turn it on only in a validation branch to prove your alert flow works.
1.3 Use this minimal API
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapGet(
"/",
() =>
Results.Ok(
new
{
Name = "SbomDependencyTrackDemo.Api",
Runtime = Environment.Version.ToString(),
Utc = DateTimeOffset.UtcNow,
}
)
);
app.MapGet(
"/dependencies",
() =>
Results.Ok(
new[] { "Microsoft.AspNetCore.OpenApi", "Dapper", "NodaTime", "Serilog.AspNetCore" }
)
);
app.Run();
1.4 Verify local build
dotnet restore
dotnet build
dotnet run --project src/SbomDependencyTrackDemo.Api
At this stage, validate two details:
- The solution and project names used in scripts match your real repository paths.
- Your package graph includes transitive dependencies, not only direct references.
You can quickly inspect direct and transitive packages with:
dotnet list src/SbomDependencyTrackDemo.Api/SbomDependencyTrackDemo.Api.csproj package --include-transitive
This command is not a replacement for SBOM, but it is useful as a sanity check during setup.
Step 2: Generate CycloneDX SBOM
Pinning tool version avoids "works on my machine" drift between local and CI.
2.1 Create tool manifest and install CycloneDX
dotnet new tool-manifest
dotnet tool install CycloneDX
Tool manifest file:
{
"version": 1,
"isRoot": true,
"tools": {
"cyclonedx": {
"version": "6.2.0",
"commands": ["dotnet-CycloneDX"],
"rollForward": false
}
}
}
2.2 Generate SBOM manually
dotnet restore SbomDependencyTrackDemo.slnx
dotnet tool restore
dotnet dotnet-CycloneDX SbomDependencyTrackDemo.slnx \
-o artifacts/sbom \
-fn sbom.cyclonedx.json \
-F Json \
-rs
What these flags do in plain language:
-o artifacts/sbom: keeps BOM output in a predictable folder for upload and retention.-fn sbom.cyclonedx.json: stable file naming simplifies CI scripting.-F Json: JSON is easy to inspect and process with command-line tooling.-rs: recurse and include referenced projects where applicable.
2.3 Use a script for local and CI reuse
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT_DIR="${ROOT_DIR}/artifacts/sbom"
OUTPUT_FILE="sbom.cyclonedx.json"
SOLUTION_FILE="${ROOT_DIR}/SbomDependencyTrackDemo.slnx"
mkdir -p "${OUTPUT_DIR}"
pushd "${ROOT_DIR}" >/dev/null
dotnet restore "${SOLUTION_FILE}"
dotnet tool restore
dotnet dotnet-CycloneDX "${SOLUTION_FILE}" \
-o "${OUTPUT_DIR}" \
-fn "${OUTPUT_FILE}" \
-F Json \
-rs
popd >/dev/null
echo "SBOM generated at ${OUTPUT_DIR}/${OUTPUT_FILE}"
Expected output file:
artifacts/sbom/sbom.cyclonedx.json
2.4 Validate SBOM before upload
A small validation step saves a lot of debugging later.
jq -e '.bomFormat == "CycloneDX"' artifacts/sbom/sbom.cyclonedx.json
jq -e '.metadata.component.name != null' artifacts/sbom/sbom.cyclonedx.json
jq -e '.components | length > 0' artifacts/sbom/sbom.cyclonedx.json
You can also print a short summary:
echo "Format: $(jq -r '.bomFormat' artifacts/sbom/sbom.cyclonedx.json)"
echo "Spec: $(jq -r '.specVersion' artifacts/sbom/sbom.cyclonedx.json)"
echo "Components: $(jq -r '.components | length' artifacts/sbom/sbom.cyclonedx.json)"
This quick check helps you catch empty or malformed BOM files before upload.
Step 3: Run Dependency-Track with Docker Compose
For local use, the simplest stable setup is API server, frontend, and PostgreSQL in one stack.
3.1 Environment file
POSTGRES_USERNAME=dtrack
POSTGRES_PASSWORD=dtrack
POSTGRES_DB=dtrack
CORS_ALLOW_ORIGIN=*
3.2 Docker Compose file
version: '3.9'
services:
dtrack-apiserver:
image: dependencytrack/apiserver:4.13.5
depends_on:
postgres-db:
condition: service_healthy
environment:
ALPINE_DATABASE_MODE: external
ALPINE_DATABASE_URL: jdbc:postgresql://postgres-db:5432/${POSTGRES_DB}
ALPINE_DATABASE_DRIVER: org.postgresql.Driver
ALPINE_DATABASE_USERNAME: ${POSTGRES_USERNAME}
ALPINE_DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
ALPINE_CORS_ENABLED: 'true'
ALPINE_CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN}
ports:
- '8081:8080'
volumes:
- dependency-track-data:/data
restart: unless-stopped
dtrack-frontend:
image: dependencytrack/frontend:4.13.5
depends_on:
- dtrack-apiserver
environment:
API_BASE_URL: http://localhost:8081
ports:
- '8080:8080'
restart: unless-stopped
postgres-db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USERNAME}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
healthcheck:
test: ['CMD-SHELL', 'pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USERNAME}']
interval: 5s
timeout: 5s
retries: 10
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres-data:
dependency-track-data:
3.3 Start services
cp dependency-track/.env.example dependency-track/.env
docker compose --env-file dependency-track/.env -f dependency-track/docker-compose.yml up -d
Access points:
- UI:
http://localhost:8080 - API:
http://localhost:8081
First login is commonly admin/admin. Rotate credentials right away.
3.4 Quick notes for safer setup
For local development, the compose file is enough.
For shared environments, do these changes:
- Do not expose PostgreSQL port unless you need it.
- Replace default credentials.
- Use explicit CORS origin values instead of
*. - Put API behind HTTPS.
Step 4: Upload SBOM to Dependency-Track
In Dependency-Track UI:
- Create a project.
- Create a team for automation with
BOM_UPLOADpermission. - Generate API key.
- Copy project UUID.
4.1 Upload with curl
export DT_BASE_URL="http://localhost:8081"
export DT_API_KEY="your-api-key"
export DT_PROJECT_UUID="your-project-uuid"
curl -X POST "${DT_BASE_URL}/api/v1/bom" \
-H "X-API-Key: ${DT_API_KEY}" \
-H "Content-Type: multipart/form-data" \
-F "project=${DT_PROJECT_UUID}" \
-F "bom=@artifacts/sbom/sbom.cyclonedx.json"
4.2 Upload with script
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SBOM_PATH="${ROOT_DIR}/artifacts/sbom/sbom.cyclonedx.json"
DT_BASE_URL="${DT_BASE_URL:-http://localhost:8081}"
DT_API_KEY="${DT_API_KEY:-}"
DT_PROJECT_UUID="${DT_PROJECT_UUID:-}"
if [[ -z "${DT_API_KEY}" ]]; then
echo "DT_API_KEY is required."
exit 1
fi
if [[ -z "${DT_PROJECT_UUID}" ]]; then
echo "DT_PROJECT_UUID is required."
exit 1
fi
if [[ ! -f "${SBOM_PATH}" ]]; then
echo "SBOM file not found at ${SBOM_PATH}. Run scripts/generate-sbom.sh first."
exit 1
fi
echo "Uploading ${SBOM_PATH} to ${DT_BASE_URL}/api/v1/bom"
curl -sS -X POST "${DT_BASE_URL}/api/v1/bom" \
-H "X-API-Key: ${DT_API_KEY}" \
-H "Content-Type: multipart/form-data" \
-F "project=${DT_PROJECT_UUID}" \
-F "bom=@${SBOM_PATH}"
echo
echo "Upload request sent. Check Dependency-Track project dashboard for processing results."
After upload, Dependency-Track ingests the BOM and correlates components with vulnerability sources.
4.3 Wait for processing and verify in UI
After upload, results are not always immediate. Dependency-Track processes the BOM in background.
Check the project page in UI and confirm:
- components count is not zero
- vulnerability tab has results
- project version is correct
Step 5: Configure alerts and policy rules
This part makes the dashboard useful for daily work.
At minimum, configure these:
- Notification publishers: email, Slack, webhook, or your ticketing target.
- Policy conditions: for example, fail on
Critical, warn onHigh. - License policy checks if your org has explicit allow/deny license rules.
A practical starting policy:
Criticalvulnerability found in project component: trigger immediate alert.Highvulnerability older than 7 days: trigger escalated alert.- Forbidden license detected: trigger legal/security notification.
For cleaner results, keep a clear project version strategy. Use the version that maps to what you deploy.
5.1 Simple policy you can start with
Start with three rules:
- notify on Critical vulnerabilities
- notify on High vulnerabilities
- notify on forbidden license findings
Once that is stable, decide if you want to block releases based on severity.
Step 6: Automate with GitHub Actions
Use this workflow as a starting point:
name: sbom-and-dependency-track
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
generate-and-upload-sbom:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Restore solution
run: dotnet restore SbomDependencyTrackDemo.slnx
- name: Restore local tools
run: dotnet tool restore
- name: Generate CycloneDX SBOM
run: |
mkdir -p artifacts/sbom
dotnet dotnet-CycloneDX SbomDependencyTrackDemo.slnx \
-o artifacts/sbom \
-fn sbom.cyclonedx.json \
-F Json \
-rs
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-cyclonedx
path: artifacts/sbom/sbom.cyclonedx.json
- name: Upload to Dependency-Track
if: ${{ secrets.DT_BASE_URL != '' && secrets.DT_API_KEY != '' && secrets.DT_PROJECT_UUID != '' }}
env:
DT_BASE_URL: ${{ secrets.DT_BASE_URL }}
DT_API_KEY: ${{ secrets.DT_API_KEY }}
DT_PROJECT_UUID: ${{ secrets.DT_PROJECT_UUID }}
run: |
curl -sS -X POST "${DT_BASE_URL}/api/v1/bom" \
-H "X-API-Key: ${DT_API_KEY}" \
-H "Content-Type: multipart/form-data" \
-F "project=${DT_PROJECT_UUID}" \
-F "bom=@artifacts/sbom/sbom.cyclonedx.json"
What it does:
- Restores .NET 10 and local tools.
- Generates CycloneDX SBOM.
- Uploads SBOM as workflow artifact.
- If secrets are set, uploads SBOM to Dependency-Track.
Required secrets:
DT_BASE_URLDT_API_KEYDT_PROJECT_UUID
Upload step is skipped if those secrets are missing, which is useful for public forks and early setup.
6.1 Small improvement for CI artifacts
If you want to keep SBOM artifacts for a fixed period:
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-cyclonedx
path: artifacts/sbom/sbom.cyclonedx.json
retention-days: 30
Step 7: Validate alert flow on purpose
If your pipeline is new, it helps to verify detection and notification end to end.
Use the sample toggle to include a legacy package in a test branch:
dotnet build -p:IncludeVulnerablePackage=true
./scripts/generate-sbom.sh
./scripts/upload-to-dependency-track.sh
Then verify:
- Vulnerability appears in Dependency-Track.
- Policy violation is generated.
- Notification path works.
Run this only in a non-production validation workflow.
To make this repeatable, automate a scheduled validation job in a sandbox project. This gives confidence that alert plumbing still works after platform or workflow updates.
Minimal weekly validation flow:
- Build with vulnerable package toggle.
- Generate and upload BOM.
- Assert that at least one policy violation appears.
- Assert that notification reached expected channel.
- Delete or archive validation project version.
Treat this as a health check for your software supply chain controls.
Conclusion
CycloneDX plus Dependency-Track gives you a dependency visibility loop that is actually actionable in day-to-day delivery. The setup is not complicated, but consistency matters.
Generate SBOM for every meaningful build, upload automatically, and review findings in Dependency-Track. Once this is part of your normal build process, vulnerability tracking becomes much easier.
If you want the complete ready-to-run version with all files in one place, use this repository:
https://github.com/mehdihadeli/blog-samples/tree/main/sbom-dependency-track-dotnet10


