- 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.
5.9 KiB
5.9 KiB
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;