save progress
This commit is contained in:
367
docs/modules/advisory-ai/chat-interface.md
Normal file
367
docs/modules/advisory-ai/chat-interface.md
Normal 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)
|
||||
|
||||
327
docs/modules/opsmemory/README.md
Normal file
327
docs/modules/opsmemory/README.md
Normal 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
|
||||
393
docs/modules/opsmemory/architecture.md
Normal file
393
docs/modules/opsmemory/architecture.md
Normal 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
|
||||
551
docs/modules/reachability/architecture.md
Normal file
551
docs/modules/reachability/architecture.md
Normal 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_
|
||||
579
docs/modules/sarif-export/architecture.md
Normal file
579
docs/modules/sarif-export/architecture.md
Normal 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_
|
||||
287
docs/modules/triage/evidence-panel.md
Normal file
287
docs/modules/triage/evidence-panel.md
Normal 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
|
||||

|
||||
|
||||
### Policy Tab with Lattice Trace
|
||||

|
||||
|
||||
### Attestation Chain Expanded
|
||||

|
||||
|
||||
## Changelog
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0.0 | 2026-01-09 | Initial implementation |
|
||||
Reference in New Issue
Block a user