17 KiB
SBOM Generation Flow
Overview
The SBOM Generation Flow describes how StellaOps creates Software Bills of Materials for container images using its 11 language-specific analyzers. The flow covers layer extraction, dependency detection, transitive resolution, and final SBOM assembly in SPDX 3.0.1 or CycloneDX 1.6 format.
Business Value: Complete, accurate SBOMs enable precise vulnerability matching and regulatory compliance (Executive Order 14028, EU CRA).
Actors
| Actor | Type | Role |
|---|---|---|
| Scanner | Service | Orchestrates SBOM generation |
| Layer Extractor | Component | Unpacks OCI layers |
| OS Detector | Component | Identifies base operating system |
| Language Analyzers (11) | Components | Detect ecosystem-specific dependencies |
| SBOM Assembler | Component | Merges results into final SBOM |
| Attestor | Service | Signs SBOM with DSSE envelope |
Prerequisites
- Container image accessible (registry or local)
- Registry credentials if private registry
- Analyzer plugins enabled for target ecosystems
Language Analyzers
StellaOps includes 11 specialized analyzers:
| Analyzer | Ecosystems | Lock Files | Manifests |
|---|---|---|---|
| DotNet | NuGet, .NET | packages.lock.json, *.deps.json |
*.csproj, *.fsproj, Directory.Packages.props |
| Java | Maven, Gradle | pom.xml, build.gradle.kts |
gradle.lockfile, maven-dependency-tree.txt |
| Node | npm, yarn, pnpm | package-lock.json, yarn.lock, pnpm-lock.yaml |
package.json |
| Python | pip, Poetry, Pipenv | requirements.txt, poetry.lock, Pipfile.lock |
pyproject.toml, setup.py |
| Go | Go Modules | go.sum |
go.mod |
| Rust | Cargo | Cargo.lock |
Cargo.toml |
| PHP | Composer | composer.lock |
composer.json |
| Ruby | Bundler | Gemfile.lock |
Gemfile |
| Deno | Deno, JSR | deno.lock |
deno.json, import_map.json |
| Bun | Bun | bun.lockb |
package.json |
| Binary | Native ELF/PE/Mach-O | N/A | Symbol tables, build IDs |
Flow Diagram
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 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
│ │ │ │<─────┘
│ │ │ │
│ │ │ ... (8 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 Detector identifies the base operating system:
| Detection Method | Files Checked |
|---|---|
/etc/os-release |
ID, VERSION_ID |
/etc/alpine-release |
Alpine version |
/etc/debian_version |
Debian version |
| Package manager | apk, dpkg, rpm |
Result:
{
"os": "alpine",
"version": "3.19",
"package_manager": "apk",
"architecture": "amd64"
}
4. Parallel Analyzer Execution
All 11 analyzers run in parallel on the merged filesystem:
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
SBOM Assembler merges all analyzer results:
- Deduplication: Remove duplicate PURLs across analyzers
- Relationship mapping: Build component dependency graph
- Metadata enrichment: Add licenses, hashes, supplier info
- Format conversion: Output as SPDX 3.0.1 or CycloneDX 1.6
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"timestamp": "2024-12-29T10:30:00Z",
"tools": [{"name": "stellaops-scanner", "version": "2.1.0"}],
"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
Attestor creates DSSE envelope for the SBOM:
{
"_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@2.1.0",
"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..."
}
]
}
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
| Format | Schema Version | Use Case |
|---|---|---|
| CycloneDX | 1.6 | Default, rich dependency graph |
| SPDX | 3.0.1 | Regulatory compliance, legal |
| SPDX | 2.3 | Legacy compatibility |
Error Handling
| Error | Recovery |
|---|---|
| Layer download failed | Retry with exponential backoff |
| Analyzer timeout | Mark analyzer as partial, continue |
| Lock file parse error | Fall back to manifest parsing |
| Invalid PURL | Log warning, skip component |
| Attestation failed | Return SBOM without attestation |
Observability
Metrics
| Metric | Type | Labels |
|---|---|---|
sbom_generation_duration_seconds |
Histogram | image_size, layer_count |
sbom_analyzer_duration_seconds |
Histogram | analyzer |
sbom_components_total |
Counter | analyzer, ecosystem |
sbom_size_bytes |
Histogram | format |
Key Log Events
| Event | Level | Fields |
|---|---|---|
sbom.generation.start |
INFO | image, digest |
sbom.analyzer.complete |
DEBUG | analyzer, package_count |
sbom.assembly.complete |
INFO | total_components, format |
sbom.attestation.signed |
INFO | digest, keyid |
Related Flows
- Scan Submission Flow - Parent flow
- Binary Delta Attestation Flow - Binary-level analysis
- Evidence Bundle Export Flow - SBOM packaging