save development progress
This commit is contained in:
278
docs/modules/concelier/federation-bundle-export.md
Normal file
278
docs/modules/concelier/federation-bundle-export.md
Normal 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
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
438
docs/modules/provcache/architecture.md
Normal file
438
docs/modules/provcache/architecture.md
Normal 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.
|
||||
419
docs/modules/provcache/metrics-alerting.md
Normal file
419
docs/modules/provcache/metrics-alerting.md
Normal 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
|
||||
439
docs/modules/provcache/oci-attestation-verification.md
Normal file
439
docs/modules/provcache/oci-attestation-verification.md
Normal 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
|
||||
@@ -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.
|
||||
|
||||
461
docs/modules/ui/provcache-components.md
Normal file
461
docs/modules/ui/provcache-components.md
Normal 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
|
||||
Reference in New Issue
Block a user