save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -0,0 +1,367 @@
# AdvisoryAI Chat Interface
> **Sprint:** SPRINT_20260107_006_003 Task CH-016
> **Status:** Active
> **Last Updated:** 2026-01-09
The AdvisoryAI Chat Interface provides a conversational experience for security operators to investigate vulnerabilities, understand findings, and take remediation actions—all grounded in internal evidence with citations.
## Overview
The chat interface enables:
- **Multi-turn conversations** about vulnerabilities, components, and security posture
- **Grounded responses** with citations to internal evidence (SBOMs, VEX, attestations)
- **Action proposals** for risk approval, quarantine, and VEX creation
- **Streaming responses** for real-time feedback
---
## API Reference
### Create Conversation
Creates a new conversation session.
```http
POST /api/v1/advisory-ai/conversations
Content-Type: application/json
Authorization: Bearer <token>
{
"tenantId": "tenant-123",
"context": {
"findingId": "f-456",
"scanId": "s-789",
"cveId": "CVE-2023-44487",
"component": "pkg:npm/lodash@4.17.21"
},
"metadata": {
"source": "ui",
"version": "1.0"
}
}
```
**Response:**
```json
{
"conversationId": "conv-abc123",
"tenantId": "tenant-123",
"userId": "user-xyz",
"createdAt": "2026-01-09T12:00:00Z",
"updatedAt": "2026-01-09T12:00:00Z",
"context": {
"currentCveId": "CVE-2023-44487",
"currentComponent": "pkg:npm/lodash@4.17.21"
},
"turnCount": 0
}
```
### Send Message (Streaming)
Sends a user message and streams the AI response.
```http
POST /api/v1/advisory-ai/conversations/{conversationId}/turns
Content-Type: application/json
Accept: text/event-stream
Authorization: Bearer <token>
{
"message": "Is CVE-2023-44487 exploitable in our environment?"
}
```
**Response (Server-Sent Events):**
```
event: token
data: {"content": "Based on the reachability analysis "}
event: token
data: {"content": "[reach:api-gateway:grpc.Server], "}
event: citation
data: {"type": "reach", "path": "api-gateway:grpc.Server", "verified": true}
event: token
data: {"content": "this vulnerability **is reachable** in your environment."}
event: action
data: {"type": "approve", "label": "Accept Risk", "enabled": true, "parameters": {"cve_id": "CVE-2023-44487"}}
event: grounding
data: {"score": 0.92, "citations": 3, "claims": 2}
event: done
data: {"turnId": "turn-xyz", "groundingScore": 0.92}
```
### Get Conversation
Retrieves a conversation with its history.
```http
GET /api/v1/advisory-ai/conversations/{conversationId}
Authorization: Bearer <token>
```
**Response:**
```json
{
"conversationId": "conv-abc123",
"tenantId": "tenant-123",
"userId": "user-xyz",
"createdAt": "2026-01-09T12:00:00Z",
"updatedAt": "2026-01-09T12:05:00Z",
"context": { ... },
"turns": [
{
"turnId": "turn-001",
"role": "user",
"content": "Is CVE-2023-44487 exploitable?",
"timestamp": "2026-01-09T12:01:00Z"
},
{
"turnId": "turn-002",
"role": "assistant",
"content": "Based on the reachability analysis...",
"timestamp": "2026-01-09T12:01:05Z",
"evidenceLinks": [
{"type": "reach", "uri": "reach://api-gateway:grpc.Server", "label": "Reachability trace"}
],
"groundingScore": 0.92
}
],
"turnCount": 2
}
```
### List Conversations
Lists conversations for a tenant/user.
```http
GET /api/v1/advisory-ai/conversations?tenantId=tenant-123&userId=user-xyz&limit=20
Authorization: Bearer <token>
```
### Delete Conversation
Deletes a conversation and its history.
```http
DELETE /api/v1/advisory-ai/conversations/{conversationId}
Authorization: Bearer <token>
```
---
## Object Link Format
AI responses include object links that reference internal evidence. These links enable deep navigation to source data.
### Supported Link Types
| Type | Format | Example | Description |
|------|--------|---------|-------------|
| SBOM | `[sbom:{id}]` | `[sbom:abc123]` | Link to SBOM document |
| Reachability | `[reach:{service}:{function}]` | `[reach:api-gateway:grpc.Server]` | Link to reachability trace |
| Runtime | `[runtime:{service}:traces]` | `[runtime:api-gateway:traces]` | Link to runtime traces |
| VEX | `[vex:{issuer}:{digest}]` | `[vex:stellaops:sha256:abc]` | Link to VEX statement |
| Attestation | `[attest:dsse:{digest}]` | `[attest:dsse:sha256:xyz]` | Link to DSSE attestation |
| Authority Key | `[auth:keys/{keyId}]` | `[auth:keys/gitlab-oidc]` | Link to signing key |
| Documentation | `[docs:{path}]` | `[docs:scopes/ci-webhook]` | Link to documentation |
### Link Resolution
Object links are validated by the grounding system:
- **Valid links** resolve to existing objects and render as clickable chips
- **Invalid links** are flagged as grounding issues and may lower the grounding score
- Links must be within `MaxLinkDistance` characters of related claims to count as grounding
### Example Response with Links
```markdown
The vulnerability **CVE-2023-44487** affects your deployment.
**Evidence:**
- The component is present in your SBOM [sbom:scan-2026-01-09-abc123]
- Reachability analysis shows the vulnerable function is called [reach:api-gateway:grpc.Server]
- No VEX statement currently exists for this product
**Recommendation:** Consider creating a VEX statement to document your assessment.
```
---
## Action Types
The AI can propose actions that users can execute directly from the chat interface. Actions are permission-gated and require explicit confirmation.
### Available Actions
| Action | Required Role | Parameters | Description |
|--------|---------------|------------|-------------|
| `approve` | `approver` | `cve_id`, `rationale?`, `expiry?` | Accept risk for a vulnerability |
| `quarantine` | `operator` | `image_digest` | Block an image from deployment |
| `defer` | `triage` | `cve_id`, `assignee?` | Mark for later investigation |
| `generate_manifest` | `admin` | `integration_type` | Create integration manifest |
| `create_vex` | `issuer` | `product`, `vulnerability`, `status` | Draft VEX statement |
### Action Button Format
Actions appear in responses using this format:
```
[Action Label]{action:type,param1=value1,param2=value2}
```
Example:
```
You may want to accept this risk: [Accept Risk]{action:approve,cve_id=CVE-2023-44487,rationale=tested}
```
### Action Execution Flow
1. **Parsing**: ActionProposalParser extracts actions from model output
2. **Permission Check**: User roles are validated against required role
3. **Display**: Allowed actions render as buttons; blocked actions show disabled with reason
4. **Confirmation**: User clicks button and confirms in modal
5. **Execution**: Backend executes action with audit trail
6. **Result**: Success/failure displayed in chat
### Blocked Actions
When a user lacks permission for an action:
```json
{
"actionType": "approve",
"label": "Accept Risk",
"isAllowed": false,
"blockedReason": "Requires 'approver' role. You have: viewer, triage"
}
```
---
## Grounding System
All AI responses are validated for proper grounding—ensuring claims are backed by evidence.
### Grounding Score
The grounding score (0.0-1.0) measures response quality:
| Score Range | Quality | Description |
|-------------|---------|-------------|
| 0.90-1.00 | Excellent | All claims cited, all links valid |
| 0.70-0.89 | Good | Most claims cited, minor issues |
| 0.50-0.69 | Acceptable | Some claims uncited |
| < 0.50 | Rejected | Response regenerated |
### Grounding Validation
The validator checks:
1. **Link Validity**: Do object links resolve to real objects?
2. **Claim Grounding**: Are security claims near valid citations?
3. **Citation Density**: Are there enough citations for the response length?
### Claim Detection
Claims requiring grounding include:
- Vulnerability status: "is affected", "is not affected", "is vulnerable"
- Fix status: "has been fixed", "is patched", "is mitigated"
- Severity assessments: "CVSS score is 9.8", "severity is critical"
- Investigation status: "is under investigation"
### Grounding Issues
| Issue Type | Severity | Description |
|------------|----------|-------------|
| `InvalidLink` | Error | Object link does not resolve |
| `UngroundedClaim` | Warning | Security claim without nearby citation |
| `BelowThreshold` | Critical | Overall score below minimum |
### Configuration
```yaml
AdvisoryAI:
Chat:
Grounding:
MinGroundingScore: 0.5 # Minimum acceptable score
MaxLinkDistance: 200 # Max chars between claim and citation
```
---
## Example Flows
### Vulnerability Investigation
```n User: What is the status of CVE-2023-44487 in our environment?
Assistant: CVE-2023-44487 (HTTP/2 Rapid Reset) **is reachable** in your environment
[reach:api-gateway:grpc.Server].
**Status:** AFFECTED
- Component: grpc-go v1.56.0 [sbom:scan-abc123]
- Reachable path: api-gateway -> grpc.Server.handleStream()
- No VEX statement exists
**Recommended actions:**
[Accept Risk]{action:approve,cve_id=CVE-2023-44487}
[Create VEX]{action:create_vex,product=api-gateway,vulnerability=CVE-2023-44487,status=affected}
```n
---
### Action Execution
```n User: Please create a VEX statement marking CVE-2023-44487 as affected for api-gateway.
Assistant: I will create a VEX statement with the following details:
- **Product:** api-gateway
- **Vulnerability:** CVE-2023-44487
- **Status:** affected
- **Justification:** Vulnerable code is reachable
[Create VEX Statement]{action:create_vex,product=api-gateway,vulnerability=CVE-2023-44487,status=affected,justification=vulnerable_code_present}
```n
---
## Configuration
```yaml
AdvisoryAI:
Chat:
ConversationRetention: '7.00:00:00' # 7 days
MaxTurnsPerConversation: 50
TokenBudget: 8192
StreamingEnabled: true
Grounding:
MinGroundingScore: 0.5
MaxLinkDistance: 200
Actions:
RequireConfirmation: true
AuditAllExecutions: true```n
---
## Error Handling
| Status Code | Error | Description |
|-------------|-------|-------------|
| 400 | InvalidRequest | Malformed request body |
| 401 | Unauthorized | Missing or invalid token |
| 403 | Forbidden | Insufficient permissions |
| 404 | ConversationNotFound | Conversation does not exist |
| 429 | RateLimited | Too many requests |
| 500 | InternalError | Server error |
---
## See Also
- [AdvisoryAI Architecture](architecture.md)
- [Deployment Guide](deployment.md)
- [Security Guardrails](/docs/security/assistant-guardrails.md)

