save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,278 @@
# Federation Bundle Export
Per SPRINT_8200_0014_0002.
## Overview
Federation bundles enable multi-site synchronization of canonical advisory data. Each bundle contains a delta of changes since a specified cursor position, allowing incremental sync between federated Concelier instances.
## Bundle Format
Bundles use a TAR archive compressed with ZStandard (ZST):
```
feedser-bundle-v1.zst
├── MANIFEST.json # Bundle metadata
├── canonicals.ndjson # Canonical advisories (one per line)
├── edges.ndjson # Source edges (one per line)
├── deletions.ndjson # Withdrawn/deleted canonical IDs
└── SIGNATURE.json # DSSE envelope (optional)
```
### MANIFEST.json
```json
{
"version": "feedser-bundle/1.0",
"site_id": "site-us-west-1",
"export_cursor": "2025-01-15T10:30:00.000Z#0042",
"since_cursor": "2025-01-14T00:00:00.000Z#0000",
"exported_at": "2025-01-15T10:30:15.123Z",
"counts": {
"canonicals": 1234,
"edges": 3456,
"deletions": 12,
"total": 4702
},
"bundle_hash": "sha256:a1b2c3d4..."
}
```
| Field | Type | Description |
|-------|------|-------------|
| `version` | string | Bundle format version identifier |
| `site_id` | string | Identifier of the exporting site |
| `export_cursor` | string | Cursor position after this export |
| `since_cursor` | string? | Cursor position from which changes were exported (null for full export) |
| `exported_at` | ISO8601 | Timestamp when bundle was created |
| `counts` | object | Item counts by type |
| `bundle_hash` | string | SHA256 hash of compressed bundle content |
### canonicals.ndjson
Each line contains a canonical advisory record:
```json
{"id":"uuid","cve":"CVE-2024-1234","affects_key":"pkg:npm/express@4.0.0","merge_hash":"a1b2c3...","status":"active","severity":"high","title":"..."}
```
### edges.ndjson
Each line contains a source edge linking a canonical to its source advisory:
```json
{"id":"uuid","canonical_id":"uuid","source":"nvd","source_advisory_id":"CVE-2024-1234","vendor_status":"affected"}
```
### deletions.ndjson
Each line contains a deletion record for withdrawn or deleted canonicals:
```json
{"canonical_id":"uuid","deleted_at":"2025-01-15T10:00:00Z","reason":"withdrawn"}
```
### SIGNATURE.json
When signing is enabled, contains a DSSE envelope over the bundle hash:
```json
{
"payload_type": "application/vnd.stellaops.bundle-hash+json",
"payload": "eyJidW5kbGVfaGFzaCI6InNoYTI1NjphMWIy..."}",
"signatures": [
{
"keyid": "sha256:xyz...",
"sig": "MEUCIQD..."
}
]
}
```
## API Endpoints
### Export Bundle
```
GET /api/v1/federation/export
```
Exports a delta bundle for federation sync.
**Query Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `since_cursor` | string | null | Export changes since this cursor (null = full export) |
| `sign` | bool | true | Sign the bundle with Authority key |
| `max_items` | int | 10000 | Maximum items per bundle (1-100000) |
| `compress_level` | int | 3 | ZST compression level (1-19) |
**Response Headers:**
| Header | Description |
|--------|-------------|
| `Content-Type` | `application/zstd` |
| `Content-Disposition` | `attachment; filename="feedser-bundle-{timestamp}.zst"` |
| `X-Bundle-Hash` | SHA256 hash of bundle content |
| `X-Export-Cursor` | Cursor position after this export |
| `X-Items-Count` | Total items in bundle |
**Response:** Streaming ZST-compressed TAR archive.
**Errors:**
| Status | Code | Description |
|--------|------|-------------|
| 400 | `VALIDATION_FAILED` | Invalid parameter values |
| 503 | `FEDERATION_DISABLED` | Federation is not enabled |
### Preview Export
```
GET /api/v1/federation/export/preview
```
Preview export statistics without creating a bundle.
**Query Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `since_cursor` | string | null | Preview changes since this cursor |
**Response:**
```json
{
"since_cursor": "2025-01-14T00:00:00Z#0000",
"estimated_canonicals": 1234,
"estimated_edges": 3456,
"estimated_deletions": 12,
"estimated_size_bytes": 5242880,
"estimated_size_mb": 5.0
}
```
### Federation Status
```
GET /api/v1/federation/status
```
Get federation configuration status.
**Response:**
```json
{
"enabled": true,
"site_id": "site-us-west-1",
"default_compression_level": 3,
"default_max_items": 10000
}
```
## CLI Commands
### Export Bundle
```bash
stella feedser bundle export [options]
```
**Options:**
| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--since-cursor` | `-c` | null | Export changes since cursor |
| `--output` | `-o` | stdout | Output file path |
| `--sign` | `-s` | true | Sign bundle with Authority key |
| `--compress-level` | `-l` | 3 | ZST compression level (1-19) |
| `--max-items` | `-m` | 10000 | Maximum items per bundle |
| `--json` | | false | Output metadata as JSON |
**Examples:**
```bash
# Full export to file
stella feedser bundle export -o ./bundle.zst
# Delta export since cursor
stella feedser bundle export -c "2025-01-14T00:00:00Z#0000" -o ./delta.zst
# Export without signing (for testing)
stella feedser bundle export --sign=false -o ./unsigned.zst
# High compression for archival
stella feedser bundle export -l 19 -o ./archived.zst
```
### Preview Export
```bash
stella feedser bundle preview [options]
```
**Options:**
| Option | Short | Description |
|--------|-------|-------------|
| `--since-cursor` | `-c` | Preview changes since cursor |
| `--json` | | Output as JSON |
**Example:**
```bash
stella feedser bundle preview -c "2025-01-14T00:00:00Z#0000"
```
## Configuration
Federation is configured in `concelier.yaml`:
```yaml
Federation:
Enabled: true
SiteId: "site-us-west-1"
DefaultCompressionLevel: 3
DefaultMaxItems: 10000
RequireSignature: true
```
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `Enabled` | bool | false | Enable federation endpoints |
| `SiteId` | string | "default" | Identifier for this site |
| `DefaultCompressionLevel` | int | 3 | Default ZST compression level |
| `DefaultMaxItems` | int | 10000 | Default max items per bundle |
| `RequireSignature` | bool | true | Require bundle signatures |
## Cursor Format
Cursors encode a timestamp and sequence number:
```
{ISO8601}#{sequence}
```
Example: `2025-01-15T10:30:00.000Z#0042`
- Timestamp: When the change was recorded
- Sequence: Monotonically increasing within timestamp
Cursors are opaque to consumers and should be passed through unchanged.
## Determinism
Bundles are deterministic:
- Same cursor range produces identical bundle content
- Same content produces identical bundle hash
- Suitable for caching and deduplication
## Security
- Bundles can be signed with DSSE for integrity verification
- Signatures use Authority keys for cross-site trust
- Bundle hash prevents tampering during transit
- ZST compression is not encryption - bundles should be transferred over TLS

View File

