SBOM Generation Flow

Overview

The SBOM Generation Flow describes how Stella Ops creates Software Bills of Materials for container images using its language-specific analyzers, OS package analyzers, and a native (ELF) analyzer. The flow covers layer extraction, dependency detection, transitive resolution, and final SBOM assembly in SPDX 3.0.1 or CycloneDX 1.7 format.

Source of truth: src/Scanner/. As of writing the repository ships 14 language analyzers (src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.*), a set of OS package analyzers (StellaOps.Scanner.Analyzers.OS.*), and a native ELF analyzer (StellaOps.Scanner.Analyzers.Native). Analyzers are loaded by the Scanner Worker as DSSE-signed plug-ins; they are not separate services. Verify the analyzer roster against src/ before relying on counts here.

Business Value: Complete, accurate SBOMs enable precise vulnerability matching and regulatory compliance (Executive Order 14028, EU CRA).

Actors

ActorTypeRole
Scanner WebServiceServiceAccepts scan/SBOM submissions, exposes results (src/Scanner/StellaOps.Scanner.WebService)
Scanner WorkerServiceRuns the staged scan pipeline that produces the SBOM (src/Scanner/StellaOps.Scanner.Worker)
Layer ExtractorStageUnpacks OCI layers (PullLayersStageExecutor, BuildFilesystemStageExecutor, LayerArchiveExtractor)
OS Package AnalyzersPlug-insIdentify base OS packages (apk, dpkg, rpm, pacman, portage, homebrew, macOS bundle/pkgutil, Windows MSI/Chocolatey/WinSxS)
Language Analyzers (14)Plug-insDetect ecosystem-specific dependencies
Native AnalyzerPlug-inExtracts ELF symbols / call-graph for reachability (StellaOps.Scanner.Analyzers.Native)
SBOM ComposerComponentMerges fragments into CycloneDX / SPDX (StellaOps.Scanner.Emit composers)
Signer / AttestorServiceProduces DSSE-signed surface manifests, PoE artifacts, and verdict attestations

Prerequisites

Language Analyzers

Stella Ops ships 14 language analyzers under src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.*. The table below lists the manifest and lock files each analyzer keys on; verify the column values against the analyzer source (e.g. *BuildFileDiscovery, *LockFileCollector, *Parser) — the lock/manifest distinction below is a summary, not an exhaustive list.

AnalyzerEcosystemsManifests (declared)Lock / pinned files
DotNetNuGet, .NET*.csproj, *.fsproj, *.vbproj, Directory.Build.props, Directory.Packages.props, packages.config, global.jsonpackages.lock.json, *.deps.json
JavaMaven, Gradlepom.xml, build.gradle, build.gradle.kts, embedded MANIFEST.MF / pom.xml inside *.jargradle.lockfile
Nodenpm, yarn, pnpmpackage.jsonpackage-lock.json, yarn.lock, pnpm-lock.yaml
Pythonpip, Poetry, Pipenvpyproject.toml, setup.pyrequirements.txt, poetry.lock, Pipfile.lock
GoGo Modulesgo.mod, go.workgo.sum, vendor/modules.txt, plus build-info embedded in Go binaries
RustCargoCargo.tomlCargo.lock
PHPComposercomposer.jsoncomposer.lock
RubyBundlerGemfileGemfile.lock
DenoDeno, JSRdeno.json, import_map.jsondeno.lock
BunBunpackage.jsonbun.lockb
C/C++ (Ccpp)Native C/C++ build inputs
SwiftSwift / Mach-OPackage.swiftPackage.resolved
DartDart / Flutter (pub)pubspec.yamlpubspec.lock
ElixirMix / Hexmix.exs, Erlang .app filesmix.lock

The native binary analyzer (StellaOps.Scanner.Analyzers.Native) is separate from the language analyzers above. It parses ELF binaries (symbol tables, build IDs) to build a reachability call-graph; PE and Mach-O are handled within the .NET and Swift analyzers respectively rather than by a single unified “Binary” analyzer.

OS Package Analyzers

