work
This commit is contained in:
44
.gitea/workflows/concelier-attestation-tests.yml
Normal file
44
.gitea/workflows/concelier-attestation-tests.yml
Normal 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'
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
70
docs/advisory-ai/cli.md
Normal 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.
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
55
docs/advisory-ai/sbom-context-hand-off.md
Normal file
55
docs/advisory-ai/sbom-context-hand-off.md
Normal 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).
|
||||||
80
docs/devportal/publishing.md
Normal file
80
docs/devportal/publishing.md
Normal 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.
|
||||||
@@ -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/`.
|
||||||
|
|||||||
@@ -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 (air‑gap 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.
|
||||||
|
|||||||
8
docs/modules/advisory-ai/TASKS.md
Normal file
8
docs/modules/advisory-ai/TASKS.md
Normal 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. |
|
||||||
|
|
||||||
19
docs/modules/attestor/prep/2025-11-24-attest-plan-2001.md
Normal file
19
docs/modules/attestor/prep/2025-11-24-attest-plan-2001.md
Normal 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.
|
||||||
25
docs/modules/cli/guides/commands/advisory.md
Normal file
25
docs/modules/cli/guides/commands/advisory.md
Normal 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.
|
||||||
21
docs/modules/cli/guides/commands/aoc.md
Normal file
21
docs/modules/cli/guides/commands/aoc.md
Normal 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.
|
||||||
19
docs/modules/cli/guides/commands/auth.md
Normal file
19
docs/modules/cli/guides/commands/auth.md
Normal 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.
|
||||||
25
docs/modules/cli/guides/commands/export.md
Normal file
25
docs/modules/cli/guides/commands/export.md
Normal 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.
|
||||||
24
docs/modules/cli/guides/commands/notify.md
Normal file
24
docs/modules/cli/guides/commands/notify.md
Normal 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`.
|
||||||
23
docs/modules/cli/guides/commands/orchestrator.md
Normal file
23
docs/modules/cli/guides/commands/orchestrator.md
Normal 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`.
|
||||||
25
docs/modules/cli/guides/commands/policy.md
Normal file
25
docs/modules/cli/guides/commands/policy.md
Normal 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.
|
||||||
25
docs/modules/cli/guides/commands/sbom.md
Normal file
25
docs/modules/cli/guides/commands/sbom.md
Normal 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.
|
||||||
23
docs/modules/cli/guides/commands/vex.md
Normal file
23
docs/modules/cli/guides/commands/vex.md
Normal 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.
|
||||||
25
docs/modules/cli/guides/commands/vuln.md
Normal file
25
docs/modules/cli/guides/commands/vuln.md
Normal 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.
|
||||||
40
docs/modules/cli/guides/configuration.md
Normal file
40
docs/modules/cli/guides/configuration.md
Normal 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.
|
||||||
32
docs/modules/cli/guides/forensics.md
Normal file
32
docs/modules/cli/guides/forensics.md
Normal 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
|
||||||
|
```
|
||||||
32
docs/modules/cli/guides/observability.md
Normal file
32
docs/modules/cli/guides/observability.md
Normal 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]'
|
||||||
|
```
|
||||||
34
docs/modules/cli/guides/output-and-exit-codes.md
Normal file
34
docs/modules/cli/guides/output-and-exit-codes.md
Normal 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 `2–5` 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.
|
||||||
32
docs/modules/cli/guides/overview.md
Normal file
32
docs/modules/cli/guides/overview.md
Normal 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/`.
|
||||||
19
docs/modules/cli/guides/parity-matrix.md
Normal file
19
docs/modules/cli/guides/parity-matrix.md
Normal 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.
|
||||||
73
docs/modules/concelier/api/evidence-batch.md
Normal file
73
docs/modules/concelier/api/evidence-batch.md
Normal 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.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
7
docs/modules/vuln-explorer/api.md
Normal file
7
docs/modules/vuln-explorer/api.md
Normal 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.
|
||||||
188
docs/modules/vuln-explorer/openapi/vuln-explorer.v1.yaml
Normal file
188
docs/modules/vuln-explorer/openapi/vuln-explorer.v1.yaml
Normal 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
|
||||||
@@ -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 AI’s 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 Core’s double-underscore convention (`ADVISORYAI__Inference__Mode`, etc.).
|
This note centralises the tunable knobs that control Advisory AI’s 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 Core’s 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`. |
|
||||||
|
|||||||
42
docs/runbooks/assistant-ops.md
Normal file
42
docs/runbooks/assistant-ops.md
Normal 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).
|
||||||
44
docs/runbooks/concelier-airgap-bundle-deploy.md
Normal file
44
docs/runbooks/concelier-airgap-bundle-deploy.md
Normal 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.
|
||||||
3
docs/samples/airgap/concelier-airgap-sample.ndjson
Normal file
3
docs/samples/airgap/concelier-airgap-sample.ndjson
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
a:1
|
||||||
|
b:2
|
||||||
|
c:3
|
||||||
43
docs/sbom/remediation-heuristics.md
Normal file
43
docs/sbom/remediation-heuristics.md
Normal 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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
32
ops/devops/ci-110-runner/README.md
Normal file
32
ops/devops/ci-110-runner/README.md
Normal 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.
|
||||||
92
ops/devops/ci-110-runner/run-ci-110.sh
Normal file
92
ops/devops/ci-110-runner/run-ci-110.sh
Normal 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 "$@"
|
||||||
26
ops/devops/concelier-ci-runner/README.md
Normal file
26
ops/devops/concelier-ci-runner/README.md
Normal 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.
|
||||||
75
ops/devops/concelier-ci-runner/run-concelier-ci.sh
Normal file
75
ops/devops/concelier-ci-runner/run-concelier-ci.sh
Normal 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}/}"
|
||||||
49
ops/devops/observability/incident-mode.md
Normal file
49
ops/devops/observability/incident-mode.md
Normal 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).
|
||||||
36
ops/devops/orchestrator/README.md
Normal file
36
ops/devops/orchestrator/README.md
Normal 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.
|
||||||
30
ops/devops/orchestrator/alerts.yaml
Normal file
30
ops/devops/orchestrator/alerts.yaml
Normal 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"
|
||||||
49
ops/devops/orchestrator/docker-compose.orchestrator.yml
Normal file
49
ops/devops/orchestrator/docker-compose.orchestrator.yml
Normal 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:
|
||||||
42
ops/devops/orchestrator/grafana/orchestrator-overview.json
Normal file
42
ops/devops/orchestrator/grafana/orchestrator-overview.json
Normal 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"}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
134
scripts/observability/incident-mode.sh
Normal file
134
scripts/observability/incident-mode.sh
Normal 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
|
||||||
59
scripts/orchestrator/smoke.sh
Normal file
59
scripts/orchestrator/smoke.sh
Normal 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"
|
||||||
@@ -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`). |
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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"))
|
||||||
{
|
{
|
||||||
|
// Allow a fully pre-bound options instance to be supplied by the test host.
|
||||||
|
#pragma warning disable ASP0000 // test-only: create provider to fetch pre-bound options
|
||||||
|
using var tempProvider = builder.Services.BuildServiceProvider();
|
||||||
|
#pragma warning restore ASP0000
|
||||||
|
concelierOptions = tempProvider.GetService<IOptions<ConcelierOptions>>()?.Value ?? new ConcelierOptions
|
||||||
|
{
|
||||||
|
Storage = new ConcelierOptions.StorageOptions
|
||||||
|
{
|
||||||
|
Dsn = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? "mongodb://localhost:27017/test-health",
|
||||||
|
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);
|
ConcelierOptionsPostConfigure.Apply(opts, contentRootPath);
|
||||||
ConcelierOptionsValidator.Validate(opts);
|
var skipValidation = string.Equals(Environment.GetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION"), "1", StringComparison.OrdinalIgnoreCase);
|
||||||
});
|
if (!skipValidation)
|
||||||
builder.Services.AddOptions<ConcelierOptions>()
|
|
||||||
.Bind(builder.Configuration)
|
|
||||||
.PostConfigure(options =>
|
|
||||||
{
|
{
|
||||||
ConcelierOptionsPostConfigure.Apply(options, contentRootPath);
|
ConcelierOptionsValidator.Validate(opts);
|
||||||
ConcelierOptionsValidator.Validate(options);
|
}
|
||||||
})
|
});
|
||||||
.ValidateOnStart();
|
}
|
||||||
|
|
||||||
|
// 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,6 +3250,14 @@ var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonSerializerOptions CreateJsonOptions()
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||||
|
options.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot)
|
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot)
|
||||||
{
|
{
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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
44
src/Excititor/AGENTS.md
Normal 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.
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user