@@ -344,6 +344,375 @@ paths:
application/yaml:
schema:
type: string
# Evidence-Weighted Score (EWS) Endpoints - Sprint 8200.0012.0004
/api/v1/findings/{findingId}/score:
post:
summary: Calculate evidence-weighted score for a finding
description: >-
Calculates the Evidence-Weighted Score (EWS) for a finding by aggregating
reachability, runtime signals, exploit likelihood, source trust, and mitigation
effectiveness. Returns a 0-100 score and action bucket (ActNow, ScheduleNext,
Investigate, Watchlist).
operationId: calculateFindingScore
tags: [scoring]
security:
- bearerAuth: [write:scores]
parameters:
- name: findingId
in: path
required: true
description: Finding identifier in format CVE-ID@pkg:PURL
schema:
type: string
pattern: "^[A-Z]+-\\d+@pkg:.+$"
example: "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4"
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/CalculateScoreRequest'
example:
forceRecalculate: false
includeBreakdown: true
policyVersion: null
responses:
'200':
description: Score calculated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/EvidenceWeightedScoreResponse'
example:
findingId: "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4"
score: 78
bucket: "ScheduleNext"
inputs:
rch: 0.85
rts: 0.40
bkp: 0.00
xpl: 0.70
src: 0.80
mit: 0.10
weights:
rch: 0.30
rts: 0.25
bkp: 0.15
xpl: 0.15
src: 0.10
mit: 0.10
flags: ["live-signal", "proven-path"]
explanations:
- "Static reachability: path to vulnerable sink (confidence: 85%)"
- "Runtime: 3 observations in last 24 hours"
policyDigest: "sha256:abc123..."
calculatedAt: "2026-01-15T14:30:00Z"
cachedUntil: "2026-01-15T15:30:00Z"
'400':
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/ScoringErrorResponse'
'404':
description: Finding not found or no evidence available
content:
application/json:
schema:
$ref: '#/components/schemas/ScoringErrorResponse'
'429':
description: Rate limit exceeded (100/min)
get:
summary: Get cached evidence-weighted score for a finding
description: Returns the most recently calculated score from cache. Returns 404 if no score has been calculated.
operationId: getFindingScore
tags: [scoring]
security:
- bearerAuth: [read:scores]
parameters:
- name: findingId
in: path
required: true
schema:
type: string
responses:
'200':
description: Cached score retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/EvidenceWeightedScoreResponse'
'404':
description: No cached score found
/api/v1/findings/scores:
post:
summary: Calculate evidence-weighted scores for multiple findings
description: Batch calculation of scores for up to 100 findings. Returns summary statistics and individual results.
operationId: calculateFindingScoresBatch
tags: [scoring]
security:
- bearerAuth: [write:scores]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CalculateScoresBatchRequest'
example:
findingIds:
- "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4"
- "CVE-2024-5678@pkg:npm/lodash@4.17.20"
forceRecalculate: false
includeBreakdown: true
responses:
'200':
description: Batch scores calculated
content:
application/json:
schema:
$ref: '#/components/schemas/CalculateScoresBatchResponse'
'400':
description: Invalid request or batch too large (max 100)
content:
application/json:
schema:
$ref: '#/components/schemas/ScoringErrorResponse'
'429':
description: Rate limit exceeded (10/min)
/api/v1/findings/{findingId}/score-history:
get:
summary: Get score history for a finding
description: Returns historical score calculations with pagination. Tracks score changes, triggers, and which factors changed.
operationId: getFindingScoreHistory
tags: [scoring]
security:
- bearerAuth: [read:scores]
parameters:
- name: findingId
in: path
required: true
schema:
type: string
- name: from
in: query
description: Start of date range (inclusive)
schema:
type: string
format: date-time
- name: to
in: query
description: End of date range (inclusive)
schema:
type: string
format: date-time
- name: limit
in: query
description: Maximum entries to return (1-100, default 50)
schema:
type: integer
default: 50
minimum: 1
maximum: 100
- name: cursor
in: query
description: Pagination cursor from previous response
schema:
type: string
responses:
'200':
description: Score history retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ScoreHistoryResponse'
'404':
description: Finding not found
/api/v1/scoring/policy:
get:
summary: Get active scoring policy configuration
description: Returns the currently active evidence weight policy including weights, guardrails, and bucket thresholds.
operationId: getActiveScoringPolicy
tags: [scoring]
security:
- bearerAuth: [read:scores]
responses:
'200':
description: Active policy retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ScoringPolicyResponse'
example:
version: "ews.v1.2"
digest: "sha256:abc123..."
activeSince: "2026-01-01T00:00:00Z"
environment: "production"
weights:
rch: 0.30
rts: 0.25
bkp: 0.15
xpl: 0.15
src: 0.10
mit: 0.10
guardrails:
notAffectedCap: { enabled: true, maxScore: 15 }
runtimeFloor: { enabled: true, minScore: 60 }
speculativeCap: { enabled: true, maxScore: 45 }
buckets:
actNowMin: 90
scheduleNextMin: 70
investigateMin: 40
/api/v1/scoring/policy/{version}:
get:
summary: Get specific scoring policy version
description: Returns a specific version of the scoring policy for historical comparison or audit.
operationId: getScoringPolicyVersion
tags: [scoring]
security:
- bearerAuth: [read:scores]
parameters:
- name: version
in: path
required: true
schema:
type: string
example: "ews.v1.2"
responses:
'200':
description: Policy version retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ScoringPolicyResponse'
'404':
description: Policy version not found
/api/v1/scoring/webhooks:
post:
summary: Register a webhook for score change notifications
description: >-
Registers a webhook to receive notifications when finding scores change.
Supports filtering by finding patterns, minimum score change threshold,
and bucket changes. Webhook payloads are signed with HMAC-SHA256 if a
secret is provided.
operationId: registerScoringWebhook
tags: [scoring, webhooks]
security:
- bearerAuth: [admin:scoring]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterWebhookRequest'
example:
url: "https://example.com/webhook/scores"
secret: "webhook-secret-key"
findingPatterns: ["CVE-*"]
minScoreChange: 10
triggerOnBucketChange: true
responses:
'201':
description: Webhook registered
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
'400':
description: Invalid webhook URL or configuration
'429':
description: Rate limit exceeded (10/min)
get:
summary: List all registered webhooks
operationId: listScoringWebhooks
tags: [scoring, webhooks]
security:
- bearerAuth: [admin:scoring]
responses:
'200':
description: List of webhooks
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookListResponse'
/api/v1/scoring/webhooks/{id}:
get:
summary: Get a specific webhook by ID
operationId: getScoringWebhook
tags: [scoring, webhooks]
security:
- bearerAuth: [admin:scoring]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Webhook details
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
'404':
description: Webhook not found
put:
summary: Update a webhook configuration
operationId: updateScoringWebhook
tags: [scoring, webhooks]
security:
- bearerAuth: [admin:scoring]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterWebhookRequest'
responses:
'200':
description: Webhook updated
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
'404':
description: Webhook not found
'400':
description: Invalid configuration
delete:
summary: Delete a webhook
operationId: deleteScoringWebhook
tags: [scoring, webhooks]
security:
- bearerAuth: [admin:scoring]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: Webhook deleted
'404':
description: Webhook not found
components:
securitySchemes:
bearerAuth:
@@ -410,6 +779,464 @@ components:
default: 200
maximum: 1000
schemas:
# Evidence-Weighted Score (EWS) Schemas - Sprint 8200.0012.0004
CalculateScoreRequest:
type: object
description: Request to calculate score for a finding
properties:
forceRecalculate:
type: boolean
default: false
description: Force recalculation even if cached score exists
includeBreakdown:
type: boolean
default: true
description: Include detailed breakdown in response
policyVersion:
type: string
nullable: true
description: Specific policy version to use. Null = use latest
CalculateScoresBatchRequest:
type: object
required: [findingIds]
description: Request to calculate scores for multiple findings
properties:
findingIds:
type: array
maxItems: 100
items:
type: string
description: Finding IDs to calculate scores for. Max 100.
forceRecalculate:
type: boolean
default: false
includeBreakdown:
type: boolean
default: true
policyVersion:
type: string
nullable: true
EvidenceWeightedScoreResponse:
type: object
required: [findingId, score, bucket, policyDigest, calculatedAt]
description: Evidence-weighted score calculation result
properties:
findingId:
type: string
description: Finding identifier
score:
type: integer
minimum: 0
maximum: 100
description: Calculated score (0-100). Higher = more urgent.
bucket:
type: string
enum: [ActNow, ScheduleNext, Investigate, Watchlist]
description: Action bucket classification
inputs:
$ref: '#/components/schemas/EvidenceInputs'
weights:
$ref: '#/components/schemas/EvidenceWeights'
flags:
type: array
items:
type: string
enum: [live-signal, proven-path, vendor-na, speculative]
description: Active flags affecting the score
explanations:
type: array
items:
type: string
description: Human-readable explanations for each factor
caps:
$ref: '#/components/schemas/AppliedCaps'
policyDigest:
type: string
pattern: "^sha256:[a-f0-9]{64}$"
description: Policy digest used for calculation
calculatedAt:
type: string
format: date-time
description: When the score was calculated
cachedUntil:
type: string
format: date-time
description: When the cached score expires
fromCache:
type: boolean
description: Whether this result came from cache
EvidenceInputs:
type: object
description: Normalized evidence input values (0-1 scale)
properties:
rch:
type: number
format: double
minimum: 0
maximum: 1
description: Reachability
rts:
type: number
format: double
minimum: 0
maximum: 1
description: Runtime signal
bkp:
type: number
format: double
minimum: 0
maximum: 1
description: Backport availability
xpl:
type: number
format: double
minimum: 0
maximum: 1
description: Exploit likelihood
src:
type: number
format: double
minimum: 0
maximum: 1
description: Source trust
mit:
type: number
format: double
minimum: 0
maximum: 1
description: Mitigation effectiveness
EvidenceWeights:
type: object
description: Evidence weight configuration (weights must sum to 1.0)
properties:
rch:
type: number
format: double
description: Reachability weight
rts:
type: number
format: double
description: Runtime signal weight
bkp:
type: number
format: double
description: Backport weight
xpl:
type: number
format: double
description: Exploit weight
src:
type: number
format: double
description: Source trust weight
mit:
type: number
format: double
description: Mitigation weight
AppliedCaps:
type: object
description: Applied guardrail caps and floors
properties:
speculativeCap:
type: boolean
description: Speculative cap applied (no runtime evidence)
notAffectedCap:
type: boolean
description: Not-affected cap applied (VEX status)
runtimeFloor:
type: boolean
description: Runtime floor applied (observed in production)
CalculateScoresBatchResponse:
type: object
required: [results, summary, policyDigest, calculatedAt]
description: Batch score calculation result
properties:
results:
type: array
items:
$ref: '#/components/schemas/EvidenceWeightedScoreResponse'
description: Individual score results
summary:
$ref: '#/components/schemas/BatchSummary'
errors:
type: array
items:
$ref: '#/components/schemas/ScoringError'
description: Errors for failed calculations
policyDigest:
type: string
description: Policy digest used for all calculations
calculatedAt:
type: string
format: date-time
BatchSummary:
type: object
required: [total, succeeded, failed, byBucket, averageScore, calculationTimeMs]
properties:
total:
type: integer
description: Total findings processed
succeeded:
type: integer
description: Successful calculations
failed:
type: integer
description: Failed calculations
byBucket:
$ref: '#/components/schemas/BucketDistribution'
averageScore:
type: number
format: double
description: Average score across all findings
calculationTimeMs:
type: number
format: double
description: Total calculation time in milliseconds
BucketDistribution:
type: object
description: Score distribution by bucket
properties:
actNow:
type: integer
scheduleNext:
type: integer
investigate:
type: integer
watchlist:
type: integer
ScoreHistoryResponse:
type: object
required: [findingId, history, pagination]
description: Score history response
properties:
findingId:
type: string
description: Finding ID
history:
type: array
items:
$ref: '#/components/schemas/ScoreHistoryEntry'
description: History entries
pagination:
$ref: '#/components/schemas/Pagination'
ScoreHistoryEntry:
type: object
required: [score, bucket, policyDigest, calculatedAt, trigger]
description: Score history entry
properties:
score:
type: integer
minimum: 0
maximum: 100
description: Score value at this point in time
bucket:
type: string
description: Bucket at this point in time
policyDigest:
type: string
description: Policy digest used
calculatedAt:
type: string
format: date-time
description: When calculated
trigger:
type: string
enum: [evidence_update, policy_change, scheduled, manual]
description: What triggered recalculation
changedFactors:
type: array
items:
type: string
description: Which factors changed since previous calculation
Pagination:
type: object
required: [hasMore]
description: Pagination metadata
properties:
hasMore:
type: boolean
description: Whether more results are available
nextCursor:
type: string
description: Cursor for next page. Null if no more pages.
ScoringPolicyResponse:
type: object
required: [version, digest, activeSince, environment, weights, guardrails, buckets]
description: Scoring policy response
properties:
version:
type: string
description: Policy version identifier
digest:
type: string
description: Policy content digest
activeSince:
type: string
format: date-time
description: When this policy became active
environment:
type: string
description: Environment (production, staging, etc.)
weights:
$ref: '#/components/schemas/EvidenceWeights'
guardrails:
$ref: '#/components/schemas/GuardrailsConfig'
buckets:
$ref: '#/components/schemas/BucketThresholds'
GuardrailsConfig:
type: object
description: Guardrail configuration
properties:
notAffectedCap:
$ref: '#/components/schemas/Guardrail'
runtimeFloor:
$ref: '#/components/schemas/Guardrail'
speculativeCap:
$ref: '#/components/schemas/Guardrail'
Guardrail:
type: object
required: [enabled]
description: Individual guardrail settings
properties:
enabled:
type: boolean
maxScore:
type: integer
minScore:
type: integer
BucketThresholds:
type: object
required: [actNowMin, scheduleNextMin, investigateMin]
description: Bucket threshold configuration
properties:
actNowMin:
type: integer
description: Minimum score for ActNow bucket
scheduleNextMin:
type: integer
description: Minimum score for ScheduleNext bucket
investigateMin:
type: integer
description: Minimum score for Investigate bucket
RegisterWebhookRequest:
type: object
required: [url]
description: Request to register a webhook for score changes
properties:
url:
type: string
format: uri
description: Webhook URL to call on score changes
secret:
type: string
description: Optional secret for HMAC-SHA256 signature
findingPatterns:
type: array
items:
type: string
description: Finding ID patterns to watch. Empty = all findings.
minScoreChange:
type: integer
default: 5
description: Minimum score change to trigger webhook
triggerOnBucketChange:
type: boolean
default: true
description: Whether to trigger on bucket changes
WebhookResponse:
type: object
required: [id, url, hasSecret, minScoreChange, triggerOnBucketChange, createdAt]
description: Webhook registration response
properties:
id:
type: string
format: uuid
description: Webhook ID
url:
type: string
description: Webhook URL
hasSecret:
type: boolean
description: Whether secret is configured
findingPatterns:
type: array
items:
type: string
description: Finding patterns being watched
minScoreChange:
type: integer
description: Minimum score change threshold
triggerOnBucketChange:
type: boolean
description: Whether to trigger on bucket changes
createdAt:
type: string
format: date-time
description: When webhook was created
WebhookListResponse:
type: object
required: [webhooks, totalCount]
properties:
webhooks:
type: array
items:
$ref: '#/components/schemas/WebhookResponse'
totalCount:
type: integer
ScoringError:
type: object
required: [findingId, code, message]
description: Scoring error information
properties:
findingId:
type: string
description: Finding ID that failed
code:
type: string
description: Error code
message:
type: string
description: Error message
ScoringErrorResponse:
type: object
required: [code, message]
description: Scoring error response
properties:
code:
type: string
description: Error code
message:
type: string
description: Error message
details:
type: object
additionalProperties: true
description: Additional details
traceId:
type: string
description: Trace ID for debugging
# Existing Ledger Schemas
LedgerEvent:
type: object
required: [event]