In addition to language analyzers, the Scanner ships OS package analyzers (StellaOps.Scanner.Analyzers.OS.*) that enumerate distro-managed packages:

AnalyzerSource
apkAlpine package database
dpkgDebian/Ubuntu dpkg status
rpmRPM database (RHEL/Fedora/SUSE)
pacmanArch Linux
portageGentoo
homebrewmacOS Homebrew
macOS bundle / pkgutilmacOS app bundles and installer receipts
Windows MSI / Chocolatey / WinSxSWindows installed-product and side-by-side assemblies

The Scanner also ships a Secrets analyzer (StellaOps.Scanner.Analyzers.Secrets, run by the worker’s separate scan-secrets stage) and a build-info/SBOM indexer (StellaOps.Scanner.Sbomer.BuildXPlugin). The Secrets analyzer detects leaked credentials and does not contribute components to the SBOM; it is out of scope for this flow.

Flow Diagram

Simplified sequence. In the implementation the Scanner Worker drives an ordered stage pipeline (ScanStageNames.Ordered) — resolve-image → pull-layers → build-filesystem → checkout-source → execute-analyzers → … → compose-artifacts → emit-reports → generate-poe → push-verdict. The language/OS/native analyzers run as in-process signed plug-ins during execute-analyzers, not as separate downstream services.

┌─────────────────────────────────────────────────────────────────────────────────┐
│                           SBOM Generation Flow                                   │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────┐   ┌───────────┐   ┌────────────┐   ┌───────────────────────────────┐
│ Scanner │   │  Layer    │   │    OS      │   │      Language Analyzers       │
│         │   │ Extractor │   │  Detector  │   │ (DotNet, Java, Node, etc.)    │
└────┬────┘   └─────┬─────┘   └──────┬─────┘   └───────────────┬───────────────┘
     │              │                │                         │
     │ Fetch image  │                │                         │
     │ manifest     │                │                         │
     │──────┐       │                │                         │
     │      │       │                │                         │
     │<─────┘       │                │                         │
     │              │                │                         │
     │ Extract      │                │                         │
     │ layers       │                │                         │
     │─────────────>│                │                         │
     │              │                │                         │
     │              │ Unpack to      │                         │
     │              │ work dir       │                         │
     │              │───────┐        │                         │
     │              │       │        │                         │
     │              │<──────┘        │                         │
     │              │                │                         │
     │              │ Layers ready   │                         │
     │<─────────────│                │                         │
     │              │                │                         │
     │ Detect OS    │                │                         │
     │──────────────────────────────>│                         │
     │              │                │                         │
     │              │                │ Parse /etc/os-release   │
     │              │                │ Check package manager   │
     │              │                │──────────┐              │
     │              │                │          │              │
     │              │                │<─────────┘              │
     │              │                │                         │
     │ OS: Alpine 3.19               │                         │
     │<──────────────────────────────│                         │
     │              │                │                         │
     │ Fan-out to   │                │                         │
     │ all analyzers│                │                         │
     │────────────────────────────────────────────────────────>│
     │              │                │                         │
     │              │                │                         │ ┌─────────┐
     │              │                │                         │ │ DotNet  │
     │              │                │                         │ └────┬────┘
     │              │                │                         │      │
     │              │                │                   Scan *.csproj
     │              │                │                   Parse deps.json
     │              │                │                         │<─────┘
     │              │                │                         │
     │              │                │                         │ ┌─────────┐
     │              │                │                         │ │  Java   │
     │              │                │                         │ └────┬────┘
     │              │                │                         │      │
     │              │                │                   Parse pom.xml
     │              │                │                   Run gradle deps
     │              │                │                         │<─────┘
     │              │                │                         │
     │              │                │                         │ ┌─────────┐
     │              │                │                         │ │  Node   │
     │              │                │                         │ └────┬────┘
     │              │                │                         │      │
     │              │                │                   Parse lockfiles
     │              │                │                   Build dep tree
     │              │                │                         │<─────┘
     │              │                │                         │
     │              │                │           ... (more analyzers)
     │              │                │                         │
     │ Analyzer     │                │                         │
     │ results      │                │                         │
     │<────────────────────────────────────────────────────────│
     │              │                │                         │
     │  ┌────────────────┐           │                         │
     │  │ SBOM Assembler │           │                         │
     │  └───────┬────────┘           │                         │
     │          │                    │                         │
     │ Merge &  │                    │                         │
     │ dedupe   │                    │                         │
     │──────────>                    │                         │
     │          │                    │                         │
     │ SBOM     │                    │                         │
     │ document │                    │                         │
     │<─────────│                    │                         │
     │          │                    │                         │
     │  ┌─────────┐                  │                         │
     │  │Attestor │                  │                         │
     │  └────┬────┘                  │                         │
     │       │                       │                         │
     │ Sign  │                       │                         │
     │───────>                       │                         │
     │       │                       │                         │
     │ DSSE  │                       │                         │
     │<──────│                       │                         │
     │       │                       │                         │

