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.
This commit is contained in:
247
docs/modules/concelier/interest-scoring.md
Normal file
247
docs/modules/concelier/interest-scoring.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# 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;
|
||||
```
|
||||
Reference in New Issue
Block a user