View File

@@ -198,14 +198,38 @@ sequenceDiagram
## Invalidation
> **See also**: [architecture.md](architecture.md#invalidation-mechanisms) for detailed invalidation flow diagrams.
### Automatic Invalidation Triggers
| Trigger | Event | Scope |
|---------|-------|-------|
| Signer Revocation | `SignerRevokedEvent` | All entries with matching `signer_set_hash` |
| Feed Epoch Advance | `FeedEpochAdvancedEvent` | Entries with older `feed_epoch` |
| Policy Update | `PolicyUpdatedEvent` | Entries with matching `policy_hash` |
| TTL Expiry | Background job | Entries past `expires_at` |
| Trigger | Event | Scope | Implementation |
|---------|-------|-------|----------------|
| Signer Revocation | `SignerRevokedEvent` | All entries with matching `signer_set_hash` | `SignerSetInvalidator` |
| Feed Epoch Advance | `FeedEpochAdvancedEvent` | Entries with older `feed_epoch` | `FeedEpochInvalidator` |
| Policy Update | `PolicyUpdatedEvent` | Entries with matching `policy_hash` | `PolicyHashInvalidator` |
| TTL Expiry | Background job | Entries past `expires_at` | `TtlExpirationService` |
### Invalidation Interfaces
```csharp
// Main invalidator interface
public interface IProvcacheInvalidator
{
Task<int> InvalidateAsync(
InvalidationCriteria criteria,
string reason,
string? correlationId = null,
CancellationToken cancellationToken = default);
}
// Revocation ledger for audit trail
public interface IRevocationLedger
{
Task RecordAsync(RevocationEntry entry, CancellationToken ct = default);
Task<IReadOnlyList<RevocationEntry>> GetEntriesSinceAsync(long sinceSeqNo, int limit = 1000, CancellationToken ct = default);
Task<RevocationLedgerStats> GetStatsAsync(CancellationToken ct = default);
}
```
### Manual Invalidation
@@ -227,8 +251,25 @@ POST /v1/provcache/invalidate
}
```
### Revocation Replay
Nodes can replay missed revocation events after restart or network partition:
```csharp
var replayService = services.GetRequiredService<IRevocationReplayService>();
var checkpoint = await replayService.GetCheckpointAsync();
var result = await replayService.ReplayFromAsync(
sinceSeqNo: checkpoint,
new RevocationReplayOptions { BatchSize = 1000 });
// result.EntriesReplayed, result.TotalInvalidations
```
## Air-Gap Integration
> **See also**: [architecture.md](architecture.md#air-gap-exportimport) for bundle format specification and architecture diagrams.
### Export Workflow
```bash
@@ -248,17 +289,56 @@ stella prov export --verikey sha256:abc123 --density strict --sign
# Import and verify Merkle root
stella prov import --input proof.bundle
# Import with lazy chunk fetch
# Import with lazy chunk fetch (connected mode)
stella prov import --input proof-lite.json --lazy-fetch --backend https://api.stellaops.com
# Import with lazy fetch from file directory (sneakernet mode)
stella prov import --input proof-lite.json --lazy-fetch --chunks-dir /mnt/usb/evidence
```
### Density Levels
| Level | Contents | Size | Use Case |
|-------|----------|------|----------|
| `lite` | DecisionDigest + ProofRoot | ~2 KB | Quick verification |
| `standard` | + First N chunks | ~200 KB | Normal audit |
| `strict` | + All chunks | Variable | Full compliance |
| Level | Contents | Size | Use Case | Lazy Fetch Support |
|-------|----------|------|----------|--------------------|
| `lite` | DecisionDigest + ProofRoot + Manifest | ~2 KB | Quick verification | Required |
| `standard` | + First N chunks (~10%) | ~200 KB | Normal audit | Partial (remaining chunks) |
| `strict` | + All chunks | Variable | Full compliance | Not needed |
### Lazy Evidence Fetching
For `lite` and `standard` density exports, missing chunks can be fetched on-demand:
```csharp
// HTTP fetcher (connected mode)
var httpFetcher = new HttpChunkFetcher(
new Uri("https://api.stellaops.com"), logger);
// File fetcher (air-gapped/sneakernet mode)
var fileFetcher = new FileChunkFetcher(
basePath: "/mnt/usb/evidence", logger);
// Orchestrate fetch + verify + store
var orchestrator = new LazyFetchOrchestrator(repository, logger);
var result = await orchestrator.FetchAndStoreAsync(
proofRoot: "sha256:...",
fetcher,
new LazyFetchOptions
{
VerifyOnFetch = true,
BatchSize = 100,
MaxChunks = 1000
});
```
### Sneakernet Export for Chunked Evidence
```csharp
// Export evidence chunks to file system for transport
await fileFetcher.ExportEvidenceChunksToFilesAsync(
manifest,
chunks,
outputDirectory: "/mnt/usb/evidence");
```
## Configuration
@@ -453,19 +533,30 @@ CREATE TABLE provcache.prov_evidence_chunks (
```sql
CREATE TABLE provcache.prov_revocations (
revocation_id UUID PRIMARY KEY,
revocation_type TEXT NOT NULL,
target_hash TEXT NOT NULL,
reason TEXT,
actor TEXT,
entries_affected BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL
seq_no BIGSERIAL PRIMARY KEY,
revocation_id UUID NOT NULL UNIQUE,
revocation_type VARCHAR(32) NOT NULL, -- signer, feed_epoch, policy, explicit, expiration
revoked_key VARCHAR(512) NOT NULL,
reason VARCHAR(1024),
entries_invalidated INTEGER NOT NULL,
source VARCHAR(128) NOT NULL,
correlation_id VARCHAR(128),
revoked_at TIMESTAMPTZ NOT NULL,
metadata JSONB,
CONSTRAINT chk_revocation_type CHECK (
revocation_type IN ('signer', 'feed_epoch', 'policy', 'explicit', 'expiration')
)
);
CREATE INDEX idx_revocations_type ON provcache.prov_revocations(revocation_type);
CREATE INDEX idx_revocations_key ON provcache.prov_revocations(revoked_key);
CREATE INDEX idx_revocations_time ON provcache.prov_revocations(revoked_at);
```
## Implementation Status
### Completed (Sprint 8200.0001.0001)
### Completed (Sprint 8200.0001.0001 - Core Backend)
| Component | Path | Status |
|-----------|------|--------|
@@ -477,18 +568,39 @@ CREATE TABLE provcache.prov_revocations (
| API Endpoints | `src/__Libraries/StellaOps.Provcache.Api/` | ✅ Done |
| Unit Tests (53) | `src/__Libraries/__Tests/StellaOps.Provcache.Tests/` | ✅ Done |
### Completed (Sprint 8200.0001.0002 - Invalidation & Air-Gap)
| Component | Path | Status |
|-----------|------|--------|
| Invalidation Interfaces | `src/__Libraries/StellaOps.Provcache/Invalidation/` | ✅ Done |
| Repository Invalidation Methods | `IEvidenceChunkRepository.Delete*Async()` | ✅ Done |
| Export Interfaces | `src/__Libraries/StellaOps.Provcache/Export/` | ✅ Done |
| IMinimalProofExporter | `Export/IMinimalProofExporter.cs` | ✅ Done |
| MinimalProofExporter | `Export/MinimalProofExporter.cs` | ✅ Done |
| Lazy Fetch - ILazyEvidenceFetcher | `LazyFetch/ILazyEvidenceFetcher.cs` | ✅ Done |
| Lazy Fetch - HttpChunkFetcher | `LazyFetch/HttpChunkFetcher.cs` | ✅ Done |
| Lazy Fetch - FileChunkFetcher | `LazyFetch/FileChunkFetcher.cs` | ✅ Done |
| Lazy Fetch - LazyFetchOrchestrator | `LazyFetch/LazyFetchOrchestrator.cs` | ✅ Done |
| Revocation - IRevocationLedger | `Revocation/IRevocationLedger.cs` | ✅ Done |
| Revocation - InMemoryRevocationLedger | `Revocation/InMemoryRevocationLedger.cs` | ✅ Done |
| Revocation - RevocationReplayService | `Revocation/RevocationReplayService.cs` | ✅ Done |
| ProvRevocationEntity | `Entities/ProvRevocationEntity.cs` | ✅ Done |
| Unit Tests (124 total) | `src/__Libraries/__Tests/StellaOps.Provcache.Tests/` | ✅ Done |
### Blocked
| Component | Reason |
|-----------|--------|
| Policy Engine Integration | `PolicyEvaluator` is `internal sealed`; requires architectural review to expose injection points for `IProvcacheService` |
| CLI e2e Tests | `AddSimRemoteCryptoProvider` method missing in CLI codebase |
### Pending
| Component | Sprint |
|-----------|--------|
| Signer Revocation Events | 8200.0001.0002 |
| CLI Export/Import | 8200.0001.0002 |
| Authority Event Integration | 8200.0001.0002 (BLOCKED - Authority needs event publishing) |
| Concelier Event Integration | 8200.0001.0002 (BLOCKED - Concelier needs event publishing) |
| PostgresRevocationLedger | Future (requires EF Core integration) |
| UI Badges & Proof Tree | 8200.0001.0003 |
| Grafana Dashboards | 8200.0001.0003 |
@@ -502,6 +614,7 @@ CREATE TABLE provcache.prov_revocations (
## Related Documentation
- **[Provcache Architecture Guide](architecture.md)** - Detailed architecture, invalidation flows, and API reference
- [Policy Engine Architecture](../policy/README.md)
- [TrustLattice Engine](../policy/design/policy-deterministic-evaluator.md)
- [Offline Kit Documentation](../../24_OFFLINE_KIT.md)

View File

@@ -0,0 +1,438 @@
# Provcache Architecture Guide
> Detailed architecture documentation for the Provenance Cache module
## Overview
Provcache provides a caching layer that maximizes "provenance density" — the amount of trustworthy evidence retained per byte. This document covers the internal architecture, invalidation mechanisms, air-gap support, and replay capabilities.
## Table of Contents
1. [Cache Architecture](#cache-architecture)
2. [Invalidation Mechanisms](#invalidation-mechanisms)
3. [Evidence Chunk Storage](#evidence-chunk-storage)
4. [Air-Gap Export/Import](#air-gap-exportimport)
5. [Lazy Evidence Fetching](#lazy-evidence-fetching)
6. [Revocation Ledger](#revocation-ledger)
7. [API Reference](#api-reference)
---
## Cache Architecture
### Storage Layers
```
┌───────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ VeriKey │───▶│ Provcache │───▶│ Policy Engine │ │
│ │ Builder │ │ Service │ │ (cache miss) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Caching Layer │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ Valkey │◀───────▶│ PostgreSQL │ │
│ │ (read-through) │ │ (write-behind queue) │ │
│ │ │ │ │ │
│ │ • Hot cache │ │ • provcache_items │ │
│ │ • Sub-ms reads │ │ • prov_evidence_chunks │ │
│ │ • TTL-based │ │ • prov_revocations │ │
│ └─────────────────┘ └──────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
```
### Key Components
| Component | Purpose |
|-----------|---------|
| `IProvcacheService` | Main service interface for cache operations |
| `IProvcacheStore` | Storage abstraction (Valkey + Postgres) |
| `WriteBehindQueue` | Async persistence to Postgres |
| `IEvidenceChunker` | Splits large evidence into Merkle-verified chunks |
| `IRevocationLedger` | Audit trail for all invalidation events |
---
## Invalidation Mechanisms
Provcache supports multiple invalidation triggers to ensure cache consistency when upstream data changes.
### Automatic Invalidation
#### 1. Signer Revocation
When a signing key is compromised or rotated:
```
┌─────────────┐ SignerRevokedEvent ┌──────────────────┐
│ Authority │ ──────────────────────────▶│ SignerSet │
│ Module │ │ Invalidator │
└─────────────┘ └────────┬─────────┘
DELETE FROM provcache_items
WHERE signer_set_hash = ?
```
**Implementation**: `SignerSetInvalidator` subscribes to `SignerRevokedEvent` and invalidates all entries signed by the revoked key.
#### 2. Feed Epoch Advancement
When vulnerability feeds are updated:
```
┌─────────────┐ FeedEpochAdvancedEvent ┌──────────────────┐
│ Concelier │ ───────────────────────────▶│ FeedEpoch │
│ Module │ │ Invalidator │
└─────────────┘ └────────┬─────────┘
DELETE FROM provcache_items
WHERE feed_epoch < ?
```
**Implementation**: `FeedEpochInvalidator` compares epochs using semantic versioning or ISO timestamps.
#### 3. Policy Updates
When policy bundles change:
```
┌─────────────┐ PolicyUpdatedEvent ┌──────────────────┐
│ Policy │ ───────────────────────────▶│ PolicyHash │
│ Engine │ │ Invalidator │
└─────────────┘ └────────┬─────────┘
DELETE FROM provcache_items
WHERE policy_hash = ?
```
### Invalidation Recording
All invalidation events are recorded in the revocation ledger for audit and replay:
```csharp
public interface IProvcacheInvalidator
{
Task<int> InvalidateAsync(
InvalidationCriteria criteria,
string reason,
string? correlationId = null,
CancellationToken cancellationToken = default);
}
```
The ledger entry includes:
- Revocation type (signer, feed_epoch, policy, explicit)
- The revoked key
- Number of entries invalidated
- Timestamp and correlation ID for tracing
---
## Evidence Chunk Storage
Large evidence (SBOMs, VEX documents, call graphs) is stored in fixed-size chunks with Merkle tree verification.
### Chunking Process
```
┌─────────────────────────────────────────────────────────────────┐
│ Original Evidence │
│ [ 2.3 MB SPDX SBOM JSON ] │
└─────────────────────────────────────────────────────────────────┘
▼ IEvidenceChunker.ChunkAsync()
┌─────────────────────────────────────────────────────────────────┐
│ Chunk 0 (64KB) │ Chunk 1 (64KB) │ ... │ Chunk N (partial) │
│ hash: abc123 │ hash: def456 │ │ hash: xyz789 │
└─────────────────────────────────────────────────────────────────┘
▼ Merkle tree construction
┌─────────────────────────────────────────────────────────────────┐
│ Proof Root │
│ sha256:merkle_root_of_all_chunks │
└─────────────────────────────────────────────────────────────────┘
```
### Database Schema
```sql
CREATE TABLE provcache.prov_evidence_chunks (
chunk_id UUID PRIMARY KEY,
proof_root VARCHAR(128) NOT NULL,
chunk_index INTEGER NOT NULL,
chunk_hash VARCHAR(128) NOT NULL,
blob BYTEA NOT NULL,
blob_size INTEGER NOT NULL,
content_type VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uk_proof_chunk UNIQUE (proof_root, chunk_index)
);
CREATE INDEX idx_evidence_proof_root ON provcache.prov_evidence_chunks(proof_root);
```
### Paging API
Evidence can be retrieved in pages to manage memory:
```http
GET /api/v1/proofs/{proofRoot}?page=0&pageSize=10
```
Response includes chunk metadata without blob data, allowing clients to fetch specific chunks on demand.
---
## Air-Gap Export/Import
Provcache supports air-gapped environments through minimal proof bundles.
### Bundle Format (v1)
```json
{
"version": "v1",
"exportedAt": "2025-01-15T10:30:00Z",
"density": "standard",
"digest": {
"veriKey": "sha256:...",
"verdictHash": "sha256:...",
"proofRoot": "sha256:...",
"trustScore": 85
},
"manifest": {
"proofRoot": "sha256:...",
"totalChunks": 42,
"totalSize": 2752512,
"chunks": [...]
},
"chunks": [
{
"index": 0,
"data": "base64...",
"hash": "sha256:..."
}
],
"signature": {
"algorithm": "ECDSA-P256",
"signature": "base64...",
"signedAt": "2025-01-15T10:30:01Z"
}
}
```
### Density Levels
| Level | Contents | Typical Size | Use Case |
|-------|----------|--------------|----------|
| **Lite** | Digest + ProofRoot + Manifest | ~2 KB | Quick verification, requires lazy fetch for full evidence |
| **Standard** | + First 10% of chunks | ~200 KB | Normal audits, balance of size vs completeness |
| **Strict** | + All chunks | Variable | Full compliance, no network needed |
### Export Example
```csharp
var exporter = serviceProvider.GetRequiredService<IMinimalProofExporter>();
// Lite export (manifest only)
var liteBundle = await exporter.ExportAsync(
veriKey: "sha256:abc123",
new MinimalProofExportOptions { Density = ProofDensity.Lite });
// Signed strict export
var strictBundle = await exporter.ExportAsync(
veriKey: "sha256:abc123",
new MinimalProofExportOptions
{
Density = ProofDensity.Strict,
SignBundle = true,
Signer = signerInstance
});
```
### Import and Verification
```csharp
var result = await exporter.ImportAsync(bundle);
if (result.DigestVerified && result.ChunksVerified)
{
// Bundle is authentic
await provcache.UpsertAsync(result.Entry);
}
```
---
## Lazy Evidence Fetching
For lite bundles, missing chunks can be fetched on-demand from connected or file sources.
### Fetcher Architecture
```
┌────────────────────┐
│ ILazyEvidenceFetcher│
└─────────┬──────────┘
┌─────┴─────┐
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ HTTP │ │ File │
│ Fetcher │ │ Fetcher │
└─────────┘ └──────────┘
```
### HTTP Fetcher (Connected Mode)
```csharp
var fetcher = new HttpChunkFetcher(
new Uri("https://api.stellaops.com"),
logger);
var orchestrator = new LazyFetchOrchestrator(repository, logger);
var result = await orchestrator.FetchAndStoreAsync(
proofRoot: "sha256:...",
fetcher,
new LazyFetchOptions
{
VerifyOnFetch = true,
BatchSize = 100
});
```
### File Fetcher (Sneakernet Mode)
For fully air-gapped environments:
1. Export full evidence to USB drive
2. Transport to isolated network
3. Import using file fetcher
```csharp
var fetcher = new FileChunkFetcher(
basePath: "/mnt/usb/evidence",
logger);
var result = await orchestrator.FetchAndStoreAsync(proofRoot, fetcher);
```
---
## Revocation Ledger
The revocation ledger provides a complete audit trail of all invalidation events.
### Schema
```sql
CREATE TABLE provcache.prov_revocations (
seq_no BIGSERIAL PRIMARY KEY,
revocation_id UUID NOT NULL,
revocation_type VARCHAR(32) NOT NULL,
revoked_key VARCHAR(512) NOT NULL,
reason VARCHAR(1024),
entries_invalidated INTEGER NOT NULL,
source VARCHAR(128) NOT NULL,
correlation_id VARCHAR(128),
revoked_at TIMESTAMPTZ NOT NULL,
metadata JSONB
);
```
### Replay for Catch-Up
After node restart or network partition, nodes can replay missed revocations:
```csharp
var replayService = serviceProvider.GetRequiredService<IRevocationReplayService>();
// Get last checkpoint
var checkpoint = await replayService.GetCheckpointAsync();
// Replay from checkpoint
var result = await replayService.ReplayFromAsync(
sinceSeqNo: checkpoint,
new RevocationReplayOptions
{
BatchSize = 1000,
SaveCheckpointPerBatch = true
});
Console.WriteLine($"Replayed {result.EntriesReplayed} revocations, {result.TotalInvalidations} entries invalidated");
```
### Statistics
```csharp
var ledger = serviceProvider.GetRequiredService<IRevocationLedger>();
var stats = await ledger.GetStatsAsync();
// stats.TotalEntries - total revocation events
// stats.EntriesByType - breakdown by type (signer, feed_epoch, etc.)
// stats.TotalEntriesInvalidated - sum of all invalidated cache entries
```
---
## API Reference
### Evidence Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/proofs/{proofRoot}` | GET | Get paged evidence chunks |
| `/api/v1/proofs/{proofRoot}/manifest` | GET | Get chunk manifest |
| `/api/v1/proofs/{proofRoot}/chunks/{index}` | GET | Get specific chunk |
| `/api/v1/proofs/{proofRoot}/verify` | POST | Verify Merkle proof |
### Invalidation Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/provcache/invalidate` | POST | Manual invalidation |
| `/api/v1/provcache/revocations` | GET | List revocation history |
| `/api/v1/provcache/stats` | GET | Cache statistics |
### CLI Commands
```bash
# Export commands
stella prov export --verikey <key> --density <lite|standard|strict> [--output <file>] [--sign]
# Import commands
stella prov import <file> [--lazy-fetch] [--backend <url>] [--chunks-dir <path>]
# Verify commands
stella prov verify <file> [--signer-cert <cert>]
```
---
## Configuration
Key settings in `appsettings.json`:
```json
{
"Provcache": {
"ChunkSize": 65536,
"MaxChunksPerEntry": 1000,
"DefaultTtl": "24:00:00",
"EnableWriteBehind": true,
"WriteBehindFlushInterval": "00:00:05"
}
}
```
See [README.md](README.md) for full configuration reference.

