feat: Initialize Zastava Webhook service with TLS and Authority authentication

- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint.
- Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately.
- Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly.
- Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
master
2025-10-19 18:36:22 +03:00
parent 2062da7a8b
commit d099a90f9b
966 changed files with 91038 additions and 1850 deletions

View File

@@ -3,12 +3,47 @@
Platform services publish strongly typed events; the JSON Schemas in this directory define those envelopes. File names follow `<event-name>@<version>.json` so producers and consumers can negotiate contracts explicitly.
## Catalog
- `scanner.report.ready@1.json` — emitted by Scanner.WebService once a signed report is persisted. Consumers: Notify, UI timeline.
- `scanner.report.ready@1.json` — emitted by Scanner.WebService once a signed report is persisted (payload embeds the canonical report plus DSSE envelope). Consumers: Notify, UI timeline.
- `scanner.scan.completed@1.json` — emitted alongside the signed report to capture scan outcomes/summary data for downstream automation. Consumers: Notify, Scheduler backfills, UI timelines.
- `scheduler.rescan.delta@1.json` — emitted by Scheduler when BOM-Index diffs require fresh scans. Consumers: Notify, Policy Engine.
- `attestor.logged@1.json` — emitted by Attestor after storing the Rekor inclusion proof. Consumers: UI attestation panel, Governance exports.
Additive payload changes (new optional fields) can stay within the same version. Any breaking change (removing a field, tightening validation, altering semantics) must increment the `@<version>` suffix and update downstream consumers.
## Envelope structure
All event envelopes share the same deterministic header. Use the following table as the quick reference when emitting or parsing events:
| Field | Type | Notes |
|-------|------|-------|
| `eventId` | `uuid` | Must be globally unique per occurrence; producers log duplicates as fatal. |
| `kind` | `string` | Fixed per schema (e.g., `scanner.report.ready`). Downstream services reject unknown kinds or versions. |
| `tenant` | `string` | Multitenant isolation key; mirror the value recorded in queue/Mongo metadata. |
| `ts` | `date-time` | RFC3339 UTC timestamp. Use monotonic clocks or atomic offsets so ordering survives retries. |
| `scope` | `object` | Optional block used when the event concerns a specific image or repository. See schema for required fields (e.g., `repo`, `digest`). |
| `payload` | `object` | Event-specific body. Schemas allow additional properties so producers can add optional hints (e.g., `reportId`, `quietedFindingCount`) without breaking consumers. For scanner events, payloads embed both the canonical report document and the DSSE envelope so consumers can reuse signatures without recomputing them. See `docs/runtime/SCANNER_RUNTIME_READINESS.md` for the runtime consumer checklist covering these hints. |
When adding new optional fields, document the behaviour in the schemas `description` block and update the consumer checklist in the next sprint sync.
## Canonical samples & validation
Reference payloads live under `docs/events/samples/`, mirroring the schema version (`<event-name>@<version>.sample.json`). They illustrate common field combinations, including the optional attributes that downstream teams rely on for UI affordances and audit trails. Scanner samples reuse the exact DSSE envelope checked into `samples/api/reports/report-sample.dsse.json`, and a unit test (`ReportSamplesTests`) guards that the payload/base64 remain canonical.
Run the following loop offline to validate both schemas and samples:
```bash
# Validate schemas (same check as CI)
for schema in docs/events/*.json; do
npx ajv compile -c ajv-formats -s "$schema"
done
# Validate canonical samples against their schemas
for sample in docs/events/samples/*.sample.json; do
schema="docs/events/$(basename "${sample%.sample.json}").json"
npx ajv validate -c ajv-formats -s "$schema" -d "$sample"
done
```
Consumers can copy the samples into integration tests to guarantee backwards compatibility. When emitting new event versions, include a matching sample and update this README so air-gapped operators stay in sync.
## CI validation
The Docs CI workflow (`.gitea/workflows/docs.yml`) installs `ajv-cli` and compiles every schema on pull requests. Run the same check locally before opening a PR:
@@ -25,6 +60,6 @@ If a schema references additional files, include `-r` flags so CI and local runs
## Working with schemas
- Producers should validate outbound payloads using the matching schema during unit tests.
- Consumers should pin to a specific version and log when encountering unknown versions to catch missing migrations early.
- Store real payload samples under `samples/events/` (mirrors the schema version) to aid contract testing.
- Store real payload samples under `docs/events/samples/` (mirrors the schema version) and mirror them into `samples/events/` when you need fixtures in integration repositories.
Contact the Platform Events group in Docs Guild if you need help shaping a new event or version strategy.

