- Add RpmVersionComparer for RPM version comparison with epoch, version, and release handling. - Introduce DebianVersion for parsing Debian EVR (Epoch:Version-Release) strings. - Create ApkVersion for parsing Alpine APK version strings with suffix support. - Define IVersionComparator interface for version comparison with proof-line generation. - Implement VersionComparisonResult struct to encapsulate comparison results and proof lines. - Add tests for Debian and RPM version comparers to ensure correct functionality and edge case handling. - Create project files for the version comparison library and its tests.
463 lines
11 KiB
Markdown
463 lines
11 KiB
Markdown
# Verdict Manifest Specification
|
|
|
|
> **Status**: Draft (Sprint 7100)
|
|
> **Last Updated**: 2025-12-22
|
|
> **Source Advisory**: `docs/product-advisories/archived/22-Dec-2026 - Building a Trust Lattice for VEX Sources.md`
|
|
|
|
## 1. Overview
|
|
|
|
A Verdict Manifest is a signed, immutable record of a VEX decisioning outcome. It captures all inputs used to produce a verdict, enabling deterministic replay and audit compliance.
|
|
|
|
### Purpose
|
|
|
|
1. **Auditability**: Prove exactly how a verdict was reached
|
|
2. **Reproducibility**: Replay the decision with identical results
|
|
3. **Compliance**: Meet regulatory requirements for security decisions
|
|
4. **Debugging**: Investigate unexpected verdict changes
|
|
|
|
---
|
|
|
|
## 2. Manifest Schema
|
|
|
|
### 2.1 Complete Schema
|
|
|
|
```typescript
|
|
interface VerdictManifest {
|
|
// Identity
|
|
manifestId: string; // Unique identifier
|
|
tenant: string; // Tenant scope
|
|
|
|
// Scope
|
|
assetDigest: string; // SHA256 of asset/SBOM
|
|
vulnerabilityId: string; // CVE/GHSA/vendor ID
|
|
|
|
// Inputs (pinned for replay)
|
|
inputs: VerdictInputs;
|
|
|
|
// Result
|
|
result: VerdictResult;
|
|
|
|
// Policy context
|
|
policyHash: string; // SHA256 of policy file
|
|
latticeVersion: string; // Trust lattice version
|
|
|
|
// Metadata
|
|
evaluatedAt: string; // ISO 8601 UTC timestamp
|
|
manifestDigest: string; // SHA256 of canonical manifest
|
|
}
|
|
|
|
interface VerdictInputs {
|
|
sbomDigests: string[]; // SBOM document digests
|
|
vulnFeedSnapshotIds: string[]; // Feed snapshot identifiers
|
|
vexDocumentDigests: string[]; // VEX document digests
|
|
reachabilityGraphIds: string[]; // Call graph identifiers
|
|
clockCutoff: string; // Evaluation timestamp
|
|
}
|
|
|
|
interface VerdictResult {
|
|
status: "affected" | "not_affected" | "fixed" | "under_investigation";
|
|
confidence: number; // 0.0 to 1.0
|
|
explanations: VerdictExplanation[];
|
|
evidenceRefs: string[]; // Attestation/proof references
|
|
}
|
|
|
|
interface VerdictExplanation {
|
|
sourceId: string;
|
|
reason: string;
|
|
provenanceScore: number;
|
|
coverageScore: number;
|
|
replayabilityScore: number;
|
|
strengthMultiplier: number;
|
|
freshnessMultiplier: number;
|
|
claimScore: number;
|
|
}
|
|
```
|
|
|
|
### 2.2 Identity Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `manifestId` | string | Format: `verd:{tenant}:{asset_short}:{vuln_id}:{timestamp}` |
|
|
| `tenant` | string | Tenant identifier for multi-tenancy |
|
|
|
|
### 2.3 Input Pinning
|
|
|
|
All inputs that affect the verdict must be pinned for deterministic replay:
|
|
|
|
| Field | Description |
|
|
|-------|-------------|
|
|
| `sbomDigests` | SHA256 digests of SBOM documents used |
|
|
| `vulnFeedSnapshotIds` | Identifiers for vulnerability feed snapshots |
|
|
| `vexDocumentDigests` | SHA256 digests of VEX documents considered |
|
|
| `reachabilityGraphIds` | Identifiers for call graph snapshots |
|
|
| `clockCutoff` | Timestamp used for freshness calculations |
|
|
|
|
### 2.4 Verdict Result
|
|
|
|
The result section contains the actual verdict and full explanation:
|
|
|
|
| Field | Description |
|
|
|-------|-------------|
|
|
| `status` | Final verdict: affected, not_affected, fixed, under_investigation |
|
|
| `confidence` | Numeric confidence score (0.0 to 1.0) |
|
|
| `explanations` | Per-source breakdown of scoring |
|
|
| `evidenceRefs` | Links to attestations and proof bundles |
|
|
|
|
---
|
|
|
|
## 3. Deterministic Serialization
|
|
|
|
### 3.1 Canonical JSON
|
|
|
|
Manifests are serialized using canonical JSON rules:
|
|
|
|
1. Keys sorted alphabetically (ASCII order)
|
|
2. No insignificant whitespace
|
|
3. UTF-8 encoding without BOM
|
|
4. Timestamps in ISO 8601 format with 'Z' suffix
|
|
5. Arrays sorted by natural key (sourceId, then score)
|
|
6. Numbers without trailing zeros
|
|
|
|
### 3.2 Digest Computation
|
|
|
|
The manifest digest is computed over the canonical JSON:
|
|
|
|
```csharp
|
|
public static string ComputeDigest(VerdictManifest manifest)
|
|
{
|
|
var json = CanonicalJsonSerializer.Serialize(manifest with { ManifestDigest = "" });
|
|
var bytes = Encoding.UTF8.GetBytes(json);
|
|
var hash = SHA256.HashData(bytes);
|
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Signing
|
|
|
|
### 4.1 DSSE Envelope
|
|
|
|
Verdict manifests are signed using [DSSE (Dead Simple Signing Envelope)](https://github.com/secure-systems-lab/dsse):
|
|
|
|
```json
|
|
{
|
|
"payloadType": "application/vnd.stellaops.verdict+json",
|
|
"payload": "<base64-encoded-manifest>",
|
|
"signatures": [
|
|
{
|
|
"keyid": "projects/stellaops/keys/verdict-signer-2025",
|
|
"sig": "<base64-encoded-signature>"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 4.2 Predicate Type
|
|
|
|
For in-toto attestations:
|
|
|
|
```
|
|
https://stella-ops.org/attestations/vex-verdict/1
|
|
```
|
|
|
|
### 4.3 Rekor Integration
|
|
|
|
Optionally, verdicts can be logged to Sigstore Rekor for transparency:
|
|
|
|
```json
|
|
{
|
|
"rekorLogId": "rekor.sigstore.dev",
|
|
"rekorLogIndex": 12345678,
|
|
"rekorEntryUrl": "https://rekor.sigstore.dev/api/v1/log/entries/..."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Storage
|
|
|
|
### 5.1 PostgreSQL Schema
|
|
|
|
```sql
|
|
CREATE TABLE authority.verdict_manifests (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
manifest_id TEXT NOT NULL UNIQUE,
|
|
tenant TEXT NOT NULL,
|
|
|
|
-- Scope
|
|
asset_digest TEXT NOT NULL,
|
|
vulnerability_id TEXT NOT NULL,
|
|
|
|
-- Inputs (JSONB)
|
|
inputs_json JSONB NOT NULL,
|
|
|
|
-- Result
|
|
status TEXT NOT NULL,
|
|
confidence DOUBLE PRECISION NOT NULL,
|
|
result_json JSONB NOT NULL,
|
|
|
|
-- Policy context
|
|
policy_hash TEXT NOT NULL,
|
|
lattice_version TEXT NOT NULL,
|
|
|
|
-- Metadata
|
|
evaluated_at TIMESTAMPTZ NOT NULL,
|
|
manifest_digest TEXT NOT NULL,
|
|
|
|
-- Signature
|
|
signature_json JSONB,
|
|
rekor_log_id TEXT,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
### 5.2 Indexing Strategy
|
|
|
|
| Index | Purpose |
|
|
|-------|---------|
|
|
| `(tenant, asset_digest, vulnerability_id)` | Primary lookup |
|
|
| `(tenant, policy_hash, lattice_version)` | Policy version queries |
|
|
| `(evaluated_at)` BRIN | Time-based queries |
|
|
| Unique: `(tenant, asset_digest, vulnerability_id, policy_hash, lattice_version)` | Ensure one verdict per configuration |
|
|
|
|
---
|
|
|
|
## 6. Replay Verification
|
|
|
|
### 6.1 Verification Protocol
|
|
|
|
1. Retrieve stored manifest by ID
|
|
2. Fetch pinned inputs (SBOM, VEX docs, feeds) by digest
|
|
3. Re-execute trust lattice evaluation with identical inputs
|
|
4. Compare result with stored verdict
|
|
5. Verify signature if present
|
|
|
|
### 6.2 Verification Request
|
|
|
|
```http
|
|
POST /api/v1/authority/verdicts/{manifestId}/replay
|
|
Authorization: Bearer <token>
|
|
```
|
|
|
|
### 6.3 Verification Response
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"originalManifest": { ... },
|
|
"replayedManifest": { ... },
|
|
"differences": [],
|
|
"signatureValid": true,
|
|
"verifiedAt": "2025-12-22T15:30:00Z"
|
|
}
|
|
```
|
|
|
|
### 6.4 Failure Handling
|
|
|
|
When replay produces different results:
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"originalManifest": { ... },
|
|
"replayedManifest": { ... },
|
|
"differences": [
|
|
{
|
|
"field": "result.confidence",
|
|
"original": 0.82,
|
|
"replayed": 0.79,
|
|
"reason": "VEX document digest mismatch"
|
|
}
|
|
],
|
|
"signatureValid": true,
|
|
"error": "Verdict replay produced different confidence score"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. API Reference
|
|
|
|
### Get Verdict Manifest
|
|
|
|
```http
|
|
GET /api/v1/authority/verdicts/{manifestId}
|
|
Authorization: Bearer <token>
|
|
|
|
Response: 200 OK
|
|
{
|
|
"manifest": { ... },
|
|
"signature": { ... },
|
|
"rekorEntry": { ... }
|
|
}
|
|
```
|
|
|
|
### List Verdicts by Scope
|
|
|
|
```http
|
|
GET /api/v1/authority/verdicts?assetDigest={digest}&vulnerabilityId={cve}
|
|
Authorization: Bearer <token>
|
|
|
|
Response: 200 OK
|
|
{
|
|
"verdicts": [ ... ],
|
|
"pageToken": "..."
|
|
}
|
|
```
|
|
|
|
### Replay Verdict
|
|
|
|
```http
|
|
POST /api/v1/authority/verdicts/{manifestId}/replay
|
|
Authorization: Bearer <token>
|
|
|
|
Response: 200 OK
|
|
{
|
|
"success": true,
|
|
...
|
|
}
|
|
```
|
|
|
|
### Download Signed Manifest
|
|
|
|
```http
|
|
GET /api/v1/authority/verdicts/{manifestId}/download
|
|
Authorization: Bearer <token>
|
|
|
|
Response: 200 OK
|
|
Content-Type: application/vnd.stellaops.verdict+json
|
|
Content-Disposition: attachment; filename="verdict-{manifestId}.json"
|
|
```
|
|
|
|
---
|
|
|
|
## 8. JSON Schema
|
|
|
|
### verdict-manifest.schema.json
|
|
|
|
```json
|
|
{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://stella-ops.org/schemas/verdict-manifest/1.0.0",
|
|
"type": "object",
|
|
"required": [
|
|
"manifestId",
|
|
"tenant",
|
|
"assetDigest",
|
|
"vulnerabilityId",
|
|
"inputs",
|
|
"result",
|
|
"policyHash",
|
|
"latticeVersion",
|
|
"evaluatedAt",
|
|
"manifestDigest"
|
|
],
|
|
"properties": {
|
|
"manifestId": {
|
|
"type": "string",
|
|
"pattern": "^verd:[a-z0-9-]+:[a-f0-9]+:[A-Z0-9-]+:[0-9]+$"
|
|
},
|
|
"tenant": { "type": "string", "minLength": 1 },
|
|
"assetDigest": {
|
|
"type": "string",
|
|
"pattern": "^sha256:[a-f0-9]{64}$"
|
|
},
|
|
"vulnerabilityId": { "type": "string", "minLength": 1 },
|
|
"inputs": { "$ref": "#/$defs/VerdictInputs" },
|
|
"result": { "$ref": "#/$defs/VerdictResult" },
|
|
"policyHash": {
|
|
"type": "string",
|
|
"pattern": "^sha256:[a-f0-9]{64}$"
|
|
},
|
|
"latticeVersion": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
|
},
|
|
"evaluatedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"manifestDigest": {
|
|
"type": "string",
|
|
"pattern": "^sha256:[a-f0-9]{64}$"
|
|
}
|
|
},
|
|
"$defs": {
|
|
"VerdictInputs": {
|
|
"type": "object",
|
|
"required": ["sbomDigests", "vulnFeedSnapshotIds", "vexDocumentDigests", "clockCutoff"],
|
|
"properties": {
|
|
"sbomDigests": {
|
|
"type": "array",
|
|
"items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
|
|
},
|
|
"vulnFeedSnapshotIds": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"vexDocumentDigests": {
|
|
"type": "array",
|
|
"items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
|
|
},
|
|
"reachabilityGraphIds": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"clockCutoff": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
}
|
|
}
|
|
},
|
|
"VerdictResult": {
|
|
"type": "object",
|
|
"required": ["status", "confidence", "explanations"],
|
|
"properties": {
|
|
"status": {
|
|
"type": "string",
|
|
"enum": ["affected", "not_affected", "fixed", "under_investigation"]
|
|
},
|
|
"confidence": {
|
|
"type": "number",
|
|
"minimum": 0,
|
|
"maximum": 1
|
|
},
|
|
"explanations": {
|
|
"type": "array",
|
|
"items": { "$ref": "#/$defs/VerdictExplanation" }
|
|
},
|
|
"evidenceRefs": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"VerdictExplanation": {
|
|
"type": "object",
|
|
"required": ["sourceId", "reason", "claimScore"],
|
|
"properties": {
|
|
"sourceId": { "type": "string" },
|
|
"reason": { "type": "string" },
|
|
"provenanceScore": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
"coverageScore": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
"replayabilityScore": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
"strengthMultiplier": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
"freshnessMultiplier": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
"claimScore": { "type": "number", "minimum": 0, "maximum": 1 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Trust Lattice Specification](../excititor/trust-lattice.md)
|
|
- [Authority Architecture](./architecture.md)
|
|
- [DSSE Signing](../../dev/dsse-signing.md)
|
|
- [API Reference](../../09_API_CLI_REFERENCE.md)
|