View File

@@ -0,0 +1,419 @@
# Provcache Metrics and Alerting Guide
This document describes the Prometheus metrics exposed by the Provcache layer and recommended alerting configurations.
## Overview
Provcache emits metrics for monitoring cache performance, hit rates, latency, and invalidation patterns. These metrics enable operators to:
- Track cache effectiveness
- Identify performance degradation
- Detect anomalous invalidation patterns
- Capacity plan for cache infrastructure
## Prometheus Metrics
### Request Counters
#### `provcache_requests_total`
Total number of cache requests.
| Label | Values | Description |
|-------|--------|-------------|
| `source` | `valkey`, `postgres` | Cache tier that handled the request |
| `result` | `hit`, `miss`, `expired` | Request outcome |
```promql
# Total requests per minute
rate(provcache_requests_total[1m])
# Hit rate percentage
sum(rate(provcache_requests_total{result="hit"}[5m])) /
sum(rate(provcache_requests_total[5m])) * 100
```
#### `provcache_hits_total`
Total cache hits (subset of requests with `result="hit"`).
| Label | Values | Description |
|-------|--------|-------------|
| `source` | `valkey`, `postgres` | Cache tier |
```promql
# Valkey vs Postgres hit ratio
sum(rate(provcache_hits_total{source="valkey"}[5m])) /
sum(rate(provcache_hits_total[5m])) * 100
```
#### `provcache_misses_total`
Total cache misses.
| Label | Values | Description |
|-------|--------|-------------|
| `reason` | `not_found`, `expired`, `invalidated` | Miss reason |
```promql
# Miss rate by reason
sum by (reason) (rate(provcache_misses_total[5m]))
```
### Latency Histogram
#### `provcache_latency_seconds`
Latency distribution for cache operations.
| Label | Values | Description |
|-------|--------|-------------|
| `operation` | `get`, `set`, `invalidate` | Operation type |
| `source` | `valkey`, `postgres` | Cache tier |
Buckets: `0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0`
```promql
# P50 latency for cache gets
histogram_quantile(0.50, rate(provcache_latency_seconds_bucket{operation="get"}[5m]))
# P95 latency
histogram_quantile(0.95, rate(provcache_latency_seconds_bucket{operation="get"}[5m]))
# P99 latency
histogram_quantile(0.99, rate(provcache_latency_seconds_bucket{operation="get"}[5m]))
```
### Gauge Metrics
#### `provcache_items_count`
Current number of items in cache.
| Label | Values | Description |
|-------|--------|-------------|
| `source` | `valkey`, `postgres` | Cache tier |
```promql
# Total cached items
sum(provcache_items_count)
# Items by tier
sum by (source) (provcache_items_count)
```
### Invalidation Metrics
#### `provcache_invalidations_total`
Total invalidation events.
| Label | Values | Description |
|-------|--------|-------------|
| `reason` | `signer_revoked`, `epoch_advanced`, `ttl_expired`, `manual` | Invalidation trigger |
```promql
# Invalidation rate by reason
sum by (reason) (rate(provcache_invalidations_total[5m]))
# Security-related invalidations
sum(rate(provcache_invalidations_total{reason="signer_revoked"}[5m]))
```
### Trust Score Metrics
#### `provcache_trust_score_average`
Gauge showing average trust score across cached decisions.
```promql
# Current average trust score
provcache_trust_score_average
```
#### `provcache_trust_score_bucket`
Histogram of trust score distribution.
Buckets: `20, 40, 60, 80, 100`
```promql
# Percentage of decisions with trust score >= 80
sum(rate(provcache_trust_score_bucket{le="100"}[5m])) -
sum(rate(provcache_trust_score_bucket{le="80"}[5m]))
```
---
## Grafana Dashboard
A pre-built dashboard is available at `deploy/grafana/dashboards/provcache-overview.json`.
### Panels
| Panel | Type | Description |
|-------|------|-------------|
| Cache Hit Rate | Gauge | Current hit rate percentage |
| Hit Rate Over Time | Time series | Hit rate trend |
| Latency Percentiles | Time series | P50, P95, P99 latency |
| Invalidation Rate | Time series | Invalidations per minute |
| Cache Size | Time series | Item count over time |
| Hits by Source | Pie chart | Valkey vs Postgres distribution |
| Entry Size Distribution | Histogram | Size of cached entries |
| Trust Score Distribution | Histogram | Decision trust scores |
### Importing the Dashboard
```bash
# Via Grafana HTTP API
curl -X POST http://grafana:3000/api/dashboards/db \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $GRAFANA_API_KEY" \
-d @deploy/grafana/dashboards/provcache-overview.json
# Via Helm (auto-provisioned)
# Dashboard is auto-imported when using StellaOps Helm chart
helm upgrade stellaops ./deploy/helm/stellaops \
--set grafana.dashboards.provcache.enabled=true
```
---
## Alerting Rules
### Recommended Alerts
#### Low Cache Hit Rate
```yaml
alert: ProvcacheLowHitRate
expr: |
sum(rate(provcache_requests_total{result="hit"}[5m])) /
sum(rate(provcache_requests_total[5m])) < 0.7
for: 10m
labels:
severity: warning
annotations:
summary: "Provcache hit rate below 70%"
description: "Cache hit rate is {{ $value | humanizePercentage }}. Check for invalidation storms or cold cache."
```
#### Critical Hit Rate Drop
```yaml
alert: ProvcacheCriticalHitRate
expr: |
sum(rate(provcache_requests_total{result="hit"}[5m])) /
sum(rate(provcache_requests_total[5m])) < 0.5
for: 5m
labels:
severity: critical
annotations:
summary: "Provcache hit rate critically low"
description: "Cache hit rate is {{ $value | humanizePercentage }}. Immediate investigation required."
```
#### High Latency
```yaml
alert: ProvcacheHighLatency
expr: |
histogram_quantile(0.95, rate(provcache_latency_seconds_bucket{operation="get"}[5m])) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "Provcache P95 latency above 100ms"
description: "P95 get latency is {{ $value | humanizeDuration }}. Check Valkey/Postgres performance."
```
#### Excessive Invalidations
```yaml
alert: ProvcacheInvalidationStorm
expr: |
sum(rate(provcache_invalidations_total[5m])) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Provcache invalidation rate spike"
description: "Invalidations at {{ $value }} per second. Check for feed epoch changes or revocations."
```
#### Signer Revocation Spike
```yaml
alert: ProvcacheSignerRevocations
expr: |
sum(rate(provcache_invalidations_total{reason="signer_revoked"}[5m])) > 10
for: 2m
labels:
severity: critical
annotations:
summary: "Signer revocation causing mass invalidation"
description: "{{ $value }} invalidations/sec due to signer revocation. Security event investigation required."
```
#### Cache Size Approaching Limit
```yaml
alert: ProvcacheSizeHigh
expr: |
sum(provcache_items_count) > 900000
for: 15m
labels:
severity: warning
annotations:
summary: "Provcache size approaching limit"
description: "Cache has {{ $value }} items. Consider scaling or tuning TTL."
```
#### Low Trust Scores
```yaml
alert: ProvcacheLowTrustScores
expr: |
provcache_trust_score_average < 60
for: 30m
labels:
severity: info
annotations:
summary: "Average trust score below 60"
description: "Average trust score is {{ $value }}. Review SBOM completeness and VEX coverage."
```
### AlertManager Configuration
```yaml
# alertmanager.yml
route:
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default-receiver'
routes:
- match:
severity: critical
receiver: 'pagerduty-critical'
- match:
alertname: ProvcacheSignerRevocations
receiver: 'security-team'
receivers:
- name: 'default-receiver'
slack_configs:
- channel: '#stellaops-alerts'
send_resolved: true
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: '<pagerduty-key>'
- name: 'security-team'
email_configs:
- to: 'security@example.com'
send_resolved: true
```
---
## Recording Rules
Pre-compute expensive queries for dashboard performance:
```yaml
# prometheus-rules.yml
groups:
- name: provcache-recording
interval: 30s
rules:
# Hit rate pre-computed
- record: provcache:hit_rate:5m
expr: |
sum(rate(provcache_requests_total{result="hit"}[5m])) /
sum(rate(provcache_requests_total[5m]))
# P95 latency pre-computed
- record: provcache:latency_p95:5m
expr: |
histogram_quantile(0.95, rate(provcache_latency_seconds_bucket{operation="get"}[5m]))
# Invalidation rate
- record: provcache:invalidation_rate:5m
expr: |
sum(rate(provcache_invalidations_total[5m]))
# Cache efficiency (hits per second vs misses)
- record: provcache:efficiency:5m
expr: |
sum(rate(provcache_hits_total[5m])) /
(sum(rate(provcache_hits_total[5m])) + sum(rate(provcache_misses_total[5m])))
```
---
## Operational Runbook
### Low Hit Rate Investigation
1. **Check invalidation metrics** — Is there an invalidation storm?
```promql
sum by (reason) (rate(provcache_invalidations_total[5m]))
```
2. **Check cache age** — Is the cache newly deployed (cold)?
```promql
sum(provcache_items_count)
```
3. **Check request patterns** — Are there many unique VeriKeys?
```promql
# High cardinality of unique requests suggests insufficient cache sharing
```
4. **Check TTL configuration** — Is TTL too aggressive?
- Review `Provcache:DefaultTtl` setting
- Consider increasing for stable workloads
### High Latency Investigation
1. **Check Valkey health**
```bash
redis-cli -h valkey info stats
```
2. **Check Postgres connections**
```sql
SELECT count(*) FROM pg_stat_activity WHERE datname = 'stellaops';
```
3. **Check entry sizes**
```promql
histogram_quantile(0.95, rate(provcache_entry_size_bytes_bucket[5m]))
```
4. **Check network latency** between services
### Invalidation Storm Response
1. **Identify cause**
```promql
sum by (reason) (increase(provcache_invalidations_total[10m]))
```
2. **If epoch-related**: Expected during feed updates. Monitor duration.
3. **If signer-related**: Security event — escalate to security team.
4. **If manual**: Check audit logs for unauthorized invalidation.
---
## Related Documentation
- [Provcache Module README](../provcache/README.md) — Core concepts
- [Provcache Architecture](../provcache/architecture.md) — Technical details
- [Telemetry Architecture](../telemetry/architecture.md) — Observability patterns
- [Grafana Dashboard Guide](../../deploy/grafana/README.md) — Dashboard management

