# 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`: ```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: ```json { "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: ```json { "mode": "incremental", // or "full" "canonical_ids": null // null = all, or specific IDs } ``` Response: ```json { "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 ```csharp // 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: ```csharp // 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: ```csharp // In VEX ingestion pipeline await interestService.RecalculateForCanonicalAsync(canonicalId, ct); ``` ## CLI Commands ```bash # 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 ```yaml # 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 ```sql 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; ```