View File

@@ -0,0 +1,327 @@
# OpsMemory Module
> **Decision Ledger for Playbook Learning**
OpsMemory is a structured ledger of prior security decisions and their outcomes. It enables playbook learning - understanding which decisions led to good outcomes and surfacing institutional knowledge for similar situations.
## What OpsMemory Is
-**Decision + Outcome pairs**: Every security decision is recorded with its eventual outcome
-**Success/failure classification**: Learn what worked and what didn't
-**Similar situation matching**: Find past decisions in comparable scenarios
-**Playbook suggestions**: Surface recommendations based on historical success
## What OpsMemory Is NOT
- ❌ Chat history (that's conversation storage)
- ❌ Audit logs (that's the Timeline)
- ❌ VEX statements (that's Excititor)
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ OpsMemory Service │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ Decision │ │ Playbook │ │ Outcome │ │
│ │ Recording │ │ Suggestion │ │ Tracking │ │
│ └──────┬──────┘ └────────┬─────────┘ └───────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ IOpsMemoryStore │ │
│ │ (PostgreSQL with similarity vectors) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Core Components
### OpsMemoryRecord
The core data structure capturing a decision and its context:
```json
{
"memoryId": "mem-abc123",
"tenantId": "tenant-xyz",
"recordedAt": "2026-01-07T12:00:00Z",
"situation": {
"cveId": "CVE-2023-44487",
"component": "pkg:npm/http2@1.0.0",
"severity": "high",
"reachability": "reachable",
"epssScore": 0.97,
"isKev": true,
"contextTags": ["production", "external-facing", "payment-service"]
},
"decision": {
"action": "Remediate",
"rationale": "KEV + reachable + payment service = immediate remediation",
"decidedBy": "security-team",
"decidedAt": "2026-01-07T12:00:00Z",
"policyReference": "policy/critical-kev.rego"
},
"outcome": {
"status": "Success",
"resolutionTime": "4:30:00",
"lessonsLearned": "Upgrade was smooth, no breaking changes",
"recordedAt": "2026-01-07T16:30:00Z"
}
}
```
### Decision Actions
| Action | Description |
|--------|-------------|
| `Accept` | Accept the risk (no action) |
| `Remediate` | Upgrade/patch the component |
| `Quarantine` | Isolate the component |
| `Mitigate` | Apply compensating controls (WAF, config) |
| `Defer` | Defer for later review |
| `Escalate` | Escalate to security team |
| `FalsePositive` | Mark as not applicable |
### Outcome Status
| Status | Description |
|--------|-------------|
| `Success` | Decision led to successful resolution |
| `PartialSuccess` | Decision led to partial resolution |
| `Ineffective` | Decision was ineffective |
| `NegativeOutcome` | Decision led to negative consequences |
| `Pending` | Outcome still pending |
## API Reference
### Record a Decision
```http
POST /api/v1/opsmemory/decisions
Content-Type: application/json
{
"tenantId": "tenant-xyz",
"cveId": "CVE-2023-44487",
"componentPurl": "pkg:npm/http2@1.0.0",
"severity": "high",
"reachability": "reachable",
"epssScore": 0.97,
"action": "Remediate",
"rationale": "KEV + reachable + payment service",
"decidedBy": "alice@example.com",
"contextTags": ["production", "payment-service"]
}
```
**Response:**
```json
{
"memoryId": "abc123def456",
"recordedAt": "2026-01-07T12:00:00Z"
}
```
### Record an Outcome
```http
POST /api/v1/opsmemory/decisions/{memoryId}/outcome?tenantId=tenant-xyz
Content-Type: application/json
{
"status": "Success",
"resolutionTimeMinutes": 270,
"lessonsLearned": "Upgrade was smooth, no breaking changes",
"recordedBy": "alice@example.com"
}
```
### Get Playbook Suggestions
```http
GET /api/v1/opsmemory/suggestions?tenantId=tenant-xyz&cveId=CVE-2024-1234&severity=high&reachability=reachable
```
**Response:**
```json
{
"suggestions": [
{
"suggestedAction": "Remediate",
"confidence": 0.87,
"rationale": "87% confidence based on 15 similar past decisions. Remediation succeeded in 93% of high-severity reachable vulnerabilities.",
"successRate": 0.93,
"similarDecisionCount": 15,
"averageResolutionTimeMinutes": 180,
"evidence": [
{
"memoryId": "abc123",
"similarity": 0.92,
"action": "Remediate",
"outcome": "Success",
"cveId": "CVE-2023-44487"
}
],
"matchingFactors": [
"Same severity: high",
"Same reachability: Reachable",
"Both are KEV",
"Shared context: production"
]
}
],
"analyzedRecords": 15,
"topSimilarity": 0.92
}
```
### Query Past Decisions
```http
GET /api/v1/opsmemory/decisions?tenantId=tenant-xyz&action=Remediate&pageSize=20
```
### Get Statistics
```http
GET /api/v1/opsmemory/stats?tenantId=tenant-xyz
```
**Response:**
```json
{
"tenantId": "tenant-xyz",
"totalDecisions": 1250,
"decisionsWithOutcomes": 980,
"successRate": 0.87
}
```
## Similarity Algorithm
OpsMemory uses a 50-dimensional vector to represent each security situation:
| Dimensions | Feature |
|------------|---------|
| 0-9 | CVE category (memory, injection, auth, crypto, dos, etc.) |
| 10-14 | Severity (none, low, medium, high, critical) |
| 15-18 | Reachability (unknown, reachable, not-reachable, potential) |
| 19-23 | EPSS band (0-0.2, 0.2-0.4, 0.4-0.6, 0.6-0.8, 0.8-1.0) |
| 24-28 | CVSS band (0-2, 2-4, 4-6, 6-8, 8-10) |
| 29 | KEV flag |
| 30-39 | Component type (npm, maven, pypi, nuget, go, cargo, etc.) |
| 40-49 | Context tags (production, external-facing, payment, etc.) |
Similarity is computed using **cosine similarity** between normalized vectors.
## Integration Points
### Decision Recording Hook
OpsMemory integrates with the Findings Ledger to automatically capture decisions:
```csharp
public class OpsMemoryHook : IDecisionHook
{
public async Task OnDecisionRecordedAsync(FindingDecision decision)
{
var record = new OpsMemoryRecord
{
TenantId = decision.TenantId,
Situation = ExtractSituation(decision),
Decision = ExtractDecision(decision)
};
// Fire-and-forget to not block the decision flow
_ = _store.RecordDecisionAsync(record);
}
}
```
### Outcome Tracking
The OutcomeTrackingService monitors for resolution events and prompts users:
1. **Auto-detect resolution**: When a finding is marked resolved
2. **Calculate resolution time**: Time from decision to resolution
3. **Prompt for classification**: Ask user about outcome quality
4. **Link to original decision**: Update the OpsMemory record
## Configuration
```yaml
opsmemory:
connectionString: "Host=localhost;Database=stellaops"
similarity:
minThreshold: 0.6 # Minimum similarity for suggestions
maxResults: 10 # Maximum similar records to analyze
suggestions:
maxSuggestions: 3 # Maximum suggestions to return
minConfidence: 0.5 # Minimum confidence threshold
outcomeTracking:
autoPromptDelay: 24h # Delay before prompting for outcome
reminderInterval: 7d # Reminder interval for pending outcomes
```
## Database Schema
```sql
CREATE SCHEMA IF NOT EXISTS opsmemory;
CREATE TABLE opsmemory.decisions (
memory_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL,
-- Situation (JSONB for flexibility)
situation JSONB NOT NULL,
-- Decision (JSONB)
decision JSONB NOT NULL,
-- Outcome (nullable, updated later)
outcome JSONB,
-- Similarity vector (array for simple cosine similarity)
similarity_vector REAL[] NOT NULL
);
CREATE INDEX idx_decisions_tenant ON opsmemory.decisions(tenant_id);
CREATE INDEX idx_decisions_recorded ON opsmemory.decisions(recorded_at DESC);
CREATE INDEX idx_decisions_cve ON opsmemory.decisions((situation->>'cveId'));
```
## Best Practices
### Recording Decisions
1. **Include context tags**: The more context, the better similarity matching
2. **Document rationale**: Future users benefit from understanding why
3. **Reference policies**: Link to the policy that guided the decision
### Recording Outcomes
1. **Be timely**: Record outcomes as soon as resolution is confirmed
2. **Be honest**: Failed decisions are valuable learning data
3. **Add lessons learned**: Help future users avoid pitfalls
### Using Suggestions
1. **Review evidence**: Look at the similar past decisions
2. **Check matching factors**: Ensure the situations are truly comparable
3. **Trust but verify**: Suggestions are guidance, not mandates
## Related Modules
- [Findings Ledger](../findings-ledger/README.md) - Source of decision events
- [Timeline](../timeline-indexer/README.md) - Audit trail
- [Excititor](../excititor/README.md) - VEX statement management
- [Risk Engine](../risk-engine/README.md) - Risk scoring

View File

@@ -0,0 +1,393 @@
# OpsMemory Architecture
> **Technical deep-dive into the Decision Ledger**
## Overview
OpsMemory provides a structured approach to organizational learning from security decisions. It captures the complete lifecycle of a decision - from the situation context through the action taken to the eventual outcome.
## Design Principles
### 1. Determinism First
All operations produce deterministic, reproducible results:
- Similarity vectors are computed from stable inputs
- Confidence scores use fixed formulas
- No randomness in suggestion ranking
### 2. Multi-Tenant Isolation
Every operation is scoped to a tenant:
- Records cannot be accessed across tenants
- Similarity search is tenant-isolated
- Statistics are per-tenant
### 3. Fire-and-Forget Integration
Decision recording is async and non-blocking:
- UI decisions complete immediately
- OpsMemory recording happens in background
- Failures don't affect the primary flow
### 4. Offline Capable
All features work without network access:
- Local PostgreSQL storage
- No external API dependencies
- Self-contained similarity computation
## Component Architecture
```
┌────────────────────────────────────────────────────────────────────┐
│ WebService Layer │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ OpsMemoryEndpoints │ │
│ │ POST /decisions GET /decisions GET /suggestions GET /stats│ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬───────────────────────────────────┘
┌────────────────────────────────┼───────────────────────────────────┐
│ Service Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
│ │ PlaybookSuggest │ │ OutcomeTracking │ │ SimilarityVector │ │
│ │ Service │ │ Service │ │ Generator │ │
│ └────────┬────────┘ └────────┬────────┘ └─────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ IOpsMemoryStore │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬───────────────────────────────────┘
┌────────────────────────────────┼───────────────────────────────────┐
│ Storage Layer │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ PostgresOpsMemoryStore │ │
│ │ - Decision CRUD │ │
│ │ - Outcome updates │ │
│ │ - Similarity search (array-based cosine) │ │
│ │ - Query with pagination │ │
│ │ - Statistics aggregation │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
```
## Data Model
### OpsMemoryRecord
The core aggregate containing all decision information:
```csharp
public sealed record OpsMemoryRecord
{
public required string MemoryId { get; init; }
public required string TenantId { get; init; }
public required DateTimeOffset RecordedAt { get; init; }
public required SituationContext Situation { get; init; }
public required DecisionRecord Decision { get; init; }
public OutcomeRecord? Outcome { get; init; }
public ImmutableArray<float> SimilarityVector { get; init; }
}
```
### SituationContext
Captures the security context at decision time:
```csharp
public sealed record SituationContext
{
public string? CveId { get; init; }
public string? Component { get; init; } // PURL
public string? Severity { get; init; } // low/medium/high/critical
public ReachabilityStatus Reachability { get; init; }
public double? EpssScore { get; init; } // 0-1
public double? CvssScore { get; init; } // 0-10
public bool IsKev { get; init; }
public ImmutableArray<string> ContextTags { get; init; }
}
```
### DecisionRecord
The action taken and why:
```csharp
public sealed record DecisionRecord
{
public required DecisionAction Action { get; init; }
public required string Rationale { get; init; }
public required string DecidedBy { get; init; }
public required DateTimeOffset DecidedAt { get; init; }
public string? PolicyReference { get; init; }
public MitigationDetails? Mitigation { get; init; }
}
```
### OutcomeRecord
The result of the decision:
```csharp
public sealed record OutcomeRecord
{
public required OutcomeStatus Status { get; init; }
public TimeSpan? ResolutionTime { get; init; }
public string? ActualImpact { get; init; }
public string? LessonsLearned { get; init; }
public required string RecordedBy { get; init; }
public required DateTimeOffset RecordedAt { get; init; }
}
```
## Similarity Algorithm
### Vector Generation
The `SimilarityVectorGenerator` creates 50-dimensional feature vectors:
```
Vector Layout:
[0-9] : CVE category one-hot (memory, injection, auth, crypto, dos,
info-disclosure, privilege-escalation, xss, path-traversal, other)
[10-14] : Severity one-hot (none, low, medium, high, critical)
[15-18] : Reachability one-hot (unknown, reachable, not-reachable, potential)
[19-23] : EPSS band one-hot (0-0.2, 0.2-0.4, 0.4-0.6, 0.6-0.8, 0.8-1.0)
[24-28] : CVSS band one-hot (0-2, 2-4, 4-6, 6-8, 8-10)
[29] : KEV flag (0 or 1)
[30-39] : Component type one-hot (npm, maven, pypi, nuget, go, cargo,
deb, rpm, apk, other)
[40-49] : Context tag presence (production, development, staging,
external-facing, internal, payment, auth, data, api, frontend)
```
### Cosine Similarity
Similarity between vectors A and B:
```
similarity = (A · B) / (||A|| × ||B||)
```
Where `A · B` is the dot product and `||A||` is the L2 norm.
### CVE Classification
CVEs are classified by analyzing keywords in the CVE ID and description:
| Category | Keywords |
|----------|----------|
| memory | buffer, overflow, heap, stack, use-after-free |
| injection | sql, command, code injection, ldap |
| auth | authentication, authorization, bypass |
| crypto | cryptographic, encryption, key |
| dos | denial of service, resource exhaustion |
| info-disclosure | information disclosure, leak |
| privilege-escalation | privilege escalation, elevation |
| xss | cross-site scripting, xss |
| path-traversal | path traversal, directory traversal |
## Playbook Suggestion Algorithm
### Confidence Calculation
```csharp
confidence = baseSimilarity
× successRateBonus
× recencyBonus
× evidenceCountBonus
```
Where:
- `baseSimilarity`: Highest similarity score from matching records
- `successRateBonus`: `1 + (successRate - 0.5) * 0.5` (rewards high success rate)
- `recencyBonus`: More recent decisions weighted higher
- `evidenceCountBonus`: More evidence = higher confidence
### Suggestion Ranking
1. Group past decisions by action taken
2. For each action, calculate:
- Average similarity of records with that action
- Success rate for that action
- Number of similar decisions
3. Compute confidence score
4. Rank by confidence descending
5. Return top N suggestions
### Rationale Generation
Rationales are generated programmatically:
```
"{confidence}% confidence based on {count} similar past decisions.
{action} succeeded in {successRate}% of {factors}."
```
## Storage Design
### PostgreSQL Schema
```sql
CREATE TABLE opsmemory.decisions (
memory_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL,
-- Denormalized situation fields for indexing
cve_id TEXT,
component TEXT,
severity TEXT,
-- Full data as JSONB
situation JSONB NOT NULL,
decision JSONB NOT NULL,
outcome JSONB,
-- Similarity vector as array (not pgvector)
similarity_vector REAL[] NOT NULL
);
-- Indexes
CREATE INDEX idx_decisions_tenant ON opsmemory.decisions(tenant_id);
CREATE INDEX idx_decisions_recorded ON opsmemory.decisions(recorded_at DESC);
CREATE INDEX idx_decisions_cve ON opsmemory.decisions(cve_id) WHERE cve_id IS NOT NULL;
CREATE INDEX idx_decisions_component ON opsmemory.decisions(component) WHERE component IS NOT NULL;
```
### Why Not pgvector?
The current implementation uses PostgreSQL arrays instead of pgvector:
1. **Simpler deployment**: No extension installation required
2. **Smaller dataset**: OpsMemory is per-org, not global
3. **Adequate performance**: Array operations are fast enough for <100K records
4. **Future option**: Can migrate to pgvector if needed
### Cosine Similarity in SQL
```sql
-- Cosine similarity between query vector and stored vectors
SELECT memory_id,
(
SELECT SUM(a * b)
FROM UNNEST(similarity_vector, @query_vector) AS t(a, b)
) / (
SQRT((SELECT SUM(a * a) FROM UNNEST(similarity_vector) AS t(a))) *
SQRT((SELECT SUM(b * b) FROM UNNEST(@query_vector) AS t(b)))
) AS similarity
FROM opsmemory.decisions
WHERE tenant_id = @tenant_id
ORDER BY similarity DESC
LIMIT @top_k;
```
## API Design
### Endpoint Overview
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/v1/opsmemory/decisions` | Record a new decision |
| GET | `/api/v1/opsmemory/decisions/{id}` | Get decision details |
| POST | `/api/v1/opsmemory/decisions/{id}/outcome` | Record outcome |
| GET | `/api/v1/opsmemory/suggestions` | Get playbook suggestions |
| GET | `/api/v1/opsmemory/decisions` | Query past decisions |
| GET | `/api/v1/opsmemory/stats` | Get statistics |
### Request/Response DTOs
The API uses string-based DTOs that convert to/from internal enums:
```csharp
// API accepts strings
public record RecordDecisionRequest
{
public required string Action { get; init; } // "Remediate", "Accept", etc.
public string? Reachability { get; init; } // "reachable", "not-reachable"
}
// Internal uses enums
public enum DecisionAction { Accept, Remediate, Quarantine, ... }
public enum ReachabilityStatus { Unknown, Reachable, NotReachable, Potential }
```
## Testing Strategy
### Unit Tests (26 tests)
**SimilarityVectorGeneratorTests:**
- Vector dimension validation
- Feature encoding (severity, reachability, EPSS, CVSS, KEV)
- Component type classification
- Context tag encoding
- Vector normalization
- Cosine similarity computation
- Matching factor detection
**PlaybookSuggestionServiceTests:**
- Empty history handling
- Single record suggestions
- Multiple record ranking
- Confidence calculation
- Rationale generation
- Evidence linking
### Integration Tests (5 tests)
**PostgresOpsMemoryStoreTests:**
- Decision persistence and retrieval
- Outcome updates
- Tenant isolation
- Query filtering
- Statistics calculation
## Performance Considerations
### Indexing Strategy
- Primary key on `memory_id` for direct lookups
- Index on `tenant_id` for isolation
- Index on `recorded_at` for recent-first queries
- Partial indexes on `cve_id` and `component` for filtered queries
### Query Optimization
- Limit similarity search to last N days by default
- Return only top-K similar records
- Use cursor-based pagination for large result sets
### Caching
Currently no caching (records are infrequently accessed). Future options:
- Cache similarity vectors in memory
- Cache recent suggestions per tenant
- Use read replicas for heavy read loads
## Future Enhancements
### pgvector Migration
If dataset grows significantly:
1. Install pgvector extension
2. Add vector column with IVFFlat index
3. Replace array-based similarity with vector operations
4. ~100x speedup for large datasets
### ML-Based Suggestions
Replace rule-based confidence with ML model:
1. Train on historical decision-outcome pairs
2. Include more features (time of day, team, etc.)
3. Use gradient boosting or neural network
4. Continuous learning from new outcomes
### Outcome Prediction
Predict outcome before decision is made:
1. Use past outcomes as training data
2. Predict success probability per action
3. Show predicted outcomes in UI
4. Track prediction accuracy over time

View File

@@ -0,0 +1,551 @@
# Reachability Module Architecture
## Overview
The **Reachability** module provides a unified hybrid reachability analysis system that combines static call-graph analysis with runtime execution evidence to determine whether vulnerable code paths are actually exploitable in a given artifact. It serves as the **evidence backbone** for VEX (Vulnerability Exploitability eXchange) verdicts.
## Problem Statement
Vulnerability scanners generate excessive false positives:
- **Static analysis** over-approximates: flags code that is dead, feature-gated, or unreachable
- **Runtime analysis** under-approximates: misses rarely-executed but exploitable paths
- **No unified view** across static and runtime evidence sources
- **Symbol mismatch** between static extraction (Roslyn, ASM) and runtime observation (ETW, eBPF)
### Before Reachability Module
| Question | Answer Method | Limitation |
|----------|---------------|------------|
| Is CVE reachable statically? | Query ReachGraph | No runtime context |
| Was CVE executed at runtime? | Query Signals runtime facts | No static context |
| Should we mark CVE as NA? | Manual analysis | No evidence, no audit trail |
| What's the confidence? | Guesswork | No formal model |
### After Reachability Module
Single `IReachabilityIndex.QueryHybridAsync()` call returns:
- Lattice state (8-level certainty model)
- Confidence score (0.0-1.0)
- Evidence URIs (auditable, reproducible)
- Recommended VEX status + justification
---
## Module Location
```
src/__Libraries/StellaOps.Reachability.Core/
├── IReachabilityIndex.cs # Main facade interface
├── ReachabilityIndex.cs # Implementation
├── ReachabilityQueryOptions.cs # Query configuration
├── Models/
│ ├── SymbolRef.cs # Symbol reference
│ ├── CanonicalSymbol.cs # Canonicalized symbol
│ ├── StaticReachabilityResult.cs # Static query result
│ ├── RuntimeReachabilityResult.cs # Runtime query result
│ ├── HybridReachabilityResult.cs # Combined result
│ └── LatticeState.cs # 8-state lattice enum
├── Symbols/
│ ├── ISymbolCanonicalizer.cs # Symbol normalization interface
│ ├── SymbolCanonicalizer.cs # Implementation
│ ├── Normalizers/
│ │ ├── DotNetSymbolNormalizer.cs # .NET symbols
│ │ ├── JavaSymbolNormalizer.cs # Java symbols
│ │ ├── NativeSymbolNormalizer.cs # C/C++/Rust
│ │ └── ScriptSymbolNormalizer.cs # JS/Python/PHP
│ └── SymbolMatchOptions.cs # Matching configuration
├── CveMapping/
│ ├── ICveSymbolMappingService.cs # CVE-symbol mapping interface
│ ├── CveSymbolMappingService.cs # Implementation
│ ├── CveSymbolMapping.cs # Mapping record
│ ├── VulnerableSymbol.cs # Vulnerable symbol record
│ ├── MappingSource.cs # Source enum
│ └── Extractors/
│ ├── IPatchSymbolExtractor.cs # Patch analysis interface
│ ├── GitDiffExtractor.cs # Git diff parsing
│ ├── OsvEnricher.cs # OSV API enrichment
│ └── DeltaSigMatcher.cs # Binary signature matching
├── Lattice/
│ ├── ReachabilityLattice.cs # Lattice state machine
│ ├── LatticeTransition.cs # State transitions
│ └── ConfidenceCalculator.cs # Confidence scoring
├── Evidence/
│ ├── EvidenceUriBuilder.cs # stella:// URI construction
│ ├── EvidenceBundle.cs # Evidence collection
│ └── EvidenceAttestationService.cs # DSSE signing
└── Integration/
├── ReachGraphAdapter.cs # ReachGraph integration
├── SignalsAdapter.cs # Signals integration
└── PolicyEngineAdapter.cs # Policy Engine integration
```
---
## Core Concepts
### 1. Reachability Lattice (8-State Model)
The lattice provides mathematically sound evidence aggregation:
```
X (Contested)
/ \
/ \
CR (Confirmed CU (Confirmed
Reachable) Unreachable)
| \ / |
| \ / |
RO (Runtime RU (Runtime
Observed) Unobserved)
| |
| |
SR (Static SU (Static
Reachable) Unreachable)
\ /
\ /
U (Unknown)
```
| State | Code | Description | Confidence Base |
|-------|------|-------------|-----------------|
| Unknown | U | No analysis performed | 0.00 |
| Static Reachable | SR | Call graph shows path exists | 0.30 |
| Static Unreachable | SU | Call graph proves no path | 0.40 |
| Runtime Observed | RO | Symbol executed at runtime | 0.70 |
| Runtime Unobserved | RU | Observation window passed, no execution | 0.60 |
| Confirmed Reachable | CR | Multiple sources confirm reachability | 0.90 |
| Confirmed Unreachable | CU | Multiple sources confirm no reachability | 0.95 |
| Contested | X | Evidence conflict | 0.20 (requires review) |
### 2. Symbol Canonicalization
Symbols from different sources must be normalized to enable matching:
| Source | Raw Format | Canonical Format |
|--------|-----------|------------------|
| Roslyn (.NET) | `StellaOps.Scanner.Core.SbomGenerator::GenerateAsync` | `stellaops.scanner.core/sbomgenerator/generateasync/(cancellationtoken)` |
| ASM (Java) | `org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;` | `org.apache.log4j.core.lookup/jndilookup/lookup/(string)` |
| eBPF (Native) | `_ZN4llvm12DenseMapBaseINS_...` | `llvm/densemapbase/operator[]/(keytype)` |
| ETW (.NET) | `MethodID=12345 ModuleID=67890` | (resolved via metadata) |
### 3. CVE-Symbol Mapping
Maps CVE identifiers to specific vulnerable symbols:
```json
{
"cveId": "CVE-2021-44228",
"symbols": [
{
"canonicalId": "sha256:abc123...",
"displayName": "org.apache.log4j.core.lookup/jndilookup/lookup/(string)",
"type": "Sink",
"condition": "When lookup string contains ${jndi:...}"
}
],
"source": "PatchAnalysis",
"confidence": 0.98,
"patchCommitUrl": "https://github.com/apache/logging-log4j2/commit/abc123"
}
```
### 4. Evidence URIs
Standardized `stella://` URI scheme for evidence references:
| Pattern | Example |
|---------|---------|
| `stella://reachgraph/{digest}` | `stella://reachgraph/blake3:abc123` |
| `stella://reachgraph/{digest}/slice?symbol={id}` | `stella://reachgraph/blake3:abc123/slice?symbol=sha256:def` |
| `stella://signals/runtime/{tenant}/{artifact}` | `stella://signals/runtime/acme/sha256:abc` |
| `stella://cvemap/{cveId}` | `stella://cvemap/CVE-2021-44228` |
| `stella://attestation/{digest}` | `stella://attestation/sha256:sig789` |
---
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Reachability Core Library │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ IReachabilityIndex │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ QueryStaticAsync │ │ QueryRuntimeAsync│ │ QueryHybridAsync │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ └────────────┬───────────────┘ │ │
│ └───────────┼────────────────────┼─────────────────────────┼────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐│
│ │ Internal Components ││
│ │ ││
│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────────┐ ││
│ │ │ Symbol │ │ CVE-Symbol │ │ Reachability │ ││
│ │ │ Canonicalizer │ │ Mapping │ │ Lattice │ ││
│ │ │ │ │ │ │ │ ││
│ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌───────────────────────┐ │ ││
│ │ │ │.NET Norm. │ │ │ │PatchExtract│ │ │ │ State Machine │ │ ││
│ │ │ │Java Norm. │ │ │ │OSV Enrich │ │ │ │ Confidence Calc │ │ ││
│ │ │ │Native Norm.│ │ │ │DeltaSig │ │ │ │ Transition Rules │ │ ││
│ │ │ │Script Norm.│ │ │ │Manual Input│ │ │ └───────────────────────┘ │ ││
│ │ │ └────────────┘ │ │ └────────────┘ │ │ │ ││
│ │ └────────────────┘ └────────────────┘ └────────────────────────────┘ ││
│ │ ││
│ └──────────────────────────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐│
│ │ Evidence Layer ││
│ │ ││
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ ││
│ │ │ Evidence URI │ │ Evidence Bundle │ │ Evidence Attestation │ ││
│ │ │ Builder │ │ (Collection) │ │ Service (DSSE) │ ││
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────────────┘ ││
│ │ ││
│ └──────────────────────────────────────────────────────────────────────────────┘│
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ ReachGraph │ │ Signals │ │ Policy Engine │
│ Adapter │ │ Adapter │ │ Adapter │
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ ReachGraph │ │ Signals │ │ Policy Engine │
│ WebService │ │ WebService │ │ (VEX Emit) │
└────────────────┘ └────────────────┘ └────────────────┘
```
---
## Data Flow
### Query Flow
```
1. Consumer calls IReachabilityIndex.QueryHybridAsync(symbol, artifact, options)
2. SymbolCanonicalizer normalizes input symbol to CanonicalSymbol
3. Parallel queries:
├── ReachGraphAdapter.QueryAsync() → StaticReachabilityResult
└── SignalsAdapter.QueryRuntimeFactsAsync() → RuntimeReachabilityResult
4. ReachabilityLattice computes combined state from evidence
5. ConfidenceCalculator applies evidence weights and guardrails
6. EvidenceBundle collects URIs for audit trail
7. Return HybridReachabilityResult with verdict recommendation
```
### Ingestion Flow (CVE Mapping)
```
1. Patch commit detected (Concelier, Feedser, or manual)
2. GitDiffExtractor parses diff to find changed functions
3. SymbolCanonicalizer normalizes extracted symbols
4. OsvEnricher adds context from OSV database
5. CveSymbolMappingService persists mapping with provenance
6. Mapping available for reachability queries
```
---
## API Contracts
### IReachabilityIndex
```csharp
public interface IReachabilityIndex
{
/// <summary>
/// Query static reachability from call graph.
/// </summary>
Task<StaticReachabilityResult> QueryStaticAsync(
SymbolRef symbol,
string artifactDigest,
CancellationToken ct);
/// <summary>
/// Query runtime reachability from observed facts.
/// </summary>
Task<RuntimeReachabilityResult> QueryRuntimeAsync(
SymbolRef symbol,
string artifactDigest,
TimeSpan observationWindow,
CancellationToken ct);
/// <summary>
/// Query hybrid reachability combining static + runtime.
/// </summary>
Task<HybridReachabilityResult> QueryHybridAsync(
SymbolRef symbol,
string artifactDigest,
HybridQueryOptions options,
CancellationToken ct);
/// <summary>
/// Batch query for CVE vulnerability analysis.
/// </summary>
Task<IReadOnlyList<HybridReachabilityResult>> QueryBatchAsync(
IEnumerable<SymbolRef> symbols,
string artifactDigest,
HybridQueryOptions options,
CancellationToken ct);
/// <summary>
/// Get vulnerable symbols for a CVE.
/// </summary>
Task<CveSymbolMapping?> GetCveMappingAsync(
string cveId,
CancellationToken ct);
}
```
### Result Types
```csharp
public sealed record HybridReachabilityResult
{
public required SymbolRef Symbol { get; init; }
public required string ArtifactDigest { get; init; }
public required LatticeState LatticeState { get; init; }
public required double Confidence { get; init; }
public required StaticEvidence? StaticEvidence { get; init; }
public required RuntimeEvidence? RuntimeEvidence { get; init; }
public required VerdictRecommendation Verdict { get; init; }
public required ImmutableArray<string> EvidenceUris { get; init; }
public required DateTimeOffset ComputedAt { get; init; }
public required string ComputedBy { get; init; }
}
public sealed record VerdictRecommendation
{
public required VexStatus Status { get; init; }
public VexJustification? Justification { get; init; }
public required ConfidenceBucket ConfidenceBucket { get; init; }
public string? ImpactStatement { get; init; }
public string? ActionStatement { get; init; }
}
public enum LatticeState
{
Unknown = 0,
StaticReachable = 1,
StaticUnreachable = 2,
RuntimeObserved = 3,
RuntimeUnobserved = 4,
ConfirmedReachable = 5,
ConfirmedUnreachable = 6,
Contested = 7
}
```
---
## Integration Points
### Upstream (Data Sources)
| Module | Interface | Data |
|--------|-----------|------|
| ReachGraph | `IReachGraphSliceService` | Static call-graph nodes/edges |
| Signals | `IRuntimeFactsService` | Runtime method observations |
| Scanner.CallGraph | `ICallGraphExtractor` | Per-artifact call graphs |
| Feedser | `IBackportProofService` | Patch analysis results |
### Downstream (Consumers)
| Module | Interface | Usage |
|--------|-----------|-------|
| Policy Engine | `IReachabilityAwareVexEmitter` | VEX verdict with evidence |
| VexLens | `IReachabilityIndex` | Consensus enrichment |
| Web Console | REST API | Evidence panel display |
| CLI | `stella reachability` | Command-line queries |
| ExportCenter | `IReachabilityExporter` | Offline bundles |
---
## Storage
### PostgreSQL Schema
```sql
-- CVE-Symbol Mappings
CREATE TABLE reachability.cve_symbol_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
cve_id TEXT NOT NULL,
symbol_canonical_id TEXT NOT NULL,
symbol_display_name TEXT NOT NULL,
vulnerability_type TEXT NOT NULL,
condition TEXT,
source TEXT NOT NULL,
confidence DECIMAL(3,2) NOT NULL,
patch_commit_url TEXT,
delta_sig_digest TEXT,
extracted_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, cve_id, symbol_canonical_id)
);
-- Query Cache
CREATE TABLE reachability.query_cache (
cache_key TEXT PRIMARY KEY,
artifact_digest TEXT NOT NULL,
symbol_canonical_id TEXT NOT NULL,
lattice_state INTEGER NOT NULL,
confidence DECIMAL(3,2) NOT NULL,
result_json JSONB NOT NULL,
computed_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
-- Audit Log
CREATE TABLE reachability.query_audit_log (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
query_type TEXT NOT NULL,
artifact_digest TEXT NOT NULL,
symbol_count INTEGER NOT NULL,
lattice_state INTEGER NOT NULL,
confidence DECIMAL(3,2) NOT NULL,
duration_ms INTEGER NOT NULL,
queried_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```
### Valkey (Redis) Caching
| Key Pattern | TTL | Purpose |
|-------------|-----|---------|
| `reach:static:{artifact}:{symbol}` | 1h | Static query cache |
| `reach:runtime:{artifact}:{symbol}` | 5m | Runtime query cache |
| `reach:hybrid:{artifact}:{symbol}:{options_hash}` | 15m | Hybrid query cache |
| `cvemap:{cve_id}` | 24h | CVE mapping cache |
---
## Determinism Guarantees
### Reproducibility Rules
1. **Canonical Symbol IDs:** SHA-256 of `purl|namespace|type|method|signature` (lowercase, sorted)
2. **Stable Lattice Transitions:** Deterministic state machine, no randomness
3. **Ordered Evidence:** Evidence URIs sorted lexicographically
4. **Time Injection:** All `ComputedAt` via `TimeProvider`
5. **Culture Invariance:** `InvariantCulture` for all string operations
### Replay Verification
```csharp
public interface IReachabilityReplayService
{
Task<ReplayResult> ReplayAsync(
HybridReachabilityInputs inputs,
HybridReachabilityResult expected,
CancellationToken ct);
}
```
---
## Performance Characteristics
| Operation | Target P95 | Notes |
|-----------|-----------|-------|
| Static query (cached) | <10ms | Valkey hit |
| Static query (uncached) | <100ms | ReachGraph slice |
| Runtime query (cached) | <5ms | Valkey hit |
| Runtime query (uncached) | <50ms | Signals lookup |
| Hybrid query | <50ms | Parallel static + runtime |
| Batch query (100 symbols) | <500ms | Parallelized |
| CVE mapping lookup | <10ms | Cached |
| Symbol canonicalization | <1ms | In-memory |
---
## Security Considerations
### Access Control
| Operation | Required Scope |
|-----------|---------------|
| Query reachability | `reachability:read` |
| Ingest CVE mapping | `reachability:write` |
| Admin CVE mapping | `reachability:admin` |
| Export bundles | `reachability:export` |
### Tenant Isolation
- All queries filtered by `tenant_id`
- RLS policies on all tables
- Cache keys include tenant prefix
### Data Sensitivity
- Symbol names may reveal internal architecture
- Runtime traces expose execution patterns
- CVE mappings are security-sensitive
---
## Observability
### Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `reachability_query_duration_seconds` | histogram | query_type, cache_hit |
| `reachability_lattice_state_total` | counter | state |
| `reachability_cache_hit_ratio` | gauge | cache_type |
| `reachability_cvemap_count` | gauge | source |
### Traces
| Span | Description |
|------|-------------|
| `reachability.query.static` | Static graph query |
| `reachability.query.runtime` | Runtime facts query |
| `reachability.query.hybrid` | Combined computation |
| `reachability.canonicalize` | Symbol normalization |
| `reachability.lattice.compute` | State calculation |
---
## Related Documentation
- [Product Advisory: Hybrid Reachability](../../product/advisories/09-Jan-2026%20-%20Hybrid%20Reachability%20and%20VEX%20Integration%20(Revised).md)
- [ReachGraph Architecture](../reach-graph/architecture.md)
- [Signals Architecture](../signals/architecture.md)
- [VexLens Architecture](../vex-lens/architecture.md)
- [Sprint Index](../../implplan/SPRINT_20260109_009_000_INDEX_hybrid_reachability.md)
---
_Last updated: 09-Jan-2026_

View File

@@ -0,0 +1,579 @@
# SARIF Export Module Architecture
## Overview
The **SARIF Export** module provides SARIF 2.1.0 compliant output for StellaOps Scanner findings, enabling integration with GitHub Code Scanning, GitLab SAST, Azure DevOps, and other platforms that consume SARIF.
## Current State
| Component | Status | Location |
|-----------|--------|----------|
| SARIF 2.1.0 Models | **Implemented** | `Scanner.Sarif/Models/SarifModels.cs` |
| SmartDiff SARIF Generator | **Implemented** | `Scanner.SmartDiff/Output/SarifOutputGenerator.cs` |
| SmartDiff SARIF Endpoint | **Implemented** | `GET /smart-diff/scans/{scanId}/sarif` |
| Findings SARIF Mapper | **Implemented** | `Scanner.Sarif/SarifExportService.cs` |
| SARIF Rule Registry | **Implemented** | `Scanner.Sarif/Rules/SarifRuleRegistry.cs` |
| Fingerprint Generator | **Implemented** | `Scanner.Sarif/Fingerprints/FingerprintGenerator.cs` |
| GitHub Upload Client | **Not Implemented** | Proposed |
---
## Module Location
```
src/Scanner/__Libraries/StellaOps.Scanner.Sarif/
├── ISarifExportService.cs # Main export interface
├── SarifExportService.cs # Implementation (DONE)
├── SarifExportOptions.cs # Configuration (DONE)
├── FindingInput.cs # Input model (DONE)
├── Models/
│ └── SarifModels.cs # Complete SARIF 2.1.0 types (DONE)
├── Rules/
│ ├── ISarifRuleRegistry.cs # Rule registry interface (DONE)
│ └── SarifRuleRegistry.cs # 21 rules implemented (DONE)
└── Fingerprints/
├── IFingerprintGenerator.cs # Fingerprint interface (DONE)
└── FingerprintGenerator.cs # SHA-256 fingerprints (DONE)
```
---
## Existing SmartDiff SARIF Implementation
The SmartDiff module provides a reference implementation:
### SarifModels.cs (Existing)
```csharp
// Already implemented record types
public sealed record SarifLog(
string Version,
string Schema,
ImmutableArray<SarifRun> Runs);
public sealed record SarifRun(
SarifTool Tool,
ImmutableArray<SarifResult> Results,
ImmutableArray<SarifArtifact> Artifacts,
ImmutableArray<SarifVersionControlDetails> VersionControlProvenance,
ImmutableDictionary<string, object> Properties);
public sealed record SarifResult(
string RuleId,
int? RuleIndex,
SarifLevel Level,
SarifMessage Message,
ImmutableArray<SarifLocation> Locations,
ImmutableDictionary<string, string> Fingerprints,
ImmutableDictionary<string, string> PartialFingerprints,
ImmutableDictionary<string, object> Properties);
```
### SarifOutputGenerator.cs (Existing)
```csharp
// Existing generator for SmartDiff findings
public class SarifOutputGenerator
{
public SarifLog Generate(
IEnumerable<MaterialRiskChangeResult> changes,
SarifOutputOptions options);
}
```
---
## New Findings SARIF Architecture
### ISarifExportService
```csharp
namespace StellaOps.Scanner.Sarif;
/// <summary>
/// Service for exporting scanner findings to SARIF format.
/// </summary>
public interface ISarifExportService
{
/// <summary>
/// Export findings to SARIF 2.1.0 format.
/// </summary>
/// <param name="findings">Scanner findings to export.</param>
/// <param name="options">Export options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>SARIF log document.</returns>
Task<SarifLog> ExportAsync(
IEnumerable<Finding> findings,
SarifExportOptions options,
CancellationToken ct);
/// <summary>
/// Export findings to SARIF JSON string.
/// </summary>
Task<string> ExportToJsonAsync(
IEnumerable<Finding> findings,
SarifExportOptions options,
CancellationToken ct);
/// <summary>
/// Export findings to SARIF JSON stream.
/// </summary>
Task ExportToStreamAsync(
IEnumerable<Finding> findings,
SarifExportOptions options,
Stream outputStream,
CancellationToken ct);
/// <summary>
/// Validate SARIF output against schema.
/// </summary>
Task<SarifValidationResult> ValidateAsync(
SarifLog log,
CancellationToken ct);
}
```
### SarifExportOptions
```csharp
namespace StellaOps.Scanner.Sarif;
/// <summary>
/// Options for SARIF export.
/// </summary>
public sealed record SarifExportOptions
{
/// <summary>Tool name in SARIF output.</summary>
public string ToolName { get; init; } = "StellaOps Scanner";
/// <summary>Tool version.</summary>
public required string ToolVersion { get; init; }
/// <summary>Tool information URI.</summary>
public string ToolUri { get; init; } = "https://stellaops.io/scanner";
/// <summary>Minimum severity to include.</summary>
public Severity? MinimumSeverity { get; init; }
/// <summary>Include reachability evidence in properties.</summary>
public bool IncludeReachability { get; init; } = true;
/// <summary>Include VEX status in properties.</summary>
public bool IncludeVexStatus { get; init; } = true;
/// <summary>Include EPSS scores in properties.</summary>
public bool IncludeEpss { get; init; } = true;
/// <summary>Include KEV status in properties.</summary>
public bool IncludeKev { get; init; } = true;
/// <summary>Include evidence URIs in properties.</summary>
public bool IncludeEvidenceUris { get; init; } = false;
/// <summary>Include attestation reference in run properties.</summary>
public bool IncludeAttestation { get; init; } = true;
/// <summary>Version control provenance.</summary>
public VersionControlInfo? VersionControl { get; init; }
/// <summary>Pretty-print JSON output.</summary>
public bool IndentedJson { get; init; } = false;
/// <summary>Category for GitHub upload (distinguishes multiple tools).</summary>
public string? Category { get; init; }
/// <summary>Base URI for source files.</summary>
public string? SourceRoot { get; init; }
}
public sealed record VersionControlInfo
{
public required string RepositoryUri { get; init; }
public required string RevisionId { get; init; }
public string? Branch { get; init; }
}
```
---
## Rule Registry
### ISarifRuleRegistry
```csharp
namespace StellaOps.Scanner.Sarif.Rules;
/// <summary>
/// Registry of SARIF rules for StellaOps findings.
/// </summary>
public interface ISarifRuleRegistry
{
/// <summary>Get rule by ID.</summary>
SarifRule? GetRule(string ruleId);
/// <summary>Get rule for finding type and severity.</summary>
SarifRule GetRuleForFinding(FindingType type, Severity severity);
/// <summary>Get all registered rules.</summary>
IReadOnlyList<SarifRule> GetAllRules();
/// <summary>Get rules by category.</summary>
IReadOnlyList<SarifRule> GetRulesByCategory(string category);
}
```
### Rule Definitions
```csharp
namespace StellaOps.Scanner.Sarif.Rules;
public static class VulnerabilityRules
{
public static readonly SarifRule Critical = new()
{
Id = "STELLA-VULN-001",
Name = "CriticalVulnerability",
ShortDescription = "Critical vulnerability detected (CVSS >= 9.0)",
FullDescription = "A critical severity vulnerability was detected. " +
"This may be a known exploited vulnerability (KEV) or " +
"have a CVSS score of 9.0 or higher.",
HelpUri = "https://stellaops.io/rules/STELLA-VULN-001",
DefaultLevel = SarifLevel.Error,
Properties = new Dictionary<string, object>
{
["precision"] = "high",
["problem.severity"] = "error",
["security-severity"] = "10.0",
["tags"] = new[] { "security", "vulnerability", "critical" }
}.ToImmutableDictionary()
};
public static readonly SarifRule High = new()
{
Id = "STELLA-VULN-002",
Name = "HighVulnerability",
ShortDescription = "High severity vulnerability detected (CVSS 7.0-8.9)",
FullDescription = "A high severity vulnerability was detected with " +
"CVSS score between 7.0 and 8.9.",
HelpUri = "https://stellaops.io/rules/STELLA-VULN-002",
DefaultLevel = SarifLevel.Error,
Properties = new Dictionary<string, object>
{
["precision"] = "high",
["problem.severity"] = "error",
["security-severity"] = "8.0",
["tags"] = new[] { "security", "vulnerability", "high" }
}.ToImmutableDictionary()
};
public static readonly SarifRule Medium = new()
{
Id = "STELLA-VULN-003",
Name = "MediumVulnerability",
ShortDescription = "Medium severity vulnerability detected (CVSS 4.0-6.9)",
HelpUri = "https://stellaops.io/rules/STELLA-VULN-003",
DefaultLevel = SarifLevel.Warning,
Properties = new Dictionary<string, object>
{
["precision"] = "high",
["problem.severity"] = "warning",
["security-severity"] = "5.5",
["tags"] = new[] { "security", "vulnerability", "medium" }
}.ToImmutableDictionary()
};
public static readonly SarifRule Low = new()
{
Id = "STELLA-VULN-004",
Name = "LowVulnerability",
ShortDescription = "Low severity vulnerability detected (CVSS < 4.0)",
HelpUri = "https://stellaops.io/rules/STELLA-VULN-004",
DefaultLevel = SarifLevel.Note,
Properties = new Dictionary<string, object>
{
["precision"] = "high",
["problem.severity"] = "note",
["security-severity"] = "2.0",
["tags"] = new[] { "security", "vulnerability", "low" }
}.ToImmutableDictionary()
};
// Reachability-enhanced rules
public static readonly SarifRule RuntimeReachable = new()
{
Id = "STELLA-VULN-005",
Name = "ReachableVulnerability",
ShortDescription = "Runtime-confirmed reachable vulnerability",
FullDescription = "A vulnerability with runtime-confirmed reachability. " +
"The vulnerable code path was observed during execution.",
HelpUri = "https://stellaops.io/rules/STELLA-VULN-005",
DefaultLevel = SarifLevel.Error,
Properties = new Dictionary<string, object>
{
["precision"] = "very-high",
["problem.severity"] = "error",
["security-severity"] = "9.5",
["tags"] = new[] { "security", "vulnerability", "reachable", "runtime" }
}.ToImmutableDictionary()
};
}
```
---
## Fingerprint Generation
### IFingerprintGenerator
```csharp
namespace StellaOps.Scanner.Sarif.Fingerprints;
/// <summary>
/// Generates deterministic fingerprints for SARIF deduplication.
/// </summary>
public interface IFingerprintGenerator
{
/// <summary>
/// Generate primary fingerprint for a finding.
/// </summary>
string GeneratePrimary(Finding finding, FingerprintStrategy strategy);
/// <summary>
/// Generate partial fingerprints for GitHub fallback.
/// </summary>
ImmutableDictionary<string, string> GeneratePartial(
Finding finding,
string? sourceContent);
}
public enum FingerprintStrategy
{
/// <summary>Hash of ruleId + purl + vulnId + artifactDigest.</summary>
Standard,
/// <summary>Hash including file location for source-level findings.</summary>
WithLocation,
/// <summary>Hash including content hash for maximum stability.</summary>
ContentBased
}
```
### Implementation
```csharp
public class FingerprintGenerator : IFingerprintGenerator
{
public string GeneratePrimary(Finding finding, FingerprintStrategy strategy)
{
var input = strategy switch
{
FingerprintStrategy.Standard => string.Join("|",
finding.RuleId,
finding.ComponentPurl,
finding.VulnerabilityId ?? "",
finding.ArtifactDigest),
FingerprintStrategy.WithLocation => string.Join("|",
finding.RuleId,
finding.ComponentPurl,
finding.VulnerabilityId ?? "",
finding.ArtifactDigest,
finding.FilePath ?? "",
finding.LineNumber?.ToString(CultureInfo.InvariantCulture) ?? ""),
FingerprintStrategy.ContentBased => string.Join("|",
finding.RuleId,
finding.ComponentPurl,
finding.VulnerabilityId ?? "",
finding.ContentHash ?? finding.ArtifactDigest),
_ => throw new ArgumentOutOfRangeException(nameof(strategy))
};
return ComputeSha256(input);
}
public ImmutableDictionary<string, string> GeneratePartial(
Finding finding,
string? sourceContent)
{
var partial = new Dictionary<string, string>();
// Line hash for GitHub deduplication
if (!string.IsNullOrEmpty(sourceContent) && finding.LineNumber.HasValue)
{
var lines = sourceContent.Split('\n');
if (finding.LineNumber.Value <= lines.Length)
{
var line = lines[finding.LineNumber.Value - 1];
partial["primaryLocationLineHash"] = ComputeSha256(line.Trim());
}
}
return partial.ToImmutableDictionary();
}
private static string ComputeSha256(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
```
---
## Severity Mapping
```csharp
public static class SeverityMapper
{
public static SarifLevel MapToSarifLevel(Severity severity, bool isReachable = false)
{
// Reachable vulnerabilities are always error level
if (isReachable && severity >= Severity.Medium)
return SarifLevel.Error;
return severity switch
{
Severity.Critical => SarifLevel.Error,
Severity.High => SarifLevel.Error,
Severity.Medium => SarifLevel.Warning,
Severity.Low => SarifLevel.Note,
Severity.Info => SarifLevel.Note,
_ => SarifLevel.None
};
}
public static double MapToSecuritySeverity(double cvssScore)
{
// GitHub uses security-severity for ordering
// Map CVSS 0-10 scale directly
return Math.Clamp(cvssScore, 0.0, 10.0);
}
}
```
---
## Determinism Requirements
Following CLAUDE.md rules:
1. **Canonical JSON:** RFC 8785 sorted keys, no nulls
2. **Stable Rule Ordering:** Rules sorted by ID
3. **Stable Result Ordering:** Results sorted by (ruleId, location, fingerprint)
4. **Time Injection:** Use `TimeProvider` for timestamps
5. **Culture Invariance:** `InvariantCulture` for all string operations
6. **Immutable Collections:** All outputs use `ImmutableArray`, `ImmutableDictionary`
---
## API Endpoints
### Scanner Export Endpoints
```csharp
public static class SarifExportEndpoints
{
public static void MapSarifEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/v1/scans/{scanId}/exports")
.RequireAuthorization("scanner:read");
// SARIF export
group.MapGet("/sarif", ExportSarif)
.WithName("ExportScanSarif")
.Produces<string>(StatusCodes.Status200OK, "application/sarif+json");
// SARIF with options
group.MapPost("/sarif", ExportSarifWithOptions)
.WithName("ExportScanSarifWithOptions")
.Produces<string>(StatusCodes.Status200OK, "application/sarif+json");
}
private static async Task<IResult> ExportSarif(
Guid scanId,
[FromQuery] string? minSeverity,
[FromQuery] bool pretty = false,
[FromQuery] bool includeReachability = true,
ISarifExportService sarifService,
IFindingsService findingsService,
CancellationToken ct)
{
var findings = await findingsService.GetByScanIdAsync(scanId, ct);
var options = new SarifExportOptions
{
ToolVersion = GetToolVersion(),
MinimumSeverity = ParseSeverity(minSeverity),
IncludeReachability = includeReachability,
IndentedJson = pretty
};
var json = await sarifService.ExportToJsonAsync(findings, options, ct);
return Results.Content(json, "application/sarif+json");
}
}
```
---
## Integration with GitHub
See `src/Integrations/__Plugins/StellaOps.Integrations.Plugin.GitHubApp/` for GitHub connector.
New GitHub Code Scanning client extends existing infrastructure:
```csharp
public interface IGitHubCodeScanningClient
{
/// <summary>Upload SARIF to GitHub Code Scanning.</summary>
Task<SarifUploadResult> UploadSarifAsync(
string owner,
string repo,
SarifUploadRequest request,
CancellationToken ct);
/// <summary>Get upload status.</summary>
Task<SarifUploadStatus> GetUploadStatusAsync(
string owner,
string repo,
string sarifId,
CancellationToken ct);
/// <summary>List code scanning alerts.</summary>
Task<IReadOnlyList<CodeScanningAlert>> ListAlertsAsync(
string owner,
string repo,
AlertFilter? filter,
CancellationToken ct);
}
```
---
## Performance Targets
| Operation | Target P95 | Notes |
|-----------|-----------|-------|
| Export 100 findings | < 100ms | In-memory |
| Export 10,000 findings | < 5s | Streaming |
| SARIF serialization | < 50ms/MB | RFC 8785 |
| Schema validation | < 200ms | JSON Schema |
| Fingerprint generation | < 1ms/finding | SHA-256 |
---
## Related Documentation
- [Product Advisory](../../product/advisories/09-Jan-2026%20-%20GitHub%20Code%20Scanning%20Integration%20(Revised).md)
- [SARIF 2.1.0 Specification](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
- [GitHub SARIF Support](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning)
- [Existing SmartDiff SARIF](../../../src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/Output/)
---
_Last updated: 09-Jan-2026_

View File

@@ -0,0 +1,287 @@
# Evidence Panel Component
> **Sprint:** SPRINT_20260107_006_001_FE
> **Module:** Triage UI
> **Version:** 1.0.0
## Overview
The Evidence Panel provides a unified tabbed interface for viewing all evidence related to a security finding. It consolidates five categories of evidence:
1. **Provenance** - DSSE attestation chain, signer identity, Rekor transparency
2. **Reachability** - Code path analysis showing if vulnerability is reachable
3. **Diff** - Source code changes introducing the vulnerability
4. **Runtime** - Runtime telemetry and execution evidence
5. **Policy** - OPA/Rego policy decisions and lattice trace
## Architecture
```
┌──────────────────────────────────────────────────────────────────────┐
│ TabbedEvidencePanelComponent │
├──────────────────────────────────────────────────────────────────────┤
│ [Provenance] [Reachability] [Diff] [Runtime] [Policy] │
├──────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Tab Content (lazy-loaded) │ │
│ │ │ │
│ │ ProvenanceTabComponent / ReachabilityTabComponent / etc. │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
## Components
### TabbedEvidencePanelComponent
**Selector:** `app-tabbed-evidence-panel`
**Inputs:**
- `findingId: string` - The finding ID to load evidence for
**Outputs:**
- `tabChange: EventEmitter<EvidenceTabType>` - Emitted when tab changes
**Usage:**
```html
<app-tabbed-evidence-panel
[findingId]="selectedFindingId"
(tabChange)="onTabChange($event)"
/>
```
### ProvenanceTabComponent
Displays DSSE attestation information including:
- DSSE verification badge (verified/partial/missing)
- Attestation chain visualization (build → scan → triage → policy)
- Signer identity and key information
- Rekor log index with verification link
- Collapsible in-toto statement JSON
### DsseBadgeComponent
**Selector:** `app-dsse-badge`
Displays the DSSE verification status as a badge.
**Inputs:**
- `status: DsseBadgeStatus` - 'verified' | 'partial' | 'missing'
- `details?: DsseVerificationDetails` - Additional verification details
- `showTooltip?: boolean` - Show tooltip on hover (default: true)
- `animate?: boolean` - Enable hover animations (default: true)
**States:**
| State | Color | Icon | Meaning |
|-------|-------|------|---------|
| verified | Green | ✓ | Full DSSE chain verified |
| partial | Amber | ⚠ | Some attestations missing |
| missing | Red | ✗ | No valid attestation |
### AttestationChainComponent
**Selector:** `app-attestation-chain`
Visualizes the attestation chain as connected nodes.
**Inputs:**
- `nodes: AttestationChainNode[]` - Chain nodes to display
**Outputs:**
- `nodeClick: EventEmitter<AttestationChainNode>` - Emitted on node click
### PolicyTabComponent
Displays policy evaluation details including:
- Verdict badge (ALLOW/DENY/QUARANTINE/REVIEW)
- OPA/Rego rule path that matched
- K4 lattice merge trace visualization
- Counterfactual analysis ("What would change verdict?")
- Policy version and editor link
### ReachabilityTabComponent
Integrates the existing `ReachabilityContextComponent` with:
- Summary header with status badge
- Confidence percentage display
- Entry points list
- Link to full graph view
## Services
### EvidenceTabService
**Path:** `services/evidence-tab.service.ts`
Fetches evidence data for each tab with caching.
```typescript
interface EvidenceTabService {
getProvenanceEvidence(findingId: string, forceRefresh?: boolean): Observable<LoadState<ProvenanceEvidence>>;
getReachabilityEvidence(findingId: string, forceRefresh?: boolean): Observable<LoadState<ReachabilityData>>;
getDiffEvidence(findingId: string, forceRefresh?: boolean): Observable<LoadState<DiffEvidence>>;
getRuntimeEvidence(findingId: string, forceRefresh?: boolean): Observable<LoadState<RuntimeEvidence>>;
getPolicyEvidence(findingId: string, forceRefresh?: boolean): Observable<LoadState<PolicyEvidence>>;
clearCache(findingId?: string): void;
}
```
### TabUrlPersistenceService
**Path:** `services/tab-url-persistence.service.ts`
Manages URL query param persistence for selected tab.
```typescript
interface TabUrlPersistenceService {
readonly selectedTab$: Observable<EvidenceTabType>;
getCurrentTab(): EvidenceTabType;
setTab(tab: EvidenceTabType): void;
navigateToTab(tab: EvidenceTabType): void;
}
```
## Keyboard Shortcuts
| Key | Action |
|-----|--------|
| `1` | Go to Provenance tab |
| `2` | Go to Reachability tab |
| `3` | Go to Diff tab |
| `4` | Go to Runtime tab |
| `5` | Go to Policy tab |
| `→` | Next tab |
| `←` | Previous tab |
| `Home` | First tab |
| `End` | Last tab |
## URL Persistence
The selected tab is persisted in the URL query string:
```
/triage/findings/CVE-2024-1234?tab=provenance
/triage/findings/CVE-2024-1234?tab=reachability
/triage/findings/CVE-2024-1234?tab=diff
/triage/findings/CVE-2024-1234?tab=runtime
/triage/findings/CVE-2024-1234?tab=policy
```
This enables:
- Deep linking to specific evidence
- Browser history navigation
- Sharing links with colleagues
## Data Models
### ProvenanceEvidence
```typescript
interface ProvenanceEvidence {
dsseStatus: DsseBadgeStatus;
dsseDetails?: DsseVerificationDetails;
attestationChain: AttestationChainNode[];
signer?: SignerInfo;
rekorLogIndex?: number;
rekorVerifyUrl?: string;
inTotoStatement?: object;
}
```
### AttestationChainNode
```typescript
interface AttestationChainNode {
id: string;
type: 'build' | 'scan' | 'triage' | 'policy' | 'custom';
label: string;
status: 'verified' | 'pending' | 'missing' | 'failed';
predicateType?: string;
digest?: string;
timestamp?: string;
signer?: string;
details?: AttestationDetails;
}
```
### PolicyEvidence
```typescript
interface PolicyEvidence {
verdict: PolicyVerdict;
rulePath?: string;
latticeTrace?: LatticeTraceStep[];
counterfactuals?: PolicyCounterfactual[];
policyVersion?: string;
policyDigest?: string;
policyEditorUrl?: string;
evaluatedAt?: string;
}
```
## Accessibility
The Evidence Panel follows WAI-ARIA tabs pattern:
- `role="tablist"` on tab navigation
- `role="tab"` on each tab button
- `role="tabpanel"` on each panel
- `aria-selected` indicates active tab
- `aria-controls` links tabs to panels
- `aria-labelledby` links panels to tabs
- `tabindex` management for keyboard navigation
- Screen reader announcements on tab change
## Testing
### Unit Tests
Located in `evidence-panel/*.spec.ts`:
- Tab navigation behavior
- DSSE badge states and styling
- Attestation chain rendering
- Keyboard navigation
- URL persistence
- Loading/error states
### E2E Tests
Located in `e2e/evidence-panel.e2e.spec.ts`:
- Full tab switching workflow
- Evidence loading and display
- Copy JSON functionality
- URL persistence across reloads
- Accessibility compliance
## API Dependencies
The Evidence Panel depends on these API endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/evidence/provenance/{findingId}` | GET | Fetch provenance data |
| `/api/evidence/reachability/{findingId}` | GET | Fetch reachability data |
| `/api/evidence/diff/{findingId}` | GET | Fetch diff data |
| `/api/evidence/runtime/{findingId}` | GET | Fetch runtime data |
| `/api/evidence/policy/{findingId}` | GET | Fetch policy data |
See [Evidence API Reference](../../../api/evidence-api.md) for details.
## Screenshots
### Provenance Tab
![Provenance Tab](../../assets/screenshots/evidence-provenance.png)
### Policy Tab with Lattice Trace
![Policy Tab](../../assets/screenshots/evidence-policy.png)
### Attestation Chain Expanded
![Attestation Chain](../../assets/screenshots/attestation-chain.png)
## Changelog
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2026-01-09 | Initial implementation |