View File

@@ -0,0 +1,21 @@
{
"eventId": "1fdcaa1a-7a27-4154-8bac-cf813d8f4f6f",
"kind": "attestor.logged",
"tenant": "tenant-acme-solar",
"ts": "2025-10-18T15:45:27+00:00",
"payload": {
"artifactSha256": "sha256:8927d9151ad3f44e61a9c647511f9a31af2b4d245e7e031fe5cb4a0e8211c5d9",
"dsseEnvelopeDigest": "sha256:51c1dd189d5f16cfe87e82841d67b4fbc27d6fa9f5a09af0cd7e18945fb4c2a9",
"rekor": {
"index": 563421,
"url": "https://rekor.example/api/v1/log/entries/d6d0f897e7244edc9cb0bb2c68b05c96",
"uuid": "d6d0f897e7244edc9cb0bb2c68b05c96"
},
"signer": "cosign-stellaops",
"subject": {
"name": "scanner/report/sha256-0f0a8de5c1f93d6716b7249f6f4ea3a8",
"type": "report"
}
},
"attributes": {}
}

View File

@@ -0,0 +1,70 @@
{
"eventId": "6d2d1b77-f3c3-4f70-8a9d-6f2d0c8801ab",
"kind": "scanner.report.ready",
"tenant": "tenant-alpha",
"ts": "2025-10-19T12:34:56+00:00",
"scope": {
"namespace": "acme/edge",
"repo": "api",
"digest": "sha256:feedface",
"labels": {},
"attributes": {}
},
"payload": {
"delta": {
"kev": ["CVE-2024-9999"],
"newCritical": 1
},
"dsse": {
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
"payloadType": "application/vnd.stellaops.report\u002Bjson",
"signatures": [{
"algorithm": "hs256",
"keyId": "test-key",
"signature": "signature-value"
}]
},
"generatedAt": "2025-10-19T12:34:56+00:00",
"links": {
"ui": "https://scanner.example/ui/reports/report-abc"
},
"quietedFindingCount": 0,
"report": {
"generatedAt": "2025-10-19T12:34:56+00:00",
"imageDigest": "sha256:feedface",
"issues": [],
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
},
"reportId": "report-abc",
"summary": {
"blocked": 1,
"ignored": 0,
"quieted": 0,
"total": 1,
"warned": 0
},
"verdict": "blocked",
"verdicts": [
{
"findingId": "finding-1",
"status": "Blocked",
"score": 47.5,
"sourceTrust": "NVD",
"reachability": "runtime"
}
]
},
"reportId": "report-abc",
"summary": {
"blocked": 1,
"ignored": 0,
"quieted": 0,
"total": 1,
"warned": 0
},
"verdict": "fail"
},
"attributes": {}
}

View File

@@ -0,0 +1,78 @@
{
"eventId": "08a6de24-4a94-4d14-8432-9d14f36f6da3",
"kind": "scanner.scan.completed",
"tenant": "tenant-alpha",
"ts": "2025-10-19T12:34:56+00:00",
"scope": {
"namespace": "acme/edge",
"repo": "api",
"digest": "sha256:feedface",
"labels": {},
"attributes": {}
},
"payload": {
"delta": {
"kev": ["CVE-2024-9999"],
"newCritical": 1
},
"digest": "sha256:feedface",
"dsse": {
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
"payloadType": "application/vnd.stellaops.report\u002Bjson",
"signatures": [{
"algorithm": "hs256",
"keyId": "test-key",
"signature": "signature-value"
}]
},
"findings": [
{
"cve": "CVE-2024-9999",
"id": "finding-1",
"reachability": "runtime",
"severity": "Critical"
}
],
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
},
"report": {
"generatedAt": "2025-10-19T12:34:56+00:00",
"imageDigest": "sha256:feedface",
"issues": [],
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
},
"reportId": "report-abc",
"summary": {
"blocked": 1,
"ignored": 0,
"quieted": 0,
"total": 1,
"warned": 0
},
"verdict": "blocked",
"verdicts": [
{
"findingId": "finding-1",
"status": "Blocked",
"score": 47.5,
"sourceTrust": "NVD",
"reachability": "runtime"
}
]
},
"reportId": "report-abc",
"summary": {
"blocked": 1,
"ignored": 0,
"quieted": 0,
"total": 1,
"warned": 0
},
"verdict": "fail"
},
"attributes": {}
}

View File

