Files
git.stella-ops.org/docs/modules/concelier/sbom-learning-api.md
StellaOps Bot 17613acf57 feat: add bulk triage view component and related stories
- Exported BulkTriageViewComponent and its related types from findings module.
- Created a new accessibility test suite for score components using axe-core.
- Introduced design tokens for score components to standardize styling.
- Enhanced score breakdown popover for mobile responsiveness with drag handle.
- Added date range selector functionality to score history chart component.
- Implemented unit tests for date range selector in score history chart.
- Created Storybook stories for bulk triage view and score history chart with date range selector.
2025-12-26 01:01:35 +02:00

10 KiB

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.

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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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

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

// 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

# 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

# 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

// 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

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)
);