362 lines
12 KiB
Markdown
362 lines
12 KiB
Markdown
# SBOM Learning API
|
|
|
|
Per SPRINT_8200_0013_0003.
|
|
|
|
## Overview
|
|
|
|
The SBOM Learning API enables Concelier to learn which advisories are relevant to your organization by registering SBOMs from scanned images. When an SBOM is registered, Concelier matches its components against the canonical advisory database and updates interest scores accordingly.
|
|
|
|
## SBOM Extraction
|
|
Concelier normalizes incoming CycloneDX 1.7 and SPDX 3.0.1 documents into the internal `ParsedSbom` model for matching and downstream analysis.
|
|
|
|
Current extraction coverage (SPRINT_20260119_015):
|
|
- Document metadata: format, specVersion, serialNumber, created, name, namespace when present
|
|
- Components: bomRef, type, name, version, purl, cpe, hashes (including SPDX verifiedUsing), license IDs/expressions, license text (base64 decode), external references, properties, scope/modified, supplier/manufacturer, evidence, pedigree, cryptoProperties, modelCard (CycloneDX)
|
|
- Dependencies: component dependency edges (CycloneDX dependencies, SPDX relationships)
|
|
- Services: endpoints, authentication, crossesTrustBoundary, data flows, licenses, external references (CycloneDX)
|
|
- Formulation: components, workflows, tasks, properties (CycloneDX)
|
|
- Build metadata: buildId, buildType, timestamps, config source, environment, parameters (SPDX)
|
|
- Document properties
|
|
|
|
Notes:
|
|
- Full SPDX Licensing profile objects, vulnerabilities, and other SPDX profiles are pending in SPRINT_20260119_015.
|
|
- Matching currently uses PURL and CPE; additional fields are stored for downstream consumers.
|
|
|
|
## Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ SBOM Learning Flow │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────┐ scan ┌─────────┐ SBOM ┌───────────┐ │
|
|
│ │ Image │ ──────────► │ Scanner │ ─────────► │ Concelier │ │
|
|
│ │ │ │ │ │ │ │
|
|
│ └─────────┘ └─────────┘ └─────┬─────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────┐ │
|
|
│ │ SBOM Registration │ │
|
|
│ │ ┌───────────────┐ │ │
|
|
│ │ │ Extract PURLs │ │ │
|
|
│ │ └───────┬───────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ▼ │ │
|
|
│ │ ┌───────────────┐ │ │
|
|
│ │ │ Match Advs │ │ │
|
|
│ │ └───────┬───────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ▼ │ │
|
|
│ │ ┌───────────────┐ │ │
|
|
│ │ │ Update Scores │ │ │
|
|
│ │ └───────────────┘ │ │
|
|
│ └─────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
### Register SBOM
|
|
|
|
```
|
|
POST /api/v1/learn/sbom
|
|
Content-Type: application/vnd.cyclonedx+json
|
|
```
|
|
|
|
or
|
|
|
|
```
|
|
POST /api/v1/learn/sbom
|
|
Content-Type: application/spdx+json
|
|
```
|
|
|
|
**Request Body:** CycloneDX or SPDX SBOM document
|
|
|
|
**Query Parameters:**
|
|
|
|
| Parameter | Type | Default | Description |
|
|
|-----------|------|---------|-------------|
|
|
| `artifact_id` | string | required | Image digest or artifact identifier |
|
|
| `update_scores` | bool | true | Trigger immediate score recalculation |
|
|
| `include_reachability` | bool | true | Include reachability data in matching |
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"sbom_id": "uuid",
|
|
"sbom_digest": "sha256:abc123...",
|
|
"artifact_id": "sha256:image...",
|
|
"component_count": 234,
|
|
"matched_advisories": 15,
|
|
"scores_updated": true,
|
|
"registered_at": "2025-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
### Get Affected Advisories
|
|
|
|
```
|
|
GET /api/v1/sboms/{digest}/affected
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"sbom_digest": "sha256:abc123...",
|
|
"artifact_id": "sha256:image...",
|
|
"matched_advisories": [
|
|
{
|
|
"canonical_id": "uuid",
|
|
"cve": "CVE-2024-1234",
|
|
"severity": "high",
|
|
"interest_score": 0.85,
|
|
"matched_component": "pkg:npm/express@4.17.1",
|
|
"is_reachable": true
|
|
},
|
|
{
|
|
"canonical_id": "uuid",
|
|
"cve": "CVE-2024-5678",
|
|
"severity": "medium",
|
|
"interest_score": 0.65,
|
|
"matched_component": "pkg:npm/lodash@4.17.20",
|
|
"is_reachable": false
|
|
}
|
|
],
|
|
"total_count": 15,
|
|
"last_matched_at": "2025-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
### List Registered SBOMs
|
|
|
|
```
|
|
GET /api/v1/sboms
|
|
```
|
|
|
|
**Query Parameters:**
|
|
|
|
| Parameter | Type | Default | Description |
|
|
|-----------|------|---------|-------------|
|
|
| `artifact_id` | string | null | Filter by artifact |
|
|
| `since` | datetime | null | Only SBOMs registered after this time |
|
|
| `limit` | int | 100 | Max results |
|
|
| `cursor` | string | null | Pagination cursor |
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"sboms": [
|
|
{
|
|
"id": "uuid",
|
|
"artifact_id": "sha256:image...",
|
|
"sbom_digest": "sha256:abc123...",
|
|
"sbom_format": "cyclonedx",
|
|
"component_count": 234,
|
|
"matched_advisory_count": 15,
|
|
"registered_at": "2025-01-15T10:30:00Z"
|
|
}
|
|
],
|
|
"total_count": 42,
|
|
"next_cursor": "cursor..."
|
|
}
|
|
```
|
|
|
|
### Unregister SBOM
|
|
|
|
```
|
|
DELETE /api/v1/sboms/{digest}
|
|
```
|
|
|
|
**Query Parameters:**
|
|
|
|
| Parameter | Type | Default | Description |
|
|
|-----------|------|---------|-------------|
|
|
| `update_scores` | bool | true | Recalculate scores after removal |
|
|
|
|
## Matching Algorithm
|
|
|
|
### PURL Matching
|
|
|
|
1. **Exact Match:** `pkg:npm/express@4.17.1` matches advisories affecting exactly that version
|
|
2. **Range Match:** Uses semantic version ranges from advisory affects_key
|
|
3. **Namespace Normalization:** `@scope/pkg` normalized for comparison
|
|
|
|
### CPE Matching
|
|
|
|
For OS packages (rpm, deb):
|
|
|
|
1. Extract CPE from SBOM
|
|
2. Match against advisory CPE patterns
|
|
3. Apply distro-specific version logic (NEVRA/EVR)
|
|
|
|
### Reachability Integration
|
|
|
|
When `include_reachability=true`:
|
|
|
|
1. Query Scanner call graph data for matched components
|
|
2. Mark `is_reachable` based on path from entry point
|
|
3. Factor into interest score calculation
|
|
|
|
## Events
|
|
|
|
### SbomLearned
|
|
|
|
Published when SBOM is registered:
|
|
|
|
```json
|
|
{
|
|
"event_type": "sbom_learned",
|
|
"sbom_id": "uuid",
|
|
"sbom_digest": "sha256:...",
|
|
"artifact_id": "sha256:...",
|
|
"component_count": 234,
|
|
"matched_advisory_count": 15,
|
|
"timestamp": "2025-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
### ScoresUpdated
|
|
|
|
Published after batch score update:
|
|
|
|
```json
|
|
{
|
|
"event_type": "scores_updated",
|
|
"trigger": "sbom_registration",
|
|
"sbom_digest": "sha256:...",
|
|
"advisories_updated": 15,
|
|
"timestamp": "2025-01-15T10:30:05Z"
|
|
}
|
|
```
|
|
|
|
## Auto-Learning
|
|
|
|
Subscribe to Scanner events for automatic SBOM registration:
|
|
|
|
### Configuration
|
|
|
|
```yaml
|
|
SbomIntegration:
|
|
AutoLearn:
|
|
Enabled: true
|
|
SubscribeToScanEvents: true
|
|
EventSource: "scanner:scan_completed"
|
|
|
|
Matching:
|
|
EnablePurl: true
|
|
EnableCpe: true
|
|
IncludeReachability: true
|
|
|
|
ScoreUpdate:
|
|
BatchSize: 1000
|
|
DelaySeconds: 5 # Debounce rapid updates
|
|
```
|
|
|
|
### Event Handler
|
|
|
|
```csharp
|
|
// Automatic registration on scan completion
|
|
public class ScanCompletedHandler : IEventHandler<ScanCompletedEvent>
|
|
{
|
|
public async Task HandleAsync(ScanCompletedEvent evt, CancellationToken ct)
|
|
{
|
|
await _sbomService.LearnFromScanAsync(
|
|
artifactId: evt.ImageDigest,
|
|
sbomDigest: evt.SbomDigest,
|
|
sbomContent: evt.SbomContent,
|
|
cancellationToken: ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
## CLI Commands
|
|
|
|
```bash
|
|
# Register SBOM from file
|
|
stella learn sbom --file ./sbom.json --artifact sha256:image...
|
|
|
|
# Register from stdin
|
|
cat sbom.json | stella learn sbom --artifact sha256:image...
|
|
|
|
# List affected advisories
|
|
stella sbom affected sha256:sbomdigest...
|
|
|
|
# List registered SBOMs
|
|
stella sbom list --limit 20
|
|
|
|
# Unregister SBOM
|
|
stella sbom unregister sha256:sbomdigest...
|
|
```
|
|
|
|
## Integration Examples
|
|
|
|
### CI/CD Pipeline
|
|
|
|
```yaml
|
|
# Example GitHub Actions workflow
|
|
- name: Scan image
|
|
run: stella scan image myapp:latest -o sbom.json
|
|
|
|
- name: Register SBOM
|
|
run: stella learn sbom --file sbom.json --artifact ${{ steps.build.outputs.digest }}
|
|
|
|
- name: Check for critical advisories
|
|
run: |
|
|
AFFECTED=$(stella sbom affected ${{ steps.sbom.outputs.digest }} --severity critical --count)
|
|
if [ "$AFFECTED" -gt 0 ]; then
|
|
echo "::error::Found $AFFECTED critical advisories"
|
|
exit 1
|
|
fi
|
|
```
|
|
|
|
### Programmatic Registration
|
|
|
|
```csharp
|
|
// Register SBOM from code
|
|
var result = await sbomService.RegisterSbomAsync(
|
|
artifactId: imageDigest,
|
|
sbomContent: sbomJson,
|
|
format: SbomFormat.CycloneDX,
|
|
options: new RegistrationOptions
|
|
{
|
|
UpdateScores = true,
|
|
IncludeReachability = true
|
|
},
|
|
cancellationToken);
|
|
|
|
// Get affected advisories
|
|
var affected = await sbomService.GetAffectedAdvisoriesAsync(
|
|
sbomDigest: result.SbomDigest,
|
|
cancellationToken);
|
|
```
|
|
|
|
## Database Schema
|
|
|
|
```sql
|
|
CREATE TABLE vuln.sbom_registry (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
artifact_id TEXT NOT NULL,
|
|
sbom_digest TEXT NOT NULL,
|
|
sbom_format TEXT NOT NULL,
|
|
component_count INT NOT NULL DEFAULT 0,
|
|
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_matched_at TIMESTAMPTZ,
|
|
CONSTRAINT uq_sbom_registry_digest UNIQUE (tenant_id, sbom_digest)
|
|
);
|
|
|
|
CREATE TABLE vuln.sbom_canonical_match (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
sbom_id UUID NOT NULL REFERENCES vuln.sbom_registry(id),
|
|
canonical_id UUID NOT NULL REFERENCES vuln.advisory_canonical(id),
|
|
matched_purl TEXT NOT NULL,
|
|
is_reachable BOOLEAN NOT NULL DEFAULT false,
|
|
matched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT uq_sbom_canonical_match UNIQUE (sbom_id, canonical_id)
|
|
);
|
|
```
|