Step-by-Step

1. Image Manifest Fetch

Scanner retrieves OCI image manifest:

GET /v2/library/nginx/manifests/1.25 HTTP/1.1
Host: registry-1.docker.io
Accept: application/vnd.oci.image.manifest.v1+json

Response contains layer digests:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "digest": "sha256:config123..."
  },
  "layers": [
    {"digest": "sha256:layer1...", "size": 31456789},
    {"digest": "sha256:layer2...", "size": 1234567}
  ]
}

2. Layer Extraction

Layer Extractor downloads and unpacks each layer:

work/
├── layer-0/          # Base OS layer (Alpine, Debian, etc.)
│   ├── etc/
│   ├── lib/
│   └── usr/
├── layer-1/          # Application layer
│   ├── app/
│   │   ├── node_modules/
│   │   ├── package.json
│   │   └── package-lock.json
│   └── ...
└── merged/           # Union filesystem view

3. OS Detection

OS detection and OS-package enumeration are performed by the OS package analyzers (StellaOps.Scanner.Analyzers.OS.*) during analyzer execution; the table below summarizes the signals they key on:

Detection MethodFiles Checked
/etc/os-releaseID, VERSION_ID
/etc/alpine-releaseAlpine version
/etc/debian_versionDebian version
Package managerapk, dpkg, rpm

Result:

{
  "os": "alpine",
  "version": "3.19",
  "package_manager": "apk",
  "architecture": "amd64"
}

4. Analyzer Execution

The Worker’s execute-analyzers stage runs entry-trace plus the analyzer dispatcher over the built filesystem; each loaded analyzer plug-in contributes a fragment. Examples (see the full roster in Language Analyzers):

DotNet Analyzer

Scanning for:
- *.csproj, *.fsproj, *.vbproj
- *.deps.json (runtime dependencies)
- packages.lock.json (NuGet lock)
- Directory.Packages.props (central management)

Output:
{
  "packages": [
    {"purl": "pkg:nuget/Newtonsoft.Json@13.0.3", "scope": "runtime"},
    {"purl": "pkg:nuget/Microsoft.Extensions.Logging@8.0.0", "scope": "runtime"}
  ]
}

Java Analyzer

Scanning for:
- pom.xml (Maven)
- build.gradle, build.gradle.kts (Gradle)
- *.jar in lib/ directories
- MANIFEST.MF inside JARs

Output:
{
  "packages": [
    {"purl": "pkg:maven/com.google.guava/guava@32.1.2-jre", "scope": "compile"},
    {"purl": "pkg:maven/org.slf4j/slf4j-api@2.0.9", "scope": "runtime"}
  ]
}

Node Analyzer

Scanning for:
- package.json + package-lock.json (npm)
- yarn.lock (Yarn)
- pnpm-lock.yaml (pnpm)
- node_modules/.package-lock.json

Output:
{
  "packages": [
    {"purl": "pkg:npm/express@4.18.2", "scope": "runtime"},
    {"purl": "pkg:npm/lodash@4.17.21", "scope": "runtime"}
  ]
}

5. Transitive Resolution

Each analyzer resolves transitive dependencies:

express@4.18.2
├── accepts@1.3.8
│   ├── mime-types@2.1.35
│   │   └── mime-db@1.52.0
│   └── negotiator@0.6.3
├── body-parser@1.20.1
│   ├── bytes@3.1.2
│   └── ...
└── ...

6. SBOM Assembly

The StellaOps.Scanner.Emit composers (CycloneDxComposer, SpdxComposer) merge all analyzer fragments:

  1. Deduplication: Remove duplicate PURLs across analyzers
  2. Relationship mapping: Build component dependency graph
  3. Metadata enrichment: Add licenses, hashes, supplier info
  4. Format conversion: Output as CycloneDX 1.7 or SPDX 3.0.1 (SpdxDefaults.SpecVersion = "3.0.1")

The CycloneDX composer serializes with the bundled library’s SpecificationVersion.v1_6 and then upgrades the emitted document to 1.7 (CycloneDx17Extensions.UpgradeJsonTo17 / UpgradeXmlTo17) because SpecificationVersion.v1_7 is not yet supported upstream. Emitted media types are therefore application/vnd.cyclonedx+json|+xml|+protobuf; version=1.7. The composer emits two views: an Inventory view (all components) and a Usage view (only components reached by an entrypoint), each available as JSON, XML, or Protobuf. A composition recipe and a Merkle root over the fragments are recorded for deterministic replay.

{
  "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "version": 1,
  "metadata": {
    "timestamp": "2024-12-29T10:30:00Z",
    "tools": [{"name": "stellaops-scanner", "version": "<scanner-version>"}],
    "component": {
      "type": "container",
      "name": "docker.io/library/nginx",
      "version": "1.25"
    }
  },
  "components": [
    {
      "type": "library",
      "bom-ref": "pkg:npm/express@4.18.2",
      "name": "express",
      "version": "4.18.2",
      "purl": "pkg:npm/express@4.18.2",
      "hashes": [{"alg": "SHA-256", "content": "abc123..."}]
    }
  ],
  "dependencies": [
    {
      "ref": "pkg:npm/express@4.18.2",
      "dependsOn": ["pkg:npm/accepts@1.3.8", "pkg:npm/body-parser@1.20.1"]
    }
  ]
}

7. SBOM Attestation

DSSE signing happens inside the Worker pipeline rather than as a post-hoc external call: the compose-artifacts stage DSSE-signs the surface manifest (IDsseEnvelopeSigner), the generate-poe stage signs Proof-of-Exposure artifacts via the Signer (HttpSignerPoEDsseSigningService), and the push-verdict stage publishes verdicts as OCI referrer artifacts. A representative SBOM in-toto statement (predicate detail below is illustrative):

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "docker.io/library/nginx",
      "digest": {"sha256": "abc123..."}
    }
  ],
  "predicateType": "https://spdx.dev/Document",
  "predicate": {
    "sbom": "base64-encoded-sbom...",
    "generator": "stellaops-scanner@<scanner-version>",
    "timestamp": "2024-12-29T10:30:00Z"
  }
}

Signed with DSSE:

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "base64-encoded-statement...",
  "signatures": [
    {
      "keyid": "sha256:signer-key-fingerprint",
      "sig": "base64-encoded-signature..."
    }
  ]
}

Authorization & Scopes

Gotcha (repo-wide): the ASP.NET policy name the Scanner WebService enforces is in dot-form, while the canonical scope claim in StellaOpsScopes is colon-form. The Scanner accepts either form for read/write because the policies are registered as any-of. Always check src/Scanner/StellaOps.Scanner.WebService/Program.cs and Security/ScannerPolicies.cs for the live mapping.

OperationEndpointPolicy (dot-form)Accepted scope claims
Upload (bring-your-own) SBOMPOST /api/v1/sbom/uploadscanner.scans.writescanner.scans.write or scanner:write
Read uploaded SBOM recordGET /api/v1/sbom/uploads/{sbomId}scanner.scans.readscanner.scans.read or scanner:read
Read scan SBOM evidenceGET /api/v1/sbom/{scanId}/evidencescanner.scans.readscanner.scans.read or scanner:read
Write signed-SBOM material(signed-SBOM material endpoints)scanner.signed-sbom-material.writescanner.signed-sbom-material.write (single scope, not any-of)

