9 min readMehdi Hadeli

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

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:

  1. Build your .NET app.
  2. Generate CycloneDX SBOM.
  3. Run Dependency-Track locally.
  4. Upload the SBOM.
  5. See vulnerability results in Dependency-Track.
  6. Automate the same flow in GitHub Actions.

Architecture Overview

Dependency Tracker Flow

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:

  1. The solution and project names used in scripts match your real repository paths.
  2. 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:

  1. -o artifacts/sbom: keeps BOM output in a predictable folder for upload and retention.
  2. -fn sbom.cyclonedx.json: stable file naming simplifies CI scripting.
  3. -F Json: JSON is easy to inspect and process with command-line tooling.
  4. -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:

  1. Do not expose PostgreSQL port unless you need it.
  2. Replace default credentials.
  3. Use explicit CORS origin values instead of *.
  4. Put API behind HTTPS.

Step 4: Upload SBOM to Dependency-Track

In Dependency-Track UI:

  1. Create a project.
  2. Create a team for automation with BOM_UPLOAD permission.
  3. Generate API key.
  4. 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:

  1. components count is not zero
  2. vulnerability tab has results
  3. project version is correct

Step 5: Configure alerts and policy rules

This part makes the dashboard useful for daily work.

At minimum, configure these:

  1. Notification publishers: email, Slack, webhook, or your ticketing target.
  2. Policy conditions: for example, fail on Critical, warn on High.
  3. License policy checks if your org has explicit allow/deny license rules.

A practical starting policy:

  1. Critical vulnerability found in project component: trigger immediate alert.
  2. High vulnerability older than 7 days: trigger escalated alert.
  3. 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:

  1. notify on Critical vulnerabilities
  2. notify on High vulnerabilities
  3. 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:

  1. Restores .NET 10 and local tools.
  2. Generates CycloneDX SBOM.
  3. Uploads SBOM as workflow artifact.
  4. If secrets are set, uploads SBOM to Dependency-Track.

Required secrets:

  • DT_BASE_URL
  • DT_API_KEY
  • DT_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:

  1. Vulnerability appears in Dependency-Track.
  2. Policy violation is generated.
  3. 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:

  1. Build with vulnerable package toggle.
  2. Generate and upload BOM.
  3. Assert that at least one policy violation appears.
  4. Assert that notification reached expected channel.
  5. 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

References