feat(graph): introduce graph.inspect.v1 contract and schema for SBOM relationships
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Console CI / console-ci (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Console CI / console-ci (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
- Added graph.inspect.v1 documentation outlining payload structure and determinism rules. - Created JSON schema for graph.inspect.v1 to enforce payload validation. - Defined mapping rules for graph relationships, advisories, and VEX statements. feat(notifications): establish remediation blueprint for gaps NR1-NR10 - Documented requirements, evidence, and tests for Notifier runtime. - Specified deliverables and next steps for addressing identified gaps. docs(notifications): organize operations and schemas documentation - Created README files for operations, schemas, and security notes to clarify deliverables and policies. feat(advisory): implement PostgreSQL caching for Link-Not-Merge linksets - Created database schema for advisory linkset cache. - Developed repository for managing advisory linkset cache operations. - Added tests to ensure correct functionality of the AdvisoryLinksetCacheRepository.
This commit is contained in:
@@ -3,34 +3,37 @@
|
||||
Scope: Evidence Bundle v1 produced by Evidence Locker and consumed by Concelier, Excititor, Export Center, CLI, and Policy Engine.
|
||||
|
||||
## Predicates & subjects
|
||||
- **Subject**: OCI manifest digest (`sha256:<digest>`) of the bundle, plus optional replay pack digest.
|
||||
- **Subject (mandatory):** Merkle root derived from `checksums.txt` (sha256 of the sorted file) for the sealed bundle. OCI digest of the tarball remains recorded as `bundle_oci_digest` inside the predicate for registry mirroring but is not the DSSE subject.
|
||||
- **Predicates (DSSE/In-Toto)**
|
||||
- `stellaops.evidence.bundle.v1`: declares bundle layout (manifests, CAS paths, replay log offsets).
|
||||
- `stellaops.evidence.transparency.v1`: optional Rekor log inclusion proof (UUID, log index, root hash at inclusion).
|
||||
- `stellaops.evidence.integrity.v1`: hashes for each payload (SBOMs, VEX, policy packs, telemetry snapshots), keyed by logical path.
|
||||
- `stellaops.evidence.bundle.v1`: declares bundle layout (manifests, CAS paths, replay log offsets, Merkle recipe, chunking strategy).
|
||||
- `stellaops.evidence.transparency.v1`: Rekor/log inclusion proof (UUID, log index, root hash at inclusion). Required when network-permitted; when offline, include `reason="offline"` and omit log pointers.
|
||||
- `stellaops.evidence.integrity.v1`: hashes for each payload (SBOMs, VEX, policy packs, telemetry snapshots), keyed by canonical path; must match entries in `bundle.manifest.schema.json`.
|
||||
|
||||
## Required claim set
|
||||
- `bundle_id` (UUID v4)
|
||||
- `produced_at` (UTC ISO-8601)
|
||||
- `producer` (`evidence-locker:<region>`)
|
||||
- `subject_digest` (OCI digest string)
|
||||
- `hashes` (map: logical path → sha256)
|
||||
- `subject_merkle_root` (sha256 from checksums.txt)
|
||||
- `hashes` (map: canonicalPath → sha256) sorted lexicographically
|
||||
- `sbom` (array of SPDX/CycloneDX digests and mediaTypes)
|
||||
- `vex` (array of VEX doc digests and schema versions)
|
||||
- `replay_manifest` (optional; digest + sequence number)
|
||||
- `transparency` (optional; Rekor UUID, logIndex, rootHash)
|
||||
- `replay_manifest` (optional; digest + sequence number, ledger URI, dsseEnvelope)
|
||||
- `transparency` (optional; Rekor UUID, logIndex, rootHash, inclusionProof) or `reason="offline"`
|
||||
- `signing_profile` (`sovereign-default` | `fips` | `gost` | `pq-experimental`)
|
||||
|
||||
## Bundling & signing rules
|
||||
- DSSE envelope using the module’s configured crypto provider; keys must be short-lived (<24h) and recorded in provider registry.
|
||||
- Hash list and subject digest MUST match the on-disk CAS objects; deterministic sort by logical path.
|
||||
- Rekor entry is optional; when absent, set `transparency=null` and add `transparency_reason="offline"` to provenance note.
|
||||
- DSSE envelope is **mandatory** for every sealed bundle using the configured `ICryptoProviderRegistry` profile; keys must be short-lived (<24h) and recorded in provider registry.
|
||||
- Subject = sha256(Merkle root) from `checksums.txt`; verifier must recompute to match.
|
||||
- Hash list must match `bundle.manifest.schema.json` (entries + optional `hashSummary`), sorted by `canonicalPath`.
|
||||
- Rekor/logging policy:
|
||||
- If outbound log is allowed, submit to configured log and embed UUID/logIndex/rootHash in `transparency`.
|
||||
- If outbound log is disallowed/offline, set `transparency` to null and include `reason="offline"` plus `log_policy="skip"` inside the predicate.
|
||||
|
||||
## Verification plan
|
||||
- Verify DSSE signature against provider registry (per profile) and check key expiry.
|
||||
- Recompute sha256 for every CAS object; fail if any mismatch.
|
||||
- If `transparency` present, verify inclusion proof against bundled Rekor root; fail closed on mismatch.
|
||||
- Emit verification report JSON and store beside bundle as `verify.json` (deterministic key order).
|
||||
- Recompute sha256 for every manifest entry and the Merkle root; fail if subject differs.
|
||||
- If `transparency` present, verify inclusion proof against bundled Rekor root; fail closed on mismatch. If absent, require `reason="offline"` and `log_policy="skip"`.
|
||||
- Emit verification report JSON (deterministic key order) and store beside bundle as `verify.json`.
|
||||
|
||||
## Fixtures
|
||||
- Sample bundle + report: `docs/samples/evidence-locker/bundle-v1-sample.tar.gz` (sha256 TBD at publish time).
|
||||
|
||||
@@ -38,7 +38,7 @@ The endpoint reuses `EvidenceBundlePackagingService` and caches the packaged obj
|
||||
|
||||
## Verification guidance
|
||||
|
||||
Upcoming EB1–EB10 remediation (Sprint 0161; advisory `docs/product-advisories/28-Nov-2025 - Evidence Bundle and Replay Contracts.md`):
|
||||
Upcoming EB1–EB10 remediation (Sprint 0161; advisory `docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Evidence Bundle and Replay Contracts.md`):
|
||||
- Publish `bundle.manifest.schema.json` and `checksums.schema.json` with canonical JSON rules and signatures.
|
||||
- Document the Merkle hash recipe and DSSE predicate/log policy.
|
||||
- Ship an offline verifier script and golden bundles/replay fixtures to prove determinism.
|
||||
@@ -61,6 +61,13 @@ Use the resulting root as the DSSE subject and store `checksums.txt` inside the
|
||||
|
||||
These steps match the offline procedure described in `docs/forensics/evidence-locker.md` (Portable Evidence section). Update that guide whenever packaging fields change.
|
||||
|
||||
### Merkle + CAS rules (EB3/EB6)
|
||||
- **Canonical inventory:** `checksums.txt` MUST be generated from the manifest entries sorted lexicographically by `canonicalPath`.
|
||||
- **Subject binding:** DSSE subject is `sha256(checksums.txt)` (Merkle root); OCI digest of `bundle.tgz` is secondary metadata only.
|
||||
- **Chunking strategy:** Default `strategy=none`. When chunked CAS storage is enabled, record `chunking.strategy`, `chunkSizeBytes`, and `casDigestAlgorithm` in `checksums.schema.json`; chunk hashes are folded deterministically (fixed-size or buzhash) before the per-entry sha256 is calculated.
|
||||
- **Compression invariants:** tar → gzip with pinned mtime (`2025-01-01T00:00:00Z`), `0644` perms, uid/gid `0:0`, UTF-8 headers; failing these invalidates fixtures.
|
||||
- **Stable tooling:** CI must pin `tar`, `gzip`, and hashing tool versions; regenerate golden fixtures only when these pins change and record the versions in `tests/EvidenceLocker/Bundles/Golden/expected.json`.
|
||||
|
||||
## Portable bundle (`portable-bundle-v1.tgz`)
|
||||
|
||||
When sealed or air-gapped environments need a redacted evidence artifact, request:
|
||||
|
||||
@@ -8,6 +8,7 @@ Incident mode is a service-wide switch that increases forensic fidelity when Ste
|
||||
2. **Debug artefacts.** Snapshot requests emit an `incident/request-*.json` payload into the object store. The payload captures the normalized request metadata/materials plus the incident stamp so offline replay tooling has everything it needs. The manifest surfaces the artefact under the `incident/` section and packaging streams it alongside the canonical bundle files.
|
||||
3. **Manifest metadata.** Bundles carry `incident.mode`, `incident.changedAt`, and `incident.retentionExtensionDays` metadata so verifiers and auditors can see exactly when the mode toggled and how long retention was extended.
|
||||
4. **Operational signals.** Activation/deactivation events are published to the Timeline Indexer (and, via the notifier stub, to the future Notify integration). The `IEvidenceTimelinePublisher` now emits `evidence.incident.mode` with `state` and retention attributes, giving Ops a canonical audit trail.
|
||||
5. **Signed activation/exit (EB7).** Incident toggles MUST be recorded inside the bundle manifest (`incident` block) and in the DSSE predicate with signer identity and timestamp. Offline environments must still include the activation record even when no Rekor entry is possible; when online, include transparency log pointers alongside the incident record.
|
||||
|
||||
Configuration lives under `EvidenceLocker:Incident`:
|
||||
|
||||
@@ -22,3 +23,8 @@ Configuration lives under `EvidenceLocker:Incident`:
|
||||
```
|
||||
|
||||
`IncidentModeManager` watches the options and raises events whenever the state flips. Tests cover retention math, timeline/notifier fan-out, and the new debug artefact path.
|
||||
|
||||
## Recording rules (EB7)
|
||||
- Manifest fields (see `bundle.manifest.schema.json`): `incident.activatedAt`, `incident.activatedBy`, `incident.reason`, and optional `incident.deactivatedAt`, `incident.deactivatedBy`.
|
||||
- DSSE predicate: include `incident` block mirroring the manifest plus `signer` (provider/keyid) and `log_policy` (`submit` | `skip-offline`).
|
||||
- CLI/API: activation/deactivation commands must require a reason string and emit a signed record even when the state is unchanged (idempotent write with identical payload hash).
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
# Replay Payload Contract (Prep for PREP-EVID-REPLAY-187-001)
|
||||
|
||||
Status: **Ready for implementation** (2025-11-20)
|
||||
Status: **Ready for implementation** (2025-12-04 refresh for EB5)
|
||||
Owners: Evidence Locker Guild · Scanner Guild
|
||||
Scope: Define deterministic scanner record payload shape required to ingest replay bundles (Sprint 0187).
|
||||
Scope: Define deterministic scanner record payload shape required to ingest replay bundles (Sprint 0187) and to link replay provenance into bundle manifests and attestations (EB5).
|
||||
|
||||
## Payload shape
|
||||
- NDJSON per record; sorted by `recordedAtUtc` then `scanId`.
|
||||
- NDJSON per record; sorted by `recordedAtUtc` then `scanId` (stable ordering required by `docs/replay/DETERMINISTIC_REPLAY.md`).
|
||||
- Fields:
|
||||
- `scanId` (GUID), `tenantId`, `subjectDigest` (sha256:...), `scanKind` (sbom|vuln|policy),
|
||||
- `startedAtUtc`, `completedAtUtc` (ISO-8601),
|
||||
- `startedAtUtc`, `completedAtUtc`, `recordedAtUtc` (ISO-8601, UTC),
|
||||
- `artifacts`: array of `{ type: sbom|vex|log, digest, uri }`,
|
||||
- `provenance`: `{ dsseEnvelope, transparencyLog? }` (base64 DSSE; optional Rekor entry),
|
||||
- `summary`: `{ findings: int, advisories: int, policies: int }`.
|
||||
- Determinism: no wall-clock except the recorded timestamps above; DSSE envelope copied verbatim from scanner output.
|
||||
- Replay provenance (new, EB5):
|
||||
- Evidence Locker records `replayProvenance` in the bundle manifest with `recordDigest`, optional `sequence`, `ledgerUri`, `dsseEnvelope`, and optional `transparencyLog { rekorUuid, logIndex, inclusionProof }`.
|
||||
- The `recordDigest` is the sha256 of the canonical NDJSON content (as ingested) and becomes part of `hashSummary` in the manifest and the DSSE predicate.
|
||||
- Determinism: no wall-clock except the recorded timestamps above; DSSE envelope copied verbatim from scanner output; NDJSON must be UTF-8, LF line endings, and canonical key ordering per record.
|
||||
|
||||
## Acceptance criteria
|
||||
- Scanner Guild provides sample NDJSON (10 records) with DSSE envelope redacted allowed.
|
||||
- Evidence Locker can ingest and store bundle with deterministic ordering and hash (SHA256) across runs.
|
||||
- Contract published here and referenced in Sprint 0187 P1/P2/P3.
|
||||
- Evidence Locker can ingest and store bundle with deterministic ordering and hash (SHA256) across runs; recomputes `recordDigest` to bind replayProvenance.
|
||||
- Contract published here and referenced in Sprint 0187 P1/P2/P3 and manifests/attestations per EB5.
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ This reference describes the Export Center API introduced in Export Center Phase
|
||||
- `export:download` for bundle downloads and manifests.
|
||||
- **Tenant context:** Provide `X-Stella-Tenant` when the token carries multiple tenants; defaults to token tenant otherwise.
|
||||
- **Idempotency:** Mutating endpoints accept `Idempotency-Key` (UUID). Retrying with the same key returns the original result.
|
||||
- **Rate limits and quotas:** Responses include `X-Stella-Quota-Limit`, `X-Stella-Quota-Remaining`, and `X-Stella-Quota-Reset`. Exceeding quotas returns `429 Too Many Requests` with `ERR_EXPORT_QUOTA`.
|
||||
- **Rate limits and quotas:** Responses include `X-Stella-Quota-Limit`, `X-Stella-Quota-Remaining`, and `X-Stella-Quota-Reset`. Exceeding quotas returns `429 Too Many Requests` with `ERR_EXPORT_QUOTA`.
|
||||
- **Integrity headers (downloads):** `Digest: sha-256=<base64>`, `X-Stella-Signature: dsse-b64=<payload>`, and `X-Stella-Immutability: true` accompany bundle/manifest downloads; clients must validate before use.
|
||||
- **Content negotiation:** Requests and responses use `application/json; charset=utf-8` unless otherwise stated. Downloads stream binary content with profile-specific media types.
|
||||
- **SSE:** Event streams set `Content-Type: text/event-stream` and keep connections alive with comment heartbeats every 15 seconds.
|
||||
|
||||
@@ -100,21 +101,29 @@ Scopes: export:profile:manage
|
||||
**Request**
|
||||
|
||||
```json
|
||||
{
|
||||
"profileId": "prof-airgap-mirror",
|
||||
"name": "Airgap Mirror Weekly",
|
||||
"kind": "mirror",
|
||||
"variant": "full",
|
||||
"include": ["advisories", "vex", "sboms", "policy"],
|
||||
"distribution": ["http", "object"],
|
||||
"encryption": {
|
||||
"enabled": true,
|
||||
"recipientKeys": ["age1tenantkey..."],
|
||||
"strict": false
|
||||
},
|
||||
"retention": {"mode": "days", "value": 30}
|
||||
}
|
||||
```
|
||||
{
|
||||
"profileId": "prof-airgap-mirror",
|
||||
"name": "Airgap Mirror Weekly",
|
||||
"kind": "mirror",
|
||||
"variant": "full",
|
||||
"include": ["advisories", "vex", "sboms", "policy"],
|
||||
"distribution": ["http", "object"],
|
||||
"encryption": {
|
||||
"enabled": true,
|
||||
"recipientKeys": ["age1tenantkey..."],
|
||||
"strict": false
|
||||
},
|
||||
"retention": {"mode": "days", "value": 30},
|
||||
"limits": {
|
||||
"maxActiveRuns": 4,
|
||||
"maxQueuedRuns": 50,
|
||||
"backpressureMode": "reject"
|
||||
},
|
||||
"approval": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 201**
|
||||
|
||||
@@ -183,16 +192,24 @@ Scopes: export:run
|
||||
{
|
||||
"runId": "run-20251029-01",
|
||||
"status": "pending",
|
||||
"profileId": "prof-json-raw",
|
||||
"createdAt": "2025-10-29T12:12:11Z",
|
||||
"createdBy": "user:ops",
|
||||
"selectors": { "...": "..." },
|
||||
"links": {
|
||||
"self": "/api/export/runs/run-20251029-01",
|
||||
"events": "/api/export/runs/run-20251029-01/events"
|
||||
}
|
||||
}
|
||||
```
|
||||
"profileId": "prof-json-raw",
|
||||
"createdAt": "2025-10-29T12:12:11Z",
|
||||
"createdBy": "user:ops",
|
||||
"selectors": { "...": "..." },
|
||||
"links": {
|
||||
"self": "/api/export/runs/run-20251029-01",
|
||||
"events": "/api/export/runs/run-20251029-01/events"
|
||||
},
|
||||
"quotas": {
|
||||
"maxActiveRuns": 4,
|
||||
"maxQueuedRuns": 50,
|
||||
"backpressureMode": "reject"
|
||||
},
|
||||
"approval": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 List runs
|
||||
|
||||
@@ -214,11 +231,15 @@ Response fields:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `status` | `pending`, `running`, `success`, `failed`, `canceled`. |
|
||||
| `progress` | Object with `adapters`, `bytesWritten`, `recordsProcessed`. |
|
||||
| `errorCode` | Populated when `status=failed` (`signing`, `distribution`, etc). |
|
||||
| `policySnapshotId` | Returned for policy-aware profiles. |
|
||||
| `distributions` | List of available distribution descriptors (type, location, sha256, expiresAt). |
|
||||
| `status` | `pending`, `running`, `success`, `failed`, `canceled`. |
|
||||
| `progress` | Object with `adapters`, `bytesWritten`, `recordsProcessed`. |
|
||||
| `errorCode` | Populated when `status=failed` (`signing`, `distribution`, etc). |
|
||||
| `policySnapshotId` | Returned for policy-aware profiles. |
|
||||
| `distributions` | List of available distribution descriptors (type, location, sha256, expiresAt). |
|
||||
| `rerunHash` | SHA-256 over sorted `contents[*].digest`; used for determinism checks. |
|
||||
| `integrity` | Expected HTTP headers (`Digest`, `X-Stella-Signature`, `X-Stella-Immutability`) and OCI annotations (`io.stellaops.export.*`). |
|
||||
| `quotas` | Active limits/backpressure settings returned with the run. |
|
||||
| `approval` | Cross-tenant approval ticket when selectors span multiple tenants/wildcards. |
|
||||
|
||||
### 4.4 Cancel a run
|
||||
|
||||
@@ -273,14 +294,16 @@ GET /api/export/runs/{runId}/download
|
||||
Scopes: export:download
|
||||
```
|
||||
|
||||
Streams the primary bundle (tarball, zip, or profile-specific layout). Headers:
|
||||
|
||||
- `Content-Disposition: attachment; filename="export-run-20251029-01.tar.zst"`
|
||||
- `X-Export-Digest: sha256:...`
|
||||
- `X-Export-Size: 73482019`
|
||||
- `X-Export-Encryption: age` (when mirror encryption enabled)
|
||||
|
||||
Supports HTTP range requests for resume functionality. If no bundle exists yet, responds `409` with `ERR_EXPORT_007`.
|
||||
Streams the primary bundle (tarball, zip, or profile-specific layout). Headers:
|
||||
|
||||
- `Content-Disposition: attachment; filename="export-run-20251029-01.tar.zst"`
|
||||
- `Digest: sha-256=<base64>` (EC5)
|
||||
- `X-Stella-Signature: dsse-b64:<payload>` (EC3/EC5)
|
||||
- `X-Stella-Immutability: true`
|
||||
- `X-Export-Size: 73482019`
|
||||
- `X-Export-Encryption: age` (when mirror encryption enabled)
|
||||
|
||||
Supports HTTP range requests for resume functionality. If no bundle exists yet, responds `409` with `ERR_EXPORT_007`.
|
||||
|
||||
### 6.2 Manifest download
|
||||
|
||||
@@ -289,7 +312,8 @@ GET /api/export/runs/{runId}/manifest
|
||||
Scopes: export:download
|
||||
```
|
||||
|
||||
Returns signed `export.json`. To fetch the detached signature, append `?signature=true`.
|
||||
Returns signed `export.json`. To fetch the detached signature, append `?signature=true`.
|
||||
- Integrity annotations are mirrored in response headers (`Digest`, `X-Stella-Signature`, `X-Stella-Immutability`) and in the manifest `integrity` block to keep rerun-hash deterministic.
|
||||
|
||||
### 6.3 Provenance download
|
||||
|
||||
@@ -298,7 +322,7 @@ GET /api/export/runs/{runId}/provenance
|
||||
Scopes: export:download
|
||||
```
|
||||
|
||||
Returns signed `provenance.json`. Supports `?signature=true`. Provenance includes attestation subject digests, policy snapshot ids, adapter versions, and KMS key identifiers.
|
||||
Returns signed `provenance.json`. Supports `?signature=true`. Provenance includes attestation subject digests, policy snapshot ids, adapter versions, and KMS key identifiers.
|
||||
|
||||
### 6.4 Distribution descriptors
|
||||
|
||||
|
||||
@@ -144,16 +144,20 @@ stella export provenance run-20251029-01 --output manifests/provenance.json
|
||||
|
||||
Retrieves the signed provenance file. `--signature` behaves like the manifest command.
|
||||
|
||||
### 4.4 `stella export verify`
|
||||
|
||||
```
|
||||
stella export verify run-20251029-01 \
|
||||
--manifest manifests/export.json \
|
||||
--provenance manifests/provenance.json \
|
||||
--key keys/acme-export.pub
|
||||
```
|
||||
|
||||
Wrapper around `cosign verify`. Returns exit `0` when signatures and digests validate. Exit `20` when verification fails.
|
||||
### 4.4 `stella export verify`
|
||||
|
||||
```
|
||||
stella export verify run-20251029-01 \
|
||||
--manifest manifests/export.json \
|
||||
--provenance manifests/provenance.json \
|
||||
--key keys/acme-export.pub
|
||||
```
|
||||
|
||||
Wrapper around `cosign verify`. Returns exit `0` when signatures and digests validate. Exit `20` when verification fails.
|
||||
|
||||
Integrity and determinism checks (EC1–EC10):
|
||||
- `stella export manifest` and `provenance` commands emit `Digest`/`X-Stella-Signature` headers; cache them for rerun-hash validation.
|
||||
- Offline kits: run `docs/modules/export-center/operations/verify-export-kit.sh <kit_dir>` to assert rerunHash, integrity headers vs OCI annotations, quotas/backpressure block, approvals, and log metadata in provenance.
|
||||
|
||||
## 5. CI recipe (GitHub Actions example)
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"schemaVersion": "graph.inspect.v1",
|
||||
"tenant": "acme-dev",
|
||||
"artifactDigest": "sha256:8f2c1f4c8f9d4c3bb2efc0a9d0a35d4492a0bba4f3c1a2b9d5c7e1f4a8c6b2d1",
|
||||
"sbomDigest": "sha256:1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
|
||||
"collectedAt": "2025-12-04T15:30:00Z",
|
||||
"components": [
|
||||
{
|
||||
"purl": "pkg:maven/org.example/foo@1.2.3",
|
||||
"version": "1.2.3",
|
||||
"scopes": [
|
||||
"runtime"
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"type": "contains",
|
||||
"targetPurl": "pkg:docker/library/alpine@3.19.0",
|
||||
"scope": "runtime",
|
||||
"evidenceHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
|
||||
"source": "scanner.sbom.v1"
|
||||
},
|
||||
{
|
||||
"type": "depends_on",
|
||||
"targetPurl": "pkg:npm/lodash@4.17.21",
|
||||
"scope": "runtime",
|
||||
"evidenceHash": "89abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345",
|
||||
"source": "concelier.linkset.v1"
|
||||
}
|
||||
],
|
||||
"advisories": [
|
||||
{
|
||||
"advisoryId": "CVE-2024-1111",
|
||||
"source": "ghsa",
|
||||
"status": "affected",
|
||||
"severity": "HIGH",
|
||||
"cvss": {
|
||||
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"score": 9.8
|
||||
},
|
||||
"justification": "exploitable_in_default_config",
|
||||
"justificationSummary": "Unauthenticated RCE in JSON parser; no mitigations applied.",
|
||||
"linksetDigest": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
||||
"evidenceHash": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
|
||||
"modifiedAt": "2025-11-30T12:00:00Z",
|
||||
"provenance": {
|
||||
"source": "concelier.linkset.v1",
|
||||
"collectedAt": "2025-11-30T11:55:00Z",
|
||||
"eventOffset": 4421,
|
||||
"linksetDigest": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
||||
"evidenceHash": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
||||
}
|
||||
}
|
||||
],
|
||||
"vexStatements": [
|
||||
{
|
||||
"statementId": "VEX-2025-0001",
|
||||
"source": "excitor.vex.v1",
|
||||
"status": "not_affected",
|
||||
"justification": "component_not_present",
|
||||
"impactStatement": "Library excluded from production image; only used in tests.",
|
||||
"knownExploited": false,
|
||||
"issuedAt": "2025-12-01T08:00:00Z",
|
||||
"expiresAt": "2026-12-01T00:00:00Z",
|
||||
"evidenceHash": "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
|
||||
"provenance": {
|
||||
"source": "excitor.overlay.v1",
|
||||
"collectedAt": "2025-12-01T08:00:00Z",
|
||||
"eventOffset": 171,
|
||||
"evidenceHash": "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"provenance": {
|
||||
"source": "concelier.linkset.v1",
|
||||
"collectedAt": "2025-12-04T15:29:00Z",
|
||||
"eventOffset": 5123,
|
||||
"linksetDigest": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
||||
"evidenceHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"sbomObservationEventId": "obs-2025-11-22-001",
|
||||
"linksetDigest": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
}
|
||||
}
|
||||
48
docs/modules/graph/contracts/graph.inspect.v1.md
Normal file
48
docs/modules/graph/contracts/graph.inspect.v1.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# graph.inspect.v1 · Inspector Contract (2025-12-04)
|
||||
|
||||
Replaces the archived Cartographer handshake note and grounds the Concelier/Excititor → Graph Indexer/API interface. Use this shape for any inspector payloads that carry SBOM relationships plus advisory/VEX linkouts destined for graph ingestion or replay.
|
||||
|
||||
## Purpose
|
||||
- Give Concelier (Link-Not-Merge v1) and Excititor a frozen, deterministic payload so graph nodes/edges can be regenerated or replayed offline.
|
||||
- Encode the evidence hashes and provenance needed for append-only rebuilds and audit trails.
|
||||
- Keep ordering stable to avoid hash churn between runs.
|
||||
|
||||
## Payload shape (summary)
|
||||
- `schemaVersion` — must be `graph.inspect.v1`.
|
||||
- `tenant`, `artifactDigest`, `sbomDigest`, `collectedAt`.
|
||||
- `components[]` (ordered):
|
||||
- `purl`, optional `version`, optional sorted `scopes[]`.
|
||||
- `relationships[]` → `contains|depends_on|provides|runtime_observed` with `targetPurl`, `scope?`, `source`, `evidenceHash`.
|
||||
- `advisories[]` → `advisoryId`, `source`, `status`, `severity?`, `cvss?`, `justification?`, `linksetDigest?`, `evidenceHash`, `modifiedAt`.
|
||||
- `vexStatements[]` → `statementId`, `source`, `status`, `justification`, `impactStatement?`, `knownExploited?`, `issuedAt`, `expiresAt?`, `evidenceHash`.
|
||||
- `provenance` → `source`, `collectedAt`, `eventOffset?`, `linksetDigest?`, `evidenceHash?`.
|
||||
- Optional `links.sbomObservationEventId` and `links.linksetDigest` for traceability.
|
||||
|
||||
Full machine-readable schema: `docs/modules/graph/contracts/graph.inspect.v1.schema.json`.
|
||||
|
||||
## Determinism rules
|
||||
- Preserve input order for `components[]` and `relationships[]`; sort `scopes`, `advisories`, and `vexStatements` by (`source`, `advisoryId`/`statementId`) before emitting.
|
||||
- All timestamps UTC ISO-8601; hashes are SHA-256 hex.
|
||||
- Keep arrays unique where noted (`scopes`), and avoid null/empty strings.
|
||||
|
||||
## Graph mapping
|
||||
- `relationships` → graph edges: `contains/depends_on/provides/runtime_observed` align with `CONTAINS`, `DEPENDS_ON`, `PROVIDES`, `OBSERVED_RUNTIME`.
|
||||
- `advisories` → `advisory` nodes + `AFFECTED_BY` edges (use `linksetDigest` and `evidenceHash` for provenance).
|
||||
- `vexStatements` → `vex_statement` nodes + `VEX_EXEMPTS` edges (propagate `justification`, `knownExploited`, `impactStatement`).
|
||||
- `provenance` fields flow into graph `provenance` objects and `eventOffset` becomes the replay cursor.
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
# Example (offline-friendly if ajv is already installed locally)
|
||||
ajv validate -s docs/modules/graph/contracts/graph.inspect.v1.schema.json \
|
||||
-d docs/modules/graph/contracts/examples/graph.inspect.v1.sample.json
|
||||
```
|
||||
Use any JSON Schema Draft 2020-12 validator if `ajv` is unavailable.
|
||||
|
||||
## Sample
|
||||
See `docs/modules/graph/contracts/examples/graph.inspect.v1.sample.json` for a one-component payload that exercises relationships, advisory observations, VEX statements, and provenance fields.
|
||||
|
||||
## Upstream alignment
|
||||
- Link-Not-Merge fixtures: `docs/modules/concelier/link-not-merge-schema.md`.
|
||||
- Excititor overlay prep: `docs/modules/excititor/prep/2025-11-20-graph-21-001-prep.md` and `...graph-21-002-prep.md`.
|
||||
- Graph canonical model: `docs/modules/graph/schema.md` (node/edge taxonomy the inspector feeds).
|
||||
217
docs/modules/graph/contracts/graph.inspect.v1.schema.json
Normal file
217
docs/modules/graph/contracts/graph.inspect.v1.schema.json
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.org/schemas/graph.inspect.v1.schema.json",
|
||||
"title": "graph.inspect.v1",
|
||||
"description": "Inspector payload consumed by the Graph Indexer/API. Carries SBOM relationships plus advisory/VEX linkouts with deterministic ordering so downstream graph nodes/edges can be rebuilt or replayed offline.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"const": "graph.inspect.v1",
|
||||
"description": "Version marker; reject if not exactly graph.inspect.v1."
|
||||
},
|
||||
"tenant": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Tenant identifier applied to all nested records."
|
||||
},
|
||||
"artifactDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9_.:+-]{8,}$",
|
||||
"description": "Primary artifact digest (e.g., image digest) that the inspector payload refers to."
|
||||
},
|
||||
"sbomDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9_.:+-]{8,}$",
|
||||
"description": "SBOM digest or manifest hash that produced the relationships below."
|
||||
},
|
||||
"collectedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "UTC ISO-8601 timestamp when the inspector snapshot was produced."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "Component records kept in the same order as the upstream feed (stable ordering expected).",
|
||||
"items": { "$ref": "#/$defs/component" }
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Optional pointers for traceability (event ids, evidence URLs).",
|
||||
"properties": {
|
||||
"sbomObservationEventId": { "type": "string" },
|
||||
"linksetDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$",
|
||||
"description": "Optional Concelier linkset digest that seeded this payload."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"tenant",
|
||||
"artifactDigest",
|
||||
"sbomDigest",
|
||||
"collectedAt",
|
||||
"components"
|
||||
],
|
||||
"$defs": {
|
||||
"component": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["purl", "relationships", "provenance"],
|
||||
"properties": {
|
||||
"purl": {
|
||||
"type": "string",
|
||||
"description": "Normalized PURL for the component."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Optional explicit version if not derivable from the PURL."
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"description": "Sorted, unique scopes such as runtime/build/dev.",
|
||||
"items": { "type": "string" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"relationships": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "SBOM relationships that map to graph edges.",
|
||||
"items": { "$ref": "#/$defs/relationship" }
|
||||
},
|
||||
"advisories": {
|
||||
"type": "array",
|
||||
"description": "Advisory observations used to emit AFFECTED_BY edges.",
|
||||
"items": { "$ref": "#/$defs/advisoryObservation" }
|
||||
},
|
||||
"vexStatements": {
|
||||
"type": "array",
|
||||
"description": "VEX statements used to emit VEX_EXEMPTS edges.",
|
||||
"items": { "$ref": "#/$defs/vexStatement" }
|
||||
},
|
||||
"provenance": { "$ref": "#/$defs/provenance" }
|
||||
}
|
||||
},
|
||||
"relationship": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "targetPurl", "source", "evidenceHash"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["contains", "depends_on", "provides", "runtime_observed"],
|
||||
"description": "Relationship kind; aligns to graph edge taxonomy."
|
||||
},
|
||||
"targetPurl": {
|
||||
"type": "string",
|
||||
"description": "Normalized PURL of the target component."
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Scope label propagated to graph edges (e.g., runtime, build)."
|
||||
},
|
||||
"evidenceHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$",
|
||||
"description": "SHA-256 of the evidence payload referenced by this relationship."
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Provenance source for the relationship (e.g., concelier.linkset.v1, scanner.sbom.v1)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"advisoryObservation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["advisoryId", "source", "status", "modifiedAt", "evidenceHash"],
|
||||
"properties": {
|
||||
"advisoryId": { "type": "string" },
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Advisory source/provider (nvd, ghsa, distro id, etc.)."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["affected", "not_affected", "fixed", "under_investigation", "unknown"]
|
||||
},
|
||||
"severity": { "type": "string" },
|
||||
"cvss": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"vector": { "type": "string" },
|
||||
"score": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"justification": { "type": "string" },
|
||||
"justificationSummary": { "type": "string" },
|
||||
"linksetDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$"
|
||||
},
|
||||
"evidenceHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$"
|
||||
},
|
||||
"modifiedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"provenance": { "$ref": "#/$defs/provenance" }
|
||||
}
|
||||
},
|
||||
"vexStatement": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["statementId", "status", "justification", "issuedAt", "evidenceHash", "source"],
|
||||
"properties": {
|
||||
"statementId": { "type": "string" },
|
||||
"source": { "type": "string" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["not_affected", "affected", "under_investigation", "fixed"]
|
||||
},
|
||||
"justification": { "type": "string" },
|
||||
"impactStatement": { "type": "string" },
|
||||
"knownExploited": { "type": "boolean" },
|
||||
"issuedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"evidenceHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$"
|
||||
},
|
||||
"provenance": { "$ref": "#/$defs/provenance" }
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["source", "collectedAt"],
|
||||
"properties": {
|
||||
"source": { "type": "string" },
|
||||
"collectedAt": { "type": "string", "format": "date-time" },
|
||||
"eventOffset": { "type": "integer", "minimum": 0 },
|
||||
"linksetDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$"
|
||||
},
|
||||
"evidenceHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Fa-f0-9]{64}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user