Routes verified against SbomUploadEndpoints.cs / SbomEvidenceEndpoints.cs and the default API base path /api/v1 (ScannerWebServiceOptions.Api.BasePath). The upload handler takes a JSON SbomUploadRequestDto (base64-encoded SBOM in sbomBase64 plus an optional format hint); it does not use content-type negotiation. ScansRead/ScansWrite are registered as any-of (dot-form or colon-form); SignedSbomMaterialWrite is a single-scope policy.

Canonical scopes related to SBOM generation/consumption in StellaOpsScopes.cs: scanner:read, scanner:scan, scanner:export, scanner:write, scanner.signed-sbom-material.write, and the SBOM document scopes sbom:read, sbom:write, sbom:attest.

Data Contracts

Analyzer Output Schema

interface AnalyzerOutput {
  analyzer: string;
  ecosystem: string;
  packages: Array<{
    purl: string;
    name: string;
    version: string;
    scope: 'runtime' | 'dev' | 'optional';
    locations: string[];
    hashes?: Record<string, string>;
    licenses?: string[];
  }>;
  relationships: Array<{
    parent: string;
    child: string;
    type: 'depends-on' | 'dev-depends-on';
  }>;
}

SBOM Output Formats

Emitted by the StellaOps.Scanner.Emit composers:

FormatSchema VersionUse Case
CycloneDX1.7Default; rich dependency graph (JSON, XML, and Protobuf serializers)
SPDX3.0.1Regulatory compliance, legal (JSON-LD)

Ingest vs. emit. The Scanner emits CycloneDX 1.7 (serialized as 1.6, upgraded to 1.7) and SPDX 3.0.1; there is no SPDX 2.x emitter in src/Scanner/__Libraries/StellaOps.Scanner.Emit. The “bring-your-own-SBOM” upload endpoint (POST /api/v1/sbom/upload) is more permissive on ingest: SbomByosUploadService.ValidateFormat accepts CycloneDX 1.4–1.6 and SPDX 2.3 and 3.0 (note SPDX 2.3 is accepted on ingest even though it is never emitted). Format/version are detected by parsing the JSON body (bomFormat/spdxVersion), with an optional format field as an override — there is no content-type negotiation. CycloneDX 1.7 is not currently accepted on upload despite being the emit version; verify the supported range against SbomByosUploadService.IsSupportedCycloneDx/IsSupportedSpdx before relying on it.

Error Handling

ErrorRecovery
Layer download failedRetry with exponential backoff
Analyzer timeoutMark analyzer as partial, continue
Lock file parse errorFall back to manifest parsing
Invalid PURLLog warning, skip component
Attestation failedReturn SBOM without attestation

Observability

NOT IMPLEMENTED as named below. The metric and log-event names in the tables below are illustrative and do not match what the Scanner emits today — none of sbom_generation_duration_seconds, sbom_analyzer_duration_seconds, sbom_components_total, sbom_size_bytes, or the sbom.* log events exist in src/Scanner/. The implemented worker instruments use millisecond-suffixed names such as scanner_worker_job_duration_ms, scanner_worker_stage_duration_ms, scanner_worker_secrets_analysis_duration_ms, and scanner_worker_surface_manifest_publish_duration_ms. Verify live names against the worker meters (e.g. StellaOps.Scanner.Worker) before wiring dashboards/alerts.

Metrics (illustrative — see note above)

MetricTypeLabels
sbom_generation_duration_secondsHistogramimage_size, layer_count
sbom_analyzer_duration_secondsHistogramanalyzer
sbom_components_totalCounteranalyzer, ecosystem
sbom_size_bytesHistogramformat

Key Log Events (illustrative — see note above)

EventLevelFields
sbom.generation.startINFOimage, digest
sbom.analyzer.completeDEBUGanalyzer, package_count
sbom.assembly.completeINFOtotal_components, format
sbom.attestation.signedINFOdigest, keyid