Data Schemas & Persistence Contracts
This reference describes the persistence contracts behind Stella Ops: the Valkey keyspace, the PostgreSQL tables, and the on-disk blob shapes that the scanner, policy, scheduler, and notify subsystems read and write.
Audience — backend developers, plug-in authors, and database administrators.
Altitude note. This document captures the shape, contracts, and invariants of each schema. Exhaustive field/enum/key lists live in source and are the authority — when this doc and the code disagree, the code wins. Each section points at the canonical model, JSON Schema, or sample fixture.
0 Document Conventions
- CamelCase for JSON.
- All timestamps are RFC 3339 / ISO 8601 with
Z(UTC). ⭑= planned but not shipped yet (kept on Feature Matrix “To Do”).
1 SBOM Wrapper Envelope
Every SBOM blob (regardless of format) is stored on disk or in object storage with a sidecar JSON file that indexes it for the scanners.
1.1 JSON Shape
A wrapper records the digest of the SBOM file (id), the source imageDigest, created, the SBOM format (trivy-json-v2 | spdx-json | cyclonedx-json), the ordered layers[], a partial flag, and a planned provenanceId link to an SLSA attestation (⭑).
{
"id": "sha256:417f…", // digest of the SBOM file itself
"imageDigest": "sha256:e2b9…", // digest of the original container image
"format": "trivy-json-v2", // trivy-json-v2 | spdx-json | cyclonedx-json
"layers": ["sha256:d38b…"], // ordered layer digests
"partial": false // true => delta SBOM (only some layers)
}
formatsupports multiple SBOM formats; partialis true when generated via the delta SBOM flow (§1.3).
1.2 File‑system Layout
blobs/
└─ 417f…/ # digest prefix
├─ sbom.json # payload (any format)
└─ sbom.meta.json # wrapper (shape above)
Note – RustFS is the primary object store; S3/MinIO compatibility layer available for legacy deployments; driver plug‑ins support multiple backends.
1.3 Delta SBOM Extension
When partial: true, only the missing layers have been scanned. Merging logic inside the Scanner stitches new data onto the cached full SBOM in Valkey, keyed off layers:* (§2).
2 Valkey Keyspace
Valkey (Redis-compatible) provides cache, DPoP nonces, event streams, and queues for real-time messaging and rate limiting.
| Key pattern | Type | TTL | Purpose |
|---|---|---|---|
scan:<digest> | string | ∞ | Last scan JSON result (as returned by /scan) |
layers:<digest> | set | 90d | Layers already possessing SBOMs (delta cache) |
policy:active | string | ∞ | YAML or Rego ruleset |
quota:<token> | string | until next UTC midnight | Per‑token scan counter for Free tier ({{quota_token}} scans). |
policy:history | list | ∞ | Change audit IDs (see PostgreSQL) |
feed:nvd:json | string | 24h | Normalised feed snapshot |
locator:<imageDigest> | string | 30d | Maps image digest → sbomBlobId |
dpop:<jti> | string | 5m | DPoP nonce cache (RFC 9449) for sender-constrained tokens |
events:* | stream | 7d | Event streams for Scheduler/Notify (Valkey Streams) |
queue:* | stream | — | Task queues (Scanner jobs, Notify deliveries) |
metrics:… | various | — | Prom / OTLP runtime metrics |
Delta SBOM uses
layers:*to skip work in <20 ms. Quota enforcement incrementsquota:<token>atomically; once the counter reaches the {{quota_token}}-scan ceiling the API returns 429. DPoP & Events: Valkey Streams support high-throughput, ordered event delivery for re-evaluation and notification triggers. Alternative: NATS JetStream can replace Valkey for queues (opt-in only; requires explicit configuration).
3 PostgreSQL Tables
PostgreSQL is the canonical persistent store for long-term audit and history.
| Table | Shape (summary) | Indexes |
|---|---|---|
sbom_history | Wrapper JSON + replace_ts on overwrite | (image_digest) (created) |
policy_versions | {id, yaml, rego, author_id, created, comment} | (created) |
attestations ⭑ | SLSA provenance doc + Rekor log pointer | (image_digest) |
audit_log | Fully rendered RFC 5424 entries (UI & CLI actions) | (user_id) (ts) |
policy_versions stores the YAML ruleset plus an optional rego field (filled only when Rego is uploaded), the authorId, created, and an import comment.
3.1 Scheduler Artifacts
Tables. schedules, runs, impact_snapshots, audit, run_summaries (module-local). All rows use the canonical JSON emitted by StellaOps.Scheduler.Models so agents and fixtures remain deterministic.
Canonical source — models:
src/JobEngine/StellaOps.Scheduler.__Libraries/StellaOps.Scheduler.Models/(Schedule.cs,Run.cs,ImpactSet.cs,AuditRecord.cs,RunSummary.cs,CanonicalJsonSerializer.cs,Validation.cs). Canonical source — fixtures:samples/api/scheduler/(schedule.json,run.json,impact-set.json,audit.json,run-summary.json). Use these for the exhaustive field-by-field shape.
The shapes and their load-bearing invariants:
Schedule (
schedules) —id,tenantId,name,enabled,cronExpression,timezone,mode, aselectionblock (scope / namespaces / repositories / tags / labels), optionalonlyIfandnotifygates,limits,subscribers, and audit timestamps. Invariants: arrays are alphabetically sorted;selection.tenantIdis optional but when present must matchtenantId; cron expressions are validated for newline/length; timezones viaTimeZoneInfo.Run (
runs) —scheduleId,trigger,state, astatscounter block, an optionalreason, lifecycle timestamps,error, and adeltas[]array of per-image findings (counts, KEV hits, top findings, attestation pointer). Invariants: counters clamped to ≥0; timestamps coerced to UTC; delta arrays sorted by severity precedence (critical → info) then vulnerability id; missingdeltas⇒ “no change”.Impact Snapshot (
impact_snapshots) — aselector, animages[]set (digest / registry / repository / namespaces / tags / usage / labels),usageOnly,total, andsnapshotId. Invariants: images deduplicated and sorted by digest; label keys lowercased to avoid case-sensitive duplicates;snapshotIdlets run planners diff snapshots for drift.Audit (
audit) —category,action,occurredAt, anactor, optionalscheduleId/runId,correlationId, ametadatamap, and amessage. Invariants: metadata keys lowercased, first-writer wins (casing duplicates ignored); empty optional IDs trimmed; emit via the canonical serializer so audit digests stay reproducible.Run Summary (
run_summaries) — materialized roll-up powering Scheduler UI dashboards (last-run banners, sparklines) without scanningruns. HoldslastRun, arecent[]window, and aggregatecounters. Invariants:_id=tenant:schedule;recentkeeps the 20 newest runs bycreatedAt(UTC), replacing the entry per run to honour state transitions;countersaggregate over that 20-run window and are recomputed on every update; the projection service must be called after each run state change. Sample:samples/api/scheduler/run-summary.json.
4 Policy Schema (YAML v1.0)
Minimal viable grammar (subset of OSV‑SCHEMA ideas) — a version plus a list of rules, each with a name, severity[], an action (block | ignore | escalate), and optional environments, sources, and expires.
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
- name: Ignore Low Dev
severity: [Low, None]
environments: [dev, staging]
action: ignore
expires: "2026-01-01"
Canonical schema:
src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-schema@1.json(embedded intoStellaOps.Policy). Reusable validator:src/Policy/__Libraries/StellaOps.Policy/PolicyValidationCli.cs(PolicyValidationCli.RunAsync(PolicyValidationCliOptions{ Inputs, Strict })) — the command handler the main CLI wires up.
4.1 Rego Variant (Advanced ⭑)
Accepted but stored as-is in the rego field. Planned for evaluation via an internal OPA side-car once the feature ships.
4.2 Policy Scoring Config (JSON)
Configures how findings are scored: version, a severityWeights map, action penalties (quietPenalty / warnPenalty / ignorePenalty), trustOverrides, reachabilityBuckets (numeric multipliers, default ≤1.0), and an unknownConfidence block (initial / decayPerDay / floor plus ordered confidence bands).
Schema id:
https://schemas.stella-ops.org/policy/policy-scoring-schema@1.jsonSource:src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-scoring-schema@1.json; default fixture…/Schemas/policy-scoring-default.json(both embedded inStellaOps.Policy). The exhaustive default weights/keys live there.
Validation runs alongside policy binding (PolicyScoringConfigBinder), producing deterministic digests via PolicyScoringConfigDigest. Bands are ordered descending by min so consumers resolve confidence tiers deterministically. Reachability bucket keys are case-insensitive.
Runtime usage (contract)
trustOverridesmatchfinding.tags(trust:<key>) first, thenfinding.source/finding.vendor; missing keys default to1.0.reachabilityBucketsconsumefinding.tagsprefixedreachability:(fallbackusage:orunknown). Missing buckets fall back to theunknownweight when present, else1.0.- Policy verdicts expose scoring inputs (
severityWeight,trustWeight,reachabilityWeight,baseScore, penalties) plus unknown-state metadata (unknownConfidence,unknownAgeDays,confidenceBand) for auditability. Reference payloads:samples/policy/policy-preview-unknown.json,samples/policy/policy-report-unknown.json. - Unknown confidence derives from
unknown-age-days:(preferred) orunknown-since:+observed-at:tags; with no hints the engine keepsinitialconfidence, decaying bydecayPerDaydown tofloor, then resolving to the first matching band.
Validate the sample payloads with Ajv (--spec=draft2020 -c ajv-formats) against their published schemas before changing them; see the Docs CI validation loop in §6.
5 SLSA Attestation Schema ⭑
Planned for Q1‑2026 (kept here for early plug‑in authors). A provenance doc carries id, imageDigest, buildType, a builder, build metadata (invocation parameters + timestamps + completeness), materials[], and a rekorLogIndex into the local Rekor mirror — following the SLSA provenance v1 layout.
6 Notify Foundations (Rule · Channel · Template · Event)
Canonically describes the Notify data shapes that UI, workers, and storage consume. JSON Schemas live under docs/modules/notify/resources/schemas/; deterministic fixtures under docs/modules/notify/resources/samples/.
| Artifact | Schema | Sample |
|---|---|---|
| Rule (catalogued routing logic) | …/schemas/notify-rule@1.json | …/samples/notify-rule@1.sample.json |
| Channel (delivery endpoint definition) | …/schemas/notify-channel@1.json | …/samples/notify-channel@1.sample.json |
| Template (rendering payload) | …/schemas/notify-template@1.json | …/samples/notify-template@1.sample.json |
| Event envelope (Notify ingest surface) | …/schemas/notify-event@1.json | …/samples/notify-event@1.sample.json |
Cross-cutting invariants (all four artifacts):
- Keys are lower-cased camelCase; each document pins a
schemaVersion(notify.rule@1,notify.channel@1,notify.template@1). UseStellaOps.Notify.Models.NotifySchemaMigration.Upgrade*(JsonNode)(src/Notify/__Libraries/StellaOps.Notify.Models/NotifySchemaMigration.cs) to backfill the version when deserialising legacy payloads. - Array selectors are pre-sorted and case-normalised for deterministic serialisation.
- Rules, channels, and templates carry a soft-delete marker (
deletedAt) persisted in PostgreSQL; repository/API queries filter them by default while retaining revision history.
Per-artifact notes (shape is in the schema; these are the contracts that aren’t):
- Rule — mandatory
match+actions;actions[].throttleserialises as an ISO 8601 duration (PT5M) mirroring worker backoff; optionalvexgates exclude accepted/not-affected justifications. - Channel —
typematches plug-in identifiers (slack/teams/email/webhook/custom);config.secretRefstores an external secret handle (Authority/Vault/K8s) — Notify never persists raw credentials;config.limits.timeoutuses ISO 8601 durations. - Template — presentation key (
channelType/key/locale) plus raw body;renderModeenumerates engines (aligns withNotifyTemplateRenderMode);formatsignals the downstream connector. - Event envelope — platform event contract:
eventIdUUID, RFC 3339ts, tenant isolation; enumeratedkind(e.g.scanner.report.ready,scheduler.rescan.delta,zastava.admission);scope.labels/scope.attributesplus top-levelattributesmirror the worker templating/audit metadata. Workers wrap payloads via the same migration helper so schema additions stay additive.
Validation loop (matches Docs CI): compile every …/schemas/*.json with npx ajv compile -c ajv-formats, then validate each *.sample.json against its sibling schema with npx ajv validate. Integration tests embed the sample fixtures to guarantee deterministic serialisation from the StellaOps.Notify.Models DTOs.
7 Validator Contracts
- SBOM wrapper –
ISbomValidator(DLL plug‑in) must return a typed error list. - YAML policies – JSON‑Schema (§4,
policy-schema@1.json). - Rego – OPA
opa eval --fail-definedunder the hood. - Free‑tier quotas –
IQuotaServiceintegration tests ensurequota:<token>resets at UTC midnight and produces correctRetry‑Afterheaders.
8 Migration Notes
- Add
formatcolumn to existing SBOM wrappers; default totrivy-json-v2. - Populate
layers&partialvia backfill script (ships withstellopsctl migratewizard). - Policy YAML previously stored in Valkey → copy to PostgreSQL if persistence enabled.
- Prepare
attestationstable (empty) – safe to create in advance.
9 Open Questions / Future Work
- How to de‑duplicate identical Rego policies differing only in whitespace?
- Embed GOST 34.11‑2018 digests when users enable Russian crypto suite?
- Should enterprise tiers share the same Valkey quota keys or switch to a JWT claim
tier != Freebypass? - Evaluate sliding‑window quota instead of strict daily reset.
- Consider rate‑limit for
/layers/missingto avoid brute‑force enumeration.
10 Change Log
| Date | Note |
|---|---|
| 2025‑07‑14 | Added: format, partial, delta cache keys, YAML policy schema v1.0. |
| 2025‑07‑12 | Initial public draft – SBOM wrapper, Valkey keyspace, audit collections. |