@@ -0,0 +1,20 @@
{
"eventId": "51d0ef8d-3a17-4af3-b2d7-4ad3db3d9d2c",
"kind": "scheduler.rescan.delta",
"tenant": "tenant-acme-solar",
"ts": "2025-10-18T15:40:11+00:00",
"payload": {
"impactedDigests": [
"sha256:0f0a8de5c1f93d6716b7249f6f4ea3a8db451dc3f3c3ff823f53c9cbde5d5e8a",
"sha256:ab921f9679dd8d0832f3710a4df75dbadbd58c2d95f26a4d4efb2fa8c3d9b4ce"
],
"reason": "policy-change:scoring/v2",
"scheduleId": "rescan-weekly-critical",
"summary": {
"newCritical": 0,
"newHigh": 1,
"total": 4
}
},
"attributes": {}
}

View File

@@ -21,14 +21,28 @@
"type": "object",
"required": ["verdict", "delta", "links"],
"properties": {
"reportId": {"type": "string"},
"generatedAt": {"type": "string", "format": "date-time"},
"verdict": {"enum": ["pass", "warn", "fail"]},
"summary": {
"type": "object",
"properties": {
"total": {"type": "integer", "minimum": 0},
"blocked": {"type": "integer", "minimum": 0},
"warned": {"type": "integer", "minimum": 0},
"ignored": {"type": "integer", "minimum": 0},
"quieted": {"type": "integer", "minimum": 0}
},
"additionalProperties": false
},
"delta": {
"type": "object",
"properties": {
"newCritical": {"type": "integer", "minimum": 0},
"newHigh": {"type": "integer", "minimum": 0},
"kev": {"type": "array", "items": {"type": "string"}}
}
},
"additionalProperties": false
},
"links": {
"type": "object",
@@ -37,6 +51,30 @@
"rekor": {"type": "string", "format": "uri"}
},
"additionalProperties": false
},
"quietedFindingCount": {"type": "integer", "minimum": 0},
"report": {"type": "object"},
"dsse": {
"type": "object",
"required": ["payloadType", "payload", "signatures"],
"properties": {
"payloadType": {"type": "string"},
"payload": {"type": "string"},
"signatures": {
"type": "array",
"items": {
"type": "object",
"required": ["keyId", "algorithm", "signature"],
"properties": {
"keyId": {"type": "string"},
"algorithm": {"type": "string"},
"signature": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": true

View File

@@ -0,0 +1,97 @@
{
"$id": "https://stella-ops.org/schemas/events/scanner.scan.completed@1.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["eventId", "kind", "tenant", "ts", "scope", "payload"],
"properties": {
"eventId": {"type": "string", "format": "uuid"},
"kind": {"const": "scanner.scan.completed"},
"tenant": {"type": "string"},
"ts": {"type": "string", "format": "date-time"},
"scope": {
"type": "object",
"required": ["repo", "digest"],
"properties": {
"namespace": {"type": "string"},
"repo": {"type": "string"},
"digest": {"type": "string"}
}
},
"payload": {
"type": "object",
"required": ["reportId", "digest", "verdict", "summary"],
"properties": {
"reportId": {"type": "string"},
"digest": {"type": "string"},
"verdict": {"enum": ["pass", "warn", "fail"]},
"summary": {
"type": "object",
"properties": {
"total": {"type": "integer", "minimum": 0},
"blocked": {"type": "integer", "minimum": 0},
"warned": {"type": "integer", "minimum": 0},
"ignored": {"type": "integer", "minimum": 0},
"quieted": {"type": "integer", "minimum": 0}
},
"additionalProperties": false
},
"delta": {
"type": "object",
"properties": {
"newCritical": {"type": "integer", "minimum": 0},
"newHigh": {"type": "integer", "minimum": 0},
"kev": {"type": "array", "items": {"type": "string"}}
},
"additionalProperties": false
},
"policy": {
"type": "object",
"properties": {
"revisionId": {"type": "string"},
"digest": {"type": "string"}
},
"additionalProperties": false
},
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"severity": {"type": "string"},
"cve": {"type": "string"},
"purl": {"type": "string"},
"reachability": {"type": "string"}
},
"additionalProperties": true
}
},
"report": {"type": "object"},
"dsse": {
"type": "object",
"required": ["payloadType", "payload", "signatures"],
"properties": {
"payloadType": {"type": "string"},
"payload": {"type": "string"},
"signatures": {
"type": "array",
"items": {
"type": "object",
"required": ["keyId", "algorithm", "signature"],
"properties": {
"keyId": {"type": "string"},
"algorithm": {"type": "string"},
"signature": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}