feat: Add VEX compact fixture and implement offline verifier for Findings Ledger exports
- Introduced a new VEX compact fixture for testing purposes. - Implemented `verify_export.py` script to validate Findings Ledger exports, ensuring deterministic ordering and applying redaction manifests. - Added a lightweight stub `HarnessRunner` for unit tests to validate ledger hashing expectations. - Documented tasks related to the Mirror Creator. - Created models for entropy signals and implemented the `EntropyPenaltyCalculator` to compute penalties based on scanner outputs. - Developed unit tests for `EntropyPenaltyCalculator` to ensure correct penalty calculations and handling of edge cases. - Added tests for symbol ID normalization in the reachability scanner. - Enhanced console status service with comprehensive unit tests for connection handling and error recovery. - Included Cosign tool version 2.6.0 with checksums for various platforms.
This commit is contained in:
@@ -9,6 +9,7 @@ _Frozen v1 (add-only) — approved 2025-11-17 for CONCELIER-LNM-21-001/002/101._
|
||||
|
||||
## Status
|
||||
- Frozen v1 as of 2025-11-17; further schema changes must go through ADR + sprint gating (CONCELIER-LNM-22x+).
|
||||
- Canonical JSON Schemas + signed manifest live in `docs/modules/concelier/schemas/` (advisory observation, linkset, offline bundle). Verify with `openssl dgst -sha256 -verify schema-signing-pub.pem -signature schema.manifest.sig schema.manifest.json`.
|
||||
|
||||
## Observation document (Mongo JSON Schema excerpt)
|
||||
```json
|
||||
|
||||
32
docs/modules/concelier/schemas/README.md
Normal file
32
docs/modules/concelier/schemas/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Concelier schema bundle (CI1–CI10 remediation)
|
||||
|
||||
This folder publishes the signed JSON Schemas for Link-Not-Merge ingestion artifacts and the offline bundle manifest used by Offline Kit builds.
|
||||
|
||||
- `advisory-observation.schema.json` — canonical observation shape (provenance + content hash enforced).
|
||||
- `advisory-linkset.schema.json` — linkset materialization with conflict reasons and deterministic IDs.
|
||||
- `offline-advisory-bundle.schema.json` — manifest for air-gapped advisory bundles, including staleness and signature metadata.
|
||||
- `schema.manifest.json` — digest manifest over all schemas.
|
||||
- `schema.manifest.sig` — detached ECDSA (P-256) signature over the manifest (public key: `schema-signing-pub.pem`).
|
||||
- `schema.manifest.sig.b64` — base64 view of the signature for air-gapped copy/paste.
|
||||
- `samples/` — deterministic sample payloads for CI fixtures (see `tests` notes below).
|
||||
|
||||
## Verify locally (deterministic, offline)
|
||||
|
||||
```bash
|
||||
# 1) Validate schemas are unchanged
|
||||
sha256sum -c schema.manifest.json
|
||||
|
||||
# 2) Verify detached signature with the published public key
|
||||
openssl dgst -sha256 -verify schema-signing-pub.pem \
|
||||
-signature schema.manifest.sig \
|
||||
schema.manifest.json
|
||||
```
|
||||
|
||||
## Test coverage
|
||||
|
||||
The fixtures in `samples/` are consumed by `StellaOps.Concelier.Core.Tests` to assert:
|
||||
- deterministic idempotency keys and conflict ordering (`Linksets/AdvisoryLinksetIdempotencyTests`),
|
||||
- tenant normalization and signature requirements for observations (`Aoc/AdvisoryObservationWriteGuardTests`),
|
||||
- offline bundle manifest validation (`Schemas/OfflineBundleSchemaTests`).
|
||||
|
||||
Keep the manifest and signature updated whenever schema files change. Keys are dev/test-only; production signing happens in the release pipeline.
|
||||
85
docs/modules/concelier/schemas/advisory-linkset.schema.json
Normal file
85
docs/modules/concelier/schemas/advisory-linkset.schema.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.local/concelier/schemas/advisory-linkset.schema.json",
|
||||
"title": "Concelier Advisory Linkset",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"linksetId",
|
||||
"tenantId",
|
||||
"advisoryId",
|
||||
"source",
|
||||
"observationIds",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"linksetId": { "type": "string", "pattern": "^sha256:[A-Fa-f0-9]{64}$" },
|
||||
"tenantId": { "type": "string", "minLength": 1 },
|
||||
"source": { "type": "string", "minLength": 1 },
|
||||
"advisoryId": { "type": "string", "minLength": 1 },
|
||||
"observationIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true,
|
||||
"minItems": 1
|
||||
},
|
||||
"normalized": {
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"provenance": {
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"observationHashes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "pattern": "^sha256:[A-Fa-f0-9]{64}$" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"toolVersion": { "type": "string" },
|
||||
"policyHash": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 1 },
|
||||
"conflicts": {
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["field", "reason"],
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"severity-mismatch",
|
||||
"affected-range-divergence",
|
||||
"reference-clash",
|
||||
"alias-inconsistency",
|
||||
"metadata-gap",
|
||||
"statement-conflict"
|
||||
]
|
||||
},
|
||||
"sourceIds": {
|
||||
"type": ["array", "null"],
|
||||
"items": { "type": "string" },
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"aliases": {
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"primary": { "type": "string" },
|
||||
"others": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
||||
}
|
||||
},
|
||||
"purls": { "type": ["array", "null"], "items": { "type": "string" }, "uniqueItems": true },
|
||||
"cpes": { "type": ["array", "null"], "items": { "type": "string" }, "uniqueItems": true },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": ["string", "null"], "format": "date-time" },
|
||||
"builtByJobId": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
163
docs/modules/concelier/schemas/advisory-observation.schema.json
Normal file
163
docs/modules/concelier/schemas/advisory-observation.schema.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.local/concelier/schemas/advisory-observation.schema.json",
|
||||
"title": "Concelier Advisory Observation (Link-Not-Merge)",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"observationId",
|
||||
"tenant",
|
||||
"source",
|
||||
"upstream",
|
||||
"content",
|
||||
"linkset",
|
||||
"rawLinkset",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"observationId": { "type": "string", "minLength": 1 },
|
||||
"tenant": { "type": "string", "minLength": 1, "pattern": "^[a-z0-9:-]+$" },
|
||||
"source": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["vendor", "stream", "api"],
|
||||
"properties": {
|
||||
"vendor": { "type": "string", "minLength": 1 },
|
||||
"stream": { "type": "string", "minLength": 1 },
|
||||
"api": { "type": "string", "format": "uri" },
|
||||
"collectorVersion": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"upstream": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"upstreamId",
|
||||
"fetchedAt",
|
||||
"receivedAt",
|
||||
"contentHash",
|
||||
"signature"
|
||||
],
|
||||
"properties": {
|
||||
"upstreamId": { "type": "string", "minLength": 1 },
|
||||
"documentVersion": { "type": "string" },
|
||||
"fetchedAt": { "type": "string", "format": "date-time" },
|
||||
"receivedAt": { "type": "string", "format": "date-time" },
|
||||
"contentHash": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[A-Fa-f0-9]{64}$"
|
||||
},
|
||||
"signature": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["present"],
|
||||
"properties": {
|
||||
"present": { "type": "boolean" },
|
||||
"format": { "type": "string" },
|
||||
"keyId": { "type": "string" },
|
||||
"signature": { "type": "string" }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "present": { "const": true } } },
|
||||
"then": {
|
||||
"required": ["format", "keyId", "signature"],
|
||||
"properties": {
|
||||
"format": { "minLength": 1 },
|
||||
"keyId": { "minLength": 1 },
|
||||
"signature": { "minLength": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "present": { "const": false } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"format": { "maxLength": 0 },
|
||||
"keyId": { "maxLength": 0 },
|
||||
"signature": { "maxLength": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"propertyNames": { "pattern": "^[A-Za-z0-9_.:-]+$" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["format", "raw"],
|
||||
"properties": {
|
||||
"format": { "type": "string", "minLength": 1 },
|
||||
"specVersion": { "type": "string" },
|
||||
"raw": { "type": ["object", "array"] },
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"propertyNames": { "pattern": "^[A-Za-z0-9_.:-]+$" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"linkset": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"aliases": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"purls": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"cpes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"references": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "url"],
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"url": { "type": "string", "format": "uri" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"reconciledFrom": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
||||
}
|
||||
},
|
||||
"rawLinkset": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"aliases": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"packageUrls": { "type": "array", "items": { "type": "string" } },
|
||||
"cpes": { "type": "array", "items": { "type": "string" } },
|
||||
"references": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
},
|
||||
"required": ["type", "url"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"relationships": { "type": "array", "items": { "type": "object" } },
|
||||
"reconciledFrom": { "type": "array", "items": { "type": "string" } },
|
||||
"scopes": { "type": "array", "items": { "type": "string" } },
|
||||
"notes": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"propertyNames": { "pattern": "^[A-Za-z0-9_.:-]+$" }
|
||||
},
|
||||
"createdAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.local/concelier/schemas/offline-advisory-bundle.schema.json",
|
||||
"title": "Concelier Offline Advisory Bundle",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"bundleId",
|
||||
"tenant",
|
||||
"exportKind",
|
||||
"snapshot",
|
||||
"manifest",
|
||||
"hashes",
|
||||
"signatures",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"bundleId": { "type": "string", "pattern": "^bundle:[A-Za-z0-9._:-]+$" },
|
||||
"tenant": { "type": "string", "minLength": 1 },
|
||||
"exportKind": { "type": "string", "enum": ["json", "trivydb"] },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"snapshot": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["windowStart", "windowEnd", "sources"],
|
||||
"properties": {
|
||||
"windowStart": { "type": "string", "format": "date-time" },
|
||||
"windowEnd": { "type": "string", "format": "date-time" },
|
||||
"stalenessHours": { "type": "integer", "minimum": 0 },
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "cursor", "hash"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"cursor": { "type": "string" },
|
||||
"hash": { "type": "string", "pattern": "^sha256:[A-Fa-f0-9]{64}$" },
|
||||
"snapshotUri": { "type": "string", "format": "uri" }
|
||||
}
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"manifest": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path", "sha256", "size"],
|
||||
"properties": {
|
||||
"path": { "type": "string" },
|
||||
"sha256": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" },
|
||||
"size": { "type": "integer", "minimum": 0 },
|
||||
"contentType": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"hashes": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^sha256$": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }
|
||||
}
|
||||
},
|
||||
"signatures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "keyId", "signature"],
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["dsse-inline", "detached"] },
|
||||
"keyId": { "type": "string" },
|
||||
"signature": { "type": "string" },
|
||||
"envelopeDigest": { "type": "string", "pattern": "^sha256:[A-Fa-f0-9]{64}$" },
|
||||
"rekor": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"logIndex": { "type": "integer", "minimum": 0 },
|
||||
"uuid": { "type": "string" },
|
||||
"integratedTime": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"determinism": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"contentHash": { "type": "string", "pattern": "^sha256:[A-Fa-f0-9]{64}$" },
|
||||
"idempotencyKey": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"canonVersion": { "type": "string", "default": "1" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"$schema": "../offline-advisory-bundle.schema.json",
|
||||
"bundleId": "bundle:concelier:offline:2025-12-02T00-00Z",
|
||||
"tenant": "default",
|
||||
"exportKind": "json",
|
||||
"createdAt": "2025-12-02T00:00:00Z",
|
||||
"snapshot": {
|
||||
"windowStart": "2025-11-25T00:00:00Z",
|
||||
"windowEnd": "2025-12-01T23:59:59Z",
|
||||
"stalenessHours": 168,
|
||||
"sources": [
|
||||
{
|
||||
"name": "osv",
|
||||
"cursor": "2025-12-01T23:50:00Z",
|
||||
"hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
|
||||
"snapshotUri": "https://mirror.example/offline/osv-2025-12-01.zip"
|
||||
},
|
||||
{
|
||||
"name": "redhat",
|
||||
"cursor": "2025-12-01T23:45:00Z",
|
||||
"hash": "sha256:abcd456789abcdef0123456789abcdef0123456789abcdef0123456789abcd"
|
||||
}
|
||||
]
|
||||
},
|
||||
"manifest": [
|
||||
{
|
||||
"path": "export/index.json",
|
||||
"sha256": "89abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567",
|
||||
"size": 482192,
|
||||
"contentType": "application/json"
|
||||
},
|
||||
{
|
||||
"path": "export/db/trivy.db",
|
||||
"sha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
|
||||
"size": 1289932,
|
||||
"contentType": "application/octet-stream"
|
||||
}
|
||||
],
|
||||
"hashes": {
|
||||
"sha256": "0f0e0d0c0b0a09080706050403020100ffeeddccbbaa99887766554433221100"
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"type": "dsse-inline",
|
||||
"keyId": "schema-offline-pub",
|
||||
"signature": "MEUCIQDkexampleSignedDigestx+deterministicSig==",
|
||||
"envelopeDigest": "sha256:aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55"
|
||||
}
|
||||
],
|
||||
"determinism": {
|
||||
"contentHash": "sha256:d3c3f6c75c6a3f0906bcee457cc77a2d6d7c0f9d1a1d7da78c0d2ab8e0dba111",
|
||||
"idempotencyKey": "29d58b9fdc5c4e65b26c03f3bd9f442ff0c7f8514b8a9225f8b6417ffabc0101",
|
||||
"canonVersion": "1"
|
||||
}
|
||||
}
|
||||
4
docs/modules/concelier/schemas/schema-signing-pub.pem
Normal file
4
docs/modules/concelier/schemas/schema-signing-pub.pem
Normal file
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyi7gVscxgRXQzX5ErNuQFN3dPjVw
|
||||
YzU0JE3PGhjSinBwpODxtweLfP6zw2N6f0H9z25t8HwTpFeuk1PWqTX7Gg==
|
||||
-----END PUBLIC KEY-----
|
||||
22
docs/modules/concelier/schemas/schema.manifest.json
Normal file
22
docs/modules/concelier/schemas/schema.manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generatedAt": "2025-12-02T00:00:00Z",
|
||||
"files": [
|
||||
{
|
||||
"path": "advisory-observation.schema.json",
|
||||
"sha256": "e3f40aea09794f72f2722c46657377518489e2ca7e3122cfbb65655c3296c083"
|
||||
},
|
||||
{
|
||||
"path": "advisory-linkset.schema.json",
|
||||
"sha256": "e3b40a0cca5aff85be2fbc5af9a96f00f5b7a20f6740a3f32947fae56bd599e5"
|
||||
},
|
||||
{
|
||||
"path": "offline-advisory-bundle.schema.json",
|
||||
"sha256": "9b64af7c2e5fa0c071af7dc04b7984fd1787b4f9e2082cb47174610097e2dc51"
|
||||
},
|
||||
{
|
||||
"path": "samples/offline-advisory-bundle.sample.json",
|
||||
"sha256": "15874bbafe5b2ead5ec9a853c32d715a4b48d41107ff2887d6ccdc222e462f45"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
docs/modules/concelier/schemas/schema.manifest.sig
Normal file
BIN
docs/modules/concelier/schemas/schema.manifest.sig
Normal file
Binary file not shown.
2
docs/modules/concelier/schemas/schema.manifest.sig.b64
Normal file
2
docs/modules/concelier/schemas/schema.manifest.sig.b64
Normal file
@@ -0,0 +1,2 @@
|
||||
MEUCIBDcyrpqWmYQUrkWLTwMs6QyG2YWCFTxte10/7TobThlAiEAvqOSESmIxNFQ6pDtHlhpfL1K
|
||||
1SZrDM+PhdAMSOMwoU4=
|
||||
@@ -2,3 +2,9 @@
|
||||
# Findings Ledger
|
||||
|
||||
Start here for ledger docs.
|
||||
|
||||
## Quick links
|
||||
- FL1–FL10 remediation tracker: `gaps-FL1-FL10.md`
|
||||
- Schema catalog (events/projections/exports): `schema-catalog.md`
|
||||
- Merkle & external anchor policy: `merkle-anchor-policy.md`
|
||||
- Tenant isolation & redaction manifest: `tenant-isolation-redaction.md`
|
||||
|
||||
26
docs/modules/findings-ledger/dsse-policy-linkage.md
Normal file
26
docs/modules/findings-ledger/dsse-policy-linkage.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# DSSE & Policy Hash Linkage (FL6)
|
||||
|
||||
**Goal:** Every export, replay report, and anchor manifest is tied to the exact policy digest that produced it and is verifiable offline via DSSE.
|
||||
|
||||
## Binding rules
|
||||
1. **Policy digest:** `policyVersion` (SHA-256 over policy bundle) is mandatory in ledger events, projections, exports, and replay reports.
|
||||
2. **DSSE payload types**
|
||||
- `application/vnd.stella-ledger-export+json` — export manifests (hashlist + filtersHash).
|
||||
- `application/vnd.stella-ledger-anchor+json` — Merkle anchors (see `merkle-anchor-policy.md`).
|
||||
- `application/vnd.stella-ledger-harness+json` — replay harness report.
|
||||
3. **Hashlists:** export manifests contain `sha256` for each emitted NDJSON line (`lineDigest`), plus a dataset digest (`datasetSha256`) over concatenated line digests. Replay harness exposes `eventStreamChecksum` and `projectionChecksum`.
|
||||
4. **Policy linkage:** DSSE payload must include `policyHash` and `schemaVersion` to prevent replay under mismatched policy versions.
|
||||
|
||||
## Offline verification flow
|
||||
1. Verify DSSE signature (local key or Rekor transparency log if online).
|
||||
2. Recompute dataset checksum with `tools/LedgerReplayHarness/scripts/verify_export.py --input <export.ndjson> --expected <datasetSha256>`.
|
||||
3. Cross-check `policyHash` in payload matches policy bundle in use; mismatch → block import/export.
|
||||
|
||||
## File locations
|
||||
- Harness DSSE placeholder now embeds `policyHash` when `LEDGER_POLICY_HASH` env var is set.
|
||||
- Export manifests and checksums: `docs/modules/findings-ledger/golden-checksums.json`.
|
||||
- External anchors: `docs/modules/findings-ledger/merkle-anchor-policy.md` (DSSE template).
|
||||
- Set `LEDGER_POLICY_HASH` before running `tools/LedgerReplayHarness` to imprint the policy digest into the generated `.sig` file.
|
||||
|
||||
## Change management
|
||||
- Any change to payloadType or hash recipe bumps schema version in `schema-catalog.md` and requires new DSSE key roll announcement.
|
||||
28
docs/modules/findings-ledger/gaps-FL1-FL10.md
Normal file
28
docs/modules/findings-ledger/gaps-FL1-FL10.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Findings Ledger — FL1–FL10 Remediation (LEDGER-GAPS-121-009)
|
||||
|
||||
**Source advisory:** `docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Findings Ledger and Immutable Audit Trail.md`
|
||||
**Created:** 2025-12-02 · **Owner:** Findings Ledger Guild
|
||||
|
||||
## Gap closure map
|
||||
|
||||
| ID | Gap summary | Remediation artefact(s) | Evidence / notes |
|
||||
| --- | ----------- | ----------------------- | ---------------- |
|
||||
| FL1 | Versioned ledger event schema (canonical JSON + hashes) | `docs/modules/findings-ledger/schema-catalog.md` §1; updated `docs/modules/findings-ledger/schema.md` canonical rules | Canonical envelope `v1.0.0` stamped; hash derivation pinned to `sha256(canonicalJson)` + `sha256(eventHash-sequence)`. |
|
||||
| FL2 | Projection schema versions + cycle hash determinism | `schema-catalog.md` §2; `schema.md` §4 | Projection `v1.0.0` with cycle-hash recipe and required fields; rebuild checksum guard in harness. |
|
||||
| FL3 | Export schema (canonical/compact) + filter hash versioning | `schema-catalog.md` §3; golden fixtures under `src/Findings/StellaOps.Findings.Ledger/fixtures/golden/` | Canonical export shape tagged `export.v1.canonical`; compact tagged `export.v1.compact`; fixtures hashed. |
|
||||
| FL4 | Merkle + external anchor policy (Rekor/offline) | `docs/modules/findings-ledger/merkle-anchor-policy.md` | Anchoring cadence (1k/15m), Rekor/air-gap policy, anchor ref format, DSSE anchoring manifest. |
|
||||
| FL5 | Tenant isolation + redaction manifest for exports/logs | `docs/modules/findings-ledger/tenant-isolation-redaction.md`; manifest: `docs/modules/findings-ledger/redaction-manifest.yaml` | Per-tenant partitions, export field redaction (comments, actor ids), signed manifest checksum. |
|
||||
| FL6 | DSSE + policy hash linkage for exports and attestations | `docs/modules/findings-ledger/dsse-policy-linkage.md`; harness DSSE placeholder includes `policyHash` | Describes payloadType + bindings to policy digest and export hashlist. |
|
||||
| FL7 | Deterministic export fixtures (golden) | `fixtures/golden/*.ndjson` (findings, vex, advisories, sboms) | Each includes `filtersHash`, `cycleHash`, `policyVersion`; hashes logged in manifest. |
|
||||
| FL8 | Offline verifier script for bundles/exports | `tools/LedgerReplayHarness/scripts/verify_export.py` | Pure-Python, no deps; validates ordering, recomputes SHA-256 and optional expected hash file. |
|
||||
| FL9 | Replay/rebuild checksum guard | Harness update: `tools/LedgerReplayHarness/Program.cs` (`--expected-checksum`) | Computes event-stream and projection checksums; fails on mismatch; emitted in report. |
|
||||
| FL10 | Quotas/backpressure metrics and alerts | Metrics update: `Observability/LedgerMetrics.cs`; doc: `observability.md` §2/§4 | New counters `ledger_backpressure_applied_total`, gauge `ledger_quota_remaining`, alert guidance. |
|
||||
|
||||
## How to verify
|
||||
- Run `dotnet run --project tools/LedgerReplayHarness -- --fixture <path> --connection <conn> --tenant <tenant> --report out/report.json --metrics out/metrics.json --expected-checksum <baseline-checksums.json>` (use a file produced by a known-good run; template: `docs/modules/findings-ledger/replay-checksums.sample.json`).
|
||||
- Validate exports: `python tools/LedgerReplayHarness/scripts/verify_export.py --input fixtures/golden/findings-canonical.ndjson --schema export.v1.canonical`.
|
||||
- Check manifest hashes: `sha256sum docs/modules/findings-ledger/redaction-manifest.yaml fixtures/golden/*.ndjson`.
|
||||
|
||||
## Follow-ons
|
||||
- Integrate Rekor anchor publishing toggle into Helm/Compose overlays (tracked separately).
|
||||
- Mirror golden fixtures into Offline Kit once export pipeline emits real data.
|
||||
53
docs/modules/findings-ledger/golden-checksums.json
Normal file
53
docs/modules/findings-ledger/golden-checksums.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"generatedAt": "2025-12-02T00:00:00Z",
|
||||
"policyHash": "sha256:policy-v1",
|
||||
"datasets": {
|
||||
"findings-canonical.ndjson": {
|
||||
"path": "src/Findings/StellaOps.Findings.Ledger/fixtures/golden/findings-canonical.ndjson",
|
||||
"schema": "export.v1.canonical",
|
||||
"records": 2,
|
||||
"filtersHash": "a81d6c6d2bcf9c0e7cbb1fcd292e4b7cc21f6d5c4e3f2b1a0c9d8e7f6c5b4a3e",
|
||||
"sha256": "cd270235484748f2f4c871e9d574796e6f61b48df9cc65e009dab4ba0769dfa4"
|
||||
},
|
||||
"vex-compact.ndjson": {
|
||||
"path": "src/Findings/StellaOps.Findings.Ledger/fixtures/golden/vex-compact.ndjson",
|
||||
"schema": "export.v1.compact",
|
||||
"records": 1,
|
||||
"filtersHash": "b5c6d7e8f9a0b1c2d3e4f50617283940aa5544332211ffeeccbb998877665544",
|
||||
"sha256": "e786a12b4ee08776df73f7f2a97907280b5f8bb76cc7a901e2a680d3fe69e85e"
|
||||
},
|
||||
"advisories-canonical.ndjson": {
|
||||
"path": "src/Findings/StellaOps.Findings.Ledger/fixtures/golden/advisories-canonical.ndjson",
|
||||
"schema": "export.v1.canonical",
|
||||
"records": 1,
|
||||
"filtersHash": "c6d7e8f9a0b1c2d3e4f50617283940aa5544332211ffeeccbb99887766554433",
|
||||
"sha256": "6d5a2d522179b616c112c255c7dd06b3434ae0a4992009d25ea82f50144425ab"
|
||||
},
|
||||
"sboms-compact.ndjson": {
|
||||
"path": "src/Findings/StellaOps.Findings.Ledger/fixtures/golden/sboms-compact.ndjson",
|
||||
"schema": "export.v1.compact",
|
||||
"records": 1,
|
||||
"filtersHash": "d7e8f9a0b1c2d3e4f50617283940aa5544332211ffeeccbb9988776655443322",
|
||||
"sha256": "c89be7fcc511c4ef5a4a291c45061da1a7f4592506150e5b9bce92ba2bb5bbe2"
|
||||
}
|
||||
},
|
||||
"manifests": {
|
||||
"redaction-manifest.yaml": {
|
||||
"path": "docs/modules/findings-ledger/redaction-manifest.yaml",
|
||||
"schema": "redaction.v1",
|
||||
"sha256": "7c2f437a47c6514ad4688072b8b5e33b2e0cd0f9f289f15b49bf2f7def54a730"
|
||||
},
|
||||
"redaction-manifest.json": {
|
||||
"path": "docs/modules/findings-ledger/redaction-manifest.json",
|
||||
"schema": "redaction.v1",
|
||||
"sha256": "6965ea311f65482e6f51da0fd26cae1995997fcd456cea6dac84ab7b3354990a"
|
||||
}
|
||||
},
|
||||
"replay": {
|
||||
"sample": {
|
||||
"path": "docs/modules/findings-ledger/replay-checksums.sample.json",
|
||||
"schema": "ledger.harness.v1",
|
||||
"note": "replace with harness-produced checksums before enforcement"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
docs/modules/findings-ledger/merkle-anchor-policy.md
Normal file
50
docs/modules/findings-ledger/merkle-anchor-policy.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Merkle & External Anchor Policy (FL4)
|
||||
|
||||
**Audience:** Findings Ledger Guild · DevOps · Compliance
|
||||
**Applies to:** `src/Findings/StellaOps.Findings.Ledger` (Merkle worker, anchoring jobs)
|
||||
|
||||
## Anchoring cadence
|
||||
- **Batch size:** 1,000 events or **15 minutes**, whichever is first (`LedgerServiceOptions:Merkle.BatchSize/WindowDuration`).
|
||||
- **Tree:** flat Merkle over `merkle_leaf_hash` (see `schema-catalog.md` §1). Root hashed with SHA-256; no salt.
|
||||
- **Partitions:** per-tenant batching only; no cross-tenant mixing.
|
||||
- **Ordering:** leaves ordered by `(sequence_no, recorded_at)`. Any deviation is a failure.
|
||||
|
||||
## Anchor references
|
||||
- `ledger_merkle_roots.anchor_reference` formats:
|
||||
- `rekor::<uuid>` when pushed to Rekor.
|
||||
- `airgap::<bundleId>` when sealed in offline bundle.
|
||||
- `none` (empty) for internal-only anchors.
|
||||
- External publication is optional but **must** include DSSE envelope with payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"payloadType": "application/vnd.stella-ledger-anchor+json",
|
||||
"payload": {
|
||||
"tenant": "<tenant>",
|
||||
"rootHash": "<sha256>",
|
||||
"leafCount": 1000,
|
||||
"windowStart": "2025-12-02T00:00:00Z",
|
||||
"windowEnd": "2025-12-02T00:15:00Z",
|
||||
"policyHash": "<policyVersion>",
|
||||
"schemaVersion": "ledger.event.v1"
|
||||
},
|
||||
"signatures": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Determinism & recovery
|
||||
- Anchor worker enforces stable ordering; replay harness recomputes Merkle roots and fails when root mismatch (FL9 guard).
|
||||
- Root hash + DSSE signature are stored alongside export bundles for offline verification.
|
||||
- External anchors **never** include tenant-identifying data beyond tenant id already present in ledger tables.
|
||||
|
||||
## Air-gap posture
|
||||
- Rekor publication optional; when disabled, anchors are sealed inside offline bundles with `anchor_reference=airgap::<bundleId>`.
|
||||
- Anchor manifest is bundled in Offline Kit under `offline/ledger/anchors/<tenant>/<anchorId>.json`.
|
||||
- No outbound network calls when `ExternalAnchoring:Enabled=false`.
|
||||
|
||||
## Monitoring
|
||||
- Metrics: `ledger_merkle_anchor_duration_seconds`, `ledger_merkle_anchor_failures_total`, `ledger_backpressure_applied_total{reason="anchoring"}`, `ledger_quota_remaining{kind="ingest"}`.
|
||||
- Alerts: see `observability.md` (AnchorFailure + new Backpressure alert).
|
||||
|
||||
## Change control
|
||||
- Any change to batch size/window or hash recipe requires bumping `ledger.event` schema minor version and updating `schema-catalog.md`.
|
||||
@@ -14,7 +14,10 @@
|
||||
| --- | --- | --- | --- |
|
||||
| `ledger_write_duration_seconds` | Histogram | `tenant`, `event_type`, `source` | End-to-end append latency (API ingress → persisted). P95 ≤ 120 ms. |
|
||||
| `ledger_events_total` | Counter | `tenant`, `event_type`, `source` (`policy`, `workflow`, `orchestrator`) | Incremented per committed event. Mirrors Merkle leaf count. |
|
||||
| `ledger_ingest_backlog_events` | Gauge | — | Number of events buffered in the writer/anchor queues. Alert when >5 000 for 5 min. |
|
||||
| `ledger_ingest_backlog_events` | Gauge | `tenant` | Number of events buffered in the writer/anchor queues. Alert when >5 000 for 5 min. |
|
||||
| `ledger_quota_remaining` | Gauge | `tenant` | Remaining ingest capacity before backpressure applies (defaults to 5 000 events). |
|
||||
| `ledger_backpressure_applied_total` | Counter | `tenant`, `reason`, `limit` | Incremented whenever backlog crosses quota threshold. |
|
||||
| `ledger_quota_rejections_total` | Counter | `tenant`, `reason` | Incremented when requests are actively rejected due to quotas. |
|
||||
| `ledger_projection_lag_seconds` | Gauge | `tenant` | Wall-clock difference between latest ledger event and projection tail. Target <30 s. |
|
||||
| `ledger_projection_rebuild_seconds` | Histogram | `tenant` | Duration of replay/rebuild operations triggered by LEDGER-29-008 harness. |
|
||||
| `ledger_projection_apply_seconds` | Histogram | `tenant`, `event_type`, `policy_version`, `evaluation_status` | Time to apply a single ledger event to projection. Target P95 <1 s. |
|
||||
@@ -43,6 +46,7 @@
|
||||
| --- | --- | --- |
|
||||
| **LedgerWriteSLA** | `ledger_write_latency_seconds` P95 > 1 s for 3 intervals | Check DB contention, review queue backlog, scale writer. |
|
||||
| **LedgerBacklogGrowing** | `ledger_ingest_backlog_events` > 5 000 for 5 min | Inspect upstream policy runs, ensure projector keeping up. |
|
||||
| **LedgerBackpressure** | `ledger_backpressure_applied_total` increases while `ledger_quota_remaining` < 0 | Throttle callers, raise quota or scale anchor worker. |
|
||||
| **ProjectionLag** | `ledger_projection_lag_seconds` > 30 s | Trigger rebuild, verify change streams. |
|
||||
| **AnchorFailure** | `ledger_merkle_anchor_failures_total` increase > 0 | Collect logs, rerun anchor, verify signing service. |
|
||||
| **AttachmentSecurityError** | `ledger_attachments_encryption_failures_total` increase > 0 | Audit attachments pipeline; check key material and storage endpoints. |
|
||||
|
||||
29
docs/modules/findings-ledger/redaction-manifest.json
Normal file
29
docs/modules/findings-ledger/redaction-manifest.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schemaVersion": "redaction.v1",
|
||||
"generatedAt": "2025-12-02T00:00:00Z",
|
||||
"owner": "findings-ledger-guild",
|
||||
"rules": {
|
||||
"ledger.event": [
|
||||
{ "path": "$.actor.id", "action": "mask", "maskWith": "user:<realm>" },
|
||||
{ "path": "$.payload.comment", "action": "drop" },
|
||||
{ "path": "$.payload.ticket.url", "action": "drop" },
|
||||
{ "path": "$.payload.attachments[*].downloadUrl", "action": "drop" }
|
||||
],
|
||||
"export.canonical": [
|
||||
{ "path": "$.actorId", "action": "mask", "maskWith": "user:<realm>" },
|
||||
{ "path": "$.comment", "action": "drop" },
|
||||
{ "path": "$.attachments", "action": "drop" }
|
||||
],
|
||||
"export.compact": [
|
||||
{ "path": "$.actorId", "action": "drop" },
|
||||
{ "path": "$.comment", "action": "drop" },
|
||||
{ "path": "$.policyRationale", "action": "drop" },
|
||||
{ "path": "$.attachments", "action": "drop" },
|
||||
{ "path": "$.labels", "action": "drop" }
|
||||
],
|
||||
"observability": [
|
||||
{ "path": "$.event_body", "action": "drop" },
|
||||
{ "path": "$.actor_id", "action": "hash", "hashWith": "sha256" }
|
||||
]
|
||||
}
|
||||
}
|
||||
39
docs/modules/findings-ledger/redaction-manifest.yaml
Normal file
39
docs/modules/findings-ledger/redaction-manifest.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
schemaVersion: redaction.v1
|
||||
generatedAt: 2025-12-02T00:00:00Z
|
||||
owner: findings-ledger-guild
|
||||
rules:
|
||||
ledger.event:
|
||||
- path: $.actor.id
|
||||
action: mask
|
||||
maskWith: "user:<realm>"
|
||||
- path: $.payload.comment
|
||||
action: drop
|
||||
- path: $.payload.ticket.url
|
||||
action: drop
|
||||
- path: $.payload.attachments[*].downloadUrl
|
||||
action: drop
|
||||
export.canonical:
|
||||
- path: $.actorId
|
||||
action: mask
|
||||
maskWith: "user:<realm>"
|
||||
- path: $.comment
|
||||
action: drop
|
||||
- path: $.attachments
|
||||
action: drop
|
||||
export.compact:
|
||||
- path: $.actorId
|
||||
action: drop
|
||||
- path: $.comment
|
||||
action: drop
|
||||
- path: $.policyRationale
|
||||
action: drop
|
||||
- path: $.attachments
|
||||
action: drop
|
||||
- path: $.labels
|
||||
action: drop
|
||||
observability:
|
||||
- path: $.event_body
|
||||
action: drop
|
||||
- path: $.actor_id
|
||||
action: hash
|
||||
hashWith: sha256
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"eventStream": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"projection": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"notes": "Replace with real values from harness output before enforcing checksum guard."
|
||||
}
|
||||
75
docs/modules/findings-ledger/schema-catalog.md
Normal file
75
docs/modules/findings-ledger/schema-catalog.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Findings Ledger Schema Catalog (FL1–FL3)
|
||||
|
||||
**Scope:** Versioned canonical schemas for ledger events, projections, and exports.
|
||||
**Status:** v1.0.0 sealed (2025-12-02) — breaking changes require new minor/major version tags.
|
||||
|
||||
## 1) Ledger event envelope — `ledger.event.v1`
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `event.id` | `uuid` | V7 GUID allowed. |
|
||||
| `event.type` | `string` (`ledger_event_type`) | See `schema.md` §2.2. |
|
||||
| `event.tenant` | `string` | Partition key. |
|
||||
| `event.chainId` | `uuid` | Derived when absent (`tenant :: policyVersion`), see `workflow-inference.md`. |
|
||||
| `event.sequence` | `long` | Gapless per chain, starts at 1. |
|
||||
| `event.policyVersion` | `string` | SHA-256 digest of policy bundle; propagated into exports and DSSE. |
|
||||
| `event.finding` | object | `id`, `artifactId`, `vulnId`. |
|
||||
| `event.actor` | object | `id`, `type` (`system|operator|integration`). |
|
||||
| `event.occurredAt` | `string` (UTC ISO-8601 ms) | Domain clock. |
|
||||
| `event.recordedAt` | `string` (UTC ISO-8601 ms) | Service `TimeProvider`. |
|
||||
| `event.payload` | object | Mutation-specific body. |
|
||||
| `event.evidenceBundleRef` | `string?` | DSSE/capsule id (optional). |
|
||||
| `event.airgap.bundle` | object? | See `airgap-provenance.md`. |
|
||||
| `event_hash` | `char(64)` | `sha256(canonicalJson)` lower-hex. |
|
||||
| `previous_hash` | `char(64)` | All-zero for chain genesis. |
|
||||
| `merkle_leaf_hash` | `char(64)` | `sha256(event_hash || "-" || sequence)`. |
|
||||
|
||||
Canonicalisation: UTF-8, sorted keys, lower-case enums, ISO-8601 UTC with millisecond precision, arrays stable-order. Any field addition bumps minor version.
|
||||
|
||||
## 2) Finding projection — `ledger.projection.v1`
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `tenantId` | `string` | Partition key. |
|
||||
| `findingId` | `string` | Stable identity. |
|
||||
| `policyVersion` | `string` | Hash of active policy bundle. |
|
||||
| `status` | `string` | `affected|triaged|accepted_risk|resolved|unknown`. |
|
||||
| `severity` | `number` | 0–10, 3 decimal places. |
|
||||
| `riskScore` | `number` | 0–10, 3 decimal places. |
|
||||
| `riskSeverity` | `string` | `low|medium|high|critical|unknown`. |
|
||||
| `riskProfileVersion` | `string` | Version/hash from Risk Engine. |
|
||||
| `riskExplanationId` | `uuid?` | Links to explain bundle. |
|
||||
| `labels` | `json` | KEV/runtime flags, sorted keys. |
|
||||
| `currentEventId` | `uuid` | Source ledger event. |
|
||||
| `explainRef` | `string?` | Object storage / DSSE reference. |
|
||||
| `policyRationale` | `json` | Array of rationale refs. |
|
||||
| `updatedAt` | `string` UTC | Projection timestamp. |
|
||||
| `cycleHash` | `char(64)` | `sha256(canonicalProjectionJson)`; used in exports. |
|
||||
|
||||
Projection deterministic hash recipe: serialize projection record with sorted keys (excluding `updatedAt` jitter) and hash via SHA-256. The replay harness recomputes and compares.
|
||||
|
||||
## 3) Export payloads — `export.v1`
|
||||
|
||||
Shapes share headers: `policyVersion`, `projectionVersion` (cycle hash), `filtersHash`, `pageToken`, `observedAt`, `provenance` (`ledgerRoot`, `projectorVersion`, `policyHash`, optional `dsseDigest`).
|
||||
|
||||
### Canonical vs compact
|
||||
- **Canonical (`export.v1.canonical`)** — full provenance fields, evidence refs, DSSE linkage.
|
||||
- **Compact (`export.v1.compact`)** — drops verbose fields (`policyRationale`, comments, actor ids), keeps `cycleHash` + `filtersHash` for determinism; redaction manifest enforced.
|
||||
|
||||
### Record fields
|
||||
- Findings: `findingId`, `eventSequence`, `status`, `severity`, `risk`, `advisories[]`, `evidenceBundleRef`, `cycleHash`.
|
||||
- VEX: `vexStatementId`, `product`, `status`, `justification`, `knownExploited`, `cycleHash`.
|
||||
- Advisories: `advisoryId`, `source`, `cvss{version,vector,baseScore}`, `epss`, `kev`, `cycleHash`.
|
||||
- SBOMs: `sbomId`, `subject{digest,mediaType}`, `sbomFormat`, `componentsCount`, `materials[]`, `cycleHash`.
|
||||
|
||||
Filters hash: `sha256(sortedQueryString)`; stored alongside fixtures for replayability.
|
||||
|
||||
## 4) Versioning rules
|
||||
- Patch: backward-compatible field additions (new optional key) — bump patch digit.
|
||||
- Minor: additive required fields or canonical rule tweaks — bump minor.
|
||||
- Major: breaking change (field removal/rename, hash recipe) — bump major and keep prior schema frozen.
|
||||
|
||||
## 5) Reference artefacts
|
||||
- Golden fixtures: `src/Findings/StellaOps.Findings.Ledger/fixtures/golden/*.ndjson`.
|
||||
- Checksum manifest: `docs/modules/findings-ledger/golden-checksums.json`.
|
||||
- Offline verifier: `tools/LedgerReplayHarness/scripts/verify_export.py`.
|
||||
@@ -119,6 +119,11 @@ Canonicalisation rules:
|
||||
5. Numbers use decimal notation; omit trailing zeros.
|
||||
6. Arrays maintain supplied order.
|
||||
|
||||
### 2.4 Versioning & DSSE linkage (FL1, FL6)
|
||||
- Canonical schema identifiers are catalogued in `schema-catalog.md` (`ledger.event.v1`, `ledger.projection.v1`, `export.v1.*`).
|
||||
- Any change to the envelope, hash recipe, or required fields bumps the catalog version; legacy versions remain frozen.
|
||||
- DSSE artefacts (anchors, exports, replay reports) **must** embed `policyVersion` and `schemaVersion` (see `dsse-policy-linkage.md`).
|
||||
|
||||
Hash pipeline:
|
||||
|
||||
```
|
||||
@@ -270,7 +275,7 @@ Ordering and pagination: `ORDER BY recorded_at ASC, attestation_id ASC` with cur
|
||||
1. Canonical serialize the envelope (§2.3).
|
||||
2. Compute `event_hash` and store along with `previous_hash`.
|
||||
3. Build Merkle tree per anchoring window using leaf hash `SHA256(event_hash || '-' || sequence_no)`.
|
||||
4. Persist root in `ledger_merkle_roots` and, when configured, submit to external transparency log (Rekor v2). Store receipt/UUID in `anchor_reference`.
|
||||
4. Persist root in `ledger_merkle_roots` and, when configured, submit to external transparency log (Rekor v2). Store receipt/UUID in `anchor_reference` (see `merkle-anchor-policy.md`).
|
||||
5. Projection rows compute `cycle_hash = SHA256(canonical_projection_json)` where canonical projection includes fields `{tenant_id, finding_id, policy_version, status, severity, labels, current_event_id}` with sorted keys.
|
||||
|
||||
Verification flow for auditors:
|
||||
@@ -284,6 +289,8 @@ Verification flow for auditors:
|
||||
- Initial migration script: `src/Findings/StellaOps.Findings.Ledger/migrations/001_initial.sql`.
|
||||
- Sample canonical event: `seed-data/findings-ledger/fixtures/ledger-event.sample.json` (includes pre-computed `eventHash`, `previousHash`, and `merkleLeafHash` values).
|
||||
- Sample projection row: `seed-data/findings-ledger/fixtures/finding-projection.sample.json` (includes canonical `cycleHash` for replay validation).
|
||||
- Golden export fixtures (FL7): `src/Findings/StellaOps.Findings.Ledger/fixtures/golden/*.ndjson` with checksums in `docs/modules/findings-ledger/golden-checksums.json`.
|
||||
- Redaction manifest (FL5): `docs/modules/findings-ledger/redaction-manifest.yaml` governs mask/drop rules for canonical vs compact exports.
|
||||
|
||||
Fixtures follow canonical key ordering and include precomputed hashes to validate tooling.
|
||||
|
||||
|
||||
28
docs/modules/findings-ledger/tenant-isolation-redaction.md
Normal file
28
docs/modules/findings-ledger/tenant-isolation-redaction.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Tenant Isolation & Redaction Manifest (FL5)
|
||||
|
||||
**Purpose:** Document how Findings Ledger enforces tenant boundaries and which fields are redacted in deterministic exports.
|
||||
|
||||
## Isolation controls
|
||||
- Storage: all ledger, projection, history, and merkle tables are **LIST-partitioned by `tenant_id`** (PostgreSQL). Cross-tenant queries are disallowed at repo level.
|
||||
- Queueing: Merkle batches and projector pipelines are keyed by `(tenant_id, chain_id)`; no mixing.
|
||||
- Exports: `/ledger/export/*` requires `X-Stella-Tenant`; service rejects multi-tenant requests.
|
||||
- Hashing: event/projection hashes include `tenant_id` as part of canonical envelope, preventing replay across tenants.
|
||||
|
||||
## Redaction policy
|
||||
- User-generated content (comments, attachments metadata) is excluded from compact exports and masked in canonical exports per manifest.
|
||||
- Actor identifiers are truncated to realm (`user:<realm>`); emails/PII never emitted.
|
||||
- Evidence bundle references are retained, but inline evidence payloads are not stored in ledger.
|
||||
|
||||
## Manifest
|
||||
- Path: `docs/modules/findings-ledger/redaction-manifest.yaml` (JSON twin: `redaction-manifest.json` for offline tooling).
|
||||
- Content: declarative list of fields redacted or truncated for each export shape.
|
||||
- The manifest is signed in checksum list `docs/modules/findings-ledger/golden-checksums.json`; sha256 must match before release.
|
||||
|
||||
### Applying the manifest
|
||||
- Canonical exports apply `redact: mask` rules only to PII (`actorId`, `comment`); compact exports drop (`drop: true`) the same fields plus verbose rationale arrays.
|
||||
- Log pipelines ensure `event_body` is never written to logs; only metadata/hashes appear (see `observability.md`).
|
||||
|
||||
## Validation steps
|
||||
1. `sha256sum docs/modules/findings-ledger/redaction-manifest.yaml` matches `golden-checksums.json`.
|
||||
2. Run `python tools/LedgerReplayHarness/scripts/verify_export.py --input fixtures/golden/findings-canonical.ndjson --schema export.v1.canonical --manifest docs/modules/findings-ledger/redaction-manifest.json` (script enforces mask/drop rules offline).
|
||||
3. Confirm export responses in staging omit masked fields for the requesting tenant.
|
||||
@@ -12,6 +12,7 @@ Applies to `mirror-thin-v1.*` artefacts in `out/mirror/thin/`.
|
||||
- Payload: `mirror-thin-v1.manifest.json`
|
||||
- Signature: ed25519 over base64url(payload)
|
||||
- Envelope path: `out/mirror/thin/mirror-thin-v1.manifest.dsse.json`
|
||||
- Bundle meta DSSE (OK1/OK3/MS8): payload type `application/vnd.stellaops.mirror.bundle+json`, payload `mirror-thin-v1.bundle.json`, envelope path `mirror-thin-v1.bundle.dsse.json`.
|
||||
|
||||
## TUF metadata layout
|
||||
```
|
||||
@@ -23,9 +24,10 @@ out/mirror/thin/tuf/
|
||||
keys/mirror-ed25519-test-1.pub
|
||||
```
|
||||
|
||||
### Targets mapping
|
||||
- `mirror-thin-v1.tar.gz` → targets entry with sha256 `210dc49e8d3e25509298770a94da277aa2c9d4c387d3c24505a61fe1d7695a49`
|
||||
- `mirror-thin-v1.manifest.json` → sha256 `0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504`
|
||||
### Targets mapping (latest dev build 2025-12-02)
|
||||
- `mirror-thin-v1.tar.gz` → targets entry with sha256 `fb1ce26388a1f1ab2eb90aae6d63ac05de326fbbd947fbf7a17b980232c9fc7d`
|
||||
- `mirror-thin-v1.manifest.json` → sha256 `1affb0b796ff037117b46aa1f1d8056a9c80755e925af058ea72132ba158becf`
|
||||
- `mirror-thin-v1.bundle.json` (top-level kit manifest) → sha256 `a3b16f5d1b74ffdf9aedbbfe9282d368dc3dcf70676c8ac7e8cdd984162e7f90`
|
||||
|
||||
### Determinism rules
|
||||
- Sort keys in JSON; indent=2; trailing newline.
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
MIRROR_SIGN_KEY_B64: ${{ secrets.MIRROR_SIGN_KEY_B64 }}
|
||||
REQUIRE_PROD_SIGNING: 1
|
||||
OCI: 1
|
||||
TENANT_SCOPE: tenant-demo
|
||||
ENV_SCOPE: lab
|
||||
run: |
|
||||
scripts/mirror/check_signing_prereqs.sh
|
||||
scripts/mirror/ci-sign.sh
|
||||
@@ -40,7 +42,9 @@ MIRROR_SIGN_KEY_B64=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndC
|
||||
**Do not ship with this key.** Set `REQUIRE_PROD_SIGNING=1` for release/tag builds so they fail without the real key. Add the production key as a Gitea secret (`MIRROR_SIGN_KEY_B64`) and rerun the workflow; remove this temporary key block once rotated.
|
||||
|
||||
## Verification
|
||||
The CI step already runs `scripts/mirror/verify_thin_bundle.py`. For OCI, ensure `out/mirror/thin/oci/index.json` references the manifest digest.
|
||||
The CI step already runs `scripts/mirror/verify_thin_bundle.py --bundle-meta mirror-thin-v1.bundle.json --tenant $TENANT_SCOPE --environment $ENV_SCOPE --pubkey out/mirror/thin/tuf/keys/ci-ed25519.pub` so offline-kit policies (OK1–OK10), Rekor policy (RK1–RK10), and mirror-format policy (MS1–MS10) are validated alongside the tarball. For OCI, ensure `out/mirror/thin/oci/index.json` references the manifest digest.
|
||||
|
||||
`milestone.json` now carries manifest/tar/bundle/bundle-dsse hashes plus policy layer hashes to allow air-gapped import verification.
|
||||
|
||||
## Fallback (if secret absent)
|
||||
- CI can fall back to an embedded test Ed25519 key when `MIRROR_SIGN_KEY_B64` is unset **only when `REQUIRE_PROD_SIGNING` is not set**. This is for dev smoke runs; release/tag jobs must set `REQUIRE_PROD_SIGNING=1` to forbid fallback.
|
||||
|
||||
@@ -112,7 +112,7 @@ Key notes:
|
||||
| **DSL Compiler** (`Dsl/`) | Parse, canonicalise, IR generation, checksum caching. | Uses Roslyn-like pipeline; caches by `policyId+version+hash`. |
|
||||
| **Selection Layer** (`Selection/`) | Batch SBOM ↔ advisory ↔ VEX joiners; apply equivalence tables; support incremental cursors. | Deterministic ordering (SBOM → advisory → VEX). |
|
||||
| **Evaluator** (`Evaluation/`) | Execute IR with first-match semantics, compute severity/trust/reachability weights, record rule hits. | Stateless; all inputs provided by selection layer. |
|
||||
| **Signals** (`Signals/`) | Normalizes reachability, trust, entropy, uncertainty, runtime hits into a single dictionary passed to Evaluator; supplies default `unknown` values when signals missing. | Aligns with `signals.*` namespace in DSL. |
|
||||
| **Signals** (`Signals/`) | Normalizes reachability, trust, entropy, uncertainty, runtime hits into a single dictionary passed to Evaluator; supplies default `unknown` values when signals missing. Entropy penalties are derived from Scanner `layer_summary.json`/`entropy.report.json` (K=0.5, cap=0.3, block at image opaque ratio > 0.15 w/ unknown provenance) and exported via `policy_entropy_penalty_value` / `policy_entropy_image_opaque_ratio`. | Aligns with `signals.*` namespace in DSL. |
|
||||
| **Materialiser** (`Materialization/`) | Upsert effective findings, append history, manage explain bundle exports. | Mongo transactions per SBOM chunk. |
|
||||
| **Orchestrator** (`Runs/`) | Change-stream ingestion, fairness, retry/backoff, queue writer. | Works with Scheduler Models DTOs. |
|
||||
| **API** (`Api/`) | Minimal API endpoints, DTO validation, problem responses, idempotency. | Generated clients for CLI/UI. |
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Zastava monitors running workloads, verifies supply chain posture, and enforces runtime policy via Kubernetes admission webhooks.
|
||||
|
||||
## Latest updates (2025-11-30)
|
||||
## Latest updates (2025-12-02)
|
||||
- DSSE-signed schemas, thresholds, exports, and deterministic `zastava-kit` bundle published under `docs/modules/zastava`; verification via `kit/verify.sh` and hashes in `SHA256SUMS`.
|
||||
- Sprint tracker `docs/implplan/SPRINT_0335_0001_0001_docs_modules_zastava.md` and module `TASKS.md` added to mirror status.
|
||||
- Observability runbook stub + dashboard placeholder added under `operations/` (offline import).
|
||||
- Surface.Env/Surface.Secrets adoption remains pending platform contracts; align with platform docs before enabling sealed mode.
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
e65d4b68c9bdaa569c6d4c5a9b0a8bc1dc41876f948983011ff6f9d3466565d0 schemas/observer_event.schema.json
|
||||
f466bf2b399f065558867eaf3c961cff8803f4a1506bae5539c9ce62e9ab005d schemas/webhook_admission.schema.json
|
||||
1b05f31ab9486f9a03ecf872fa5c681e9acbad2adb71a776c271dbcf997ca2a8 schemas/observer_event.schema.json
|
||||
99382de0e6a2b9c21146c03640c2e08b0e5e41be11fdbc213f0f071357da5a99 schemas/observer_event.schema.json.dsse
|
||||
222db5258f5ba1ee720f8df03858263363b8636ff8ec9370f5ad390e8def0b3c schemas/webhook_admission.schema.json
|
||||
19f108da1a512a488536bc2cd9d9cb1cf9824d748d8fc6a32d0e31c89be9a897 schemas/webhook_admission.schema.json.dsse
|
||||
da065beabf8e038298a54f04ffa3e140cc149e0d64c301f6fd4c3925f2d64ee6 schemas/examples/observer_event.example.json
|
||||
7e3cd0c18c9dfaf9001a16a99be7f9ff01e2d21b14eca9fb97c332342ac53c94 schemas/examples/webhook_admission.example.json
|
||||
e17d36a2a39d748b76994ad3e3e4f3fa8db1b9298a3ce5eaaafb575791c01da3 schemas/README.md
|
||||
f88bdebaa9858ffe3cd0fbb46e914c933e18709165bfc59f976136097fa8493d exports/observer_events.ndjson
|
||||
de9b24675a0a758e40647844a31a13a1be1667750a39fe59465b0353fd0dddd9 exports/observer_events.ndjson.dsse
|
||||
232809cf6a1cc7ba5fa34e0daf00fab9b6f970a613bc822457eef0d841fb2229 exports/webhook_admissions.ndjson
|
||||
0edf6cabd636c7bb1f210af2aecaf83de3cc21c82113a646429242ae72618b17 exports/webhook_admissions.ndjson.dsse
|
||||
40fabd4d7bc75c35ae063b2e931e79838c79b447528440456f5f4846951ff59d thresholds.yaml
|
||||
652fce7d7b622ae762c8fb65a1e592bec14b124c3273312f93a63d2c29a2b989 kit/verify.sh
|
||||
f3f84fbe780115608268a91a5203d2d3ada50b4317e7641d88430a692e61e1f4 kit/README.md
|
||||
2411a16a68c98c8fdd402e19b9c29400b469c0054d0b6067541ee343988b85e0 schemas/examples/observer_event.example.json
|
||||
4ab47977b0717c8bdb39c52f52880742785cbcf0b5ba73d9ecc835155d445dc1 schemas/examples/webhook_admission.example.json
|
||||
4dc099a742429a7ec300ac4c9eefe2f6b80bc0c10d7a7a3bbaf7f0a0f0ad7f20 thresholds.yaml.dsse
|
||||
f69f953c78134ef504b870cea47ba62d5e37a7a86ec0043d824dcb6073cd43fb kit/verify.sh
|
||||
1cf8f0448881d067e5e001a1dfe9734b4cdfcaaf16c3e9a7321ceae56e4af8f2 kit/README.md
|
||||
eaba054428fa72cd9476cffe7a94450e4345ffe2e294e9079eb7c3703bcf7df0 kit/ed25519.pub
|
||||
40a40b31480d876cf4487d07ca8d8b5166c7df455bef234e2c1861b7b3dc7e3b evidence/README.md
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| ZASTAVA-DOCS-0001 | DONE (2025-11-30) | Docs Guild | README/architecture refreshed; Surface Env/Secrets and sprint links added. |
|
||||
| ZASTAVA-ENG-0001 | DONE (2025-11-30) | Module Team | TASKS board created; statuses mirrored with `docs/implplan/SPRINT_0335_0001_0001_docs_modules_zastava.md`. |
|
||||
| ZASTAVA-OPS-0001 | DONE (2025-11-30) | Ops Guild | Observability runbook stub + Grafana JSON placeholder added under `operations/`. |
|
||||
| ZASTAVA-SCHEMAS-0001 | TODO | Zastava Guild | Publish signed observer/admission schemas + test vectors under `docs/modules/zastava/schemas/`; DSSE + SHA256 required. |
|
||||
| ZASTAVA-KIT-0001 | TODO | Zastava Guild | Build signed `zastava-kit` bundle with thresholds.yaml, schemas, observations/admissions export, SHA256SUMS, and verify.sh; ensure offline parity. |
|
||||
| ZASTAVA-THRESHOLDS-0001 | TODO | Zastava Guild | DSSE-sign `thresholds.yaml` and align with kit; publish Evidence Locker URI and update sprint 0144 checkpoints. |
|
||||
| ZASTAVA-SCHEMAS-0001 | DONE (2025-12-02) | Zastava Guild | Signed observer/admission schemas + test vectors under `docs/modules/zastava/schemas/`; DSSE + SHA256 published. |
|
||||
| ZASTAVA-KIT-0001 | DONE (2025-12-02) | Zastava Guild | Built signed `zastava-kit` bundle with thresholds, schemas, exports, SHA256SUMS, verify.sh; offline parity verified. |
|
||||
| ZASTAVA-THRESHOLDS-0001 | DONE (2025-12-02) | Zastava Guild | DSSE-signed `thresholds.yaml`, recorded Evidence Locker targets, and aligned with kit packaging. |
|
||||
| ZASTAVA-GAPS-144-007 | DONE (2025-12-02) | Zastava Guild | Remediation plan for ZR1–ZR10 published at `docs/modules/zastava/gaps/2025-12-02-zr-gaps.md`; follow-on schemas/kit/thresholds to be produced and signed. |
|
||||
|
||||
> Keep this table in lockstep with the sprint Delivery Tracker (TODO/DOING/DONE/BLOCKED updates go to both places).
|
||||
|
||||
@@ -1,29 +1,53 @@
|
||||
# Zastava Evidence Locker Plan (schemas/kit)
|
||||
# Zastava Evidence Locker (schemas/kit)
|
||||
|
||||
Artifacts to sign (target 2025-12-06):
|
||||
- `schemas/observer_event.schema.json` — predicate `stella.ops/zastavaSchema@v1`
|
||||
- `schemas/webhook_admission.schema.json` — predicate `stella.ops/zastavaSchema@v1`
|
||||
- `thresholds.yaml` — predicate `stella.ops/zastavaThresholds@v1`
|
||||
- `zastava-kit.tzst` + `SHA256SUMS` — predicate `stella.ops/zastavaKit@v1`
|
||||
Signed 2025-12-02 with Ed25519 key (pub base64url: `mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc`). Private key stored offline; all signatures use DSSEv1 pre-auth encoding.
|
||||
Public key copy: `docs/modules/zastava/kit/ed25519.pub`.
|
||||
|
||||
Planned Evidence Locker paths (fill after signing):
|
||||
- `evidence-locker/zastava/2025-12-06/observer_event.schema.dsse`
|
||||
- `evidence-locker/zastava/2025-12-06/webhook_admission.schema.dsse`
|
||||
- `evidence-locker/zastava/2025-12-06/thresholds.dsse`
|
||||
- `evidence-locker/zastava/2025-12-06/zastava-kit.tzst`
|
||||
- `evidence-locker/zastava/2025-12-06/SHA256SUMS`
|
||||
## Artefacts
|
||||
- `schemas/observer_event.schema.json.dsse` (payloadType `application/vnd.stellaops.zastava.schema+json;name=observer_event;version=1`)
|
||||
- `schemas/webhook_admission.schema.json.dsse` (payloadType `application/vnd.stellaops.zastava.schema+json;name=webhook_admission;version=1`)
|
||||
- `thresholds.yaml.dsse` (payloadType `application/vnd.stellaops.zastava.thresholds+yaml;version=1`)
|
||||
- `exports/observer_events.ndjson.dsse` (payloadType `application/vnd.stellaops.zastava.observer-events+ndjson;version=1`)
|
||||
- `exports/webhook_admissions.ndjson.dsse` (payloadType `application/vnd.stellaops.zastava.webhook-admissions+ndjson;version=1`)
|
||||
- `kit/zastava-kit.tzst.dsse` (payloadType `application/vnd.stellaops.zastava.kit+tzst;version=1`)
|
||||
|
||||
Signing template (replace KEY and file):
|
||||
## Evidence Locker targets
|
||||
- `evidence-locker/zastava/2025-12-02/observer_event.schema.json.dsse`
|
||||
- `evidence-locker/zastava/2025-12-02/webhook_admission.schema.json.dsse`
|
||||
- `evidence-locker/zastava/2025-12-02/thresholds.yaml.dsse`
|
||||
- `evidence-locker/zastava/2025-12-02/observer_events.ndjson.dsse`
|
||||
- `evidence-locker/zastava/2025-12-02/webhook_admissions.ndjson.dsse`
|
||||
- `evidence-locker/zastava/2025-12-02/zastava-kit.tzst`
|
||||
- `evidence-locker/zastava/2025-12-02/zastava-kit.tzst.dsse`
|
||||
- `evidence-locker/zastava/2025-12-02/SHA256SUMS`
|
||||
|
||||
## Signing template (Python, ed25519)
|
||||
```bash
|
||||
cosign sign-blob \
|
||||
--key cosign.key \
|
||||
--predicate-type stella.ops/zastavaSchema@v1 \
|
||||
--output-signature schemas/observer_event.schema.dsse \
|
||||
schemas/observer_event.schema.json
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
from base64 import urlsafe_b64encode
|
||||
import json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
priv = serialization.load_pem_private_key(Path('/tmp/zastava-ed25519.key').read_bytes(), password=None)
|
||||
pub = priv.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
|
||||
keyid = urlsafe_b64encode(pub).decode().rstrip('=')
|
||||
pt = '<payload-type>'
|
||||
payload = Path('<file-to-sign>').read_bytes()
|
||||
pae = b' '.join([b'DSSEv1', str(len(pt)).encode(), pt.encode(), str(len(payload)).encode(), payload])
|
||||
sig = priv.sign(pae)
|
||||
env = {
|
||||
'payloadType': pt,
|
||||
'payload': urlsafe_b64encode(payload).decode().rstrip('='),
|
||||
'signatures': [{'keyid': keyid, 'sig': urlsafe_b64encode(sig).decode().rstrip('=')}],
|
||||
}
|
||||
Path('<file-to-sign>.dsse').write_text(json.dumps(env, indent=2, sort_keys=True) + '\n')
|
||||
print('signed', '<file-to-sign>', 'with keyid', keyid)
|
||||
PY
|
||||
```
|
||||
|
||||
Post-sign steps:
|
||||
1) Verify DSSEs with `cosign verify-blob` using `cosign.pub`.
|
||||
2) Upload DSSEs + SHA256SUMS to Evidence Locker paths above.
|
||||
3) Update `docs/implplan/SPRINT_0144_0001_0001_zastava_runtime_signals.md` Decisions & Risks and Next Checkpoints with final URIs.
|
||||
4) Mark tasks ZASTAVA-SCHEMAS-0001 / ZASTAVA-THRESHOLDS-0001 / ZASTAVA-KIT-0001 to DONE in both sprint and TASKS tables.
|
||||
## Post-sign checklist
|
||||
1) Run `kit/verify.sh` to validate hashes + DSSE.
|
||||
2) Upload artefacts + DSSEs + SHA256SUMS to the Evidence Locker paths above.
|
||||
3) Record URIs in sprint 0144 Decisions & Risks and mark ZASTAVA-SCHEMAS-0001 / ZASTAVA-THRESHOLDS-0001 / ZASTAVA-KIT-0001 as DONE.
|
||||
|
||||
1
docs/modules/zastava/exports/observer_events.ndjson
Normal file
1
docs/modules/zastava/exports/observer_events.ndjson
Normal file
@@ -0,0 +1 @@
|
||||
{"event_type":"runtime_fact","firmware_version":"1.2.3","graph_revision_id":"graph-r1","ledger_id":"ledger-789","monotonic_nanos":123456789,"observed_at":"2025-12-02T12:00:00Z","payload":{"pid":4242,"process":"nginx"},"payload_hash":"sha256:7476a5068a3f0780c552f81c90d061d9e39c37f425a243ecff961b08676546fd","policy_hash":"sha256:deadbeef","project_id":"proj-123","replay_manifest":"manifest-r1","sensor_id":"observer-01","signature":"dsse://observer-events/2025-12-02/observer_events.ndjson.dsse#line1","tenant_id":"tenant-a"}
|
||||
10
docs/modules/zastava/exports/observer_events.ndjson.dsse
Normal file
10
docs/modules/zastava/exports/observer_events.ndjson.dsse
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"payload": "eyJldmVudF90eXBlIjoicnVudGltZV9mYWN0IiwiZmlybXdhcmVfdmVyc2lvbiI6IjEuMi4zIiwiZ3JhcGhfcmV2aXNpb25faWQiOiJncmFwaC1yMSIsImxlZGdlcl9pZCI6ImxlZGdlci03ODkiLCJtb25vdG9uaWNfbmFub3MiOjEyMzQ1Njc4OSwib2JzZXJ2ZWRfYXQiOiIyMDI1LTEyLTAyVDEyOjAwOjAwWiIsInBheWxvYWQiOnsicGlkIjo0MjQyLCJwcm9jZXNzIjoibmdpbngifSwicGF5bG9hZF9oYXNoIjoic2hhMjU2Ojc0NzZhNTA2OGEzZjA3ODBjNTUyZjgxYzkwZDA2MWQ5ZTM5YzM3ZjQyNWEyNDNlY2ZmOTYxYjA4Njc2NTQ2ZmQiLCJwb2xpY3lfaGFzaCI6InNoYTI1NjpkZWFkYmVlZiIsInByb2plY3RfaWQiOiJwcm9qLTEyMyIsInJlcGxheV9tYW5pZmVzdCI6Im1hbmlmZXN0LXIxIiwic2Vuc29yX2lkIjoib2JzZXJ2ZXItMDEiLCJzaWduYXR1cmUiOiJkc3NlOi8vb2JzZXJ2ZXItZXZlbnRzLzIwMjUtMTItMDIvb2JzZXJ2ZXJfZXZlbnRzLm5kanNvbi5kc3NlI2xpbmUxIiwidGVuYW50X2lkIjoidGVuYW50LWEifQo",
|
||||
"payloadType": "application/vnd.stellaops.zastava.observer-events+ndjson;version=1",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc",
|
||||
"sig": "5DPpjAcyWSeCM_yPCiIsQl92FtUwnccN8J5lY5AxKBE1qfYbU6dEgGQudDWlY2_-FUak6fupQ79vrgGbGiDDDQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
docs/modules/zastava/exports/webhook_admissions.ndjson
Normal file
1
docs/modules/zastava/exports/webhook_admissions.ndjson
Normal file
@@ -0,0 +1 @@
|
||||
{"bypass_waiver_id":null,"decision":"allow","decision_at":"2025-12-02T12:00:10Z","decision_reason":"surface cache fresh","graph_revision_id":"graph-r1","ledger_id":"ledger-789","manifest_pointer":"surfacefs://cache/sha256:abc","monotonic_nanos":2233445566,"namespace":"prod","payload":{"images":[{"digest":"sha256:abcd","name":"ghcr.io/acme/api:1.2.3","sbom_referrer":true,"signed":true}],"manifest_pointer":"surfacefs://cache/sha256:abc","policy_hash":"sha256:deadbeef","verdict":"allow"},"payload_hash":"sha256:36bfb2bc81b7050bbb508e12cafe7ad5a51336aad397ef3a23b0e258aed73dc6","policy_hash":"sha256:deadbeef","project_id":"proj-123","replay_manifest":"manifest-r1","request_uid":"abcd-1234","resource_kind":"Deployment","side_effect":"none","signature":"dsse://webhook-admissions/2025-12-02/webhook_admissions.ndjson.dsse#line1","tenant_id":"tenant-a","workload_name":"api"}
|
||||
10
docs/modules/zastava/exports/webhook_admissions.ndjson.dsse
Normal file
10
docs/modules/zastava/exports/webhook_admissions.ndjson.dsse
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"payload": "eyJieXBhc3Nfd2FpdmVyX2lkIjpudWxsLCJkZWNpc2lvbiI6ImFsbG93IiwiZGVjaXNpb25fYXQiOiIyMDI1LTEyLTAyVDEyOjAwOjEwWiIsImRlY2lzaW9uX3JlYXNvbiI6InN1cmZhY2UgY2FjaGUgZnJlc2giLCJncmFwaF9yZXZpc2lvbl9pZCI6ImdyYXBoLXIxIiwibGVkZ2VyX2lkIjoibGVkZ2VyLTc4OSIsIm1hbmlmZXN0X3BvaW50ZXIiOiJzdXJmYWNlZnM6Ly9jYWNoZS9zaGEyNTY6YWJjIiwibW9ub3RvbmljX25hbm9zIjoyMjMzNDQ1NTY2LCJuYW1lc3BhY2UiOiJwcm9kIiwicGF5bG9hZCI6eyJpbWFnZXMiOlt7ImRpZ2VzdCI6InNoYTI1NjphYmNkIiwibmFtZSI6ImdoY3IuaW8vYWNtZS9hcGk6MS4yLjMiLCJzYm9tX3JlZmVycmVyIjp0cnVlLCJzaWduZWQiOnRydWV9XSwibWFuaWZlc3RfcG9pbnRlciI6InN1cmZhY2VmczovL2NhY2hlL3NoYTI1NjphYmMiLCJwb2xpY3lfaGFzaCI6InNoYTI1NjpkZWFkYmVlZiIsInZlcmRpY3QiOiJhbGxvdyJ9LCJwYXlsb2FkX2hhc2giOiJzaGEyNTY6MzZiZmIyYmM4MWI3MDUwYmJiNTA4ZTEyY2FmZTdhZDVhNTEzMzZhYWQzOTdlZjNhMjNiMGUyNThhZWQ3M2RjNiIsInBvbGljeV9oYXNoIjoic2hhMjU2OmRlYWRiZWVmIiwicHJvamVjdF9pZCI6InByb2otMTIzIiwicmVwbGF5X21hbmlmZXN0IjoibWFuaWZlc3QtcjEiLCJyZXF1ZXN0X3VpZCI6ImFiY2QtMTIzNCIsInJlc291cmNlX2tpbmQiOiJEZXBsb3ltZW50Iiwic2lkZV9lZmZlY3QiOiJub25lIiwic2lnbmF0dXJlIjoiZHNzZTovL3dlYmhvb2stYWRtaXNzaW9ucy8yMDI1LTEyLTAyL3dlYmhvb2tfYWRtaXNzaW9ucy5uZGpzb24uZHNzZSNsaW5lMSIsInRlbmFudF9pZCI6InRlbmFudC1hIiwid29ya2xvYWRfbmFtZSI6ImFwaSJ9Cg",
|
||||
"payloadType": "application/vnd.stellaops.zastava.webhook-admissions+ndjson;version=1",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc",
|
||||
"sig": "UwXQm2oZPVIISQecILLkvxvSXZiXeZdPVe5RNqFxZ8Dv5xDT1nEcTq0pn2Tl3unk0sY44Lh-dU_599nxaHD9Aw"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -43,7 +43,8 @@
|
||||
- Delivery paths for schemas/thresholds/kit will be added when produced; DSSE signatures required for all artefacts.
|
||||
|
||||
## Next steps
|
||||
1) Generate schemas + test vectors and place under `docs/modules/zastava/schemas/`; sign DSSE.
|
||||
2) Draft `thresholds.yaml` with budgets and sign DSSE.
|
||||
3) Build `zastava-kit` bundle + `verify.sh`; include Evidence Locker path and SHA256.
|
||||
1) ✅ Schemas + test vectors generated and DSSE-signed under `docs/modules/zastava/schemas/` (2025-12-02).
|
||||
2) ✅ `thresholds.yaml` DSSE-signed and included in kit (2025-12-02).
|
||||
3) ✅ Deterministic `zastava-kit` bundle + `verify.sh` built; kit DSSE stored at `docs/modules/zastava/kit/zastava-kit.tzst.dsse` with hashes in `SHA256SUMS` (2025-12-02).
|
||||
4) Add tenancy/ordering/provenance enforcement to Observer/Webhook validators and tests; mirror changes in sprint and TASKS boards.
|
||||
5) Upload DSSE artefacts + kit to Evidence Locker paths in `docs/modules/zastava/evidence/README.md` and backfill operations docs with verifier usage.
|
||||
|
||||
@@ -1,17 +1,83 @@
|
||||
# Zastava Kit (offline bundle) – Draft
|
||||
# Zastava Kit (offline bundle)
|
||||
|
||||
Contents to include when built:
|
||||
- Observations and admissions exports (NDJSON) signed via DSSE.
|
||||
- Schemas: `schemas/observer_event.schema.json`, `schemas/webhook_admission.schema.json`.
|
||||
- Thresholds: `thresholds.yaml` (DSSE-signed).
|
||||
- Hash manifest: `SHA256SUMS` (covering all kit files).
|
||||
- Verify script: `verify.sh` (hash + DSSE verification; fail closed on mismatch).
|
||||
## Contents
|
||||
- Schemas + DSSE: `schemas/observer_event.schema.json(.dsse)`, `schemas/webhook_admission.schema.json(.dsse)`.
|
||||
- Examples: `schemas/examples/*.json` (canonicalised, hashed).
|
||||
- Thresholds + DSSE: `thresholds.yaml(.dsse)`.
|
||||
- Exports + DSSE: `exports/observer_events.ndjson(.dsse)`, `exports/webhook_admissions.ndjson(.dsse)`.
|
||||
- Verification assets: `SHA256SUMS`, `kit/verify.sh`, `kit/ed25519.pub`, `schemas/README.md`, `evidence/README.md`.
|
||||
|
||||
Deterministic packaging: `tar --mtime @0 --owner 0 --group 0 --numeric-owner -cf - kit | zstd -19 --long=27 --no-progress > zastava-kit.tzst`.
|
||||
## Build (deterministic)
|
||||
From `docs/modules/zastava`:
|
||||
|
||||
Pending: fill with signed artefacts and Evidence Locker URIs after DSSE signing.
|
||||
Planned Evidence Locker paths (post-signing):
|
||||
- `evidence-locker/zastava/2025-12-06/observer_event.schema.dsse`
|
||||
- `evidence-locker/zastava/2025-12-06/webhook_admission.schema.dsse`
|
||||
- `evidence-locker/zastava/2025-12-06/thresholds.dsse`
|
||||
- `evidence-locker/zastava/2025-12-06/zastava-kit.tzst` + `SHA256SUMS`
|
||||
```bash
|
||||
tar --mtime @0 --owner 0 --group 0 --numeric-owner --sort=name \
|
||||
-cf - \
|
||||
SHA256SUMS schemas exports thresholds.yaml thresholds.yaml.dsse \
|
||||
schemas/examples schemas/README.md \
|
||||
schemas/observer_event.schema.json schemas/observer_event.schema.json.dsse \
|
||||
schemas/webhook_admission.schema.json schemas/webhook_admission.schema.json.dsse \
|
||||
exports/observer_events.ndjson exports/observer_events.ndjson.dsse \
|
||||
exports/webhook_admissions.ndjson exports/webhook_admissions.ndjson.dsse \
|
||||
evidence/README.md kit/README.md kit/verify.sh kit/ed25519.pub \
|
||||
| zstd -19 --long=27 --no-progress > kit/zastava-kit.tzst
|
||||
```
|
||||
|
||||
Sign the kit itself with the same Ed25519 key (base64url pub: `mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc`):
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
from base64 import urlsafe_b64encode
|
||||
import json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
priv = serialization.load_pem_private_key(Path('/tmp/zastava-ed25519.key').read_bytes(), password=None)
|
||||
pub = priv.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
|
||||
keyid = urlsafe_b64encode(pub).decode().rstrip('=')
|
||||
pt = 'application/vnd.stellaops.zastava.kit+tzst;version=1'
|
||||
payload = Path('kit/zastava-kit.tzst').read_bytes()
|
||||
pae = b' '.join([b'DSSEv1', str(len(pt)).encode(), pt.encode(), str(len(payload)).encode(), payload])
|
||||
sig = priv.sign(pae)
|
||||
env = {
|
||||
'payloadType': pt,
|
||||
'payload': urlsafe_b64encode(payload).decode().rstrip('='),
|
||||
'signatures': [{'keyid': keyid, 'sig': urlsafe_b64encode(sig).decode().rstrip('=')}],
|
||||
}
|
||||
Path('kit/zastava-kit.tzst.dsse').write_text(json.dumps(env, indent=2, sort_keys=True) + '\n')
|
||||
print('wrote kit/zastava-kit.tzst.dsse with keyid', keyid)
|
||||
PY
|
||||
```
|
||||
|
||||
## Verify
|
||||
1) Verify the kit DSSE before unpacking (optional but recommended) using the public key shipped alongside the kit (run from `docs/modules/zastava`):
|
||||
```bash
|
||||
cd docs/modules/zastava
|
||||
python - <<'PY'
|
||||
import base64, json, sys
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
|
||||
root = Path('.')
|
||||
pub = base64.urlsafe_b64decode((root / 'kit' / 'ed25519.pub').read_text().strip() + '==')
|
||||
env = json.loads((root / 'kit' / 'zastava-kit.tzst.dsse').read_text())
|
||||
payload = (root / 'kit' / 'zastava-kit.tzst').read_bytes()
|
||||
pt = env['payloadType'].encode()
|
||||
pae = b' '.join([b'DSSEv1', str(len(pt)).encode(), pt, str(len(payload)).encode(), payload])
|
||||
sig = base64.urlsafe_b64decode(env['signatures'][0]['sig'] + '==')
|
||||
ed25519.Ed25519PublicKey.from_public_bytes(pub).verify(sig, pae)
|
||||
decoded_payload = base64.urlsafe_b64decode(env['payload'] + '==')
|
||||
assert decoded_payload == payload
|
||||
print('OK: kit DSSE verified')
|
||||
PY
|
||||
```
|
||||
2) Extract and run offline validation of the inner artefacts:
|
||||
```bash
|
||||
zstd -d kit/zastava-kit.tzst -c | tar -xf -
|
||||
./kit/verify.sh
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Private signing key is held offline; only the public key is shipped.
|
||||
- All files are deterministic (mtime=0, numeric owners) to keep hashes stable for Evidence Locker ingestion.
|
||||
|
||||
1
docs/modules/zastava/kit/ed25519.pub
Normal file
1
docs/modules/zastava/kit/ed25519.pub
Normal file
@@ -0,0 +1 @@
|
||||
mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc
|
||||
@@ -1,24 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if ! command -v sha256sum >/dev/null; then
|
||||
echo "sha256sum required" >&2; exit 1
|
||||
fi
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
MODULE_ROOT="${ROOT}/.."
|
||||
cd "$MODULE_ROOT"
|
||||
export MODULE_ROOT
|
||||
|
||||
command -v sha256sum >/dev/null || { echo "sha256sum required" >&2; exit 1; }
|
||||
command -v python >/dev/null || { echo "python required" >&2; exit 1; }
|
||||
|
||||
sha256sum --check SHA256SUMS
|
||||
if command -v cosign >/dev/null && [ -f cosign.pub ]; then
|
||||
echo "cosign present; DSSE verification placeholders (update paths when signed):"
|
||||
echo "- observer_event.schema.dsse"
|
||||
echo "- webhook_admission.schema.dsse"
|
||||
echo "- thresholds.dsse"
|
||||
# Example commands (uncomment once DSSE files exist):
|
||||
# cosign verify-blob --key cosign.pub --signature observer_event.schema.dsse schemas/observer_event.schema.json
|
||||
# cosign verify-blob --key cosign.pub --signature webhook_admission.schema.dsse schemas/webhook_admission.schema.json
|
||||
# cosign verify-blob --key cosign.pub --signature thresholds.dsse thresholds.yaml
|
||||
else
|
||||
echo "cosign not found or cosign.pub missing; skipped DSSE verification"
|
||||
fi
|
||||
|
||||
echo "OK: hashes verified (DSSE verification pending)"
|
||||
python - <<'PY'
|
||||
import base64, json, os, sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
except Exception as exc:
|
||||
raise SystemExit(f"cryptography package required for DSSE verification: {exc}")
|
||||
|
||||
root = Path(os.environ['MODULE_ROOT']).resolve()
|
||||
pub_b64 = (root / "kit" / "ed25519.pub").read_text().strip()
|
||||
pub = base64.urlsafe_b64decode(pub_b64 + "==")
|
||||
verifier = ed25519.Ed25519PublicKey.from_public_bytes(pub)
|
||||
|
||||
def pae(payload_type: bytes, payload: bytes) -> bytes:
|
||||
parts = [b"DSSEv1", str(len(payload_type)).encode(), payload_type, str(len(payload)).encode(), payload]
|
||||
return b" ".join(parts)
|
||||
|
||||
def verify(name: str, payload_path: Path, envelope_path: Path, payload_type: str):
|
||||
payload = payload_path.read_bytes()
|
||||
envelope = json.loads(envelope_path.read_text())
|
||||
if envelope.get("payloadType") != payload_type:
|
||||
raise SystemExit(f"{name}: payloadType mismatch ({envelope.get('payloadType')} != {payload_type})")
|
||||
if not envelope.get("signatures"):
|
||||
raise SystemExit(f"{name}: missing signatures")
|
||||
sig_entry = envelope["signatures"][0]
|
||||
sig = base64.urlsafe_b64decode(sig_entry["sig"] + "==")
|
||||
decoded_payload = base64.urlsafe_b64decode(envelope["payload"] + "==")
|
||||
if decoded_payload != payload:
|
||||
raise SystemExit(f"{name}: payload body mismatch vs envelope")
|
||||
verifier.verify(sig, pae(payload_type.encode(), payload))
|
||||
print(f"OK: {name}")
|
||||
|
||||
targets = [
|
||||
("observer schema", root / "schemas" / "observer_event.schema.json", root / "schemas" / "observer_event.schema.json.dsse", "application/vnd.stellaops.zastava.schema+json;name=observer_event;version=1"),
|
||||
("webhook schema", root / "schemas" / "webhook_admission.schema.json", root / "schemas" / "webhook_admission.schema.json.dsse", "application/vnd.stellaops.zastava.schema+json;name=webhook_admission;version=1"),
|
||||
("thresholds", root / "thresholds.yaml", root / "thresholds.yaml.dsse", "application/vnd.stellaops.zastava.thresholds+yaml;version=1"),
|
||||
("observer exports", root / "exports" / "observer_events.ndjson", root / "exports" / "observer_events.ndjson.dsse", "application/vnd.stellaops.zastava.observer-events+ndjson;version=1"),
|
||||
("webhook exports", root / "exports" / "webhook_admissions.ndjson", root / "exports" / "webhook_admissions.ndjson.dsse", "application/vnd.stellaops.zastava.webhook-admissions+ndjson;version=1"),
|
||||
]
|
||||
|
||||
for name, payload_path, envelope_path, ptype in targets:
|
||||
verify(name, payload_path, envelope_path, ptype)
|
||||
PY
|
||||
|
||||
echo "OK: SHA256 + DSSE signatures verified"
|
||||
|
||||
BIN
docs/modules/zastava/kit/zastava-kit.tzst
Normal file
BIN
docs/modules/zastava/kit/zastava-kit.tzst
Normal file
Binary file not shown.
10
docs/modules/zastava/kit/zastava-kit.tzst.dsse
Normal file
10
docs/modules/zastava/kit/zastava-kit.tzst.dsse
Normal file
File diff suppressed because one or more lines are too long
19
docs/modules/zastava/schemas/README.md
Normal file
19
docs/modules/zastava/schemas/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Zastava schemas (runtime & admission)
|
||||
|
||||
## Canonicalisation & hashing
|
||||
- JSON is stored with sorted keys and two-space indentation; hashes use **JCS-style** encoding: `json.dumps(payload, separators=(',', ':'), sort_keys=True)`.
|
||||
- `payload_hash` fields in examples and exports are computed from the canonical payload bytes and formatted as `sha256:<hex>`.
|
||||
- Schema negotiation stays on the `zastava.*@v1.x` line; breaking changes bump the major version.
|
||||
|
||||
## DSSE signing
|
||||
- Payload types:
|
||||
- `application/vnd.stellaops.zastava.schema+json;name=observer_event;version=1`
|
||||
- `application/vnd.stellaops.zastava.schema+json;name=webhook_admission;version=1`
|
||||
- Ed25519 public key (base64url, no padding): `mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc`.
|
||||
- Signatures are emitted as `<file>.dsse` with DSSEv1 pre-auth encoding over the raw file bytes.
|
||||
- Regenerate signatures with `docs/modules/zastava/kit/verify.sh` prerequisites (Python + cryptography) and the private key held offline.
|
||||
|
||||
## Test vectors
|
||||
- Example payloads: `schemas/examples/*.json`.
|
||||
- Signed exports: `exports/observer_events.ndjson(.dsse)` and `exports/webhook_admissions.ndjson(.dsse)`.
|
||||
- Kit verification aggregates all signatures via `kit/verify.sh`.
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"tenant_id": "tenant-a",
|
||||
"project_id": "proj-123",
|
||||
"sensor_id": "observer-01",
|
||||
"event_type": "runtime_fact",
|
||||
"firmware_version": "1.2.3",
|
||||
"policy_hash": "sha256:deadbeef",
|
||||
"graph_revision_id": "graph-r1",
|
||||
"ledger_id": "ledger-789",
|
||||
"replay_manifest": "manifest-r1",
|
||||
"event_type": "runtime_fact",
|
||||
"observed_at": "2025-12-02T00:00:00Z",
|
||||
"monotonic_nanos": 123456789,
|
||||
"observed_at": "2025-12-02T00:00:00Z",
|
||||
"payload": {
|
||||
"process": "nginx",
|
||||
"pid": 4242
|
||||
"pid": 4242,
|
||||
"process": "nginx"
|
||||
},
|
||||
"payload_hash": "sha256:payloadhash",
|
||||
"signature": "dsse://observer-event"
|
||||
"payload_hash": "sha256:7476a5068a3f0780c552f81c90d061d9e39c37f425a243ecff961b08676546fd",
|
||||
"policy_hash": "sha256:deadbeef",
|
||||
"project_id": "proj-123",
|
||||
"replay_manifest": "manifest-r1",
|
||||
"sensor_id": "observer-01",
|
||||
"signature": "dsse://observer-events/2025-12-02/observer_events.ndjson.dsse#line1",
|
||||
"tenant_id": "tenant-a"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
{
|
||||
"tenant_id": "tenant-a",
|
||||
"project_id": "proj-123",
|
||||
"request_uid": "abcd-1234",
|
||||
"resource_kind": "Deployment",
|
||||
"namespace": "prod",
|
||||
"workload_name": "api",
|
||||
"policy_hash": "sha256:deadbeef",
|
||||
"bypass_waiver_id": null,
|
||||
"decision": "allow",
|
||||
"decision_at": "2025-12-02T00:00:00Z",
|
||||
"decision_reason": "surface cache fresh",
|
||||
"graph_revision_id": "graph-r1",
|
||||
"ledger_id": "ledger-789",
|
||||
"replay_manifest": "manifest-r1",
|
||||
"manifest_pointer": "surfacefs://cache/sha256:abc",
|
||||
"decision": "allow",
|
||||
"decision_reason": "surface cache fresh",
|
||||
"decision_at": "2025-12-02T00:00:00Z",
|
||||
"monotonic_nanos": 2233445566,
|
||||
"namespace": "prod",
|
||||
"payload": {
|
||||
"images": [
|
||||
{
|
||||
"digest": "sha256:abcd",
|
||||
"name": "ghcr.io/acme/api:1.2.3",
|
||||
"sbom_referrer": true,
|
||||
"signed": true
|
||||
}
|
||||
],
|
||||
"manifest_pointer": "surfacefs://cache/sha256:abc",
|
||||
"policy_hash": "sha256:deadbeef",
|
||||
"verdict": "allow"
|
||||
},
|
||||
"payload_hash": "sha256:36bfb2bc81b7050bbb508e12cafe7ad5a51336aad397ef3a23b0e258aed73dc6",
|
||||
"policy_hash": "sha256:deadbeef",
|
||||
"project_id": "proj-123",
|
||||
"replay_manifest": "manifest-r1",
|
||||
"request_uid": "abcd-1234",
|
||||
"resource_kind": "Deployment",
|
||||
"side_effect": "none",
|
||||
"bypass_waiver_id": null,
|
||||
"payload_hash": "sha256:payloadhash",
|
||||
"signature": "dsse://webhook-admission"
|
||||
"signature": "dsse://webhook-admissions/2025-12-02/webhook_admissions.ndjson.dsse#line1",
|
||||
"tenant_id": "tenant-a",
|
||||
"workload_name": "api"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,67 @@
|
||||
{
|
||||
"$id": "https://stella-ops.org/schemas/zastava/observer_event.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Zastava Observer Event",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"event_type": {
|
||||
"enum": [
|
||||
"runtime_fact",
|
||||
"drift",
|
||||
"policy_violation",
|
||||
"heartbeat"
|
||||
]
|
||||
},
|
||||
"firmware_version": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"graph_revision_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ledger_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"monotonic_nanos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"observed_at": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"description": "Canonical runtime payload (JCS) used for hashing.",
|
||||
"type": "object"
|
||||
},
|
||||
"payload_hash": {
|
||||
"description": "sha256 over canonical JSON (JCS) of payload",
|
||||
"pattern": "^sha256:[0-9a-f]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"policy_hash": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"project_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"replay_manifest": {
|
||||
"type": "string"
|
||||
},
|
||||
"sensor_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"signature": {
|
||||
"description": "DSSE envelope reference",
|
||||
"pattern": "^dsse://[A-Za-z0-9._:/-]+$",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tenant_id",
|
||||
"project_id",
|
||||
@@ -12,23 +71,10 @@
|
||||
"graph_revision_id",
|
||||
"event_type",
|
||||
"observed_at",
|
||||
"payload",
|
||||
"payload_hash",
|
||||
"signature"
|
||||
"signature"
|
||||
],
|
||||
"properties": {
|
||||
"tenant_id": { "type": "string" },
|
||||
"project_id": { "type": "string" },
|
||||
"sensor_id": { "type": "string" },
|
||||
"firmware_version": { "type": "string" },
|
||||
"policy_hash": { "type": "string" },
|
||||
"graph_revision_id": { "type": "string" },
|
||||
"ledger_id": { "type": "string" },
|
||||
"replay_manifest": { "type": "string" },
|
||||
"event_type": { "enum": ["runtime_fact", "drift", "policy_violation", "heartbeat"] },
|
||||
"observed_at": { "type": "string", "format": "date-time" },
|
||||
"monotonic_nanos": { "type": "integer" },
|
||||
"payload": { "type": "object" },
|
||||
"payload_hash": { "type": "string", "description": "sha256 over canonical JSON (JCS) of payload" },
|
||||
"signature": { "type": "string", "description": "DSSE envelope reference" }
|
||||
}
|
||||
"title": "Zastava Observer Event",
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
10
docs/modules/zastava/schemas/observer_event.schema.json.dsse
Normal file
10
docs/modules/zastava/schemas/observer_event.schema.json.dsse
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"payload": "ewogICIkaWQiOiAiaHR0cHM6Ly9zdGVsbGEtb3BzLm9yZy9zY2hlbWFzL3phc3RhdmEvb2JzZXJ2ZXJfZXZlbnQuc2NoZW1hLmpzb24iLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInByb3BlcnRpZXMiOiB7CiAgICAiZXZlbnRfdHlwZSI6IHsKICAgICAgImVudW0iOiBbCiAgICAgICAgInJ1bnRpbWVfZmFjdCIsCiAgICAgICAgImRyaWZ0IiwKICAgICAgICAicG9saWN5X3Zpb2xhdGlvbiIsCiAgICAgICAgImhlYXJ0YmVhdCIKICAgICAgXQogICAgfSwKICAgICJmaXJtd2FyZV92ZXJzaW9uIjogewogICAgICAibWluTGVuZ3RoIjogMSwKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJncmFwaF9yZXZpc2lvbl9pZCI6IHsKICAgICAgIm1pbkxlbmd0aCI6IDEsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAibGVkZ2VyX2lkIjogewogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgIm1vbm90b25pY19uYW5vcyI6IHsKICAgICAgInR5cGUiOiAiaW50ZWdlciIKICAgIH0sCiAgICAib2JzZXJ2ZWRfYXQiOiB7CiAgICAgICJmb3JtYXQiOiAiZGF0ZS10aW1lIiwKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJwYXlsb2FkIjogewogICAgICAiZGVzY3JpcHRpb24iOiAiQ2Fub25pY2FsIHJ1bnRpbWUgcGF5bG9hZCAoSkNTKSB1c2VkIGZvciBoYXNoaW5nLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIKICAgIH0sCiAgICAicGF5bG9hZF9oYXNoIjogewogICAgICAiZGVzY3JpcHRpb24iOiAic2hhMjU2IG92ZXIgY2Fub25pY2FsIEpTT04gKEpDUykgb2YgcGF5bG9hZCIsCiAgICAgICJwYXR0ZXJuIjogIl5zaGEyNTY6WzAtOWEtZl17NjR9JCIsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAicG9saWN5X2hhc2giOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInByb2plY3RfaWQiOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInJlcGxheV9tYW5pZmVzdCI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJzZW5zb3JfaWQiOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImRlc2NyaXB0aW9uIjogIkRTU0UgZW52ZWxvcGUgcmVmZXJlbmNlIiwKICAgICAgInBhdHRlcm4iOiAiXmRzc2U6Ly9bQS1aYS16MC05Ll86Ly1dKyQiLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInRlbmFudF9pZCI6IHsKICAgICAgIm1pbkxlbmd0aCI6IDEsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJ0ZW5hbnRfaWQiLAogICAgInByb2plY3RfaWQiLAogICAgInNlbnNvcl9pZCIsCiAgICAiZmlybXdhcmVfdmVyc2lvbiIsCiAgICAicG9saWN5X2hhc2giLAogICAgImdyYXBoX3JldmlzaW9uX2lkIiwKICAgICJldmVudF90eXBlIiwKICAgICJvYnNlcnZlZF9hdCIsCiAgICAicGF5bG9hZCIsCiAgICAicGF5bG9hZF9oYXNoIiwKICAgICJzaWduYXR1cmUiCiAgXSwKICAidGl0bGUiOiAiWmFzdGF2YSBPYnNlcnZlciBFdmVudCIsCiAgInR5cGUiOiAib2JqZWN0Igp9Cg",
|
||||
"payloadType": "application/vnd.stellaops.zastava.schema+json;name=observer_event;version=1",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc",
|
||||
"sig": "axmdd1ucHyZyJMAyLzWmpuai7VrS20QenSDQyXRKlmtsAF4Zl4Ke_cHy8konBStBCoJgGA3SM2236QgAbkQMBw"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,91 @@
|
||||
{
|
||||
"$id": "https://stella-ops.org/schemas/zastava/webhook_admission.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Zastava Webhook Admission",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bypass_waiver_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"decision": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny",
|
||||
"dry-run"
|
||||
]
|
||||
},
|
||||
"decision_at": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"decision_reason": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"graph_revision_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ledger_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"manifest_pointer": {
|
||||
"description": "Surface.FS manifest pointer",
|
||||
"type": "string"
|
||||
},
|
||||
"monotonic_nanos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"namespace": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"description": "AdmissionReview payload (canonical JSON) hashed via payload_hash",
|
||||
"type": "object"
|
||||
},
|
||||
"payload_hash": {
|
||||
"pattern": "^sha256:[0-9a-f]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"policy_hash": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"project_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"replay_manifest": {
|
||||
"type": "string"
|
||||
},
|
||||
"request_uid": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"resource_kind": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"side_effect": {
|
||||
"enum": [
|
||||
"none",
|
||||
"mutating",
|
||||
"bypass"
|
||||
]
|
||||
},
|
||||
"signature": {
|
||||
"description": "DSSE envelope reference",
|
||||
"pattern": "^dsse://[A-Za-z0-9._:/-]+$",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_id": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"workload_name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tenant_id",
|
||||
"project_id",
|
||||
@@ -16,27 +99,10 @@
|
||||
"decision_reason",
|
||||
"decision_at",
|
||||
"manifest_pointer",
|
||||
"payload",
|
||||
"payload_hash",
|
||||
"signature"
|
||||
],
|
||||
"properties": {
|
||||
"tenant_id": { "type": "string" },
|
||||
"project_id": { "type": "string" },
|
||||
"request_uid": { "type": "string" },
|
||||
"resource_kind": { "type": "string" },
|
||||
"namespace": { "type": "string" },
|
||||
"workload_name": { "type": "string" },
|
||||
"policy_hash": { "type": "string" },
|
||||
"graph_revision_id": { "type": "string" },
|
||||
"ledger_id": { "type": "string" },
|
||||
"replay_manifest": { "type": "string" },
|
||||
"manifest_pointer": { "type": "string", "description": "Surface.FS manifest pointer" },
|
||||
"decision": { "enum": ["allow", "deny", "dry-run"] },
|
||||
"decision_reason": { "type": "string" },
|
||||
"decision_at": { "type": "string", "format": "date-time" },
|
||||
"monotonic_nanos": { "type": "integer" },
|
||||
"side_effect": { "enum": ["none", "mutating", "bypass"] },
|
||||
"bypass_waiver_id": { "type": "string" },
|
||||
"payload_hash": { "type": "string" },
|
||||
"signature": { "type": "string", "description": "DSSE envelope reference" }
|
||||
}
|
||||
"title": "Zastava Webhook Admission",
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"payload": "ewogICIkaWQiOiAiaHR0cHM6Ly9zdGVsbGEtb3BzLm9yZy9zY2hlbWFzL3phc3RhdmEvd2ViaG9va19hZG1pc3Npb24uc2NoZW1hLmpzb24iLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInByb3BlcnRpZXMiOiB7CiAgICAiYnlwYXNzX3dhaXZlcl9pZCI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJkZWNpc2lvbiI6IHsKICAgICAgImVudW0iOiBbCiAgICAgICAgImFsbG93IiwKICAgICAgICAiZGVueSIsCiAgICAgICAgImRyeS1ydW4iCiAgICAgIF0KICAgIH0sCiAgICAiZGVjaXNpb25fYXQiOiB7CiAgICAgICJmb3JtYXQiOiAiZGF0ZS10aW1lIiwKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJkZWNpc2lvbl9yZWFzb24iOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgImdyYXBoX3JldmlzaW9uX2lkIjogewogICAgICAibWluTGVuZ3RoIjogMSwKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJsZWRnZXJfaWQiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAibWFuaWZlc3RfcG9pbnRlciI6IHsKICAgICAgImRlc2NyaXB0aW9uIjogIlN1cmZhY2UuRlMgbWFuaWZlc3QgcG9pbnRlciIsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAibW9ub3RvbmljX25hbm9zIjogewogICAgICAidHlwZSI6ICJpbnRlZ2VyIgogICAgfSwKICAgICJuYW1lc3BhY2UiOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInBheWxvYWQiOiB7CiAgICAgICJkZXNjcmlwdGlvbiI6ICJBZG1pc3Npb25SZXZpZXcgcGF5bG9hZCAoY2Fub25pY2FsIEpTT04pIGhhc2hlZCB2aWEgcGF5bG9hZF9oYXNoIiwKICAgICAgInR5cGUiOiAib2JqZWN0IgogICAgfSwKICAgICJwYXlsb2FkX2hhc2giOiB7CiAgICAgICJwYXR0ZXJuIjogIl5zaGEyNTY6WzAtOWEtZl17NjR9JCIsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAicG9saWN5X2hhc2giOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInByb2plY3RfaWQiOiB7CiAgICAgICJtaW5MZW5ndGgiOiAxLAogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgInJlcGxheV9tYW5pZmVzdCI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJyZXF1ZXN0X3VpZCI6IHsKICAgICAgIm1pbkxlbmd0aCI6IDEsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAicmVzb3VyY2Vfa2luZCI6IHsKICAgICAgIm1pbkxlbmd0aCI6IDEsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAic2lkZV9lZmZlY3QiOiB7CiAgICAgICJlbnVtIjogWwogICAgICAgICJub25lIiwKICAgICAgICAibXV0YXRpbmciLAogICAgICAgICJieXBhc3MiCiAgICAgIF0KICAgIH0sCiAgICAic2lnbmF0dXJlIjogewogICAgICAiZGVzY3JpcHRpb24iOiAiRFNTRSBlbnZlbG9wZSByZWZlcmVuY2UiLAogICAgICAicGF0dGVybiI6ICJeZHNzZTovL1tBLVphLXowLTkuXzovLV0rJCIsCiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAidGVuYW50X2lkIjogewogICAgICAibWluTGVuZ3RoIjogMSwKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJ3b3JrbG9hZF9uYW1lIjogewogICAgICAibWluTGVuZ3RoIjogMSwKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfQogIH0sCiAgInJlcXVpcmVkIjogWwogICAgInRlbmFudF9pZCIsCiAgICAicHJvamVjdF9pZCIsCiAgICAicmVxdWVzdF91aWQiLAogICAgInJlc291cmNlX2tpbmQiLAogICAgIm5hbWVzcGFjZSIsCiAgICAid29ya2xvYWRfbmFtZSIsCiAgICAicG9saWN5X2hhc2giLAogICAgImdyYXBoX3JldmlzaW9uX2lkIiwKICAgICJkZWNpc2lvbiIsCiAgICAiZGVjaXNpb25fcmVhc29uIiwKICAgICJkZWNpc2lvbl9hdCIsCiAgICAibWFuaWZlc3RfcG9pbnRlciIsCiAgICAicGF5bG9hZCIsCiAgICAicGF5bG9hZF9oYXNoIiwKICAgICJzaWduYXR1cmUiCiAgXSwKICAidGl0bGUiOiAiWmFzdGF2YSBXZWJob29rIEFkbWlzc2lvbiIsCiAgInR5cGUiOiAib2JqZWN0Igp9Cg",
|
||||
"payloadType": "application/vnd.stellaops.zastava.schema+json;name=webhook_admission;version=1",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc",
|
||||
"sig": "Vk0mACAjBtUuVn_S2M5HU81zMbH8wDCQYOHVsft7cmxl0JbDrSIA9z3xlTI5JiT7DYOGsDUc96dlC1njldN4Aw"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
docs/modules/zastava/thresholds.yaml.dsse
Normal file
10
docs/modules/zastava/thresholds.yaml.dsse
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"payload": "dmVyc2lvbjogMQp1cGRhdGVkX2F0OiAyMDI1LTEyLTAyVDAwOjAwOjAwWgpidWRnZXRzOgogIGxhdGVuY3lfbXNfcDk1OiAyNTAKICBlcnJvcl9yYXRlOiAwLjAxCiAgZHJvcF9yYXRlOiAwLjAwNQpidXJuX3JhdGVzOgogIGFkbWlzc2lvbl9kZW5pZXNfcGVyX21pbjogNQogIG9ic2VydmVyX2RyaWZ0c19wZXJfaG91cjogMgogIGhlYXJ0YmVhdF9taXNzX21pbnV0ZXM6IDMKYWxlcnRzOgogIHRocmVzaG9sZF9jaGFuZ2U6IHRydWUKICBidXJuX3JhdGVfZXhjZWVkZWQ6IHRydWUKICBraWxsX3N3aXRjaF90cmlnZ2VyZWQ6IHRydWUKc2lnbmluZzoKICBwcmVkaWNhdGU6IHN0ZWxsYS5vcHMvemFzdGF2YVRocmVzaG9sZHNAdjEKICBkc3NlX3JlcXVpcmVkOiB0cnVlCg",
|
||||
"payloadType": "application/vnd.stellaops.zastava.thresholds+yaml;version=1",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc",
|
||||
"sig": "uQFBmx7vF4fj8uQsCiCN6VbxNS2m3XM-vJNFrj3rexL1PPzHH6IVtWRGexF7CsLrrpUV8U0AmS02S37vOk3zDA"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user