Files
git.stella-ops.org/docs/modules/concelier/interest-scoring.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

5.9 KiB
Raw Blame History

Interest Scoring

Per SPRINT_8200_0013_0002.

Overview

Interest scoring learns which advisories matter to your organization by analyzing SBOM intersections, reachability data, VEX status, and deployment signals. High-interest advisories are prioritized and cached, while low-interest ones are degraded to lightweight stubs to save resources.

Score Components

The interest score is computed from weighted factors:

Factor Weight Description
in_sbom 0.4 Advisory affects a component in registered SBOMs
reachable 0.25 Affected code is reachable in call graph
deployed 0.15 Component is deployed in production
no_vex_na 0.1 No VEX "not_affected" statement exists
recent 0.1 Advisory is recent (age decay applied)

Score Calculation

score = (in_sbom × 0.4) + (reachable × 0.25) + (deployed × 0.15)
      + (no_vex_na × 0.1) + (recent × 0.1)

Each factor is binary (0.0 or 1.0), except recent which decays over time.

Configuration

Configure in concelier.yaml:

InterestScoring:
  Enabled: true

  # Factor weights (must sum to 1.0)
  Weights:
    InSbom: 0.4
    Reachable: 0.25
    Deployed: 0.15
    NoVexNa: 0.1
    Recent: 0.1

  # Age decay for recent factor
  RecentDecay:
    HalfLifeDays: 90
    MinScore: 0.1

  # Stub degradation policy
  Degradation:
    Enabled: true
    ThresholdScore: 0.1
    GracePeriodDays: 30
    RetainCve: true  # Keep CVE ID even in stub form

  # Recalculation job
  RecalculationJob:
    Enabled: true
    IncrementalIntervalMinutes: 15
    FullRecalculationHour: 3  # 3 AM daily

API Endpoints

Get Score

GET /api/v1/canonical/{id}/score

Response:

{
  "canonical_id": "uuid",
  "score": 0.85,
  "tier": "high",
  "reasons": [
    { "factor": "in_sbom", "value": 1.0, "weight": 0.4, "contribution": 0.4 },
    { "factor": "reachable", "value": 1.0, "weight": 0.25, "contribution": 0.25 },
    { "factor": "deployed", "value": 1.0, "weight": 0.15, "contribution": 0.15 },
    { "factor": "no_vex_na", "value": 0.0, "weight": 0.1, "contribution": 0.0 },
    { "factor": "recent", "value": 0.5, "weight": 0.1, "contribution": 0.05 }
  ],
  "computed_at": "2025-01-15T10:30:00Z",
  "last_seen_in_build": "uuid"
}

Trigger Recalculation (Admin)

POST /api/v1/scores/recalculate

Request:

{
  "mode": "incremental",  // or "full"
  "canonical_ids": null    // null = all, or specific IDs
}

Response:

{
  "job_id": "uuid",
  "mode": "incremental",
  "canonicals_queued": 1234,
  "started_at": "2025-01-15T10:30:00Z"
}

Score Tiers

Tier Score Range Cache TTL Behavior
High >= 0.7 24 hours Full advisory cached, prioritized in queries
Medium 0.3 - 0.7 4 hours Full advisory cached
Low 0.1 - 0.3 1 hour Full advisory, eligible for degradation
Negligible < 0.1 none Degraded to stub after grace period

Stub Degradation

Low-interest advisories are degraded to lightweight stubs to save storage:

Full Advisory vs Stub

Field Full Stub
id
cve
affects_key
merge_hash
severity
title
summary
references
weaknesses
source_edges

Restoration

Stubs are restored to full advisories when:

  • Interest score increases above threshold
  • Advisory is directly queried by ID
  • Advisory appears in scan results
// Automatic restoration
await scoringService.RestoreFromStubAsync(canonicalId, ct);

Integration Points

SBOM Registration

When an SBOM is registered, interest scores are updated:

POST /api/v1/learn/sbom → Triggers score recalculation

Scan Events

Subscribe to ScanCompleted events:

// In event handler
public async Task HandleScanCompletedAsync(ScanCompletedEvent evt)
{
    await sbomService.LearnFromScanAsync(evt.SbomDigest, ct);
    // This triggers interest score updates for affected advisories
}

VEX Updates

VEX imports trigger score recalculation:

// In VEX ingestion pipeline
await interestService.RecalculateForCanonicalAsync(canonicalId, ct);

CLI Commands

# View score for advisory
stella scores get sha256:mergehash...

# Trigger recalculation
stella scores recalculate --mode incremental

# Full recalculation
stella scores recalculate --mode full

# List high-interest advisories
stella scores list --tier high --limit 100

# Force restore from stub
stella scores restore sha256:mergehash...

Monitoring

Metrics

Metric Type Description
interest_score_computed_total Counter Scores computed
interest_score_distribution Histogram Score distribution
interest_stubs_created_total Counter Advisories degraded to stubs
interest_stubs_restored_total Counter Stubs restored to full
interest_job_duration_seconds Histogram Recalculation job duration

Alerts

# Example Prometheus alert
- alert: InterestScoreJobStale
  expr: time() - interest_score_last_full_recalc > 172800
  for: 1h
  labels:
    severity: warning
  annotations:
    summary: "Interest score full recalculation hasn't run in 2 days"

Database Schema

CREATE TABLE vuln.interest_score (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    canonical_id UUID NOT NULL REFERENCES vuln.advisory_canonical(id),
    score NUMERIC(3,2) NOT NULL CHECK (score >= 0 AND score <= 1),
    reasons JSONB NOT NULL DEFAULT '[]',
    last_seen_in_build UUID,
    computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_interest_score_canonical UNIQUE (canonical_id)
);

CREATE INDEX idx_interest_score_score ON vuln.interest_score(score DESC);
CREATE INDEX idx_interest_score_high ON vuln.interest_score(canonical_id)
    WHERE score >= 0.7;