View File

@@ -0,0 +1,439 @@
# Provcache OCI Attestation Verification Guide
This document describes how to verify Provcache decision attestations attached to OCI container images.
## Overview
StellaOps can attach provenance cache decisions as OCI-attached attestations to container images. These attestations enable:
- **Supply chain verification** — Verify security decisions were made by trusted evaluators
- **Audit trails** — Retrieve the exact decision state at image push time
- **Policy gates** — Admission controllers can verify attestations before deployment
- **Offline verification** — Decisions verifiable without calling StellaOps services
## Attestation Format
### Predicate Type
```
stella.ops/provcache@v1
```
### Predicate Schema
```json
{
"_type": "stella.ops/provcache@v1",
"veriKey": "sha256:abc123...",
"decision": {
"digestVersion": "v1",
"verdictHash": "sha256:def456...",
"proofRoot": "sha256:789abc...",
"trustScore": 85,
"createdAt": "2025-12-24T12:00:00Z",
"expiresAt": "2025-12-25T12:00:00Z"
},
"inputs": {
"sourceDigest": "sha256:image...",
"sbomDigest": "sha256:sbom...",
"policyDigest": "sha256:policy...",
"feedEpoch": "2024-W52"
},
"verdicts": {
"CVE-2024-1234": "mitigated",
"CVE-2024-5678": "affected"
}
}
```
### Field Descriptions
| Field | Type | Description |
|-------|------|-------------|
| `_type` | string | Predicate type URI |
| `veriKey` | string | VeriKey hash identifying this decision context |
| `decision.digestVersion` | string | Decision digest schema version |
| `decision.verdictHash` | string | Hash of all verdicts |
| `decision.proofRoot` | string | Merkle proof root hash |
| `decision.trustScore` | number | Overall trust score (0-100) |
| `decision.createdAt` | string | ISO-8601 creation timestamp |
| `decision.expiresAt` | string | ISO-8601 expiry timestamp |
| `inputs.sourceDigest` | string | Container image digest |
| `inputs.sbomDigest` | string | SBOM document digest |
| `inputs.policyDigest` | string | Policy bundle digest |
| `inputs.feedEpoch` | string | Feed epoch identifier |
| `verdicts` | object | Map of CVE IDs to verdict status |
---
## Verification with Cosign
### Prerequisites
```bash
# Install cosign
brew install cosign # macOS
# or
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
```
### Basic Verification
```bash
# Verify attestation exists and is signed
cosign verify-attestation \
--type stella.ops/provcache@v1 \
registry.example.com/app:v1.2.3
```
### Verify with Identity Constraints
```bash
# Verify with signer identity (Fulcio)
cosign verify-attestation \
--type stella.ops/provcache@v1 \
--certificate-identity-regexp '.*@stellaops\.example\.com' \
--certificate-oidc-issuer https://auth.stellaops.example.com \
registry.example.com/app:v1.2.3
```
### Verify with Custom Trust Root
```bash
# Using enterprise CA
cosign verify-attestation \
--type stella.ops/provcache@v1 \
--certificate /path/to/enterprise-ca.crt \
--certificate-chain /path/to/ca-chain.crt \
registry.example.com/app:v1.2.3
```
### Extract Attestation Payload
```bash
# Get raw attestation JSON
cosign verify-attestation \
--type stella.ops/provcache@v1 \
--certificate-identity-regexp '.*@stellaops\.example\.com' \
--certificate-oidc-issuer https://auth.stellaops.example.com \
registry.example.com/app:v1.2.3 | jq '.payload' | base64 -d | jq .
```
---
## Verification with StellaOps CLI
### Verify Attestation
```bash
# Verify using StellaOps CLI
stella verify attestation \
--image registry.example.com/app:v1.2.3 \
--type provcache
# Output:
# ✓ Attestation found: stella.ops/provcache@v1
# ✓ Signature valid (Fulcio)
# ✓ Trust score: 85
# ✓ Decision created: 2025-12-24T12:00:00Z
# ✓ Decision expires: 2025-12-25T12:00:00Z
```
### Verify with Policy Requirements
```bash
# Verify with minimum trust score
stella verify attestation \
--image registry.example.com/app:v1.2.3 \
--type provcache \
--min-trust-score 80
# Verify with freshness requirement
stella verify attestation \
--image registry.example.com/app:v1.2.3 \
--type provcache \
--max-age 24h
```
### Extract Decision Details
```bash
# Get full decision details
stella verify attestation \
--image registry.example.com/app:v1.2.3 \
--type provcache \
--output json | jq .
# Get specific fields
stella verify attestation \
--image registry.example.com/app:v1.2.3 \
--type provcache \
--output json | jq '.predicate.verdicts'
```
---
## Kubernetes Admission Control
### Gatekeeper Policy
```yaml
# constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: provcacheattestation
spec:
crd:
spec:
names:
kind: ProvcacheAttestation
validation:
openAPIV3Schema:
type: object
properties:
minTrustScore:
type: integer
minimum: 0
maximum: 100
maxAgeHours:
type: integer
minimum: 1
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package provcacheattestation
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
image := container.image
not has_valid_attestation(image)
msg := sprintf("Image %v missing valid provcache attestation", [image])
}
has_valid_attestation(image) {
attestation := get_attestation(image, "stella.ops/provcache@v1")
attestation.predicate.decision.trustScore >= input.parameters.minTrustScore
not is_expired(attestation.predicate.decision.expiresAt)
}
is_expired(expiry) {
time.parse_rfc3339_ns(expiry) < time.now_ns()
}
```
```yaml
# constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: ProvcacheAttestation
metadata:
name: require-provcache-attestation
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- production
parameters:
minTrustScore: 80
maxAgeHours: 48
```
### Kyverno Policy
```yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-provcache-attestation
spec:
validationFailureAction: enforce
background: true
rules:
- name: check-provcache-attestation
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "*"
attestations:
- predicateType: stella.ops/provcache@v1
conditions:
- all:
- key: "{{ decision.trustScore }}"
operator: GreaterThanOrEquals
value: 80
- key: "{{ decision.expiresAt }}"
operator: GreaterThan
value: "{{ time.Now() }}"
attestors:
- entries:
- keyless:
issuer: https://auth.stellaops.example.com
subject: ".*@stellaops\\.example\\.com"
```
---
## CI/CD Integration
### GitHub Actions
```yaml
# .github/workflows/verify-attestation.yml
name: Verify Provcache Attestation
on:
workflow_dispatch:
inputs:
image:
description: 'Image to verify'
required: true
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Verify attestation
run: |
cosign verify-attestation \
--type stella.ops/provcache@v1 \
--certificate-identity-regexp '.*@stellaops\.example\.com' \
--certificate-oidc-issuer https://auth.stellaops.example.com \
${{ inputs.image }}
- name: Check trust score
run: |
TRUST_SCORE=$(cosign verify-attestation \
--type stella.ops/provcache@v1 \
--certificate-identity-regexp '.*@stellaops\.example\.com' \
--certificate-oidc-issuer https://auth.stellaops.example.com \
${{ inputs.image }} | jq -r '.payload' | base64 -d | jq '.predicate.decision.trustScore')
if [ "$TRUST_SCORE" -lt 80 ]; then
echo "Trust score $TRUST_SCORE is below threshold (80)"
exit 1
fi
```
### GitLab CI
```yaml
# .gitlab-ci.yml
verify-attestation:
stage: verify
image: gcr.io/projectsigstore/cosign:latest
script:
- cosign verify-attestation
--type stella.ops/provcache@v1
--certificate-identity-regexp '.*@stellaops\.example\.com'
--certificate-oidc-issuer https://auth.stellaops.example.com
${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
rules:
- if: $CI_COMMIT_TAG
```
---
## Troubleshooting
### No Attestation Found
```bash
# List all attestations on image
cosign tree registry.example.com/app:v1.2.3
# Check if attestation was pushed
crane manifest registry.example.com/app:sha256-<digest>.att
```
### Signature Verification Failed
```bash
# Check certificate details
cosign verify-attestation \
--type stella.ops/provcache@v1 \
--output text \
registry.example.com/app:v1.2.3 2>&1 | grep -A5 "Certificate"
# Verify with verbose output
COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
--type stella.ops/provcache@v1 \
registry.example.com/app:v1.2.3 -v
```
### Attestation Expired
```bash
# Check expiry timestamp
cosign verify-attestation \
--type stella.ops/provcache@v1 \
--certificate-identity-regexp '.*@stellaops\.example\.com' \
--certificate-oidc-issuer https://auth.stellaops.example.com \
registry.example.com/app:v1.2.3 | \
jq -r '.payload' | base64 -d | jq '.predicate.decision.expiresAt'
```
### Trust Score Below Threshold
```bash
# Check trust score breakdown
stella verify attestation \
--image registry.example.com/app:v1.2.3 \
--type provcache \
--output json | jq '.predicate.decision.trustScore'
# If score is low, check individual components:
# - SBOM completeness
# - VEX coverage
# - Reachability analysis
# - Policy freshness
# - Signer trust
```
---
## Security Considerations
### Key Management
- **Fulcio** — Ephemeral certificates tied to OIDC identity; recommended for public workflows
- **Enterprise CA** — Long-lived certificates for air-gapped environments
- **Self-signed** — Only for development/testing; not recommended for production
### Attestation Integrity
- Attestations are signed at push time
- Signature covers the entire predicate payload
- Modifying any field invalidates the signature
### Expiry Handling
- Attestations have `expiresAt` timestamps
- Expired attestations should be rejected by admission controllers
- Consider re-scanning images before deployment to get fresh attestations
### Verdict Reconciliation
- Verdicts in attestation reflect state at push time
- New vulnerabilities discovered after push won't appear
- Use `stella verify attestation --check-freshness` to compare against current feeds
---
## Related Documentation
- [Provcache Module README](./README.md) — Core concepts
- [Provcache Metrics and Alerting](./metrics-alerting.md) — Observability
- [Signer Module](../signer/architecture.md) — Signing infrastructure
- [Attestor Module](../attestor/architecture.md) — Attestation generation
- [OCI Artifact Spec](https://github.com/opencontainers/image-spec) — OCI standards
- [In-toto Attestation Spec](https://github.com/in-toto/attestation) — Attestation format
- [Sigstore Documentation](https://docs.sigstore.dev/) — Cosign and Fulcio

View File

@@ -108,6 +108,200 @@ Scanner analyses container images layer-by-layer, producing deterministic SBOM f
- Platform events rollout with scanner.report.ready@1 and scanner.scan.completed@1
- Surface-cache environment resolution with startup validation
## Gating Explainability (Quiet-by-Design Triage)
The Scanner WebService exposes gating explainability through the triage APIs to support the "Quiet-by-Design" UX pattern where noise is gated at the source and proof is surfaced with one click.
### Gating Reasons
Findings can be hidden by default based on:
| Gating Reason | Description |
|---------------|-------------|
| `unreachable` | Not reachable from any application entrypoint |
| `policy_dismissed` | Waived or tolerated by policy rules |
| `backported` | Patched via distro backport |
| `vex_not_affected` | VEX statement declares not affected with sufficient trust |
| `superseded` | Superseded by newer advisory |
| `user_muted` | Explicitly muted by user |
### Key DTOs
- `FindingTriageStatusDto` - Extended with `GatingReason`, `IsHiddenByDefault`, `SubgraphId`, `DeltasId`, `GatingExplanation`
- `TriageVexStatusDto` - Includes `TrustScore`, `PolicyTrustThreshold`, `MeetsPolicyThreshold`, `TrustBreakdown`
- `GatedBucketsSummaryDto` - Counts of hidden findings by gating reason for chip display
- `BulkTriageQueryResponseDto` - Includes `GatedBuckets` and `ActionableCount`
### VEX Trust Scoring
VEX statements are evaluated against a policy-defined trust threshold (default 0.8). The trust score is computed from:
- **Authority** (0-1): Issuer reputation and category
- **Accuracy** (0-1): Historical correctness
- **Timeliness** (0-1): Response speed
- **Verification** (0-1): Signature validity
When `TrustScore >= PolicyTrustThreshold`, the VEX not_affected claim gates the finding.
### Unified Evidence Endpoint
`GET /v1/triage/findings/{findingId}/evidence` returns all evidence tabs in one call:
- SBOM reference and component metadata
- Reachability subgraph with call paths
- VEX claims with trust scores
- Attestation summaries
- Delta comparison
- Policy evaluation results
- Manifest hashes for verification
- Replay command for deterministic reproduction
### Evidence Bundle Export
`GET /v1/triage/findings/{findingId}/evidence/export` returns a downloadable archive containing:
- `MANIFEST.json` - Bundle manifest with hashes
- `finding-status.json` - Triage status
- `proof-bundle.json` - Proof bundle
- `replay-command.json` - Replay command info
- `replay.sh` / `replay.ps1` - Replay scripts
- `README.md` - Human-readable documentation
### Replay Command Generation
The `IReplayCommandService` generates copy-ready CLI commands:
```bash
stella scan replay --artifact sha256:abc... --manifest sha256:def... --feeds sha256:ghi... --policy sha256:jkl...
```
For offline replay: `stella scan replay --offline --artifact ... --verify-inputs`
### UI Wireframes
#### Gated Buckets Summary
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Gated Findings Summary │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌────────────────────────┐ │
│ │ 12 actionable │ (96 hidden) │
│ └────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ +42 │ │ +15 │ │ +8 │ │ +23 │ │
│ │ unreachable │ │ policy │ │ backported │ │ VEX │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ +3 │ │ +5 │ │ [Show all] │ │
│ │ superseded │ │ muted │ └─────────────────┘ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
#### VEX Trust Display
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ VEX Status: not_affected │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Trust Score: ████████░░ 0.85 │
│ Threshold: ──────── 0.80 ✓ Meets policy │
│ │
│ Issuer: vendor.example │
│ Justification: vulnerable_code_not_in_execute_path │
│ │
│ ┌─ Trust Breakdown ─────────────────────────────────────────────────┐ │
│ │ Authority: ██████████░ 0.90 │ │
│ │ Accuracy: ████████░░░ 0.85 │ │
│ │ Timeliness: ████████░░░ 0.80 │ │
│ │ Verification: ████████░░░ 0.85 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
#### Replay Command Component
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Replay Command │
│ Reproduce this verdict deterministically │
├──────────┬─────────┬─────────────────────────────────────────────────────────┤
│ [Full] │ Short │ Offline │
├──────────┴─────────┴─────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ $ stella scan replay \ │ │
│ │ --artifact sha256:a1b2c3d4e5f6... \ │ │
│ │ --manifest sha256:def456... \ │ │
│ │ --feeds sha256:feed789... \ │ │
│ │ --policy sha256:policy321... │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ ┌───────────────┐ │
│ │ 📋 Copy │ │
│ └───────────────┘ │
├─────────────────────────────────────────────────────────────────────────────┤
│ 📦 Download Evidence Bundle 12.5 KB · ZIP │
├─────────────────────────────────────────────────────────────────────────────┤
│ Expected verdict hash: sha256:verdict123... │
└─────────────────────────────────────────────────────────────────────────────┘
```
#### Gating Explainer Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Finding: CVE-2024-1234 │
│ lodash@4.17.15 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Status: Hidden by default [Why hidden?] ←─┐ │
│ │ │
│ ┌───────────────┴───────────────────────────┐ │
│ │ Why is this finding hidden? │ │
│ ├───────────────────────────────────────────┤ │
│ │ │ │
│ │ This finding is gated because: │ │
│ │ │ │
│ │ ✓ VEX not_affected (trust: 0.85) │ │
│ │ Vendor issued not_affected statement │ │
│ │ with justification: │ │
│ │ "vulnerable_code_not_in_execute_path" │ │
│ │ │ │
│ │ Evidence: │ │
│ │ • VEX document: vex-vendor-2025-001 │ │
│ │ • Issued: 2025-12-15T10:00:00Z │ │
│ │ • Signature: ✓ Valid (ES256) │ │
│ │ │ │
│ │ [View Evidence] [Close] │ │
│ └───────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
#### Evidence Bundle Contents
```
evidence-f-abc123/
├── manifest.json ← Archive manifest with SHA-256 hashes
├── README.md ← Human-readable documentation
├── sbom.cdx.json ← CycloneDX SBOM slice
├── reachability.json ← Reachability analysis
├── vex/
│ ├── vendor.json ← Vendor VEX statement
│ ├── nvd.json ← NVD data
│ └── cisa-kev.json ← CISA KEV flag
├── attestations/
│ ├── sbom.dsse.json ← SBOM DSSE envelope
│ └── scan.dsse.json ← Scan DSSE envelope
├── policy/
│ └── evaluation.json ← Policy evaluation result
├── delta.json ← Delta comparison
├── replay-command.txt ← Copy-ready CLI command
├── replay.sh ← Bash replay script
└── replay.ps1 ← PowerShell replay script
```
See Sprint 9200.0001.0001-0004 for implementation details.
## Epic alignment
- **Epic 6 Vulnerability Explorer:** provide policy-aware scan outputs, explain traces, and findings ledger hooks for triage workflows.
- **Epic 10 Export Center:** generate export-ready artefacts, manifests, and DSSE metadata for bundles.

View File

@@ -0,0 +1,461 @@
# Provcache UI Components
This document describes the Angular components for visualizing provenance cache data, trust scores, and proof trees in the StellaOps Console.
## Overview
The Provcache UI components provide visual feedback for:
- **Cache state awareness** — Users see whether decisions come from cache (fast) or are freshly computed
- **Trust transparency** — Trust scores with breakdowns by evidence component
- **Proof visualization** — Interactive proof trees showing evidence chain
- **Input manifest** — Details of all inputs that form a cached decision
## Components
### ProvenanceBadgeComponent
**Selector:** `stellaops-provenance-badge`
Displays a compact badge indicating the provenance state of a decision.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `state` | `'cached' \| 'computed' \| 'stale' \| 'unknown'` | `'unknown'` | Current provenance state |
| `cacheDetails` | `CacheDetails \| null` | `null` | Optional cache metadata |
| `trustScore` | `number \| null` | `null` | Optional trust score (0-100) |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Badge size variant |
#### Outputs
| Output | Type | Description |
|--------|------|-------------|
| `viewProofTree` | `EventEmitter<void>` | Emitted when user clicks to view proof |
#### States & Icons
| State | Icon | Tooltip | CSS Class |
|-------|------|---------|-----------|
| `cached` | ⚡ | "Provenance-cached" | `badge--cached` |
| `computed` | 🔄 | "Freshly computed" | `badge--computed` |
| `stale` | ⏳ | "Stale - recomputing" | `badge--stale` |
| `unknown` | ❓ | "Unknown provenance" | `badge--unknown` |
#### Usage
```html
<!-- Basic usage -->
<stellaops-provenance-badge [state]="'cached'"></stellaops-provenance-badge>
<!-- With cache details -->
<stellaops-provenance-badge
[state]="'cached'"
[cacheDetails]="{ source: 'valkey', ageSeconds: 45, veriKey: 'sha256:abc...' }"
[trustScore]="85"
(viewProofTree)="openProofTree()">
</stellaops-provenance-badge>
```
#### Accessibility
- Uses `role="status"` for screen reader announcements
- Tooltip content exposed via `aria-label`
- Color is not the sole indicator — icons and text labels provided
---
### TrustScoreDisplayComponent
**Selector:** `stellaops-trust-score-display`
Visualizes a trust score (0-100) with optional breakdown by evidence component.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `score` | `number` | `0` | Overall trust score (0-100) |
| `breakdown` | `TrustScoreBreakdown \| null` | `null` | Component-level scores |
| `mode` | `'donut' \| 'badge' \| 'inline'` | `'donut'` | Display mode |
| `showBreakdown` | `boolean` | `false` | Show breakdown tooltip |
| `compact` | `boolean` | `false` | Use compact sizing |
| `thresholds` | `{ high: number; medium: number }` | `{ high: 80, medium: 50 }` | Color thresholds |
#### TrustScoreBreakdown Interface
```typescript
interface TrustScoreBreakdown {
reachability: number; // 0.0-1.0 (25% weight)
sbomCompleteness: number; // 0.0-1.0 (20% weight)
vexCoverage: number; // 0.0-1.0 (20% weight)
policyFreshness: number; // 0.0-1.0 (15% weight)
signerTrust: number; // 0.0-1.0 (20% weight)
}
```
#### Display Modes
| Mode | Description |
|------|-------------|
| `donut` | Circular SVG chart with score in center |
| `badge` | Compact pill-shaped badge |
| `inline` | Text-only display for tight spaces |
#### Color Coding
| Score Range | Class | Color |
|-------------|-------|-------|
| ≥ high (80) | `score--high` | Green (#4caf50) |
| ≥ medium (50) | `score--medium` | Yellow (#ff9800) |
| < medium | `score--low` | Red (#f44336) |
#### Usage
```html
<!-- Donut chart with breakdown -->
<stellaops-trust-score-display
[score]="85"
[breakdown]="breakdown"
[mode]="'donut'"
[showBreakdown]="true">
</stellaops-trust-score-display>
<!-- Compact badge -->
<stellaops-trust-score-display
[score]="72"
[mode]="'badge'"
[compact]="true">
</stellaops-trust-score-display>
```
---
### ProofTreeComponent
**Selector:** `stellaops-proof-tree`
Renders a decision's proof chain as a collapsible tree with verification capabilities.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `digest` | `DecisionDigest \| null` | `null` | The decision digest to display |
| `merkleTree` | `MerkleTree \| null` | `null` | Optional Merkle tree structure |
| `verdicts` | `VerdictEntry[]` | `[]` | Vulnerability verdicts |
| `evidenceChunks` | `EvidenceChunk[]` | `[]` | Evidence artifacts |
| `isVerifying` | `boolean` | `false` | Show verification spinner |
#### Outputs
| Output | Type | Description |
|--------|------|-------------|
| `copyVeriKey` | `EventEmitter<string>` | VeriKey copied to clipboard |
| `copyHash` | `EventEmitter<string>` | Any hash copied |
| `verify` | `EventEmitter<void>` | Verify button clicked |
| `downloadEvidence` | `EventEmitter<EvidenceChunk>` | Evidence download requested |
#### Tree Structure
```
DecisionDigest
├── VeriKey: sha256:abc123...
├── Trust Score: 85
│ ├── Reachability: 95%
│ ├── SBOM Completeness: 85%
│ ├── VEX Coverage: 90%
│ ├── Policy Freshness: 88%
│ └── Signer Trust: 92%
├── Verdicts (12)
│ ├── CVE-2025-1234 → not_affected
│ ├── CVE-2025-5678 → fixed
│ └── ...
├── Merkle Tree
│ ├── Root: sha256:root...
│ ├── Left
│ │ ├── sha256:sbom... (sbom-hash)
│ │ └── sha256:vex... (vex-set-hash)
│ └── Right
│ ├── sha256:policy... (policy-hash)
│ └── sha256:signers... (signers-hash)
└── Evidence Chunks (5)
├── [sbom] SPDX 2.3 SBOM document
├── [vex] VEX statement bundle
└── ...
```
#### Verdict Statuses
| Status | Color | Meaning |
|--------|-------|---------|
| `not_affected` | Green | Vulnerability not applicable |
| `fixed` | Blue | Patched/remediated |
| `affected` | Red | Vulnerable |
| `under_investigation` | Yellow | Pending analysis |
| `mitigated` | Cyan | Mitigating controls in place |
#### Usage
```html
<stellaops-proof-tree
[digest]="decisionDigest"
[merkleTree]="merkleTree"
[verdicts]="verdicts"
[evidenceChunks]="chunks"
[isVerifying]="verifying"
(verify)="verifyProof()"
(copyVeriKey)="copyToClipboard($event)"
(downloadEvidence)="downloadChunk($event)">
</stellaops-proof-tree>
```
---
### InputManifestComponent
**Selector:** `stellaops-input-manifest`
Displays the exact inputs that formed a VeriKey and cached decision.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `manifest` | `InputManifest \| null` | `null` | The manifest to display |
| `mode` | `'full' \| 'compact' \| 'summary'` | `'full'` | Display mode |
| `displayConfig` | `InputManifestDisplayConfig` | (all true) | Section visibility |
#### Outputs
| Output | Type | Description |
|--------|------|-------------|
| `copyVeriKey` | `EventEmitter<string>` | VeriKey copied |
| `refresh` | `EventEmitter<void>` | Refresh requested |
#### InputManifest Interface
```typescript
interface InputManifest {
veriKey: string;
sourceArtifact: SourceArtifactInfo;
sbom: SbomInfo;
vex: VexInfo;
policy: PolicyInfoManifest;
signers: SignerInfo;
timeWindow: TimeWindowInfo;
generatedAt: string;
}
interface SourceArtifactInfo {
digest: string;
artifactType: string;
ociReference?: string;
sizeBytes?: number;
}
interface SbomInfo {
hash: string;
format: string; // 'spdx-2.3', 'cyclonedx-1.6'
packageCount: number;
completenessScore: number;
createdAt: string;
}
interface VexInfo {
hashSetHash: string;
statementCount: number;
sources: string[];
latestStatementAt: string;
}
interface PolicyInfoManifest {
hash: string;
packId: string;
version: number;
lastUpdatedAt: string;
name?: string;
}
interface SignerInfo {
setHash: string;
signerCount: number;
certificates?: CertificateInfo[];
}
interface CertificateInfo {
subject: string;
issuer: string;
fingerprint: string;
expiresAt: string;
trustLevel: 'fulcio' | 'enterprise-ca' | 'self-signed';
}
interface TimeWindowInfo {
bucket: string;
startsAt: string;
endsAt: string;
}
```
#### Display Modes
| Mode | Description |
|------|-------------|
| `full` | All sections with full details |
| `compact` | Reduced spacing, abbreviated values |
| `summary` | Grid layout for quick overview |
#### Section Visibility
```typescript
interface InputManifestDisplayConfig {
showSource: boolean;
showSbom: boolean;
showVex: boolean;
showPolicy: boolean;
showSigners: boolean;
showTimeWindow: boolean;
}
```
#### Usage
```html
<!-- Full manifest display -->
<stellaops-input-manifest
[manifest]="inputManifest"
[mode]="'full'"
(copyVeriKey)="copyToClipboard($event)">
</stellaops-input-manifest>
<!-- Security-focused view -->
<stellaops-input-manifest
[manifest]="inputManifest"
[mode]="'compact'"
[displayConfig]="{ showSource: false, showSbom: false, showVex: true, showPolicy: true, showSigners: true, showTimeWindow: false }">
</stellaops-input-manifest>
```
---
## Storybook Documentation
All components have Storybook stories in `src/Web/StellaOps.Web/src/stories/provcache/`:
| File | Description |
|------|-------------|
| `provenance-badge.stories.ts` | All badge states, trust scores, sizes |
| `trust-score-display.stories.ts` | Score ranges, modes, breakdowns |
| `input-manifest.stories.ts` | Modes, SBOM formats, certificates |
| `proof-tree.stories.ts` | Tree depths, verdicts, verification |
Run Storybook to explore:
```bash
cd src/Web/StellaOps.Web
npm run storybook
```
---
## Integration Examples
### Finding Row Integration
```typescript
// finding-row.component.ts
@Component({
selector: 'stellaops-finding-row',
template: `
<div class="finding-row">
<stellaops-provenance-badge
[state]="finding.provenanceState"
[cacheDetails]="finding.cacheDetails"
[trustScore]="finding.trustScore"
(viewProofTree)="openProofPanel.emit(finding)">
</stellaops-provenance-badge>
<!-- ... rest of finding row -->
</div>
`
})
export class FindingRowComponent {
@Input() finding!: FindingRow;
@Output() openProofPanel = new EventEmitter<FindingRow>();
}
```
### Decision Detail Panel
```typescript
// decision-detail-panel.component.ts
@Component({
template: `
<div class="decision-detail-panel">
<stellaops-trust-score-display
[score]="digest.trustScore"
[breakdown]="digest.trustScoreBreakdown"
[mode]="'donut'"
[showBreakdown]="true">
</stellaops-trust-score-display>
<stellaops-proof-tree
[digest]="digest"
[merkleTree]="merkleTree"
[verdicts]="verdicts"
[evidenceChunks]="chunks"
[isVerifying]="verifying$ | async"
(verify)="verifyProof()"
(downloadEvidence)="downloadChunk($event)">
</stellaops-proof-tree>
<stellaops-input-manifest
[manifest]="manifest"
[mode]="'full'">
</stellaops-input-manifest>
</div>
`
})
export class DecisionDetailPanelComponent {
// ...
}
```
---
## Theming
Components use CSS custom properties for theming:
```css
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
--trust-score-high: #81c784;
--trust-score-medium: #ffb74d;
--trust-score-low: #e57373;
--badge-cached-bg: #1b5e20;
--badge-computed-bg: #0d47a1;
}
```
---
## Accessibility
All components follow WCAG 2.1 AA guidelines:
- **Keyboard navigation** All interactive elements focusable and operable
- **Screen reader support** ARIA labels, roles, and live regions
- **Color independence** Icons and text supplement color coding
- **Focus indicators** Visible focus outlines on interactive elements
- **Motion preferences** Reduced motion respected where applicable
---
## Related Documentation
- [Provcache Module README](../provcache/README.md) Core concepts and architecture
- [Provcache Architecture](../provcache/architecture.md) Technical deep-dive
- [UI Architecture](./architecture.md) Angular patterns and state management
- [Accessibility Guide](../../accessibility.md) WCAG compliance guidelines