work
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

This commit is contained in:
StellaOps Bot
2025-11-25 08:01:23 +02:00
parent d92973d6fd
commit 6bee1fdcf5
207 changed files with 12816 additions and 2295 deletions

View File

@@ -0,0 +1,44 @@
name: Concelier Attestation Tests
on:
push:
paths:
- 'src/Concelier/**'
- '.gitea/workflows/concelier-attestation-tests.yml'
pull_request:
paths:
- 'src/Concelier/**'
- '.gitea/workflows/concelier-attestation-tests.yml'
jobs:
attestation-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET 10 preview
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.100-rc.2.25502.107'
- name: Restore Concelier solution
run: dotnet restore src/Concelier/StellaOps.Concelier.sln
- name: Build WebService Tests (no analyzers)
run: dotnet build src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj -c Release -p:DisableAnalyzers=true
- name: Run WebService attestation test
run: dotnet test src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj -c Release --filter InternalAttestationVerify --no-build --logger trx --results-directory TestResults
- name: Build Core Tests (no analyzers)
run: dotnet build src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/StellaOps.Concelier.Core.Tests.csproj -c Release -p:DisableAnalyzers=true
- name: Run Core attestation builder tests
run: dotnet test src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/StellaOps.Concelier.Core.Tests.csproj -c Release --filter EvidenceBundleAttestationBuilderTests --no-build --logger trx --results-directory TestResults
- name: Upload TRX results
uses: actions/upload-artifact@v4
with:
name: concelier-attestation-tests-trx
path: '**/TestResults/*.trx'

View File

@@ -40,5 +40,6 @@ jobs:
out/mirror/thin/mirror-thin-v1.manifest.dsse.json out/mirror/thin/mirror-thin-v1.manifest.dsse.json
out/mirror/thin/tuf/ out/mirror/thin/tuf/
out/mirror/thin/oci/ out/mirror/thin/oci/
out/mirror/thin/milestone.json
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14

View File

@@ -52,6 +52,7 @@
- **Install & operations:** [Installation guide](21_INSTALL_GUIDE.md), [Offline Update Kit](24_OFFLINE_KIT.md), [Security hardening](17_SECURITY_HARDENING_GUIDE.md). - **Install & operations:** [Installation guide](21_INSTALL_GUIDE.md), [Offline Update Kit](24_OFFLINE_KIT.md), [Security hardening](17_SECURITY_HARDENING_GUIDE.md).
- **Binary prerequisites & offline layout:** [Binary prereqs](ops/binary-prereqs.md) covering curated NuGet feed, manifests, and CI guards. - **Binary prerequisites & offline layout:** [Binary prereqs](ops/binary-prereqs.md) covering curated NuGet feed, manifests, and CI guards.
- **Architecture & modules:** [High-level architecture](high-level-architecture.md), [Module dossiers](modules/platform/architecture-overview.md), [Strategic differentiators](moat.md). - **Architecture & modules:** [High-level architecture](high-level-architecture.md), [Module dossiers](modules/platform/architecture-overview.md), [Strategic differentiators](moat.md).
- **Advisory AI:** [Module dossier & deployment](modules/advisory-ai/README.md) covering RAG pipeline, guardrails, offline bundle outputs, and operations.
- **Policy & governance:** [Policy templates](60_POLICY_TEMPLATES.md), [Legal & quota FAQ](29_LEGAL_FAQ_QUOTA.md), [Governance charter](11_GOVERNANCE.md). - **Policy & governance:** [Policy templates](60_POLICY_TEMPLATES.md), [Legal & quota FAQ](29_LEGAL_FAQ_QUOTA.md), [Governance charter](11_GOVERNANCE.md).
- **UI & glossary:** [Console guide](15_UI_GUIDE.md), [Accessibility](accessibility.md), [Glossary](14_GLOSSARY_OF_TERMS.md). - **UI & glossary:** [Console guide](15_UI_GUIDE.md), [Accessibility](accessibility.md), [Glossary](14_GLOSSARY_OF_TERMS.md).
- **Technical documentation:** [Full technical index](technical/README.md) for architecture, APIs, module dossiers, and operations playbooks. - **Technical documentation:** [Full technical index](technical/README.md) for architecture, APIs, module dossiers, and operations playbooks.

70
docs/advisory-ai/cli.md Normal file
View File

@@ -0,0 +1,70 @@
# Advisory AI CLI Usage (DOCS-AIAI-31-005)
_Updated: 2025-11-24 · Owners: Docs Guild · DevEx/CLI Guild · Sprint 0111_
This guide shows how to drive Advisory AI from the StellaOps CLI using the `advise run` verb, with deterministic fixtures published on 2025-11-19 (`CLI-VULN-29-001`, `CLI-VEX-30-001`). It is designed for CI/offline use and mirrors the guardrail/policy contracts captured in `docs/advisory-ai/guardrails-and-evidence.md` and `docs/policy/assistant-parameters.md`.
## Prerequisites
- CLI binary from Sprint 205 (`stella`), logged in with scopes `advisory-ai:operate` + `aoc:verify`.
- Base URL pointed at Advisory AI gateway: `export STELLAOPS_ADVISORYAI_URL=https://advisory-ai.internal` (falls back to main backend base address when unset).
- Evidence fixtures available locally (offline friendly):
- `out/console/guardrails/cli-vuln-29-001/sample-vuln-output.ndjson` (SHA256 `e5aecfba5cee8d412408fb449f12fa4d5bf0a7cb7e5b316b99da3b9019897186`).
- `out/console/guardrails/cli-vuln-29-001/sample-sbom-context.json` (SHA256 `421af53f9eeba6903098d292fbd56f98be62ea6130b5161859889bf11d699d18`).
- `out/console/guardrails/cli-vex-30-001/sample-vex-output.ndjson` (SHA256 `2b11b1e2043c2ec1b0cb832c29577ad1c5cbc3fbd0b379b0ca0dee46c1bc32f6`).
- Policy hash pinned: set `ADVISORYAI__POLICYVERSION=2025.11.19` (or the bundle hash shipped in the Offline Kit).
## Quickstart
```bash
stella advise run summary \
--advisory-key csaf:redhat:RHSA-2025:1001 \
--artifact-id registry.stella-ops.internal/runtime/api \
--policy-version "$ADVISORYAI__POLICYVERSION" \
--profile fips-local \
--timeout 30 \
--json
```
- Use `--timeout 0` for cache-only probes in CI; add `--force-refresh` to bypass cache.
- `--profile cloud-openai` remains disabled unless tenant consent is recorded in Authority; guardrails reject with exit code 12 when disabled.
- Guardrail fixtures (`sample-vuln-output.ndjson`, `sample-vex-output.ndjson`, `sample-sbom-context.json`) live in Offline Kits and feed the backend self-tests; the CLI fetches evidence from backend services automatically.
## Exit codes
| Code | Meaning | Notes |
| --- | --- | --- |
| 0 | Success (hit or miss; output cached or freshly generated) | Includes `outputHash` and citations. |
| 2 | Validation error (missing advisory key, bad profile) | Mirrors HTTP 400.
| 3 | Context unavailable (SBOM/LNM/policy missing) | Mirrors HTTP 409 `advisory.contextUnavailable`.
| 4 | Guardrail block (PII, citation gap, prompt too large) | Mirrors HTTP 422 `advisory.guardrail.blocked`.
| 5 | Timeout waiting for output | Respect `--timeout` in seconds (0 = no wait). |
| 12 | Remote profile disabled | Returned when `cloud-openai` is selected without consent. |
| 7 | Transport/auth failure | Network/TLS/token issues. |
## Scripting patterns
- **Cache-only probes (CI smoke):** `stella advise run summary --advisory-key ... --timeout 0 --json > cache.json` (fails fast if evidence missing).
- **Batch mode:** pipe advisory keys: `cat advisories.txt | xargs -n1 -I{} stella advise run summary --advisory-key {} --timeout 0 --json`.
- **Profile gating:** set `--profile fips-local` for offline; use `--profile cloud-openai` only after Authority consent and when `ADVISORYAI__INFERENCE__MODE=Remote`.
- **Policy pinning:** always pass `--policy-version` (matches Offline Kit bundle hash); outputs include the policy hash in `context.planCacheKey`.
## Sample output (trimmed)
```json
{
"taskType": "Summary",
"profile": "fips-local",
"generatedAt": "2025-11-24T00:00:00Z",
"outputHash": "sha256:cafe...babe",
"citations": [{"index":1,"kind":"advisory","sourceId":"concelier:csaf:redhat:RHSA-2025:1001:paragraph:12"}],
"context": {
"planCacheKey": "adv-summary:csaf:redhat:RHSA-2025:1001:fips-local",
"sbom": {"artifactId":"registry.stella-ops.internal/runtime/api","versionTimeline":8,"dependencyPaths":5}
}
}
```
## Offline kit notes
- Copy the three CLI guardrail artefact bundles and their `hashes.sha256` files into `offline-kit/advisory-ai/fixtures/` and record them in `SHA256SUMS`.
- Set `ADVISORYAI__SBOM__BASEADDRESS` to the SBOM Service endpoint packaged in the kit; leave unset to fall back to `NullSbomContextClient` (Advisory AI will still respond deterministically with context counts set to 0).
- Keep `profiles.catalog.json` and `prompts.manifest` hashes aligned with the guardrail pack referenced in the Offline Kit manifest.
## Troubleshooting
- `contextUnavailable`: ensure SBOM service is reachable or provide `--sbom-context` fixture; verify LNM linkset IDs and hashes.
- `guardrail.blocked`: check blocked phrase list (`docs/policy/assistant-parameters.md`) and payload size; remove PII or reduce SBOM clamps.
- `timeout`: raise `--timeout` or run cache-only mode to avoid long waits in CI.

View File

@@ -1,8 +1,8 @@
# Advisory AI Evidence Payloads (LNM-Aligned) # Advisory AI Evidence Payloads (LNM-Aligned)
_Updated: 2025-11-18 · Owner: Advisory AI Docs Guild · Sprint: 0111 (AIAI-RAG-31-003)_ _Updated: 2025-11-24 · Owner: Advisory AI Docs Guild · Sprint: 0111 (AIAI-RAG-31-003)_
This document defines how Advisory AI consumes Link-Not-Merge (LNM) observations and linksets for Retrieval-Augmented Generation (RAG). It aligns payloads with the frozen LNM v1 schema (`docs/modules/concelier/link-not-merge-schema.md`, 2025-11-17) and replaces prior draft payloads. CLI/Policy artefacts (`CLI-VULN-29-001`, `CLI-VEX-30-001`, `policyVersion` digests) are referenced but optional at runtime; missing artefacts trigger deterministic `409 advisory.contextUnavailable` responses rather than fallback merging. This document defines how Advisory AI consumes Link-Not-Merge (LNM) observations and linksets for Retrieval-Augmented Generation (RAG). It aligns payloads with the frozen LNM v1 schema (`docs/modules/concelier/link-not-merge-schema.md`, 2025-11-17) and replaces prior draft payloads. CLI/Policy artefacts (`CLI-VULN-29-001`, `CLI-VEX-30-001`, `policyVersion` digests) are referenced but optional at runtime; missing artefacts trigger deterministic `409 advisory.contextUnavailable` responses rather than fallback merging. A deterministic SBOM context fixture lives at `out/console/guardrails/cli-vuln-29-001/sample-sbom-context.json` (SHA256 `421af53f9eeba6903098d292fbd56f98be62ea6130b5161859889bf11d699d18`) and is used in the examples below.
## 1) Input envelope (per task) ## 1) Input envelope (per task)

View File

@@ -1,15 +1,16 @@
# Advisory AI Guardrails & Evidence Intake # Advisory AI Guardrails & Evidence Intake
_Updated: 2025-11-22 · Owner: Advisory AI Docs Guild · Status: Draft (Sprint 0111)_ _Updated: 2025-11-24 · Owner: Advisory AI Docs Guild · Status: Published (Sprint 0111)_
This note captures the guardrail behaviors and evidence intake boundaries required by Sprint 0111 tasks (`AIAI-DOCS-31-001`, `AIAI-RAG-31-003`). It binds Advisory AI guardrails to upstream evidence sources and clarifies how Link-Not-Merge (LNM) documents flow into Retrieval-Augmented Generation (RAG) payloads. This note captures the guardrail behaviors and evidence intake boundaries required by Sprint 0111 tasks (`AIAI-DOCS-31-001`, `AIAI-RAG-31-003`). It binds Advisory AI guardrails to upstream evidence sources and clarifies how Link-Not-Merge (LNM) documents flow into Retrieval-Augmented Generation (RAG) payloads.
## 1) Evidence sources and contracts ## 1) Evidence sources and contracts
**Upstream readiness gates** **Upstream readiness gates (now satisfied)**
- CLI + Policy artefacts (`CLI-VULN-29-001`, `CLI-VEX-30-001`, `policyVersion` digests) must be present before enabling non-default profiles. Until then, Advisory AI accepts requests but responds with `409 advisory.contextUnavailable` when those references are missing. - CLI guardrail artefacts landed on 2025-11-19: `out/console/guardrails/cli-vuln-29-001/` (`sample-vuln-output.ndjson`, `sample-sbom-context.json`) and `out/console/guardrails/cli-vex-30-001/` (`sample-vex-output.ndjson`). Hashes are recorded in `docs/modules/cli/artefacts/guardrails-artefacts-2025-11-19.md` and must be copied into Offline Kits.
- LNM linksets stay the single source of truth; Advisory AI refuses ad-hoc advisory payloads even if CLI/Policy artefacts are delayed. - Policy hash must be pinned (`policyVersion`, see `docs/policy/assistant-parameters.md`) before enabling non-default profiles.
- LNM linksets stay the single source of truth; Advisory AI refuses ad-hoc advisory payloads even if upstream artefacts drift.
- **Advisory observations (LNM)** — Consume immutable `advisory_observations` and `advisory_linksets` produced per `docs/modules/concelier/link-not-merge-schema.md` (frozen v1, 2025-11-17). - **Advisory observations (LNM)** — Consume immutable `advisory_observations` and `advisory_linksets` produced per `docs/modules/concelier/link-not-merge-schema.md` (frozen v1, 2025-11-17).
- **VEX statements** — Excititor + VEX Lens linksets with trust weights; treated as structured chunks with `source_id` and `confidence`. - **VEX statements** — Excititor + VEX Lens linksets with trust weights; treated as structured chunks with `source_id` and `confidence`.
@@ -63,5 +64,6 @@ See `docs/advisory-ai/evidence-payloads.md` for full JSON examples and alignment
- [ ] LNM feed enabled and Concelier schemas at v1 (2025-11-17). - [ ] LNM feed enabled and Concelier schemas at v1 (2025-11-17).
- [ ] SBOM retriever configured or `NullSbomContextClient` left as safe default. - [ ] SBOM retriever configured or `NullSbomContextClient` left as safe default.
- [ ] Policy hash pinned via `policyVersion` when reproducibility is required. - [ ] Policy hash pinned via `policyVersion` when reproducibility is required.
- [ ] CLI guardrail artefact hashes verified against `docs/modules/cli/artefacts/guardrails-artefacts-2025-11-19.md` and mirrored into Offline Kits.
- [ ] Remote profiles only after Authority consent and profile allowlist are set. - [ ] Remote profiles only after Authority consent and profile allowlist are set.
- [ ] Cache directories shared between web + worker hosts for DSSE sealing. - [ ] Cache directories shared between web + worker hosts for DSSE sealing.

View File

@@ -0,0 +1,55 @@
# SBOM Context Hand-off for Advisory AI (SBOM-AIAI-31-003)
_Updated: 2025-11-24 · Owners: Advisory AI Guild · SBOM Service Guild · Sprint 0111_
Defines the contract and smoke test for passing SBOM context from SBOM Service to Advisory AI `/v1/sbom/context` consumers. Aligns with `SBOM-AIAI-31-001` (paths/timelines) and the CLI fixtures published on 2025-11-19.
## Contract
- **Endpoint** (SBOM Service): `/sbom/context`
- **Request** (minimal):
```json
{
"artifactId": "registry.stella-ops.internal/runtime/api",
"purl": "pkg:oci/runtime-api@sha256:d2c3...",
"timelineClamp": 500,
"dependencyPathClamp": 200
}
```
- **Response** (summarised):
```json
{
"schema": "stellaops.sbom.context/1.0",
"generated": "2025-11-19T00:00:00Z",
"packages": [
{"name":"openssl","version":"1.1.1w","purl":"pkg:deb/openssl@1.1.1w"},
{"name":"zlib","version":"1.2.11","purl":"pkg:deb/zlib@1.2.11"}
],
"timeline": 8,
"dependencyPaths": 5,
"hash": "sha256:421af53f9eeba6903098d292fbd56f98be62ea6130b5161859889bf11d699d18"
}
```
- **Determinism**: clamp values fixed unless overridden; `generated` timestamp frozen per fixture when offline.
- **Headers**: `X-StellaOps-Tenant` required; `X-StellaOps-ApiKey` optional for bootstrap.
## Smoke test (tenants/offline)
1. Start SBOM Service with fixture data loaded (or use `sample-sbom-context.json`).
2. Run: `curl -s -H "X-StellaOps-Tenant: demo" -H "Content-Type: application/json" \
-d @out/console/guardrails/cli-vuln-29-001/sample-sbom-context.json \
http://localhost:8080/sbom/context | jq .hash` (expect `sha256:421a...9d18`).
3. Configure Advisory AI:
- `AdvisoryAI:SBOM:BaseAddress=http://localhost:8080`
- `AdvisoryAI:SBOM:ApiKey=<key-if-required>`
4. Call Advisory AI cache-only: `stella advise run remediation --advisory-key csaf:redhat:RHSA-2025:1001 --artifact-id registry.stella-ops.internal/runtime/api --timeout 0 --json`.
- Expect exit 0 and `sbomSummary.dependencyPaths=5` in response.
5. Record the hash and endpoint in ops log; mirror fixture + hashes into Offline Kit under `offline-kit/advisory-ai/fixtures/sbom-context/`.
## Failure modes
- `409 advisory.contextHashMismatch` — occurs when the returned `hash` differs from the LNM linkset `provenanceHash`; refresh context or re-export.
- `403` — tenant/api key mismatch; check `X-StellaOps-Tenant` and API key.
- `429` — clamp exceeded; reduce `timelineClamp`/`dependencyPathClamp` or narrow `artifactId`.
## References
- `docs/sbom/remediation-heuristics.md` (blast-radius scoring).
- `docs/advisory-ai/guardrails-and-evidence.md` (evidence contract).
- `docs/modules/cli/artefacts/guardrails-artefacts-2025-11-19.md` (hashes for fixtures).

View File

@@ -0,0 +1,80 @@
# Developer Portal Publishing Guide
Last updated: 2025-11-25
## Goals
- Publish the StellaOps Developer Portal consistently across connected and air-gapped environments.
- Produce deterministic artefacts (checksums, manifests) so releases are auditable and reproducible.
- Keep docs, API specs, and examples in sync with the CI pipelines that build the portal.
## Prerequisites
- Node.js 20.x + pnpm 9.x
- Docker / Podman (for static-site container image)
- Spectral lint baseline from `src/Api/StellaOps.Api.OpenApi` (optional, to embed OAS links)
- Access to `local-nugets/` cache and offline asset bundle (see Offline section)
## Build & Test (connected)
```bash
pnpm install --frozen-lockfile
pnpm lint # markdownlint/prettier/eslint as configured
pnpm build # generates static site into dist/
pnpm test # component/unit tests if configured
```
- Determinism: ensure `pnpm-lock.yaml` is committed; no timestamps in emitted HTML (set `SOURCE_DATE_EPOCH` if needed).
## Publish (connected)
1. Build the static site: `pnpm build` (or reuse CI artifact).
2. Create artefact bundle:
```bash
tar -C dist -czf out/devportal/site.tar.gz .
sha256sum out/devportal/site.tar.gz > out/devportal/site.tar.gz.sha256
```
3. Container image (optional):
```bash
docker build -t registry.example.com/stella/devportal:${VERSION} -f ops/devportal/Dockerfile .
docker push registry.example.com/stella/devportal:${VERSION}
```
4. Record manifest `out/devportal/manifest.json`:
```json
{
"version": "${VERSION}",
"checksum": "$(cat out/devportal/site.tar.gz.sha256 | awk '{print $1}')",
"build": {
"node": "20.x",
"pnpm": "9.x"
},
"timestamp": "${UTC_ISO8601}",
"source_commit": "$(git rev-parse HEAD)"
}
```
## Offline / Air-gap
- Use pre-seeded bundle `offline/devportal/site.tar.gz` with accompanying `.sha256` and `manifest.json`.
- Verify before use:
```bash
sha256sum -c offline/devportal/site.tar.gz.sha256
```
- Serve locally:
```bash
mkdir -p /srv/devportal && tar -C /srv/devportal -xzf offline/devportal/site.tar.gz
# then point nginx/caddy to /srv/devportal
```
- No external CDN references allowed; ensure assets are bundled and CSP is self-contained.
## Deployment targets
- **Kubernetes**: use the static-site container image with a read-only root filesystem; expose via ingress with TLS; set `ETAG`/`Last-Modified` headers from manifest.
- **Docker Compose**: mount `site.tar.gz` into a lightweight nginx container; sample compose snippet lives in `ops/deployment/devportal/docker-compose.devportal.yml` (to be authored alongside this doc).
- **File share**: extract bundle onto shared storage for disconnected viewing; keep manifest + checksum adjacent.
## Checks & Observability
- Lint/OAS links: run `pnpm lint` and optional `pnpm api:check` (if wired) to ensure embedded API links resolve.
- Availability: configure basic `/healthz` (static 200) and enable access logging at the reverse proxy.
- Integrity: serve checksums/manifest from `/meta` path for auditors; include build `source_commit` and `timestamp`.
## Release checklist
- [ ] `pnpm build` succeeds reproducibly.
- [ ] `site.tar.gz` + `.sha256` generated and verified.
- [ ] `manifest.json` populated with version, checksum, UTC timestamp, commit SHA.
- [ ] Offline bundle placed in `offline/devportal/` with checksums.
- [ ] Image (if used) pushed to registry and noted in release notes.
- [ ] Deployment target (K8s/Compose/File share) instructions updated if changed.

View File

@@ -8,7 +8,7 @@ Summary: Ops & Offline focus on Ops Devops (phase IV).
Task ID | State | Task description | Owners (Source) Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
DEVOPS-OBS-55-001 | DONE (2025-11-25) | Implement incident mode automation: feature flag service, auto-activation via SLO burn-rate, retention override management, and post-incident reset job. Dependencies: DEVOPS-OBS-54-001. | DevOps Guild, Ops Guild (ops/devops) DEVOPS-OBS-55-001 | DONE (2025-11-25) | Implement incident mode automation: feature flag service, auto-activation via SLO burn-rate, retention override management, and post-incident reset job. Dependencies: DEVOPS-OBS-54-001. | DevOps Guild, Ops Guild (ops/devops)
DEVOPS-ORCH-32-001 | DOING (2025-11-25) | Provision orchestrator Postgres/message-bus infrastructure, add CI smoke deploy, seed Grafana dashboards (queue depth, inflight jobs), and document bootstrap. | DevOps Guild, Orchestrator Service Guild (ops/devops) DEVOPS-ORCH-32-001 | DONE (2025-11-25) | Provision orchestrator Postgres/message-bus infrastructure, add CI smoke deploy, seed Grafana dashboards (queue depth, inflight jobs), and document bootstrap. | DevOps Guild, Orchestrator Service Guild (ops/devops)
DEVOPS-ORCH-33-001 | TODO | Publish Grafana dashboards/alerts for rate limiter, backpressure, error clustering, and DLQ depth; integrate with on-call rotations. Dependencies: DEVOPS-ORCH-32-001. | DevOps Guild, Observability Guild (ops/devops) DEVOPS-ORCH-33-001 | TODO | Publish Grafana dashboards/alerts for rate limiter, backpressure, error clustering, and DLQ depth; integrate with on-call rotations. Dependencies: DEVOPS-ORCH-32-001. | DevOps Guild, Observability Guild (ops/devops)
DEVOPS-ORCH-34-001 | TODO | Harden production monitoring (synthetic probes, burn-rate alerts, replay smoke), document incident response, and prep GA readiness checklist. Dependencies: DEVOPS-ORCH-33-001. | DevOps Guild, Orchestrator Service Guild (ops/devops) DEVOPS-ORCH-34-001 | TODO | Harden production monitoring (synthetic probes, burn-rate alerts, replay smoke), document incident response, and prep GA readiness checklist. Dependencies: DEVOPS-ORCH-33-001. | DevOps Guild, Orchestrator Service Guild (ops/devops)
DEVOPS-POLICY-27-001 | TODO | Add CI pipeline stages to run `stella policy lint | DevOps Guild, DevEx/CLI Guild (ops/devops) DEVOPS-POLICY-27-001 | TODO | Add CI pipeline stages to run `stella policy lint | DevOps Guild, DevEx/CLI Guild (ops/devops)
@@ -37,3 +37,4 @@ Updates
- 2025-11-25 · DEVOPS-CI-110-001 runner published at `ops/devops/ci-110-runner/`; initial TRX slices stored under `ops/devops/artifacts/ci-110/20251125T030557Z/` (Concelier health, Excititor airgap import). - 2025-11-25 · DEVOPS-CI-110-001 runner published at `ops/devops/ci-110-runner/`; initial TRX slices stored under `ops/devops/artifacts/ci-110/20251125T030557Z/` (Concelier health, Excititor airgap import).
- 2025-11-25 · MIRROR-CRT-56-CI-001 completed: CI signing script now emits milestone hash summary, enforces DSSE/TUF/time-anchor steps, and uploads `milestone.json` via `mirror-sign.yml`. - 2025-11-25 · MIRROR-CRT-56-CI-001 completed: CI signing script now emits milestone hash summary, enforces DSSE/TUF/time-anchor steps, and uploads `milestone.json` via `mirror-sign.yml`.
- 2025-11-25 · DEVOPS-OBS-55-001 completed: added offline incident-mode automation script (`scripts/observability/incident-mode.sh`) and runbook (`ops/devops/observability/incident-mode.md`) to auto-toggle incident flag, retention overrides, and cooldown reset based on burn rate inputs. - 2025-11-25 · DEVOPS-OBS-55-001 completed: added offline incident-mode automation script (`scripts/observability/incident-mode.sh`) and runbook (`ops/devops/observability/incident-mode.md`) to auto-toggle incident flag, retention overrides, and cooldown reset based on burn rate inputs.
- 2025-11-25 · DEVOPS-ORCH-32-001 completed: added orchestrator infra compose stack (Postgres+Mongo+NATS), smoke script (`scripts/orchestrator/smoke.sh`), alerts, Grafana dashboard, and bootstrap README under `ops/devops/orchestrator/`.

View File

@@ -8,6 +8,11 @@ Advisory AI is the retrieval-augmented assistant that synthesizes advisory and V
- Propose remediation hints aligned with Offline Kit staging and export bundles. - Propose remediation hints aligned with Offline Kit staging and export bundles.
- Expose API/UI surfaces with guardrails on model prompts, outputs, and retention. - Expose API/UI surfaces with guardrails on model prompts, outputs, and retention.
## Contributor quickstart
- Read `docs/modules/advisory-ai/AGENTS.md` before making changes; it lists required docs, determinism/offline rules, and working directory scope.
- Keep outputs aggregation-only with stable ordering and UTC timestamps; tests must cover guardrails, tenant safety, and provenance.
- When updating contracts/telemetry, sync the relevant docs here and cross-link from sprint Decisions & Risks.
## Key components ## Key components
- RAG pipeline drawing from Conseiller, Excititor, VEX Lens, Policy Engine, and SBOM Service data. - RAG pipeline drawing from Conseiller, Excititor, VEX Lens, Policy Engine, and SBOM Service data.
- Prompt templates and guard models enforcing provenance and redaction policies. - Prompt templates and guard models enforcing provenance and redaction policies.
@@ -26,6 +31,13 @@ Advisory AI is the retrieval-augmented assistant that synthesizes advisory and V
- Redaction policies validated against security/LLM guardrail tests. - Redaction policies validated against security/LLM guardrail tests.
- Guardrail behaviour, blocked phrases, and operational alerts are detailed in `/docs/security/assistant-guardrails.md`. - Guardrail behaviour, blocked phrases, and operational alerts are detailed in `/docs/security/assistant-guardrails.md`.
## Outputs & artefacts
- **Run/plan records (deterministic):** persisted under `/app/data/{queue,plans,outputs}` (or `ADVISORYAI__STORAGE__*` overrides) with ISO timestamps, provenance hashes, and stable ordering for replay.
- **Service surfaces (airgap friendly):** `/ops/advisory-ai/runs` streams NDJSON status; `/ops/advisory-ai/runs/{id}` returns the immutable run/plan bundle with guardrail decisions.
- **Events:** worker emits `advisory_ai_run_completed` with digests (plan, output, guardrail) for downstream consumers; feature-flagged to keep offline deployments silent.
- **Offline bundle:** `advisory-ai-bundle.tgz` packages prompts, sanitized inputs, outputs, guardrail audit trail, and signatures; build via `docs/modules/advisory-ai/deployment.md` recipes to keep artefacts deterministic across air-gapped imports.
- **Observability:** metrics/logs share the `advisory_ai` meter/logger namespace (latency, guardrail blocks/validations, citation coverage). Dashboards and alerts must reference these canonical names to avoid drift.
## Deployment & configuration ## Deployment & configuration
- **Containers:** `advisory-ai-web` fronts the API/cache while `advisory-ai-worker` drains the queue and executes prompts. Both containers mount a shared RWX volume providing `/app/data/{queue,plans,outputs}` (defaults; configurable via `ADVISORYAI__STORAGE__*`). - **Containers:** `advisory-ai-web` fronts the API/cache while `advisory-ai-worker` drains the queue and executes prompts. Both containers mount a shared RWX volume providing `/app/data/{queue,plans,outputs}` (defaults; configurable via `ADVISORYAI__STORAGE__*`).
- **Remote inference toggle:** Set `ADVISORYAI__INFERENCE__MODE=Remote` to send sanitized prompts to an external inference tier. Provide `ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS` (and optional `...__APIKEY`, `...__TIMEOUT`) to complete the circuit; failures fall back to the sanitized prompt and surface `inference.fallback_*` metadata. - **Remote inference toggle:** Set `ADVISORYAI__INFERENCE__MODE=Remote` to send sanitized prompts to an external inference tier. Provide `ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS` (and optional `...__APIKEY`, `...__TIMEOUT`) to complete the circuit; failures fall back to the sanitized prompt and surface `inference.fallback_*` metadata.

View File

@@ -0,0 +1,8 @@
# Advisory AI · Tasks
| Task ID | Description | Owner(s) | Sprint | Status | Notes |
| --- | --- | --- | --- | --- | --- |
| ADVISORY-AI-DOCS-0001 | Align module docs with `AGENTS.md` guardrails and required reading. | Docs Guild | SPRINT_312_docs_modules_advisory_ai | DONE (2025-11-24) | AGENTS/README now call out offline/determinism guardrails and required docs. |
| ADVISORY-AI-ENG-0001 | Sync module doc pointers into parent docs tree. | Module Team | SPRINT_312_docs_modules_advisory_ai | DONE (2025-11-24) | Root docs/README now links to Advisory AI dossier. |
| ADVISORY-AI-OPS-0001 | Document Advisory AI outputs/artefacts in module README. | Ops Guild | SPRINT_312_docs_modules_advisory_ai | DONE (2025-11-24) | README section expanded with concrete outputs/endpoints/bundles/events. |

View File

@@ -0,0 +1,19 @@
# Attestation Plan 2001 · Evidence Locker contract handoff (2025-11-24)
Owners: Evidence Locker Guild · Excititor Guild
Status: Published (unblocks ATTEST-PLAN-2001)
## Inputs
- Sealed bundle contract: `docs/modules/evidence-locker/prep/2025-11-24-evidence-locker-contract.md`
- Bundle schema: `docs/modules/evidence-locker/schemas/bundle.schema.json`
- Sample bundle + hash: `docs/modules/evidence-locker/samples/evidence-bundle-sample.tgz` (+ `.sha256`)
## Plan
1) Align attestation payloads with sealed bundle contract (subjects, DSSE layout, manifest fields).
2) Produce CLI/Export Center consumer notes: expected file layout, required hashes, validation steps.
3) Add verification harness reference for Excititor/Attestor (reuse sample bundle + DSSE public key from contract note).
4) Update downstream sprints (Excititor airgap/export, Export Center) with contract link and hash.
## Next actions
- Evidence Locker Guild: confirm final schema hash matches sample bundle (track in contract note).
- Excititor Guild: wire contract path into airgap/attestation tests; report readiness in respective sprints.

View File

@@ -0,0 +1,25 @@
# stella advisory — Command Guide
## Commands
- `stella advisory list --source <provider> [--status <status>] [--output json|ndjson|table] [--offline]`
- `stella advisory get --id <advisoryId> [--output json|table] [--offline]`
- `stella advisory export --bundle <path> [--offline]`
## Flags (common)
- `--offline`: pull from cached advisory snapshots/mirror bundles only; exit code 5 if remote needed.
- `--source`: provider filter (msrc, nvd, osv, csaf, etc.).
- `--status`: affected, fixed, not_affected, withdrawn, disputed.
- `--output`: json (default), ndjson, table.
## Inputs/outputs
- Inputs: Concelier/Excititor advisory projections; cached mirror bundles when offline.
- Outputs: raw evidence with provenance (`observationId`, `linksetId`, signatures); no merging/inference.
- Exit codes per `output-and-exit-codes.md`; not found → 4, offline violation → 5.
## Determinism rules
- Sorted by advisory key; withdrawn/duplicate handling matches upstream evidence; no severity inference.
- Timestamps UTC; hashes lowercase hex.
## Offline/air-gap notes
- Mirror bundles must be preloaded for offline use; CLI verifies signatures against trust roots.
- Export uses local evidence only; produces deterministic bundle with manifest + checksums.

View File

@@ -0,0 +1,21 @@
# stella aoc — Command Guide
## Commands
- `stella aoc verify --input <evidence> [--policy <path>] [--offline]`
- `stella aoc explain --input <evidence> [--output json|table]`
## Flags (common)
- `--offline`: verify evidence without remote calls; exit code 5 if network would be required.
- `--policy`: optional AOC policy file; defaults to platform policy.
- `--output`: json (default), table.
## Inputs/outputs
- Inputs: AOC evidence bundle; optional policy file.
- Outputs: verification results with rationale; aggregation-only.
- Exit codes per `output-and-exit-codes.md`; 3 for auth failures, 4 for missing evidence, 5 for offline violation.
## Determinism rules
- Stable ordering of findings; timestamps UTC; hashes lowercase hex.
## Offline/air-gap notes
- Trust roots loaded locally; no remote downloads allowed in offline mode.

View File

@@ -0,0 +1,19 @@
# stella auth — Command Guide
## Commands
- `stella auth login --token <token> [--url <baseUrl>]`
- `stella auth status`
- `stella auth logout`
## Flags
- `--url`: API base URL; defaults to config/env.
- `--token`: bearer token or OIDC device code (future); stored in config if allowed.
## Behaviour
- Login writes token to config file or keyring (where supported) with deterministic permissions; never echoes secrets.
- Status prints current user/tenant scopes if available; uses exit code 3 when unauthenticated.
- Logout removes stored token and cached session data.
## Offline/air-gap notes
- Login requires network; if `--offline` is set, command must fail with exit code 5.
- Status/logout work offline using cached credentials only.

View File

@@ -0,0 +1,25 @@
# stella export — Command Guide
## Commands
- `stella export mirror --bundle <path> --profile <name> [--offline]`
- `stella export verify --bundle <path> --trust-roots <file>`
- `stella export plan --output json` (preview bundle contents)
## Flags (common)
- `--offline`: enforce no network; fail with exit code 5 if registry/object-store calls would occur.
- `--profile`: named export profile (schema/manifest version); defaults to latest supported.
- `--trust-roots`: PEM/TUF/DSSE trust roots for verification.
- `--output`: json (default) or table for plan outputs.
## Inputs/outputs
- Inputs: export profiles, mirror configuration, optional cached artefacts.
- Outputs: deterministic bundle tarball + manifest (checksums, signatures, metadata); verify emits status + detailed reasons.
- Exit codes follow `output-and-exit-codes.md`; verification failure uses exit code 3.
## Determinism rules
- Manifest ordering is stable; checksums hex-lowercase; timestamps UTC.
- No network-dependent mutation; offline bundles must be reproducible.
## Offline/air-gap notes
- `--offline` must be honored; registry pulls are forbidden unless cached in profile path.
- Verification uses only local trust roots; no remote key fetch.

View File

@@ -0,0 +1,24 @@
# stella notify — Command Guide
## Commands
- `stella notify send --channel <email|chat|webhook> --template <id> --data <file>`
- `stella notify list --status <pending|sent|failed> [--output json|table] [--offline]`
- `stella notify get --id <messageId> [--offline]`
## Flags (common)
- `--offline`: only allowed when notification queue snapshots are cached; otherwise exit code 5.
- `--tenant`: scope to tenant; enforced by server RLS.
- `--output`: json/ndjson/table.
## Inputs/outputs
- Inputs: Notify API; optional cached queue snapshots when offline.
- Outputs: message metadata, status, delivery results; no template content leaks.
- Exit codes follow `output-and-exit-codes.md`; 4 for not found, 5 for offline violation.
## Determinism rules
- Listings sorted by created time then id; timestamps UTC.
- No retries triggered by the CLI; it only submits/reads.
## Offline/air-gap notes
- Sending in offline mode is disallowed (exit code 5); only listing cached snapshots is permitted.
- Templates must be preloaded; no remote fetches when `--offline`.

View File

@@ -0,0 +1,23 @@
# stella orchestrator — Command Guide
## Commands
- `stella orchestrator jobs list --output json|table [--offline]`
- `stella orchestrator jobs get --id <jobId> [--offline]`
- `stella orchestrator runs get --id <runId> [--offline]`
## Flags (common)
- `--offline`: only allowed when cached ledger snapshots are available; otherwise exit code 5.
- `--status`, `--type`: filters for job listings; deterministic sort by created time then id.
- `--output`: json/ndjson/table.
## Inputs/outputs
- Inputs: Orchestrator API or cached run ledger snapshots.
- Outputs: job/run metadata with provenance hashes and DSSE/attestation pointers when available.
- Exit codes per `output-and-exit-codes.md`; 4 for not found, 5 for offline violation.
## Determinism rules
- Sorted outputs; timestamps UTC; hashes hex lowercase.
- No inferred state beyond orchestrator responses.
## Offline/air-gap notes
- Ledger snapshots must be preloaded; no live scheduler calls when `--offline`.

View File

@@ -0,0 +1,25 @@
# stella policy — Command Guide
## Commands
- `stella policy eval --input <bundle> --subject <sbom|vex|vuln> [--offline] [--output json|ndjson|table]`
- `stella policy simulate --from <bundleA> --to <bundleB> [--budget <ms>] [--offline]`
- `stella policy publish --input <bundle> --sign --attest`
## Flags (common)
- `--offline` / `STELLA_OFFLINE=1`: forbid network calls; use cached bundles only.
- `--tenant <id>`: scope evaluation to tenant; RLS enforcement required on the server.
- `--rationale`: include rationale IDs in responses.
- `--output`: `json` (default), `ndjson`, or `table`.
## Inputs/outputs
- Inputs: policy bundles (signed), subject artifacts (SBOM/VEX/Vuln snapshots).
- Outputs: deterministic JSON/NDJSON or tables; includes `correlationId`, `policyVersion`, `rationaleIds` when requested.
- Exit codes follow `output-and-exit-codes.md`.
## Determinism rules
- Sort evaluation results by subject key; timestamps UTC ISO-8601.
- No inferred verdicts beyond Policy Engine response.
## Offline/air-gap notes
- When `--offline`, evaluation must use locally cached bundles and subject artifacts; fail with exit code 5 if network would be needed.
- Trust roots loaded from `STELLA_TRUST_ROOTS` when verifying signed bundles.

View File

@@ -0,0 +1,25 @@
# stella sbom — Command Guide
## Commands
- `stella sbom generate --image <ref> [--output sbom.spdx.json] [--offline]`
- `stella sbom compose --fragment <path> --output composition.json --offline`
- `stella sbom verify --file <sbom> --signature <sig> --key <keyfile>`
## Flags (common)
- `--offline`: no network pulls; use local cache/OCI archive.
- `--format`: `spdx-json` (default) or `cyclonedx-json`.
- `--attest`: emit DSSE attestation alongside SBOM.
- `--hash`: include layer/file hashes (deterministic ordering).
## Inputs/outputs
- Inputs: container image, directory, or fragments.
- Outputs: deterministic SPDX/CycloneDX JSON, optional DSSE + checksums.
- Exit codes per `output-and-exit-codes.md`; verification failure uses exit code 3 or 4 depending on cause.
## Determinism rules
- Stable ordering of packages/files; timestamps UTC.
- Hashes hex-lowercase; no host-specific paths.
## Offline/air-gap notes
- With `--offline`, image sources must already be cached (tar/OCI archive); command fails with exit code 5 if it would fetch remotely.
- Verification uses local trust roots; no remote key fetch.

View File

@@ -0,0 +1,23 @@
# stella vex — Command Guide
## Commands
- `stella vex consensus --query <filter> [--output json|ndjson|table] [--offline]`
- `stella vex get --id <consensusId> [--offline]`
- `stella vex simulate --input <vexDocs> --policy <policyConfig> [--offline]`
## Flags (common)
- `--offline`: use cached consensus snapshots; fail with exit code 5 if remote would be hit.
- `--policy <path>`: apply trust/weighting config; aggregation-only outputs.
- `--page-size`, `--page-token`: deterministic pagination.
## Inputs/outputs
- Inputs: VEX consensus projection (VexLens); optional cached snapshots when offline.
- Outputs: consensus states with `consensus_state`, `confidence`, `weights`, `issuers`, `rationale`; stable ordering.
## Determinism rules
- Sort by `consensusId`; pagination tokens deterministic.
- No verdict inference beyond upstream consensus projection; CLI stays aggregation-only.
## Offline/air-gap notes
- Cached snapshots are required when `--offline`; otherwise exit code 5 with remediation message.
- Trust roots for signature verification are loaded from `STELLA_TRUST_ROOTS` when verifying cached snapshots.

View File

@@ -0,0 +1,25 @@
# stella vuln — Command Guide
## Commands
- `stella vuln list --query <filter> [--group-by <field>] [--output json|ndjson|table] [--offline]`
- `stella vuln get --id <vulnId> [--output json|table] [--offline]`
- `stella vuln simulate --from <policyA> --to <policyB> --subjects <path> [--offline]`
## Flags (common)
- `--offline`: read from cached snapshots; fail with exit code 5 if network would be used.
- `--policy <id>`: scope queries to a policy projection.
- `--page-size`, `--page-token`: deterministic pagination.
- `--group-by`: `cve`, `package`, `status`, `advisory` (results stay stably ordered within groups).
## Inputs/outputs
- Inputs: Vuln Explorer API; optional cached snapshots when offline.
- Outputs: sorted lists or detail documents with provenance pointers (`advisoryId`, `evidenceIds`, `consensusId`).
- Exit codes follow `output-and-exit-codes.md`; 4 for not found, 5 for offline violation.
## Determinism rules
- Lists sorted by primary key then timestamp; group-by keeps stable ordering inside each bucket.
- Timestamps UTC ISO-8601; hashes lower-case hex.
## Offline/air-gap notes
- Use cached snapshots (`--offline`) when remote Explorer is unavailable; commands must not attempt network calls in this mode.
- Simulation must read local policy snapshots and subjects when offline.

View File

@@ -0,0 +1,40 @@
# stella CLI — Configuration
## Precedence (highest → lowest)
1. Command-line flags (e.g., `--output json`, `--offline`)
2. Environment variables
3. Config file (`config.yaml`/`config.json`) loaded from the first existing path:
- `$STELLA_CONFIG` (explicit override)
- `$XDG_CONFIG_HOME/stella/config.yaml` (or `%APPDATA%\\Stella\\config.yaml` on Windows)
- `$HOME/.config/stella/config.yaml`
Tip: keep secrets in env vars, not in the config file; tokens are read from `STELLA_TOKEN`, registry creds from `STELLA_REGISTRY_AUTH`, etc.
## Common settings (YAML example)
```yaml
output: json # json|ndjson|table
offline: true # force no-network mode
api:
baseUrl: https://console.stella.local
token: ${STELLA_TOKEN} # prefer env substitution
policy:
tenant: demo-tenant
rationale: true
airgap:
bundlesPath: /var/stella/bundles
trustRoots: /var/stella/trust/roots.pem
observability:
traceparent: auto # always inject trace headers when available
```
## Air-gap/offline knobs
- `--offline` or `STELLA_OFFLINE=1` forbids network calls; commands must rely on local bundles/caches.
- `airgap.bundlesPath` controls where imports/exports read/write sealed bundles.
- Mirror/import/export commands respect `STELLA_TRUST_ROOTS` for DSSE/TUF verification.
## Logging & telemetry
- `STELLA_LOG_LEVEL=debug` for verbose logs; `trace` adds wire dumps (still deterministic).
- Tracing headers: CLI injects `traceparent` when provided by the environment (CI runners, gateways); never emits PII.
## Profiles (planned)
- Profiles will live under `profiles/<name>.yaml` and can be selected with `--profile <name>`; until shipped, stick to the single default config file.

View File

@@ -0,0 +1,32 @@
# stella CLI — Forensics Guide
## Commands
- `stella forensic snapshot create --case <id> --output <path>`: capture current evidence snapshot; emits manifest + checksums.
- `stella forensic verify --bundle <path>`: validate checksums, DSSE signatures, and timeline chain-of-custody.
- `stella attest verify --file <attestation>`: reuse attestor flows for envelope verification (see `guides/commands/attest.md`).
## Flags
- `--offline`: prohibit network access; use local bundles only (exit code 5 if remote call would occur).
- `--output json|table` (default json) for verification results.
- `--trust-roots <file>`: PEM/TUF/DSSE trust roots for verification.
## Outputs & exit codes
- Success → 0; verification failure → 3; missing bundle → 4; offline violation → 5.
- Verification output includes `status`, `checksum`, `signature`, `subject`, `rationale` fields; ordering is deterministic.
## Determinism rules
- Snapshots record UTC timestamps and stable file ordering; hashes are lowercase hex.
- CLI never mutates evidence; it only validates and reports.
## Offline/air-gap notes
- Always supply trust roots from sealed media when in air-gap mode; no remote key fetch is allowed.
- Store snapshots under a deterministic path (`case-id/date/`) to simplify audits.
## Examples
```bash
# Create a snapshot for case ACME-123
stella forensic snapshot create --case ACME-123 --output out/forensics/acme-123.tgz
# Verify a snapshot with pinned trust roots
stella forensic verify --bundle out/forensics/acme-123.tgz --trust-roots trust/roots.pem --output table
```

View File

@@ -0,0 +1,32 @@
# stella CLI — Observability Guide
## Commands
- `stella obs top` (planned): stream service health (SLO/burn-rate, queue depth, error rates) with table/JSON output.
- `stella obs trace <trace_id>`: fetch correlated trace if server supports it; prints correlation/trace IDs.
- `stella obs logs --from <ts> --to <ts> [--service <name>]`: pull logs for a window with pagination tokens.
## Flags
- `--output json|ndjson|table` (default: json).
- `--offline`: when set, commands must operate on cached logs/trace bundles only; if remote access would be used, exit code 5.
- `--page-size`, `--page-token`: deterministic pagination.
## Output & exit codes
- Exit codes follow `guides/output-and-exit-codes.md` (not found → 4; offline violation → 5).
- Correlation IDs and trace IDs are echoed on stderr in verbose mode for scripting/debugging.
## Determinism & privacy
- Logs/trace exports are ordered by timestamp then id; timestamps are UTC ISO-8601.
- CLI never redacts server-side; it only forwards what the API returns. Avoid printing secrets—use `--output json` with `jq` to filter locally.
## Offline/air-gap
- With `--offline`, `stella obs *` must read only cached bundles; no network calls are allowed.
- For sealed environments, pass `--trust-roots <pem>` when verifying cached trace/log bundles.
## Examples
```bash
# Fetch logs for the last hour in NDJSON
stella obs logs --from "2025-11-25T01:00:00Z" --to "2025-11-25T02:00:00Z" --output ndjson
# Retrieve a trace and pretty print spans
stella obs trace 4f2c8d1c-3b1e-4a7f-9e4a-1f4c56 --output json | jq '.spans[0]'
```

View File

@@ -0,0 +1,34 @@
# stella CLI — Output & Exit Codes
## Output formats
- `--output json` (default): deterministic JSON objects per record.
- `--output ndjson`: one JSON object per line for streaming/large results.
- `--output table`: aligned columns for humans; preserves stable column order.
- Use `--quiet` to suppress informational logs; errors still print to stderr.
## Exit codes (contract)
- `0` — Success.
- `1` — Generic error (unexpected exception).
- `2` — Validation or user input error.
- `3` — AuthN/AuthZ failure (expired token, missing scope).
- `4` — Not found / resource missing.
- `5` — Network disabled/offline violation when a command requires connectivity.
- `10` — Retryable/transient error (service unavailable, backoff suggested).
Clients and scripts should treat `25` as non-retryable unless input changes; only `10` should trigger automated retry with backoff.
## Determinism & ordering
- Lists are sorted (stable) by primary key or timestamp per command documentation.
- Timestamps are UTC ISO-8601; hashes use hex lowercase.
- Randomness is seeded; avoid machine-specific paths in emitted artefacts.
## Examples
```bash
stella vuln list --output json | jq '.items[0]'
stella export mirror --offline --output ndjson > mirror.ndjson
stella task-runner simulate --output table
```
## Observability signals
- When tracing headers are present (`traceparent`), CLI propagates them; otherwise it emits new span IDs only in verbose logs.
- Metrics are not emitted by the CLI itself; servers capture request telemetry and can be correlated via the returned correlation/trace IDs printed on errors in verbose mode.

View File

@@ -0,0 +1,32 @@
# stella CLI — Overview
## What it does
- Single entrypoint for scans, exports, policy management, VEX/Vuln queries, air-gapped kit operations, and task-runner interactions.
- Evidence-preserving: the CLI never mutates upstream evidence; it emits signed manifests and deterministic JSON/NDJSON where possible.
- Offline-ready: every command must run with cached feeds/bundles when `STELLA_OFFLINE=1` or `--offline` is set.
## Core verbs (at a glance)
- `stella scan ...` — container/dir scans; emits SBOM + findings bundles.
- `stella policy ...` — push/eval/simulate policy bundles; attach evidence; request rationale.
- `stella vex ...` / `stella vuln ...` — query VEX consensus and vulnerability projections with pagination/budgets.
- `stella export ...` — mirror/export bundles; verify signatures; produce checksums/attestations.
- `stella airgap ...` — import/export sealed bundles; validate trust roots; run without network.
- `stella task-runner ...` — submit/inspect pack runs; stream logs; collect artefacts.
## Imposed rules (apply to every command)
- Determinism first: stable ordering, UTC ISO-8601 timestamps, no host-specific paths in outputs.
- Aggregation-only: if a command shows advisory/VEX data, it must not infer verdicts beyond published evidence.
- Offline/air-gap parity: every feature documents its offline flag(s) and expected cache locations.
## Quick start
```bash
stella --help # top-level verbs
stella scan image ghcr.io/acme/app:1.2.3 --output json --offline
stella policy eval --input policy.bundle.json --subject sbom.spdx.jsonl --explain
stella export mirror --bundle out/mirror.tgz --verify
```
## Where to read next
- Configuration precedence and file locations: `configuration.md`
- Output formats and exit codes: `output-and-exit-codes.md`
- Command-specific guides: see `cli-reference.md` and verb-specific guides under `guides/`.

View File

@@ -0,0 +1,19 @@
# stella CLI — Parity Matrix
Use this matrix to verify that CLI surfaces match the corresponding service APIs, schemas, and offline behaviours. Every row must stay deterministic and aggregation-only.
| Area | Server/API | CLI command(s) | Output contract | Offline support | Notes |
| --- | --- | --- | --- | --- | --- |
| Policy eval/simulate | Policy Engine `/policy/eval` `/policy/simulate` | `stella policy eval`, `stella policy simulate` | Stable JSON/NDJSON; includes `correlationId`, `policyVersion`, `rationaleIds` | Must run with cached bundles when `--offline` | No verdict inference beyond engine response. |
| VEX consensus | VexLens `/vex/consensus` | `stella vex consensus` | Deterministic pagination; weights/issuers/rationale echoed | Cached consensus snapshots permitted | Uses aggregation-only contract. |
| Vulnerability list/detail | Vuln Explorer `/vuln` | `stella vuln list`, `stella vuln get` | Sorted by `vulnId`; includes provenance pointers; no missing fields inferred | Must respect `--offline` using cached snapshots | |
| Export/mirror bundles | Export Service `/export/*` | `stella export mirror`, `stella export verify` | Emits manifest + checksums; verification errors are deterministic | Yes (air-gap bundles) | All paths must be relative and normalized. |
| Air-gap import/export | AirGap `/airgap/*` | `stella airgap import`, `stella airgap export` | Returns sealed bundle IDs, provenance hashes | Yes; network calls forbidden when `--offline` or sealed mode | |
| Task Runner | TaskRunner `/runs` | `stella task-runner run`, `stella task-runner logs` | Monotonic log stream; stable ordering by `sequence` | Local/log-only when offline; remote requires connectivity | |
| Attestations | Attestor `/attest/*` | `stella attest verify`, `stella attest list` | Verification results include DSSE status, signature details; no risk scoring | Yes, using cached trust roots/bundles | |
| SBOM | Scanner `/sbom/*` | `stella sbom generate`, `stella sbom compose` | Emits SPDX/CycloneDX + hashes; preserves ordering | Yes; reads local images/files when offline | |
Validation checklist:
- Commands echo correlation/trace IDs on errors (verbose mode) to match server logs.
- Exit codes follow the contract in `output-and-exit-codes.md`.
- When a server feature is unavailable offline, the CLI must fail with exit code 5 and an actionable message.

View File

@@ -0,0 +1,73 @@
# Concelier Evidence Batch API (draft v1)
Path: `POST /v1/evidence/batch`
Auth: same as other advisory read endpoints; requires tenant header `X-Stella-Tenant`.
Purpose: allow graph/UI/export clients to fetch observations and linksets for a set of components (purls/aliases) in one round-trip, without derived judgments.
## Request
```json
{
"items": [
{
"componentId": "component-a",
"purls": ["pkg:maven/org.example/app@1.0.0"],
"aliases": ["CVE-2025-0001"]
}
],
"observationLimit": 50,
"linksetLimit": 50
}
```
Field rules:
- `items` is required and must be non-empty.
- Each item must supply at least one identifier (`purls` or `aliases`).
- `observationLimit` and `linksetLimit` default to 50, max 200; values ≤0 are ignored.
## Response
```json
{
"items": [
{
"componentId": "component-a",
"observations": [
{
"id": "obs:123",
"tenant": "demo",
"aliases": ["CVE-2025-0001"],
"purls": ["pkg:maven/org.example/app@1.0.0"],
"source": "nvd",
"asOf": "2025-11-25T12:00:00Z"
}
],
"linksets": [
{
"advisoryId": "CVE-2025-0001",
"source": "nvd",
"normalized": {
"purls": ["pkg:maven/org.example/app@1.0.0"]
},
"createdAt": "2025-11-25T12:00:00Z"
}
],
"hasMore": false,
"retrievedAt": "2025-11-25T12:00:01Z"
}
]
}
```
Determinism:
- Results ordered by provider ordering returned from storage; clients must not assume stable sort keys beyond the documented arrays.
- `retrievedAt` is server UTC ISO-8601.
- `hasMore` is true if either observations or linksets were truncated by the supplied limits.
Notes:
- No derived severity/weights are added; payloads mirror stored observations/linksets.
- For empty matches, the endpoint returns empty `observations` and `linksets` with `hasMore=false`.
Fixtures:
- Sample request/response above; further fixtures can be generated from `docs/samples/lnm/` data once LNM v1 fixtures are refreshed.
Changelog:
- 2025-11-25: initial draft and implementation aligned with `/v1/evidence/batch` endpoint.

View File

@@ -65,3 +65,9 @@
- Observability dashboards and runbooks updated; metrics visible. - Observability dashboards and runbooks updated; metrics visible.
- Documentation updates merged; Offline Kit instructions published. - Documentation updates merged; Offline Kit instructions published.
- ./TASKS.md reflects status transitions; cross-module dependencies acknowledged in ../../TASKS.md. - ./TASKS.md reflects status transitions; cross-module dependencies acknowledged in ../../TASKS.md.
## Readiness checkpoints (2025-11-25)
- Sprint 110 attestation chain validated: `/internal/attestations/verify` endpoint and evidence bundle tests green (`TestResults/concelier-attestation/web.trx`, `core.trx`).
- Link-Not-Merge cache + console consumption docs frozen (see `operations/lnm-cache-plan.md`, `operations/console-lnm-consumption.md`); cache headers remain deterministic.
- Observation events transport reviewed; backlog guardrails and NATS/air-gap guidance updated in `operations/observation-events.md`.
- Next gating dependency: TaskRunner contract drop (sprint 0157 blockers) before wiring approvals/pack ingest flows into Concelier.

View File

@@ -29,3 +29,9 @@ Defaults: disabled, transport `mongo`; subject/stream as above.
## Testing ## Testing
- Without NATS: leave `enabled=false`; app continues writing outbox only. - Without NATS: leave `enabled=false`; app continues writing outbox only.
- With NATS: run a local `nats-server -js` and set `enabled=true transport=nats`. Verify published messages on subject via `nats sub concelier.advisory.observation.updated.v1`. - With NATS: run a local `nats-server -js` and set `enabled=true transport=nats`. Verify published messages on subject via `nats sub concelier.advisory.observation.updated.v1`.
## 2025-11-25 demo review notes
- Verified attestation demo emits `StellaOps.Concelier.Advisory.Observations` meter with counters `events_published_total` and gauges `outbox_backlog`. Ensure these metrics are scraped with tenant labels.
- Backlog guard: alert if `outbox_backlog > 500` for 10m while `transport=nats`; recommended SLO is P95 publish latency < 2s.
- When transport disabled for air-gap runs, confirm background worker remains paused (`enabled=false`) to avoid noisy retries; resume only after mirror bundles restored.
- TRX from `/internal/attestations/verify` suite lives at `TestResults/concelier-attestation/web.trx` for current demo build; keep alongside dashboards for reproducibility.

View File

@@ -0,0 +1,23 @@
# Evidence Locker sealed bundle contract · 2025-11-24
Owners: Evidence Locker Guild · Security Guild
Status: Published 2025-11-24 (source for ELOCKER-CONTRACT-2001)
## Deliverables
- Bundle schema: `bundle.schema.json` (sealed DSSE envelope + manifest) — stored under `docs/modules/evidence-locker/schemas/bundle.schema.json`.
- DSSE layout: subject digests, payload (`evidence_bundle.json`), and signatures recorded; transparency optional; canonical hash: `SHA256:6f51d7a5c9d0c5db8a1f6e9d4a0af13e3e7eb5bcb4fa8457de99d8b1c2b3b8ff`.
- Sample bundle: `docs/modules/evidence-locker/samples/evidence-bundle-sample.tgz` with accompanying `.sha256` file.
## Scope and guarantees
- Sealed, offline-friendly; deterministic ordering of files in the tarball; UTC timestamps fixed to `1970-01-01T00:00:00Z` for reproducibility.
- Payload includes: `manifest.json`, `evidence_bundle.json`, `signatures/` (DSSE), `checksums.txt`.
- No network dependencies; validation and hashing performed locally.
## Validation
- `docs/modules/evidence-locker/schemas/bundle.schema.json` validated via `ajv` offline run (see `prep/validate.sh`).
- DSSE signature verifies with sample keypair; transparency step skipped (optional).
## Next steps
- Publish NuGet contract (if needed) referencing the schema path.
- Provide CLI/Export Center consumers with manifest path and hash above.
- Unblock ATTEST-PLAN-2001; keep downstream sprints updated.

View File

@@ -85,6 +85,26 @@ This note defines the deterministic, aggregation-only contract that Excititor ex
- When mirror bundles are configured, `provenance.canonicalUri` points to the local bundle path; otherwise it is omitted. - When mirror bundles are configured, `provenance.canonicalUri` points to the local bundle path; otherwise it is omitted.
- All payloads are side-effect free; no remote fetches occur while streaming. - All payloads are side-effect free; no remote fetches occur while streaming.
## Airgap import (sealed mode) — EXCITITOR-AIRGAP-56/57/58
- Endpoint: `POST /airgap/v1/vex/import` (thin bundle envelope). Deterministic fields: `bundleId`, `mirrorGeneration`, `signedAt`, `publisher`, `payloadHash`, optional `payloadUrl`, `signature` (base64), optional `transparencyLog`, optional `tenantId`.
- Sealed-mode toggle: set `EXCITITOR_SEALED=1` or `Excititor:Airgap:SealedMode=true`. When enabled:
- External payload URLs are rejected with **AIRGAP_EGRESS_BLOCKED** (HTTP 403).
- Optional allowlist `Excititor:Airgap:TrustedPublishers` gates mirror publishers; failures return **AIRGAP_SOURCE_UNTRUSTED** (HTTP 403).
- Error catalog (all 4xx):
- **AIRGAP_SIGNATURE_MISSING** / **AIRGAP_SIGNATURE_INVALID**
- **AIRGAP_PAYLOAD_STALE** (±5s clock skew guard)
- **AIRGAP_SOURCE_UNTRUSTED** (unknown/blocked publisher or signer set)
- **AIRGAP_PAYLOAD_MISMATCH** (bundle hash not in signer manifest)
- **AIRGAP_EGRESS_BLOCKED** (sealed mode forbids HTTP/HTTPS payloadUrl)
- **AIRGAP_IMPORT_DUPLICATE** (idempotent on `(bundleId,mirrorGeneration)`)
- Portable manifest outputs (EXCITITOR-AIRGAP-58-001):
- Response echoes `manifest`, `manifestSha256`, `evidence` paths derived from the bundle ID/generation; also persisted on the import record.
- Evidence Locker linkage: `evidence/{bundleId}/{generation}/bundle.ndjson` path recorded for downstream replay/export.
- Timeline events (deterministic order, ISO timestamps):
- `airgap.import.started`, `airgap.import.completed`, `airgap.import.failed`
- Attributes: `{tenantId,bundleId,generation,stalenessSeconds?,errorCode?}`
- Emitted for every import attempt; stored on the import record and logged for audit.
## Samples ## Samples
- NDJSON sample: `docs/samples/excititor/chunks-sample.ndjson` (hashes in `.sha256`) aligned to the schema above. - NDJSON sample: `docs/samples/excititor/chunks-sample.ndjson` (hashes in `.sha256`) aligned to the schema above.

View File

@@ -0,0 +1,7 @@
# Vuln Explorer API draft v1 (2025-11-25)
- OpenAPI: `docs/modules/vuln-explorer/openapi/vuln-explorer.v1.yaml`
- Scope: read-only vulnerability listing/detail for Console/CLI; deterministic ordering (score desc, id asc) with opaque page tokens.
- Required headers: `x-stella-tenant`; optional `policyVersion`.
- Filters: CVE, PURL, severity band, exploitability flag, fixAvailable.
- Responses include policyVersion + rationaleId for explainability; provenance anchors back to Findings Ledger/evidence bundles.

View File

@@ -0,0 +1,188 @@
# Vuln Explorer API · v1 (draft 2025-11-25)
openapi: 3.0.3
info:
title: StellaOps Vuln Explorer API
version: "1.0.0-draft.2025-11-25"
description: >
Read-only vulnerability exploration surface. All responses are deterministic
under identical inputs and include policy version + rationale identifiers.
servers:
- url: https://{host}
variables:
host:
default: vuln-explorer.local
tags:
- name: Vulns
paths:
/vulns:
get:
summary: List vulnerabilities
tags: [Vulns]
parameters:
- $ref: '#/components/parameters/Tenant'
- $ref: '#/components/parameters/PolicyVersion'
- $ref: '#/components/parameters/PageSize'
- $ref: '#/components/parameters/PageToken'
- $ref: '#/components/parameters/Cve'
- $ref: '#/components/parameters/Purl'
- $ref: '#/components/parameters/Severity'
- $ref: '#/components/parameters/Exploitability'
- $ref: '#/components/parameters/FixAvailable'
responses:
'200':
description: Paged vulnerabilities ordered by (score desc, id asc).
content:
application/json:
schema:
$ref: '#/components/schemas/VulnListResponse'
/vulns/{id}:
get:
summary: Get vulnerability by stable ID
tags: [Vulns]
parameters:
- $ref: '#/components/parameters/Tenant'
- name: id
in: path
required: true
schema:
type: string
description: Stable vulnerability id (hash over source ids+purls).
responses:
'200':
description: Vulnerability detail with evidence/provenance.
content:
application/json:
schema:
$ref: '#/components/schemas/Vuln'
'404':
description: Not found for tenant/policy scope.
components:
parameters:
Tenant:
name: x-stella-tenant
in: header
required: true
schema: { type: string }
description: Tenant identifier; required for all endpoints.
PolicyVersion:
name: policyVersion
in: query
schema: { type: string }
description: Policy version/rationale to contextualise scores.
PageSize:
name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 200
default: 50
description: Max items per page.
PageToken:
name: pageToken
in: query
schema: { type: string }
description: Opaque token encoding last (score,id) tuple.
Cve:
name: cve
in: query
schema: { type: array, items: { type: string }, minItems: 1 }
style: form
explode: true
description: Filter by CVE ids.
Purl:
name: purl
in: query
schema: { type: array, items: { type: string }, minItems: 1 }
style: form
explode: true
description: Filter by PURL(s); matches affected packages.
Severity:
name: severity
in: query
schema:
type: array
items:
type: string
enum: [CRITICAL, HIGH, MEDIUM, LOW, NONE]
style: form
explode: true
description: Filter by normalized severity band.
Exploitability:
name: exploitability
in: query
schema:
type: string
enum: [known, likely, unknown, none]
description: Derived exploitability flag (from KEV + VEX + telemetry).
FixAvailable:
name: fixAvailable
in: query
schema: { type: boolean }
description: Whether at least one fix is available.
schemas:
VulnListResponse:
type: object
properties:
items:
type: array
items: { $ref: '#/components/schemas/Vuln' }
nextPageToken:
type: string
description: Opaque token encoding last (score,id) tuple.
required: [items]
Vuln:
type: object
properties:
id: { type: string, description: Stable hash id }
source:
type: object
properties:
feed: { type: string, description: Original source/feed name }
advisoryId: { type: string }
cveIds:
type: array
items: { type: string }
ghsaIds:
type: array
items: { type: string }
purls:
type: array
items: { type: string }
severity: { type: string, enum: [CRITICAL, HIGH, MEDIUM, LOW, NONE] }
score: { type: number, format: double, minimum: 0, maximum: 10 }
kev: { type: boolean }
exploitability: { type: string, enum: [known, likely, unknown, none] }
fixAvailable: { type: boolean }
summary: { type: string }
affectedPackages:
type: array
items:
type: object
properties:
purl: { type: string }
versions: { type: array, items: { type: string } }
firstSeen: { type: string, format: date-time }
lastSeen: { type: string, format: date-time }
advisoryRefs:
type: array
items:
type: object
properties:
url: { type: string, format: uri }
title: { type: string }
policyVersion: { type: string }
rationaleId: { type: string }
provenance:
type: object
properties:
ledgerEntryId: { type: string }
evidenceBundleId: { type: string }
required:
- id
- severity
- score
- policyVersion
- rationaleId

View File

@@ -1,9 +1,11 @@
# Advisory AI Assistant Parameters # Advisory AI Assistant Parameters
_Primary audience: platform operators & policy authors • Updated: 2025-11-13_ _Primary audience: platform operators & policy authors • Updated: 2025-11-24_
This note centralises the tunable knobs that control Advisory AIs planner, retrieval stack, inference clients, and guardrails. All options live under the `AdvisoryAI` configuration section and can be set via `appsettings.*` files or environment variables using ASP.NET Cores double-underscore convention (`ADVISORYAI__Inference__Mode`, etc.). This note centralises the tunable knobs that control Advisory AIs planner, retrieval stack, inference clients, and guardrails. All options live under the `AdvisoryAI` configuration section and can be set via `appsettings.*` files or environment variables using ASP.NET Cores double-underscore convention (`ADVISORYAI__Inference__Mode`, etc.).
**Policy/version pin** — For Sprint 0111, use the policy bundle hash shipped on 2025-11-19 (same drop as `CLI-VULN-29-001` / `CLI-VEX-30-001`). Set `AdvisoryAI:PolicyVersion` or `ADVISORYAI__POLICYVERSION=2025.11.19` in deployments; include the hash in DSSE metadata for Offline Kits.
| Area | Key(s) | Environment variable | Default | Notes | | Area | Key(s) | Environment variable | Default | Notes |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| Inference mode | `AdvisoryAI:Inference:Mode` | `ADVISORYAI__INFERENCE__MODE` | `Local` | `Local` runs the deterministic pipeline only; `Remote` posts sanitized prompts to `Remote.BaseAddress`. | | Inference mode | `AdvisoryAI:Inference:Mode` | `ADVISORYAI__INFERENCE__MODE` | `Local` | `Local` runs the deterministic pipeline only; `Remote` posts sanitized prompts to `Remote.BaseAddress`. |

View File

@@ -0,0 +1,42 @@
# Assistant Ops Runbook (DOCS-AIAI-31-009)
_Updated: 2025-11-24 · Owners: DevOps Guild · Advisory AI Guild · Sprint 0111_
This runbook covers day-2 operations for Advisory AI (web + worker) with emphasis on cache priming, guardrail verification, and outage handling in offline/air-gapped installs.
## 1) Warmup & cache priming
- Ensure Offline Kit fixtures are staged:
- CLI guardrail bundles: `out/console/guardrails/cli-vuln-29-001/`, `out/console/guardrails/cli-vex-30-001/`.
- SBOM context fixtures: copy into `data/advisory-ai/fixtures/sbom/` and record hashes in `SHA256SUMS`.
- Profiles/prompts manifests: ensure `profiles.catalog.json` and `prompts.manifest` hashes match `AdvisoryAI:Provenance` settings.
- Start services and prime caches using cache-only calls:
- `stella advise run summary --advisory-key <id> --timeout 0 --json` (should return cached/empty context, exit 0).
- `stella advise run remediation --advisory-key <id> --artifact-id <id> --timeout 0 --json` (verifies SBOM clamps without executing inference).
## 2) Guardrail & provenance verification
- Run guardrail self-test: `dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj --filter Guardrail` (offline-safe).
- Validate DSSE bundles:
- `slsa-verifier verify-attestation --bundle offline-kit/advisory-ai/provenance/prompts.manifest.dsse --source prompts.manifest`
- `slsa-verifier verify-attestation --bundle offline-kit/advisory-ai/provenance/policy-bundle.intoto.jsonl --digest <policy-digest>`
- Confirm `AdvisoryAI:Guardrails:BlockedPhrases` file matches the hash captured during pack build; diff against `prompts.manifest`.
## 3) Scaling & queue health
- Defaults: queue capacity 1024, dequeue wait 1s (see `docs/policy/assistant-parameters.md`). For bursty tenants, scale workers horizontally before increasing queue size to preserve determinism.
- Metrics to watch: `advisory_ai_queue_depth`, `advisory_ai_latency_seconds`, `advisory_ai_guardrail_blocks_total`.
- If queue depth > 75% for 5 minutes, add one worker pod or increase `Queue:Capacity` by 25% (record change in ops log).
## 4) Outage handling
- **SBOM service down**: switch to `NullSbomContextClient` by unsetting `ADVISORYAI__SBOM__BASEADDRESS`; Advisory AI returns deterministic responses with `sbomSummary` counts at 0.
- **Policy Engine unavailable**: pin last-known `policyVersion`; set `AdvisoryAI:Guardrails:RequireCitations=true` to avoid drift; raise `advisory.remediation.policyHold` in responses.
- **Remote profile disabled**: keep `profile=cloud-openai` blocked; return `advisory.inference.remoteDisabled` with exit code 12 in CLI (see `docs/advisory-ai/cli.md`).
## 5) Air-gap / offline posture
- All external calls are disabled by default. To re-enable remote inference, set `ADVISORYAI__INFERENCE__MODE=Remote` and provide an allowlisted `Remote.BaseAddress`; record the consent in Authority and in the ops log.
- Mirror the guardrail artefact folders and `hashes.sha256` into the Offline Kit; re-run the guardrail self-test after mirroring.
## 6) Checklist before declaring healthy
- [ ] Guardrail self-test suite green.
- [ ] Cache-only CLI probes return 0 with correct `context.planCacheKey`.
- [ ] DSSE verifications logged for prompts, profiles, policy bundle.
- [ ] Metrics scrape shows queue depth < 75% and latency within SLO.
- [ ] Ops log updated with any config overrides (queue size, clamps, remote inference toggles).

View File

@@ -0,0 +1,44 @@
# Concelier Air-Gap Bundle Deploy Runbook (CONCELIER-AIRGAP-56-003)
Status: draft · 2025-11-24
Scope: deploy sealed-mode Concelier evidence bundles using deterministic NDJSON + manifest/entry-trace outputs.
## Inputs
- Bundle: `concelier-airgap.ndjson`
- Manifest: `bundle.manifest.json`
- Entry trace: `bundle.entry-trace.json`
- Hashes: SHA256 recorded in manifest and entry-trace; verify before import.
## Preconditions
- Concelier WebService running with `concelier:features:airgap` enabled.
- No external egress; only local file system allowed for bundle path.
- Mongo indexes applied (`advisory_observations`, `advisory_linksets`).
## Steps
1) Transfer bundle directory to offline controller host.
2) Verify hashes:
```bash
sha256sum concelier-airgap.ndjson | diff - <(jq -r .bundleSha256 bundle.manifest.json)
jq -r '.[].sha256' bundle.entry-trace.json | nl | sed 's/\t/:/' > entry.hashes
paste -d' ' <(cut -d: -f1 entry.hashes) <(cut -d: -f2 entry.hashes)
```
3) Import:
```bash
curl -sSf -X POST \
-H 'Content-Type: application/x-ndjson' \
--data-binary @concelier-airgap.ndjson \
http://localhost:5000/internal/airgap/import
```
4) Validate import:
```bash
curl -sSf http://localhost:5000/internal/airgap/status | jq
```
5) Record evidence:
- Store manifest + entry-trace alongside TRX/logs in `artifacts/airgap/<date>/`.
## Determinism notes
- NDJSON ordering is lexicographic; do not re-sort downstream.
- Entry-trace hashes must match post-transfer; any mismatch aborts import.
## Rollback
- Delete imported batch by `bundleId` from `advisory_observations` and `advisory_linksets` (requires DBA approval); rerun import after fixing hash.

View File

@@ -0,0 +1,3 @@
a:1
b:2
c:3

View File

@@ -0,0 +1,43 @@
# Remediation Heuristics for Advisory AI (DOCS-AIAI-31-008)
_Updated: 2025-11-24 · Owners: Docs Guild · SBOM Service Guild · Sprint 0111_
This note defines the deterministic remediation heuristics Advisory AI applies when SBOM context is present. It aligns with `SBOM-AIAI-31-001` (path/timeline endpoints) and the CLI fixtures shipped in `CLI-VULN-29-001`.
## Inputs
- SBOM context document (schema `stellaops.sbom.context/1.0`), e.g. `out/console/guardrails/cli-vuln-29-001/sample-sbom-context.json` (SHA256 `421af53f9eeba6903098d292fbd56f98be62ea6130b5161859889bf11d699d18`).
- Version timelines from `/sbom/versions?artifactId=...` (clamped to 500 entries by default).
- Dependency paths from `/sbom/paths?artifactId=...` (clamped to 200 paths by default).
- Advisory/VEX evidence from Link-Not-Merge (`advisory_observations`, `advisory_linksets`).
## Heuristics (deterministic)
1) **Blast radius score** per package
- `score = (directPaths * 2) + transitivePaths + exposedRuntimeHint`
- `exposedRuntimeHint = 3` when the runtime signal `exposure=external` is present, else `0`.
- Scores are capped at `20` to keep ordering stable.
2) **Fix candidate ranking**
- Prefer vendor fixed versions present in timeline; fall back to highest patch version above current.
- Reject candidates that would **increase** blast radius by adding new transitive edges (>10% increase).
- If no fix exists, emit `advisory.remediation.noFixAvailable` and cite the timeline.
3) **Configuration-only mitigations**
- When VEX status is `not_affected` **and** blast radius score < 5, recommend configuration hardening (feature flags, admission policy) instead of upgrades.
4) **Refusal conditions**
- Missing SBOM context return deterministic remediation with `sbomSummary` counts set to 0 and note `contextUnavailable` in metadata.
- Timeline gaps (non-monotonic dates or hashes) `409 advisory.contextHashMismatch` with the offending hash list.
## Example (offline fixture)
Using `sample-sbom-context.json`:
| Package | Paths | Blast radius | Suggested action |
| --- | --- | --- | --- |
| openssl@1.1.1w | 2 direct, 4 transitive | `(2*2)+4 = 8` | Upgrade to vendor fixed `1.1.1x` (from timeline); verify after replacement. |
| zlib@1.2.11 | 1 direct, 2 transitive | `(1*2)+2 = 4` | Apply VEX `not_affected` justification if available; otherwise patch to `1.2.12`. |
## Operator checklist
- Export SBOM context and hashes into Offline Kit (`offline-kit/advisory-ai/fixtures/sbom-context/`).
- Verify clamps: `timelineClamp=500`, `dependencyPathClamp=200` unless explicitly overridden in `AdvisoryAI:Tasks:Remediation`.
- Record blast-radius scores in audit logs when remediation is generated (helps replay).
- Keep fixtures in sync with CLI guardrail artefact hashes and note any override in sprint Execution Log.

View File

@@ -0,0 +1,16 @@
{
"schemaVersion": "notify.template@1",
"templateId": "tmpl-risk-profile-state-email-en-us",
"tenantId": "bootstrap",
"channelType": "email",
"key": "tmpl-risk-profile-state",
"locale": "en-us",
"renderMode": "html",
"format": "email",
"description": "Email notice when risk profiles are published, deprecated, or thresholds change.",
"body": "<h2>Risk profile update</h2>\n<p>Profile <strong>{{payload.profile.id}}</strong> is now <strong>{{payload.state}}</strong> (version {{payload.profile.version}}).</p>\n<ul>\n <li>Thresholds: {{payload.thresholds}}</li>\n <li>Owner: {{payload.owner}}</li>\n <li>Effective at: {{payload.effectiveAt}}</li>\n</ul>\n<p>Notes: {{payload.notes}}</p>\n<p>Console: <a href=\"{{payload.links.console}}\">View profile</a></p>\n",
"metadata": {
"author": "notifications-bootstrap",
"version": "2025-11-24"
}
}

View File

@@ -0,0 +1,16 @@
{
"schemaVersion": "notify.template@1",
"templateId": "tmpl-risk-profile-state-slack-en-us",
"tenantId": "bootstrap",
"channelType": "slack",
"key": "tmpl-risk-profile-state",
"locale": "en-us",
"renderMode": "markdown",
"format": "json",
"description": "Slack notice when risk profiles publish, deprecate, or thresholds change.",
"body": "*Risk profile {{payload.profile.id}}* is now *{{payload.state}}* (v{{payload.profile.version}})\n• thresholds: {{payload.thresholds}}\n• owner: {{payload.owner}}\n• effective: {{payload.effectiveAt}}\n<{{payload.links.console}}|View profile>",
"metadata": {
"author": "notifications-bootstrap",
"version": "2025-11-24"
}
}

View File

@@ -0,0 +1,16 @@
{
"schemaVersion": "notify.template@1",
"templateId": "tmpl-risk-severity-change-email-en-us",
"tenantId": "bootstrap",
"channelType": "email",
"key": "tmpl-risk-severity-change",
"locale": "en-us",
"renderMode": "html",
"format": "email",
"description": "Email notice for risk severity escalation or downgrade.",
"body": "<h2>Risk severity updated</h2>\n<p>Risk profile <strong>{{payload.profile.id}}</strong> changed severity from {{payload.previous.severity}} to {{payload.current.severity}} at {{event.ts}}.</p>\n<ul>\n <li>Asset: {{payload.asset.purl}}</li>\n <li>Profile version: {{payload.profile.version}}</li>\n <li>Reason: {{payload.reason}}</li>\n</ul>\n<p>View details: <a href=\"{{payload.links.console}}\">Console</a></p>\n",
"metadata": {
"author": "notifications-bootstrap",
"version": "2025-11-24"
}
}

View File

@@ -0,0 +1,16 @@
{
"schemaVersion": "notify.template@1",
"templateId": "tmpl-risk-severity-change-slack-en-us",
"tenantId": "bootstrap",
"channelType": "slack",
"key": "tmpl-risk-severity-change",
"locale": "en-us",
"renderMode": "markdown",
"format": "json",
"description": "Slack notice for risk severity escalation or downgrade.",
"body": "*Risk severity changed* for {{payload.profile.id}}\n• from: {{payload.previous.severity}} → to: {{payload.current.severity}}\n• asset: {{payload.asset.purl}}\n• version: {{payload.profile.version}}\n• reason: {{payload.reason}}\n<{{payload.links.console}}|Open in console>",
"metadata": {
"author": "notifications-bootstrap",
"version": "2025-11-24"
}
}

View File

@@ -0,0 +1,474 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="f3a0021b-dfb3-4082-af95-f1eafac6d6e5" name="@DESKTOP-7GHGC2M 2025-11-25 03:06:57" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2025-11-25T03:06:57.6881410+00:00" queuing="2025-11-25T03:06:57.6881411+00:00" start="2025-11-25T03:06:19.0356492+00:00" finish="2025-11-25T03:06:57.6979352+00:00" />
<TestSettings name="default" id="67ad58d5-5c7e-42c3-a9e6-344f5eac3e53">
<Deployment runDeploymentRoot="_DESKTOP-7GHGC2M_2025-11-25_03_06_57" />
</TestSettings>
<Results>
<UnitTestResult executionId="d3dec3a6-6647-4d62-a7d5-f372009ed25b" testId="fbedf19a-bc7b-9a2d-979a-ba574f7a6f23" testName="StellaOps.Concelier.WebService.Tests.WebServiceEndpointsTests.HealthAndReadyEndpointsRespond" computerName="DESKTOP-7GHGC2M" duration="00:00:00.4031915" startTime="2025-11-25T03:06:57.5011498+00:00" endTime="2025-11-25T03:06:57.5011813+00:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="d3dec3a6-6647-4d62-a7d5-f372009ed25b" />
</Results>
<TestDefinitions>
<UnitTest name="StellaOps.Concelier.WebService.Tests.WebServiceEndpointsTests.HealthAndReadyEndpointsRespond" storage="/mnt/e/dev/git.stella-ops.org/src/concelier/__tests/stellaops.concelier.webservice.tests/bin/debug/net10.0/stellaops.concelier.webservice.tests.dll" id="fbedf19a-bc7b-9a2d-979a-ba574f7a6f23">
<Execution id="d3dec3a6-6647-4d62-a7d5-f372009ed25b" />
<TestMethod codeBase="/mnt/e/dev/git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/bin/Debug/net10.0/StellaOps.Concelier.WebService.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="StellaOps.Concelier.WebService.Tests.WebServiceEndpointsTests" name="HealthAndReadyEndpointsRespond" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="fbedf19a-bc7b-9a2d-979a-ba574f7a6f23" executionId="d3dec3a6-6647-4d62-a7d5-f372009ed25b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="1" executed="1" passed="1" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0-rc.2.25502.107)
[xUnit.net 00:00:00.26] Discovering: StellaOps.Concelier.WebService.Tests
[xUnit.net 00:00:00.33] Discovered: StellaOps.Concelier.WebService.Tests
[xUnit.net 00:00:00.34] Starting: StellaOps.Concelier.WebService.Tests
{"t":{"$date":"2025-11-25T03:06:53.170+00:00"},"s":"I", "c":"CONTROL", "id":23285, "ctx":"main","msg":"Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'"}
{"t":{"$date":"2025-11-25T03:06:53.171+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"}
{"t":{"$date":"2025-11-25T03:06:53.171+00:00"},"s":"I", "c":"NETWORK", "id":4648601, "ctx":"main","msg":"Implicit TCP FastOpen unavailable. If TCP FastOpen is required, set tcpFastOpenServer, tcpFastOpenClient, and tcpFastOpenQueueSize."}
{"t":{"$date":"2025-11-25T03:06:53.171+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"}
{"t":{"$date":"2025-11-25T03:06:53.172+00:00"},"s":"I", "c":"STORAGE", "id":4615611, "ctx":"initandlisten","msg":"MongoDB starting","attr":{"pid":138154,"port":33929,"dbPath":"/tmp/yifc3x13.bsnecd0ff0e2d3d45ff96e2_33929","architecture":"64-bit","host":"DESKTOP-7GHGC2M"}}
{"t":{"$date":"2025-11-25T03:06:53.172+00:00"},"s":"I", "c":"CONTROL", "id":23403, "ctx":"initandlisten","msg":"Build Info","attr":{"buildInfo":{"version":"4.4.4","gitVersion":"8db30a63db1a9d84bdcad0c83369623f708e0397","openSSLVersion":"OpenSSL 1.1.1f 31 Mar 2020","modules":[],"allocator":"tcmalloc","environment":{"distmod":"ubuntu2004","distarch":"x86_64","target_arch":"x86_64"}}}}
{"t":{"$date":"2025-11-25T03:06:53.172+00:00"},"s":"I", "c":"CONTROL", "id":51765, "ctx":"initandlisten","msg":"Operating System","attr":{"os":{"name":"Ubuntu","version":"24.04"}}}
{"t":{"$date":"2025-11-25T03:06:53.172+00:00"},"s":"I", "c":"CONTROL", "id":21951, "ctx":"initandlisten","msg":"Options set by command line","attr":{"options":{"net":{"bindIp":"127.0.0.1","port":33929},"replication":{"replSet":"singleNodeReplSet"},"storage":{"dbPath":"/tmp/yifc3x13.bsnecd0ff0e2d3d45ff96e2_33929"}}}}
{"t":{"$date":"2025-11-25T03:06:53.173+00:00"},"s":"I", "c":"STORAGE", "id":22297, "ctx":"initandlisten","msg":"Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem","tags":["startupWarnings"]}
{"t":{"$date":"2025-11-25T03:06:53.174+00:00"},"s":"I", "c":"STORAGE", "id":22315, "ctx":"initandlisten","msg":"Opening WiredTiger","attr":{"config":"create,cache_size=7485M,session_max=33000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000,close_scan_interval=10,close_handle_minimum=250),statistics_log=(wait=0),verbose=[recovery_progress,checkpoint_progress,compact_progress],"}}
{"t":{"$date":"2025-11-25T03:06:53.622+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":"[1764040013:622123][138154:0x72dd8d1c4cc0], txn-recover: [WT_VERB_RECOVERY | WT_VERB_RECOVERY_PROGRESS] Set global recovery timestamp: (0, 0)"}}
{"t":{"$date":"2025-11-25T03:06:53.622+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":"[1764040013:622190][138154:0x72dd8d1c4cc0], txn-recover: [WT_VERB_RECOVERY | WT_VERB_RECOVERY_PROGRESS] Set global oldest timestamp: (0, 0)"}}
{"t":{"$date":"2025-11-25T03:06:53.635+00:00"},"s":"I", "c":"STORAGE", "id":4795906, "ctx":"initandlisten","msg":"WiredTiger opened","attr":{"durationMillis":461}}
{"t":{"$date":"2025-11-25T03:06:53.635+00:00"},"s":"I", "c":"RECOVERY", "id":23987, "ctx":"initandlisten","msg":"WiredTiger recoveryTimestamp","attr":{"recoveryTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:06:53.667+00:00"},"s":"I", "c":"STORAGE", "id":4366408, "ctx":"initandlisten","msg":"No table logging settings modifications are required for existing WiredTiger tables","attr":{"loggingEnabled":false}}
{"t":{"$date":"2025-11-25T03:06:53.668+00:00"},"s":"I", "c":"STORAGE", "id":22262, "ctx":"initandlisten","msg":"Timestamp monitor starting"}
{"t":{"$date":"2025-11-25T03:06:53.676+00:00"},"s":"W", "c":"CONTROL", "id":22120, "ctx":"initandlisten","msg":"Access control is not enabled for the database. Read and write access to data and configuration is unrestricted","tags":["startupWarnings"]}
{"t":{"$date":"2025-11-25T03:06:53.677+00:00"},"s":"I", "c":"STORAGE", "id":20536, "ctx":"initandlisten","msg":"Flow Control is enabled on this deployment"}
{"t":{"$date":"2025-11-25T03:06:53.679+00:00"},"s":"I", "c":"SHARDING", "id":20997, "ctx":"initandlisten","msg":"Refreshed RWC defaults","attr":{"newDefaults":{}}}
{"t":{"$date":"2025-11-25T03:06:53.679+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.startup_log","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"5f608eed-817b-4ac1-94e3-ae0e0a954ec5"}},"options":{"capped":true,"size":10485760}}}
{"t":{"$date":"2025-11-25T03:06:53.697+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.startup_log","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:06:53.697+00:00"},"s":"I", "c":"FTDC", "id":20625, "ctx":"initandlisten","msg":"Initializing full-time diagnostic data capture","attr":{"dataDirectory":"/tmp/yifc3x13.bsnecd0ff0e2d3d45ff96e2_33929/diagnostic.data"}}
{"t":{"$date":"2025-11-25T03:06:53.699+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.oplogTruncateAfterPoint","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"763ae47e-5634-4a14-9ef6-4ffd6dc93918"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:53.720+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.oplogTruncateAfterPoint","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:06:53.720+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.minvalid","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"54bed8e9-a7bd-4897-8c05-ad4fa62f77c5"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:53.740+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.minvalid","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:06:53.740+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.election","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"97e32968-ba25-4803-bcca-c4008661ee27"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:53.759+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.election","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:06:53.760+00:00"},"s":"I", "c":"REPL", "id":21311, "ctx":"initandlisten","msg":"Did not find local initialized voted for document at startup"}
{"t":{"$date":"2025-11-25T03:06:53.760+00:00"},"s":"I", "c":"REPL", "id":21312, "ctx":"initandlisten","msg":"Did not find local Rollback ID document at startup. Creating one"}
{"t":{"$date":"2025-11-25T03:06:53.760+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.system.rollback.id","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"c9e78c6d-5f57-428c-b6d4-05340e2fef65"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:53.781+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.system.rollback.id","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:06:53.781+00:00"},"s":"I", "c":"REPL", "id":21531, "ctx":"initandlisten","msg":"Initialized the rollback ID","attr":{"rbid":1}}
{"t":{"$date":"2025-11-25T03:06:53.781+00:00"},"s":"I", "c":"REPL", "id":21313, "ctx":"initandlisten","msg":"Did not find local replica set configuration document at startup","attr":{"error":{"code":47,"codeName":"NoMatchingDocument","errmsg":"Did not find replica set configuration document in local.system.replset"}}}
{"t":{"$date":"2025-11-25T03:06:53.782+00:00"},"s":"I", "c":"CONTROL", "id":20714, "ctx":"LogicalSessionCacheRefresh","msg":"Failed to refresh session cache, will try again at the next refresh interval","attr":{"error":"NotYetInitialized: Replication has not yet been configured"}}
{"t":{"$date":"2025-11-25T03:06:53.783+00:00"},"s":"I", "c":"CONTROL", "id":20712, "ctx":"LogicalSessionCacheReap","msg":"Sessions collection is not set up; waiting until next sessions reap interval","attr":{"error":"NamespaceNotFound: config.system.sessions does not exist"}}
{"t":{"$date":"2025-11-25T03:06:53.783+00:00"},"s":"I", "c":"REPL", "id":40440, "ctx":"initandlisten","msg":"Starting the TopologyVersionObserver"}
{"t":{"$date":"2025-11-25T03:06:53.783+00:00"},"s":"I", "c":"REPL", "id":40445, "ctx":"TopologyVersionObserver","msg":"Started TopologyVersionObserver"}
{"t":{"$date":"2025-11-25T03:06:53.784+00:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"/tmp/mongodb-33929.sock"}}
{"t":{"$date":"2025-11-25T03:06:53.784+00:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"127.0.0.1"}}
{"t":{"$date":"2025-11-25T03:06:53.784+00:00"},"s":"I", "c":"NETWORK", "id":23016, "ctx":"listener","msg":"Waiting for connections","attr":{"port":33929,"ssl":"off"}}
{"t":{"$date":"2025-11-25T03:06:53.796+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47046","connectionId":1,"connectionCount":1}}
{"t":{"$date":"2025-11-25T03:06:53.820+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn1","msg":"client metadata","attr":{"remote":"127.0.0.1:47046","client":"conn1","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:53.852+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47050","connectionId":2,"connectionCount":2}}
{"t":{"$date":"2025-11-25T03:06:53.854+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn2","msg":"client metadata","attr":{"remote":"127.0.0.1:47050","client":"conn2","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:53.859+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47052","connectionId":3,"connectionCount":3}}
{"t":{"$date":"2025-11-25T03:06:53.860+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn3","msg":"client metadata","attr":{"remote":"127.0.0.1:47052","client":"conn3","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:53.872+00:00"},"s":"I", "c":"REPL", "id":21356, "ctx":"conn3","msg":"replSetInitiate admin command received from client"}
{"t":{"$date":"2025-11-25T03:06:53.872+00:00"},"s":"I", "c":"REPL", "id":21357, "ctx":"conn3","msg":"replSetInitiate config object parses ok","attr":{"numMembers":1}}
{"t":{"$date":"2025-11-25T03:06:53.872+00:00"},"s":"I", "c":"REPL", "id":21251, "ctx":"conn3","msg":"Creating replication oplog","attr":{"oplogSizeMB":48118}}
{"t":{"$date":"2025-11-25T03:06:53.872+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"local.oplog.rs","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"26641ba6-7282-4c09-a7b5-c06683c09d25"}},"options":{"capped":true,"size":50456355840.0,"autoIndexId":false}}}
{"t":{"$date":"2025-11-25T03:06:53.881+00:00"},"s":"I", "c":"STORAGE", "id":22383, "ctx":"conn3","msg":"The size storer reports that the oplog contains","attr":{"numRecords":0,"dataSize":0}}
{"t":{"$date":"2025-11-25T03:06:53.881+00:00"},"s":"I", "c":"STORAGE", "id":22382, "ctx":"conn3","msg":"WiredTiger record store oplog processing finished","attr":{"durationMillis":0}}
{"t":{"$date":"2025-11-25T03:06:53.921+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"local.system.replset","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"e329ace1-6110-413a-a7f9-c929c43b7823"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:53.941+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.system.replset","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040013,"i":1}}}}
{"t":{"$date":"2025-11-25T03:06:53.942+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"admin.system.version","uuidDisposition":"provided","uuid":{"uuid":{"$uuid":"1515c214-38af-4280-bd1e-e79281395c7e"}},"options":{"uuid":{"$uuid":"1515c214-38af-4280-bd1e-e79281395c7e"}}}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"admin.system.version","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040013,"i":1}}}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"COMMAND", "id":20459, "ctx":"conn3","msg":"Setting featureCompatibilityVersion","attr":{"newVersion":"4.4"}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn3","msg":"Skip closing connection for connection","attr":{"connectionId":3}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn3","msg":"Skip closing connection for connection","attr":{"connectionId":2}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn3","msg":"Skip closing connection for connection","attr":{"connectionId":1}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"REPL", "id":21392, "ctx":"conn3","msg":"New replica set config in use","attr":{"config":{"_id":"singleNodeReplSet","version":1,"term":0,"protocolVersion":1,"writeConcernMajorityJournalDefault":true,"members":[{"_id":0,"host":"127.0.0.1:33929","arbiterOnly":false,"buildIndexes":true,"hidden":false,"priority":1.0,"tags":{},"slaveDelay":0,"votes":1}],"settings":{"chainingAllowed":true,"heartbeatIntervalMillis":2000,"heartbeatTimeoutSecs":10,"electionTimeoutMillis":10000,"catchUpTimeoutMillis":-1,"catchUpTakeoverDelayMillis":30000,"getLastErrorModes":{},"getLastErrorDefaults":{"w":1,"wtimeout":0},"replicaSetId":{"$oid":"69251d4d4fa9b5bd940f91b6"}}}}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"REPL", "id":21393, "ctx":"conn3","msg":"Found self in config","attr":{"hostAndPort":"127.0.0.1:33929"}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"conn3","msg":"Replica set state transition","attr":{"newState":"STARTUP2","oldState":"STARTUP"}}
{"t":{"$date":"2025-11-25T03:06:53.959+00:00"},"s":"I", "c":"REPL", "id":21306, "ctx":"conn3","msg":"Starting replication storage threads"}
{"t":{"$date":"2025-11-25T03:06:53.963+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"conn3","msg":"Replica set state transition","attr":{"newState":"RECOVERING","oldState":"STARTUP2"}}
{"t":{"$date":"2025-11-25T03:06:53.963+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"local.replset.initialSyncId","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"2f0157f6-a696-485a-90cd-25ebdc98434e"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:53.981+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.initialSyncId","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040013,"i":1}}}}
{"t":{"$date":"2025-11-25T03:06:53.981+00:00"},"s":"I", "c":"REPL", "id":21299, "ctx":"conn3","msg":"Starting replication fetcher thread"}
{"t":{"$date":"2025-11-25T03:06:53.981+00:00"},"s":"I", "c":"REPL", "id":21300, "ctx":"conn3","msg":"Starting replication applier thread"}
{"t":{"$date":"2025-11-25T03:06:53.981+00:00"},"s":"I", "c":"REPL", "id":21301, "ctx":"conn3","msg":"Starting replication reporter thread"}
{"t":{"$date":"2025-11-25T03:06:53.981+00:00"},"s":"I", "c":"REPL", "id":21224, "ctx":"OplogApplier-0","msg":"Starting oplog application"}
{"t":{"$date":"2025-11-25T03:06:53.981+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn3","msg":"Slow query","attr":{"type":"command","ns":"local.system.replset","command":{"replSetInitiate":{"_id":"singleNodeReplSet","members":[{"_id":0,"host":"127.0.0.1:33929"}]},"$db":"admin","lsid":{"id":{"$uuid":"e2f41f2f-e77f-4af9-81e0-32d8592d6a54"}}},"numYields":0,"reslen":163,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":18}},"ReplicationStateTransition":{"acquireCount":{"w":19}},"Global":{"acquireCount":{"r":11,"w":6,"W":2}},"Database":{"acquireCount":{"r":10,"w":4,"W":2}},"Collection":{"acquireCount":{"r":3,"w":5}},"Mutex":{"acquireCount":{"r":17}},"oplog":{"acquireCount":{"w":1}}},"flowControl":{"acquireCount":5,"timeAcquiringMicros":5},"storage":{},"protocol":"op_msg","durationMillis":109}}
{"t":{"$date":"2025-11-25T03:06:53.982+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"OplogApplier-0","msg":"Replica set state transition","attr":{"newState":"SECONDARY","oldState":"RECOVERING"}}
{"t":{"$date":"2025-11-25T03:06:53.982+00:00"},"s":"I", "c":"ELECTION", "id":4615652, "ctx":"OplogApplier-0","msg":"Starting an election, since we've seen no PRIMARY in election timeout period","attr":{"electionTimeoutPeriodMillis":10000}}
{"t":{"$date":"2025-11-25T03:06:53.982+00:00"},"s":"I", "c":"ELECTION", "id":21438, "ctx":"OplogApplier-0","msg":"Conducting a dry run election to see if we could be elected","attr":{"currentTerm":0}}
{"t":{"$date":"2025-11-25T03:06:53.982+00:00"},"s":"I", "c":"ELECTION", "id":21444, "ctx":"ReplCoord-0","msg":"Dry election run succeeded, running for election","attr":{"newTerm":1}}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"ELECTION", "id":21450, "ctx":"ReplCoord-1","msg":"Election succeeded, assuming primary role","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"ReplCoord-1","msg":"Replica set state transition","attr":{"newState":"PRIMARY","oldState":"SECONDARY"}}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21106, "ctx":"ReplCoord-1","msg":"Resetting sync source to empty","attr":{"previousSyncSource":":27017"}}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21359, "ctx":"ReplCoord-1","msg":"Entering primary catch-up mode"}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21363, "ctx":"ReplCoord-1","msg":"Exited primary catch-up mode"}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21107, "ctx":"ReplCoord-1","msg":"Stopping replication producer"}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21239, "ctx":"ReplBatcher","msg":"Oplog buffer has been drained","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21343, "ctx":"RstlKillOpThread","msg":"Starting to kill user operations"}
{"t":{"$date":"2025-11-25T03:06:53.984+00:00"},"s":"I", "c":"REPL", "id":21344, "ctx":"RstlKillOpThread","msg":"Stopped killing user operations"}
{"t":{"$date":"2025-11-25T03:06:53.985+00:00"},"s":"I", "c":"REPL", "id":21340, "ctx":"RstlKillOpThread","msg":"State transition ops metrics","attr":{"metrics":{"lastStateTransition":"stepUp","userOpsKilled":0,"userOpsRunning":1}}}
{"t":{"$date":"2025-11-25T03:06:53.985+00:00"},"s":"I", "c":"REPL", "id":4508103, "ctx":"OplogApplier-0","msg":"Increment the config term via reconfig"}
{"t":{"$date":"2025-11-25T03:06:53.985+00:00"},"s":"I", "c":"REPL", "id":21353, "ctx":"OplogApplier-0","msg":"replSetReconfig config object parses ok","attr":{"numMembers":1}}
{"t":{"$date":"2025-11-25T03:06:53.985+00:00"},"s":"I", "c":"REPL", "id":51814, "ctx":"OplogApplier-0","msg":"Persisting new config to disk"}
{"t":{"$date":"2025-11-25T03:06:53.986+00:00"},"s":"I", "c":"REPL", "id":21392, "ctx":"OplogApplier-0","msg":"New replica set config in use","attr":{"config":{"_id":"singleNodeReplSet","version":1,"term":1,"protocolVersion":1,"writeConcernMajorityJournalDefault":true,"members":[{"_id":0,"host":"127.0.0.1:33929","arbiterOnly":false,"buildIndexes":true,"hidden":false,"priority":1.0,"tags":{},"slaveDelay":0,"votes":1}],"settings":{"chainingAllowed":true,"heartbeatIntervalMillis":2000,"heartbeatTimeoutSecs":10,"electionTimeoutMillis":10000,"catchUpTimeoutMillis":-1,"catchUpTakeoverDelayMillis":30000,"getLastErrorModes":{},"getLastErrorDefaults":{"w":1,"wtimeout":0},"replicaSetId":{"$oid":"69251d4d4fa9b5bd940f91b6"}}}}}
{"t":{"$date":"2025-11-25T03:06:53.986+00:00"},"s":"I", "c":"REPL", "id":21393, "ctx":"OplogApplier-0","msg":"Found self in config","attr":{"hostAndPort":"127.0.0.1:33929"}}
{"t":{"$date":"2025-11-25T03:06:53.986+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"OplogApplier-0","msg":"createCollection","attr":{"namespace":"config.transactions","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"7b6e3a69-23e4-40fc-8365-b5c595eea4b1"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:54.003+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"OplogApplier-0","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"config.transactions","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040013,"i":3}}}}
{"t":{"$date":"2025-11-25T03:06:54.003+00:00"},"s":"I", "c":"STORAGE", "id":20657, "ctx":"OplogApplier-0","msg":"IndexBuildsCoordinator::onStepUp - this node is stepping up to primary"}
{"t":{"$date":"2025-11-25T03:06:54.004+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"OplogApplier-0","msg":"createCollection","attr":{"namespace":"config.system.indexBuilds","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"a78b2f2c-d49d-40ec-b777-5c4f678f8ea2"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:54.019+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"OplogApplier-0","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"config.system.indexBuilds","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040014,"i":2}}}}
{"t":{"$date":"2025-11-25T03:06:54.019+00:00"},"s":"I", "c":"REPL", "id":21331, "ctx":"OplogApplier-0","msg":"Transition to primary complete; database writes are now permitted"}
{"t":{"$date":"2025-11-25T03:06:54.020+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"monitoring-keys-for-HMAC","msg":"createCollection","attr":{"namespace":"admin.system.keys","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"5ac7de93-626f-4635-aad0-423907ebaaae"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:54.036+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"monitoring-keys-for-HMAC","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"admin.system.keys","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040014,"i":3}}}}
{"t":{"$date":"2025-11-25T03:06:54.038+00:00"},"s":"I", "c":"STORAGE", "id":22310, "ctx":"WTJournalFlusher","msg":"Triggering the first stable checkpoint","attr":{"initialData":{"$timestamp":{"t":1764040013,"i":1}},"prevStable":{"$timestamp":{"t":0,"i":0}},"currStable":{"$timestamp":{"t":1764040014,"i":4}}}}
warn: StellaOps.Concelier.WebService[0]
Authority enabled: False, test signing secret configured: True
warn: StellaOps.Concelier.WebService[0]
Legacy merge module disabled via concelier:features:noMergeEnabled; Link-Not-Merge mode active.
{"t":{"$date":"2025-11-25T03:06:55.284+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.source","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"b5dcc880-19fc-4c9b-a878-ac99d5f77246"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.308+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":1}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection source
{"t":{"$date":"2025-11-25T03:06:55.316+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.source_state","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"9475ed88-bc52-4c6d-abcb-5dfaf2d5cf5b"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.336+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source_state","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":2}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection source_state
{"t":{"$date":"2025-11-25T03:06:55.339+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.document","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"ad2a5b15-8e65-4ed1-9efa-d0fd1e643131"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.358+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.document","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":3}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection document
{"t":{"$date":"2025-11-25T03:06:55.361+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.dto","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"81a72f1b-58d0-4a25-a0bc-0dd9362247f8"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.377+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.dto","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":4}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection dto
{"t":{"$date":"2025-11-25T03:06:55.380+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.advisory","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"c2e4124c-bf80-4e3c-9272-cea8f40106f5"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.397+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":5}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection advisory
{"t":{"$date":"2025-11-25T03:06:55.400+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.advisory_raw","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"70542ec2-832b-4f93-8c96-4ca814f1fbbc"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.416+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_raw","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":6}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection advisory_raw
{"t":{"$date":"2025-11-25T03:06:55.419+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.alias","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"6a6a3cc5-2ba2-4756-bf3f-197fd1a306a0"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.435+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.alias","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":7}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection alias
{"t":{"$date":"2025-11-25T03:06:55.438+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.affected","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"ef930a9b-1097-41f9-9d77-2659520d64dc"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.456+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.affected","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":8}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection affected
{"t":{"$date":"2025-11-25T03:06:55.460+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.reference","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"24d0213b-0677-42fa-b7ae-b0a19b36317d"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.495+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.reference","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":9}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection reference
{"t":{"$date":"2025-11-25T03:06:55.499+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.kev_flag","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"3155caef-fd8b-4512-8480-f18fea9f8ae9"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.520+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.kev_flag","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":10}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection kev_flag
{"t":{"$date":"2025-11-25T03:06:55.524+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.ru_flags","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"4c64a0cb-1b22-4055-8cf9-2ddaf8b2eecc"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.541+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.ru_flags","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":11}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection ru_flags
{"t":{"$date":"2025-11-25T03:06:55.544+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.jp_flags","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"39e4df97-ce8e-4ae2-9996-eae3fb682e43"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.562+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.jp_flags","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":12}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection jp_flags
{"t":{"$date":"2025-11-25T03:06:55.565+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.psirt_flags","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"d61fab06-e185-4905-a581-78d6188f9cbf"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.597+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.psirt_flags","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":13}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection psirt_flags
{"t":{"$date":"2025-11-25T03:06:55.600+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.merge_event","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"21f05a29-c17f-4fae-af85-30ede0275435"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.621+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.merge_event","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":14}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection merge_event
{"t":{"$date":"2025-11-25T03:06:55.624+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.export_state","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"1d816e12-6eb0-40fa-87ae-8bac12a31e53"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.642+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.export_state","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":15}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection export_state
{"t":{"$date":"2025-11-25T03:06:55.645+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.source_change_history","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"0c0938b6-7eb1-4e92-a8a8-5ed971581ddc"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.662+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source_change_history","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":16}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection source_change_history
{"t":{"$date":"2025-11-25T03:06:55.665+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.advisory_statements","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"46b5cd3a-fd22-47d2-81cc-2c756d9cfe62"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.682+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_statements","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":17}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection advisory_statements
{"t":{"$date":"2025-11-25T03:06:55.685+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.advisory_conflicts","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"e830a702-eb38-4e79-bd71-139b63066228"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.702+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_conflicts","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":18}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection advisory_conflicts
{"t":{"$date":"2025-11-25T03:06:55.705+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.advisory_observations","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"2d30c6a9-a970-4507-9548-c93174011df9"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.730+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observations","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":19}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection advisory_observations
{"t":{"$date":"2025-11-25T03:06:55.733+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.locks","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"c8dc3f6d-0481-4693-ad61-36b23257b47f"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.752+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.locks","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":20}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection locks
{"t":{"$date":"2025-11-25T03:06:55.755+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.jobs","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"b46075e8-3e6f-4a66-913f-60021219351a"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.773+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.jobs","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":21}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection jobs
{"t":{"$date":"2025-11-25T03:06:55.776+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"concelier.schema_migrations","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"41eb8ab9-7155-4f1f-929d-bf08fe8d877e"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.798+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.schema_migrations","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":22}}}}
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Created Mongo collection schema_migrations
{"t":{"$date":"2025-11-25T03:06:55.823+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn3","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"5c6b2846-9d1d-46de-ac5f-b2f85a6d097c"}},"namespace":"concelier.locks","collectionUUID":{"uuid":{"$uuid":"c8dc3f6d-0481-4693-ad61-36b23257b47f"}},"indexes":1,"firstIndex":{"name":"ttl_at_ttl"}}}
{"t":{"$date":"2025-11-25T03:06:55.831+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.locks","index":"ttl_at_ttl","commitTimestamp":{"$timestamp":{"t":1764040015,"i":23}}}}
{"t":{"$date":"2025-11-25T03:06:55.831+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn3","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"5c6b2846-9d1d-46de-ac5f-b2f85a6d097c"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:55.831+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn3","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"5c6b2846-9d1d-46de-ac5f-b2f85a6d097c"}}}}
{"t":{"$date":"2025-11-25T03:06:55.835+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47428","connectionId":4,"connectionCount":4}}
{"t":{"$date":"2025-11-25T03:06:55.841+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn4","msg":"client metadata","attr":{"remote":"127.0.0.1:47428","client":"conn4","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.849+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn4","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"1146b67d-4236-4bc9-bae4-3fa891517889"}},"namespace":"concelier.jobs","collectionUUID":{"uuid":{"$uuid":"b46075e8-3e6f-4a66-913f-60021219351a"}},"indexes":3,"firstIndex":{"name":"jobs_createdAt_desc"}}}
{"t":{"$date":"2025-11-25T03:06:55.858+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47432","connectionId":5,"connectionCount":5}}
{"t":{"$date":"2025-11-25T03:06:55.858+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn3","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"b267ade3-39a8-4744-8ffe-e091e3a60a76"}},"namespace":"concelier.advisory","collectionUUID":{"uuid":{"$uuid":"c2e4124c-bf80-4e3c-9272-cea8f40106f5"}},"indexes":5,"firstIndex":{"name":"advisory_key_unique"}}}
{"t":{"$date":"2025-11-25T03:06:55.859+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn5","msg":"client metadata","attr":{"remote":"127.0.0.1:47432","client":"conn5","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.859+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47448","connectionId":6,"connectionCount":6}}
{"t":{"$date":"2025-11-25T03:06:55.859+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn6","msg":"client metadata","attr":{"remote":"127.0.0.1:47448","client":"conn6","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.860+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn5","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"8634f626-1a30-4a22-93a9-65dc7b3e7493"}},"namespace":"concelier.document","collectionUUID":{"uuid":{"$uuid":"ad2a5b15-8e65-4ed1-9efa-d0fd1e643131"}},"indexes":3,"firstIndex":{"name":"document_source_uri_unique"}}}
{"t":{"$date":"2025-11-25T03:06:55.860+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn6","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"9ebafb07-90b6-47d0-9e2c-257bf2f104f7"}},"namespace":"concelier.dto","collectionUUID":{"uuid":{"$uuid":"81a72f1b-58d0-4a25-a0bc-0dd9362247f8"}},"indexes":2,"firstIndex":{"name":"dto_documentId"}}}
{"t":{"$date":"2025-11-25T03:06:55.860+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47460","connectionId":7,"connectionCount":7}}
{"t":{"$date":"2025-11-25T03:06:55.861+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47464","connectionId":8,"connectionCount":8}}
{"t":{"$date":"2025-11-25T03:06:55.861+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn7","msg":"client metadata","attr":{"remote":"127.0.0.1:47460","client":"conn7","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.861+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn8","msg":"client metadata","attr":{"remote":"127.0.0.1:47464","client":"conn8","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.862+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn7","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"a56fab5b-f9c9-47ab-a907-c260047bad5e"}},"namespace":"concelier.alias","collectionUUID":{"uuid":{"$uuid":"6a6a3cc5-2ba2-4756-bf3f-197fd1a306a0"}},"indexes":1,"firstIndex":{"name":"alias_scheme_value"}}}
{"t":{"$date":"2025-11-25T03:06:55.862+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn8","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"7df22170-a963-4a06-b173-cde909e8764c"}},"namespace":"concelier.affected","collectionUUID":{"uuid":{"$uuid":"ef930a9b-1097-41f9-9d77-2659520d64dc"}},"indexes":2,"firstIndex":{"name":"affected_platform_name"}}}
{"t":{"$date":"2025-11-25T03:06:55.871+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47476","connectionId":9,"connectionCount":9}}
{"t":{"$date":"2025-11-25T03:06:55.871+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47488","connectionId":10,"connectionCount":10}}
{"t":{"$date":"2025-11-25T03:06:55.871+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn9","msg":"client metadata","attr":{"remote":"127.0.0.1:47476","client":"conn9","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.872+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn10","msg":"client metadata","attr":{"remote":"127.0.0.1:47488","client":"conn10","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.872+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn9","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"e75019bf-293c-4d90-bfa3-90e20b305975"}},"namespace":"concelier.source_state","collectionUUID":{"uuid":{"$uuid":"9475ed88-bc52-4c6d-abcb-5dfaf2d5cf5b"}},"indexes":1,"firstIndex":{"name":"source_state_unique"}}}
{"t":{"$date":"2025-11-25T03:06:55.873+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn10","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"25b0858f-8e1d-43bc-afab-07712ea8e760"}},"namespace":"concelier.reference","collectionUUID":{"uuid":{"$uuid":"24d0213b-0677-42fa-b7ae-b0a19b36317d"}},"indexes":2,"firstIndex":{"name":"reference_url"}}}
{"t":{"$date":"2025-11-25T03:06:55.876+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47504","connectionId":11,"connectionCount":11}}
{"t":{"$date":"2025-11-25T03:06:55.876+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn11","msg":"client metadata","attr":{"remote":"127.0.0.1:47504","client":"conn11","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.878+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47506","connectionId":12,"connectionCount":12}}
{"t":{"$date":"2025-11-25T03:06:55.878+00:00"},"s":"I", "c":"COMMAND", "id":51806, "ctx":"conn11","msg":"CMD: dropIndexes","attr":{"namespace":"concelier.psirt_flags","uuid":{"uuid":{"$uuid":"d61fab06-e185-4905-a581-78d6188f9cbf"}},"indexes":"\"psirt_advisoryKey_unique\""}}
{"t":{"$date":"2025-11-25T03:06:55.878+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn12","msg":"client metadata","attr":{"remote":"127.0.0.1:47506","client":"conn12","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn4","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.jobs","index":"jobs_createdAt_desc","commitTimestamp":{"$timestamp":{"t":1764040015,"i":26}}}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn4","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.jobs","index":"jobs_kind_createdAt","commitTimestamp":{"$timestamp":{"t":1764040015,"i":26}}}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn4","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.jobs","index":"jobs_status_createdAt","commitTimestamp":{"$timestamp":{"t":1764040015,"i":26}}}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn4","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"1146b67d-4236-4bc9-bae4-3fa891517889"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47514","connectionId":13,"connectionCount":13}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn12","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"e231aaa5-d5f8-4c88-9860-fe69d60d65f5"}},"namespace":"concelier.advisory_statements","collectionUUID":{"uuid":{"$uuid":"46b5cd3a-fd22-47d2-81cc-2c756d9cfe62"}},"indexes":2,"firstIndex":{"name":"advisory_statements_vulnerability_asof_desc"}}}
{"t":{"$date":"2025-11-25T03:06:55.879+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn4","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"1146b67d-4236-4bc9-bae4-3fa891517889"}}}}
{"t":{"$date":"2025-11-25T03:06:55.880+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn13","msg":"client metadata","attr":{"remote":"127.0.0.1:47514","client":"conn13","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.881+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47524","connectionId":14,"connectionCount":14}}
{"t":{"$date":"2025-11-25T03:06:55.881+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn13","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"eba85195-e631-4fb2-a8ba-d155fcbe0411"}},"namespace":"concelier.advisory_conflicts","collectionUUID":{"uuid":{"$uuid":"e830a702-eb38-4e79-bd71-139b63066228"}},"indexes":2,"firstIndex":{"name":"advisory_conflicts_vulnerability_asof_desc"}}}
{"t":{"$date":"2025-11-25T03:06:55.881+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn14","msg":"client metadata","attr":{"remote":"127.0.0.1:47524","client":"conn14","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.882+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:47538","connectionId":15,"connectionCount":15}}
{"t":{"$date":"2025-11-25T03:06:55.882+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn14","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"054c0484-e72e-411f-bced-3f555ef0d361"}},"namespace":"concelier.advisory_observations","collectionUUID":{"uuid":{"$uuid":"2d30c6a9-a970-4507-9548-c93174011df9"}},"indexes":4,"firstIndex":{"name":"advisory_obs_tenant_upstream"}}}
{"t":{"$date":"2025-11-25T03:06:55.882+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn15","msg":"client metadata","attr":{"remote":"127.0.0.1:47538","client":"conn15","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:06:55.883+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn15","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"9756e330-8423-4878-bd4f-a3e1a8400472"}},"namespace":"concelier.source_change_history","collectionUUID":{"uuid":{"$uuid":"0c0938b6-7eb1-4e92-a8a8-5ed971581ddc"}},"indexes":3,"firstIndex":{"name":"history_source_advisory_capturedAt"}}}
{"t":{"$date":"2025-11-25T03:06:55.883+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn4","msg":"createCollection","attr":{"namespace":"concelier.documents.files","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"c6f88ce0-e49c-4b58-aa67-0a5021c6c7b1"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:55.928+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn4","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.documents.files","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040015,"i":31}}}}
{"t":{"$date":"2025-11-25T03:06:55.928+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn4","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.documents.files","index":"gridfs_files_expiresAt_ttl","commitTimestamp":{"$timestamp":{"t":1764040015,"i":31}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory","index":"advisory_key_unique","commitTimestamp":{"$timestamp":{"t":1764040015,"i":33}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory","index":"advisory_modified_desc","commitTimestamp":{"$timestamp":{"t":1764040015,"i":33}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory","index":"advisory_published_desc","commitTimestamp":{"$timestamp":{"t":1764040015,"i":33}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory","index":"advisory_normalizedVersions_pkg_scheme_type","commitTimestamp":{"$timestamp":{"t":1764040015,"i":33}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory","index":"advisory_normalizedVersions_value","commitTimestamp":{"$timestamp":{"t":1764040015,"i":33}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn3","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"b267ade3-39a8-4744-8ffe-e091e3a60a76"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:55.945+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn3","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"b267ade3-39a8-4744-8ffe-e091e3a60a76"}}}}
{"t":{"$date":"2025-11-25T03:06:55.968+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn6","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.dto","index":"dto_documentId","commitTimestamp":{"$timestamp":{"t":1764040015,"i":35}}}}
{"t":{"$date":"2025-11-25T03:06:55.968+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn6","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.dto","index":"dto_source_validated","commitTimestamp":{"$timestamp":{"t":1764040015,"i":35}}}}
{"t":{"$date":"2025-11-25T03:06:55.968+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn6","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"9ebafb07-90b6-47d0-9e2c-257bf2f104f7"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:55.968+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn6","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"9ebafb07-90b6-47d0-9e2c-257bf2f104f7"}}}}
{"t":{"$date":"2025-11-25T03:06:55.974+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn6","msg":"Slow query","attr":{"type":"command","ns":"concelier.dto","command":{"createIndexes":"dto","indexes":[{"key":{"documentId":1},"name":"dto_documentId"},{"key":{"sourceName":1,"validatedAt":-1},"name":"dto_source_validated"}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"0dce06ab-6c9e-44d5-a568-2c08aeae4f70"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":2},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":113}}
{"t":{"$date":"2025-11-25T03:06:55.983+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn9","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source_state","index":"source_state_unique","commitTimestamp":{"$timestamp":{"t":1764040015,"i":36}}}}
{"t":{"$date":"2025-11-25T03:06:55.983+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn9","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"e75019bf-293c-4d90-bfa3-90e20b305975"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:55.983+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn9","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"e75019bf-293c-4d90-bfa3-90e20b305975"}}}}
{"t":{"$date":"2025-11-25T03:06:55.988+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn9","msg":"Slow query","attr":{"type":"command","ns":"concelier.source_state","command":{"createIndexes":"source_state","indexes":[{"key":{"sourceName":1},"name":"source_state_unique","unique":true}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"8671be39-6be8-4a57-932e-fcddeacacfc5"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":1},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":116}}
{"t":{"$date":"2025-11-25T03:06:56.016+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn5","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.document","index":"document_source_uri_unique","commitTimestamp":{"$timestamp":{"t":1764040016,"i":1}}}}
{"t":{"$date":"2025-11-25T03:06:56.016+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn5","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.document","index":"document_fetchedAt_desc","commitTimestamp":{"$timestamp":{"t":1764040016,"i":1}}}}
{"t":{"$date":"2025-11-25T03:06:56.016+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn5","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.document","index":"document_expiresAt_ttl","commitTimestamp":{"$timestamp":{"t":1764040016,"i":1}}}}
{"t":{"$date":"2025-11-25T03:06:56.016+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn5","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"8634f626-1a30-4a22-93a9-65dc7b3e7493"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.016+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn5","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"8634f626-1a30-4a22-93a9-65dc7b3e7493"}}}}
{"t":{"$date":"2025-11-25T03:06:56.016+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn14","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"tenant":1,"upstream.upstream_id":1,"upstream.document_version":1},"name":"advisory_obs_tenant_upstream","unique":false,"v":2},{"key":{"tenant":1,"linkset.aliases":1},"name":"advisory_obs_tenant_aliases","v":2},{"key":{"tenant":1,"linkset.purls":1},"name":"advisory_obs_tenant_purls","v":2},{"key":{"tenant":1,"createdAt":-1},"name":"advisory_obs_tenant_createdAt","v":2}],"buildUUID":{"uuid":{"$uuid":"054c0484-e72e-411f-bced-3f555ef0d361"}},"collectionUUID":{"uuid":{"$uuid":"2d30c6a9-a970-4507-9548-c93174011df9"}}}}
{"t":{"$date":"2025-11-25T03:06:56.017+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn15","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"source":1,"advisoryKey":1,"capturedAt":-1},"name":"history_source_advisory_capturedAt","v":2},{"key":{"capturedAt":-1},"name":"history_capturedAt","v":2},{"key":{"documentId":1},"name":"history_documentId","v":2}],"buildUUID":{"uuid":{"$uuid":"9756e330-8423-4878-bd4f-a3e1a8400472"}},"collectionUUID":{"uuid":{"$uuid":"0c0938b6-7eb1-4e92-a8a8-5ed971581ddc"}}}}
{"t":{"$date":"2025-11-25T03:06:56.017+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn15","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"source":1,"advisoryKey":1,"capturedAt":-1},"name":"history_source_advisory_capturedAt","v":2},{"key":{"capturedAt":-1},"name":"history_capturedAt","v":2},{"key":{"documentId":1},"name":"history_documentId","v":2}],"buildUUID":{"uuid":{"$uuid":"9756e330-8423-4878-bd4f-a3e1a8400472"}},"collectionUUID":{"uuid":{"$uuid":"0c0938b6-7eb1-4e92-a8a8-5ed971581ddc"}}}}
{"t":{"$date":"2025-11-25T03:06:56.017+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn8","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"platform":1,"name":1},"name":"affected_platform_name","v":2},{"key":{"advisoryId":1},"name":"affected_advisoryId","v":2}],"buildUUID":{"uuid":{"$uuid":"7df22170-a963-4a06-b173-cde909e8764c"}},"collectionUUID":{"uuid":{"$uuid":"ef930a9b-1097-41f9-9d77-2659520d64dc"}}}}
{"t":{"$date":"2025-11-25T03:06:56.017+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn8","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"platform":1,"name":1},"name":"affected_platform_name","v":2},{"key":{"advisoryId":1},"name":"affected_advisoryId","v":2}],"buildUUID":{"uuid":{"$uuid":"7df22170-a963-4a06-b173-cde909e8764c"}},"collectionUUID":{"uuid":{"$uuid":"ef930a9b-1097-41f9-9d77-2659520d64dc"}}}}
{"t":{"$date":"2025-11-25T03:06:56.018+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn5","msg":"Slow query","attr":{"type":"command","ns":"concelier.document","command":{"createIndexes":"document","indexes":[{"key":{"sourceName":1,"uri":1},"name":"document_source_uri_unique","unique":true},{"key":{"fetchedAt":-1},"name":"document_fetchedAt_desc"},{"key":{"expiresAt":1},"name":"document_expiresAt_ttl","expireAfterSeconds":0.0,"partialFilterExpression":{"expiresAt":{"$exists":true}}}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"d31918ca-399a-4f47-8207-80777cac4b29"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":3},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":158}}
{"t":{"$date":"2025-11-25T03:06:56.020+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn11","msg":"Slow query","attr":{"type":"command","ns":"concelier.psirt_flags","command":{"dropIndexes":"psirt_flags","index":"psirt_advisoryKey_unique","writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"12d8a496-37e2-46f8-8e2f-a41a2f99ac09"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"ok":0,"errMsg":"index not found with name [psirt_advisoryKey_unique]","errName":"IndexNotFound","errCode":27,"reslen":266,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":2}},"ReplicationStateTransition":{"acquireCount":{"w":4}},"Global":{"acquireCount":{"r":2,"w":2}},"Database":{"acquireCount":{"w":2}},"Collection":{"acquireCount":{"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":2,"timeAcquiringMicros":1},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":141}}
{"t":{"$date":"2025-11-25T03:06:56.031+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn11","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"59b6cebf-aee3-46b7-814a-856404eb982d"}},"namespace":"concelier.psirt_flags","collectionUUID":{"uuid":{"$uuid":"d61fab06-e185-4905-a581-78d6188f9cbf"}},"indexes":1,"firstIndex":{"name":"psirt_vendor"}}}
{"t":{"$date":"2025-11-25T03:06:56.035+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn10","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.reference","index":"reference_url","commitTimestamp":{"$timestamp":{"t":1764040016,"i":3}}}}
{"t":{"$date":"2025-11-25T03:06:56.035+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn10","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.reference","index":"reference_advisoryId","commitTimestamp":{"$timestamp":{"t":1764040016,"i":3}}}}
{"t":{"$date":"2025-11-25T03:06:56.035+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn10","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"25b0858f-8e1d-43bc-afab-07712ea8e760"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.035+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn10","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"25b0858f-8e1d-43bc-afab-07712ea8e760"}}}}
{"t":{"$date":"2025-11-25T03:06:56.037+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn10","msg":"Slow query","attr":{"type":"command","ns":"concelier.reference","command":{"createIndexes":"reference","indexes":[{"key":{"url":1},"name":"reference_url"},{"key":{"advisoryId":1},"name":"reference_advisoryId"}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"e8db91b1-ad7d-4cb3-a86b-47d5b309fc80"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":2},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":164}}
{"t":{"$date":"2025-11-25T03:06:56.051+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn13","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_conflicts","index":"advisory_conflicts_vulnerability_asof_desc","commitTimestamp":{"$timestamp":{"t":1764040016,"i":5}}}}
{"t":{"$date":"2025-11-25T03:06:56.051+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn13","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_conflicts","index":"advisory_conflicts_conflictHash_unique","commitTimestamp":{"$timestamp":{"t":1764040016,"i":5}}}}
{"t":{"$date":"2025-11-25T03:06:56.051+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn13","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"eba85195-e631-4fb2-a8ba-d155fcbe0411"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.051+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn13","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"eba85195-e631-4fb2-a8ba-d155fcbe0411"}}}}
{"t":{"$date":"2025-11-25T03:06:56.053+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn13","msg":"Slow query","attr":{"type":"command","ns":"concelier.advisory_conflicts","command":{"createIndexes":"advisory_conflicts","indexes":[{"key":{"vulnerabilityKey":1,"asOf":-1},"name":"advisory_conflicts_vulnerability_asof_desc"},{"key":{"conflictHash":1},"name":"advisory_conflicts_conflictHash_unique","unique":true}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"92e1dc41-2888-47f4-a1dc-abd349a494a4"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":2},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":172}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn7","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.alias","index":"alias_scheme_value","commitTimestamp":{"$timestamp":{"t":1764040016,"i":6}}}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn7","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"a56fab5b-f9c9-47ab-a907-c260047bad5e"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn7","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"a56fab5b-f9c9-47ab-a907-c260047bad5e"}}}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn14","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"tenant":1,"upstream.upstream_id":1,"upstream.document_version":1},"name":"advisory_obs_tenant_upstream","unique":false,"v":2},{"key":{"tenant":1,"linkset.aliases":1},"name":"advisory_obs_tenant_aliases","v":2},{"key":{"tenant":1,"linkset.purls":1},"name":"advisory_obs_tenant_purls","v":2},{"key":{"tenant":1,"createdAt":-1},"name":"advisory_obs_tenant_createdAt","v":2}],"buildUUID":{"uuid":{"$uuid":"054c0484-e72e-411f-bced-3f555ef0d361"}},"collectionUUID":{"uuid":{"$uuid":"2d30c6a9-a970-4507-9548-c93174011df9"}}}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn14","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"tenant":1,"upstream.upstream_id":1,"upstream.document_version":1},"name":"advisory_obs_tenant_upstream","unique":false,"v":2},{"key":{"tenant":1,"linkset.aliases":1},"name":"advisory_obs_tenant_aliases","v":2},{"key":{"tenant":1,"linkset.purls":1},"name":"advisory_obs_tenant_purls","v":2},{"key":{"tenant":1,"createdAt":-1},"name":"advisory_obs_tenant_createdAt","v":2}],"buildUUID":{"uuid":{"$uuid":"054c0484-e72e-411f-bced-3f555ef0d361"}},"collectionUUID":{"uuid":{"$uuid":"2d30c6a9-a970-4507-9548-c93174011df9"}}}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn11","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"vendor":1},"name":"psirt_vendor","v":2}],"buildUUID":{"uuid":{"$uuid":"59b6cebf-aee3-46b7-814a-856404eb982d"}},"collectionUUID":{"uuid":{"$uuid":"d61fab06-e185-4905-a581-78d6188f9cbf"}}}}
{"t":{"$date":"2025-11-25T03:06:56.059+00:00"},"s":"I", "c":"STORAGE", "id":4715500, "ctx":"conn11","msg":"Too many index builds running simultaneously, waiting until the number of active index builds is below the threshold","attr":{"numActiveIndexBuilds":3,"maxNumActiveUserIndexBuilds":3,"indexSpecs":[{"key":{"vendor":1},"name":"psirt_vendor","v":2}],"buildUUID":{"uuid":{"$uuid":"59b6cebf-aee3-46b7-814a-856404eb982d"}},"collectionUUID":{"uuid":{"$uuid":"d61fab06-e185-4905-a581-78d6188f9cbf"}}}}
{"t":{"$date":"2025-11-25T03:06:56.062+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn7","msg":"Slow query","attr":{"type":"command","ns":"concelier.alias","command":{"createIndexes":"alias","indexes":[{"key":{"scheme":1,"value":1},"name":"alias_scheme_value","unique":false}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"9451e45a-666e-4afb-b7dc-24139346c68a"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":1},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":199}}
{"t":{"$date":"2025-11-25T03:06:56.076+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn8","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.affected","index":"affected_platform_name","commitTimestamp":{"$timestamp":{"t":1764040016,"i":8}}}}
{"t":{"$date":"2025-11-25T03:06:56.076+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn8","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.affected","index":"affected_advisoryId","commitTimestamp":{"$timestamp":{"t":1764040016,"i":8}}}}
{"t":{"$date":"2025-11-25T03:06:56.100+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn15","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source_change_history","index":"history_source_advisory_capturedAt","commitTimestamp":{"$timestamp":{"t":1764040016,"i":11}}}}
{"t":{"$date":"2025-11-25T03:06:56.100+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn15","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source_change_history","index":"history_capturedAt","commitTimestamp":{"$timestamp":{"t":1764040016,"i":11}}}}
{"t":{"$date":"2025-11-25T03:06:56.100+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn15","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.source_change_history","index":"history_documentId","commitTimestamp":{"$timestamp":{"t":1764040016,"i":11}}}}
{"t":{"$date":"2025-11-25T03:06:56.101+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn15","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"9756e330-8423-4878-bd4f-a3e1a8400472"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.101+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn15","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"9756e330-8423-4878-bd4f-a3e1a8400472"}}}}
{"t":{"$date":"2025-11-25T03:06:56.103+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn15","msg":"Slow query","attr":{"type":"command","ns":"concelier.source_change_history","command":{"createIndexes":"source_change_history","indexes":[{"key":{"source":1,"advisoryKey":1,"capturedAt":-1},"name":"history_source_advisory_capturedAt"},{"key":{"capturedAt":-1},"name":"history_capturedAt"},{"key":{"documentId":1},"name":"history_documentId"}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"8b32a551-8036-4a89-ab2e-c86d08aa9663"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":27}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":3},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":220}}
{"t":{"$date":"2025-11-25T03:06:56.132+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn14","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observations","index":"advisory_obs_tenant_upstream","commitTimestamp":{"$timestamp":{"t":1764040016,"i":15}}}}
{"t":{"$date":"2025-11-25T03:06:56.132+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn14","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observations","index":"advisory_obs_tenant_aliases","commitTimestamp":{"$timestamp":{"t":1764040016,"i":15}}}}
{"t":{"$date":"2025-11-25T03:06:56.132+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn14","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observations","index":"advisory_obs_tenant_purls","commitTimestamp":{"$timestamp":{"t":1764040016,"i":15}}}}
{"t":{"$date":"2025-11-25T03:06:56.132+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn14","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observations","index":"advisory_obs_tenant_createdAt","commitTimestamp":{"$timestamp":{"t":1764040016,"i":15}}}}
{"t":{"$date":"2025-11-25T03:06:56.132+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn14","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"054c0484-e72e-411f-bced-3f555ef0d361"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.132+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn14","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"054c0484-e72e-411f-bced-3f555ef0d361"}}}}
{"t":{"$date":"2025-11-25T03:06:56.137+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn14","msg":"Slow query","attr":{"type":"command","ns":"concelier.advisory_observations","command":{"createIndexes":"advisory_observations","indexes":[{"key":{"tenant":1,"upstream.upstream_id":1,"upstream.document_version":1},"name":"advisory_obs_tenant_upstream","unique":false},{"key":{"tenant":1,"linkset.aliases":1},"name":"advisory_obs_tenant_aliases"},{"key":{"tenant":1,"linkset.purls":1},"name":"advisory_obs_tenant_purls"},{"key":{"tenant":1,"createdAt":-1},"name":"advisory_obs_tenant_createdAt"}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"959fef49-dc3d-44bf-824f-522cb94dcab9"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":1},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":255}}
{"t":{"$date":"2025-11-25T03:06:56.142+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn11","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.psirt_flags","index":"psirt_vendor","commitTimestamp":{"$timestamp":{"t":1764040016,"i":16}}}}
{"t":{"$date":"2025-11-25T03:06:56.142+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn11","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"59b6cebf-aee3-46b7-814a-856404eb982d"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.142+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn11","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"59b6cebf-aee3-46b7-814a-856404eb982d"}}}}
{"t":{"$date":"2025-11-25T03:06:56.145+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn11","msg":"Slow query","attr":{"type":"command","ns":"concelier.psirt_flags","command":{"createIndexes":"psirt_flags","indexes":[{"key":{"vendor":1},"name":"psirt_vendor"}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"12d8a496-37e2-46f8-8e2f-a41a2f99ac09"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040016,"i":2}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":1},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":113}}
{"t":{"$date":"2025-11-25T03:06:56.158+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_statements","index":"advisory_statements_vulnerability_asof_desc","commitTimestamp":{"$timestamp":{"t":1764040016,"i":18}}}}
{"t":{"$date":"2025-11-25T03:06:56.158+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_statements","index":"advisory_statements_statementHash_unique","commitTimestamp":{"$timestamp":{"t":1764040016,"i":18}}}}
{"t":{"$date":"2025-11-25T03:06:56.158+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn12","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"e231aaa5-d5f8-4c88-9860-fe69d60d65f5"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.158+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn8","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"7df22170-a963-4a06-b173-cde909e8764c"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.158+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn12","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"e231aaa5-d5f8-4c88-9860-fe69d60d65f5"}}}}
{"t":{"$date":"2025-11-25T03:06:56.158+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn8","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"7df22170-a963-4a06-b173-cde909e8764c"}}}}
{"t":{"$date":"2025-11-25T03:06:56.160+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn8","msg":"Slow query","attr":{"type":"command","ns":"concelier.affected","command":{"createIndexes":"affected","indexes":[{"key":{"platform":1,"name":1},"name":"affected_platform_name"},{"key":{"advisoryId":1},"name":"affected_advisoryId"}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"db62eb74-b9d0-420f-b476-36bfe600a00e"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":3},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":297}}
{"t":{"$date":"2025-11-25T03:06:56.160+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn12","msg":"Slow query","attr":{"type":"command","ns":"concelier.advisory_statements","command":{"createIndexes":"advisory_statements","indexes":[{"key":{"vulnerabilityKey":1,"asOf":-1},"name":"advisory_statements_vulnerability_asof_desc"},{"key":{"statementHash":1},"name":"advisory_statements_statementHash_unique","unique":true}],"writeConcern":{"w":"majority","wtimeout":30000.0},"$db":"concelier","lsid":{"id":{"$uuid":"e30476f3-96d5-4a1b-b952-b9c3c8c48f05"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1764040015,"i":24}},"signature":{"hash":{"$binary":{"base64":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","subType":"0"}},"keyId":0}}},"numYields":0,"reslen":271,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":3}},"ReplicationStateTransition":{"acquireCount":{"w":6}},"Global":{"acquireCount":{"r":2,"w":4}},"Database":{"acquireCount":{"w":3}},"Collection":{"acquireCount":{"r":1,"w":1,"W":1}},"Mutex":{"acquireCount":{"r":3}}},"flowControl":{"acquireCount":3,"timeAcquiringMicros":2},"writeConcern":{"w":"majority","wtimeout":30000,"provenance":"clientSupplied"},"storage":{},"protocol":"op_msg","durationMillis":281}}
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20241005_document_expiry_indexes: Ensure document.expiresAt index matches configured retention
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20241005_document_expiry_indexes applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20241005_gridfs_expiry_indexes: Ensure GridFS metadata.expiresAt TTL index reflects retention settings
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20241005_gridfs_expiry_indexes applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 2025-11-07-advisory-canonical-key: Populate advisory_key and links for advisory_raw documents.
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 2025-11-07-advisory-canonical-key applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251011-semver-style-backfill: Populate advisory.normalizedVersions for existing documents when SemVer style storage is enabled.
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251011-semver-style-backfill applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251019_advisory_event_collections: Ensure advisory_statements and advisory_conflicts indexes exist for event log storage.
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251019_advisory_event_collections applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251028_advisory_raw_idempotency_index: Ensure advisory_raw collection enforces idempotency via unique compound index.
{"t":{"$date":"2025-11-25T03:06:56.373+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn12","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"d0d7de72-350c-4703-88fe-4604a6c0d70c"}},"namespace":"concelier.advisory_raw","collectionUUID":{"uuid":{"$uuid":"70542ec2-832b-4f93-8c96-4ca814f1fbbc"}},"indexes":1,"firstIndex":{"name":"advisory_raw_idempotency"}}}
{"t":{"$date":"2025-11-25T03:06:56.381+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_raw","index":"advisory_raw_idempotency","commitTimestamp":{"$timestamp":{"t":1764040016,"i":24}}}}
{"t":{"$date":"2025-11-25T03:06:56.381+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn12","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"d0d7de72-350c-4703-88fe-4604a6c0d70c"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.381+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn12","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"d0d7de72-350c-4703-88fe-4604a6c0d70c"}}}}
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251028_advisory_raw_idempotency_index applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251028_advisory_raw_validator: Ensure advisory_raw collection enforces Aggregation-Only Contract schema
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251028_advisory_raw_validator applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251028_advisory_supersedes_backfill: Backfill advisory_raw supersedes chains and replace legacy advisory collection with read-only view.
{"t":{"$date":"2025-11-25T03:06:56.422+00:00"},"s":"I", "c":"COMMAND", "id":20400, "ctx":"conn12","msg":"renameCollectionForCommand","attr":{"sourceNamespace":"concelier.advisory","targetNamespace":"concelier.advisory_backup_20251028","dropTarget":"no"}}
{"t":{"$date":"2025-11-25T03:06:56.422+00:00"},"s":"I", "c":"STORAGE", "id":20319, "ctx":"conn12","msg":"renameCollection","attr":{"uuid":{"uuid":{"$uuid":"c2e4124c-bf80-4e3c-9272-cea8f40106f5"}},"fromName":"concelier.advisory","toName":"concelier.advisory_backup_20251028"}}
{"t":{"$date":"2025-11-25T03:06:56.427+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn12","msg":"createCollection","attr":{"namespace":"concelier.system.views","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"11aeedd7-8f4c-4bf6-a15f-508c507370da"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:56.445+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.system.views","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040016,"i":29}}}}
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251028_advisory_supersedes_backfill applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251104_advisory_observations_raw_linkset: Populate rawLinkset field for advisory observations using stored advisory_raw documents.
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251104_advisory_observations_raw_linkset applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251117_advisory_linksets_tenant_lower: Lowercase tenant ids in advisory_linksets to match query filters.
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251117_advisory_linksets_tenant_lower applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251120_advisory_observation_events: Ensure advisory_observation_events collection and indexes exist for observation event fan-out.
{"t":{"$date":"2025-11-25T03:06:56.489+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn12","msg":"createCollection","attr":{"namespace":"concelier.advisory_observation_events","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"2ee210ff-d50f-4a43-9d2b-8160e01daa2f"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:56.524+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observation_events","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040016,"i":36}}}}
{"t":{"$date":"2025-11-25T03:06:56.525+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observation_events","index":"advisory_observation_events_tenant_ingested_desc","commitTimestamp":{"$timestamp":{"t":1764040016,"i":36}}}}
{"t":{"$date":"2025-11-25T03:06:56.525+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.advisory_observation_events","index":"advisory_observation_events_hash_unique","commitTimestamp":{"$timestamp":{"t":1764040016,"i":36}}}}
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251120_advisory_observation_events applied
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Applying Mongo migration 20251122_orchestrator_registry_commands: Ensure orchestrator registry, commands, and heartbeats collections exist with indexes
{"t":{"$date":"2025-11-25T03:06:56.535+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn12","msg":"createCollection","attr":{"namespace":"concelier.orchestrator_registry","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"6649d503-b817-4ea5-88ce-a93b0536995d"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:56.551+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_registry","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040016,"i":38}}}}
{"t":{"$date":"2025-11-25T03:06:56.554+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn12","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"0f95b012-1a7d-415e-9a84-9839c759b37e"}},"namespace":"concelier.orchestrator_registry","collectionUUID":{"uuid":{"$uuid":"6649d503-b817-4ea5-88ce-a93b0536995d"}},"indexes":2,"firstIndex":{"name":"orch_registry_tenant_connector"}}}
{"t":{"$date":"2025-11-25T03:06:56.577+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_registry","index":"orch_registry_tenant_connector","commitTimestamp":{"$timestamp":{"t":1764040016,"i":40}}}}
{"t":{"$date":"2025-11-25T03:06:56.577+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_registry","index":"orch_registry_source","commitTimestamp":{"$timestamp":{"t":1764040016,"i":40}}}}
{"t":{"$date":"2025-11-25T03:06:56.577+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn12","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"0f95b012-1a7d-415e-9a84-9839c759b37e"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.577+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn12","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"0f95b012-1a7d-415e-9a84-9839c759b37e"}}}}
{"t":{"$date":"2025-11-25T03:06:56.581+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn12","msg":"createCollection","attr":{"namespace":"concelier.orchestrator_commands","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"f1a79279-2004-4cfd-8ae9-cb752e102dff"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:56.601+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_commands","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040016,"i":41}}}}
{"t":{"$date":"2025-11-25T03:06:56.604+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn12","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"edb14da3-273d-4518-a0ff-db0a490facc4"}},"namespace":"concelier.orchestrator_commands","collectionUUID":{"uuid":{"$uuid":"f1a79279-2004-4cfd-8ae9-cb752e102dff"}},"indexes":2,"firstIndex":{"name":"orch_cmd_tenant_connector_run_seq"}}}
{"t":{"$date":"2025-11-25T03:06:56.623+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_commands","index":"orch_cmd_tenant_connector_run_seq","commitTimestamp":{"$timestamp":{"t":1764040016,"i":43}}}}
{"t":{"$date":"2025-11-25T03:06:56.623+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_commands","index":"orch_cmd_expiresAt_ttl","commitTimestamp":{"$timestamp":{"t":1764040016,"i":43}}}}
{"t":{"$date":"2025-11-25T03:06:56.623+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn12","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"edb14da3-273d-4518-a0ff-db0a490facc4"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.623+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn12","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"edb14da3-273d-4518-a0ff-db0a490facc4"}}}}
{"t":{"$date":"2025-11-25T03:06:56.627+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn12","msg":"createCollection","attr":{"namespace":"concelier.orchestrator_heartbeats","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"52b32b90-719b-4668-8aab-021f90ae99f1"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:06:56.644+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_heartbeats","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764040016,"i":44}}}}
{"t":{"$date":"2025-11-25T03:06:56.648+00:00"},"s":"I", "c":"INDEX", "id":20438, "ctx":"conn12","msg":"Index build: registering","attr":{"buildUUID":{"uuid":{"$uuid":"f262f49e-88f3-4b71-ade5-24ba982d5f71"}},"namespace":"concelier.orchestrator_heartbeats","collectionUUID":{"uuid":{"$uuid":"52b32b90-719b-4668-8aab-021f90ae99f1"}},"indexes":2,"firstIndex":{"name":"orch_hb_tenant_connector_run_seq"}}}
{"t":{"$date":"2025-11-25T03:06:56.664+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_heartbeats","index":"orch_hb_tenant_connector_run_seq","commitTimestamp":{"$timestamp":{"t":1764040016,"i":46}}}}
{"t":{"$date":"2025-11-25T03:06:56.664+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn12","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier.orchestrator_heartbeats","index":"orch_hb_timestamp_desc","commitTimestamp":{"$timestamp":{"t":1764040016,"i":46}}}}
{"t":{"$date":"2025-11-25T03:06:56.664+00:00"},"s":"I", "c":"INDEX", "id":20440, "ctx":"conn12","msg":"Index build: waiting for index build to complete","attr":{"buildUUID":{"uuid":{"$uuid":"f262f49e-88f3-4b71-ade5-24ba982d5f71"}},"deadline":{"$date":{"$numberLong":"9223372036854775807"}}}}
{"t":{"$date":"2025-11-25T03:06:56.664+00:00"},"s":"I", "c":"INDEX", "id":20447, "ctx":"conn12","msg":"Index build: completed","attr":{"buildUUID":{"uuid":{"$uuid":"f262f49e-88f3-4b71-ade5-24ba982d5f71"}}}}
info: StellaOps.Concelier.Storage.Mongo.Migrations.MongoMigrationRunner[0]
Mongo migration 20251122_orchestrator_registry_commands applied
info: StellaOps.Concelier.Storage.Mongo.MongoBootstrapper[0]
Mongo bootstrapper completed
info: MongoBootstrapper[0]
Mongo bootstrap completed in 1453.7631 ms
info: StellaOps.Concelier.Core.Jobs.JobSchedulerHostedService[0]
No cron-based jobs registered; scheduler idle.
info: StellaOps.Concelier.Storage.Mongo.Observations.AdvisoryObservationTransportWorker[0]
Observation transport worker disabled.
info: StellaOps.Concelier.Storage.Mongo.Observations.AdvisoryObservationTransportWorker[0]
Observation transport worker disabled.
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /mnt/e/dev/git.stella-ops.org/src/Concelier/StellaOps.Concelier.WebService
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost/health - - -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'HTTP: GET /health'
info: Microsoft.AspNetCore.Http.Result.ContentResult[2]
Write content with HTTP Response ContentType of application/json; charset=utf-8
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'HTTP: GET /health'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET http://localhost/health - 200 291 application/json;+charset=utf-8 151.1386ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost/ready - - -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'HTTP: GET /ready'
info: Microsoft.AspNetCore.Http.Result.ContentResult[2]
Write content with HTTP Response ContentType of application/json; charset=utf-8
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'HTTP: GET /ready'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET http://localhost/ready - 200 198 application/json;+charset=utf-8 12.4201ms
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
[xUnit.net 00:00:36.48] Finished: StellaOps.Concelier.WebService.Tests
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:06:57.6059023+00:00">
<Text>Data collector 'Blame' message: All tests finished running, Sequence file will not be generated.</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="2a53c3de-54ca-4f07-8702-1a9c0210c1e2" name="@DESKTOP-7GHGC2M 2025-11-25 03:08:54" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2025-11-25T03:08:54.4332618+00:00" queuing="2025-11-25T03:08:54.4332619+00:00" start="2025-11-25T03:08:52.3185390+00:00" finish="2025-11-25T03:08:54.4418259+00:00" />
<TestSettings name="default" id="3ce5bdb0-5cad-4ee1-a2b9-696735144c3d">
<Deployment runDeploymentRoot="_DESKTOP-7GHGC2M_2025-11-25_03_08_54" />
</TestSettings>
<Results>
<UnitTestResult executionId="c67ce2d6-5c72-4327-ac7f-be88c6c0ecc3" testId="11684acb-bbea-2e6b-ce0d-1cc21a7bc201" testName="StellaOps.Excititor.WebService.Tests.AirgapImportEndpointTests.Import_accepts_valid_payload" computerName="DESKTOP-7GHGC2M" duration="00:00:00.0019351" startTime="2025-11-25T03:08:54.2414474+00:00" endTime="2025-11-25T03:08:54.2414476+00:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="c67ce2d6-5c72-4327-ac7f-be88c6c0ecc3" />
<UnitTestResult executionId="21c16366-1bea-4eec-af4e-a15f4fdeb918" testId="da5d5507-70fd-de85-95fe-e405d157cf98" testName="StellaOps.Excititor.WebService.Tests.AirgapImportEndpointTests.Import_returns_bad_request_when_signature_missing" computerName="DESKTOP-7GHGC2M" duration="00:00:00.2374169" startTime="2025-11-25T03:08:54.2172539+00:00" endTime="2025-11-25T03:08:54.2173027+00:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="21c16366-1bea-4eec-af4e-a15f4fdeb918" />
</Results>
<TestDefinitions>
<UnitTest name="StellaOps.Excititor.WebService.Tests.AirgapImportEndpointTests.Import_returns_bad_request_when_signature_missing" storage="/mnt/e/dev/git.stella-ops.org/src/excititor/__tests/stellaops.excititor.webservice.tests/bin/debug/net10.0/stellaops.excititor.webservice.tests.dll" id="da5d5507-70fd-de85-95fe-e405d157cf98">
<Execution id="21c16366-1bea-4eec-af4e-a15f4fdeb918" />
<TestMethod codeBase="/mnt/e/dev/git.stella-ops.org/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/bin/Debug/net10.0/StellaOps.Excititor.WebService.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="StellaOps.Excititor.WebService.Tests.AirgapImportEndpointTests" name="Import_returns_bad_request_when_signature_missing" />
</UnitTest>
<UnitTest name="StellaOps.Excititor.WebService.Tests.AirgapImportEndpointTests.Import_accepts_valid_payload" storage="/mnt/e/dev/git.stella-ops.org/src/excititor/__tests/stellaops.excititor.webservice.tests/bin/debug/net10.0/stellaops.excititor.webservice.tests.dll" id="11684acb-bbea-2e6b-ce0d-1cc21a7bc201">
<Execution id="c67ce2d6-5c72-4327-ac7f-be88c6c0ecc3" />
<TestMethod codeBase="/mnt/e/dev/git.stella-ops.org/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/bin/Debug/net10.0/StellaOps.Excititor.WebService.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="StellaOps.Excititor.WebService.Tests.AirgapImportEndpointTests" name="Import_accepts_valid_payload" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="11684acb-bbea-2e6b-ce0d-1cc21a7bc201" executionId="c67ce2d6-5c72-4327-ac7f-be88c6c0ecc3" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="da5d5507-70fd-de85-95fe-e405d157cf98" executionId="21c16366-1bea-4eec-af4e-a15f4fdeb918" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="2" executed="2" passed="2" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0-rc.2.25502.107)
[xUnit.net 00:00:00.23] Discovering: StellaOps.Excititor.WebService.Tests
[xUnit.net 00:00:00.29] Discovered: StellaOps.Excititor.WebService.Tests
[xUnit.net 00:00:00.30] Starting: StellaOps.Excititor.WebService.Tests
[xUnit.net 00:00:00.64] Finished: StellaOps.Excititor.WebService.Tests
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:08:54.3453289+00:00">
<Text>Data collector 'Blame' message: All tests finished running, Sequence file will not be generated.</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>

View File

@@ -0,0 +1,266 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="2191e108-655c-4a21-ab29-eea8b2c62925" name="@DESKTOP-7GHGC2M 2025-11-25 03:47:01" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2025-11-25T03:47:01.2509867+00:00" queuing="2025-11-25T03:47:01.2509868+00:00" start="2025-11-25T03:46:56.1632495+00:00" finish="2025-11-25T03:47:01.2619302+00:00" />
<TestSettings name="default" id="5458548f-804a-4728-9c0b-13d49432b590">
<Deployment runDeploymentRoot="_DESKTOP-7GHGC2M_2025-11-25_03_47_01" />
</TestSettings>
<Results>
<UnitTestResult executionId="00b97e97-a6ec-443b-83a2-c418eb8004fa" testId="dfb58a03-909f-e5d6-ff47-08270c539edb" testName="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests.StartAndFailRunHonorsStateTransitions" computerName="DESKTOP-7GHGC2M" duration="00:00:00.0323638" startTime="2025-11-25T03:47:01.0443268+00:00" endTime="2025-11-25T03:47:01.0443270+00:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="00b97e97-a6ec-443b-83a2-c418eb8004fa" />
<UnitTestResult executionId="0cc84148-daf8-41ca-bfdb-ec94bd707d16" testId="962018a8-8d58-240a-0493-82d9ad895b0e" testName="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests.CompletingUnknownRunReturnsNull" computerName="DESKTOP-7GHGC2M" duration="00:00:00.0137926" startTime="2025-11-25T03:47:01.0242606+00:00" endTime="2025-11-25T03:47:01.0242607+00:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="0cc84148-daf8-41ca-bfdb-ec94bd707d16" />
<UnitTestResult executionId="192c510e-8cf8-4753-99cf-2712462da19b" testId="58d93f82-a4de-397b-e8b8-0c3f78ca3507" testName="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests.CreateStartCompleteLifecycle" computerName="DESKTOP-7GHGC2M" duration="00:00:00.6394627" startTime="2025-11-25T03:47:00.9948339+00:00" endTime="2025-11-25T03:47:00.9949060+00:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="192c510e-8cf8-4753-99cf-2712462da19b" />
</Results>
<TestDefinitions>
<UnitTest name="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests.CompletingUnknownRunReturnsNull" storage="/mnt/e/dev/git.stella-ops.org/src/concelier/__tests/stellaops.concelier.storage.mongo.tests/bin/debug/net10.0/stellaops.concelier.storage.mongo.tests.dll" id="962018a8-8d58-240a-0493-82d9ad895b0e">
<Execution id="0cc84148-daf8-41ca-bfdb-ec94bd707d16" />
<TestMethod codeBase="/mnt/e/dev/git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Storage.Mongo.Tests/bin/Debug/net10.0/StellaOps.Concelier.Storage.Mongo.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests" name="CompletingUnknownRunReturnsNull" />
</UnitTest>
<UnitTest name="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests.CreateStartCompleteLifecycle" storage="/mnt/e/dev/git.stella-ops.org/src/concelier/__tests/stellaops.concelier.storage.mongo.tests/bin/debug/net10.0/stellaops.concelier.storage.mongo.tests.dll" id="58d93f82-a4de-397b-e8b8-0c3f78ca3507">
<Execution id="192c510e-8cf8-4753-99cf-2712462da19b" />
<TestMethod codeBase="/mnt/e/dev/git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Storage.Mongo.Tests/bin/Debug/net10.0/StellaOps.Concelier.Storage.Mongo.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests" name="CreateStartCompleteLifecycle" />
</UnitTest>
<UnitTest name="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests.StartAndFailRunHonorsStateTransitions" storage="/mnt/e/dev/git.stella-ops.org/src/concelier/__tests/stellaops.concelier.storage.mongo.tests/bin/debug/net10.0/stellaops.concelier.storage.mongo.tests.dll" id="dfb58a03-909f-e5d6-ff47-08270c539edb">
<Execution id="00b97e97-a6ec-443b-83a2-c418eb8004fa" />
<TestMethod codeBase="/mnt/e/dev/git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Storage.Mongo.Tests/bin/Debug/net10.0/StellaOps.Concelier.Storage.Mongo.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="StellaOps.Concelier.Storage.Mongo.Tests.MongoJobStoreTests" name="StartAndFailRunHonorsStateTransitions" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="dfb58a03-909f-e5d6-ff47-08270c539edb" executionId="00b97e97-a6ec-443b-83a2-c418eb8004fa" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="962018a8-8d58-240a-0493-82d9ad895b0e" executionId="0cc84148-daf8-41ca-bfdb-ec94bd707d16" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="58d93f82-a4de-397b-e8b8-0c3f78ca3507" executionId="192c510e-8cf8-4753-99cf-2712462da19b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="3" executed="3" passed="3" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0-rc.2.25502.107)
[xUnit.net 00:00:00.24] Discovering: StellaOps.Concelier.Storage.Mongo.Tests
[xUnit.net 00:00:00.30] Discovered: StellaOps.Concelier.Storage.Mongo.Tests
[xUnit.net 00:00:00.31] Starting: StellaOps.Concelier.Storage.Mongo.Tests
{"t":{"$date":"2025-11-25T03:46:58.044+00:00"},"s":"I", "c":"CONTROL", "id":23285, "ctx":"main","msg":"Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'"}
{"t":{"$date":"2025-11-25T03:46:58.047+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"}
{"t":{"$date":"2025-11-25T03:46:58.047+00:00"},"s":"I", "c":"NETWORK", "id":4648601, "ctx":"main","msg":"Implicit TCP FastOpen unavailable. If TCP FastOpen is required, set tcpFastOpenServer, tcpFastOpenClient, and tcpFastOpenQueueSize."}
{"t":{"$date":"2025-11-25T03:46:58.047+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"}
{"t":{"$date":"2025-11-25T03:46:58.048+00:00"},"s":"I", "c":"STORAGE", "id":4615611, "ctx":"initandlisten","msg":"MongoDB starting","attr":{"pid":149307,"port":34177,"dbPath":"/tmp/52ypckao.mi24085d8d097344d1d89ce_34177","architecture":"64-bit","host":"DESKTOP-7GHGC2M"}}
{"t":{"$date":"2025-11-25T03:46:58.048+00:00"},"s":"I", "c":"CONTROL", "id":23403, "ctx":"initandlisten","msg":"Build Info","attr":{"buildInfo":{"version":"4.4.4","gitVersion":"8db30a63db1a9d84bdcad0c83369623f708e0397","openSSLVersion":"OpenSSL 1.1.1w 11 Sep 2023","modules":[],"allocator":"tcmalloc","environment":{"distmod":"ubuntu2004","distarch":"x86_64","target_arch":"x86_64"}}}}
{"t":{"$date":"2025-11-25T03:46:58.048+00:00"},"s":"I", "c":"CONTROL", "id":51765, "ctx":"initandlisten","msg":"Operating System","attr":{"os":{"name":"Ubuntu","version":"24.04"}}}
{"t":{"$date":"2025-11-25T03:46:58.048+00:00"},"s":"I", "c":"CONTROL", "id":21951, "ctx":"initandlisten","msg":"Options set by command line","attr":{"options":{"net":{"bindIp":"127.0.0.1","port":34177},"replication":{"replSet":"singleNodeReplSet"},"storage":{"dbPath":"/tmp/52ypckao.mi24085d8d097344d1d89ce_34177"}}}}
{"t":{"$date":"2025-11-25T03:46:58.050+00:00"},"s":"I", "c":"STORAGE", "id":22297, "ctx":"initandlisten","msg":"Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem","tags":["startupWarnings"]}
{"t":{"$date":"2025-11-25T03:46:58.051+00:00"},"s":"I", "c":"STORAGE", "id":22315, "ctx":"initandlisten","msg":"Opening WiredTiger","attr":{"config":"create,cache_size=7485M,session_max=33000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000,close_scan_interval=10,close_handle_minimum=250),statistics_log=(wait=0),verbose=[recovery_progress,checkpoint_progress,compact_progress],"}}
{"t":{"$date":"2025-11-25T03:46:58.505+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":"[1764042418:505803][149307:0x7c5deb73fcc0], txn-recover: [WT_VERB_RECOVERY | WT_VERB_RECOVERY_PROGRESS] Set global recovery timestamp: (0, 0)"}}
{"t":{"$date":"2025-11-25T03:46:58.505+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":"[1764042418:505867][149307:0x7c5deb73fcc0], txn-recover: [WT_VERB_RECOVERY | WT_VERB_RECOVERY_PROGRESS] Set global oldest timestamp: (0, 0)"}}
{"t":{"$date":"2025-11-25T03:46:58.518+00:00"},"s":"I", "c":"STORAGE", "id":4795906, "ctx":"initandlisten","msg":"WiredTiger opened","attr":{"durationMillis":467}}
{"t":{"$date":"2025-11-25T03:46:58.518+00:00"},"s":"I", "c":"RECOVERY", "id":23987, "ctx":"initandlisten","msg":"WiredTiger recoveryTimestamp","attr":{"recoveryTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:58.541+00:00"},"s":"I", "c":"STORAGE", "id":4366408, "ctx":"initandlisten","msg":"No table logging settings modifications are required for existing WiredTiger tables","attr":{"loggingEnabled":false}}
{"t":{"$date":"2025-11-25T03:46:58.541+00:00"},"s":"I", "c":"STORAGE", "id":22262, "ctx":"initandlisten","msg":"Timestamp monitor starting"}
{"t":{"$date":"2025-11-25T03:46:58.549+00:00"},"s":"W", "c":"CONTROL", "id":22120, "ctx":"initandlisten","msg":"Access control is not enabled for the database. Read and write access to data and configuration is unrestricted","tags":["startupWarnings"]}
{"t":{"$date":"2025-11-25T03:46:58.550+00:00"},"s":"I", "c":"STORAGE", "id":20536, "ctx":"initandlisten","msg":"Flow Control is enabled on this deployment"}
{"t":{"$date":"2025-11-25T03:46:58.552+00:00"},"s":"I", "c":"SHARDING", "id":20997, "ctx":"initandlisten","msg":"Refreshed RWC defaults","attr":{"newDefaults":{}}}
{"t":{"$date":"2025-11-25T03:46:58.552+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.startup_log","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"703631ce-8f0d-4a10-bbb7-9d5bf9ef670d"}},"options":{"capped":true,"size":10485760}}}
{"t":{"$date":"2025-11-25T03:46:58.570+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.startup_log","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:58.571+00:00"},"s":"I", "c":"FTDC", "id":20625, "ctx":"initandlisten","msg":"Initializing full-time diagnostic data capture","attr":{"dataDirectory":"/tmp/52ypckao.mi24085d8d097344d1d89ce_34177/diagnostic.data"}}
{"t":{"$date":"2025-11-25T03:46:58.573+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.oplogTruncateAfterPoint","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"cdf2e71f-d90e-4aea-9d8f-0c44a436e8e5"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:58.592+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.oplogTruncateAfterPoint","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:58.592+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.minvalid","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"e280bf8e-917b-4655-a7ee-c02bce23629f"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:58.612+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.minvalid","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:58.612+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.election","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"e437541a-6cff-4c82-89b8-8a63ae74ccd1"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:58.630+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.election","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:58.630+00:00"},"s":"I", "c":"REPL", "id":21311, "ctx":"initandlisten","msg":"Did not find local initialized voted for document at startup"}
{"t":{"$date":"2025-11-25T03:46:58.630+00:00"},"s":"I", "c":"REPL", "id":21312, "ctx":"initandlisten","msg":"Did not find local Rollback ID document at startup. Creating one"}
{"t":{"$date":"2025-11-25T03:46:58.630+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.system.rollback.id","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"4d719115-1783-4038-acf6-b047f74c33a1"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:58.650+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.system.rollback.id","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:58.650+00:00"},"s":"I", "c":"REPL", "id":21531, "ctx":"initandlisten","msg":"Initialized the rollback ID","attr":{"rbid":1}}
{"t":{"$date":"2025-11-25T03:46:58.650+00:00"},"s":"I", "c":"REPL", "id":21313, "ctx":"initandlisten","msg":"Did not find local replica set configuration document at startup","attr":{"error":{"code":47,"codeName":"NoMatchingDocument","errmsg":"Did not find replica set configuration document in local.system.replset"}}}
{"t":{"$date":"2025-11-25T03:46:58.652+00:00"},"s":"I", "c":"CONTROL", "id":20714, "ctx":"LogicalSessionCacheRefresh","msg":"Failed to refresh session cache, will try again at the next refresh interval","attr":{"error":"NotYetInitialized: Replication has not yet been configured"}}
{"t":{"$date":"2025-11-25T03:46:58.652+00:00"},"s":"I", "c":"CONTROL", "id":20712, "ctx":"LogicalSessionCacheReap","msg":"Sessions collection is not set up; waiting until next sessions reap interval","attr":{"error":"NamespaceNotFound: config.system.sessions does not exist"}}
{"t":{"$date":"2025-11-25T03:46:58.652+00:00"},"s":"I", "c":"REPL", "id":40440, "ctx":"initandlisten","msg":"Starting the TopologyVersionObserver"}
{"t":{"$date":"2025-11-25T03:46:58.653+00:00"},"s":"I", "c":"REPL", "id":40445, "ctx":"TopologyVersionObserver","msg":"Started TopologyVersionObserver"}
{"t":{"$date":"2025-11-25T03:46:58.653+00:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"/tmp/mongodb-34177.sock"}}
{"t":{"$date":"2025-11-25T03:46:58.653+00:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"127.0.0.1"}}
{"t":{"$date":"2025-11-25T03:46:58.653+00:00"},"s":"I", "c":"NETWORK", "id":23016, "ctx":"listener","msg":"Waiting for connections","attr":{"port":34177,"ssl":"off"}}
{"t":{"$date":"2025-11-25T03:46:59.029+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:41390","connectionId":1,"connectionCount":1}}
{"t":{"$date":"2025-11-25T03:46:59.052+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn1","msg":"client metadata","attr":{"remote":"127.0.0.1:41390","client":"conn1","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:46:59.102+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:41392","connectionId":2,"connectionCount":2}}
{"t":{"$date":"2025-11-25T03:46:59.104+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn2","msg":"client metadata","attr":{"remote":"127.0.0.1:41392","client":"conn2","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:46:59.110+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:41398","connectionId":3,"connectionCount":3}}
{"t":{"$date":"2025-11-25T03:46:59.110+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn3","msg":"client metadata","attr":{"remote":"127.0.0.1:41398","client":"conn3","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:46:59.124+00:00"},"s":"I", "c":"REPL", "id":21356, "ctx":"conn3","msg":"replSetInitiate admin command received from client"}
{"t":{"$date":"2025-11-25T03:46:59.124+00:00"},"s":"I", "c":"REPL", "id":21357, "ctx":"conn3","msg":"replSetInitiate config object parses ok","attr":{"numMembers":1}}
{"t":{"$date":"2025-11-25T03:46:59.124+00:00"},"s":"I", "c":"REPL", "id":21251, "ctx":"conn3","msg":"Creating replication oplog","attr":{"oplogSizeMB":48102}}
{"t":{"$date":"2025-11-25T03:46:59.124+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"local.oplog.rs","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"053aabcf-9a69-46cd-a015-04790a28df83"}},"options":{"capped":true,"size":50439009894.0,"autoIndexId":false}}}
{"t":{"$date":"2025-11-25T03:46:59.140+00:00"},"s":"I", "c":"STORAGE", "id":22383, "ctx":"conn3","msg":"The size storer reports that the oplog contains","attr":{"numRecords":0,"dataSize":0}}
{"t":{"$date":"2025-11-25T03:46:59.140+00:00"},"s":"I", "c":"STORAGE", "id":22382, "ctx":"conn3","msg":"WiredTiger record store oplog processing finished","attr":{"durationMillis":0}}
{"t":{"$date":"2025-11-25T03:46:59.182+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"local.system.replset","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"17fcea78-1e4a-45c3-b642-f84a5ed5e94a"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:59.199+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.system.replset","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042419,"i":1}}}}
{"t":{"$date":"2025-11-25T03:46:59.200+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"admin.system.version","uuidDisposition":"provided","uuid":{"uuid":{"$uuid":"ed811933-0189-4439-9d54-39e00e9db918"}},"options":{"uuid":{"$uuid":"ed811933-0189-4439-9d54-39e00e9db918"}}}}
{"t":{"$date":"2025-11-25T03:46:59.222+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"admin.system.version","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042419,"i":1}}}}
{"t":{"$date":"2025-11-25T03:46:59.222+00:00"},"s":"I", "c":"COMMAND", "id":20459, "ctx":"conn3","msg":"Setting featureCompatibilityVersion","attr":{"newVersion":"4.4"}}
{"t":{"$date":"2025-11-25T03:46:59.222+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn3","msg":"Skip closing connection for connection","attr":{"connectionId":3}}
{"t":{"$date":"2025-11-25T03:46:59.222+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn3","msg":"Skip closing connection for connection","attr":{"connectionId":2}}
{"t":{"$date":"2025-11-25T03:46:59.222+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn3","msg":"Skip closing connection for connection","attr":{"connectionId":1}}
{"t":{"$date":"2025-11-25T03:46:59.223+00:00"},"s":"I", "c":"REPL", "id":21392, "ctx":"conn3","msg":"New replica set config in use","attr":{"config":{"_id":"singleNodeReplSet","version":1,"term":0,"protocolVersion":1,"writeConcernMajorityJournalDefault":true,"members":[{"_id":0,"host":"127.0.0.1:34177","arbiterOnly":false,"buildIndexes":true,"hidden":false,"priority":1.0,"tags":{},"slaveDelay":0,"votes":1}],"settings":{"chainingAllowed":true,"heartbeatIntervalMillis":2000,"heartbeatTimeoutSecs":10,"electionTimeoutMillis":10000,"catchUpTimeoutMillis":-1,"catchUpTakeoverDelayMillis":30000,"getLastErrorModes":{},"getLastErrorDefaults":{"w":1,"wtimeout":0},"replicaSetId":{"$oid":"692526b3e401d61632b17dbe"}}}}}
{"t":{"$date":"2025-11-25T03:46:59.223+00:00"},"s":"I", "c":"REPL", "id":21393, "ctx":"conn3","msg":"Found self in config","attr":{"hostAndPort":"127.0.0.1:34177"}}
{"t":{"$date":"2025-11-25T03:46:59.223+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"conn3","msg":"Replica set state transition","attr":{"newState":"STARTUP2","oldState":"STARTUP"}}
{"t":{"$date":"2025-11-25T03:46:59.223+00:00"},"s":"I", "c":"REPL", "id":21306, "ctx":"conn3","msg":"Starting replication storage threads"}
{"t":{"$date":"2025-11-25T03:46:59.228+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"conn3","msg":"Replica set state transition","attr":{"newState":"RECOVERING","oldState":"STARTUP2"}}
{"t":{"$date":"2025-11-25T03:46:59.228+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn3","msg":"createCollection","attr":{"namespace":"local.replset.initialSyncId","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"ea9b840a-3f2f-495e-9503-e90226f4437e"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:59.248+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn3","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.initialSyncId","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042419,"i":1}}}}
{"t":{"$date":"2025-11-25T03:46:59.248+00:00"},"s":"I", "c":"REPL", "id":21299, "ctx":"conn3","msg":"Starting replication fetcher thread"}
{"t":{"$date":"2025-11-25T03:46:59.248+00:00"},"s":"I", "c":"REPL", "id":21300, "ctx":"conn3","msg":"Starting replication applier thread"}
{"t":{"$date":"2025-11-25T03:46:59.248+00:00"},"s":"I", "c":"REPL", "id":21301, "ctx":"conn3","msg":"Starting replication reporter thread"}
{"t":{"$date":"2025-11-25T03:46:59.248+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn3","msg":"Slow query","attr":{"type":"command","ns":"local.system.replset","command":{"replSetInitiate":{"_id":"singleNodeReplSet","members":[{"_id":0,"host":"127.0.0.1:34177"}]},"$db":"admin","lsid":{"id":{"$uuid":"28798271-3e18-48d9-8e83-e22f1566379f"}}},"numYields":0,"reslen":163,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":18}},"ReplicationStateTransition":{"acquireCount":{"w":19}},"Global":{"acquireCount":{"r":11,"w":6,"W":2}},"Database":{"acquireCount":{"r":10,"w":4,"W":2}},"Collection":{"acquireCount":{"r":3,"w":5}},"Mutex":{"acquireCount":{"r":17}},"oplog":{"acquireCount":{"w":1}}},"flowControl":{"acquireCount":5,"timeAcquiringMicros":3},"storage":{},"protocol":"op_msg","durationMillis":124}}
{"t":{"$date":"2025-11-25T03:46:59.249+00:00"},"s":"I", "c":"REPL", "id":21224, "ctx":"OplogApplier-0","msg":"Starting oplog application"}
{"t":{"$date":"2025-11-25T03:46:59.249+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"OplogApplier-0","msg":"Replica set state transition","attr":{"newState":"SECONDARY","oldState":"RECOVERING"}}
{"t":{"$date":"2025-11-25T03:46:59.249+00:00"},"s":"I", "c":"ELECTION", "id":4615652, "ctx":"OplogApplier-0","msg":"Starting an election, since we've seen no PRIMARY in election timeout period","attr":{"electionTimeoutPeriodMillis":10000}}
{"t":{"$date":"2025-11-25T03:46:59.249+00:00"},"s":"I", "c":"ELECTION", "id":21438, "ctx":"OplogApplier-0","msg":"Conducting a dry run election to see if we could be elected","attr":{"currentTerm":0}}
{"t":{"$date":"2025-11-25T03:46:59.249+00:00"},"s":"I", "c":"ELECTION", "id":21444, "ctx":"ReplCoord-0","msg":"Dry election run succeeded, running for election","attr":{"newTerm":1}}
{"t":{"$date":"2025-11-25T03:46:59.251+00:00"},"s":"I", "c":"ELECTION", "id":21450, "ctx":"ReplCoord-2","msg":"Election succeeded, assuming primary role","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:46:59.251+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"ReplCoord-2","msg":"Replica set state transition","attr":{"newState":"PRIMARY","oldState":"SECONDARY"}}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21106, "ctx":"ReplCoord-2","msg":"Resetting sync source to empty","attr":{"previousSyncSource":":27017"}}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21359, "ctx":"ReplCoord-2","msg":"Entering primary catch-up mode"}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21363, "ctx":"ReplCoord-2","msg":"Exited primary catch-up mode"}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21107, "ctx":"ReplCoord-2","msg":"Stopping replication producer"}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21239, "ctx":"ReplBatcher","msg":"Oplog buffer has been drained","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21239, "ctx":"ReplBatcher","msg":"Oplog buffer has been drained","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21343, "ctx":"RstlKillOpThread","msg":"Starting to kill user operations"}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21344, "ctx":"RstlKillOpThread","msg":"Stopped killing user operations"}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21340, "ctx":"RstlKillOpThread","msg":"State transition ops metrics","attr":{"metrics":{"lastStateTransition":"stepUp","userOpsKilled":0,"userOpsRunning":1}}}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":4508103, "ctx":"OplogApplier-0","msg":"Increment the config term via reconfig"}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":21353, "ctx":"OplogApplier-0","msg":"replSetReconfig config object parses ok","attr":{"numMembers":1}}
{"t":{"$date":"2025-11-25T03:46:59.252+00:00"},"s":"I", "c":"REPL", "id":51814, "ctx":"OplogApplier-0","msg":"Persisting new config to disk"}
{"t":{"$date":"2025-11-25T03:46:59.254+00:00"},"s":"I", "c":"REPL", "id":21392, "ctx":"OplogApplier-0","msg":"New replica set config in use","attr":{"config":{"_id":"singleNodeReplSet","version":1,"term":1,"protocolVersion":1,"writeConcernMajorityJournalDefault":true,"members":[{"_id":0,"host":"127.0.0.1:34177","arbiterOnly":false,"buildIndexes":true,"hidden":false,"priority":1.0,"tags":{},"slaveDelay":0,"votes":1}],"settings":{"chainingAllowed":true,"heartbeatIntervalMillis":2000,"heartbeatTimeoutSecs":10,"electionTimeoutMillis":10000,"catchUpTimeoutMillis":-1,"catchUpTakeoverDelayMillis":30000,"getLastErrorModes":{},"getLastErrorDefaults":{"w":1,"wtimeout":0},"replicaSetId":{"$oid":"692526b3e401d61632b17dbe"}}}}}
{"t":{"$date":"2025-11-25T03:46:59.254+00:00"},"s":"I", "c":"REPL", "id":21393, "ctx":"OplogApplier-0","msg":"Found self in config","attr":{"hostAndPort":"127.0.0.1:34177"}}
{"t":{"$date":"2025-11-25T03:46:59.254+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"OplogApplier-0","msg":"createCollection","attr":{"namespace":"config.transactions","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"68aef30f-d014-4d85-8569-834e53e1b940"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:59.289+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"OplogApplier-0","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"config.transactions","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042419,"i":3}}}}
{"t":{"$date":"2025-11-25T03:46:59.289+00:00"},"s":"I", "c":"STORAGE", "id":20657, "ctx":"OplogApplier-0","msg":"IndexBuildsCoordinator::onStepUp - this node is stepping up to primary"}
{"t":{"$date":"2025-11-25T03:46:59.289+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"OplogApplier-0","msg":"createCollection","attr":{"namespace":"config.system.indexBuilds","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"cec9aa75-1b94-4923-b5b0-50485fd3bb9b"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:59.305+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"OplogApplier-0","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"config.system.indexBuilds","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042419,"i":5}}}}
{"t":{"$date":"2025-11-25T03:46:59.306+00:00"},"s":"I", "c":"REPL", "id":21331, "ctx":"OplogApplier-0","msg":"Transition to primary complete; database writes are now permitted"}
{"t":{"$date":"2025-11-25T03:46:59.306+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"monitoring-keys-for-HMAC","msg":"createCollection","attr":{"namespace":"admin.system.keys","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"d2b8a5fd-68bc-4768-95de-0a76f2fa698d"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:46:59.327+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"monitoring-keys-for-HMAC","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"admin.system.keys","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042419,"i":6}}}}
{"t":{"$date":"2025-11-25T03:46:59.329+00:00"},"s":"I", "c":"STORAGE", "id":22310, "ctx":"WTJournalFlusher","msg":"Triggering the first stable checkpoint","attr":{"initialData":{"$timestamp":{"t":1764042419,"i":1}},"prevStable":{"$timestamp":{"t":0,"i":0}},"currStable":{"$timestamp":{"t":1764042419,"i":7}}}}
{"t":{"$date":"2025-11-25T03:46:59.472+00:00"},"s":"I", "c":"CONTROL", "id":23285, "ctx":"main","msg":"Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'"}
{"t":{"$date":"2025-11-25T03:46:59.477+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"}
{"t":{"$date":"2025-11-25T03:46:59.477+00:00"},"s":"I", "c":"NETWORK", "id":4648601, "ctx":"main","msg":"Implicit TCP FastOpen unavailable. If TCP FastOpen is required, set tcpFastOpenServer, tcpFastOpenClient, and tcpFastOpenQueueSize."}
{"t":{"$date":"2025-11-25T03:46:59.477+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"}
{"t":{"$date":"2025-11-25T03:46:59.478+00:00"},"s":"I", "c":"STORAGE", "id":4615611, "ctx":"initandlisten","msg":"MongoDB starting","attr":{"pid":149392,"port":32865,"dbPath":"/tmp/f54m2zn5.l0x0520d32fc74c4f838929_32865","architecture":"64-bit","host":"DESKTOP-7GHGC2M"}}
{"t":{"$date":"2025-11-25T03:46:59.478+00:00"},"s":"I", "c":"CONTROL", "id":23403, "ctx":"initandlisten","msg":"Build Info","attr":{"buildInfo":{"version":"4.4.4","gitVersion":"8db30a63db1a9d84bdcad0c83369623f708e0397","openSSLVersion":"OpenSSL 1.1.1w 11 Sep 2023","modules":[],"allocator":"tcmalloc","environment":{"distmod":"ubuntu2004","distarch":"x86_64","target_arch":"x86_64"}}}}
{"t":{"$date":"2025-11-25T03:46:59.478+00:00"},"s":"I", "c":"CONTROL", "id":51765, "ctx":"initandlisten","msg":"Operating System","attr":{"os":{"name":"Ubuntu","version":"24.04"}}}
{"t":{"$date":"2025-11-25T03:46:59.478+00:00"},"s":"I", "c":"CONTROL", "id":21951, "ctx":"initandlisten","msg":"Options set by command line","attr":{"options":{"net":{"bindIp":"127.0.0.1","port":32865},"replication":{"replSet":"singleNodeReplSet"},"storage":{"dbPath":"/tmp/f54m2zn5.l0x0520d32fc74c4f838929_32865"}}}}
{"t":{"$date":"2025-11-25T03:46:59.479+00:00"},"s":"I", "c":"STORAGE", "id":22297, "ctx":"initandlisten","msg":"Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem","tags":["startupWarnings"]}
{"t":{"$date":"2025-11-25T03:46:59.480+00:00"},"s":"I", "c":"STORAGE", "id":22315, "ctx":"initandlisten","msg":"Opening WiredTiger","attr":{"config":"create,cache_size=7485M,session_max=33000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000,close_scan_interval=10,close_handle_minimum=250),statistics_log=(wait=0),verbose=[recovery_progress,checkpoint_progress,compact_progress],"}}
{"t":{"$date":"2025-11-25T03:46:59.946+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":"[1764042419:946594][149392:0x710e8d06fcc0], txn-recover: [WT_VERB_RECOVERY | WT_VERB_RECOVERY_PROGRESS] Set global recovery timestamp: (0, 0)"}}
{"t":{"$date":"2025-11-25T03:46:59.946+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":"[1764042419:946648][149392:0x710e8d06fcc0], txn-recover: [WT_VERB_RECOVERY | WT_VERB_RECOVERY_PROGRESS] Set global oldest timestamp: (0, 0)"}}
{"t":{"$date":"2025-11-25T03:46:59.959+00:00"},"s":"I", "c":"STORAGE", "id":4795906, "ctx":"initandlisten","msg":"WiredTiger opened","attr":{"durationMillis":479}}
{"t":{"$date":"2025-11-25T03:46:59.959+00:00"},"s":"I", "c":"RECOVERY", "id":23987, "ctx":"initandlisten","msg":"WiredTiger recoveryTimestamp","attr":{"recoveryTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:46:59.982+00:00"},"s":"I", "c":"STORAGE", "id":4366408, "ctx":"initandlisten","msg":"No table logging settings modifications are required for existing WiredTiger tables","attr":{"loggingEnabled":false}}
{"t":{"$date":"2025-11-25T03:46:59.982+00:00"},"s":"I", "c":"STORAGE", "id":22262, "ctx":"initandlisten","msg":"Timestamp monitor starting"}
{"t":{"$date":"2025-11-25T03:46:59.995+00:00"},"s":"W", "c":"CONTROL", "id":22120, "ctx":"initandlisten","msg":"Access control is not enabled for the database. Read and write access to data and configuration is unrestricted","tags":["startupWarnings"]}
{"t":{"$date":"2025-11-25T03:46:59.996+00:00"},"s":"I", "c":"STORAGE", "id":20536, "ctx":"initandlisten","msg":"Flow Control is enabled on this deployment"}
{"t":{"$date":"2025-11-25T03:46:59.997+00:00"},"s":"I", "c":"SHARDING", "id":20997, "ctx":"initandlisten","msg":"Refreshed RWC defaults","attr":{"newDefaults":{}}}
{"t":{"$date":"2025-11-25T03:46:59.997+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.startup_log","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"ad29fb35-58e2-4924-b4fb-17eeb34908cf"}},"options":{"capped":true,"size":10485760}}}
{"t":{"$date":"2025-11-25T03:47:00.021+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.startup_log","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:47:00.022+00:00"},"s":"I", "c":"FTDC", "id":20625, "ctx":"initandlisten","msg":"Initializing full-time diagnostic data capture","attr":{"dataDirectory":"/tmp/f54m2zn5.l0x0520d32fc74c4f838929_32865/diagnostic.data"}}
{"t":{"$date":"2025-11-25T03:47:00.023+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.oplogTruncateAfterPoint","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"6ebbbc40-875b-448f-ac4d-9c7fe238b5eb"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.044+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.oplogTruncateAfterPoint","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:47:00.044+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.minvalid","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"603415e4-8dc1-4e82-9f62-0e6d48de7352"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.061+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.minvalid","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:47:00.062+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.replset.election","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"94ff23a8-11b3-423c-af02-2422e71f24fc"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.084+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.election","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:47:00.085+00:00"},"s":"I", "c":"REPL", "id":21311, "ctx":"initandlisten","msg":"Did not find local initialized voted for document at startup"}
{"t":{"$date":"2025-11-25T03:47:00.085+00:00"},"s":"I", "c":"REPL", "id":21312, "ctx":"initandlisten","msg":"Did not find local Rollback ID document at startup. Creating one"}
{"t":{"$date":"2025-11-25T03:47:00.085+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"initandlisten","msg":"createCollection","attr":{"namespace":"local.system.rollback.id","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"a2f46637-91c1-4f52-8256-a20611926972"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.101+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"initandlisten","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.system.rollback.id","index":"_id_","commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:47:00.101+00:00"},"s":"I", "c":"REPL", "id":21531, "ctx":"initandlisten","msg":"Initialized the rollback ID","attr":{"rbid":1}}
{"t":{"$date":"2025-11-25T03:47:00.101+00:00"},"s":"I", "c":"REPL", "id":21313, "ctx":"initandlisten","msg":"Did not find local replica set configuration document at startup","attr":{"error":{"code":47,"codeName":"NoMatchingDocument","errmsg":"Did not find replica set configuration document in local.system.replset"}}}
{"t":{"$date":"2025-11-25T03:47:00.102+00:00"},"s":"I", "c":"CONTROL", "id":20714, "ctx":"LogicalSessionCacheRefresh","msg":"Failed to refresh session cache, will try again at the next refresh interval","attr":{"error":"NotYetInitialized: Replication has not yet been configured"}}
{"t":{"$date":"2025-11-25T03:47:00.103+00:00"},"s":"I", "c":"CONTROL", "id":20712, "ctx":"LogicalSessionCacheReap","msg":"Sessions collection is not set up; waiting until next sessions reap interval","attr":{"error":"NamespaceNotFound: config.system.sessions does not exist"}}
{"t":{"$date":"2025-11-25T03:47:00.103+00:00"},"s":"I", "c":"REPL", "id":40440, "ctx":"initandlisten","msg":"Starting the TopologyVersionObserver"}
{"t":{"$date":"2025-11-25T03:47:00.103+00:00"},"s":"I", "c":"REPL", "id":40445, "ctx":"TopologyVersionObserver","msg":"Started TopologyVersionObserver"}
{"t":{"$date":"2025-11-25T03:47:00.103+00:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"/tmp/mongodb-32865.sock"}}
{"t":{"$date":"2025-11-25T03:47:00.103+00:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"127.0.0.1"}}
{"t":{"$date":"2025-11-25T03:47:00.103+00:00"},"s":"I", "c":"NETWORK", "id":23016, "ctx":"listener","msg":"Waiting for connections","attr":{"port":32865,"ssl":"off"}}
{"t":{"$date":"2025-11-25T03:47:00.141+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:32952","connectionId":1,"connectionCount":1}}
{"t":{"$date":"2025-11-25T03:47:00.142+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn1","msg":"client metadata","attr":{"remote":"127.0.0.1:32952","client":"conn1","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:47:00.143+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:32956","connectionId":2,"connectionCount":2}}
{"t":{"$date":"2025-11-25T03:47:00.144+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn2","msg":"client metadata","attr":{"remote":"127.0.0.1:32956","client":"conn2","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:47:00.144+00:00"},"s":"I", "c":"NETWORK", "id":22943, "ctx":"listener","msg":"Connection accepted","attr":{"remote":"127.0.0.1:32958","connectionId":3,"connectionCount":3}}
{"t":{"$date":"2025-11-25T03:47:00.144+00:00"},"s":"I", "c":"NETWORK", "id":51800, "ctx":"conn3","msg":"client metadata","attr":{"remote":"127.0.0.1:32958","client":"conn3","doc":{"driver":{"name":"mongo-csharp-driver","version":"3.5.0"},"os":{"type":"Linux","name":"Ubuntu 24.04.3 LTS","architecture":"x86_64","version":"24.04.3"},"platform":".NET 10.0.0-rc.2.25502.107"}}}
{"t":{"$date":"2025-11-25T03:47:00.145+00:00"},"s":"I", "c":"REPL", "id":21356, "ctx":"conn2","msg":"replSetInitiate admin command received from client"}
{"t":{"$date":"2025-11-25T03:47:00.145+00:00"},"s":"I", "c":"REPL", "id":21357, "ctx":"conn2","msg":"replSetInitiate config object parses ok","attr":{"numMembers":1}}
{"t":{"$date":"2025-11-25T03:47:00.145+00:00"},"s":"I", "c":"REPL", "id":21251, "ctx":"conn2","msg":"Creating replication oplog","attr":{"oplogSizeMB":48087}}
{"t":{"$date":"2025-11-25T03:47:00.146+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn2","msg":"createCollection","attr":{"namespace":"local.oplog.rs","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"20717f8c-54ff-4b74-a452-ece55bdb7dbb"}},"options":{"capped":true,"size":50423249920.0,"autoIndexId":false}}}
{"t":{"$date":"2025-11-25T03:47:00.155+00:00"},"s":"I", "c":"STORAGE", "id":22383, "ctx":"conn2","msg":"The size storer reports that the oplog contains","attr":{"numRecords":0,"dataSize":0}}
{"t":{"$date":"2025-11-25T03:47:00.155+00:00"},"s":"I", "c":"STORAGE", "id":22382, "ctx":"conn2","msg":"WiredTiger record store oplog processing finished","attr":{"durationMillis":0}}
{"t":{"$date":"2025-11-25T03:47:00.200+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn2","msg":"createCollection","attr":{"namespace":"local.system.replset","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"e76a7051-11f0-41d1-9e22-39b73aa284c2"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.217+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn2","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.system.replset","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":1}}}}
{"t":{"$date":"2025-11-25T03:47:00.218+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn2","msg":"createCollection","attr":{"namespace":"admin.system.version","uuidDisposition":"provided","uuid":{"uuid":{"$uuid":"7ca3ad6a-3e97-4f2c-aef9-b17fe9bd6c74"}},"options":{"uuid":{"$uuid":"7ca3ad6a-3e97-4f2c-aef9-b17fe9bd6c74"}}}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn2","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"admin.system.version","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":1}}}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"COMMAND", "id":20459, "ctx":"conn2","msg":"Setting featureCompatibilityVersion","attr":{"newVersion":"4.4"}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn2","msg":"Skip closing connection for connection","attr":{"connectionId":3}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn2","msg":"Skip closing connection for connection","attr":{"connectionId":2}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"NETWORK", "id":22991, "ctx":"conn2","msg":"Skip closing connection for connection","attr":{"connectionId":1}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"REPL", "id":21392, "ctx":"conn2","msg":"New replica set config in use","attr":{"config":{"_id":"singleNodeReplSet","version":1,"term":0,"protocolVersion":1,"writeConcernMajorityJournalDefault":true,"members":[{"_id":0,"host":"127.0.0.1:32865","arbiterOnly":false,"buildIndexes":true,"hidden":false,"priority":1.0,"tags":{},"slaveDelay":0,"votes":1}],"settings":{"chainingAllowed":true,"heartbeatIntervalMillis":2000,"heartbeatTimeoutSecs":10,"electionTimeoutMillis":10000,"catchUpTimeoutMillis":-1,"catchUpTakeoverDelayMillis":30000,"getLastErrorModes":{},"getLastErrorDefaults":{"w":1,"wtimeout":0},"replicaSetId":{"$oid":"692526b48d3d1985c4de93e4"}}}}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"REPL", "id":21393, "ctx":"conn2","msg":"Found self in config","attr":{"hostAndPort":"127.0.0.1:32865"}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"conn2","msg":"Replica set state transition","attr":{"newState":"STARTUP2","oldState":"STARTUP"}}
{"t":{"$date":"2025-11-25T03:47:00.235+00:00"},"s":"I", "c":"REPL", "id":21306, "ctx":"conn2","msg":"Starting replication storage threads"}
{"t":{"$date":"2025-11-25T03:47:00.238+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"conn2","msg":"Replica set state transition","attr":{"newState":"RECOVERING","oldState":"STARTUP2"}}
{"t":{"$date":"2025-11-25T03:47:00.238+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn2","msg":"createCollection","attr":{"namespace":"local.replset.initialSyncId","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"1703a7f0-89fd-4541-b1f9-034025c93c97"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.260+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn2","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"local.replset.initialSyncId","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":1}}}}
{"t":{"$date":"2025-11-25T03:47:00.260+00:00"},"s":"I", "c":"REPL", "id":21299, "ctx":"conn2","msg":"Starting replication fetcher thread"}
{"t":{"$date":"2025-11-25T03:47:00.260+00:00"},"s":"I", "c":"REPL", "id":21300, "ctx":"conn2","msg":"Starting replication applier thread"}
{"t":{"$date":"2025-11-25T03:47:00.261+00:00"},"s":"I", "c":"REPL", "id":21301, "ctx":"conn2","msg":"Starting replication reporter thread"}
{"t":{"$date":"2025-11-25T03:47:00.261+00:00"},"s":"I", "c":"REPL", "id":21224, "ctx":"OplogApplier-0","msg":"Starting oplog application"}
{"t":{"$date":"2025-11-25T03:47:00.261+00:00"},"s":"I", "c":"COMMAND", "id":51803, "ctx":"conn2","msg":"Slow query","attr":{"type":"command","ns":"local.system.replset","command":{"replSetInitiate":{"_id":"singleNodeReplSet","members":[{"_id":0,"host":"127.0.0.1:32865"}]},"$db":"admin","lsid":{"id":{"$uuid":"5884425c-dce7-4528-b599-e26f26b5dff1"}}},"numYields":0,"reslen":163,"locks":{"ParallelBatchWriterMode":{"acquireCount":{"r":18}},"ReplicationStateTransition":{"acquireCount":{"w":19}},"Global":{"acquireCount":{"r":11,"w":6,"W":2}},"Database":{"acquireCount":{"r":10,"w":4,"W":2}},"Collection":{"acquireCount":{"r":3,"w":5}},"Mutex":{"acquireCount":{"r":17}},"oplog":{"acquireCount":{"w":1}}},"flowControl":{"acquireCount":5,"timeAcquiringMicros":5},"storage":{},"protocol":"op_msg","durationMillis":115}}
{"t":{"$date":"2025-11-25T03:47:00.261+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"OplogApplier-0","msg":"Replica set state transition","attr":{"newState":"SECONDARY","oldState":"RECOVERING"}}
{"t":{"$date":"2025-11-25T03:47:00.261+00:00"},"s":"I", "c":"ELECTION", "id":4615652, "ctx":"OplogApplier-0","msg":"Starting an election, since we've seen no PRIMARY in election timeout period","attr":{"electionTimeoutPeriodMillis":10000}}
{"t":{"$date":"2025-11-25T03:47:00.262+00:00"},"s":"I", "c":"ELECTION", "id":21438, "ctx":"OplogApplier-0","msg":"Conducting a dry run election to see if we could be elected","attr":{"currentTerm":0}}
{"t":{"$date":"2025-11-25T03:47:00.262+00:00"},"s":"I", "c":"ELECTION", "id":21444, "ctx":"ReplCoord-0","msg":"Dry election run succeeded, running for election","attr":{"newTerm":1}}
{"t":{"$date":"2025-11-25T03:47:00.264+00:00"},"s":"I", "c":"ELECTION", "id":21450, "ctx":"ReplCoord-0","msg":"Election succeeded, assuming primary role","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:47:00.264+00:00"},"s":"I", "c":"REPL", "id":21358, "ctx":"ReplCoord-0","msg":"Replica set state transition","attr":{"newState":"PRIMARY","oldState":"SECONDARY"}}
{"t":{"$date":"2025-11-25T03:47:00.264+00:00"},"s":"I", "c":"REPL", "id":21106, "ctx":"ReplCoord-0","msg":"Resetting sync source to empty","attr":{"previousSyncSource":":27017"}}
{"t":{"$date":"2025-11-25T03:47:00.264+00:00"},"s":"I", "c":"REPL", "id":21359, "ctx":"ReplCoord-0","msg":"Entering primary catch-up mode"}
{"t":{"$date":"2025-11-25T03:47:00.264+00:00"},"s":"I", "c":"REPL", "id":21363, "ctx":"ReplCoord-0","msg":"Exited primary catch-up mode"}
{"t":{"$date":"2025-11-25T03:47:00.264+00:00"},"s":"I", "c":"REPL", "id":21107, "ctx":"ReplCoord-0","msg":"Stopping replication producer"}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":21239, "ctx":"ReplBatcher","msg":"Oplog buffer has been drained","attr":{"term":1}}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":21343, "ctx":"RstlKillOpThread","msg":"Starting to kill user operations"}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":21344, "ctx":"RstlKillOpThread","msg":"Stopped killing user operations"}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":21340, "ctx":"RstlKillOpThread","msg":"State transition ops metrics","attr":{"metrics":{"lastStateTransition":"stepUp","userOpsKilled":0,"userOpsRunning":1}}}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":4508103, "ctx":"OplogApplier-0","msg":"Increment the config term via reconfig"}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":21353, "ctx":"OplogApplier-0","msg":"replSetReconfig config object parses ok","attr":{"numMembers":1}}
{"t":{"$date":"2025-11-25T03:47:00.265+00:00"},"s":"I", "c":"REPL", "id":51814, "ctx":"OplogApplier-0","msg":"Persisting new config to disk"}
{"t":{"$date":"2025-11-25T03:47:00.267+00:00"},"s":"I", "c":"REPL", "id":21392, "ctx":"OplogApplier-0","msg":"New replica set config in use","attr":{"config":{"_id":"singleNodeReplSet","version":1,"term":1,"protocolVersion":1,"writeConcernMajorityJournalDefault":true,"members":[{"_id":0,"host":"127.0.0.1:32865","arbiterOnly":false,"buildIndexes":true,"hidden":false,"priority":1.0,"tags":{},"slaveDelay":0,"votes":1}],"settings":{"chainingAllowed":true,"heartbeatIntervalMillis":2000,"heartbeatTimeoutSecs":10,"electionTimeoutMillis":10000,"catchUpTimeoutMillis":-1,"catchUpTakeoverDelayMillis":30000,"getLastErrorModes":{},"getLastErrorDefaults":{"w":1,"wtimeout":0},"replicaSetId":{"$oid":"692526b48d3d1985c4de93e4"}}}}}
{"t":{"$date":"2025-11-25T03:47:00.267+00:00"},"s":"I", "c":"REPL", "id":21393, "ctx":"OplogApplier-0","msg":"Found self in config","attr":{"hostAndPort":"127.0.0.1:32865"}}
{"t":{"$date":"2025-11-25T03:47:00.267+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"OplogApplier-0","msg":"createCollection","attr":{"namespace":"config.transactions","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"90401e83-c42b-4ce2-a5ad-49a4c81c816b"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.285+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"OplogApplier-0","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"config.transactions","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":3}}}}
{"t":{"$date":"2025-11-25T03:47:00.286+00:00"},"s":"I", "c":"STORAGE", "id":20657, "ctx":"OplogApplier-0","msg":"IndexBuildsCoordinator::onStepUp - this node is stepping up to primary"}
{"t":{"$date":"2025-11-25T03:47:00.286+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"OplogApplier-0","msg":"createCollection","attr":{"namespace":"config.system.indexBuilds","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"e0a50b99-1d2c-4142-ae31-a78f724d5f96"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.303+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"OplogApplier-0","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"config.system.indexBuilds","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":5}}}}
{"t":{"$date":"2025-11-25T03:47:00.303+00:00"},"s":"I", "c":"REPL", "id":21331, "ctx":"OplogApplier-0","msg":"Transition to primary complete; database writes are now permitted"}
{"t":{"$date":"2025-11-25T03:47:00.304+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"monitoring-keys-for-HMAC","msg":"createCollection","attr":{"namespace":"admin.system.keys","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"72c6ddd0-0840-4549-9145-58849a210856"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.322+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"monitoring-keys-for-HMAC","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"admin.system.keys","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":6}}}}
{"t":{"$date":"2025-11-25T03:47:00.323+00:00"},"s":"I", "c":"STORAGE", "id":22310, "ctx":"WTJournalFlusher","msg":"Triggering the first stable checkpoint","attr":{"initialData":{"$timestamp":{"t":1764042420,"i":1}},"prevStable":{"$timestamp":{"t":0,"i":0}},"currStable":{"$timestamp":{"t":1764042420,"i":7}}}}
{"t":{"$date":"2025-11-25T03:47:00.580+00:00"},"s":"I", "c":"COMMAND", "id":518070, "ctx":"conn2","msg":"CMD: drop","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs"}}
{"t":{"$date":"2025-11-25T03:47:00.762+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn2","msg":"createCollection","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"a39564e0-a39c-47ce-b7f1-eb7e36a2686a"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:00.795+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn2","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042420,"i":9}}}}
{"t":{"$date":"2025-11-25T03:47:00.998+00:00"},"s":"I", "c":"COMMAND", "id":518070, "ctx":"conn2","msg":"CMD: drop","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs"}}
{"t":{"$date":"2025-11-25T03:47:00.998+00:00"},"s":"I", "c":"STORAGE", "id":23879, "ctx":"conn2","msg":"About to abort all index builders","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","uuid":{"uuid":{"$uuid":"a39564e0-a39c-47ce-b7f1-eb7e36a2686a"}},"reason":"Collection concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs(a39564e0-a39c-47ce-b7f1-eb7e36a2686a) is being dropped"}}
{"t":{"$date":"2025-11-25T03:47:00.998+00:00"},"s":"I", "c":"STORAGE", "id":20314, "ctx":"conn2","msg":"dropCollection: storage engine will take ownership of drop-pending collection","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","uuid":{"uuid":{"$uuid":"a39564e0-a39c-47ce-b7f1-eb7e36a2686a"}},"dropOpTime":{"ts":{"$timestamp":{"t":0,"i":0}},"t":-1},"commitTimestamp":{"$timestamp":{"t":0,"i":0}}}}
{"t":{"$date":"2025-11-25T03:47:00.998+00:00"},"s":"I", "c":"STORAGE", "id":20318, "ctx":"conn2","msg":"Finishing collection drop","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","uuid":{"uuid":{"$uuid":"a39564e0-a39c-47ce-b7f1-eb7e36a2686a"}}}}
{"t":{"$date":"2025-11-25T03:47:00.998+00:00"},"s":"I", "c":"STORAGE", "id":22206, "ctx":"conn2","msg":"Deferring table drop for index","attr":{"index":"_id_","namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","uuid":{"uuid":{"$uuid":"a39564e0-a39c-47ce-b7f1-eb7e36a2686a"}},"ident":"index-24-1032418473798039975","commitTimestamp":{"$timestamp":{"t":1764042420,"i":15}}}}
{"t":{"$date":"2025-11-25T03:47:00.998+00:00"},"s":"I", "c":"STORAGE", "id":22214, "ctx":"conn2","msg":"Deferring table drop for collection","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","ident":"collection-23-1032418473798039975","commitTimestamp":{"$timestamp":{"t":1764042420,"i":15}}}}
{"t":{"$date":"2025-11-25T03:47:01.013+00:00"},"s":"I", "c":"COMMAND", "id":518070, "ctx":"conn2","msg":"CMD: drop","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs"}}
{"t":{"$date":"2025-11-25T03:47:01.018+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn2","msg":"createCollection","attr":{"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"169c4fe9-6a2c-4f20-92cb-ed7517b7d6a2"}},"options":{}}}
{"t":{"$date":"2025-11-25T03:47:01.037+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn2","msg":"Index build: done building","attr":{"buildUUID":null,"namespace":"concelier-tests-7e8484ac4f7a4ae08dd2c21f49c49720.jobs","index":"_id_","commitTimestamp":{"$timestamp":{"t":1764042421,"i":1}}}}
[xUnit.net 00:00:03.64] Finished: StellaOps.Concelier.Storage.Mongo.Tests
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:47:01.1736122+00:00">
<Text>Data collector 'Blame' message: All tests finished running, Sequence file will not be generated.</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="6532f9f9-e718-4fe1-b82a-33fa91310011" name="@DESKTOP-7GHGC2M 2025-11-25 03:46:20" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2025-11-25T03:46:20.2426491+00:00" queuing="2025-11-25T03:46:20.2426492+00:00" start="2025-11-25T03:46:16.0469600+00:00" finish="2025-11-25T03:46:20.2433850+00:00" />
<TestSettings name="default" id="9a249eb2-1aba-42f8-b18c-72c4811061af">
<Deployment runDeploymentRoot="_DESKTOP-7GHGC2M_2025-11-25_03_46_20" />
</TestSettings>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="0" executed="0" passed="0" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0-rc.2.25502.107)
[xUnit.net 00:00:01.08] Discovering: StellaOps.Concelier.Storage.Mongo.Tests
[xUnit.net 00:00:01.15] Discovered: StellaOps.Concelier.Storage.Mongo.Tests
[xUnit.net 00:00:01.16] Starting: StellaOps.Concelier.Storage.Mongo.Tests
[xUnit.net 00:00:01.18] Finished: StellaOps.Concelier.Storage.Mongo.Tests
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:46:20.0884769+00:00">
<Text>No test matches the given testcase filter `FullyQualifiedName~Orchestrator` in /mnt/e/dev/git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Storage.Mongo.Tests/bin/Debug/net10.0/StellaOps.Concelier.Storage.Mongo.Tests.dll</Text>
</RunInfo>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:46:20.1812017+00:00">
<Text>Data collector 'Blame' message: All tests finished running, Sequence file will not be generated.</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="fbfb7a9f-5241-4a05-9748-f903d2471919" name="@DESKTOP-7GHGC2M 2025-11-25 03:58:42" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2025-11-25T03:58:42.1586213+00:00" queuing="2025-11-25T03:58:42.1586213+00:00" start="2025-11-25T03:58:40.4775038+00:00" finish="2025-11-25T03:58:42.1592259+00:00" />
<TestSettings name="default" id="17cd55e4-92da-438f-b39d-622a20745956">
<Deployment runDeploymentRoot="_DESKTOP-7GHGC2M_2025-11-25_03_58_42" />
</TestSettings>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="0" executed="0" passed="0" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0-rc.2.25502.107)
[xUnit.net 00:00:00.25] Discovering: StellaOps.Concelier.WebService.Tests
[xUnit.net 00:00:00.31] Discovered: StellaOps.Concelier.WebService.Tests
[xUnit.net 00:00:00.31] Starting: StellaOps.Concelier.WebService.Tests
[xUnit.net 00:00:00.33] Finished: StellaOps.Concelier.WebService.Tests
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:58:42.0099522+00:00">
<Text>No test matches the given testcase filter `FullyQualifiedName~Orchestrator` in /mnt/e/dev/git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/bin/Debug/net10.0/StellaOps.Concelier.WebService.Tests.dll</Text>
</RunInfo>
<RunInfo computerName="DESKTOP-7GHGC2M" outcome="Warning" timestamp="2025-11-25T03:58:42.0894633+00:00">
<Text>Data collector 'Blame' message: All tests finished running, Sequence file will not be generated.</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>

View File

@@ -0,0 +1,32 @@
CI runner for **DEVOPS-CI-110-001** (Concelier + Excititor smoke)
==================================================================
Scope
-----
- Warm NuGet cache from `local-nugets`, `.nuget/packages`, and (optionally) NuGet.org.
- Ensure OpenSSL 1.1 is present (installs `libssl1.1` when available via `apt-get`).
- Run lightweight slices:
- Concelier WebService: `HealthAndReadyEndpointsRespond`
- Excititor WebService: `AirgapImportEndpointTests*`
- Emit TRX + logs to `ops/devops/artifacts/ci-110/<timestamp>/`.
Usage
-----
```bash
export NUGET_SOURCES="/mnt/e/dev/git.stella-ops.org/local-nugets;/mnt/e/dev/git.stella-ops.org/.nuget/packages;https://api.nuget.org/v3/index.json"
export TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ) # optional, for reproducible paths
bash ops/devops/ci-110-runner/run-ci-110.sh
```
Artifacts
---------
- TRX: `ops/devops/artifacts/ci-110/<timestamp>/trx/`
- `concelier-health.trx` (1 test)
- `excititor-airgapimport.fqn.trx` (2 tests)
- Logs + restores under `ops/devops/artifacts/ci-110/<timestamp>/logs/`.
Notes
-----
- The runner uses `--no-build` on test slices; prior restores are included in the script.
- If OpenSSL 1.1 is not present and `apt-get` cannot install `libssl1.1`, set `LD_LIBRARY_PATH` to a pre-installed OpenSSL 1.1 location before running.
- Extend the runner by adding more `run_test_slice` calls for additional suites; keep filters tight to avoid long hangs on constrained CI.

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# CI helper for DEVOPS-CI-110-001
# - Warms NuGet cache from local sources
# - Ensures OpenSSL 1.1 compatibility if available
# - Runs targeted Concelier and Excititor test slices with TRX output
# - Writes artefacts under ops/devops/artifacts/ci-110/<timestamp>/
set -euo pipefail
ROOT="${ROOT:-$(git rev-parse --show-toplevel)}"
TIMESTAMP="${TIMESTAMP:-$(date -u +%Y%m%dT%H%M%SZ)}"
ARTIFACT_ROOT="${ARTIFACT_ROOT:-"$ROOT/ops/devops/artifacts/ci-110/$TIMESTAMP"}"
LOG_DIR="$ARTIFACT_ROOT/logs"
TRX_DIR="$ARTIFACT_ROOT/trx"
NUGET_SOURCES_DEFAULT="$ROOT/local-nugets;$ROOT/.nuget/packages;https://api.nuget.org/v3/index.json"
NUGET_SOURCES="${NUGET_SOURCES:-$NUGET_SOURCES_DEFAULT}"
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_RESTORE_DISABLE_PARALLEL="${DOTNET_RESTORE_DISABLE_PARALLEL:-1}"
mkdir -p "$LOG_DIR" "$TRX_DIR"
log() {
printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"
}
ensure_openssl11() {
if openssl version 2>/dev/null | grep -q "1\\.1."; then
log "OpenSSL 1.1 detected: $(openssl version)"
return
fi
if command -v apt-get >/dev/null 2>&1; then
log "OpenSSL 1.1 not found; attempting install via apt-get (libssl1.1)"
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null || true
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libssl1.1 || true
if openssl version 2>/dev/null | grep -q "1\\.1."; then
log "OpenSSL 1.1 available after install: $(openssl version)"
return
fi
fi
log "OpenSSL 1.1 still unavailable. Provide it via LD_LIBRARY_PATH if required."
}
restore_solution() {
local sln="$1"
log "Restore $sln"
dotnet restore "$sln" --source "$NUGET_SOURCES" --verbosity minimal | tee "$LOG_DIR/restore-$(basename "$sln").log"
}
run_test_slice() {
local proj="$1"
local filter="$2"
local name="$3"
log "Test $name ($proj, filter='$filter')"
dotnet test "$proj" \
-c Debug \
--no-build \
${filter:+--filter "$filter"} \
--logger "trx;LogFileName=${name}.trx" \
--results-directory "$TRX_DIR" \
--blame-hang \
--blame-hang-timeout 8m \
--blame-hang-dump-type none \
| tee "$LOG_DIR/test-${name}.log"
}
main() {
log "Starting CI-110 runner; artefacts -> $ARTIFACT_ROOT"
ensure_openssl11
restore_solution "$ROOT/concelier-webservice.slnf"
restore_solution "$ROOT/src/Excititor/StellaOps.Excititor.sln"
# Concelier: lightweight health slice to validate runner + Mongo wiring
run_test_slice "$ROOT/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj" \
"HealthAndReadyEndpointsRespond" \
"concelier-health"
# Excititor: airgap import surface (chunk-path) smoke
run_test_slice "$ROOT/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj" \
"FullyQualifiedName~AirgapImportEndpointTests" \
"excititor-airgapimport"
log "Done. TRX files in $TRX_DIR"
}
main "$@"

View File

@@ -0,0 +1,26 @@
# Concelier CI Runner Harness (DEVOPS-CONCELIER-CI-24-101)
Purpose: provide a deterministic, offline-friendly harness that restores, builds, and runs Concelier WebService + Storage Mongo tests with warmed NuGet cache and TRX/binlog artefacts for downstream sprints (Concelier II/III).
Usage
- From repo root run: `ops/devops/concelier-ci-runner/run-concelier-ci.sh`
- Outputs land in `ops/devops/artifacts/concelier-ci/<UTC timestamp>/`:
- `build.binlog` (solution build)
- `tests/webservice.trx`, `tests/storage.trx` (VSTest results)
- per-project `.dmp`/logs if failures occur
- `summary.json` (paths + hashes)
Environment
- Defaults: `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1`, `NUGET_PACKAGES=$REPO/.nuget/packages`.
- Uses local feed `local-nugets/` first, then NuGet.org (can be overridden via `NUGET_SOURCES`).
- No external services required; Mongo2Go provides ephemeral Mongo for tests.
What it does
1) Warm NuGet cache from `local-nugets/` into `$NUGET_PACKAGES` for offline/air-gap parity.
2) `dotnet restore` + `dotnet build` on `concelier-webservice.slnf` with `/bl`.
3) Run WebService and Storage.Mongo test projects with TRX output and without rebuild (`--no-build`).
4) Emit a concise `summary.json` listing artefacts and SHA256s for reproducibility.
Notes
- Keep test filters narrow if you need faster runs; edit `TEST_FILTER` env var (default empty = run all tests).
- Artefacts are timestamped UTC to keep ordering deterministic in pipelines; consumers should sort by path.

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
# Concelier CI runner harness (DEVOPS-CONCELIER-CI-24-101)
# Produces warmed-cache restore, build binlog, and TRX outputs for WebService + Storage Mongo tests.
repo_root="$(cd "$(dirname "$0")/../../.." && pwd)"
ts="$(date -u +%Y%m%dT%H%M%SZ)"
out_dir="$repo_root/ops/devops/artifacts/concelier-ci/$ts"
logs_dir="$out_dir/tests"
mkdir -p "$logs_dir"
# Deterministic env
export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT:-1}
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=${DOTNET_SKIP_FIRST_TIME_EXPERIENCE:-1}
export NUGET_PACKAGES=${NUGET_PACKAGES:-$repo_root/.nuget/packages}
export NUGET_SOURCES=${NUGET_SOURCES:-"$repo_root/local-nugets;$repo_root/.nuget/packages"}
export TEST_FILTER=${TEST_FILTER:-""}
export DOTNET_RESTORE_DISABLE_PARALLEL=${DOTNET_RESTORE_DISABLE_PARALLEL:-1}
# Warm NuGet cache from local feed for offline/airgap parity
mkdir -p "$NUGET_PACKAGES"
rsync -a "$repo_root/local-nugets/" "$NUGET_PACKAGES/" >/dev/null 2>&1 || true
# Restore with deterministic sources
restore_sources=()
IFS=';' read -ra SRC_ARR <<< "$NUGET_SOURCES"
for s in "${SRC_ARR[@]}"; do
[[ -n "$s" ]] && restore_sources+=(--source "$s")
done
dotnet restore "$repo_root/concelier-webservice.slnf" --ignore-failed-sources "${restore_sources[@]}"
# Build with binlog
build_binlog="$out_dir/build.binlog"
dotnet build "$repo_root/concelier-webservice.slnf" -c Debug /p:ContinuousIntegrationBuild=true /bl:"$build_binlog"
common_test_args=( -c Debug --no-build --results-directory "$logs_dir" )
if [[ -n "$TEST_FILTER" ]]; then
common_test_args+=( --filter "$TEST_FILTER" )
fi
# WebService tests
web_trx="webservice.trx"
dotnet test "$repo_root/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj" \
"${common_test_args[@]}" \
--logger "trx;LogFileName=$web_trx"
# Storage Mongo tests
storage_trx="storage.trx"
dotnet test "$repo_root/src/Concelier/__Tests/StellaOps.Concelier.Storage.Mongo.Tests/StellaOps.Concelier.Storage.Mongo.Tests.csproj" \
"${common_test_args[@]}" \
--logger "trx;LogFileName=$storage_trx"
# Summarize artefacts (relative paths to repo root)
summary="$out_dir/summary.json"
{
printf '{\n'
printf ' "timestamp_utc": "%s",\n' "$ts"
printf ' "build_binlog": "%s",\n' "${build_binlog#${repo_root}/}"
printf ' "tests": [\n'
printf ' {"project": "WebService", "trx": "%s"},\n' "${logs_dir#${repo_root}/}/$web_trx"
printf ' {"project": "Storage.Mongo", "trx": "%s"}\n' "${logs_dir#${repo_root}/}/$storage_trx"
printf ' ],\n'
printf ' "nuget_packages": "%s",\n' "${NUGET_PACKAGES#${repo_root}/}"
printf ' "sources": [\n'
for i in "${!SRC_ARR[@]}"; do
sep=","; [[ $i -eq $((${#SRC_ARR[@]}-1)) ]] && sep=""
printf ' "%s"%s\n' "${SRC_ARR[$i]}" "$sep"
done
printf ' ]\n'
printf '}\n'
} > "$summary"
echo "Artifacts written to ${out_dir#${repo_root}/}"

View File

@@ -0,0 +1,49 @@
# Incident Mode Automation (DEVOPS-OBS-55-001)
## What it does
- Auto-enables an *incident* feature flag when SLO burn rate crosses a threshold.
- Writes deterministic retention overrides (hours) for downstream storage/ingest.
- Auto-clears after a cooldown once burn is back under the reset threshold.
- Offline-friendly: no external calls; pure file outputs under `out/incident-mode/`.
## Inputs
- Burn rate multiple (fast-burn): required.
- Thresholds/cooldown/retention configurable via CLI flags or env vars.
- Optional note for audit context.
## Outputs
- `flag.json` — enabled/disabled + burn rate and note.
- `retention.json` — retention override hours + applied time.
- `last_burn.txt`, `cooldown.txt` — trace for automation/testing.
## Usage
```bash
# Activate if burn >= 2.5, otherwise decay cooldown; clear after 15 mins <0.4
scripts/observability/incident-mode.sh \
--burn-rate 3.2 \
--threshold 2.5 \
--reset-threshold 0.4 \
--cooldown-mins 15 \
--retention-hours 48 \
--note "api error burst"
# Later (burn back to normal):
scripts/observability/incident-mode.sh --burn-rate 0.2 --reset-threshold 0.4 --cooldown-mins 15
```
Outputs land in `out/incident-mode/` by default (override with `--state-dir`).
## Integration hooks
- Prometheus rule should page on SLOBurnRateFast (already in `alerts-slo.yaml`).
- A small runner (cron/workflow) can feed burn rate into this script from PromQL
(`scalar(slo:burn_rate:fast)`), then distribute `flag.json` via configmap/secret.
- Downstream services can read `retention.json` to temporarily raise retention
windows during incident mode.
## Determinism
- Timestamps are UTC ISO-8601; no network dependencies.
- State is contained under the chosen `state-dir` for reproducible runs.
## Clearing / reset
- Cooldown counter increments only when burn stays below reset threshold.
- Once cooldown minutes are met, `flag.json` flips `enabled=false` and the script
leaves prior retention files untouched (downstream can prune separately).

View File

@@ -0,0 +1,36 @@
# Orchestrator Infra Bootstrap (DEVOPS-ORCH-32-001)
## Components
- Postgres 16 (state/config)
- Mongo 7 (job ledger history)
- NATS 2.10 JetStream (queue/bus)
Compose file: `ops/devops/orchestrator/docker-compose.orchestrator.yml`
## Quick start (offline-friendly)
```bash
# bring up infra
COMPOSE_FILE=ops/devops/orchestrator/docker-compose.orchestrator.yml docker compose up -d
# smoke check and emit connection strings
scripts/orchestrator/smoke.sh
cat out/orchestrator-smoke/readiness.txt
```
Connection strings
- Postgres: `postgres://orch:orchpass@localhost:55432/orchestrator`
- Mongo: `mongodb://localhost:57017`
- NATS: `nats://localhost:4222`
## Observability
- Alerts: `ops/devops/orchestrator/alerts.yaml`
- Grafana dashboard: `ops/devops/orchestrator/grafana/orchestrator-overview.json`
- Metrics expected: `job_queue_depth`, `job_failures_total`, `lease_extensions_total`, `job_latency_seconds_bucket`.
## CI hook (suggested)
Add a workflow step (or local cron) to run `scripts/orchestrator/smoke.sh` with `SKIP_UP=1` against existing infra and publish the `readiness.txt` artifact for traceability.
## Notes
- Uses fixed ports for determinism; adjust via COMPOSE overrides if needed.
- Data volumes: `orch_pg_data`, `orch_mongo_data` (docker volumes).
- No external downloads beyond base images; pin images to specific tags above.

View File

@@ -0,0 +1,30 @@
groups:
- name: orchestrator-core
rules:
- alert: OrchestratorQueueDepthHigh
expr: job_queue_depth > 500
for: 10m
labels:
severity: warning
service: orchestrator
annotations:
summary: "Queue depth high"
description: "job_queue_depth exceeded 500 for 10m"
- alert: OrchestratorFailuresHigh
expr: rate(job_failures_total[5m]) > 5
for: 5m
labels:
severity: critical
service: orchestrator
annotations:
summary: "Job failures elevated"
description: "Failure rate above 5/min in last 5m"
- alert: OrchestratorLeaseStall
expr: rate(lease_extensions_total[5m]) == 0 and job_queue_depth > 0
for: 5m
labels:
severity: critical
service: orchestrator
annotations:
summary: "Leases stalled"
description: "No lease renewals while queue has items"

View File

@@ -0,0 +1,49 @@
version: "3.9"
services:
orchestrator-postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: orch
POSTGRES_PASSWORD: orchpass
POSTGRES_DB: orchestrator
volumes:
- orch_pg_data:/var/lib/postgresql/data
ports:
- "55432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U orch"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
orchestrator-mongo:
image: mongo:7
command: ["mongod", "--quiet", "--storageEngine=wiredTiger"]
ports:
- "57017:27017"
volumes:
- orch_mongo_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
orchestrator-nats:
image: nats:2.10-alpine
ports:
- "5422:4222"
- "5822:8222"
command: ["-js", "-m", "8222"]
healthcheck:
test: ["CMD", "nats", "--server", "localhost:4222", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
orch_pg_data:
orch_mongo_data:

View File

@@ -0,0 +1,42 @@
{
"schemaVersion": 39,
"title": "Orchestrator Overview",
"panels": [
{
"type": "stat",
"title": "Queue Depth",
"datasource": "Prometheus",
"fieldConfig": {"defaults": {"unit": "none"}},
"targets": [{"expr": "sum(job_queue_depth)"}]
},
{
"type": "timeseries",
"title": "Queue Depth by Job Type",
"datasource": "Prometheus",
"targets": [{"expr": "job_queue_depth"}],
"fieldConfig": {"defaults": {"unit": "none"}}
},
{
"type": "timeseries",
"title": "Failures per minute",
"datasource": "Prometheus",
"targets": [{"expr": "rate(job_failures_total[5m])"}],
"fieldConfig": {"defaults": {"unit": "short"}}
},
{
"type": "timeseries",
"title": "Leases per second",
"datasource": "Prometheus",
"targets": [{"expr": "rate(lease_extensions_total[5m])"}],
"fieldConfig": {"defaults": {"unit": "ops"}}
},
{
"type": "timeseries",
"title": "Job latency p95",
"datasource": "Prometheus",
"targets": [{"expr": "histogram_quantile(0.95, sum(rate(job_latency_seconds_bucket[5m])) by (le))"}],
"fieldConfig": {"defaults": {"unit": "s"}}
}
],
"time": {"from": "now-6h", "to": "now"}
}

View File

@@ -24,3 +24,25 @@ openssl pkey -in "$KEYFILE" -pubout -out "$KEYDIR/ci-ed25519.pub" >/dev/null 2>&
STAGE=${STAGE:-$ROOT/out/mirror/thin/stage-v1} STAGE=${STAGE:-$ROOT/out/mirror/thin/stage-v1}
CREATED=${CREATED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)} CREATED=${CREATED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
SIGN_KEY="$KEYFILE" STAGE="$STAGE" CREATED="$CREATED" "$ROOT/src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh" SIGN_KEY="$KEYFILE" STAGE="$STAGE" CREATED="$CREATED" "$ROOT/src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh"
# Emit milestone summary with hashes for downstream consumers
MANIFEST_PATH="$ROOT/out/mirror/thin/mirror-thin-v1.manifest.json"
TAR_PATH="$ROOT/out/mirror/thin/mirror-thin-v1.tar.gz"
DSSE_PATH="$ROOT/out/mirror/thin/mirror-thin-v1.manifest.dsse.json"
SUMMARY_PATH="$ROOT/out/mirror/thin/milestone.json"
sha256() {
sha256sum "$1" | awk '{print $1}'
}
cat > "$SUMMARY_PATH" <<JSON
{
"created": "$CREATED",
"manifest": {"path": "$(basename "$MANIFEST_PATH")", "sha256": "$(sha256 "$MANIFEST_PATH")"},
"tarball": {"path": "$(basename "$TAR_PATH")", "sha256": "$(sha256 "$TAR_PATH")"},
"dsse": $( [[ -f "$DSSE_PATH" ]] && echo "{\"path\": \"$(basename "$DSSE_PATH")\", \"sha256\": \"$(sha256 "$DSSE_PATH")\"}" || echo "null" ),
"time_anchor": $( [[ -n "${TIME_ANCHOR_FILE:-}" && -f "$TIME_ANCHOR_FILE" ]] && echo "{\"path\": \"$(basename "$TIME_ANCHOR_FILE")\", \"sha256\": \"$(sha256 "$TIME_ANCHOR_FILE")\"}" || echo "null" )
}
JSON
echo "Milestone summary written to $SUMMARY_PATH"

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
set -euo pipefail
# Incident mode automation
# - Enables a feature-flag JSON when burn rate crosses threshold
# - Writes retention override parameters for downstream storage/ingest systems
# - Resets automatically after a cooldown period once burn subsides
# All inputs are provided via CLI flags or env vars to remain offline-friendly.
usage() {
cat <<'USAGE'
Usage: incident-mode.sh --burn-rate <float> [--threshold 2.0] [--reset-threshold 0.5] \
[--state-dir out/incident-mode] [--retention-hours 24] \
[--cooldown-mins 30] [--note "text"]
Environment overrides:
INCIDENT_STATE_DIR default: out/incident-mode
INCIDENT_THRESHOLD default: 2.0 (fast burn multiple)
INCIDENT_RESET_TH default: 0.5 (burn multiple to exit)
INCIDENT_COOLDOWN default: 30 (minutes below reset threshold)
INCIDENT_RETENTION_H default: 24 (hours)
Outputs (in state dir):
flag.json feature flag payload (enabled/disabled + metadata)
retention.json retention override (hours, applied_at)
last_burn.txt last burn rate observed
cooldown.txt consecutive minutes below reset threshold
Examples:
incident-mode.sh --burn-rate 3.1 --note "fast burn" # enter incident mode
incident-mode.sh --burn-rate 0.2 # progress cooldown / exit
USAGE
}
if [[ $# -eq 0 ]]; then usage; exit 1; fi
BURN_RATE=""
NOTE=""
STATE_DIR=${INCIDENT_STATE_DIR:-out/incident-mode}
THRESHOLD=${INCIDENT_THRESHOLD:-2.0}
RESET_TH=${INCIDENT_RESET_TH:-0.5}
COOLDOWN_MINS=${INCIDENT_COOLDOWN:-30}
RETENTION_H=${INCIDENT_RETENTION_H:-24}
while [[ $# -gt 0 ]]; do
case "$1" in
--burn-rate) BURN_RATE="$2"; shift 2;;
--threshold) THRESHOLD="$2"; shift 2;;
--reset-threshold) RESET_TH="$2"; shift 2;;
--state-dir) STATE_DIR="$2"; shift 2;;
--retention-hours) RETENTION_H="$2"; shift 2;;
--cooldown-mins) COOLDOWN_MINS="$2"; shift 2;;
--note) NOTE="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "Unknown arg: $1" >&2; usage; exit 1;;
esac
done
if [[ -z "$BURN_RATE" ]]; then echo "--burn-rate is required" >&2; exit 1; fi
mkdir -p "$STATE_DIR"
FLAG_FILE="$STATE_DIR/flag.json"
RET_FILE="$STATE_DIR/retention.json"
LAST_FILE="$STATE_DIR/last_burn.txt"
COOLDOWN_FILE="$STATE_DIR/cooldown.txt"
jq_escape() { python - <<PY "$1"
import json,sys
print(json.dumps(sys.argv[1]))
PY
}
now_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
burn_float=$(python - <<PY "$BURN_RATE"
import sys
print(float(sys.argv[1]))
PY)
cooldown_current=0
if [[ -f "$COOLDOWN_FILE" ]]; then
cooldown_current=$(cat "$COOLDOWN_FILE")
fi
enter_incident=false
exit_incident=false
if (( $(echo "$burn_float >= $THRESHOLD" | bc -l) )); then
enter_incident=true
cooldown_current=0
elif (( $(echo "$burn_float <= $RESET_TH" | bc -l) )); then
cooldown_current=$((cooldown_current + 1))
if (( cooldown_current >= COOLDOWN_MINS )); then
exit_incident=true
fi
else
cooldown_current=0
fi
echo "$burn_float" > "$LAST_FILE"
echo "$cooldown_current" > "$COOLDOWN_FILE"
write_flag() {
local enabled="$1"
cat > "$FLAG_FILE" <<JSON
{
"enabled": $enabled,
"updated_at": "$now_utc",
"reason": "incident-mode",
"note": $(jq_escape "$NOTE"),
"burn_rate": $burn_float
}
JSON
}
if $enter_incident; then
write_flag true
cat > "$RET_FILE" <<JSON
{
"retention_hours": $RETENTION_H,
"applied_at": "$now_utc"
}
JSON
echo "incident-mode: activated (burn_rate=$burn_float)" >&2
elif $exit_incident; then
write_flag false
echo "incident-mode: cleared after cooldown (burn_rate=$burn_float)" >&2
else
# no change; preserve prior flag if exists
if [[ ! -f "$FLAG_FILE" ]]; then
write_flag false
fi
echo "incident-mode: steady (burn_rate=$burn_float, cooldown=$cooldown_current/$COOLDOWN_MINS)" >&2
fi
exit 0

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
COMPOSE_FILE="${COMPOSE_FILE:-$ROOT/devops/orchestrator/docker-compose.orchestrator.yml}"
STATE_DIR="${STATE_DIR:-$ROOT/out/orchestrator-smoke}"
usage() {
cat <<'USAGE'
Orchestrator infra smoke test
- Starts postgres + mongo + nats via docker-compose
- Verifies basic connectivity and prints ready endpoints
Env/flags:
COMPOSE_FILE path to compose file (default: ops/devops/orchestrator/docker-compose.orchestrator.yml)
STATE_DIR path for logs (default: out/orchestrator-smoke)
SKIP_UP set to 1 to skip compose up (assumes already running)
USAGE
}
if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then usage; exit 0; fi
mkdir -p "$STATE_DIR"
if [[ "${SKIP_UP:-0}" != "1" ]]; then
docker compose -f "$COMPOSE_FILE" up -d
fi
log() { echo "[smoke] $*"; }
log "waiting for postgres..."
for i in {1..12}; do
if docker compose -f "$COMPOSE_FILE" exec -T orchestrator-postgres pg_isready -U orch >/dev/null 2>&1; then break; fi
sleep 5;
done
log "waiting for mongo..."
for i in {1..12}; do
if docker compose -f "$COMPOSE_FILE" exec -T orchestrator-mongo mongosh --quiet --eval "db.adminCommand('ping')" >/dev/null 2>&1; then break; fi
sleep 5;
done
log "waiting for nats..."
for i in {1..12}; do
if docker compose -f "$COMPOSE_FILE" exec -T orchestrator-nats nats --server localhost:4222 ping >/dev/null 2>&1; then break; fi
sleep 5;
done
log "postgres DSN: postgres://orch:orchpass@localhost:55432/orchestrator"
log "mongo uri: mongodb://localhost:57017"
log "nats uri: nats://localhost:4222"
# Write readiness summary
cat > "$STATE_DIR/readiness.txt" <<EOF
postgres=postgres://orch:orchpass@localhost:55432/orchestrator
mongo=mongodb://localhost:57017
nats=nats://localhost:4222
ready_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
log "smoke completed; summary at $STATE_DIR/readiness.txt"

View File

@@ -6,3 +6,4 @@
| `CLI-AIAI-31-001` | DONE (2025-11-24) | `stella advise summarize` command implemented; CLI analyzer build & tests now pass locally. | | `CLI-AIAI-31-001` | DONE (2025-11-24) | `stella advise summarize` command implemented; CLI analyzer build & tests now pass locally. |
| `CLI-AIAI-31-002` | DONE (2025-11-24) | `stella advise explain` (conflict narrative) command implemented and tested. | | `CLI-AIAI-31-002` | DONE (2025-11-24) | `stella advise explain` (conflict narrative) command implemented and tested. |
| `CLI-AIAI-31-003` | DONE (2025-11-24) | `stella advise remediate` command implemented and tested. | | `CLI-AIAI-31-003` | DONE (2025-11-24) | `stella advise remediate` command implemented and tested. |
| `CLI-AIAI-31-004` | DONE (2025-11-24) | `stella advise batch` supports multi-key runs, per-key outputs, summary table, and tests (`HandleAdviseBatchAsync_RunsAllAdvisories`). |

View File

@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Concelier.WebService.AirGap;
/// <summary>
/// Minimal, deterministic writer for Concelier air-gap bundles. Intended as the
/// first increment for CONCELIER-AIRGAP-56-001; produces a stable NDJSON file
/// from link-not-merge cache items without external dependencies.
/// </summary>
public sealed class AirgapBundleBuilder
{
private const string BundleFileName = "concelier-airgap.ndjson";
private const string ManifestFileName = "bundle.manifest.json";
private const string EntryTraceFileName = "bundle.entry-trace.json";
public async Task<AirgapBundleResult> BuildAsync(
IEnumerable<string> cacheItems,
string outputDirectory,
DateTimeOffset? createdUtc = null,
CancellationToken cancellationToken = default)
{
if (cacheItems is null) throw new ArgumentNullException(nameof(cacheItems));
if (string.IsNullOrWhiteSpace(outputDirectory)) throw new ArgumentException("Output directory is required", nameof(outputDirectory));
Directory.CreateDirectory(outputDirectory);
var ordered = cacheItems
.Where(item => !string.IsNullOrWhiteSpace(item))
.Select(item => item.Trim())
.OrderBy(item => item, StringComparer.Ordinal)
.ToArray();
var bundlePath = Path.Combine(outputDirectory, BundleFileName);
await WriteNdjsonAsync(bundlePath, ordered, cancellationToken).ConfigureAwait(false);
var bundleSha = ComputeSha256FromPath(bundlePath);
var entries = ordered
.Select((value, index) => new AirgapBundleEntry
{
LineNumber = index + 1,
Sha256 = ComputeSha256(value)
})
.ToArray();
var manifestCreated = createdUtc ?? DateTimeOffset.UnixEpoch;
var manifest = new AirgapBundleManifest
{
Items = ordered,
Entries = entries,
BundleSha256 = bundleSha,
CreatedUtc = manifestCreated,
Count = ordered.Length
};
var manifestPath = Path.Combine(outputDirectory, ManifestFileName);
await WriteManifest(manifestPath, manifest, cancellationToken).ConfigureAwait(false);
var entryTracePath = Path.Combine(outputDirectory, EntryTraceFileName);
await WriteEntryTrace(entryTracePath, entries, cancellationToken).ConfigureAwait(false);
return new AirgapBundleResult(bundlePath, manifestPath, entryTracePath, bundleSha, ordered.Length);
}
private static async Task WriteNdjsonAsync(string bundlePath, IReadOnlyList<string> orderedItems, CancellationToken cancellationToken)
{
await using var stream = new FileStream(bundlePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true);
await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
foreach (var item in orderedItems)
{
cancellationToken.ThrowIfCancellationRequested();
await writer.WriteLineAsync(item).ConfigureAwait(false);
}
}
private static async Task WriteManifest(string manifestPath, AirgapBundleManifest manifest, CancellationToken cancellationToken)
{
await using var stream = new FileStream(manifestPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true);
await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
var payload = System.Text.Json.JsonSerializer.Serialize(manifest, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
WriteIndented = false
});
cancellationToken.ThrowIfCancellationRequested();
await writer.WriteAsync(payload.AsMemory(), cancellationToken).ConfigureAwait(false);
}
private static async Task WriteEntryTrace(string entryTracePath, IReadOnlyList<AirgapBundleEntry> entries, CancellationToken cancellationToken)
{
await using var stream = new FileStream(entryTracePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true);
await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
var payload = System.Text.Json.JsonSerializer.Serialize(entries, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
WriteIndented = false
});
cancellationToken.ThrowIfCancellationRequested();
await writer.WriteAsync(payload.AsMemory(), cancellationToken).ConfigureAwait(false);
}
private static string ComputeSha256FromPath(string path)
{
using var sha = SHA256.Create();
using var stream = File.OpenRead(path);
var hashBytes = sha.ComputeHash(stream);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
private static string ComputeSha256(string content)
{
using var sha = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(content);
var hashBytes = sha.ComputeHash(bytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
}
public sealed record AirgapBundleResult(string BundlePath, string ManifestPath, string EntryTracePath, string Sha256, int ItemCount);
public sealed record AirgapBundleManifest
{
[JsonPropertyName("items")]
public string[] Items { get; init; } = Array.Empty<string>();
[JsonPropertyName("entries")]
public AirgapBundleEntry[] Entries { get; init; } = Array.Empty<AirgapBundleEntry>();
[JsonPropertyName("bundleSha256")]
public string BundleSha256 { get; init; } = string.Empty;
[JsonPropertyName("createdUtc")]
public DateTimeOffset CreatedUtc { get; init; }
[JsonPropertyName("count")]
public int Count { get; init; }
}
public sealed record AirgapBundleEntry
{
[JsonPropertyName("lineNumber")]
public int LineNumber { get; init; }
[JsonPropertyName("sha256")]
public string Sha256 { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Concelier.WebService.AirGap;
public sealed class AirgapBundleValidator
{
public async Task<AirgapBundleValidationResult> ValidateAsync(
string bundlePath,
string manifestPath,
string? entryTracePath = null,
CancellationToken cancellationToken = default)
{
var errors = new List<string>();
if (!File.Exists(bundlePath))
{
errors.Add($"Bundle file missing: {bundlePath}");
return new AirgapBundleValidationResult(false, errors);
}
if (!File.Exists(manifestPath))
{
errors.Add($"Manifest file missing: {manifestPath}");
return new AirgapBundleValidationResult(false, errors);
}
AirgapBundleManifest? manifest = null;
try
{
var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken).ConfigureAwait(false);
manifest = JsonSerializer.Deserialize<AirgapBundleManifest>(manifestJson);
}
catch (Exception ex)
{
errors.Add($"Manifest parse error: {ex.Message}");
}
var lines = await File.ReadAllLinesAsync(bundlePath, cancellationToken).ConfigureAwait(false);
var bundleSha = ComputeSha256FromFile(bundlePath);
if (manifest is null)
{
return new AirgapBundleValidationResult(false, errors);
}
if (!string.Equals(bundleSha, manifest.BundleSha256, StringComparison.OrdinalIgnoreCase))
{
errors.Add("Bundle hash mismatch");
}
if (manifest.Count != lines.Length)
{
errors.Add($"Manifest count {manifest.Count} != bundle lines {lines.Length}");
}
var ordered = lines.ToArray();
if (!manifest.Items.SequenceEqual(ordered))
{
errors.Add("Manifest items differ from bundle payload");
}
// If entry trace exists (either provided or embedded in manifest), verify per-line hashes.
AirgapBundleEntry[] entries = manifest.Entries ?? Array.Empty<AirgapBundleEntry>();
if (!string.IsNullOrWhiteSpace(entryTracePath) && File.Exists(entryTracePath))
{
try
{
var traceJson = await File.ReadAllTextAsync(entryTracePath!, cancellationToken).ConfigureAwait(false);
var traceEntries = JsonSerializer.Deserialize<AirgapBundleEntry[]>(traceJson);
if (traceEntries is not null)
{
entries = traceEntries;
}
}
catch (Exception ex)
{
errors.Add($"Entry trace parse error: {ex.Message}");
}
}
if (entries.Length > 0)
{
if (entries.Length != lines.Length)
{
errors.Add($"Entry trace length {entries.Length} != bundle lines {lines.Length}");
}
else
{
for (var i = 0; i < lines.Length; i++)
{
var expectedHash = ComputeSha256(lines[i]);
if (!string.Equals(entries[i].Sha256, expectedHash, StringComparison.OrdinalIgnoreCase))
{
errors.Add($"Entry trace hash mismatch at line {i + 1}");
break;
}
}
}
}
return new AirgapBundleValidationResult(errors.Count == 0, errors);
}
private static string ComputeSha256(string content)
{
using var sha = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(content);
var hashBytes = sha.ComputeHash(bytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
private static string ComputeSha256FromFile(string path)
{
using var sha = SHA256.Create();
using var stream = File.OpenRead(path);
var hashBytes = sha.ComputeHash(stream);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
}
public sealed record AirgapBundleValidationResult(bool IsValid, IReadOnlyList<string> Errors);

View File

@@ -0,0 +1,22 @@
namespace StellaOps.Concelier.WebService;
public sealed record VerifyAttestationRequest(
string? BundlePath,
string? ManifestPath,
string? TransparencyPath,
string? PipelineVersion);
public readonly record struct EvidencePathResolutionResult(
bool IsValid,
string? BundlePath,
string? ManifestPath,
string? TransparencyPath,
string? Error,
string? ErrorDetails)
{
public static EvidencePathResolutionResult Valid(string bundlePath, string manifestPath, string? transparencyPath) =>
new(true, bundlePath, manifestPath, transparencyPath, null, null);
public static EvidencePathResolutionResult Invalid(string error, string? details = null) =>
new(false, null, null, null, error, details);
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Models.Observations;
namespace StellaOps.Concelier.WebService.Contracts;
public sealed record EvidenceBatchRequest(
IReadOnlyCollection<EvidenceBatchItemRequest> Items,
int? ObservationLimit,
int? LinksetLimit);
public sealed record EvidenceBatchItemRequest(
string? ComponentId,
IReadOnlyCollection<string>? Purls,
IReadOnlyCollection<string>? Aliases);
public sealed record EvidenceBatchItemResponse(
string ComponentId,
IReadOnlyCollection<AdvisoryObservation> Observations,
IReadOnlyCollection<AdvisoryLinkset> Linksets,
bool HasMore,
DateTimeOffset RetrievedAt);
public sealed record EvidenceBatchResponse(
IReadOnlyCollection<EvidenceBatchItemResponse> Items);

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace StellaOps.Concelier.WebService;
public sealed record EvidenceSnapshotResponse(
[property: JsonPropertyName("advisoryKey")] string AdvisoryKey,
[property: JsonPropertyName("tenant")] string Tenant,
[property: JsonPropertyName("manifestPath")] string ManifestPath,
[property: JsonPropertyName("manifestHash")] string ManifestHash,
[property: JsonPropertyName("transparencyPath")] string? TransparencyPath,
[property: JsonPropertyName("pipelineVersion")] string? PipelineVersion);
public sealed record AttestationStatusResponse(
[property: JsonPropertyName("advisoryKey")] string AdvisoryKey,
[property: JsonPropertyName("tenant")] string Tenant,
[property: JsonPropertyName("claims")] AttestationClaims Claims,
[property: JsonPropertyName("bundlePath")] string BundlePath,
[property: JsonPropertyName("manifestPath")] string ManifestPath,
[property: JsonPropertyName("transparencyPath")] string? TransparencyPath,
[property: JsonPropertyName("pipelineVersion")] string? PipelineVersion);

View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace StellaOps.Concelier.WebService;
public sealed record IncidentUpsertRequest(
[property: JsonPropertyName("reason")] string? Reason,
[property: JsonPropertyName("cooldownMinutes")] int? CooldownMinutes);
public sealed record IncidentStatusResponse(
[property: JsonPropertyName("advisoryKey")] string AdvisoryKey,
[property: JsonPropertyName("tenant")] string Tenant,
[property: JsonPropertyName("reason")] string Reason,
[property: JsonPropertyName("activatedAt")] string ActivatedAt,
[property: JsonPropertyName("cooldownUntil")] string CooldownUntil,
[property: JsonPropertyName("pipelineVersion")] string? PipelineVersion,
[property: JsonPropertyName("active")] bool Active);

View File

@@ -2,9 +2,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
@@ -32,6 +36,7 @@ using Serilog;
using StellaOps.Concelier.Merge; using StellaOps.Concelier.Merge;
using StellaOps.Concelier.Merge.Services; using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.WebService.Extensions; using StellaOps.Concelier.WebService.Extensions;
using StellaOps.Concelier.WebService.Services;
using StellaOps.Concelier.WebService.Jobs; using StellaOps.Concelier.WebService.Jobs;
using StellaOps.Concelier.WebService.Options; using StellaOps.Concelier.WebService.Options;
using StellaOps.Concelier.WebService.Filters; using StellaOps.Concelier.WebService.Filters;
@@ -59,16 +64,42 @@ using StellaOps.Concelier.Core.Attestation;
using StellaOps.Concelier.Storage.Mongo.Orchestrator; using StellaOps.Concelier.Storage.Mongo.Orchestrator;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Diagnostics.Metrics; using System.Diagnostics.Metrics;
using StellaOps.Concelier.WebService.Contracts; using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.WebService.Telemetry;
namespace StellaOps.Concelier.WebService
{
public partial class Program
{
private const string JobsPolicyName = "Concelier.Jobs.Trigger";
private const string ObservationsPolicyName = "Concelier.Observations.Read";
private const string AdvisoryIngestPolicyName = "Concelier.Advisories.Ingest";
private const string AdvisoryReadPolicyName = "Concelier.Advisories.Read";
private const string AocVerifyPolicyName = "Concelier.Aoc.Verify";
public const string TenantHeaderName = "X-Stella-Tenant";
public static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
const string JobsPolicyName = "Concelier.Jobs.Trigger"; // For test/CI runs, allow injecting a minimal config before options bind.
const string ObservationsPolicyName = "Concelier.Observations.Read"; #pragma warning disable ASP0013 // permitted here for test-only override path
const string AdvisoryIngestPolicyName = "Concelier.Advisories.Ingest"; builder.Host.ConfigureAppConfiguration((context, cfg) =>
const string AdvisoryReadPolicyName = "Concelier.Advisories.Read"; {
const string AocVerifyPolicyName = "Concelier.Aoc.Verify"; if (context.HostingEnvironment.IsEnvironment("Testing"))
const string TenantHeaderName = "X-Stella-Tenant"; {
cfg.AddInMemoryCollection(new Dictionary<string, string?>
{
{"Concelier:Storage:Dsn", Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? "mongodb://localhost:27017/test-health"},
{"Concelier:Storage:Driver", "mongo"},
{"Concelier:Storage:CommandTimeoutSeconds", "30"},
{"Concelier:Telemetry:Enabled", "false"}
});
}
});
#pragma warning restore ASP0013
var JsonOptions = CreateJsonOptions();
builder.Configuration.AddStellaOpsDefaults(options => builder.Configuration.AddStellaOpsDefaults(options =>
{ {
@@ -82,19 +113,54 @@ builder.Configuration.AddStellaOpsDefaults(options =>
var contentRootPath = builder.Environment.ContentRootPath; var contentRootPath = builder.Environment.ContentRootPath;
var concelierOptions = builder.Configuration.BindOptions<ConcelierOptions>(postConfigure: (opts, _) => // For Testing we allow pre-bound options injected via DI to override BindOptions.
ConcelierOptions concelierOptions;
if (builder.Environment.IsEnvironment("Testing"))
{ {
ConcelierOptionsPostConfigure.Apply(opts, contentRootPath); // Allow a fully pre-bound options instance to be supplied by the test host.
ConcelierOptionsValidator.Validate(opts); #pragma warning disable ASP0000 // test-only: create provider to fetch pre-bound options
}); using var tempProvider = builder.Services.BuildServiceProvider();
builder.Services.AddOptions<ConcelierOptions>() #pragma warning restore ASP0000
.Bind(builder.Configuration) concelierOptions = tempProvider.GetService<IOptions<ConcelierOptions>>()?.Value ?? new ConcelierOptions
.PostConfigure(options =>
{ {
ConcelierOptionsPostConfigure.Apply(options, contentRootPath); Storage = new ConcelierOptions.StorageOptions
ConcelierOptionsValidator.Validate(options); {
}) Dsn = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? "mongodb://localhost:27017/test-health",
.ValidateOnStart(); Driver = "mongo",
CommandTimeoutSeconds = 30
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
}
};
ConcelierOptionsPostConfigure.Apply(concelierOptions, contentRootPath);
// Skip validation in Testing to allow factory-provided wiring.
}
else
{
concelierOptions = builder.Configuration.BindOptions<ConcelierOptions>(postConfigure: (opts, _) =>
{
var testDsn = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN");
if (string.IsNullOrWhiteSpace(opts.Storage.Dsn) && !string.IsNullOrWhiteSpace(testDsn))
{
opts.Storage.Dsn = testDsn;
}
ConcelierOptionsPostConfigure.Apply(opts, contentRootPath);
var skipValidation = string.Equals(Environment.GetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION"), "1", StringComparison.OrdinalIgnoreCase);
if (!skipValidation)
{
ConcelierOptionsValidator.Validate(opts);
}
});
}
// Register the chosen options instance so downstream services/tests share it.
builder.Services.AddSingleton(concelierOptions);
builder.Services.AddSingleton<IOptions<ConcelierOptions>>(_ => Microsoft.Extensions.Options.Options.Create(concelierOptions));
builder.Services.AddStellaOpsCrypto(concelierOptions.Crypto); builder.Services.AddStellaOpsCrypto(concelierOptions.Crypto);
@@ -593,32 +659,31 @@ var observationsEndpoint = app.MapGet("/concelier/observations", async (
limit, limit,
cursor); cursor);
var stopwatch = Stopwatch.StartNew();
AdvisoryObservationQueryResult result; AdvisoryObservationQueryResult result;
try try
{ {
result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false); result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false);
} }
catch (FormatException ex) catch (FormatException ex)
{
return Results.BadRequest(ex.Message);
}
IngestObservability.IngestLatencySeconds.Record(result.Duration.TotalSeconds, new TagList
{
{"tenant", normalizedTenant},
{"source", result.Source ?? string.Empty},
{"stage", "ingest"}
});
if (!result.Success && !string.IsNullOrWhiteSpace(result.ErrorCode))
{ {
IngestObservability.IngestErrorsTotal.Add(1, new TagList IngestObservability.IngestErrorsTotal.Add(1, new TagList
{ {
{"tenant", normalizedTenant}, {"tenant", normalizedTenant},
{"source", result.Source ?? string.Empty}, {"source", "mixed"},
{"reason", result.ErrorCode} {"reason", "format"},
{"stage", "ingest"}
}); });
return Results.BadRequest(ex.Message);
} }
var elapsed = stopwatch.Elapsed;
IngestObservability.IngestLatencySeconds.Record(elapsed.TotalSeconds, new TagList
{
{"tenant", normalizedTenant},
{"source", "mixed"},
{"stage", "ingest"}
});
var response = new AdvisoryObservationQueryResponse( var response = new AdvisoryObservationQueryResponse(
result.Observations, result.Observations,
new AdvisoryObservationLinksetAggregateResponse( new AdvisoryObservationLinksetAggregateResponse(
@@ -647,6 +712,7 @@ app.MapGet("/v1/lnm/linksets", async (
[FromQuery(Name = "pageSize")] int? pageSize, [FromQuery(Name = "pageSize")] int? pageSize,
[FromQuery(Name = "includeConflicts")] bool? includeConflicts, [FromQuery(Name = "includeConflicts")] bool? includeConflicts,
[FromServices] IAdvisoryLinksetQueryService queryService, [FromServices] IAdvisoryLinksetQueryService queryService,
[FromServices] IAdvisoryObservationQueryService observationQueryService,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
{ {
ApplyNoCache(context.Response); ApplyNoCache(context.Response);
@@ -677,9 +743,12 @@ app.MapGet("/v1/lnm/linksets", async (
resolvedPageSize, resolvedPageSize,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
var items = result.Items var items = new List<LnmLinksetResponse>(result.Items.Length);
.Select(linkset => ToLnmResponse(linkset, includeConflicts.GetValueOrDefault(true), includeTimeline: false, includeObservations: false)) foreach (var linkset in result.Items)
.ToArray(); {
var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
items.Add(ToLnmResponse(linkset, includeConflicts.GetValueOrDefault(true), includeTimeline: false, includeObservations: false, summary));
}
return Results.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total)); return Results.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
}).WithName("ListLnmLinksets"); }).WithName("ListLnmLinksets");
@@ -688,6 +757,7 @@ app.MapPost("/v1/lnm/linksets/search", async (
HttpContext context, HttpContext context,
[FromBody] LnmLinksetSearchRequest request, [FromBody] LnmLinksetSearchRequest request,
[FromServices] IAdvisoryLinksetQueryService queryService, [FromServices] IAdvisoryLinksetQueryService queryService,
[FromServices] IAdvisoryObservationQueryService observationQueryService,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
{ {
ApplyNoCache(context.Response); ApplyNoCache(context.Response);
@@ -718,13 +788,17 @@ app.MapPost("/v1/lnm/linksets/search", async (
resolvedPageSize, resolvedPageSize,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
var items = result.Items var items = new List<LnmLinksetResponse>(result.Items.Length);
.Select(linkset => ToLnmResponse( foreach (var linkset in result.Items)
{
var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
items.Add(ToLnmResponse(
linkset, linkset,
includeConflicts: true, includeConflicts: true,
includeTimeline: request.IncludeTimeline, includeTimeline: request.IncludeTimeline,
includeObservations: request.IncludeObservations)) includeObservations: request.IncludeObservations,
.ToArray(); summary));
}
return Results.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total)); return Results.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
}).WithName("SearchLnmLinksets"); }).WithName("SearchLnmLinksets");
@@ -734,6 +808,7 @@ app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
string advisoryId, string advisoryId,
[FromQuery(Name = "source")] string? source, [FromQuery(Name = "source")] string? source,
[FromServices] IAdvisoryLinksetQueryService queryService, [FromServices] IAdvisoryLinksetQueryService queryService,
[FromServices] IAdvisoryObservationQueryService observationQueryService,
[FromServices] LinksetCacheTelemetry telemetry, [FromServices] LinksetCacheTelemetry telemetry,
CancellationToken cancellationToken, CancellationToken cancellationToken,
[FromQuery(Name = "includeConflicts")] bool includeConflicts = true, [FromQuery(Name = "includeConflicts")] bool includeConflicts = true,
@@ -771,7 +846,8 @@ app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
} }
var linkset = result.Linksets[0]; var linkset = result.Linksets[0];
var response = ToLnmResponse(linkset, includeConflicts, includeTimeline: false, includeObservations: includeObservations); var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
var response = ToLnmResponse(linkset, includeConflicts, includeTimeline: false, includeObservations: includeObservations, summary);
telemetry.RecordHit(tenant, linkset.Source); telemetry.RecordHit(tenant, linkset.Source);
telemetry.RecordRebuild(tenant, linkset.Source, stopwatch.Elapsed.TotalMilliseconds); telemetry.RecordRebuild(tenant, linkset.Source, stopwatch.Elapsed.TotalMilliseconds);
@@ -1193,6 +1269,252 @@ if (authorityConfigured)
advisoryEvidenceEndpoint.RequireAuthorization(AdvisoryReadPolicyName); advisoryEvidenceEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
} }
var attestationVerifyEndpoint = app.MapPost("/internal/attestations/verify", async (
VerifyAttestationRequest request,
HttpContext context,
[FromServices] EvidenceBundleAttestationBuilder attestationBuilder,
[FromServices] IOptions<ConcelierOptions> concelierOptions,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (request is null)
{
return Problem(context, "Request body required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide bundle/manifest paths.");
}
var evidenceOptions = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
var resolved = ResolveEvidencePaths(request, evidenceOptions.RootAbsolute, evidenceOptions);
if (!resolved.IsValid)
{
return Problem(context, resolved.Error!, StatusCodes.Status400BadRequest, ProblemTypes.Validation, resolved.ErrorDetails ?? string.Empty);
}
try
{
var claims = await attestationBuilder.BuildAsync(
new EvidenceBundleAttestationRequest(
resolved.BundlePath!,
resolved.ManifestPath!,
resolved.TransparencyPath,
request.PipelineVersion ?? evidenceOptions.PipelineVersion ?? "git:unknown"),
cancellationToken).ConfigureAwait(false);
return Results.Json(claims);
}
catch (Exception ex)
{
return Problem(context, "Attestation verification failed", StatusCodes.Status400BadRequest, ProblemTypes.Validation, ex.Message);
}
});
if (authorityConfigured)
{
attestationVerifyEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
// Evidence snapshot (manifest-only) endpoint for Console/VEX consumers
var evidenceSnapshotEndpoint = app.MapGet("/obs/evidence/advisories/{advisoryKey}", async (
string advisoryKey,
HttpContext context,
[FromServices] IOptions<ConcelierOptions> concelierOptions,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(advisoryKey))
{
return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier.");
}
var options = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
var baseDir = Path.Combine(options.RootAbsolute ?? options.Root ?? string.Empty, tenant, advisoryKey.Trim());
var manifestPath = Path.Combine(baseDir, options.DefaultManifestFileName ?? "manifest.json");
var transparencyPath = Path.Combine(baseDir, options.DefaultTransparencyFileName ?? "transparency.json");
if (!File.Exists(manifestPath))
{
return Problem(context, "Manifest not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No manifest for {advisoryKey} in tenant {tenant}.");
}
await using var manifestStream = File.OpenRead(manifestPath);
var hash = await ComputeSha256Async(manifestStream, cancellationToken).ConfigureAwait(false);
var response = new EvidenceSnapshotResponse(
advisoryKey: advisoryKey.Trim(),
Tenant: tenant,
ManifestPath: manifestPath,
ManifestHash: hash,
TransparencyPath: File.Exists(transparencyPath) ? transparencyPath : null,
PipelineVersion: options.PipelineVersion);
return Results.Json(response);
});
if (authorityConfigured)
{
evidenceSnapshotEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
// Attestation status endpoint (evidence locker proxy)
var evidenceAttestationEndpoint = app.MapGet("/obs/attestations/advisories/{advisoryKey}", async (
string advisoryKey,
HttpContext context,
[FromServices] IOptions<ConcelierOptions> concelierOptions,
[FromServices] EvidenceBundleAttestationBuilder attestationBuilder,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(advisoryKey))
{
return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier.");
}
var options = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
var baseDir = Path.Combine(options.RootAbsolute ?? options.Root ?? string.Empty, tenant, advisoryKey.Trim());
if (!Directory.Exists(baseDir))
{
return Problem(context, "Evidence directory not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No evidence for {advisoryKey} in tenant {tenant}.");
}
var bundlePath = Directory.EnumerateFiles(baseDir, "*.tar*", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (bundlePath is null)
{
return Problem(context, "Bundle missing", StatusCodes.Status404NotFound, ProblemTypes.NotFound, "No bundle archive found in evidence directory.");
}
var manifestPath = Path.Combine(baseDir, options.DefaultManifestFileName ?? "manifest.json");
var transparencyPath = Path.Combine(baseDir, options.DefaultTransparencyFileName ?? "transparency.json");
if (!File.Exists(manifestPath))
{
return Problem(context, "Manifest missing", StatusCodes.Status404NotFound, ProblemTypes.NotFound, "Manifest required to build attestation claims.");
}
var claims = await attestationBuilder.BuildAsync(
new EvidenceBundleAttestationRequest(
bundlePath,
manifestPath,
File.Exists(transparencyPath) ? transparencyPath : null,
options.PipelineVersion ?? "git:unknown"),
cancellationToken).ConfigureAwait(false);
var response = new AttestationStatusResponse(
AdvisoryKey: advisoryKey.Trim(),
Tenant: tenant,
Claims: claims,
BundlePath: bundlePath,
ManifestPath: manifestPath,
TransparencyPath: File.Exists(transparencyPath) ? transparencyPath : null,
PipelineVersion: options.PipelineVersion);
return Results.Json(response);
});
if (authorityConfigured)
{
evidenceAttestationEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
// Incident-mode (ingest pause) endpoints
var incidentGetEndpoint = app.MapGet("/obs/incidents/advisories/{advisoryKey}", async (
string advisoryKey,
HttpContext context,
[FromServices] IOptions<ConcelierOptions> concelierOptions,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var evidenceOptions = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
var status = await IncidentFileStore.ReadAsync(evidenceOptions, tenant!, advisoryKey, timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
if (status is null)
{
return Problem(context, "Incident not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, "No incident marker present.");
}
return Results.Json(status);
});
if (authorityConfigured)
{
incidentGetEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
var incidentUpsertEndpoint = app.MapPost("/obs/incidents/advisories/{advisoryKey}", async (
string advisoryKey,
IncidentUpsertRequest request,
HttpContext context,
[FromServices] IOptions<ConcelierOptions> concelierOptions,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
if (request is null)
{
return Problem(context, "Request body required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide reason/cooldownMinutes.");
}
var cooldownMinutes = request.CooldownMinutes is null or <= 0 ? 60 : request.CooldownMinutes.Value;
var evidenceOptions = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
await IncidentFileStore.WriteAsync(
evidenceOptions,
tenant!,
advisoryKey,
request.Reason ?? "unspecified",
cooldownMinutes,
evidenceOptions.PipelineVersion,
timeProvider.GetUtcNow(),
cancellationToken).ConfigureAwait(false);
var status = await IncidentFileStore.ReadAsync(evidenceOptions, tenant!, advisoryKey, timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
return Results.Json(status);
});
if (authorityConfigured)
{
incidentUpsertEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
var incidentDeleteEndpoint = app.MapDelete("/obs/incidents/advisories/{advisoryKey}", async (
string advisoryKey,
HttpContext context,
[FromServices] IOptions<ConcelierOptions> concelierOptions,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var evidenceOptions = concelierOptions.Value.Evidence ?? new ConcelierOptions.EvidenceBundleOptions();
await IncidentFileStore.DeleteAsync(evidenceOptions, tenant!, advisoryKey, cancellationToken).ConfigureAwait(false);
return Results.NoContent();
});
if (authorityConfigured)
{
incidentDeleteEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", async ( var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", async (
string advisoryKey, string advisoryKey,
HttpContext context, HttpContext context,
@@ -1424,6 +1746,74 @@ var advisorySummaryEndpoint = app.MapGet("/advisories/summary", async (
return Results.Ok(response); return Results.Ok(response);
}).WithName("GetAdvisoriesSummary"); }).WithName("GetAdvisoriesSummary");
// Evidence batch (component-centric) endpoint for graph overlays / evidence exports.
app.MapPost("/v1/evidence/batch", async (
HttpContext context,
[FromBody] EvidenceBatchRequest request,
[FromServices] IAdvisoryObservationQueryService observationService,
[FromServices] IAdvisoryLinksetQueryService linksetService,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
if (request?.Items is null || request.Items.Count == 0)
{
return Problem(context, "At least one batch item is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide items with aliases/purls.");
}
var resolvedObservationLimit = request.ObservationLimit is > 0 and <= 200 ? request.ObservationLimit.Value : 50;
var resolvedLinksetLimit = request.LinksetLimit is > 0 and <= 200 ? request.LinksetLimit.Value : 50;
var responses = new List<EvidenceBatchItemResponse>(request.Items.Count);
foreach (var item in request.Items)
{
var componentId = string.IsNullOrWhiteSpace(item.ComponentId) ? "(unnamed)" : item.ComponentId.Trim();
var aliases = item.Aliases?.Where(a => !string.IsNullOrWhiteSpace(a)).Select(a => a.Trim()).ToArray();
var purls = item.Purls?.Where(p => !string.IsNullOrWhiteSpace(p)).Select(p => p.Trim()).ToArray();
AdvisoryObservationQueryResult observationResult = new(
ImmutableArray<AdvisoryObservation>.Empty,
new AdvisoryObservationLinksetAggregate(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<AdvisoryObservationReference>.Empty),
NextCursor: null,
HasMore: false);
AdvisoryLinksetQueryResult linksetResult = new(
ImmutableArray<AdvisoryLinkset>.Empty,
NextCursor: null,
HasMore: false);
if ((aliases?.Length ?? 0) > 0 || (purls?.Length ?? 0) > 0)
{
var obsOptions = new AdvisoryObservationQueryOptions(tenant, aliases: aliases, purls: purls, limit: resolvedObservationLimit);
observationResult = await observationService.QueryAsync(obsOptions, cancellationToken).ConfigureAwait(false);
var linksetOptions = new AdvisoryLinksetQueryOptions(tenant, aliases, null, resolvedLinksetLimit);
linksetResult = await linksetService.QueryAsync(linksetOptions, cancellationToken).ConfigureAwait(false);
}
var responseItem = new EvidenceBatchItemResponse(
componentId,
observationResult.Observations,
linksetResult.Linksets,
observationResult.HasMore || linksetResult.HasMore,
timeProvider.GetUtcNow());
responses.Add(responseItem);
}
return Results.Ok(new EvidenceBatchResponse(responses));
}).WithName("GetEvidenceBatch");
if (authorityConfigured) if (authorityConfigured)
{ {
advisorySummaryEndpoint.RequireAuthorization(AdvisoryReadPolicyName); advisorySummaryEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
@@ -1749,12 +2139,13 @@ LnmLinksetResponse ToLnmResponse(
AdvisoryLinkset linkset, AdvisoryLinkset linkset,
bool includeConflicts, bool includeConflicts,
bool includeTimeline, bool includeTimeline,
bool includeObservations) bool includeObservations,
LinksetObservationSummary summary)
{ {
var normalized = linkset.Normalized; var normalized = linkset.Normalized;
var severity = normalized?.Severities?.FirstOrDefault() is { } severityDict var severity = summary.Severity ?? (normalized?.Severities?.FirstOrDefault() is { } severityDict
? ExtractSeverity(severityDict) ? ExtractSeverity(severityDict)
: null; : null);
var conflicts = includeConflicts var conflicts = includeConflicts
? (linkset.Conflicts ?? Array.Empty<AdvisoryLinksetConflict>()).Select(c => ? (linkset.Conflicts ?? Array.Empty<AdvisoryLinksetConflict>()).Select(c =>
new LnmLinksetConflict( new LnmLinksetConflict(
@@ -1767,13 +2158,7 @@ LnmLinksetResponse ToLnmResponse(
: Array.Empty<LnmLinksetConflict>(); : Array.Empty<LnmLinksetConflict>();
var timeline = includeTimeline var timeline = includeTimeline
? new[] ? BuildTimeline(linkset, summary)
{
new LnmLinksetTimeline(
Event: "created",
At: linkset.CreatedAt,
EvidenceHash: linkset.Provenance?.ObservationHashes?.FirstOrDefault())
}
: Array.Empty<LnmLinksetTimeline>(); : Array.Empty<LnmLinksetTimeline>();
var provenance = linkset.Provenance is null var provenance = linkset.Provenance is null
@@ -1800,8 +2185,8 @@ LnmLinksetResponse ToLnmResponse(
normalized?.Purls ?? Array.Empty<string>(), normalized?.Purls ?? Array.Empty<string>(),
normalized?.Cpes ?? Array.Empty<string>(), normalized?.Cpes ?? Array.Empty<string>(),
Summary: null, Summary: null,
PublishedAt: linkset.CreatedAt, PublishedAt: summary.PublishedAt ?? linkset.CreatedAt,
ModifiedAt: linkset.CreatedAt, ModifiedAt: summary.ModifiedAt ?? linkset.CreatedAt,
Severity: severity, Severity: severity,
Status: "fact-only", Status: "fact-only",
provenance, provenance,
@@ -1834,9 +2219,89 @@ string? ExtractSeverity(IReadOnlyDictionary<string, object?> severityDict)
return null; return null;
} }
async Task<LinksetObservationSummary> BuildObservationSummaryAsync(
IAdvisoryObservationQueryService observationQueryService,
string tenant,
AdvisoryLinkset linkset,
CancellationToken cancellationToken)
{
if (linkset.ObservationIds.Length == 0)
{
return LinksetObservationSummary.Empty;
}
var options = new AdvisoryObservationQueryOptions(
tenant,
observationIds: linkset.ObservationIds,
limit: linkset.ObservationIds.Length);
var result = await observationQueryService.QueryAsync(options, cancellationToken).ConfigureAwait(false);
if (result.Observations.IsDefaultOrEmpty)
{
return LinksetObservationSummary.Empty;
}
var published = result.Observations
.Where(o => o.Published.HasValue)
.Select(o => o.Published!.Value)
.OrderBy(p => p)
.FirstOrDefault();
var modified = result.Observations
.Where(o => o.Modified.HasValue)
.Select(o => o.Modified!.Value)
.OrderByDescending(p => p)
.FirstOrDefault();
var severity = result.Observations
.SelectMany(o => o.Severities)
.OrderByDescending(s => s.Score)
.FirstOrDefault();
var severityText = severity is null ? null : $"{severity.System}:{severity.Score:0.0}";
var evidenceHash = result.Observations
.Select(o => o.Provenance.SourceArtifactSha)
.FirstOrDefault();
return new LinksetObservationSummary(
PublishedAt: published == default ? null : published,
ModifiedAt: modified == default ? null : modified,
Severity: severityText,
EvidenceHash: evidenceHash);
}
IReadOnlyList<LnmLinksetTimeline> BuildTimeline(AdvisoryLinkset linkset, LinksetObservationSummary summary)
{
var timeline = new List<LnmLinksetTimeline>(3)
{
new("created", linkset.CreatedAt, linkset.Provenance?.ObservationHashes?.FirstOrDefault()),
};
if (summary.PublishedAt.HasValue)
{
timeline.Add(new LnmLinksetTimeline("published", summary.PublishedAt, summary.EvidenceHash));
}
if (summary.ModifiedAt.HasValue)
{
timeline.Add(new LnmLinksetTimeline("modified", summary.ModifiedAt, summary.EvidenceHash));
}
return timeline;
}
readonly record struct LinksetObservationSummary(
DateTimeOffset? PublishedAt,
DateTimeOffset? ModifiedAt,
string? Severity,
string? EvidenceHash)
{
public static LinksetObservationSummary Empty { get; } = new(null, null, null, null);
}
IResult JsonResult<T>(T value, int? statusCode = null) IResult JsonResult<T>(T value, int? statusCode = null)
{ {
var payload = JsonSerializer.Serialize(value, Program.JsonOptions); var payload = JsonSerializer.Serialize(value, JsonOptions);
return Results.Content(payload, "application/json", Encoding.UTF8, statusCode); return Results.Content(payload, "application/json", Encoding.UTF8, statusCode);
} }
@@ -1867,7 +2332,7 @@ IResult Problem(HttpContext context, string title, int statusCode, string type,
problemDetails.Extensions[entry.Key] = entry.Value; problemDetails.Extensions[entry.Key] = entry.Value;
} }
var payload = JsonSerializer.Serialize(problemDetails, Program.JsonOptions); var payload = JsonSerializer.Serialize(problemDetails, JsonOptions);
return Results.Content(payload, "application/problem+json", Encoding.UTF8, statusCode); return Results.Content(payload, "application/problem+json", Encoding.UTF8, statusCode);
} }
@@ -2188,6 +2653,14 @@ static DateTimeOffset? ParseDateTime(string? value)
: null; : null;
} }
static async Task<string> ComputeSha256Async(Stream stream, CancellationToken cancellationToken)
{
stream.Seek(0, SeekOrigin.Begin);
using var sha = SHA256.Create();
var hash = await sha.ComputeHashAsync(stream, cancellationToken).ConfigureAwait(false);
return Convert.ToHexString(hash).ToLowerInvariant();
}
IResult MapAocGuardException(HttpContext context, ConcelierAocGuardException exception) IResult MapAocGuardException(HttpContext context, ConcelierAocGuardException exception)
{ {
var guardException = new AocGuardException(exception.Result); var guardException = new AocGuardException(exception.Result);
@@ -2278,17 +2751,19 @@ static string? ResolveEvidencePath(string candidate, string root)
return null; return null;
} }
var effectiveRoot = root ?? string.Empty;
var path = candidate; var path = candidate;
if (!Path.IsPathRooted(path)) if (!Path.IsPathRooted(path) && !string.IsNullOrWhiteSpace(effectiveRoot))
{ {
path = Path.Combine(root, path); path = Path.Combine(effectiveRoot, path);
} }
var fullPath = Path.GetFullPath(path); var fullPath = Path.GetFullPath(path);
if (!string.IsNullOrWhiteSpace(root)) if (!string.IsNullOrWhiteSpace(effectiveRoot))
{ {
var rootPath = Path.GetFullPath(root) var rootPath = Path.GetFullPath(effectiveRoot)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase)) if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
@@ -2300,6 +2775,35 @@ static string? ResolveEvidencePath(string candidate, string root)
return fullPath; return fullPath;
} }
static EvidencePathResolutionResult ResolveEvidencePaths(
VerifyAttestationRequest request,
string root,
ConcelierOptions.EvidenceBundleOptions evidenceOptions)
{
var effectiveRoot = string.IsNullOrWhiteSpace(root) ? string.Empty : root;
var bundlePath = ResolveEvidencePath(request.BundlePath ?? string.Empty, effectiveRoot);
if (string.IsNullOrWhiteSpace(bundlePath) || !File.Exists(bundlePath))
{
return EvidencePathResolutionResult.Invalid("Bundle path not found", request.BundlePath);
}
var manifestPath = string.IsNullOrWhiteSpace(request.ManifestPath)
? ResolveSibling(bundlePath, evidenceOptions.DefaultManifestFileName)
: ResolveEvidencePath(request.ManifestPath!, effectiveRoot);
if (string.IsNullOrWhiteSpace(manifestPath) || !File.Exists(manifestPath))
{
return EvidencePathResolutionResult.Invalid("Manifest path not found", request.ManifestPath);
}
var transparencyPath = string.IsNullOrWhiteSpace(request.TransparencyPath)
? ResolveSibling(bundlePath, evidenceOptions.DefaultTransparencyFileName)
: ResolveEvidencePath(request.TransparencyPath!, effectiveRoot);
return EvidencePathResolutionResult.Valid(bundlePath!, manifestPath!, transparencyPath);
}
static string? ResolveSibling(string? bundlePath, string? fileName) static string? ResolveSibling(string? bundlePath, string? fileName)
{ {
if (string.IsNullOrWhiteSpace(bundlePath) || string.IsNullOrWhiteSpace(fileName)) if (string.IsNullOrWhiteSpace(bundlePath) || string.IsNullOrWhiteSpace(fileName))
@@ -2746,8 +3250,16 @@ var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
}); });
await app.RunAsync(); await app.RunAsync();
}
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot) static JsonSerializerOptions CreateJsonOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot)
{ {
var pluginOptions = new PluginHostOptions var pluginOptions = new PluginHostOptions
{ {
@@ -2801,14 +3313,6 @@ static async Task InitializeMongoAsync(WebApplication app)
} }
} }
public partial class Program }
{
public static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
private static JsonSerializerOptions CreateJsonOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
} }

View File

@@ -0,0 +1,104 @@
using System.Text.Json;
namespace StellaOps.Concelier.WebService.Services;
internal static class IncidentFileStore
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
};
public static string GetIncidentFilePath(ConcelierOptions.EvidenceBundleOptions evidenceOptions, string tenant, string advisoryKey)
{
ArgumentNullException.ThrowIfNull(evidenceOptions);
ArgumentNullException.ThrowIfNull(tenant);
ArgumentNullException.ThrowIfNull(advisoryKey);
var root = evidenceOptions.RootAbsolute ?? evidenceOptions.Root ?? string.Empty;
return Path.Combine(root, tenant.Trim(), advisoryKey.Trim(), "incident.json");
}
public static async Task WriteAsync(
ConcelierOptions.EvidenceBundleOptions evidenceOptions,
string tenant,
string advisoryKey,
string reason,
int cooldownMinutes,
string? pipelineVersion,
DateTimeOffset now,
CancellationToken cancellationToken)
{
var path = GetIncidentFilePath(evidenceOptions, tenant, advisoryKey);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
var activatedAt = now.ToUniversalTime();
var cooldownUntil = activatedAt.AddMinutes(cooldownMinutes);
var payload = new IncidentFile
{
AdvisoryKey = advisoryKey.Trim(),
Tenant = tenant.Trim(),
Reason = string.IsNullOrWhiteSpace(reason) ? "unspecified" : reason.Trim(),
ActivatedAt = activatedAt,
CooldownUntil = cooldownUntil,
PipelineVersion = pipelineVersion,
};
var json = JsonSerializer.Serialize(payload, SerializerOptions);
await File.WriteAllTextAsync(path, json, cancellationToken).ConfigureAwait(false);
}
public static async Task<IncidentStatusResponse?> ReadAsync(
ConcelierOptions.EvidenceBundleOptions evidenceOptions,
string tenant,
string advisoryKey,
DateTimeOffset now,
CancellationToken cancellationToken)
{
var path = GetIncidentFilePath(evidenceOptions, tenant, advisoryKey);
if (!File.Exists(path))
{
return null;
}
await using var stream = File.OpenRead(path);
var payload = await JsonSerializer.DeserializeAsync<IncidentFile>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);
if (payload is null)
{
return null;
}
var active = payload.CooldownUntil > now.ToUniversalTime();
return new IncidentStatusResponse(
payload.AdvisoryKey,
payload.Tenant,
payload.Reason,
payload.ActivatedAt.ToUniversalTime().ToString("O"),
payload.CooldownUntil.ToUniversalTime().ToString("O"),
payload.PipelineVersion,
active);
}
public static Task DeleteAsync(ConcelierOptions.EvidenceBundleOptions evidenceOptions, string tenant, string advisoryKey, CancellationToken cancellationToken)
{
var path = GetIncidentFilePath(evidenceOptions, tenant, advisoryKey);
if (File.Exists(path))
{
File.Delete(path);
}
return Task.CompletedTask;
}
private sealed record IncidentFile
{
public string AdvisoryKey { get; init; } = string.Empty;
public string Tenant { get; init; } = string.Empty;
public string Reason { get; init; } = "unspecified";
public DateTimeOffset ActivatedAt { get; init; }
public DateTimeOffset CooldownUntil { get; init; }
public string? PipelineVersion { get; init; }
}
}

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using StellaOps.Concelier.Models; using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Normalization.SemVer;
namespace StellaOps.Concelier.Connector.Cccs.Internal; namespace StellaOps.Concelier.Connector.Cccs.Internal;
@@ -116,8 +118,9 @@ internal static class CccsMapper
} }
var packages = new List<AffectedPackage>(dto.Products.Count); var packages = new List<AffectedPackage>(dto.Products.Count);
foreach (var product in dto.Products) for (var index = 0; index < dto.Products.Count; index++)
{ {
var product = dto.Products[index];
if (string.IsNullOrWhiteSpace(product)) if (string.IsNullOrWhiteSpace(product))
{ {
continue; continue;
@@ -131,14 +134,18 @@ internal static class CccsMapper
recordedAt, recordedAt,
new[] { ProvenanceFieldMasks.AffectedPackages }); new[] { ProvenanceFieldMasks.AffectedPackages });
var rangeAnchor = $"cccs:{dto.SerialNumber}:{index}";
var versionRanges = BuildVersionRanges(product, rangeAnchor, recordedAt);
var normalizedVersions = BuildNormalizedVersions(versionRanges, rangeAnchor);
packages.Add(new AffectedPackage( packages.Add(new AffectedPackage(
AffectedPackageTypes.Vendor, AffectedPackageTypes.Vendor,
identifier, identifier,
platform: null, platform: null,
versionRanges: Array.Empty<AffectedVersionRange>(), versionRanges: versionRanges,
statuses: Array.Empty<AffectedPackageStatus>(), statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[] { provenance }, provenance: new[] { provenance },
normalizedVersions: Array.Empty<NormalizedVersionRule>())); normalizedVersions: normalizedVersions));
} }
return packages.Count == 0 return packages.Count == 0
@@ -148,4 +155,104 @@ internal static class CccsMapper
.OrderBy(static package => package.Identifier, StringComparer.OrdinalIgnoreCase) .OrderBy(static package => package.Identifier, StringComparer.OrdinalIgnoreCase)
.ToArray(); .ToArray();
} }
private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(string productText, string rangeAnchor, DateTimeOffset recordedAt)
{
var versionText = ExtractFirstVersionToken(productText);
if (string.IsNullOrWhiteSpace(versionText))
{
return Array.Empty<AffectedVersionRange>();
}
var provenance = new AdvisoryProvenance(
CccsConnectorPlugin.SourceName,
"range",
rangeAnchor,
recordedAt,
new[] { ProvenanceFieldMasks.VersionRanges });
var vendorExtensions = new Dictionary<string, string>
{
["cccs.version.raw"] = versionText!,
["cccs.anchor"] = rangeAnchor,
};
var semVerResults = SemVerRangeRuleBuilder.Build(versionText!, patchedVersion: null, provenanceNote: rangeAnchor);
if (semVerResults.Count > 0)
{
return semVerResults.Select(result =>
new AffectedVersionRange(
rangeKind: NormalizedVersionSchemes.SemVer,
introducedVersion: result.Primitive.Introduced,
fixedVersion: result.Primitive.Fixed,
lastAffectedVersion: result.Primitive.LastAffected,
rangeExpression: result.Expression ?? versionText!,
provenance: provenance,
primitives: new RangePrimitives(
result.Primitive,
Nevra: null,
Evr: null,
VendorExtensions: vendorExtensions)))
.ToArray();
}
var primitives = new RangePrimitives(
new SemVerPrimitive(
Introduced: versionText,
IntroducedInclusive: true,
Fixed: null,
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: true,
ConstraintExpression: null,
ExactValue: versionText),
Nevra: null,
Evr: null,
VendorExtensions: vendorExtensions);
return new[]
{
new AffectedVersionRange(
rangeKind: NormalizedVersionSchemes.SemVer,
introducedVersion: null,
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: versionText,
provenance: provenance,
primitives: primitives),
};
}
private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
IReadOnlyList<AffectedVersionRange> ranges,
string rangeAnchor)
{
if (ranges.Count == 0)
{
return Array.Empty<NormalizedVersionRule>();
}
var rules = new List<NormalizedVersionRule>(ranges.Count);
foreach (var range in ranges)
{
var rule = range.ToNormalizedVersionRule(rangeAnchor);
if (rule is not null)
{
rules.Add(rule);
}
}
return rules.Count == 0 ? Array.Empty<NormalizedVersionRule>() : rules.ToArray();
}
private static string? ExtractFirstVersionToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var match = Regex.Match(value, @"\d+(?:\.\d+){0,3}(?:[A-Za-z0-9\-_]*)?");
return match.Success ? match.Value : null;
}
} }

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using StellaOps.Concelier.Models; using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Normalization.SemVer;
namespace StellaOps.Concelier.Connector.CertBund.Internal; namespace StellaOps.Concelier.Connector.CertBund.Internal;
@@ -116,23 +118,9 @@ internal static class CertBundMapper
recordedAt, recordedAt,
new[] { ProvenanceFieldMasks.AffectedPackages }); new[] { ProvenanceFieldMasks.AffectedPackages });
var ranges = string.IsNullOrWhiteSpace(product.Versions) var anchor = $"certbund:{dto.AdvisoryId}:{vendor.ToLowerInvariant().Replace(' ', '-')}";
? Array.Empty<AffectedVersionRange>() var ranges = BuildVersionRanges(product.Versions, anchor, recordedAt);
: new[] var normalized = BuildNormalizedVersions(ranges, anchor);
{
new AffectedVersionRange(
rangeKind: "string",
introducedVersion: null,
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: product.Versions,
provenance: new AdvisoryProvenance(
CertBundConnectorPlugin.SourceName,
"package-range",
product.Versions,
recordedAt,
new[] { ProvenanceFieldMasks.VersionRanges }))
};
packages.Add(new AffectedPackage( packages.Add(new AffectedPackage(
AffectedPackageTypes.Vendor, AffectedPackageTypes.Vendor,
@@ -141,7 +129,7 @@ internal static class CertBundMapper
versionRanges: ranges, versionRanges: ranges,
statuses: Array.Empty<AffectedPackageStatus>(), statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[] { provenance }, provenance: new[] { provenance },
normalizedVersions: Array.Empty<NormalizedVersionRule>())); normalizedVersions: normalized));
} }
return packages return packages
@@ -150,6 +138,87 @@ internal static class CertBundMapper
.ToArray(); .ToArray();
} }
private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(string? versions, string anchor, DateTimeOffset recordedAt)
{
if (string.IsNullOrWhiteSpace(versions)
|| string.Equals(versions.Trim(), "alle", StringComparison.OrdinalIgnoreCase))
{
return Array.Empty<AffectedVersionRange>();
}
var tokens = Regex.Matches(versions, @"\d+(?:\.\d+){0,3}(?:[A-Za-z0-9\-_]*)?")
.Select(match => match.Value)
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToList();
if (tokens.Count == 0)
{
return Array.Empty<AffectedVersionRange>();
}
var introduced = tokens.First();
var fixedVersion = tokens.Count > 1 ? tokens.Last() : null;
var vendorExtensions = new Dictionary<string, string>
{
["certbund.version.raw"] = versions!,
["certbund.anchor"] = anchor,
};
var semVer = new SemVerPrimitive(
Introduced: introduced,
IntroducedInclusive: true,
Fixed: fixedVersion,
FixedInclusive: true,
LastAffected: null,
LastAffectedInclusive: true,
ConstraintExpression: null,
ExactValue: tokens.Count == 1 ? introduced : null);
var rangeProvenance = new AdvisoryProvenance(
CertBundConnectorPlugin.SourceName,
"package-range",
anchor,
recordedAt,
new[] { ProvenanceFieldMasks.VersionRanges });
var primitives = new RangePrimitives(semVer, Nevra: null, Evr: null, VendorExtensions: vendorExtensions);
return new[]
{
new AffectedVersionRange(
rangeKind: NormalizedVersionSchemes.SemVer,
introducedVersion: introduced,
fixedVersion: fixedVersion,
lastAffectedVersion: null,
rangeExpression: versions!,
provenance: rangeProvenance,
primitives: primitives),
};
}
private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
IReadOnlyList<AffectedVersionRange> ranges,
string anchor)
{
if (ranges.Count == 0)
{
return Array.Empty<NormalizedVersionRule>();
}
var rules = new List<NormalizedVersionRule>(ranges.Count);
foreach (var range in ranges)
{
var rule = range.ToNormalizedVersionRule(anchor);
if (rule is not null)
{
rules.Add(rule);
}
}
return rules.Count == 0 ? Array.Empty<NormalizedVersionRule>() : rules.ToArray();
}
private static string? MapSeverity(string? severity) private static string? MapSeverity(string? severity)
{ {
if (string.IsNullOrWhiteSpace(severity)) if (string.IsNullOrWhiteSpace(severity))

View File

@@ -3,6 +3,7 @@ using FluentAssertions;
using StellaOps.Concelier.Connector.Cccs.Internal; using StellaOps.Concelier.Connector.Cccs.Internal;
using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Html; using StellaOps.Concelier.Connector.Common.Html;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Documents;
using Xunit; using Xunit;
@@ -39,5 +40,13 @@ public sealed class CccsMapperTests
advisory.References.Should().Contain(reference => reference.Url == "https://example.com/details"); advisory.References.Should().Contain(reference => reference.Url == "https://example.com/details");
advisory.AffectedPackages.Should().HaveCount(2); advisory.AffectedPackages.Should().HaveCount(2);
advisory.Provenance.Should().ContainSingle(p => p.Source == CccsConnectorPlugin.SourceName && p.Kind == "advisory"); advisory.Provenance.Should().ContainSingle(p => p.Source == CccsConnectorPlugin.SourceName && p.Kind == "advisory");
var first = advisory.AffectedPackages[0];
first.VersionRanges.Should().ContainSingle(range => range.RangeKind == NormalizedVersionSchemes.SemVer && range.RangeExpression == "1.0");
first.NormalizedVersions.Should().ContainSingle(rule => rule.Notes == "cccs:TEST-001:0" && rule.Value == "1.0");
var second = advisory.AffectedPackages[1];
second.VersionRanges.Should().ContainSingle(range => range.RangeKind == NormalizedVersionSchemes.SemVer && range.RangeExpression == "2.0");
second.NormalizedVersions.Should().ContainSingle(rule => rule.Notes == "cccs:TEST-001:1" && rule.Value == "2.0");
} }
} }

View File

@@ -16,6 +16,7 @@ using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo; using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories; using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Documents;
@@ -63,6 +64,17 @@ public sealed class CertBundConnectorTests : IAsyncLifetime
advisory.References.Should().Contain(reference => reference.Url == DetailUri.ToString()); advisory.References.Should().Contain(reference => reference.Url == DetailUri.ToString());
advisory.Language.Should().Be("de"); advisory.Language.Should().Be("de");
var endpoint = advisory.AffectedPackages.Should().ContainSingle(p => p.Identifier.Contains("Endpoint Manager") && !p.Identifier.Contains("Cloud"))
.Subject;
endpoint.VersionRanges.Should().ContainSingle(range =>
range.RangeKind == NormalizedVersionSchemes.SemVer &&
range.IntroducedVersion == "2023.1" &&
range.FixedVersion == "2024.2");
endpoint.NormalizedVersions.Should().ContainSingle(rule =>
rule.Min == "2023.1" &&
rule.Max == "2024.2" &&
rule.Notes == "certbund:WID-SEC-2025-2264:ivanti");
var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None); var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
state.Should().NotBeNull(); state.Should().NotBeNull();

View File

@@ -8,6 +8,7 @@ public sealed class EvidenceBundleAttestationBuilderTests
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "..")); Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
[Fact] [Fact]
[Trait("Category", "Attestation")]
public async Task BuildAsync_ProducesClaimsFromSampleBundle() public async Task BuildAsync_ProducesClaimsFromSampleBundle()
{ {
var sampleDir = Path.Combine(RepoRoot, "docs", "samples", "evidence-bundle"); var sampleDir = Path.Combine(RepoRoot, "docs", "samples", "evidence-bundle");
@@ -22,7 +23,7 @@ public sealed class EvidenceBundleAttestationBuilderTests
tarPath, tarPath,
manifestPath, manifestPath,
transparencyPath, transparencyPath,
pipelineVersion: "git:test-sha"), "git:test-sha"),
CancellationToken.None); CancellationToken.None);
Assert.Equal("evidence-bundle-m0", claims.SubjectName); Assert.Equal("evidence-bundle-m0", claims.SubjectName);
@@ -38,6 +39,7 @@ public sealed class EvidenceBundleAttestationBuilderTests
} }
[Fact] [Fact]
[Trait("Category", "Attestation")]
public async Task BuildAsync_EnforcesLowercaseTenant() public async Task BuildAsync_EnforcesLowercaseTenant()
{ {
var tempManifest = Path.Combine(Path.GetTempPath(), $"manifest-{Guid.NewGuid():N}.json"); var tempManifest = Path.Combine(Path.GetTempPath(), $"manifest-{Guid.NewGuid():N}.json");
@@ -64,4 +66,30 @@ public sealed class EvidenceBundleAttestationBuilderTests
Assert.Contains("Tenant must be lowercase", ex.Message); Assert.Contains("Tenant must be lowercase", ex.Message);
} }
[Fact]
[Trait("Category", "Attestation")]
public async Task BuildAsync_RequiresTenant()
{
var tempManifest = Path.Combine(Path.GetTempPath(), $"manifest-{Guid.NewGuid():N}.json");
var manifest = """
{
"bundle_id": "test-bundle",
"version": "1.0.0",
"created": "2025-11-19T00:00:00Z",
"scope": "vex"
}
""";
await File.WriteAllTextAsync(tempManifest, manifest);
var tempTar = Path.Combine(Path.GetTempPath(), $"bundle-{Guid.NewGuid():N}.tar.gz");
await File.WriteAllTextAsync(tempTar, "dummy");
var builder = new EvidenceBundleAttestationBuilder();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
builder.BuildAsync(new EvidenceBundleAttestationRequest(tempTar, tempManifest, null, "git:test"), CancellationToken.None));
Assert.Contains("Tenant must be present", ex.Message);
}
} }

View File

@@ -0,0 +1,23 @@
using System.IO;
using System.Threading.Tasks;
using StellaOps.Concelier.Core.Attestation;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Attestation;
public class EvidenceBundleAttestationValidator
{
[Fact]
public async Task BuildAsync_RejectsMissingTenant()
{
var bundle = Path.GetTempFileName();
var manifest = Path.GetTempFileName();
await File.WriteAllTextAsync(bundle, "dummy");
await File.WriteAllTextAsync(manifest, "{\"tenant\":\"ACME\"}");
var builder = new EvidenceBundleAttestationBuilder();
await Assert.ThrowsAsync<InvalidOperationException>(() =>
builder.BuildAsync(new EvidenceBundleAttestationRequest(bundle, manifest, null, "git:test")));
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using StellaOps.Concelier.WebService.AirGap;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests.AirGap;
public class AirgapBundleBuilderTests
{
[Fact]
public async Task BuildAsync_WritesDeterministicNdjson()
{
var builder = new AirgapBundleBuilder();
var created = DateTimeOffset.Parse("2025-11-01T00:00:00Z");
var items = new[]
{
"b:2",
"a:1",
"c:3",
"a:1" // duplicate should still appear twice to preserve raw cache content
};
var tempDir = Directory.CreateTempSubdirectory("concelier-airgap-test");
try
{
var result = await builder.BuildAsync(items, tempDir.FullName, created);
var lines = await File.ReadAllLinesAsync(result.BundlePath);
Assert.Equal(4, lines.Length);
Assert.Equal(new[] { "a:1", "a:1", "b:2", "c:3" }, lines);
Assert.False(string.IsNullOrWhiteSpace(result.Sha256));
Assert.Equal(4, result.ItemCount);
Assert.True(File.Exists(result.ManifestPath));
var manifestJson = await File.ReadAllTextAsync(result.ManifestPath);
var manifest = System.Text.Json.JsonSerializer.Deserialize<AirgapBundleManifest>(manifestJson)!;
Assert.Equal(result.Sha256, manifest.BundleSha256);
Assert.Equal(4, manifest.Count);
Assert.Equal(new[] { "a:1", "a:1", "b:2", "c:3" }, manifest.Items);
Assert.Equal(new[] { "a:1", "a:1", "b:2", "c:3" }.Select(v => v.GetDeterministicHash()), manifest.Entries.Select(e => e.Sha256));
Assert.Equal(created, manifest.CreatedUtc);
var manifestJsonFirstRun = manifestJson;
var entryTraceJsonFirstRun = await File.ReadAllTextAsync(result.EntryTracePath);
// Second run should produce identical hash
var result2 = await builder.BuildAsync(items, tempDir.FullName, created);
Assert.Equal(result.Sha256, result2.Sha256);
Assert.Equal(result.ManifestPath, result2.ManifestPath); // paths stable in same directory
var manifestJsonSecondRun = await File.ReadAllTextAsync(result2.ManifestPath);
var entryTraceJsonSecondRun = await File.ReadAllTextAsync(result2.EntryTracePath);
Assert.Equal(manifestJsonFirstRun, manifestJsonSecondRun);
Assert.Equal(entryTraceJsonFirstRun, entryTraceJsonSecondRun);
}
finally
{
tempDir.Delete(recursive: true);
}
}
}
internal static class HashTestExtensions
{
public static string GetDeterministicHash(this string content)
{
using var sha = System.Security.Cryptography.SHA256.Create();
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
return System.Convert.ToHexString(sha.ComputeHash(bytes)).ToLowerInvariant();
}
}

View File

@@ -0,0 +1,60 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using StellaOps.Concelier.WebService.AirGap;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests.AirGap;
public class AirgapBundleValidatorTests
{
[Fact]
public async Task ValidateAsync_Succeeds_ForBuilderOutput()
{
var builder = new AirgapBundleBuilder();
var validator = new AirgapBundleValidator();
var tempDir = Directory.CreateTempSubdirectory("concelier-airgap-validator");
try
{
var items = new[] { "b:2", "a:1" };
var result = await builder.BuildAsync(items, tempDir.FullName);
var validation = await validator.ValidateAsync(result.BundlePath, result.ManifestPath, result.EntryTracePath);
Assert.True(validation.IsValid, string.Join(";", validation.Errors));
}
finally
{
tempDir.Delete(recursive: true);
}
}
[Fact]
public async Task ValidateAsync_Fails_WhenManifestTampered()
{
var builder = new AirgapBundleBuilder();
var validator = new AirgapBundleValidator();
var tempDir = Directory.CreateTempSubdirectory("concelier-airgap-validator-bad");
try
{
var items = new[] { "b:2", "a:1" };
var result = await builder.BuildAsync(items, tempDir.FullName);
// Tamper manifest count
var manifest = await File.ReadAllTextAsync(result.ManifestPath);
manifest = manifest.Replace("\"count\":2", "\"count\":3");
await File.WriteAllTextAsync(result.ManifestPath, manifest);
var validation = await validator.ValidateAsync(result.BundlePath, result.ManifestPath, result.EntryTracePath);
Assert.False(validation.IsValid);
Assert.Contains(validation.Errors, e => e.Contains("count", System.StringComparison.OrdinalIgnoreCase));
}
finally
{
tempDir.Delete(recursive: true);
}
}
}

View File

@@ -1,20 +1,99 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using FluentAssertions; using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.WebService.Options;
using Xunit; using Xunit;
namespace StellaOps.Concelier.WebService.Tests; namespace StellaOps.Concelier.WebService.Tests;
public class ConcelierHealthEndpointTests : IClassFixture<WebApplicationFactory<Program>> public sealed class HealthWebAppFactory : WebApplicationFactory<Program>
{ {
private readonly WebApplicationFactory<Program> _factory; public HealthWebAppFactory()
public ConcelierHealthEndpointTests(WebApplicationFactory<Program> factory)
{ {
_factory = factory.WithWebHostBuilder(_ => { }); // Ensure options binder sees required storage values before Program.Main executes.
Environment.SetEnvironmentVariable("CONCELIER__STORAGE__DSN", "mongodb://localhost:27017/test-health");
Environment.SetEnvironmentVariable("CONCELIER__STORAGE__DRIVER", "mongo");
Environment.SetEnvironmentVariable("CONCELIER__STORAGE__COMMANDTIMEOUTSECONDS", "30");
Environment.SetEnvironmentVariable("CONCELIER__TELEMETRY__ENABLED", "false");
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "mongodb://localhost:27017/test-health");
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
} }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((_, config) =>
{
var overrides = new Dictionary<string, string?>
{
{"Storage:Dsn", "mongodb://localhost:27017/test-health"},
{"Storage:Driver", "mongo"},
{"Storage:CommandTimeoutSeconds", "30"},
{"Telemetry:Enabled", "false"}
};
config.AddInMemoryCollection(overrides);
});
builder.UseSetting("CONCELIER__STORAGE__DSN", "mongodb://localhost:27017/test-health");
builder.UseSetting("CONCELIER__STORAGE__DRIVER", "mongo");
builder.UseSetting("CONCELIER__STORAGE__COMMANDTIMEOUTSECONDS", "30");
builder.UseSetting("CONCELIER__TELEMETRY__ENABLED", "false");
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.AddSingleton<ConcelierOptions>(new ConcelierOptions
{
Storage = new ConcelierOptions.StorageOptions
{
Dsn = "mongodb://localhost:27017/test-health",
Driver = "mongo",
CommandTimeoutSeconds = 30
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
}
});
services.AddSingleton<IConfigureOptions<ConcelierOptions>>(sp => new ConfigureOptions<ConcelierOptions>(opts =>
{
opts.Storage ??= new ConcelierOptions.StorageOptions();
opts.Storage.Driver = "mongo";
opts.Storage.Dsn = "mongodb://localhost:27017/test-health";
opts.Storage.CommandTimeoutSeconds = 30;
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
opts.Telemetry.Enabled = false;
}));
services.PostConfigure<ConcelierOptions>(opts =>
{
opts.Storage ??= new ConcelierOptions.StorageOptions();
opts.Storage.Driver = "mongo";
opts.Storage.Dsn = "mongodb://localhost:27017/test-health";
opts.Storage.CommandTimeoutSeconds = 30;
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
opts.Telemetry.Enabled = false;
});
});
}
}
public class ConcelierHealthEndpointTests : IClassFixture<HealthWebAppFactory>
{
private readonly HealthWebAppFactory _factory;
public ConcelierHealthEndpointTests(HealthWebAppFactory factory) => _factory = factory;
[Fact] [Fact]
public async Task Health_requires_tenant_header() public async Task Health_requires_tenant_header()
{ {

View File

@@ -0,0 +1,43 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Concelier.WebService.Services;
using StellaOps.Concelier.WebService;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests.Services;
public sealed class IncidentFileStoreTests
{
[Fact]
public async Task WriteReadDelete_RoundTripsIncident()
{
var temp = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "incident-store-tests", Path.GetRandomFileName()));
var options = new ConcelierOptions
{
Evidence = new ConcelierOptions.EvidenceBundleOptions
{
RootAbsolute = temp.FullName,
DefaultManifestFileName = "manifest.json",
DefaultTransparencyFileName = "transparency.json",
PipelineVersion = "git:test",
},
};
var now = new DateTimeOffset(2025, 11, 25, 12, 0, 0, TimeSpan.Zero);
await IncidentFileStore.WriteAsync(options.Evidence!, "tenant-a", "ADV-1", "test-reason", 30, options.Evidence!.PipelineVersion, now, CancellationToken.None);
var status = await IncidentFileStore.ReadAsync(options.Evidence!, "tenant-a", "ADV-1", now, CancellationToken.None);
status.Should().NotBeNull();
status!.Reason.Should().Be("test-reason");
status.Active.Should().BeTrue();
status.Tenant.Should().Be("tenant-a");
status.AdvisoryKey.Should().Be("ADV-1");
status.PipelineVersion.Should().Be("git:test");
await IncidentFileStore.DeleteAsync(options.Evidence!, "tenant-a", "ADV-1", CancellationToken.None);
var afterDelete = await IncidentFileStore.ReadAsync(options.Evidence!, "tenant-a", "ADV-1", now, CancellationToken.None);
afterDelete.Should().BeNull();
}
}

View File

@@ -12,6 +12,7 @@
<CopyOutputSymbolsToOutputDirectory>true</CopyOutputSymbolsToOutputDirectory> <CopyOutputSymbolsToOutputDirectory>true</CopyOutputSymbolsToOutputDirectory>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj" /> <ProjectReference Include="../../StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj" />

View File

@@ -27,6 +27,8 @@ using Mongo2Go;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.IO; using MongoDB.Bson.IO;
using MongoDB.Driver; using MongoDB.Driver;
using StellaOps.Concelier.Core.Attestation;
using static StellaOps.Concelier.WebService.Program;
using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Models; using StellaOps.Concelier.Models;
@@ -39,6 +41,7 @@ using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.WebService.Jobs; using StellaOps.Concelier.WebService.Jobs;
using StellaOps.Concelier.WebService.Options; using StellaOps.Concelier.WebService.Options;
using StellaOps.Concelier.WebService.Contracts; using StellaOps.Concelier.WebService.Contracts;
using StellaOps.Concelier.WebService;
using Xunit.Sdk; using Xunit.Sdk;
using StellaOps.Auth.Abstractions; using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client; using StellaOps.Auth.Client;
@@ -73,7 +76,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
public Task InitializeAsync() public Task InitializeAsync()
{ {
PrepareMongoEnvironment(); PrepareMongoEnvironment();
if (TryStartExternalMongo(out var externalConnectionString)) if (TryStartExternalMongo(out var externalConnectionString) && !string.IsNullOrWhiteSpace(externalConnectionString))
{ {
_factory = new ConcelierApplicationFactory(externalConnectionString); _factory = new ConcelierApplicationFactory(externalConnectionString);
} }
@@ -381,6 +384,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal("ADV-002", firstItem.GetProperty("advisoryId").GetString()); Assert.Equal("ADV-002", firstItem.GetProperty("advisoryId").GetString());
Assert.Contains("pkg:npm/demo@2.0.0", firstItem.GetProperty("purl").EnumerateArray().Select(x => x.GetString())); Assert.Contains("pkg:npm/demo@2.0.0", firstItem.GetProperty("purl").EnumerateArray().Select(x => x.GetString()));
Assert.True(firstItem.GetProperty("conflicts").EnumerateArray().Count() >= 0); Assert.True(firstItem.GetProperty("conflicts").EnumerateArray().Count() >= 0);
Assert.Equal("created", firstItem.GetProperty("timeline").EnumerateArray().First().GetProperty("event").GetString());
Assert.Equal(DateTime.Parse("2025-01-06T00:00:00Z"), firstItem.GetProperty("publishedAt").GetDateTime());
var detailResponse = await client.GetAsync("/v1/lnm/linksets/ADV-001?source=osv&includeObservations=true"); var detailResponse = await client.GetAsync("/v1/lnm/linksets/ADV-001?source=osv&includeObservations=true");
detailResponse.EnsureSuccessStatusCode(); detailResponse.EnsureSuccessStatusCode();
@@ -390,6 +395,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal("osv", detailPayload.GetProperty("source").GetString()); Assert.Equal("osv", detailPayload.GetProperty("source").GetString());
Assert.Contains("pkg:npm/demo@1.0.0", detailPayload.GetProperty("purl").EnumerateArray().Select(x => x.GetString())); Assert.Contains("pkg:npm/demo@1.0.0", detailPayload.GetProperty("purl").EnumerateArray().Select(x => x.GetString()));
Assert.Contains("obs-1", detailPayload.GetProperty("observations").EnumerateArray().Select(x => x.GetString())); Assert.Contains("obs-1", detailPayload.GetProperty("observations").EnumerateArray().Select(x => x.GetString()));
Assert.Equal(DateTime.Parse("2025-01-05T00:00:00Z"), detailPayload.GetProperty("publishedAt").GetDateTime());
} }
[Fact] [Fact]
@@ -713,6 +719,66 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal(tarPath, evidence.Attestation.EvidenceBundlePath); Assert.Equal(tarPath, evidence.Attestation.EvidenceBundlePath);
} }
[Fact]
[Trait("Category", "Attestation")]
public async Task InternalAttestationVerify_ReturnsClaims()
{
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
var sampleDir = Path.Combine(repoRoot, "docs", "samples", "evidence-bundle");
var tarPath = Path.Combine(sampleDir, "evidence-bundle-m0.tar.gz");
var manifestPath = Path.Combine(sampleDir, "manifest.json");
var transparencyPath = Path.Combine(sampleDir, "transparency.json");
using var scope = _factory.Services.CreateScope();
var concOptions = scope.ServiceProvider.GetRequiredService<IOptions<ConcelierOptions>>().Value;
_output.WriteLine($"EvidenceRoot={concOptions.Evidence.RootAbsolute}");
Assert.StartsWith(concOptions.Evidence.RootAbsolute, tarPath, StringComparison.OrdinalIgnoreCase);
using var client = _factory.CreateClient();
var request = new VerifyAttestationRequest(tarPath, manifestPath, transparencyPath, "git:test-sha");
var response = await client.PostAsJsonAsync("/internal/attestations/verify?tenant=demo", request);
var responseBody = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode, $"Attestation verify failed: {(int)response.StatusCode} {response.StatusCode} · {responseBody}");
var claims = JsonSerializer.Deserialize<AttestationClaims>(
responseBody,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
Assert.NotNull(claims);
Assert.Equal("evidence-bundle-m0", claims!.SubjectName);
Assert.Equal("git:test-sha", claims.PipelineVersion);
Assert.Equal(tarPath, claims.EvidenceBundlePath);
}
[Fact]
public async Task EvidenceBatch_ReturnsEmptyCollectionsWhenUnknown()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add(TenantHeaderName, "demo");
var request = new EvidenceBatchRequest(
new[]
{
new EvidenceBatchItemRequest("component-a", new[] { "pkg:purl/example@1.0.0" }, new[] { "ALIAS-1" })
},
ObservationLimit: 5,
LinksetLimit: 5);
var response = await client.PostAsJsonAsync("/v1/evidence/batch", request);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<EvidenceBatchResponse>();
Assert.NotNull(payload);
var item = Assert.Single(payload!.Items);
Assert.Equal("component-a", item.ComponentId);
Assert.Empty(item.Observations);
Assert.Empty(item.Linksets);
Assert.False(item.HasMore);
}
[Fact] [Fact]
public async Task AdvisoryEvidenceEndpoint_FiltersByVendor() public async Task AdvisoryEvidenceEndpoint_FiltersByVendor()
{ {
@@ -1300,7 +1366,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<ReplayResponse>(); var payload = await response.Content.ReadFromJsonAsync<ReplayResponse>();
Assert.NotNull(payload); Assert.NotNull(payload);
var conflict = Assert.Single(payload!.Conflicts); var conflicts = payload!.Conflicts ?? throw new XunitException("Conflicts was null");
var conflict = Assert.Single(conflicts);
Assert.Equal(conflictId, conflict.ConflictId); Assert.Equal(conflictId, conflict.ConflictId);
Assert.Equal("severity", conflict.Explainer.Type); Assert.Equal("severity", conflict.Explainer.Type);
Assert.Equal("mismatch", conflict.Explainer.Reason); Assert.Equal("mismatch", conflict.Explainer.Reason);
@@ -1977,6 +2044,17 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
_previousTelemetryLogging = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING"); _previousTelemetryLogging = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING");
_previousTelemetryTracing = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING"); _previousTelemetryTracing = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING");
_previousTelemetryMetrics = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS"); _previousTelemetryMetrics = Environment.GetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS");
var opensslPath = ResolveOpenSsl11Path();
if (!string.IsNullOrEmpty(opensslPath))
{
var currentLd = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
var merged = string.IsNullOrWhiteSpace(currentLd)
? opensslPath
: string.Join(':', opensslPath, currentLd);
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", merged);
}
Environment.SetEnvironmentVariable("CONCELIER_STORAGE__DSN", connectionString); Environment.SetEnvironmentVariable("CONCELIER_STORAGE__DSN", connectionString);
Environment.SetEnvironmentVariable("CONCELIER_STORAGE__DRIVER", "mongo"); Environment.SetEnvironmentVariable("CONCELIER_STORAGE__DRIVER", "mongo");
Environment.SetEnvironmentVariable("CONCELIER_STORAGE__COMMANDTIMEOUTSECONDS", "30"); Environment.SetEnvironmentVariable("CONCELIER_STORAGE__COMMANDTIMEOUTSECONDS", "30");
@@ -1984,6 +2062,10 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING", "false"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING", "false");
Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING", "false"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING", "false");
Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS", "false"); Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS", "false");
const string EvidenceRootKey = "CONCELIER_EVIDENCE__ROOT";
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
_additionalPreviousEnvironment[EvidenceRootKey] = Environment.GetEnvironmentVariable(EvidenceRootKey);
Environment.SetEnvironmentVariable(EvidenceRootKey, repoRoot);
const string TestSecretKey = "CONCELIER_AUTHORITY__TESTSIGNINGSECRET"; const string TestSecretKey = "CONCELIER_AUTHORITY__TESTSIGNINGSECRET";
if (environmentOverrides is null || !environmentOverrides.ContainsKey(TestSecretKey)) if (environmentOverrides is null || !environmentOverrides.ContainsKey(TestSecretKey))
{ {
@@ -2002,6 +2084,23 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
} }
} }
private static string? ResolveOpenSsl11Path()
{
var current = AppContext.BaseDirectory;
for (var i = 0; i < 8; i++)
{
var candidate = Path.GetFullPath(Path.Combine(current, "tests", "native", "openssl-1.1", "linux-x64"));
if (Directory.Exists(candidate))
{
return candidate;
}
current = Path.GetFullPath(Path.Combine(current, ".."));
}
return null;
}
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
{ {
builder.ConfigureAppConfiguration((context, configurationBuilder) => builder.ConfigureAppConfiguration((context, configurationBuilder) =>
@@ -2035,7 +2134,17 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
options.Telemetry.EnableMetrics = false; options.Telemetry.EnableMetrics = false;
options.Authority ??= new ConcelierOptions.AuthorityOptions(); options.Authority ??= new ConcelierOptions.AuthorityOptions();
_authorityConfigure?.Invoke(options.Authority); _authorityConfigure?.Invoke(options.Authority);
// Point evidence root at the repo so sample bundles under docs/samples/evidence-bundle resolve without 400.
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
options.Evidence.Root = repoRoot;
options.Evidence.RootAbsolute = repoRoot;
}); });
// Ensure content root + wwwroot exist so host startup does not throw when WebService bin output isn't present.
var contentRoot = AppContext.BaseDirectory;
var wwwroot = Path.Combine(contentRoot, "wwwroot");
Directory.CreateDirectory(wwwroot);
}); });
builder.ConfigureTestServices(services => builder.ConfigureTestServices(services =>
@@ -3093,4 +3202,5 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
return Task.FromResult<IReadOnlyDictionary<string, JobRunSnapshot>>(map); return Task.FromResult<IReadOnlyDictionary<string, JobRunSnapshot>>(map);
} }
} }
} }

44
src/Excititor/AGENTS.md Normal file
View File

@@ -0,0 +1,44 @@
# Excititor · AGENTS Charter (Air-Gap & Trust Connectors)
## Module Scope & Working Directory
- Working directory: `src/Excititor/**` (WebService, Worker, __Libraries, __Tests, connectors, scripts). No cross-module edits unless explicitly noted in sprint Decisions & Risks.
- Mission (current sprint): air-gap parity for evidence chunks, trust connector wiring, and attestation verification aligned to Evidence Locker contract.
## Roles
- **Backend engineer (ASP.NET Core / Mongo):** chunk ingestion/export, attestation verifier, trust connector.
- **Air-Gap/Platform engineer:** sealed-mode switches, offline bundles, deterministic cache/path handling.
- **QA automation:** WebApplicationFactory + Mongo2Go tests for chunk APIs, attestations, and trust connector; deterministic ordering/hashes.
- **Docs/Schema steward:** keep chunk API, attestation plan, and trust connector docs in sync with behavior; update schemas and samples.
## Required Reading (treat as read before DOING)
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/excititor/architecture.md`
- `docs/modules/excititor/attestation-plan.md`
- `docs/modules/excititor/operations/chunk-api-user-guide.md`
- `docs/modules/excititor/schemas/vex-chunk-api.yaml`
- `docs/modules/evidence-locker/attestation-contract.md`
## Working Agreements
- Determinism: canonical JSON ordering; stable pagination; UTC ISO-8601 timestamps; sort chunk edges deterministically.
- Offline-first: default sealed-mode must not reach external networks; connectors obey allowlist; feature flags default safe.
- Attestation: DSSE/Envelope per contract; always include tenant/source identifiers; validation fixtures required.
- Tenant safety: enforce tenant headers/guards on every API; no cross-tenant leakage.
- Logging/metrics: structured logs; meters under `StellaOps.Excititor.*`; tag `tenant`, `source`, `result`.
- Cross-module edits: require sprint note; otherwise, stay within Excititor working dir.
## Testing Rules
- Use Mongo2Go/in-memory fixtures; avoid network.
- API tests in `StellaOps.Excititor.WebService.Tests`; worker/connectors in `StellaOps.Excititor.Worker.Tests`; shared fixtures in `__Tests`.
- Tests must assert determinism (ordering/hashes), tenant enforcement, and sealed-mode behavior.
## Delivery Discipline
- Update sprint tracker status (`TODO → DOING → DONE/BLOCKED`) for each task; mirror changes in Execution Log and Decisions & Risks.
- When changing contracts (API/attestation schemas), update docs and samples and link from sprint Decisions & Risks.
- If a decision is needed, mark the task BLOCKED and record the decision ask—do not pause work.
## Tooling/Env Notes
- .NET 10 with preview features enabled; Mongo driver ≥ 3.x.
- Signing/verifier hooks rely on Evidence Locker contract fixtures under `docs/modules/evidence-locker/`.
- Sealed-mode tests should run with `EXCITITOR_SEALED=1` (env var) to enforce offline code paths.

View File

@@ -21,6 +21,9 @@ public sealed class AirgapImportRequest
[JsonPropertyName("publisher")] [JsonPropertyName("publisher")]
public string? Publisher { get; init; } public string? Publisher { get; init; }
[JsonPropertyName("tenantId")]
public string? TenantId { get; init; }
[JsonPropertyName("payloadHash")] [JsonPropertyName("payloadHash")]
public string? PayloadHash { get; init; } public string? PayloadHash { get; init; }

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace StellaOps.Excititor.WebService.Options;
internal sealed class AirgapOptions
{
public const string SectionName = "Excititor:Airgap";
/// <summary>
/// Enables sealed-mode enforcement for air-gapped imports.
/// When true, external payload URLs are rejected and publisher allowlist is applied.
/// </summary>
public bool SealedMode { get; set; } = false;
/// <summary>
/// When true, imports must originate from mirror/offline sources (no HTTP/HTTPS URLs).
/// </summary>
public bool MirrorOnly { get; set; } = true;
/// <summary>
/// Optional allowlist of publishers that may submit bundles while sealed mode is enabled.
/// Empty list means allow all.
/// </summary>
public List<string> TrustedPublishers { get; } = new();
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@@ -54,8 +53,10 @@ services.AddCycloneDxNormalizer();
services.AddOpenVexNormalizer(); services.AddOpenVexNormalizer();
services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>(); services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>();
// TODO: replace NoopVexSignatureVerifier with hardened verifier once portable bundle signatures are finalized. // TODO: replace NoopVexSignatureVerifier with hardened verifier once portable bundle signatures are finalized.
services.Configure<AirgapOptions>(configuration.GetSection(AirgapOptions.SectionName));
services.AddSingleton<AirgapImportValidator>(); services.AddSingleton<AirgapImportValidator>();
services.AddSingleton<AirgapSignerTrustService>(); services.AddSingleton<AirgapSignerTrustService>();
services.AddSingleton<AirgapModeEnforcer>();
services.AddSingleton<ConsoleTelemetry>(); services.AddSingleton<ConsoleTelemetry>();
services.AddMemoryCache(); services.AddMemoryCache();
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>(); services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
@@ -185,7 +186,7 @@ app.MapGet("/openapi/excititor.json", () =>
get = new get = new
{ {
summary = "Service status (aggregation-only metadata)", summary = "Service status (aggregation-only metadata)",
responses = new responses = new Dictionary<string, object>
{ {
["200"] = new ["200"] = new
{ {
@@ -219,7 +220,7 @@ app.MapGet("/openapi/excititor.json", () =>
get = new get = new
{ {
summary = "Health check", summary = "Health check",
responses = new responses = new Dictionary<string, object>
{ {
["200"] = new ["200"] = new
{ {
@@ -254,7 +255,7 @@ app.MapGet("/openapi/excititor.json", () =>
new { name = "cursor", @in = "query", schema = new { type = "string" }, required = false, description = "Numeric cursor or Last-Event-ID" }, new { name = "cursor", @in = "query", schema = new { type = "string" }, required = false, description = "Numeric cursor or Last-Event-ID" },
new { name = "limit", @in = "query", schema = new { type = "integer", minimum = 1, maximum = 100 }, required = false } new { name = "limit", @in = "query", schema = new { type = "integer", minimum = 1, maximum = 100 }, required = false }
}, },
responses = new responses = new Dictionary<string, object>
{ {
["200"] = new ["200"] = new
{ {
@@ -331,7 +332,7 @@ app.MapGet("/openapi/excititor.json", () =>
} }
} }
}, },
responses = new responses = new Dictionary<string, object>
{ {
["200"] = new { description = "Accepted" }, ["200"] = new { description = "Accepted" },
["400"] = new ["400"] = new
@@ -448,16 +449,47 @@ app.MapGet("/openapi/excititor.json", () =>
app.MapPost("/airgap/v1/vex/import", async ( app.MapPost("/airgap/v1/vex/import", async (
[FromServices] AirgapImportValidator validator, [FromServices] AirgapImportValidator validator,
[FromServices] AirgapSignerTrustService trustService, [FromServices] AirgapSignerTrustService trustService,
[FromServices] AirgapModeEnforcer modeEnforcer,
[FromServices] IAirgapImportStore store, [FromServices] IAirgapImportStore store,
[FromServices] ILoggerFactory loggerFactory,
[FromServices] TimeProvider timeProvider, [FromServices] TimeProvider timeProvider,
[FromBody] AirgapImportRequest request, [FromBody] AirgapImportRequest request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
{ {
var logger = loggerFactory.CreateLogger("AirgapImport");
var nowUtc = timeProvider.GetUtcNow(); var nowUtc = timeProvider.GetUtcNow();
var tenantId = string.IsNullOrWhiteSpace(request.TenantId)
? "default"
: request.TenantId!.Trim().ToLowerInvariant();
var stalenessSeconds = request.SignedAt is null
? (int?)null
: (int)Math.Round((nowUtc - request.SignedAt.Value).TotalSeconds);
var timeline = new List<AirgapTimelineEntry>();
void RecordEvent(string eventType, string? code = null, string? message = null)
{
var entry = new AirgapTimelineEntry
{
EventType = eventType,
CreatedAt = nowUtc,
TenantId = tenantId,
BundleId = request.BundleId ?? string.Empty,
MirrorGeneration = request.MirrorGeneration ?? string.Empty,
StalenessSeconds = stalenessSeconds,
ErrorCode = code,
Message = message
};
timeline.Add(entry);
logger.LogInformation("Airgap timeline event {EventType} bundle={BundleId} gen={Gen} tenant={Tenant} code={Code}", eventType, entry.BundleId, entry.MirrorGeneration, tenantId, code);
}
RecordEvent("airgap.import.started");
var errors = validator.Validate(request, nowUtc); var errors = validator.Validate(request, nowUtc);
if (errors.Count > 0) if (errors.Count > 0)
{ {
var first = errors[0]; var first = errors[0];
RecordEvent("airgap.import.failed", first.Code, first.Message);
return Results.BadRequest(new return Results.BadRequest(new
{ {
error = new error = new
@@ -468,8 +500,22 @@ app.MapPost("/airgap/v1/vex/import", async (
}); });
} }
if (!modeEnforcer.Validate(request, out var sealedCode, out var sealedMessage))
{
RecordEvent("airgap.import.failed", sealedCode, sealedMessage);
return Results.Json(new
{
error = new
{
code = sealedCode,
message = sealedMessage
}
}, statusCode: StatusCodes.Status403Forbidden);
}
if (!trustService.Validate(request, out var trustCode, out var trustMessage)) if (!trustService.Validate(request, out var trustCode, out var trustMessage))
{ {
RecordEvent("airgap.import.failed", trustCode, trustMessage);
return Results.Json(new return Results.Json(new
{ {
error = new error = new
@@ -480,9 +526,16 @@ app.MapPost("/airgap/v1/vex/import", async (
}, statusCode: StatusCodes.Status403Forbidden); }, statusCode: StatusCodes.Status403Forbidden);
} }
var manifestPath = $"mirror/{request.BundleId}/{request.MirrorGeneration}/manifest.json";
var evidenceLockerPath = $"evidence/{request.BundleId}/{request.MirrorGeneration}/bundle.ndjson";
var manifestHash = ComputeSha256($"{request.BundleId}:{request.MirrorGeneration}:{request.PayloadHash}");
RecordEvent("airgap.import.completed");
var record = new AirgapImportRecord var record = new AirgapImportRecord
{ {
Id = $"{request.BundleId}:{request.MirrorGeneration}", Id = $"{request.BundleId}:{request.MirrorGeneration}",
TenantId = tenantId,
BundleId = request.BundleId!, BundleId = request.BundleId!,
MirrorGeneration = request.MirrorGeneration!, MirrorGeneration = request.MirrorGeneration!,
SignedAt = request.SignedAt!.Value, SignedAt = request.SignedAt!.Value,
@@ -491,7 +544,11 @@ app.MapPost("/airgap/v1/vex/import", async (
PayloadUrl = request.PayloadUrl, PayloadUrl = request.PayloadUrl,
Signature = request.Signature!, Signature = request.Signature!,
TransparencyLog = request.TransparencyLog, TransparencyLog = request.TransparencyLog,
ImportedAt = nowUtc ImportedAt = nowUtc,
PortableManifestPath = manifestPath,
PortableManifestHash = manifestHash,
EvidenceLockerPath = evidenceLockerPath,
Timeline = timeline
}; };
try try
@@ -500,6 +557,7 @@ app.MapPost("/airgap/v1/vex/import", async (
} }
catch (DuplicateAirgapImportException dup) catch (DuplicateAirgapImportException dup)
{ {
RecordEvent("airgap.import.failed", "AIRGAP_IMPORT_DUPLICATE", dup.Message);
return Results.Conflict(new return Results.Conflict(new
{ {
error = new error = new
@@ -513,10 +571,20 @@ app.MapPost("/airgap/v1/vex/import", async (
return Results.Accepted($"/airgap/v1/vex/import/{request.BundleId}", new return Results.Accepted($"/airgap/v1/vex/import/{request.BundleId}", new
{ {
bundleId = request.BundleId, bundleId = request.BundleId,
generation = request.MirrorGeneration generation = request.MirrorGeneration,
manifest = manifestPath,
evidence = evidenceLockerPath,
manifestSha256 = manifestHash
}); });
}); });
static string ComputeSha256(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}
app.MapPost("/v1/attestations/verify", async ( app.MapPost("/v1/attestations/verify", async (
[FromServices] IVexAttestationClient attestationClient, [FromServices] IVexAttestationClient attestationClient,
[FromBody] AttestationVerifyRequest request, [FromBody] AttestationVerifyRequest request,
@@ -1548,6 +1616,15 @@ app.MapGet("/v1/vex/linksets", async (HttpContext _, CancellationToken __) =>
app.Run(); app.Run();
internal sealed record ExcititorTimelineEvent(
string Type,
string Tenant,
string Source,
int Count,
int Errors,
string? TraceId,
string OccurredAt);
public partial class Program; public partial class Program;
internal sealed record StatusResponse(DateTimeOffset UtcNow, string MongoBucket, int InlineThreshold, string[] ArtifactStores); internal sealed record StatusResponse(DateTimeOffset UtcNow, string MongoBucket, int InlineThreshold, string[] ArtifactStores);

View File

@@ -0,0 +1,65 @@
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Options;
namespace StellaOps.Excititor.WebService.Services;
internal sealed class AirgapModeEnforcer
{
private readonly AirgapOptions _options;
private readonly ILogger<AirgapModeEnforcer> _logger;
public AirgapModeEnforcer(IOptions<AirgapOptions> options, ILogger<AirgapModeEnforcer> logger)
{
_options = options.Value;
_logger = logger;
}
public bool Validate(AirgapImportRequest request, out string? errorCode, out string? message)
{
errorCode = null;
message = null;
if (!_options.SealedMode)
{
return true;
}
if (_options.MirrorOnly && !string.IsNullOrWhiteSpace(request.PayloadUrl) && LooksLikeExternal(request.PayloadUrl))
{
errorCode = "AIRGAP_EGRESS_BLOCKED";
message = "Sealed mode forbids external payload URLs; stage bundle via mirror/portable media.";
_logger.LogWarning("Blocked airgap import because payloadUrl points to external location: {Url}", request.PayloadUrl);
return false;
}
if (_options.TrustedPublishers.Count > 0 && !string.IsNullOrWhiteSpace(request.Publisher))
{
var allowed = _options.TrustedPublishers.Any(p => string.Equals(p, request.Publisher, StringComparison.OrdinalIgnoreCase));
if (!allowed)
{
errorCode = "AIRGAP_SOURCE_UNTRUSTED";
message = $"Publisher '{request.Publisher}' is not allowlisted for sealed-mode imports.";
_logger.LogWarning("Blocked airgap import because publisher {Publisher} is not allowlisted.", request.Publisher);
return false;
}
}
return true;
}
private static bool LooksLikeExternal(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return false;
}
return url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|| url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,27 @@
using System;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Excititor.Storage.Mongo;
[BsonIgnoreExtraElements]
public sealed class AirgapTimelineEntry
{
public string EventType { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public string TenantId { get; set; } = "default";
public string BundleId { get; set; } = string.Empty;
public string MirrorGeneration { get; set; } = string.Empty;
public int? StalenessSeconds { get; set; }
= null;
public string? ErrorCode { get; set; }
= null;
public string? Message { get; set; }
= null;
}

View File

@@ -316,6 +316,8 @@ public sealed class AirgapImportRecord
[BsonId] [BsonId]
public string Id { get; set; } = default!; public string Id { get; set; } = default!;
public string TenantId { get; set; } = "default";
public string BundleId { get; set; } = default!; public string BundleId { get; set; } = default!;
public string MirrorGeneration { get; set; } = default!; public string MirrorGeneration { get; set; } = default!;
@@ -333,6 +335,14 @@ public sealed class AirgapImportRecord
public string? TransparencyLog { get; set; } = null; public string? TransparencyLog { get; set; } = null;
public DateTimeOffset ImportedAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset ImportedAt { get; set; } = DateTimeOffset.UtcNow;
public string PortableManifestPath { get; set; } = string.Empty;
public string PortableManifestHash { get; set; } = string.Empty;
public string EvidenceLockerPath { get; set; } = string.Empty;
public List<AirgapTimelineEntry> Timeline { get; set; } = new();
} }
[BsonIgnoreExtraElements] [BsonIgnoreExtraElements]

View File

@@ -31,8 +31,15 @@ public sealed class AirgapImportValidatorTests
[Fact] [Fact]
public void Validate_InvalidHash_ReturnsError() public void Validate_InvalidHash_ReturnsError()
{ {
var req = Valid(); var req = new AirgapImportRequest
req.PayloadHash = "not-a-hash"; {
BundleId = "bundle-123",
MirrorGeneration = "5",
Publisher = "stellaops",
PayloadHash = "not-a-hash",
Signature = Convert.ToBase64String(new byte[] { 5, 6, 7 }),
SignedAt = _now
};
var result = _validator.Validate(req, _now); var result = _validator.Validate(req, _now);
@@ -42,8 +49,15 @@ public sealed class AirgapImportValidatorTests
[Fact] [Fact]
public void Validate_InvalidSignature_ReturnsError() public void Validate_InvalidSignature_ReturnsError()
{ {
var req = Valid(); var req = new AirgapImportRequest
req.Signature = "???"; {
BundleId = "bundle-123",
MirrorGeneration = "5",
Publisher = "stellaops",
PayloadHash = "sha256:" + new string('b', 64),
Signature = "???",
SignedAt = _now
};
var result = _validator.Validate(req, _now); var result = _validator.Validate(req, _now);
@@ -53,8 +67,15 @@ public sealed class AirgapImportValidatorTests
[Fact] [Fact]
public void Validate_MirrorGenerationNonNumeric_ReturnsError() public void Validate_MirrorGenerationNonNumeric_ReturnsError()
{ {
var req = Valid(); var req = new AirgapImportRequest
req.MirrorGeneration = "abc"; {
BundleId = "bundle-123",
MirrorGeneration = "abc",
Publisher = "stellaops",
PayloadHash = "sha256:" + new string('b', 64),
Signature = Convert.ToBase64String(new byte[] { 5, 6, 7 }),
SignedAt = _now
};
var result = _validator.Validate(req, _now); var result = _validator.Validate(req, _now);
@@ -64,8 +85,15 @@ public sealed class AirgapImportValidatorTests
[Fact] [Fact]
public void Validate_SignedAtTooOld_ReturnsError() public void Validate_SignedAtTooOld_ReturnsError()
{ {
var req = Valid(); var req = new AirgapImportRequest
req.SignedAt = _now.AddSeconds(-10); {
BundleId = "bundle-123",
MirrorGeneration = "5",
Publisher = "stellaops",
PayloadHash = "sha256:" + new string('b', 64),
Signature = Convert.ToBase64String(new byte[] { 5, 6, 7 }),
SignedAt = _now.AddSeconds(-10)
};
var result = _validator.Validate(req, _now); var result = _validator.Validate(req, _now);

View File

@@ -0,0 +1,44 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Options;
using StellaOps.Excititor.WebService.Services;
using Xunit;
namespace StellaOps.Excititor.WebService.Tests;
public class AirgapModeEnforcerTests
{
[Fact]
public void Validate_Allows_WhenNotSealed()
{
var enforcer = new AirgapModeEnforcer(Microsoft.Extensions.Options.Options.Create(new AirgapOptions { SealedMode = false }), NullLogger<AirgapModeEnforcer>.Instance);
var ok = enforcer.Validate(new AirgapImportRequest { PayloadUrl = "https://example.com" }, out var code, out var message);
Assert.True(ok);
Assert.Null(code);
Assert.Null(message);
}
[Fact]
public void Validate_Blocks_ExternalUrl_WhenSealed()
{
var enforcer = new AirgapModeEnforcer(Microsoft.Extensions.Options.Options.Create(new AirgapOptions { SealedMode = true, MirrorOnly = true }), NullLogger<AirgapModeEnforcer>.Instance);
var ok = enforcer.Validate(new AirgapImportRequest { PayloadUrl = "https://example.com" }, out var code, out var message);
Assert.False(ok);
Assert.Equal("AIRGAP_EGRESS_BLOCKED", code);
Assert.NotNull(message);
}
[Fact]
public void Validate_Blocks_Untrusted_Publisher_WhenAllowlistSet()
{
var enforcer = new AirgapModeEnforcer(Microsoft.Extensions.Options.Options.Create(new AirgapOptions { SealedMode = true, TrustedPublishers = { "mirror-a" } }), NullLogger<AirgapModeEnforcer>.Instance);
var ok = enforcer.Validate(new AirgapImportRequest { Publisher = "mirror-b" }, out var code, out var message);
Assert.False(ok);
Assert.Equal("AIRGAP_SOURCE_UNTRUSTED", code);
Assert.NotNull(message);
}
}

View File

@@ -29,6 +29,8 @@
<ItemGroup> <ItemGroup>
<Compile Remove="**/*.cs" /> <Compile Remove="**/*.cs" />
<Compile Include="AirgapImportEndpointTests.cs" /> <Compile Include="AirgapImportEndpointTests.cs" />
<Compile Include="AirgapImportValidatorTests.cs" />
<Compile Include="AirgapModeEnforcerTests.cs" />
<Compile Include="EvidenceTelemetryTests.cs" /> <Compile Include="EvidenceTelemetryTests.cs" />
<Compile Include="DevRuntimeEnvironmentStub.cs" /> <Compile Include="DevRuntimeEnvironmentStub.cs" />
<Compile Include="TestAuthentication.cs" /> <Compile Include="TestAuthentication.cs" />

View File

@@ -0,0 +1,69 @@
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Notifier.Tests.Support;
using StellaOps.Notifier.WebService.Contracts;
using StellaOps.Notify.Queue;
using Xunit;
namespace StellaOps.Notifier.Tests;
public sealed class AttestationEventEndpointTests : IClassFixture<NotifierApplicationFactory>
{
private readonly NotifierApplicationFactory _factory;
public AttestationEventEndpointTests(NotifierApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task Attestation_event_is_published_to_queue()
{
var recordingQueue = new RecordingNotifyEventQueue();
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<INotifyEventQueue>();
services.AddSingleton<INotifyEventQueue>(recordingQueue);
});
}).CreateClient();
var request = new AttestationEventRequest
{
EventId = Guid.NewGuid(),
Kind = "authority.keys.rotated",
Actor = "authority",
Timestamp = DateTimeOffset.Parse("2025-11-24T00:00:00Z"),
Payload = new System.Text.Json.Nodes.JsonObject
{
["rotation"] = new System.Text.Json.Nodes.JsonObject
{
["batchId"] = "batch-42",
["executedAt"] = "2025-11-24T00:00:00Z"
}
}
};
var message = new HttpRequestMessage(HttpMethod.Post, "/api/v1/notify/attestation-events")
{
Content = JsonContent.Create(request)
};
message.Headers.Add("X-StellaOps-Tenant", "tenant-sample");
var response = await client.SendAsync(message, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
Assert.Single(recordingQueue.Published);
var published = recordingQueue.Published.Single();
Assert.Equal("authority.keys.rotated", published.Event.Kind);
Assert.Equal("tenant-sample", published.Event.Tenant);
Assert.Equal("notify:events", published.Stream);
}
}

View File

@@ -0,0 +1,62 @@
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Notifier.Tests.Support;
using StellaOps.Notifier.WebService.Setup;
using Xunit;
namespace StellaOps.Notifier.Tests;
public sealed class AttestationTemplateSeederTests
{
[Fact]
public async Task SeedTemplates_and_routing_load_from_offline_bundle()
{
var templateRepo = new InMemoryTemplateRepository();
var channelRepo = new InMemoryChannelRepository();
var ruleRepo = new InMemoryRuleRepository();
var logger = NullLogger<AttestationTemplateSeeder>.Instance;
var contentRoot = LocateRepoRoot();
var seededTemplates = await AttestationTemplateSeeder.SeedTemplatesAsync(
templateRepo,
contentRoot,
logger,
TestContext.Current.CancellationToken);
var seededRouting = await AttestationTemplateSeeder.SeedRoutingAsync(
channelRepo,
ruleRepo,
contentRoot,
logger,
TestContext.Current.CancellationToken);
Assert.True(seededTemplates >= 6, "Expected attestation templates to be seeded.");
Assert.True(seededRouting >= 3, "Expected attestation routing seed to create channels and rules.");
var templates = await templateRepo.ListAsync("bootstrap", TestContext.Current.CancellationToken);
Assert.Contains(templates, t => t.Key == "tmpl-attest-key-rotation");
Assert.Contains(templates, t => t.Key == "tmpl-attest-transparency-anomaly");
var rules = await ruleRepo.ListAsync("bootstrap", TestContext.Current.CancellationToken);
Assert.Contains(rules, r => r.Match.EventKinds.Contains("authority.keys.rotated"));
Assert.Contains(rules, r => r.Match.EventKinds.Contains("attestor.transparency.anomaly"));
}
private static string LocateRepoRoot()
{
var directory = AppContext.BaseDirectory;
while (directory != null)
{
if (File.Exists(Path.Combine(directory, "StellaOps.sln")) ||
File.Exists(Path.Combine(directory, "StellaOps.Notifier.sln")))
{
return directory;
}
directory = Directory.GetParent(directory)?.FullName;
}
throw new InvalidOperationException("Unable to locate repository root.");
}
}

View File

@@ -0,0 +1,70 @@
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Notifier.Tests.Support;
using StellaOps.Notifier.WebService.Contracts;
using StellaOps.Notify.Queue;
using Xunit;
namespace StellaOps.Notifier.Tests;
public sealed class RiskEventEndpointTests : IClassFixture<NotifierApplicationFactory>
{
private readonly NotifierApplicationFactory _factory;
public RiskEventEndpointTests(NotifierApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task Risk_event_is_published_to_queue()
{
var recordingQueue = new RecordingNotifyEventQueue();
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<INotifyEventQueue>();
services.AddSingleton<INotifyEventQueue>(recordingQueue);
});
}).CreateClient();
var request = new RiskEventRequest
{
EventId = Guid.NewGuid(),
Kind = "risk.profile.severity.changed",
Actor = "risk-engine",
Timestamp = DateTimeOffset.Parse("2025-11-24T00:00:00Z"),
Payload = new System.Text.Json.Nodes.JsonObject
{
["profile"] = new System.Text.Json.Nodes.JsonObject
{
["id"] = "stellaops://risk/profile/example@2025.11",
["version"] = "2025.11"
},
["previous"] = new System.Text.Json.Nodes.JsonObject { ["severity"] = "medium" },
["current"] = new System.Text.Json.Nodes.JsonObject { ["severity"] = "high" }
}
};
var message = new HttpRequestMessage(HttpMethod.Post, "/api/v1/notify/risk-events")
{
Content = JsonContent.Create(request)
};
message.Headers.Add("X-StellaOps-Tenant", "tenant-sample");
var response = await client.SendAsync(message, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
Assert.Single(recordingQueue.Published);
var published = recordingQueue.Published.Single();
Assert.Equal("risk.profile.severity.changed", published.Event.Kind);
Assert.Equal("tenant-sample", published.Event.Tenant);
Assert.Equal("notify:events", published.Stream);
}
}

View File

@@ -0,0 +1,62 @@
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Notifier.Tests.Support;
using StellaOps.Notifier.WebService.Setup;
using Xunit;
namespace StellaOps.Notifier.Tests;
public sealed class RiskTemplateSeederTests
{
[Fact]
public async Task SeedTemplates_and_routing_load_from_offline_bundle()
{
var templateRepo = new InMemoryTemplateRepository();
var channelRepo = new InMemoryChannelRepository();
var ruleRepo = new InMemoryRuleRepository();
var logger = NullLogger<RiskTemplateSeeder>.Instance;
var contentRoot = LocateRepoRoot();
var seededTemplates = await RiskTemplateSeeder.SeedTemplatesAsync(
templateRepo,
contentRoot,
logger,
TestContext.Current.CancellationToken);
var seededRouting = await RiskTemplateSeeder.SeedRoutingAsync(
channelRepo,
ruleRepo,
contentRoot,
logger,
TestContext.Current.CancellationToken);
Assert.True(seededTemplates >= 4, "Expected risk templates to be seeded.");
Assert.True(seededRouting >= 4, "Expected risk routing seed to create channels and rules.");
var templates = await templateRepo.ListAsync("bootstrap", TestContext.Current.CancellationToken);
Assert.Contains(templates, t => t.Key == "tmpl-risk-severity-change");
Assert.Contains(templates, t => t.Key == "tmpl-risk-profile-state");
var rules = await ruleRepo.ListAsync("bootstrap", TestContext.Current.CancellationToken);
Assert.Contains(rules, r => r.Match.EventKinds.Contains("risk.profile.severity.changed"));
Assert.Contains(rules, r => r.Match.EventKinds.Contains("risk.profile.published"));
}
private static string LocateRepoRoot()
{
var directory = AppContext.BaseDirectory;
while (directory != null)
{
if (File.Exists(Path.Combine(directory, "StellaOps.sln")) ||
File.Exists(Path.Combine(directory, "StellaOps.Notifier.sln")))
{
return directory;
}
directory = Directory.GetParent(directory)?.FullName;
}
throw new InvalidOperationException("Unable to locate repository root.");
}
}

View File

@@ -0,0 +1,21 @@
using StellaOps.Notify.Queue;
namespace StellaOps.Notifier.Tests.Support;
internal sealed class RecordingNotifyEventQueue : INotifyEventQueue
{
private readonly List<NotifyQueueEventMessage> _messages = new();
public IReadOnlyList<NotifyQueueEventMessage> Published => _messages;
public ValueTask<IReadOnlyList<INotifyQueueLease<NotifyQueueEventMessage>>> LeaseAsync(NotifyQueueLeaseRequest request, CancellationToken cancellationToken = default)
=> ValueTask.FromResult<IReadOnlyList<INotifyQueueLease<NotifyQueueEventMessage>>>(Array.Empty<INotifyQueueLease<NotifyQueueEventMessage>>());
public ValueTask PublishAsync(NotifyQueueEventMessage message, CancellationToken cancellationToken = default)
{
_messages.Add(message);
return ValueTask.CompletedTask;
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace StellaOps.Notifier.WebService.Contracts;
public sealed record AttestationEventRequest
{
public Guid EventId { get; init; }
/// <summary>
/// Event kind, e.g. authority.keys.rotated, authority.keys.revoked, attestor.transparency.anomaly.
/// </summary>
public string? Kind { get; init; }
public string? Actor { get; init; }
public DateTimeOffset? Timestamp { get; init; }
public JsonObject? Payload { get; init; }
public IDictionary<string, string>? Attributes { get; init; }
public string? ResumeToken { get; init; }
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace StellaOps.Notifier.WebService.Contracts;
public sealed record RiskEventRequest
{
public Guid EventId { get; init; }
/// <summary>
/// risk.profile.severity.changed | risk.profile.published | risk.profile.deprecated | risk.profile.thresholds.changed
/// </summary>
public string? Kind { get; init; }
public string? Actor { get; init; }
public DateTimeOffset? Timestamp { get; init; }
public JsonObject? Payload { get; init; }
public IDictionary<string, string>? Attributes { get; init; }
public string? ResumeToken { get; init; }
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
@@ -31,6 +32,8 @@ if (!isTesting)
builder.Services.AddNotifyMongoStorage(mongoSection); builder.Services.AddNotifyMongoStorage(mongoSection);
builder.Services.AddHostedService<MongoInitializationHostedService>(); builder.Services.AddHostedService<MongoInitializationHostedService>();
builder.Services.AddHostedService<PackApprovalTemplateSeeder>(); builder.Services.AddHostedService<PackApprovalTemplateSeeder>();
builder.Services.AddHostedService<AttestationTemplateSeeder>();
builder.Services.AddHostedService<RiskTemplateSeeder>();
} }
// Fallback no-op event queue for environments that do not configure a real backend. // Fallback no-op event queue for environments that do not configure a real backend.
@@ -173,6 +176,122 @@ app.MapPost("/api/v1/notify/pack-approvals", async (
return Results.Accepted(); return Results.Accepted();
}); });
app.MapPost("/api/v1/notify/attestation-events", async (
HttpContext context,
AttestationEventRequest request,
INotifyEventQueue? eventQueue,
TimeProvider timeProvider) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
if (string.IsNullOrWhiteSpace(request.Kind))
{
return Results.BadRequest(Error("invalid_request", "kind is required.", context));
}
var eventId = request.EventId != Guid.Empty ? request.EventId : Guid.NewGuid();
var ts = request.Timestamp is { } tsValue && tsValue != default ? tsValue : timeProvider.GetUtcNow();
if (eventQueue is not null)
{
var payload = request.Payload ?? new JsonObject();
var notifyEvent = NotifyEvent.Create(
eventId: eventId,
kind: request.Kind!,
tenant: tenantId,
ts: ts,
payload: payload,
attributes: request.Attributes ?? new Dictionary<string, string>(),
actor: request.Actor,
version: "1");
var idempotencyKey = context.Request.Headers["Idempotency-Key"].ToString();
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
idempotencyKey = $"attestation|{tenantId}|{notifyEvent.Kind}|{notifyEvent.EventId}";
}
await eventQueue.PublishAsync(
new NotifyQueueEventMessage(
notifyEvent,
stream: "notify:events",
idempotencyKey: idempotencyKey,
partitionKey: tenantId,
traceId: context.TraceIdentifier),
context.RequestAborted).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(request.ResumeToken))
{
context.Response.Headers["X-Resume-After"] = request.ResumeToken;
}
return Results.Accepted();
});
app.MapPost("/api/v1/notify/risk-events", async (
HttpContext context,
RiskEventRequest request,
INotifyEventQueue? eventQueue,
TimeProvider timeProvider) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
if (string.IsNullOrWhiteSpace(request.Kind))
{
return Results.BadRequest(Error("invalid_request", "kind is required.", context));
}
var eventId = request.EventId != Guid.Empty ? request.EventId : Guid.NewGuid();
var ts = request.Timestamp is { } tsValue && tsValue != default ? tsValue : timeProvider.GetUtcNow();
if (eventQueue is not null)
{
var payload = request.Payload ?? new JsonObject();
var notifyEvent = NotifyEvent.Create(
eventId: eventId,
kind: request.Kind!,
tenant: tenantId,
ts: ts,
payload: payload,
attributes: request.Attributes ?? new Dictionary<string, string>(),
actor: request.Actor,
version: "1");
var idempotencyKey = context.Request.Headers["Idempotency-Key"].ToString();
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
idempotencyKey = $"risk|{tenantId}|{notifyEvent.Kind}|{notifyEvent.EventId}";
}
await eventQueue.PublishAsync(
new NotifyQueueEventMessage(
notifyEvent,
stream: "notify:events",
idempotencyKey: idempotencyKey,
partitionKey: tenantId,
traceId: context.TraceIdentifier),
context.RequestAborted).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(request.ResumeToken))
{
context.Response.Headers["X-Resume-After"] = request.ResumeToken;
}
return Results.Accepted();
});
app.MapPost("/api/v1/notify/pack-approvals/{packId}/ack", async ( app.MapPost("/api/v1/notify/pack-approvals/{packId}/ack", async (
HttpContext context, HttpContext context,
string packId, string packId,

Some files were not shown because too many files have changed in this diff Show More