docs re-org, audit fixes, build fixes
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -656,7 +656,7 @@ Always update task status in `docs/implplan/SPRINT_*.md`:
|
|||||||
|
|
||||||
Before coding, confirm required docs are read:
|
Before coding, confirm required docs are read:
|
||||||
- `docs/README.md`
|
- `docs/README.md`
|
||||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- `docs/modules/platform/architecture-overview.md`
|
- `docs/modules/platform/architecture-overview.md`
|
||||||
- Relevant module dossier (e.g., `docs/modules/<module>/architecture.md`)
|
- Relevant module dossier (e.g., `docs/modules/<module>/architecture.md`)
|
||||||
- Module-specific `AGENTS.md` file
|
- Module-specific `AGENTS.md` file
|
||||||
@@ -674,13 +674,14 @@ Before coding, confirm required docs are read:
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **Architecture overview:** `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- **Architecture overview:** `docs/ARCHITECTURE_OVERVIEW.md`
|
||||||
|
- **Architecture reference:** `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- **Module dossiers:** `docs/modules/<module>/architecture.md`
|
- **Module dossiers:** `docs/modules/<module>/architecture.md`
|
||||||
- **Database specification:** `docs/db/SPECIFICATION.md`
|
- **Database specification:** `docs/db/SPECIFICATION.md`
|
||||||
- **PostgreSQL operations:** `docs/operations/postgresql-guide.md`
|
- **PostgreSQL operations:** `docs/operations/postgresql-guide.md`
|
||||||
- **API/CLI reference:** `docs/09_API_CLI_REFERENCE.md`
|
- **API/CLI reference:** `docs/API_CLI_REFERENCE.md`
|
||||||
- **Offline operation:** `docs/24_OFFLINE_KIT.md`
|
- **Offline operation:** `docs/OFFLINE_KIT.md`
|
||||||
- **Quickstart:** `docs/10_CONCELIER_CLI_QUICKSTART.md`
|
- **Quickstart:** `docs/CONCELIER_CLI_QUICKSTART.md`
|
||||||
- **Sprint planning:** `docs/implplan/SPRINT_*.md`
|
- **Sprint planning:** `docs/implplan/SPRINT_*.md`
|
||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- Only documentation and fixture assets live here; code changes belong to module repos and must be coordinated via the owning sprint.
|
- Only documentation and fixture assets live here; code changes belong to module repos and must be coordinated via the owning sprint.
|
||||||
|
|
||||||
## Required Reading (treat as read before DOING)
|
## Required Reading (treat as read before DOING)
|
||||||
- `docs/README.md` and `docs/07_HIGH_LEVEL_ARCHITECTURE.md`.
|
- `docs/README.md` and `docs/ARCHITECTURE_REFERENCE.md`.
|
||||||
- Module dossiers relevant to the document being edited (e.g., `docs/modules/advisory-ai/architecture.md`, `docs/modules/ui/architecture.md`, `docs/modules/airgap/architecture.md`, `docs/modules/platform/architecture-overview.md`).
|
- Module dossiers relevant to the document being edited (e.g., `docs/modules/advisory-ai/architecture.md`, `docs/modules/ui/architecture.md`, `docs/modules/airgap/architecture.md`, `docs/modules/platform/architecture-overview.md`).
|
||||||
- Active sprint file: `docs/implplan/SPRINT_0301_0001_0001_docs_md_i.md` (Docs Tasks Md.I).
|
- Active sprint file: `docs/implplan/SPRINT_0301_0001_0001_docs_md_i.md` (Docs Tasks Md.I).
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# Architecture Overview (High-Level)
|
# Architecture Overview (High-Level)
|
||||||
|
|
||||||
This document is the 10-minute tour for StellaOps: what components exist, how they fit together, and what "offline-first + deterministic + evidence-linked decisions" means in practice.
|
This document is the 10-minute tour for StellaOps: what components exist, how they fit together, and what "offline-first + deterministic + evidence-linked decisions" means in practice.
|
||||||
|
|
||||||
For the full reference map (services, boundaries, detailed flows), see `docs/07_HIGH_LEVEL_ARCHITECTURE.md`.
|
For the full reference map (services, boundaries, detailed flows), see `docs/ARCHITECTURE_REFERENCE.md`.
|
||||||
|
|
||||||
## Guiding Principles
|
## Guiding Principles
|
||||||
|
|
||||||
- **SBOM-first:** scan and reason over SBOMs; fall back to unpacking only when needed.
|
- **SBOM-first:** scan and reason over SBOMs; fall back to unpacking only when needed.
|
||||||
- **Deterministic replay:** the same inputs yield the same outputs (stable ordering, canonical hashing, UTC timestamps).
|
- **Deterministic replay:** the same inputs yield the same outputs (stable ordering, canonical hashing, UTC timestamps).
|
||||||
- **Evidence-linked decisions:** policy decisions link back to specific evidence artifacts (SBOM slices, advisory/VEX observations, reachability proofs, attestations).
|
- **Evidence-linked decisions:** policy decisions link back to specific evidence artifacts (SBOM slices, advisory/VEX observations, reachability proofs, attestations).
|
||||||
- **Aggregation-not-merge:** upstream advisories and VEX are stored and exposed with provenance; conflicts are visible, not silently collapsed.
|
- **Aggregation-not-merge:** upstream advisories and VEX are stored and exposed with provenance; conflicts are visible, not silently collapsed.
|
||||||
- **Offline-first:** the same workflow runs connected or air-gapped via Offline Kit snapshots and signed bundles.
|
- **Offline-first:** the same workflow runs connected or air-gapped via Offline Kit snapshots and signed bundles.
|
||||||
|
|
||||||
## System Map (What Runs)
|
## System Map (What Runs)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -23,9 +23,9 @@ At a high level, StellaOps is a set of services grouped by responsibility:
|
|||||||
- **Identity and authorization:** Authority (OIDC/OAuth2, scopes/tenancy)
|
- **Identity and authorization:** Authority (OIDC/OAuth2, scopes/tenancy)
|
||||||
- **Scanning and SBOM:** Scanner WebService + Worker (facts generation)
|
- **Scanning and SBOM:** Scanner WebService + Worker (facts generation)
|
||||||
- **Advisories:** Concelier (ingest/normalize/export vulnerability sources)
|
- **Advisories:** Concelier (ingest/normalize/export vulnerability sources)
|
||||||
- **VEX:** Excititor + VEX Lens (VEX observations/linksets and exploration)
|
- **VEX:** Excititor + VEX Lens (VEX observations/linksets and exploration)
|
||||||
- **Decisioning:** Policy Engine surfaces (lattice-style explainable policy)
|
- **Decisioning:** Policy Engine surfaces (lattice-style explainable policy)
|
||||||
- **Signing and transparency:** Signer + Attestor (DSSE/in-toto and optional transparency)
|
- **Signing and transparency:** Signer + Attestor (DSSE/in-toto and optional transparency)
|
||||||
- **Orchestration and delivery:** Scheduler, Notify, Export Center
|
- **Orchestration and delivery:** Scheduler, Notify, Export Center
|
||||||
- **Console:** Web UI for operators and auditors
|
- **Console:** Web UI for operators and auditors
|
||||||
|
|
||||||
@@ -38,18 +38,18 @@ At a high level, StellaOps is a set of services grouped by responsibility:
|
|||||||
| **Data plane** | PostgreSQL, Valkey, RustFS/object storage (optional NATS JetStream) | Canonical store, counters/queues, and artifact storage with deterministic layouts. |
|
| **Data plane** | PostgreSQL, Valkey, RustFS/object storage (optional NATS JetStream) | Canonical store, counters/queues, and artifact storage with deterministic layouts. |
|
||||||
|
|
||||||
## Infrastructure (What Is Required)
|
## Infrastructure (What Is Required)
|
||||||
|
|
||||||
**Required**
|
**Required**
|
||||||
|
|
||||||
- **PostgreSQL:** canonical persistent store for module schemas.
|
- **PostgreSQL:** canonical persistent store for module schemas.
|
||||||
- **Valkey:** Redis-compatible cache/streams and DPoP nonce store.
|
- **Valkey:** Redis-compatible cache/streams and DPoP nonce store.
|
||||||
- **RustFS (or equivalent S3-compatible store):** object storage for artifacts, bundles, and evidence.
|
- **RustFS (or equivalent S3-compatible store):** object storage for artifacts, bundles, and evidence.
|
||||||
|
|
||||||
**Optional (deployment-dependent)**
|
**Optional (deployment-dependent)**
|
||||||
|
|
||||||
- **NATS JetStream:** optional messaging transport in some deployments.
|
- **NATS JetStream:** optional messaging transport in some deployments.
|
||||||
- **Transparency log services:** Rekor mirror (and CA services) when transparency is enabled.
|
- **Transparency log services:** Rekor mirror (and CA services) when transparency is enabled.
|
||||||
|
|
||||||
## End-to-End Flow (Typical)
|
## End-to-End Flow (Typical)
|
||||||
|
|
||||||
1. **Evidence enters** via Concelier and Excititor connectors (Aggregation-Only Contract).
|
1. **Evidence enters** via Concelier and Excititor connectors (Aggregation-Only Contract).
|
||||||
@@ -58,12 +58,12 @@ At a high level, StellaOps is a set of services grouped by responsibility:
|
|||||||
4. **Policy Engine** merges advisories, VEX, and inventory/usage facts; emits explain traces and stable dispositions.
|
4. **Policy Engine** merges advisories, VEX, and inventory/usage facts; emits explain traces and stable dispositions.
|
||||||
5. **Signer + Attestor** wrap outputs into DSSE bundles and (optionally) anchor them in a Rekor mirror.
|
5. **Signer + Attestor** wrap outputs into DSSE bundles and (optionally) anchor them in a Rekor mirror.
|
||||||
6. **Console/CLI/Export** surface findings and package verifiable evidence; Notify emits digests/incidents.
|
6. **Console/CLI/Export** surface findings and package verifiable evidence; Notify emits digests/incidents.
|
||||||
|
|
||||||
## Extension Points (Where You Customize)
|
## Extension Points (Where You Customize)
|
||||||
|
|
||||||
- **Scanner analyzers** (restart-time plug-ins) for ecosystem-specific parsing and facts extraction.
|
- **Scanner analyzers** (restart-time plug-ins) for ecosystem-specific parsing and facts extraction.
|
||||||
- **Concelier connectors** for new advisory sources (preserving aggregation-only guardrails).
|
- **Concelier connectors** for new advisory sources (preserving aggregation-only guardrails).
|
||||||
- **Policy packs** for organization-specific gating and waivers/justifications.
|
- **Policy packs** for organization-specific gating and waivers/justifications.
|
||||||
- **Export profiles** for output formats and offline bundle shapes.
|
- **Export profiles** for output formats and offline bundle shapes.
|
||||||
|
|
||||||
## Offline & Sovereign Notes
|
## Offline & Sovereign Notes
|
||||||
@@ -73,8 +73,8 @@ At a high level, StellaOps is a set of services grouped by responsibility:
|
|||||||
- Attestor can cache transparency proofs for offline verification.
|
- Attestor can cache transparency proofs for offline verification.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- `docs/24_OFFLINE_KIT.md`
|
- `docs/OFFLINE_KIT.md`
|
||||||
- `docs/09_API_CLI_REFERENCE.md`
|
- `docs/API_CLI_REFERENCE.md`
|
||||||
- `docs/modules/platform/architecture-overview.md`
|
- `docs/modules/platform/architecture-overview.md`
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
This document is the canonical index for StellaOps architecture.
|
This document is the canonical index for StellaOps architecture.
|
||||||
It is intentionally a map, not a full re-statement of every module dossier.
|
It is intentionally a map, not a full re-statement of every module dossier.
|
||||||
|
|
||||||
If you want a short walkthrough, start with `docs/40_ARCHITECTURE_OVERVIEW.md`.
|
If you want a short walkthrough, start with `docs/ARCHITECTURE_OVERVIEW.md`.
|
||||||
|
|
||||||
## How the docs are organized
|
## How the docs are organized
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ Tenancy and identity context are part of the platform contract:
|
|||||||
## APIs and CLI reference
|
## APIs and CLI reference
|
||||||
|
|
||||||
Canonical entry points:
|
Canonical entry points:
|
||||||
- API and CLI reference hub: `docs/09_API_CLI_REFERENCE.md`
|
- API and CLI reference hub: `docs/API_CLI_REFERENCE.md`
|
||||||
- API conventions (headers, errors, pagination, determinism): `docs/api/overview.md`
|
- API conventions (headers, errors, pagination, determinism): `docs/api/overview.md`
|
||||||
- API contracts and samples: `docs/api/`
|
- API contracts and samples: `docs/api/`
|
||||||
- CLI command guides: `docs/modules/cli/guides/commands/`
|
- CLI command guides: `docs/modules/cli/guides/commands/`
|
||||||
@@ -98,15 +98,15 @@ Canonical entry points:
|
|||||||
## Offline, verification, and operations
|
## Offline, verification, and operations
|
||||||
|
|
||||||
Canonical entry points:
|
Canonical entry points:
|
||||||
- Offline Kit: `docs/24_OFFLINE_KIT.md`
|
- Offline Kit: `docs/OFFLINE_KIT.md`
|
||||||
- Security hardening: `docs/17_SECURITY_HARDENING_GUIDE.md`
|
- Security hardening: `docs/SECURITY_HARDENING_GUIDE.md`
|
||||||
- Installation guide: `docs/21_INSTALL_GUIDE.md`
|
- Installation guide: `docs/INSTALL_GUIDE.md`
|
||||||
- Ops and runbooks: `docs/operations/`, `docs/modules/*/operations/`
|
- Ops and runbooks: `docs/operations/`, `docs/modules/*/operations/`
|
||||||
|
|
||||||
## Data and schemas
|
## Data and schemas
|
||||||
|
|
||||||
Use these as the canonical map for schemas and contracts:
|
Use these as the canonical map for schemas and contracts:
|
||||||
- Data schemas (high-level index): `docs/11_DATA_SCHEMAS.md`
|
- Data schemas (high-level index): `docs/DATA_SCHEMAS.md`
|
||||||
- Database specifications: `docs/db/`
|
- Database specifications: `docs/db/`
|
||||||
- Events (schemas + samples): `docs/events/`
|
- Events (schemas + samples): `docs/events/`
|
||||||
|
|
||||||
@@ -114,5 +114,5 @@ Use these as the canonical map for schemas and contracts:
|
|||||||
|
|
||||||
- Product overview: `docs/overview.md`
|
- Product overview: `docs/overview.md`
|
||||||
- Key features: `docs/key-features.md`
|
- Key features: `docs/key-features.md`
|
||||||
- Roadmap (internal): `docs/05_ROADMAP.md`
|
- Roadmap (internal): `docs/ROADMAP.md`
|
||||||
- Glossary: `docs/14_GLOSSARY_OF_TERMS.md`
|
- Glossary: `docs/GLOSSARY.md`
|
||||||
@@ -8,8 +8,8 @@ This quickstart gets an operator to a working advisory ingestion loop:
|
|||||||
This document stays high level and defers detailed configuration and connector behavior to the Concelier module dossier.
|
This document stays high level and defers detailed configuration and connector behavior to the Concelier module dossier.
|
||||||
|
|
||||||
## 1) Prerequisites
|
## 1) Prerequisites
|
||||||
- Deployment: follow `docs/21_INSTALL_GUIDE.md` (Compose profiles under `deploy/compose/`).
|
- Deployment: follow `docs/INSTALL_GUIDE.md` (Compose profiles under `deploy/compose/`).
|
||||||
- Offline/air-gap: follow `docs/24_OFFLINE_KIT.md` and `docs/airgap/overview.md`.
|
- Offline/air-gap: follow `docs/OFFLINE_KIT.md` and `docs/airgap/overview.md`.
|
||||||
- Local dev (optional): .NET SDK version pinned by `global.json`.
|
- Local dev (optional): .NET SDK version pinned by `global.json`.
|
||||||
|
|
||||||
## 2) Run Concelier
|
## 2) Run Concelier
|
||||||
@@ -18,7 +18,7 @@ This document stays high level and defers detailed configuration and connector b
|
|||||||
Use the deterministic Compose profiles under `deploy/compose/` and enable Concelier in the selected profile.
|
Use the deterministic Compose profiles under `deploy/compose/` and enable Concelier in the selected profile.
|
||||||
|
|
||||||
Start here:
|
Start here:
|
||||||
- `docs/21_INSTALL_GUIDE.md`
|
- `docs/INSTALL_GUIDE.md`
|
||||||
- `docs/modules/concelier/operations/`
|
- `docs/modules/concelier/operations/`
|
||||||
|
|
||||||
### Option B: Run the service from source (dev/debug)
|
### Option B: Run the service from source (dev/debug)
|
||||||
@@ -99,4 +99,4 @@ For read-only inspection (list/get/export), use:
|
|||||||
- Concelier module dossier: `docs/modules/concelier/README.md`
|
- Concelier module dossier: `docs/modules/concelier/README.md`
|
||||||
- Concelier operations: `docs/modules/concelier/operations/`
|
- Concelier operations: `docs/modules/concelier/operations/`
|
||||||
- CLI command guides: `docs/modules/cli/guides/commands/`
|
- CLI command guides: `docs/modules/cli/guides/commands/`
|
||||||
- API + CLI reference index: `docs/09_API_CLI_REFERENCE.md`
|
- API + CLI reference index: `docs/API_CLI_REFERENCE.md`
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
StellaOps is a deterministic, offline-first SBOM + VEX platform built as a microservice architecture. The system is designed so every verdict can be replayed from concrete evidence (SBOM slices, advisory/VEX observations, policy decision traces, and optional attestations).
|
StellaOps is a deterministic, offline-first SBOM + VEX platform built as a microservice architecture. The system is designed so every verdict can be replayed from concrete evidence (SBOM slices, advisory/VEX observations, policy decision traces, and optional attestations).
|
||||||
|
|
||||||
### Canonical references
|
### Canonical references
|
||||||
- Architecture overview (10-minute tour): `docs/40_ARCHITECTURE_OVERVIEW.md`
|
- Architecture overview (10-minute tour): `docs/ARCHITECTURE_OVERVIEW.md`
|
||||||
- High-level reference map: `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- High-level reference map: `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- Detailed architecture index: `docs/technical/architecture/README.md`
|
- Detailed architecture index: `docs/technical/architecture/README.md`
|
||||||
- Topology: `docs/technical/architecture/platform-topology.md`
|
- Topology: `docs/technical/architecture/platform-topology.md`
|
||||||
- Infrastructure: `docs/technical/architecture/infrastructure-dependencies.md`
|
- Infrastructure: `docs/technical/architecture/infrastructure-dependencies.md`
|
||||||
@@ -170,7 +170,7 @@ Canonical guide:
|
|||||||
|
|
||||||
Related references:
|
Related references:
|
||||||
- Compose profiles: `deploy/compose/README.md`
|
- Compose profiles: `deploy/compose/README.md`
|
||||||
- Install guide: `docs/21_INSTALL_GUIDE.md`
|
- Install guide: `docs/INSTALL_GUIDE.md`
|
||||||
- Service-specific runbooks: `docs/modules/<module>/operations/`
|
- Service-specific runbooks: `docs/modules/<module>/operations/`
|
||||||
## Service-by-Service Debugging Guide
|
## Service-by-Service Debugging Guide
|
||||||
|
|
||||||
@@ -711,10 +711,10 @@ sudo docker compose -f docker-compose.dev.yaml up -d
|
|||||||
|
|
||||||
### Key Documentation
|
### Key Documentation
|
||||||
|
|
||||||
- **Architecture:** `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- **Architecture:** `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- **Build Commands:** `CLAUDE.md`
|
- **Build Commands:** `CLAUDE.md`
|
||||||
- **Database Spec:** `docs/db/SPECIFICATION.md`
|
- **Database Spec:** `docs/db/SPECIFICATION.md`
|
||||||
- **API Reference:** `docs/09_API_CLI_REFERENCE.md`
|
- **API Reference:** `docs/API_CLI_REFERENCE.md`
|
||||||
- **Module Architecture:** `docs/modules/<module>/architecture.md`
|
- **Module Architecture:** `docs/modules/<module>/architecture.md`
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
|
|||||||
@@ -6,20 +6,20 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| What is StellaOps? | A sovereign, offline-first container-security platform focused on deterministic, replayable evidence: SBOMs, advisories, VEX, policy decisions, and attestations bound to image digests. |
|
| What is StellaOps? | A sovereign, offline-first container-security platform focused on deterministic, replayable evidence: SBOMs, advisories, VEX, policy decisions, and attestations bound to image digests. |
|
||||||
| What makes it "deterministic"? | The same inputs produce the same outputs (stable ordering, stable IDs, replayable artifacts). Determinism is treated as a product feature and enforced by tests and fixtures. |
|
| What makes it "deterministic"? | The same inputs produce the same outputs (stable ordering, stable IDs, replayable artifacts). Determinism is treated as a product feature and enforced by tests and fixtures. |
|
||||||
| Does it run fully offline? | Yes. Offline operation is a first-class workflow (bundles, mirrors, importer/controller). See `docs/24_OFFLINE_KIT.md` and `docs/airgap/overview.md`. |
|
| Does it run fully offline? | Yes. Offline operation is a first-class workflow (bundles, mirrors, importer/controller). See `docs/OFFLINE_KIT.md` and `docs/airgap/overview.md`. |
|
||||||
| Which formats are supported? | SBOMs: SPDX 3.0.1 and CycloneDX 1.7 (1.6 backward compatible). VEX: OpenVEX-first decisioning with issuer trust and consensus. Attestations: in-toto/DSSE where enabled. |
|
| Which formats are supported? | SBOMs: SPDX 3.0.1 and CycloneDX 1.7 (1.6 backward compatible). VEX: OpenVEX-first decisioning with issuer trust and consensus. Attestations: in-toto/DSSE where enabled. |
|
||||||
| How do I deploy it? | Use deterministic bundles under `deploy/` (Compose/Helm) with digests sourced from `deploy/releases/`. Start with `docs/21_INSTALL_GUIDE.md`. |
|
| How do I deploy it? | Use deterministic bundles under `deploy/` (Compose/Helm) with digests sourced from `deploy/releases/`. Start with `docs/INSTALL_GUIDE.md`. |
|
||||||
| How do policy gates work? | Policy combines VEX-first inputs with lattice/precedence rules so outcomes are stable and explainable. See `docs/policy/vex-trust-model.md`. |
|
| How do policy gates work? | Policy combines VEX-first inputs with lattice/precedence rules so outcomes are stable and explainable. See `docs/policy/vex-trust-model.md`. |
|
||||||
| Is multi-tenancy supported? | Yes; tenancy boundaries and roles/scopes are documented and designed to support regulated environments. See `docs/security/tenancy-overview.md` and `docs/security/scopes-and-roles.md`. |
|
| Is multi-tenancy supported? | Yes; tenancy boundaries and roles/scopes are documented and designed to support regulated environments. See `docs/security/tenancy-overview.md` and `docs/security/scopes-and-roles.md`. |
|
||||||
| Can I extend it? | Yes: connectors, plugins, and policy packs are designed to be composable without losing determinism. Start with module dossiers under `docs/modules/`. |
|
| Can I extend it? | Yes: connectors, plugins, and policy packs are designed to be composable without losing determinism. Start with module dossiers under `docs/modules/`. |
|
||||||
| Where is the roadmap? | `docs/05_ROADMAP.md` (priority bands + definition of "done"). |
|
| Where is the roadmap? | `docs/ROADMAP.md` (priority bands + definition of "done"). |
|
||||||
| Where do I find deeper docs? | `docs/technical/README.md` is the detailed index; `docs/modules/` contains per-module dossiers. |
|
| Where do I find deeper docs? | `docs/technical/README.md` is the detailed index; `docs/modules/` contains per-module dossiers. |
|
||||||
|
|
||||||
## Further reading
|
## Further reading
|
||||||
- Vision: `docs/03_VISION.md`
|
- Vision: `docs/VISION.md`
|
||||||
- Feature matrix: `docs/04_FEATURE_MATRIX.md`
|
- Feature matrix: `docs/FEATURE_MATRIX.md`
|
||||||
- Architecture overview: `docs/40_ARCHITECTURE_OVERVIEW.md`
|
- Architecture overview: `docs/ARCHITECTURE_OVERVIEW.md`
|
||||||
- High-level architecture: `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- High-level architecture: `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- Offline kit: `docs/24_OFFLINE_KIT.md`
|
- Offline kit: `docs/OFFLINE_KIT.md`
|
||||||
- Install guide: `docs/21_INSTALL_GUIDE.md`
|
- Install guide: `docs/INSTALL_GUIDE.md`
|
||||||
- Quickstart: `docs/quickstart.md`
|
- Quickstart: `docs/quickstart.md`
|
||||||
@@ -49,7 +49,7 @@ Approval is recorded via Git forge review or a signed commit trailer
|
|||||||
|
|
||||||
* Every tag is **co‑signed by at least one Security Maintainer**.
|
* Every tag is **co‑signed by at least one Security Maintainer**.
|
||||||
* CI emits a **signed SPDX SBOM** + **Cosign provenance**.
|
* CI emits a **signed SPDX SBOM** + **Cosign provenance**.
|
||||||
* Release cadence is fixed – see [Release Engineering Playbook](13_RELEASE_ENGINEERING_PLAYBOOK.md).
|
* Release cadence is fixed – see [Release Engineering Playbook](RELEASE_ENGINEERING_PLAYBOOK.md).
|
||||||
* Security fixes may create out‑of‑band `x.y.z‑hotfix` tags.
|
* Security fixes may create out‑of‑band `x.y.z‑hotfix` tags.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -59,8 +59,8 @@ Approval is recorded via Git forge review or a signed commit trailer
|
|||||||
| Situation | Escalation |
|
| Situation | Escalation |
|
||||||
|-----------|------------|
|
|-----------|------------|
|
||||||
| Technical deadlock | **Maintainer Summit** (recorded & published) |
|
| Technical deadlock | **Maintainer Summit** (recorded & published) |
|
||||||
| Security bug | Follow [Security Policy](13_SECURITY_POLICY.md) |
|
| Security bug | Follow [Security Policy](SECURITY_POLICY.md) |
|
||||||
| Code of Conduct violation | See `12_CODE_OF_CONDUCT.md` escalation ladder |
|
| Code of Conduct violation | See `CODE_OF_CONDUCT.md` escalation ladder |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ StellaOps is a deterministic, offline-first container security platform: every v
|
|||||||
|
|
||||||
## Two Levels of Documentation
|
## Two Levels of Documentation
|
||||||
|
|
||||||
- **High-level (canonical):** the curated guides in `docs/*.md` (usually numbered).
|
- **High-level (canonical):** the curated guides in `docs/*.md`.
|
||||||
- **Detailed (reference):** deep dives under `docs/**` (module dossiers, architecture notes, API contracts/samples, runbooks, schemas). The entry point is `docs/technical/README.md`.
|
- **Detailed (reference):** deep dives under `docs/**` (module dossiers, architecture notes, API contracts/samples, runbooks, schemas). The entry point is `docs/technical/README.md`.
|
||||||
|
|
||||||
This documentation set is internal and does not keep compatibility stubs for old paths. Content is consolidated to reduce duplication and outdated pages.
|
This documentation set is internal and does not keep compatibility stubs for old paths. Content is consolidated to reduce duplication and outdated pages.
|
||||||
@@ -13,23 +13,23 @@ This documentation set is internal and does not keep compatibility stubs for old
|
|||||||
|
|
||||||
| Goal | Open this |
|
| Goal | Open this |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Understand the product in 2 minutes | [overview.md](/docs/overview/) |
|
| Understand the product in 2 minutes | [overview.md](overview.md) |
|
||||||
| Run a first scan (CLI) | [quickstart.md](/docs/quickstart/) |
|
| Run a first scan (CLI) | [quickstart.md](quickstart.md) |
|
||||||
| Browse capabilities | [key-features.md](/docs/key-features/) |
|
| Browse capabilities | [key-features.md](key-features.md) |
|
||||||
| Roadmap (priorities + definition of "done") | [05_ROADMAP.md](/docs/05_roadmap/) |
|
| Roadmap (priorities + definition of "done") | [ROADMAP.md](ROADMAP.md) |
|
||||||
| Architecture: high-level overview | [40_ARCHITECTURE_OVERVIEW.md](/docs/40_architecture_overview/) |
|
| Architecture: high-level overview | [ARCHITECTURE_OVERVIEW.md](ARCHITECTURE_OVERVIEW.md) |
|
||||||
| Architecture: full reference map | [07_HIGH_LEVEL_ARCHITECTURE.md](/docs/07_high_level_architecture/) |
|
| Architecture: full reference map | [ARCHITECTURE_REFERENCE.md](ARCHITECTURE_REFERENCE.md) |
|
||||||
| Architecture: user flows (UML) | [technical/architecture/user-flows.md](/docs/technical/architecture/user-flows/) |
|
| Architecture: user flows (UML) | [technical/architecture/user-flows.md](technical/architecture/user-flows.md) |
|
||||||
| Architecture: module matrix (46 modules) | [technical/architecture/module-matrix.md](/docs/technical/architecture/module-matrix/) |
|
| Architecture: module matrix (46 modules) | [technical/architecture/module-matrix.md](technical/architecture/module-matrix.md) |
|
||||||
| Architecture: data flows | [technical/architecture/data-flows.md](/docs/technical/architecture/data-flows/) |
|
| Architecture: data flows | [technical/architecture/data-flows.md](technical/architecture/data-flows.md) |
|
||||||
| Architecture: schema mapping | [technical/architecture/schema-mapping.md](/docs/technical/architecture/schema-mapping/) |
|
| Architecture: schema mapping | [technical/architecture/schema-mapping.md](technical/architecture/schema-mapping.md) |
|
||||||
| Offline / air-gap operations | [24_OFFLINE_KIT.md](/docs/24_offline_kit/) |
|
| Offline / air-gap operations | [OFFLINE_KIT.md](OFFLINE_KIT.md) |
|
||||||
| Security deployment hardening | [17_SECURITY_HARDENING_GUIDE.md](/docs/17_security_hardening_guide/) |
|
| Security deployment hardening | [SECURITY_HARDENING_GUIDE.md](SECURITY_HARDENING_GUIDE.md) |
|
||||||
| Ingest advisories (Concelier + CLI) | [10_CONCELIER_CLI_QUICKSTART.md](/docs/10_concelier_cli_quickstart/) |
|
| Ingest advisories (Concelier + CLI) | [CONCELIER_CLI_QUICKSTART.md](CONCELIER_CLI_QUICKSTART.md) |
|
||||||
| Develop plugins/connectors | [10_PLUGIN_SDK_GUIDE.md](/docs/10_plugin_sdk_guide/) |
|
| Develop plugins/connectors | [PLUGIN_SDK_GUIDE.md](PLUGIN_SDK_GUIDE.md) |
|
||||||
| Console (Web UI) operator guide | [15_UI_GUIDE.md](/docs/15_ui_guide/) |
|
| Console (Web UI) operator guide | [UI_GUIDE.md](UI_GUIDE.md) |
|
||||||
| VEX consensus and issuer trust | [16_VEX_CONSENSUS_GUIDE.md](/docs/16_vex_consensus_guide/) |
|
| VEX consensus and issuer trust | [VEX_CONSENSUS_GUIDE.md](VEX_CONSENSUS_GUIDE.md) |
|
||||||
| Vulnerability Explorer guide | [20_VULNERABILITY_EXPLORER_GUIDE.md](/docs/20_vulnerability_explorer_guide/) |
|
| Vulnerability Explorer guide | [VULNERABILITY_EXPLORER_GUIDE.md](VULNERABILITY_EXPLORER_GUIDE.md) |
|
||||||
|
|
||||||
## Detailed Indexes
|
## Detailed Indexes
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ flowchart LR
|
|||||||
|
|
||||||
1. Extend `scripts/dev-test.sh` so local contributors get the layer by default.
|
1. Extend `scripts/dev-test.sh` so local contributors get the layer by default.
|
||||||
2. Add a dedicated workflow in `.gitea/workflows/` (or GitLab job in `.gitlab-ci.yml`).
|
2. Add a dedicated workflow in `.gitea/workflows/` (or GitLab job in `.gitlab-ci.yml`).
|
||||||
3. Register the job in `docs/19_TEST_SUITE_OVERVIEW.md` *and* list its metric
|
3. Register the job in `docs/TEST_SUITE_OVERVIEW.md` *and* list its metric
|
||||||
in `docs/metrics/README.md`.
|
in `docs/metrics/README.md`.
|
||||||
4. If the test requires network isolation, inherit from `NetworkIsolatedTestBase`.
|
4. If the test requires network isolation, inherit from `NetworkIsolatedTestBase`.
|
||||||
5. If the test uses golden corpus, add cases to `bench/golden-corpus/`.
|
5. If the test uses golden corpus, add cases to `bench/golden-corpus/`.
|
||||||
@@ -251,11 +251,12 @@ flowchart LR
|
|||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Sprint Epic 5100 - Testing Strategy](implplan/SPRINT_5100_0000_0000_epic_summary.md)
|
|
||||||
- [Testing Strategy Models](testing/testing-strategy-models.md)
|
- [Testing Strategy Models](testing/testing-strategy-models.md)
|
||||||
- [Test Catalog](testing/TEST_CATALOG.yml)
|
- [Test Catalog](testing/TEST_CATALOG.yml)
|
||||||
|
- [Testing README](testing/README.md) - Complete testing documentation index
|
||||||
|
- [CI/CD Test Strategy](cicd/test-strategy.md) - CI/CD integration details
|
||||||
- [tests/AGENTS.md](../tests/AGENTS.md)
|
- [tests/AGENTS.md](../tests/AGENTS.md)
|
||||||
- [Offline Operation Guide](24_OFFLINE_KIT.md)
|
- [Offline Operation Guide](OFFLINE_KIT.md)
|
||||||
- [Module Architecture Dossiers](modules/)
|
- [Module Architecture Dossiers](modules/)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -325,7 +325,7 @@ Before submitting a PR that involves digests or attestations:
|
|||||||
|
|
||||||
- [docs/testing/schemas/determinism-manifest.schema.json](../testing/schemas/determinism-manifest.schema.json) - JSON Schema for manifests
|
- [docs/testing/schemas/determinism-manifest.schema.json](../testing/schemas/determinism-manifest.schema.json) - JSON Schema for manifests
|
||||||
- [docs/modules/policy/design/policy-determinism-tests.md](../modules/policy/design/policy-determinism-tests.md) - Policy engine determinism
|
- [docs/modules/policy/design/policy-determinism-tests.md](../modules/policy/design/policy-determinism-tests.md) - Policy engine determinism
|
||||||
- [docs/19_TEST_SUITE_OVERVIEW.md](../19_TEST_SUITE_OVERVIEW.md) - Testing strategy
|
- [docs/TEST_SUITE_OVERVIEW.md](../TEST_SUITE_OVERVIEW.md) - Testing strategy
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> **Comprehensive table of every capability in the platform.**
|
> **Comprehensive table of every capability in the platform.**
|
||||||
>
|
>
|
||||||
> For competitive differentiation highlights, see [`key-features.md`](key-features.md).
|
> For competitive differentiation highlights, see [`key-features.md`](key-features.md).
|
||||||
> For tier-based pricing details, see [`04_FEATURE_MATRIX.md`](04_FEATURE_MATRIX.md).
|
> For tier-based pricing details, see [`FEATURE_MATRIX.md`](FEATURE_MATRIX.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ services.AddSingleton<IGuidProvider, SystemGuidProvider>();
|
|||||||
| 2026-01-04 | DET-019 complete: Scanner.WebService refactored - 12 endpoint/service files (EpssEndpoints, EvidenceEndpoints, SmartDiffEndpoints, UnknownsEndpoints, WitnessEndpoints, TriageInboxEndpoints, ProofBundleEndpoints, ReportSigner, ScoreReplayService, TestManifestRepository, SliceQueryService, UnifiedEvidenceService) plus dependency fixes in Scanner.Sources (SourceTriggerDispatcher, SourceContracts) and Scanner.WebService (EvidenceBundleExporter, GatingReasonService). All builds verified. | Agent |
|
| 2026-01-04 | DET-019 complete: Scanner.WebService refactored - 12 endpoint/service files (EpssEndpoints, EvidenceEndpoints, SmartDiffEndpoints, UnknownsEndpoints, WitnessEndpoints, TriageInboxEndpoints, ProofBundleEndpoints, ReportSigner, ScoreReplayService, TestManifestRepository, SliceQueryService, UnifiedEvidenceService) plus dependency fixes in Scanner.Sources (SourceTriggerDispatcher, SourceContracts) and Scanner.WebService (EvidenceBundleExporter, GatingReasonService). All builds verified. | Agent |
|
||||||
| 2026-01-04 | DET-020 in progress: Scanner.Analyzers.Native hardening extractors refactored - ElfHardeningExtractor, MachoHardeningExtractor, PeHardeningExtractor with TimeProvider injection. OfflineBuildIdIndex refactored. Build verified. RuntimeCapture adapters (LinuxEbpfCaptureAdapter, MacOsDyldCaptureAdapter, WindowsEtwCaptureAdapter) pending - require TimeProvider and IGuidProvider injection for 18+ usages across eBPF/DYLD/ETW tracing. | Agent |
|
| 2026-01-04 | DET-020 in progress: Scanner.Analyzers.Native hardening extractors refactored - ElfHardeningExtractor, MachoHardeningExtractor, PeHardeningExtractor with TimeProvider injection. OfflineBuildIdIndex refactored. Build verified. RuntimeCapture adapters (LinuxEbpfCaptureAdapter, MacOsDyldCaptureAdapter, WindowsEtwCaptureAdapter) pending - require TimeProvider and IGuidProvider injection for 18+ usages across eBPF/DYLD/ETW tracing. | Agent |
|
||||||
| 2026-01-04 | DET-020 complete: RuntimeCapture adapters refactored - LinuxEbpfCaptureAdapter, MacOsDyldCaptureAdapter, WindowsEtwCaptureAdapter with TimeProvider and IGuidProvider injection (SessionId, StartTime, EndTime, Timestamp fields). RuntimeEvidenceAggregator.MergeWithStaticAnalysis updated with optional TimeProvider parameter. StackTraceCapture.CollapsedStack.Parse updated with optional TimeProvider parameter. Added StellaOps.Determinism.Abstractions reference to project. All builds verified. | Agent |
|
| 2026-01-04 | DET-020 complete: RuntimeCapture adapters refactored - LinuxEbpfCaptureAdapter, MacOsDyldCaptureAdapter, WindowsEtwCaptureAdapter with TimeProvider and IGuidProvider injection (SessionId, StartTime, EndTime, Timestamp fields). RuntimeEvidenceAggregator.MergeWithStaticAnalysis updated with optional TimeProvider parameter. StackTraceCapture.CollapsedStack.Parse updated with optional TimeProvider parameter. Added StellaOps.Determinism.Abstractions reference to project. All builds verified. | Agent |
|
||||||
|
| 2026-01-06 | DET-021(d) continued: Cryptography.Kms module refactored - AwsKmsClient, GcpKmsClient, FileKmsClient (6 usages), Pkcs11KmsClient, Pkcs11Facade, GcpKmsFacade, AwsKmsFacade, Fido2KmsClient, Fido2Options with TimeProvider injection. Removed unnecessary TimeProvider.Abstractions package (built into .NET 10). All builds verified. | Agent |
|
||||||
|
| 2026-01-06 | DET-021 continued: SbomService module refactored - Clock.cs (SystemClock delegates to TimeProvider), LineageGraphService, SbomLineageEdgeRepository, PostgresOrchestratorRepository, InMemoryOrchestratorRepository, ReplayVerificationService, LineageCompareService, LineageExportService, LineageHoverCache, RegistrySourceService, OrchestratorControlService, WatermarkService. DTOs changed from default timestamps to required fields. All builds verified. | Agent |
|
||||||
|
| 2026-01-06 | DET-021 continued: Findings module refactored - LedgerEventMapping (TimeProvider parameter), Program.cs (TimeProvider injection), EvidenceGraphBuilder (TimeProvider constructor). Fixed pre-existing null reference issue in FindingWorkflowService.cs. All builds verified. | Agent |
|
||||||
|
| 2026-01-06 | DET-021 continued: Notify module refactored - InMemoryRepositories.cs (15 repository adapters: Channel, Rule, Template, Delivery, Digest, Lock, EscalationPolicy, EscalationState, OnCallSchedule, QuietHours, MaintenanceWindow, Inbox with TimeProvider constructors). All builds verified. | Agent |
|
||||||
|
| 2026-01-06 | DET-021 continued: ExportCenter module refactored - LineageEvidencePackService (12 usages), ExportRetentionService (1 usage), InMemorySchedulingStores (1 usage), ExportVerificationModels (VerifiedAt made required), ExportVerificationService (TimeProvider constructor + Failed factory calls), ExceptionReportGenerator (4 usages). All builds verified. | Agent |
|
||||||
|
| 2026-01-07 | DET-021 continued: Orchestrator module refactored - Infrastructure/Postgres repositories (PostgresPackRunRepository, PostgresPackRegistryRepository, PostgresQuotaRepository, PostgresRunRepository, PostgresSourceRepository, PostgresThrottleRepository, PostgresWatermarkRepository with TimeProvider constructors and usage updates). WebService/Endpoints (HealthEndpoints, KpiEndpoints with TimeProvider injection via [FromServices]). Domain records (IBackfillRepository/BackfillCheckpoint.Create/Complete/Fail methods now accept timestamps). All DateTimeOffset.UtcNow usages in production Postgres/Endpoint code eliminated. Remaining: CLI module (~100 usages), Policy.Gateway module (~50 usages). | Agent |
|
||||||
|
| 2026-01-07 | DET-021 continued: CLI module critical verifiers refactored - ForensicVerifier.cs (TimeProvider constructor, 2 usages updated), ImageAttestationVerifier.cs (TimeProvider constructor, 7 usages updated for verification timestamps and max age checks). Note: Pre-existing build errors in Policy.Tools and Scanner.Analyzers.Lang.Python unrelated to determinism changes. Further CLI refactoring deferred - large scope (~90+ remaining usages across 30+ files in short-lived CLI process). | Agent |
|
||||||
|
| 2026-01-07 | DET-021 continued: Policy.Gateway module refactored - ExceptionEndpoints.cs (10 DateTimeOffset.UtcNow usages across 6 endpoints: POST, PUT, approve, activate, extend, revoke), GateEndpoints.cs (3 usages: evaluate endpoint + health check), GovernanceEndpoints.cs (9 usages across sealed mode + risk profile handlers, plus RecordAudit helper), RegistryWebhookEndpoints.cs (3 usages: Docker, Harbor, generic webhook handlers), ExceptionApprovalEndpoints.cs (2 usages: CreateApprovalRequestAsync), InMemoryGateEvaluationQueue.cs (constructor + 2 usages). All handlers now use TimeProvider via [FromServices] or constructor injection. Note: InitializeDefaultProfiles() static initializer retained DateTimeOffset.UtcNow for bootstrap/seed data - acceptable for one-time startup code. | Agent |
|
||||||
|
| 2026-01-07 | DET-021 continued: Policy.Registry module refactored - InMemoryPolicyPackStore.cs (TimeProvider constructor, 4 usages: CreateAsync, UpdateAsync, UpdateStatusAsync, AddHistoryEntry), InMemorySnapshotStore.cs (TimeProvider constructor, 1 usage), InMemoryVerificationPolicyStore.cs (TimeProvider constructor, 2 usages: CreateAsync, UpdateAsync), InMemoryOverrideStore.cs (TimeProvider constructor, 2 usages: CreateAsync, ApproveAsync), InMemoryViolationStore.cs (TimeProvider constructor, 2 usages: AppendAsync, AppendBatchAsync). All builds verified. | Agent |
|
||||||
|
| 2026-01-07 | DET-021 continued: Policy.Engine module refactored - InMemoryExceptionRepository.cs (TimeProvider constructor, 2 usages: RevokeAsync, ExpireAsync), InMemoryPolicyPackRepository.cs (TimeProvider constructor, 6 usages across CreateAsync, UpsertRevisionAsync, StoreBundleAsync). Remaining Policy.Engine usages in domain models (TenantContextModels, EvidenceBundle, ExceptionMapper), telemetry services (MigrationTelemetryService, EwsTelemetryService), and complex services (PoEValidationService, PolicyMergePreviewService, VerdictLinkService, RiskProfileConfigurationService) require additional pattern decisions - some are default property initializers requiring schema-level changes. All modified files build verified. | Agent |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach.
|
- **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> **Audience:** Deployment Guild, Console Guild, platform operators.
|
> **Audience:** Deployment Guild, Console Guild, platform operators.
|
||||||
> **Scope:** Acquire the `stellaops/web-ui` image, run it with Compose or Helm, mirror it for air‑gapped environments, and keep parity with CLI workflows.
|
> **Scope:** Acquire the `stellaops/web-ui` image, run it with Compose or Helm, mirror it for air‑gapped environments, and keep parity with CLI workflows.
|
||||||
|
|
||||||
This guide focuses on the new **StellaOps Console** container. Start with the general [Installation Guide](../21_INSTALL_GUIDE.md) for shared prerequisites (Docker, registry access, TLS) and use the steps below to layer in the console.
|
This guide focuses on the new **StellaOps Console** container. Start with the general [Installation Guide](../INSTALL_GUIDE.md) for shared prerequisites (Docker, registry access, TLS) and use the steps below to layer in the console.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ Track progress for the CLI commands via `DOCS-CONSOLE-23-014` (CLI vs UI parity
|
|||||||
- `deploy/helm/stellaops/values-*.yaml` – Helm defaults per environment.
|
- `deploy/helm/stellaops/values-*.yaml` – Helm defaults per environment.
|
||||||
- `/docs/deploy/console.md` – Detailed environment variables, CSP, health checks.
|
- `/docs/deploy/console.md` – Detailed environment variables, CSP, health checks.
|
||||||
- `/docs/security/console-security.md` – Auth flows, scopes, DPoP, monitoring.
|
- `/docs/security/console-security.md` – Auth flows, scopes, DPoP, monitoring.
|
||||||
- `docs/15_UI_GUIDE.md` – Console workflows and offline posture.
|
- `docs/UI_GUIDE.md` – Console workflows and offline posture.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Core Thesis:** Stella Ops isn't a scanner that outputs findings. It's a platform that outputs **attestable decisions that can be replayed**. That difference survives auditors, regulators, and supply-chain propagation.
|
> **Core Thesis:** Stella Ops isn't a scanner that outputs findings. It's a platform that outputs **attestable decisions that can be replayed**. That difference survives auditors, regulators, and supply-chain propagation.
|
||||||
|
|
||||||
> **Looking for the complete feature catalog?** See [`full-features-list.md`](full-features-list.md) for the comprehensive list of all platform capabilities, or [`04_FEATURE_MATRIX.md`](04_FEATURE_MATRIX.md) for tier-by-tier availability.
|
> **Looking for the complete feature catalog?** See [`full-features-list.md`](full-features-list.md) for the comprehensive list of all platform capabilities, or [`FEATURE_MATRIX.md`](FEATURE_MATRIX.md) for tier-by-tier availability.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ Layer 3 (Runtime): eBPF probe confirms function was actually executed
|
|||||||
|
|
||||||
**Why it matters:** Regression-proof audits. Evidence, not assumptions, drives releases.
|
**Why it matters:** Regression-proof audits. Evidence, not assumptions, drives releases.
|
||||||
|
|
||||||
**Reference:** `docs/testing/testing-strategy-models.md`, `docs/19_TEST_SUITE_OVERVIEW.md`
|
**Reference:** `docs/testing/testing-strategy-models.md`, `docs/TEST_SUITE_OVERVIEW.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -299,6 +299,6 @@ stella scan --offline --image <digest>
|
|||||||
- **Competitive Landscape**: `docs/market/competitive-landscape.md`
|
- **Competitive Landscape**: `docs/market/competitive-landscape.md`
|
||||||
- **Moat Strategy**: `docs/market/moat-strategy-summary.md`
|
- **Moat Strategy**: `docs/market/moat-strategy-summary.md`
|
||||||
- **Proof Architecture**: `docs/modules/platform/proof-driven-moats-architecture.md`
|
- **Proof Architecture**: `docs/modules/platform/proof-driven-moats-architecture.md`
|
||||||
- **Vision**: `docs/03_VISION.md`
|
- **Vision**: `docs/VISION.md`
|
||||||
- **Architecture Overview**: `docs/40_ARCHITECTURE_OVERVIEW.md`
|
- **Architecture Overview**: `docs/ARCHITECTURE_OVERVIEW.md`
|
||||||
- **Quickstart**: `docs/quickstart.md`
|
- **Quickstart**: `docs/quickstart.md`
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ This isn't a feature gap—it's a category difference. Retrofitting it requires:
|
|||||||
- Engineering: ensure new features keep determinism + sovereign crypto front-and-center; link reachability attestations into proof graph.
|
- Engineering: ensure new features keep determinism + sovereign crypto front-and-center; link reachability attestations into proof graph.
|
||||||
|
|
||||||
## Cross-links
|
## Cross-links
|
||||||
- Vision: `docs/03_VISION.md` (Moats section)
|
- Vision: `docs/VISION.md` (Moats section)
|
||||||
- Architecture: `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
- Architecture: `docs/ARCHITECTURE_REFERENCE.md`
|
||||||
- Reachability moat details: `docs/reachability/lead.md`
|
- Reachability moat details: `docs/reachability/lead.md`
|
||||||
- Source advisory: `docs/product-advisories/23-Nov-2025 - Stella Ops vs Competitors.md`
|
- Source advisory: `docs/product-advisories/23-Nov-2025 - Stella Ops vs Competitors.md`
|
||||||
- **Claims Citation Index**: [`docs/market/claims-citation-index.md`](claims-citation-index.md)
|
- **Claims Citation Index**: [`docs/market/claims-citation-index.md`](claims-citation-index.md)
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ Produce **entrypoint‑aware differential SBOMs** and continually **re‑enrich*
|
|||||||
{
|
{
|
||||||
"schema":"cyclonedx+stella-diff@1.0",
|
"schema":"cyclonedx+stella-diff@1.0",
|
||||||
"base_sbom":"sha256:...",
|
"base_sbom":"sha256:...",
|
||||||
"entrypoint": ["/app/bin/Release/net8.0/app.dll"],
|
"entrypoint": ["/app/bin/Release/net10.0/app.dll"],
|
||||||
"cmd": ["--urls","http://127.0.0.1:8080"],
|
"cmd": ["--urls","http://127.0.0.1:8080"],
|
||||||
"static_reachable": ["pkg:nuget/Kestrel@*", "pkg:nuget/System.Data@*"],
|
"static_reachable": ["pkg:nuget/Kestrel@*", "pkg:nuget/System.Data@*"],
|
||||||
"runtime_observed": ["pkg:rpm/openssl@3.0.9"],
|
"runtime_observed": ["pkg:rpm/openssl@3.0.9"],
|
||||||
|
|||||||
57
docs/modules/_template/AGENTS.md
Normal file
57
docs/modules/_template/AGENTS.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# <Module Name> - AI Agent Instructions
|
||||||
|
|
||||||
|
> Instructions for AI agents (Claude Code, GitHub Copilot, etc.) working on this module.
|
||||||
|
|
||||||
|
## Module Context
|
||||||
|
|
||||||
|
- **Primary Language**: C# (.NET 10)
|
||||||
|
- **Project Type**: Library / WebService / Worker
|
||||||
|
- **Test Framework**: xUnit with Testcontainers
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/<Module>/__Libraries/StellaOps.<Module>.Core/` | Core business logic |
|
||||||
|
| `src/<Module>/StellaOps.<Module>.WebService/Program.cs` | Service entry point |
|
||||||
|
| `src/<Module>/__Tests/` | Test projects |
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Feature
|
||||||
|
1. Start with the interface in `*.Core`
|
||||||
|
2. Implement in the appropriate layer
|
||||||
|
3. Add tests in the corresponding `*.Tests` project
|
||||||
|
4. Update API contracts if WebService
|
||||||
|
|
||||||
|
### Fixing a Bug
|
||||||
|
1. Write a failing test first
|
||||||
|
2. Fix the implementation
|
||||||
|
3. Verify all related tests pass
|
||||||
|
|
||||||
|
## Patterns to Follow
|
||||||
|
|
||||||
|
- **Dependency Injection**: All dependencies via constructor injection
|
||||||
|
- **Async/Await**: Propagate CancellationToken through all async chains
|
||||||
|
- **Error Handling**: Use Result<T> pattern, not exceptions for expected failures
|
||||||
|
- **Logging**: Structured logging with semantic properties
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
- Direct `new HttpClient()` - use IHttpClientFactory
|
||||||
|
- `DateTime.UtcNow` - use TimeProvider injection
|
||||||
|
- `Guid.NewGuid()` - use IGuidGenerator injection
|
||||||
|
- Culture-sensitive string operations - use InvariantCulture
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
- Unit tests for all public APIs
|
||||||
|
- Integration tests for database operations
|
||||||
|
- Snapshot tests for serialization
|
||||||
|
- All tests must be deterministic
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Architecture](./architecture.md)
|
||||||
|
- [CLAUDE.md](../../../CLAUDE.md) - Global coding rules
|
||||||
|
- [src/__Tests/AGENTS.md](../../../src/__Tests/AGENTS.md) - Test infrastructure
|
||||||
62
docs/modules/_template/README.md
Normal file
62
docs/modules/_template/README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# <Module Name>
|
||||||
|
|
||||||
|
> One-line description of what this module does.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A brief paragraph (2-3 sentences) explaining the module's purpose, its primary responsibility, and why it exists in the StellaOps platform.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Architecture](./architecture.md) - Technical design and implementation details
|
||||||
|
- [Operations](./operations/) - Operational runbooks and dashboards
|
||||||
|
- [API Reference](./api/) - API documentation (if applicable)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| **Maturity** | Production / Beta / Alpha |
|
||||||
|
| **Last Reviewed** | YYYY-MM-DD |
|
||||||
|
| **Maintainer** | Guild/Team Name |
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- Feature 1: Brief description
|
||||||
|
- Feature 2: Brief description
|
||||||
|
- Feature 3: Brief description
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Upstream (this module depends on)
|
||||||
|
- **Authority** - Authentication and authorization
|
||||||
|
- **Other Module** - Why this dependency exists
|
||||||
|
|
||||||
|
### Downstream (modules that depend on this)
|
||||||
|
- **Other Module** - How they use this module
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Minimal code example showing how to use this module
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
builder.Services.Add<ModuleName>Services();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Minimal configuration example
|
||||||
|
module_name:
|
||||||
|
setting_1: value
|
||||||
|
setting_2: value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Implementation Guides](../../<category>/) - If applicable
|
||||||
|
- [Related Module](../other-module/) - Cross-references
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Any important caveats, migration notes, or known issues.
|
||||||
98
docs/modules/_template/architecture.md
Normal file
98
docs/modules/_template/architecture.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# <Module Name> Architecture
|
||||||
|
|
||||||
|
> Technical architecture specification for <Module Name>.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
High-level description of the module's architecture and how it fits into the StellaOps platform.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Principle 1** - Description of how this module follows or implements this principle
|
||||||
|
2. **Principle 2** - Description
|
||||||
|
3. **Principle 3** - Description
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
```
|
||||||
|
<module-name>/
|
||||||
|
├── __Libraries/
|
||||||
|
│ ├── StellaOps.<ModuleName>.Core/ # Core business logic
|
||||||
|
│ ├── StellaOps.<ModuleName>.Storage/ # Data persistence (if applicable)
|
||||||
|
│ └── StellaOps.<ModuleName>.Client/ # Client SDK (if applicable)
|
||||||
|
├── StellaOps.<ModuleName>.WebService/ # HTTP API (if applicable)
|
||||||
|
├── StellaOps.<ModuleName>.Worker/ # Background processing (if applicable)
|
||||||
|
└── __Tests/
|
||||||
|
└── StellaOps.<ModuleName>.*.Tests/ # Test projects
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Input Source] → [Processing] → [Output/Storage]
|
||||||
|
```
|
||||||
|
|
||||||
|
Describe the primary data flow through this module.
|
||||||
|
|
||||||
|
## Key Abstractions
|
||||||
|
|
||||||
|
### Interface 1
|
||||||
|
```csharp
|
||||||
|
public interface I<Something>
|
||||||
|
{
|
||||||
|
Task<Result> DoSomethingAsync(Input input, CancellationToken ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Purpose and usage of this interface.
|
||||||
|
|
||||||
|
### Interface 2
|
||||||
|
```csharp
|
||||||
|
public interface I<SomethingElse>
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Purpose and usage.
|
||||||
|
|
||||||
|
## Database Schema (if applicable)
|
||||||
|
|
||||||
|
| Table | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `schema.table_1` | Description |
|
||||||
|
| `schema.table_2` | Description |
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
|
||||||
|
Non-negotiable design constraints that must always be maintained:
|
||||||
|
|
||||||
|
1. **Invariant 1** - Description and why it matters
|
||||||
|
2. **Invariant 2** - Description
|
||||||
|
3. **Invariant 3** - Description
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
How errors are handled, propagated, and logged in this module.
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
- **Metrics**: Key metrics exposed by this module
|
||||||
|
- **Traces**: OpenTelemetry trace spans
|
||||||
|
- **Logs**: Structured logging patterns
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
Security-relevant aspects of this module's design.
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
- Expected throughput
|
||||||
|
- Latency budgets
|
||||||
|
- Resource constraints
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Module README](./README.md)
|
||||||
|
- [API Documentation](./api/)
|
||||||
|
- [Operations Guide](./operations/)
|
||||||
@@ -17,7 +17,7 @@ Platform module describes cross-cutting architecture, contracts, and guardrails
|
|||||||
## Key components
|
## Key components
|
||||||
- Architecture overview in `architecture-overview.md`.
|
- Architecture overview in `architecture-overview.md`.
|
||||||
- Platform architecture summary in `architecture.md`.
|
- Platform architecture summary in `architecture.md`.
|
||||||
- High-level reference: `../../07_HIGH_LEVEL_ARCHITECTURE.md`.
|
- High-level reference: `../../ARCHITECTURE_REFERENCE.md`.
|
||||||
|
|
||||||
## Integrations & dependencies
|
## Integrations & dependencies
|
||||||
- All StellaOps services via shared contracts (AOC, telemetry, security).
|
- All StellaOps services via shared contracts (AOC, telemetry, security).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Ownership:** Architecture Guild • Docs Guild
|
> **Ownership:** Architecture Guild • Docs Guild
|
||||||
> **Audience:** Service owners, platform engineers, solution architects
|
> **Audience:** Service owners, platform engineers, solution architects
|
||||||
> **Related:** [High-Level Architecture](../../07_HIGH_LEVEL_ARCHITECTURE.md), [Concelier Architecture](../concelier/architecture.md), [Policy Engine Architecture](../policy/architecture.md), [Aggregation-Only Contract](../../aoc/aggregation-only-contract.md)
|
> **Related:** [High-Level Architecture](../../ARCHITECTURE_REFERENCE.md), [Concelier Architecture](../concelier/architecture.md), [Policy Engine Architecture](../policy/architecture.md), [Aggregation-Only Contract](../../aoc/aggregation-only-contract.md)
|
||||||
|
|
||||||
This dossier summarises the end-to-end runtime topology after the Aggregation-Only Contract (AOC) rollout. It highlights where raw facts live, how ingest services enforce guardrails, and how downstream components consume those facts to derive policy decisions and user-facing experiences.
|
This dossier summarises the end-to-end runtime topology after the Aggregation-Only Contract (AOC) rollout. It highlights where raw facts live, how ingest services enforce guardrails, and how downstream components consume those facts to derive policy decisions and user-facing experiences.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
This module aggregates cross-cutting contracts and guardrails that every StellaOps service must follow.
|
This module aggregates cross-cutting contracts and guardrails that every StellaOps service must follow.
|
||||||
|
|
||||||
## Anchors
|
## Anchors
|
||||||
- High-level system view: `../../07_HIGH_LEVEL_ARCHITECTURE.md`
|
- High-level system view: `../../ARCHITECTURE_REFERENCE.md`
|
||||||
|
- Architecture overview: `../../ARCHITECTURE_OVERVIEW.md`
|
||||||
- Platform overview: `architecture-overview.md`
|
- Platform overview: `architecture-overview.md`
|
||||||
- Platform service definition: `platform-service.md`
|
- Platform service definition: `platform-service.md`
|
||||||
- Aggregation-Only Contract: `../../aoc/aggregation-only-contract.md` (referenced across ingestion/observability docs)
|
- Aggregation-Only Contract: `../../aoc/aggregation-only-contract.md` (referenced across ingestion/observability docs)
|
||||||
|
|||||||
@@ -80,16 +80,26 @@ StellaOps.Router.slnx
|
|||||||
|
|
||||||
## Key Documents
|
## Key Documents
|
||||||
|
|
||||||
|
### Module Documentation (this directory)
|
||||||
| Document | Purpose |
|
| Document | Purpose |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
| [architecture.md](architecture.md) | Canonical specification and requirements |
|
| [architecture.md](architecture.md) | Canonical specification and requirements |
|
||||||
| [schema-validation.md](schema-validation.md) | JSON Schema validation feature |
|
| [schema-validation.md](schema-validation.md) | JSON Schema validation feature |
|
||||||
| [openapi-aggregation.md](openapi-aggregation.md) | OpenAPI document generation |
|
| [openapi-aggregation.md](openapi-aggregation.md) | OpenAPI document generation |
|
||||||
| [migration-guide.md](migration-guide.md) | WebService to Microservice migration |
|
| [migration-guide.md](migration-guide.md) | WebService to Microservice migration |
|
||||||
| [rate-limiting.md](rate-limiting.md) | Centralized router rate limiting |
|
| [rate-limiting.md](rate-limiting.md) | Centralized router rate limiting (dossier) |
|
||||||
| [aspnet-endpoint-bridge.md](aspnet-endpoint-bridge.md) | Using ASP.NET endpoint registration as Router endpoint registration |
|
| [aspnet-endpoint-bridge.md](aspnet-endpoint-bridge.md) | Using ASP.NET endpoint registration as Router endpoint registration |
|
||||||
| [messaging-valkey-transport.md](messaging-valkey-transport.md) | Messaging transport over Valkey |
|
| [messaging-valkey-transport.md](messaging-valkey-transport.md) | Messaging transport over Valkey |
|
||||||
|
|
||||||
|
### Implementation Guides (docs/router/)
|
||||||
|
| Document | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| [README.md](../../router/README.md) | Quick start and feature overview |
|
||||||
|
| [ARCHITECTURE.md](../../router/ARCHITECTURE.md) | Detailed architecture walkthrough |
|
||||||
|
| [GETTING_STARTED.md](../../router/GETTING_STARTED.md) | Step-by-step setup guide |
|
||||||
|
| [rate-limiting.md](../../router/rate-limiting.md) | Rate limiting configuration guide |
|
||||||
|
| [transports/](../../router/transports/) | Transport plugin documentation |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Gateway
|
### Gateway
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## Evidence & scoring
|
## Evidence & scoring
|
||||||
- Large confidence boost when both host (`dotnet`) and DLL artefact are present.
|
- Large confidence boost when both host (`dotnet`) and DLL artefact are present.
|
||||||
- Add evidence for runtimeconfig parsing (`"runtimeconfig TFM=net8.0"`), bundle markers, or ASP.NET env vars.
|
- Add evidence for runtimeconfig parsing (`"runtimeconfig TFM=net10.0"`), bundle markers, or ASP.NET env vars.
|
||||||
- Penalise detections lacking artefact confirmation.
|
- Penalise detections lacking artefact confirmation.
|
||||||
|
|
||||||
## Edge cases
|
## Edge cases
|
||||||
|
|||||||
@@ -277,12 +277,16 @@ dotnet run --project src/Router/examples/Examples.OrderService
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
### Implementation Guides (this directory)
|
||||||
- [Architecture Overview](./ARCHITECTURE.md)
|
- [Architecture Overview](./ARCHITECTURE.md)
|
||||||
- [Getting Started Guide](./GETTING_STARTED.md)
|
- [Getting Started Guide](./GETTING_STARTED.md)
|
||||||
- [Transport Configuration](./transports/)
|
- [Transport Configuration](./transports/)
|
||||||
- [Rate Limiting](./rate-limiting.md)
|
- [Rate Limiting](./rate-limiting.md)
|
||||||
- [Examples](./examples/README.md)
|
- [Examples](./examples/README.md)
|
||||||
|
|
||||||
|
### Module Dossier
|
||||||
|
For architectural decisions, invariants, and module context, see the [Router Module Dossier](../modules/router/README.md).
|
||||||
|
|
||||||
## Related Modules
|
## Related Modules
|
||||||
|
|
||||||
- **Authority**: OAuth/OIDC integration for gateway authentication
|
- **Authority**: OAuth/OIDC integration for gateway authentication
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Target framework monikers (e.g., net6.0, net8.0, netstandard2.1)"
|
"description": "Target framework monikers (e.g., net8.0, net10.0, netstandard2.1)"
|
||||||
},
|
},
|
||||||
"assembly_analysis": {
|
"assembly_analysis": {
|
||||||
"$ref": "#/definitions/AssemblyAnalysisConfig"
|
"$ref": "#/definitions/AssemblyAnalysisConfig"
|
||||||
@@ -1483,7 +1483,7 @@
|
|||||||
{
|
{
|
||||||
"config_id": "aspnet-core-analyzer",
|
"config_id": "aspnet-core-analyzer",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"target_frameworks": ["net6.0", "net7.0", "net8.0"],
|
"target_frameworks": ["net8.0", "net9.0", "net10.0"],
|
||||||
"assembly_analysis": {
|
"assembly_analysis": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"include_referenced_assemblies": true,
|
"include_referenced_assemblies": true,
|
||||||
|
|||||||
@@ -53,4 +53,4 @@ All SDKs and plugins must maintain determinism:
|
|||||||
|
|
||||||
- [Plugin Architecture](../plugins/ARCHITECTURE.md)
|
- [Plugin Architecture](../plugins/ARCHITECTURE.md)
|
||||||
- [Plugin Configuration](../plugins/CONFIGURATION.md)
|
- [Plugin Configuration](../plugins/CONFIGURATION.md)
|
||||||
- [Plugin SDK Guide](../10_PLUGIN_SDK_GUIDE.md)
|
- [Plugin SDK Guide](../PLUGIN_SDK_GUIDE.md)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
Use this index to locate platform-level architecture references and per-module dossiers.
|
Use this index to locate platform-level architecture references and per-module dossiers.
|
||||||
|
|
||||||
## Core views
|
## Core views
|
||||||
- [Architecture overview (10-minute tour)](../../40_ARCHITECTURE_OVERVIEW.md)
|
- [Architecture overview (10-minute tour)](../../ARCHITECTURE_OVERVIEW.md)
|
||||||
- [High-level architecture (reference map)](../../07_HIGH_LEVEL_ARCHITECTURE.md)
|
- [High-level architecture (reference map)](../../ARCHITECTURE_REFERENCE.md)
|
||||||
- [Scanner core contracts](../../scanner-core-contracts.md)
|
- [Scanner core contracts](../../scanner-core-contracts.md)
|
||||||
- [Authority (legacy overview)](../../11_AUTHORITY.md)
|
- [Authority (legacy overview)](../../AUTHORITY.md)
|
||||||
- [Console operator guide](../../15_UI_GUIDE.md) and deep dives under [console](../../console/) and [ux](../../ux/)
|
- [Console operator guide](../../UI_GUIDE.md) and deep dives under [console](../../console/) and [ux](../../ux/)
|
||||||
- [Component map](component-map.md) (quick descriptions of every module under `src/`)
|
- [Component map](component-map.md) (quick descriptions of every module under `src/`)
|
||||||
|
|
||||||
## Detailed references
|
## Detailed references
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Concise descriptions of every top-level component under `src/`, summarising the
|
|||||||
## Advisory & Evidence Services
|
## Advisory & Evidence Services
|
||||||
- **AdvisoryAI** — Experimental intelligence helpers that summarise and prioritise advisory data for humans. Ingests canonical observations from Concelier/Excititor, adds explainable insights, and feeds UI/CLI and Policy workflows. See `docs/modules/advisory-ai/architecture.md`.
|
- **AdvisoryAI** — Experimental intelligence helpers that summarise and prioritise advisory data for humans. Ingests canonical observations from Concelier/Excititor, adds explainable insights, and feeds UI/CLI and Policy workflows. See `docs/modules/advisory-ai/architecture.md`.
|
||||||
- **Concelier** — Canonical advisory ingestion engine enforcing the Aggregation-Only Contract (AOC). Produces immutable observations/linksets consumed by Policy Engine, Graph, Scheduler, and Export Center. Docs in `docs/modules/concelier/architecture.md` and `docs/aoc/aggregation-only-contract.md`.
|
- **Concelier** — Canonical advisory ingestion engine enforcing the Aggregation-Only Contract (AOC). Produces immutable observations/linksets consumed by Policy Engine, Graph, Scheduler, and Export Center. Docs in `docs/modules/concelier/architecture.md` and `docs/aoc/aggregation-only-contract.md`.
|
||||||
- **Excititor** — VEX statement normaliser applying AOC guardrails. Supplies VEX observations to Policy Engine, VEX Lens, Scheduler, and UI. Reference `docs/modules/excititor/architecture.md` and `docs/16_VEX_CONSENSUS_GUIDE.md`.
|
- **Excititor** — VEX statement normaliser applying AOC guardrails. Supplies VEX observations to Policy Engine, VEX Lens, Scheduler, and UI. Reference `docs/modules/excititor/architecture.md` and `docs/VEX_CONSENSUS_GUIDE.md`.
|
||||||
- **VexLens** — Provides focused exploration of VEX evidence, conflict analysis, and waiver insights for UI/CLI. Backed by Excititor and Policy Engine (`docs/modules/vex-lens/architecture.md`).
|
- **VexLens** — Provides focused exploration of VEX evidence, conflict analysis, and waiver insights for UI/CLI. Backed by Excititor and Policy Engine (`docs/modules/vex-lens/architecture.md`).
|
||||||
- **EvidenceLocker** — Long-term store for signed evidence bundles (DSSE, SRM, policy waivers). Integrates with Attestor, Export Center, Policy, and replay tooling (`docs/forensics/evidence-locker.md`).
|
- **EvidenceLocker** — Long-term store for signed evidence bundles (DSSE, SRM, policy waivers). Integrates with Attestor, Export Center, Policy, and replay tooling (`docs/forensics/evidence-locker.md`).
|
||||||
- **ExportCenter** — Packages reproducible evidence bundles and mirror artefacts for online/offline distribution. Pulls from Concelier, Excititor, Policy, Scanner, Attestor, and Registry (`docs/modules/export-center/architecture.md`).
|
- **ExportCenter** — Packages reproducible evidence bundles and mirror artefacts for online/offline distribution. Pulls from Concelier, Excititor, Policy, Scanner, Attestor, and Registry (`docs/modules/export-center/architecture.md`).
|
||||||
@@ -26,7 +26,7 @@ Concise descriptions of every top-level component under `src/`, summarising the
|
|||||||
- **Governance components** (Authority scopes, Policy governance, Console policy UI) are covered in `docs/security/policy-governance.md` and `docs/modules/ui/policies.md`.
|
- **Governance components** (Authority scopes, Policy governance, Console policy UI) are covered in `docs/security/policy-governance.md` and `docs/modules/ui/policies.md`.
|
||||||
|
|
||||||
## Identity, Signing & Provenance
|
## Identity, Signing & Provenance
|
||||||
- **Authority** — Identity provider issuing short-lived OpToks, enforcing scopes/tenancy, and powering every module’s authentication story (`docs/11_AUTHORITY.md`, `docs/modules/authority/architecture.md`).
|
- **Authority** — Identity provider issuing short-lived OpToks, enforcing scopes/tenancy, and powering every module's authentication story (`docs/AUTHORITY.md`, `docs/modules/authority/architecture.md`).
|
||||||
- **Signer** — DSSE signing backend supporting keyless/keyful modes with Authority-managed trust roots (`docs/modules/signer/architecture.md`).
|
- **Signer** — DSSE signing backend supporting keyless/keyful modes with Authority-managed trust roots (`docs/modules/signer/architecture.md`).
|
||||||
- **Attestor** — Manages proof bundles, optional Rekor mirror, and distribution to consumers (`docs/modules/attestor/architecture.md`).
|
- **Attestor** — Manages proof bundles, optional Rekor mirror, and distribution to consumers (`docs/modules/attestor/architecture.md`).
|
||||||
- **Provenance** — Utilities and services for DSSE/SLSA provenance verification, consumed by Export Center, EvidenceLocker, and Replay (`docs/modules/export-center/provenance-and-signing.md`).
|
- **Provenance** — Utilities and services for DSSE/SLSA provenance verification, consumed by Export Center, EvidenceLocker, and Replay (`docs/modules/export-center/provenance-and-signing.md`).
|
||||||
@@ -49,10 +49,10 @@ Concise descriptions of every top-level component under `src/`, summarising the
|
|||||||
- **Registry** — Anonymous registry/token service hosting platform images and Offline Kit artefacts (`docs/modules/registry/architecture.md`).
|
- **Registry** — Anonymous registry/token service hosting platform images and Offline Kit artefacts (`docs/modules/registry/architecture.md`).
|
||||||
- **Zastava** — Runtime observer/admission controller ensuring signed images, SBOM availability, and policy verdict enforcement in live clusters (`docs/modules/zastava/architecture.md`).
|
- **Zastava** — Runtime observer/admission controller ensuring signed images, SBOM availability, and policy verdict enforcement in live clusters (`docs/modules/zastava/architecture.md`).
|
||||||
- **Signals** (shared above) plus runtime components integrate tightly with Zastava and Policy Engine.
|
- **Signals** (shared above) plus runtime components integrate tightly with Zastava and Policy Engine.
|
||||||
- **Bench** — Performance benchmarking toolset validating platform SLAs (`docs/12_PERFORMANCE_WORKBOOK.md`).
|
- **Bench** — Performance benchmarking toolset validating platform SLAs (`docs/PERFORMANCE_WORKBOOK.md`).
|
||||||
|
|
||||||
## Offline, Telemetry & Infrastructure
|
## Offline, Telemetry & Infrastructure
|
||||||
- **AirGap** — Bundles Offline Update Kits, enforces sealed-mode operations, and distributes trust roots/feeds (`docs/24_OFFLINE_KIT.md`, `docs/airgap/`).
|
- **AirGap** — Bundles Offline Update Kits, enforces sealed-mode operations, and distributes trust roots/feeds (`docs/OFFLINE_KIT.md`, `docs/airgap/`).
|
||||||
- **Telemetry** — OpenTelemetry collector/storage deployment tooling, observability integrations, and offline metrics packages (`docs/modules/telemetry/architecture.md`, `docs/observability/`).
|
- **Telemetry** — OpenTelemetry collector/storage deployment tooling, observability integrations, and offline metrics packages (`docs/modules/telemetry/architecture.md`, `docs/observability/`).
|
||||||
- **Mirror** and **ExportCenter** (above) complement AirGap by keeping offline mirrors in sync.
|
- **Mirror** and **ExportCenter** (above) complement AirGap by keeping offline mirrors in sync.
|
||||||
- **Tools** — Collection of utility programs (fixture generators, smoke tests, migration scripts) supporting all modules (`docs/dev/fixtures.md`, module-specific tooling sections).
|
- **Tools** — Collection of utility programs (fixture generators, smoke tests, migration scripts) supporting all modules (`docs/dev/fixtures.md`, module-specific tooling sections).
|
||||||
@@ -67,7 +67,7 @@ Concise descriptions of every top-level component under `src/`, summarising the
|
|||||||
- **Aoc** library (mentioned above) is reused by ingestion components and verification tooling to enforce the Aggregation-Only Contract.
|
- **Aoc** library (mentioned above) is reused by ingestion components and verification tooling to enforce the Aggregation-Only Contract.
|
||||||
|
|
||||||
## How It All Connects
|
## How It All Connects
|
||||||
High-level flows (see `docs/40_ARCHITECTURE_OVERVIEW.md` for the 10-minute tour):
|
High-level flows (see `docs/ARCHITECTURE_OVERVIEW.md` for the 10-minute tour):
|
||||||
1. **Ingest** — Concelier and Excititor use AOC to ingest advisories/VEX; Scheduler observes deltas.
|
1. **Ingest** — Concelier and Excititor use AOC to ingest advisories/VEX; Scheduler observes deltas.
|
||||||
2. **Scan & Evaluate** — Scanner generates SBOM evidence and hands to Signer/Attestor; Policy Engine merges SBOM, advisory, VEX, runtime signals; RiskEngine prioritises.
|
2. **Scan & Evaluate** — Scanner generates SBOM evidence and hands to Signer/Attestor; Policy Engine merges SBOM, advisory, VEX, runtime signals; RiskEngine prioritises.
|
||||||
3. **Store & Export** — EvidenceLocker and Export Center package results; Registry serves artefacts; AirGap bundles offline editions.
|
3. **Store & Export** — EvidenceLocker and Export Center package results; Registry serves artefacts; AirGap bundles offline editions.
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ This document provides a complete inventory of all analyzers used in StellaOps S
|
|||||||
│ 2. Parse legacy packages.config XML │
|
│ 2. Parse legacy packages.config XML │
|
||||||
│ 3. Parse *.deps.json for runtime dependencies │
|
│ 3. Parse *.deps.json for runtime dependencies │
|
||||||
│ 4. Resolve transitive dependencies from asset files │
|
│ 4. Resolve transitive dependencies from asset files │
|
||||||
│ 5. Extract framework targeting (net6.0, net8.0, etc.) │
|
│ 5. Extract framework targeting (net8.0, net10.0, etc.) │
|
||||||
│ │
|
│ │
|
||||||
│ PURL Format: │
|
│ PURL Format: │
|
||||||
│ pkg:nuget/Newtonsoft.Json@13.0.1 │
|
│ pkg:nuget/Newtonsoft.Json@13.0.1 │
|
||||||
│ pkg:nuget/Microsoft.Extensions.Logging@8.0.0?framework=net8.0 │
|
│ pkg:nuget/Microsoft.Extensions.Logging@10.0.0?framework=net10.0 │
|
||||||
│ │
|
│ │
|
||||||
│ Special Handling: │
|
│ Special Handling: │
|
||||||
│ • Framework-specific dependencies │
|
│ • Framework-specific dependencies │
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
Resources for contributors building features, plug-ins, connectors, and tests.
|
Resources for contributors building features, plug-ins, connectors, and tests.
|
||||||
|
|
||||||
## Engineering Standards & Quality
|
## Engineering Standards & Quality
|
||||||
- [../18_CODING_STANDARDS.md](../../18_CODING_STANDARDS.md) – language guidelines, project layout, review expectations.
|
- [../CODING_STANDARDS.md](../../CODING_STANDARDS.md) – language guidelines, project layout, review expectations.
|
||||||
- [../19_TEST_SUITE_OVERVIEW.md](../../19_TEST_SUITE_OVERVIEW.md) – unit, integration, golden, and determinism test strategy.
|
- [../TEST_SUITE_OVERVIEW.md](../../TEST_SUITE_OVERVIEW.md) – unit, integration, golden, and determinism test strategy.
|
||||||
- [../12_PERFORMANCE_WORKBOOK.md](../../12_PERFORMANCE_WORKBOOK.md) – benchmark targets and reference rigs.
|
- [../PERFORMANCE_WORKBOOK.md](../../PERFORMANCE_WORKBOOK.md) – benchmark targets and reference rigs.
|
||||||
- [../cli-vs-ui-parity.md](../../cli-vs-ui-parity.md) – CLI vs Console feature parity tracking.
|
- [../cli-vs-ui-parity.md](../../cli-vs-ui-parity.md) – CLI vs Console feature parity tracking.
|
||||||
- [../scanner-core-contracts.md](../../scanner-core-contracts.md) – DTO fixtures consumed by tests.
|
- [../scanner-core-contracts.md](../../scanner-core-contracts.md) – DTO fixtures consumed by tests.
|
||||||
|
|
||||||
## Plug-ins, Connectors & Extensions
|
## Plug-ins, Connectors & Extensions
|
||||||
- [../10_PLUGIN_SDK_GUIDE.md](../../10_PLUGIN_SDK_GUIDE.md) – plug-in lifecycle, manifests, packaging.
|
- [../PLUGIN_SDK_GUIDE.md](../../PLUGIN_SDK_GUIDE.md) – plug-in lifecycle, manifests, packaging.
|
||||||
- [../10_CONCELIER_CLI_QUICKSTART.md](../../10_CONCELIER_CLI_QUICKSTART.md) – local Concelier + CLI workflow for advisory ingestion.
|
- [../CONCELIER_CLI_QUICKSTART.md](../../CONCELIER_CLI_QUICKSTART.md) – local Concelier + CLI workflow for advisory ingestion.
|
||||||
- Developer guides under [../dev/](../../dev/):
|
- Developer guides under [../dev/](../../dev/):
|
||||||
- Connector playbooks (`30_EXCITITOR_CONNECTOR_GUIDE.md`, `kisa_connector_notes.md`).
|
- Connector playbooks (`30_EXCITITOR_CONNECTOR_GUIDE.md`, `kisa_connector_notes.md`).
|
||||||
- Authority and DPoP guidance (`31_AUTHORITY_PLUGIN_DEVELOPER_GUIDE.md`, `32_AUTH_CLIENT_GUIDE.md`).
|
- Authority and DPoP guidance (`31_AUTHORITY_PLUGIN_DEVELOPER_GUIDE.md`, `32_AUTH_CLIENT_GUIDE.md`).
|
||||||
@@ -20,7 +20,7 @@ Resources for contributors building features, plug-ins, connectors, and tests.
|
|||||||
- Operational templates and fixtures (`templates/`, `fixtures.md`).
|
- Operational templates and fixtures (`templates/`, `fixtures.md`).
|
||||||
|
|
||||||
## CLI, SDKs & Automation
|
## CLI, SDKs & Automation
|
||||||
- [../09_API_CLI_REFERENCE.md](../../09_API_CLI_REFERENCE.md) – authoritative CLI commands and flags (use for scripting).
|
- [../API_CLI_REFERENCE.md](../../API_CLI_REFERENCE.md) – authoritative CLI commands and flags (use for scripting).
|
||||||
- [../api/sdk-openapi-program.md](../../api/sdk-openapi-program.md) – guidance for downstream SDK generation.
|
- [../api/sdk-openapi-program.md](../../api/sdk-openapi-program.md) – guidance for downstream SDK generation.
|
||||||
- [../policy/gateway.md](../../policy/gateway.md) & [../policy/dsl.md](../../policy/dsl.md) – foundations for automating policy programs.
|
- [../policy/gateway.md](../../policy/gateway.md) & [../policy/dsl.md](../../policy/dsl.md) – foundations for automating policy programs.
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
Deployment, runtime operations, and air-gap playbooks for running Stella Ops in production.
|
Deployment, runtime operations, and air-gap playbooks for running Stella Ops in production.
|
||||||
|
|
||||||
## Install & Upgrade
|
## Install & Upgrade
|
||||||
- [../21_INSTALL_GUIDE.md](../../21_INSTALL_GUIDE.md) – canonical install guide (Docker, air-gap considerations).
|
- [../INSTALL_GUIDE.md](../../INSTALL_GUIDE.md) – canonical install guide (Docker, air-gap considerations).
|
||||||
- [../operations/console-docker-install.md](../../operations/console-docker-install.md) – Docker install recipes.
|
- [../operations/console-docker-install.md](../../operations/console-docker-install.md) – Docker install recipes.
|
||||||
- [../deploy/containers.md](../../deploy/containers.md) – container deployment guidance for AOC environments.
|
- [../deploy/containers.md](../../deploy/containers.md) – container deployment guidance for AOC environments.
|
||||||
- [../deploy/console.md](../../deploy/console.md) – console deployment specifics.
|
- [../deploy/console.md](../../deploy/console.md) – console deployment specifics.
|
||||||
- [../13_RELEASE_ENGINEERING_PLAYBOOK.md](../../13_RELEASE_ENGINEERING_PLAYBOOK.md) – release automation, signing, reproducibility.
|
- [../RELEASE_ENGINEERING_PLAYBOOK.md](../../RELEASE_ENGINEERING_PLAYBOOK.md) – release automation, signing, reproducibility.
|
||||||
- [../artifacts/bom-index/README.md](../../artifacts/bom-index/README.md) – BOM index artifact layout for Offline Kit exports.
|
- [../artifacts/bom-index/README.md](../../artifacts/bom-index/README.md) – BOM index artifact layout for Offline Kit exports.
|
||||||
|
|
||||||
## Offline & Sovereign Operations
|
## Offline & Sovereign Operations
|
||||||
- [../quickstart.md](../../quickstart.md) – 5-minute path to first scan (useful for smoke testing installs).
|
- [../quickstart.md](../../quickstart.md) – 5-minute path to first scan (useful for smoke testing installs).
|
||||||
- [../24_OFFLINE_KIT.md](../../24_OFFLINE_KIT.md) – bundle contents, import/export workflow.
|
- [../OFFLINE_KIT.md](../../OFFLINE_KIT.md) – bundle contents, import/export workflow.
|
||||||
- [../airgap/airgap-mode.md](../../airgap/airgap-mode.md) – configuration for sealed environments.
|
- [../airgap/airgap-mode.md](../../airgap/airgap-mode.md) – configuration for sealed environments.
|
||||||
- [../license-jwt-quota.md](../../license-jwt-quota.md) – offline quota token lifecycle.
|
- [../license-jwt-quota.md](../../license-jwt-quota.md) – offline quota token lifecycle.
|
||||||
- [../10_CONCELIER_CLI_QUICKSTART.md](../../10_CONCELIER_CLI_QUICKSTART.md) – workstation ingest/export workflow (operators).
|
- [../CONCELIER_CLI_QUICKSTART.md](../../CONCELIER_CLI_QUICKSTART.md) – workstation ingest/export workflow (operators).
|
||||||
|
|
||||||
## Hardening & Governance
|
## Hardening & Governance
|
||||||
- [../17_SECURITY_HARDENING_GUIDE.md](../../17_SECURITY_HARDENING_GUIDE.md) – platform hardening checklist.
|
- [../SECURITY_HARDENING_GUIDE.md](../../SECURITY_HARDENING_GUIDE.md) – platform hardening checklist.
|
||||||
- [../accessibility.md](../../accessibility.md) – accessibility checklist for console deployments.
|
- [../accessibility.md](../../accessibility.md) – accessibility checklist for console deployments.
|
||||||
- [../security/console-security.md](../../security/console-security.md) – console-specific controls.
|
- [../security/console-security.md](../../security/console-security.md) – console-specific controls.
|
||||||
- [../security/authority-scopes.md](../../security/authority-scopes.md) – Authority scope model.
|
- [../security/authority-scopes.md](../../security/authority-scopes.md) – Authority scope model.
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Authoritative sources for threat models, governance, compliance, and security operations.
|
Authoritative sources for threat models, governance, compliance, and security operations.
|
||||||
|
|
||||||
## Policies & Governance
|
## Policies & Governance
|
||||||
- [../13_SECURITY_POLICY.md](../../13_SECURITY_POLICY.md) – responsible disclosure, support windows.
|
- [../SECURITY_POLICY.md](../../SECURITY_POLICY.md) – responsible disclosure, support windows.
|
||||||
- [../11_GOVERNANCE.md](../../11_GOVERNANCE.md) – project governance charter.
|
- [../GOVERNANCE.md](../../GOVERNANCE.md) – project governance charter.
|
||||||
- [../12_CODE_OF_CONDUCT.md](../../12_CODE_OF_CONDUCT.md) – community expectations.
|
- [../CODE_OF_CONDUCT.md](../../CODE_OF_CONDUCT.md) – community expectations.
|
||||||
- [../17_SECURITY_HARDENING_GUIDE.md](../../17_SECURITY_HARDENING_GUIDE.md) – deployment hardening steps.
|
- [../SECURITY_HARDENING_GUIDE.md](../../SECURITY_HARDENING_GUIDE.md) – deployment hardening steps.
|
||||||
- [../security/policy-governance.md](../../security/policy-governance.md) – policy governance specifics.
|
- [../security/policy-governance.md](../../security/policy-governance.md) – policy governance specifics.
|
||||||
- [../29_LEGAL_FAQ_QUOTA.md](../../29_LEGAL_FAQ_QUOTA.md) – legal interpretation of quota.
|
- [../LEGAL_FAQ_QUOTA.md](../../LEGAL_FAQ_QUOTA.md) – legal interpretation of quota.
|
||||||
- [../33_333_QUOTA_OVERVIEW.md](../../33_333_QUOTA_OVERVIEW.md) – quota policy reference.
|
- [../QUOTA_OVERVIEW.md](../../QUOTA_OVERVIEW.md) – quota policy reference.
|
||||||
- [../risk/risk-profiles.md](../../risk/risk-profiles.md) – organisational risk personas.
|
- [../risk/risk-profiles.md](../../risk/risk-profiles.md) – organisational risk personas.
|
||||||
|
|
||||||
## Threat Models & Security Architecture
|
## Threat Models & Security Architecture
|
||||||
@@ -25,8 +25,8 @@ Authoritative sources for threat models, governance, compliance, and security op
|
|||||||
- [../security/audit-events.md](../../security/audit-events.md) – audit event taxonomy.
|
- [../security/audit-events.md](../../security/audit-events.md) – audit event taxonomy.
|
||||||
- [../security/revocation-bundle.md](../../security/revocation-bundle.md) & [../security/revocation-bundle-example.json](../../security/revocation-bundle-example.json) – revocation process.
|
- [../security/revocation-bundle.md](../../security/revocation-bundle.md) & [../security/revocation-bundle-example.json](../../security/revocation-bundle-example.json) – revocation process.
|
||||||
- [../license-jwt-quota.md](../../license-jwt-quota.md) – licence/quota enforcement controls.
|
- [../license-jwt-quota.md](../../license-jwt-quota.md) – licence/quota enforcement controls.
|
||||||
- [../30_QUOTA_ENFORCEMENT_FLOW1.md](../../30_QUOTA_ENFORCEMENT_FLOW1.md) – quota enforcement sequence.
|
- [../QUOTA_ENFORCEMENT_FLOW.md](../../QUOTA_ENFORCEMENT_FLOW.md) – quota enforcement sequence.
|
||||||
- [../24_OFFLINE_KIT.md](../../24_OFFLINE_KIT.md) – tamper-evident offline artefacts.
|
- [../OFFLINE_KIT.md](../../OFFLINE_KIT.md) – tamper-evident offline artefacts.
|
||||||
- [../security/](../../security/) – browse for additional deep dives (audit, scopes, rate limits).
|
- [../security/](../../security/) – browse for additional deep dives (audit, scopes, rate limits).
|
||||||
|
|
||||||
## Supporting Material
|
## Supporting Material
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
Foundational, high-level documents that define StellaOps direction, scope, and differentiators.
|
Foundational, high-level documents that define StellaOps direction, scope, and differentiators.
|
||||||
|
|
||||||
- [Vision](../../03_VISION.md) — north-star goals, KPIs, and themes.
|
- [Vision](../../VISION.md) — north-star goals, KPIs, and themes.
|
||||||
- [Feature matrix](../../04_FEATURE_MATRIX.md) — capability matrix by tier.
|
- [Feature matrix](../../FEATURE_MATRIX.md) — capability matrix by tier.
|
||||||
- [System requirements spec](../../05_SYSTEM_REQUIREMENTS_SPEC.md) — functional and non-functional requirements baseline.
|
- [System requirements spec](../../SYSTEM_REQUIREMENTS_SPEC.md) — functional and non-functional requirements baseline.
|
||||||
- [Roadmap](../../05_ROADMAP.md) — date-free capability roadmap and definition of “done”.
|
- [Roadmap](../../ROADMAP.md) — date-free capability roadmap and definition of "done".
|
||||||
- [Architecture overview](../../40_ARCHITECTURE_OVERVIEW.md) — platform principles and module map.
|
- [Architecture overview](../../ARCHITECTURE_OVERVIEW.md) — platform principles and module map.
|
||||||
- [Moat](../../moat.md) — differentiating workstreams (determinism, policy lattice, sovereign crypto readiness, attestation graph).
|
- [Moat](../../moat.md) — differentiating workstreams (determinism, policy lattice, sovereign crypto readiness, attestation graph).
|
||||||
- [Offline Kit](../../24_OFFLINE_KIT.md) — offline story and workflows.
|
- [Offline Kit](../../OFFLINE_KIT.md) — offline story and workflows.
|
||||||
- [Security policy](../../13_SECURITY_POLICY.md) — disclosure and support expectations.
|
- [Security policy](../../SECURITY_POLICY.md) — disclosure and support expectations.
|
||||||
- [Glossary](../../14_GLOSSARY_OF_TERMS.md) — canonical vocabulary.
|
- [Glossary](../../GLOSSARY.md) — canonical vocabulary.
|
||||||
- [UI guide](../../15_UI_GUIDE.md) — console UX overview for evaluators.
|
- [UI guide](../../UI_GUIDE.md) — console UX overview for evaluators.
|
||||||
- [FAQ matrix](../../23_FAQ_MATRIX.md) — stakeholder FAQ.
|
- [FAQ matrix](../../FAQ_MATRIX.md) — stakeholder FAQ.
|
||||||
|
|
||||||
## Related concepts
|
## Related concepts
|
||||||
- [Quota framing](../../33_333_QUOTA_OVERVIEW.md) and [enforcement flow](../../30_QUOTA_ENFORCEMENT_FLOW1.md) align business policy with enforcement diagrams.
|
- [Quota framing](../../QUOTA_OVERVIEW.md) and [enforcement flow](../../QUOTA_ENFORCEMENT_FLOW.md) align business policy with enforcement diagrams.
|
||||||
- [Legal FAQ (quota)](../../29_LEGAL_FAQ_QUOTA.md) captures the AGPL-3.0 interpretation of quota enforcement.
|
- [Legal FAQ (quota)](../../LEGAL_FAQ_QUOTA.md) captures the AGPL-3.0 interpretation of quota enforcement.
|
||||||
- [License/JWT quota narrative](../../license-jwt-quota.md) documents the offline licensing story for quota tokens.
|
- [License/JWT quota narrative](../../license-jwt-quota.md) documents the offline licensing story for quota tokens.
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ npm run storybook:build
|
|||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Test Suite Overview](../TEST_SUITE_OVERVIEW.md) - High-level entry point for testing
|
||||||
- [CI/CD Overview](../cicd/README.md)
|
- [CI/CD Overview](../cicd/README.md)
|
||||||
|
- [CI/CD Test Strategy](../cicd/test-strategy.md) - Detailed CI/CD test integration
|
||||||
- [Workflow Triggers](../cicd/workflow-triggers.md)
|
- [Workflow Triggers](../cicd/workflow-triggers.md)
|
||||||
- [Path Filters](../cicd/path-filters.md)
|
- [Path Filters](../cicd/path-filters.md)
|
||||||
- [Test Infrastructure](../../src/__Tests/AGENTS.md)
|
- [Test Infrastructure](../../src/__Tests/AGENTS.md)
|
||||||
|
|||||||
@@ -149,9 +149,9 @@ If baselines become stale:
|
|||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Test Suite Overview](../19_TEST_SUITE_OVERVIEW.md)
|
- [Test Suite Overview](../TEST_SUITE_OVERVIEW.md)
|
||||||
- [Testing Strategy Models](./testing-strategy-models.md)
|
- [Testing Strategy Models](./testing-strategy-models.md)
|
||||||
- [Test Catalog](./TEST_CATALOG.yml)
|
- [Test Catalog](./TEST_CATALOG.yml)
|
||||||
- [Reachability Corpus Plan](../reachability/corpus-plan.md)
|
- [Reachability Corpus Plan](../reachability/corpus-plan.md)
|
||||||
- [Performance Workbook](../12_PERFORMANCE_WORKBOOK.md)
|
- [Performance Workbook](../PERFORMANCE_WORKBOOK.md)
|
||||||
- [Testing Quality Guardrails](./testing-quality-guardrails-implementation.md)
|
- [Testing Quality Guardrails](./testing-quality-guardrails-implementation.md)
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ Supersedes/extends: `docs/product-advisories/archived/2025-12-21-testing-strateg
|
|||||||
|
|
||||||
## Documentation moments (when to update)
|
## Documentation moments (when to update)
|
||||||
- New model or required test type: update `docs/testing/TEST_CATALOG.yml`.
|
- New model or required test type: update `docs/testing/TEST_CATALOG.yml`.
|
||||||
- New lane or gate: update `docs/19_TEST_SUITE_OVERVIEW.md` and `docs/testing/ci-quality-gates.md`.
|
- New lane or gate: update `docs/TEST_SUITE_OVERVIEW.md` and `docs/testing/ci-quality-gates.md`.
|
||||||
- Module-specific test policy change: update the module dossier under `docs/modules/<module>/`.
|
- Module-specific test policy change: update the module dossier under `docs/modules/<module>/`.
|
||||||
- New fixtures or runnable harnesses: place under `docs/benchmarks/**` or `tests/**` and link here.
|
- New fixtures or runnable harnesses: place under `docs/benchmarks/**` or `tests/**` and link here.
|
||||||
|
|
||||||
## Related artifacts
|
## Related artifacts
|
||||||
- Test catalog (source of truth): `docs/testing/TEST_CATALOG.yml`
|
- Test catalog (source of truth): `docs/testing/TEST_CATALOG.yml`
|
||||||
- Test suite overview: `docs/19_TEST_SUITE_OVERVIEW.md`
|
- Test suite overview: `docs/TEST_SUITE_OVERVIEW.md`
|
||||||
- Quality guardrails: `docs/testing/testing-quality-guardrails-implementation.md`
|
- Quality guardrails: `docs/testing/testing-quality-guardrails-implementation.md`
|
||||||
- Code samples from the advisory: `docs/benchmarks/testing/better-testing-strategy-samples.md`
|
- Code samples from the advisory: `docs/benchmarks/testing/better-testing-strategy-samples.md`
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ public sealed class HeaderScopeAuthenticationHandler : AuthenticationHandler<Aut
|
|||||||
|
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var scope in value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
foreach (var scope in value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
{
|
{
|
||||||
scopes.Add(scope);
|
scopes.Add(scope);
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
new[] { new FeedBuildConfig("nvd-feed", "nvd", "2025-06-15", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("nvd-feed", "nvd", "2025-06-15", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundleOutputPath = Path.Combine(_onlineEnvPath, "bundle");
|
var bundleOutputPath = Path.Combine(_onlineEnvPath, "bundle");
|
||||||
|
|
||||||
@@ -120,7 +121,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
|||||||
DateTimeOffset.UtcNow.AddDays(30),
|
DateTimeOffset.UtcNow.AddDays(30),
|
||||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedPath, "feeds/all-feeds.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedPath, "feeds/all-feeds.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||||
new[] { new PolicyBuildConfig("policy-1", "default", "1.0", policyPath, "policies/default.rego", PolicyType.OpaRego) },
|
new[] { new PolicyBuildConfig("policy-1", "default", "1.0", policyPath, "policies/default.rego", PolicyType.OpaRego) },
|
||||||
new[] { new CryptoBuildConfig("crypto-1", "trust-root", certPath, "certs/root.pem", CryptoComponentType.TrustRoot, null) });
|
new[] { new CryptoBuildConfig("crypto-1", "trust-root", certPath, "certs/root.pem", CryptoComponentType.TrustRoot, null) },
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundlePath = Path.Combine(_onlineEnvPath, "multi-bundle");
|
var bundlePath = Path.Combine(_onlineEnvPath, "multi-bundle");
|
||||||
|
|
||||||
@@ -161,7 +163,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
new[] { new FeedBuildConfig("feed", "nvd", "v1", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("feed", "nvd", "v1", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundlePath = Path.Combine(_onlineEnvPath, "corrupt-source");
|
var bundlePath = Path.Combine(_onlineEnvPath, "corrupt-source");
|
||||||
var manifest = await builder.BuildAsync(request, bundlePath);
|
var manifest = await builder.BuildAsync(request, bundlePath);
|
||||||
@@ -219,7 +222,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
Array.Empty<FeedBuildConfig>(),
|
Array.Empty<FeedBuildConfig>(),
|
||||||
new[] { new PolicyBuildConfig("security-policy", "security", "1.0", policyPath, "policies/security.rego", PolicyType.OpaRego) },
|
new[] { new PolicyBuildConfig("security-policy", "security", "1.0", policyPath, "policies/security.rego", PolicyType.OpaRego) },
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundlePath = Path.Combine(_onlineEnvPath, "policy-bundle");
|
var bundlePath = Path.Combine(_onlineEnvPath, "policy-bundle");
|
||||||
|
|
||||||
@@ -273,7 +277,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
|||||||
new PolicyBuildConfig("policy-2", "policy2", "1.0", policy2Path, "policies/policy2.rego", PolicyType.OpaRego),
|
new PolicyBuildConfig("policy-2", "policy2", "1.0", policy2Path, "policies/policy2.rego", PolicyType.OpaRego),
|
||||||
new PolicyBuildConfig("policy-3", "policy3", "1.0", policy3Path, "policies/policy3.rego", PolicyType.OpaRego)
|
new PolicyBuildConfig("policy-3", "policy3", "1.0", policy3Path, "policies/policy3.rego", PolicyType.OpaRego)
|
||||||
},
|
},
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundlePath = Path.Combine(_onlineEnvPath, "multi-policy");
|
var bundlePath = Path.Combine(_onlineEnvPath, "multi-policy");
|
||||||
|
|
||||||
@@ -315,7 +320,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
Array.Empty<FeedBuildConfig>(),
|
Array.Empty<FeedBuildConfig>(),
|
||||||
new[] { new PolicyBuildConfig("signed-policy", "signed", "1.0", policyPath, "policies/signed.rego", PolicyType.OpaRego) },
|
new[] { new PolicyBuildConfig("signed-policy", "signed", "1.0", policyPath, "policies/signed.rego", PolicyType.OpaRego) },
|
||||||
new[] { new CryptoBuildConfig("signing-cert", "signing", certPath, "certs/signing.pem", CryptoComponentType.SigningKey, null) });
|
new[] { new CryptoBuildConfig("signing-cert", "signing", certPath, "certs/signing.pem", CryptoComponentType.SigningKey, null) },
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundlePath = Path.Combine(_onlineEnvPath, "signed-bundle");
|
var bundlePath = Path.Combine(_onlineEnvPath, "signed-bundle");
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
|||||||
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act - First export
|
// Act - First export
|
||||||
var manifest1 = await builder.BuildAsync(request, outputPath1);
|
var manifest1 = await builder.BuildAsync(request, outputPath1);
|
||||||
@@ -163,7 +164,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
|||||||
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var manifest2 = await builder.BuildAsync(request2, outputPath2);
|
var manifest2 = await builder.BuildAsync(request2, outputPath2);
|
||||||
|
|
||||||
@@ -278,7 +280,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
|||||||
new FeedBuildConfig("f3", "osv", "v1", feed3, "feeds/f3.json", DateTimeOffset.UtcNow, FeedFormat.OsvJson)
|
new FeedBuildConfig("f3", "osv", "v1", feed3, "feeds/f3.json", DateTimeOffset.UtcNow, FeedFormat.OsvJson)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, Path.Combine(_tempRoot, "multi"));
|
var manifest = await builder.BuildAsync(request, Path.Combine(_tempRoot, "multi"));
|
||||||
@@ -332,7 +335,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
|||||||
new FeedBuildConfig("f1", "binary", "v1", source1, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
new FeedBuildConfig("f1", "binary", "v1", source1, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var request2 = new BundleBuildRequest(
|
var request2 = new BundleBuildRequest(
|
||||||
"binary-test",
|
"binary-test",
|
||||||
@@ -343,7 +347,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
|||||||
new FeedBuildConfig("f1", "binary", "v1", source2, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
new FeedBuildConfig("f1", "binary", "v1", source2, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest1 = await builder.BuildAsync(request1, Path.Combine(_tempRoot, "bin1"));
|
var manifest1 = await builder.BuildAsync(request1, Path.Combine(_tempRoot, "bin1"));
|
||||||
@@ -407,7 +412,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
|||||||
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private BundleManifest CreateDeterministicManifest(string name)
|
private BundleManifest CreateDeterministicManifest(string name)
|
||||||
|
|||||||
@@ -259,7 +259,8 @@ public sealed class BundleExportImportTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile1, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile1, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var request2 = new BundleBuildRequest(
|
var request2 = new BundleBuildRequest(
|
||||||
"determinism-test",
|
"determinism-test",
|
||||||
@@ -267,7 +268,8 @@ public sealed class BundleExportImportTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile2, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile2, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var outputPath1 = Path.Combine(_tempRoot, "determinism-output1");
|
var outputPath1 = Path.Combine(_tempRoot, "determinism-output1");
|
||||||
var outputPath2 = Path.Combine(_tempRoot, "determinism-output2");
|
var outputPath2 = Path.Combine(_tempRoot, "determinism-output2");
|
||||||
@@ -363,7 +365,8 @@ public sealed class BundleExportImportTests : IDisposable
|
|||||||
imported.Feeds[0].SnapshotAt,
|
imported.Feeds[0].SnapshotAt,
|
||||||
imported.Feeds[0].Format) },
|
imported.Feeds[0].Format) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var bundlePath2 = Path.Combine(_tempRoot, "roundtrip2");
|
var bundlePath2 = Path.Combine(_tempRoot, "roundtrip2");
|
||||||
var manifest2 = await builder.BuildAsync(reexportRequest, bundlePath2);
|
var manifest2 = await builder.BuildAsync(reexportRequest, bundlePath2);
|
||||||
@@ -409,7 +412,8 @@ public sealed class BundleExportImportTests : IDisposable
|
|||||||
null,
|
null,
|
||||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedSourcePath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedSourcePath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BundleManifest CreateTestManifest()
|
private static BundleManifest CreateTestManifest()
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
null,
|
null,
|
||||||
Array.Empty<FeedBuildConfig>(),
|
Array.Empty<FeedBuildConfig>(),
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -93,7 +94,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
FeedFormat.StellaOpsNative)
|
FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -139,7 +141,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
"policies/default.rego",
|
"policies/default.rego",
|
||||||
PolicyType.OpaRego)
|
PolicyType.OpaRego)
|
||||||
},
|
},
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -182,7 +185,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
"certs/root.pem",
|
"certs/root.pem",
|
||||||
CryptoComponentType.TrustRoot,
|
CryptoComponentType.TrustRoot,
|
||||||
DateTimeOffset.UtcNow.AddYears(10))
|
DateTimeOffset.UtcNow.AddYears(10))
|
||||||
});
|
},
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -225,7 +229,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
new PolicyBuildConfig("p1", "default", "1.0", policy, "policies/default.rego", PolicyType.OpaRego)
|
new PolicyBuildConfig("p1", "default", "1.0", policy, "policies/default.rego", PolicyType.OpaRego)
|
||||||
},
|
},
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -261,7 +266,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -288,7 +294,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -328,7 +335,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CryptoBuildConfig("c1", "root", certFile, "crypto/certs/ca/root.pem", CryptoComponentType.TrustRoot, null)
|
new CryptoBuildConfig("c1", "root", certFile, "crypto/certs/ca/root.pem", CryptoComponentType.TrustRoot, null)
|
||||||
});
|
},
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -369,7 +377,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, format)
|
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, format)
|
||||||
},
|
},
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -404,7 +413,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
new PolicyBuildConfig("p1", "test", "1.0", policyFile, "policies/test", type)
|
new PolicyBuildConfig("p1", "test", "1.0", policyFile, "policies/test", type)
|
||||||
},
|
},
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -440,7 +450,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CryptoBuildConfig("c1", "test", certFile, "certs/test", type, null)
|
new CryptoBuildConfig("c1", "test", certFile, "certs/test", type, null)
|
||||||
});
|
},
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -468,7 +479,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
Array.Empty<FeedBuildConfig>(),
|
Array.Empty<FeedBuildConfig>(),
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
@@ -496,7 +508,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
|||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CryptoBuildConfig("c1", "root", certFile, "certs/root.pem", CryptoComponentType.TrustRoot, componentExpiry)
|
new CryptoBuildConfig("c1", "root", certFile, "certs/root.pem", CryptoComponentType.TrustRoot, componentExpiry)
|
||||||
});
|
},
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public class BundleManifestTests
|
|||||||
null,
|
null,
|
||||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", sourceFile, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", sourceFile, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||||
Array.Empty<PolicyBuildConfig>(),
|
Array.Empty<PolicyBuildConfig>(),
|
||||||
Array.Empty<CryptoBuildConfig>());
|
Array.Empty<CryptoBuildConfig>(),
|
||||||
|
Array.Empty<RuleBundleBuildConfig>());
|
||||||
|
|
||||||
var outputPath = Path.Combine(tempRoot, "bundle");
|
var outputPath = Path.Combine(tempRoot, "bundle");
|
||||||
var manifest = await builder.BuildAsync(request, outputPath);
|
var manifest = await builder.BuildAsync(request, outputPath);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||||
|
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
|
||||||
|
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj" />
|
||||||
|
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
|
||||||
|
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ internal sealed class ForensicVerifier : IForensicVerifier
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly ILogger<ForensicVerifier> _logger;
|
private readonly ILogger<ForensicVerifier> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ForensicVerifier(ILogger<ForensicVerifier> logger)
|
public ForensicVerifier(ILogger<ForensicVerifier> logger, TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ForensicVerificationResult> VerifyBundleAsync(
|
public async Task<ForensicVerificationResult> VerifyBundleAsync(
|
||||||
@@ -42,7 +44,7 @@ internal sealed class ForensicVerifier : IForensicVerifier
|
|||||||
|
|
||||||
var errors = new List<ForensicVerificationError>();
|
var errors = new List<ForensicVerificationError>();
|
||||||
var warnings = new List<string>();
|
var warnings = new List<string>();
|
||||||
var verifiedAt = DateTimeOffset.UtcNow;
|
var verifiedAt = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
_logger.LogDebug("Verifying forensic bundle at {BundlePath}", bundlePath);
|
_logger.LogDebug("Verifying forensic bundle at {BundlePath}", bundlePath);
|
||||||
|
|
||||||
@@ -440,7 +442,7 @@ internal sealed class ForensicVerifier : IForensicVerifier
|
|||||||
matchingRoot.PublicKey);
|
matchingRoot.PublicKey);
|
||||||
|
|
||||||
// Check time validity
|
// Check time validity
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
var timeValid = (!matchingRoot.NotBefore.HasValue || now >= matchingRoot.NotBefore.Value) &&
|
var timeValid = (!matchingRoot.NotBefore.HasValue || now >= matchingRoot.NotBefore.Value) &&
|
||||||
(!matchingRoot.NotAfter.HasValue || now <= matchingRoot.NotAfter.Value);
|
(!matchingRoot.NotAfter.HasValue || now <= matchingRoot.NotAfter.Value);
|
||||||
|
|
||||||
|
|||||||
@@ -17,17 +17,20 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
private readonly ITrustPolicyLoader _trustPolicyLoader;
|
private readonly ITrustPolicyLoader _trustPolicyLoader;
|
||||||
private readonly IDsseSignatureVerifier _dsseVerifier;
|
private readonly IDsseSignatureVerifier _dsseVerifier;
|
||||||
private readonly ILogger<ImageAttestationVerifier> _logger;
|
private readonly ILogger<ImageAttestationVerifier> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ImageAttestationVerifier(
|
public ImageAttestationVerifier(
|
||||||
IOciRegistryClient registryClient,
|
IOciRegistryClient registryClient,
|
||||||
ITrustPolicyLoader trustPolicyLoader,
|
ITrustPolicyLoader trustPolicyLoader,
|
||||||
IDsseSignatureVerifier dsseVerifier,
|
IDsseSignatureVerifier dsseVerifier,
|
||||||
ILogger<ImageAttestationVerifier> logger)
|
ILogger<ImageAttestationVerifier> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_registryClient = registryClient ?? throw new ArgumentNullException(nameof(registryClient));
|
_registryClient = registryClient ?? throw new ArgumentNullException(nameof(registryClient));
|
||||||
_trustPolicyLoader = trustPolicyLoader ?? throw new ArgumentNullException(nameof(trustPolicyLoader));
|
_trustPolicyLoader = trustPolicyLoader ?? throw new ArgumentNullException(nameof(trustPolicyLoader));
|
||||||
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
|
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImageVerificationResult> VerifyAsync(
|
public async Task<ImageVerificationResult> VerifyAsync(
|
||||||
@@ -51,7 +54,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
ImageDigest = digest,
|
ImageDigest = digest,
|
||||||
Registry = reference.Registry,
|
Registry = reference.Registry,
|
||||||
Repository = reference.Repository,
|
Repository = reference.Repository,
|
||||||
VerifiedAt = DateTimeOffset.UtcNow
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
|
|
||||||
OciReferrersResponse referrers;
|
OciReferrersResponse referrers;
|
||||||
@@ -191,7 +194,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
Digest = candidate.Digest,
|
Digest = candidate.Digest,
|
||||||
SignerIdentity = verification.KeyId,
|
SignerIdentity = verification.KeyId,
|
||||||
Message = verification.Error ?? "Signature verification failed",
|
Message = verification.Error ?? "Signature verification failed",
|
||||||
VerifiedAt = DateTimeOffset.UtcNow
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
Digest = candidate.Digest,
|
Digest = candidate.Digest,
|
||||||
SignerIdentity = signerKeyId,
|
SignerIdentity = signerKeyId,
|
||||||
Message = "Signer not allowed by trust policy",
|
Message = "Signer not allowed by trust policy",
|
||||||
VerifiedAt = DateTimeOffset.UtcNow
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,14 +223,14 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
Digest = candidate.Digest,
|
Digest = candidate.Digest,
|
||||||
SignerIdentity = signerKeyId,
|
SignerIdentity = signerKeyId,
|
||||||
Message = "Rekor receipt missing",
|
Message = "Rekor receipt missing",
|
||||||
VerifiedAt = DateTimeOffset.UtcNow
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policy.MaxAge.HasValue)
|
if (policy.MaxAge.HasValue)
|
||||||
{
|
{
|
||||||
var created = GetCreatedAt(candidate);
|
var created = GetCreatedAt(candidate);
|
||||||
if (created.HasValue && DateTimeOffset.UtcNow - created.Value > policy.MaxAge.Value)
|
if (created.HasValue && _timeProvider.GetUtcNow() - created.Value > policy.MaxAge.Value)
|
||||||
{
|
{
|
||||||
return new AttestationVerification
|
return new AttestationVerification
|
||||||
{
|
{
|
||||||
@@ -237,7 +240,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
Digest = candidate.Digest,
|
Digest = candidate.Digest,
|
||||||
SignerIdentity = signerKeyId,
|
SignerIdentity = signerKeyId,
|
||||||
Message = "Attestation exceeded max age",
|
Message = "Attestation exceeded max age",
|
||||||
VerifiedAt = DateTimeOffset.UtcNow
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +253,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
|||||||
Digest = candidate.Digest,
|
Digest = candidate.Digest,
|
||||||
SignerIdentity = signerKeyId,
|
SignerIdentity = signerKeyId,
|
||||||
Message = "Signature valid",
|
Message = "Signature valid",
|
||||||
VerifiedAt = DateTimeOffset.UtcNow
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public sealed class CertFrFeedClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
var advisoryId = ResolveAdvisoryId(itemElement, detailUri);
|
var advisoryId = ResolveAdvisoryId(itemElement, detailUri);
|
||||||
items.Add(new CertFrFeedItem(advisoryId, detailUri, published.ToUniversalTime(), title, summary));
|
items.Add(new CertFrFeedItem(advisoryId, detailUri, published.Value.ToUniversalTime(), title, summary));
|
||||||
}
|
}
|
||||||
|
|
||||||
_diagnostics.FeedFetchSuccess(items.Count);
|
_diagnostics.FeedFetchSuccess(items.Count);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public sealed class CachePerformanceBenchmarkTests : IAsyncLifetime
|
|||||||
_cacheService = new ValkeyAdvisoryCacheService(
|
_cacheService = new ValkeyAdvisoryCacheService(
|
||||||
_connectionFactory,
|
_connectionFactory,
|
||||||
options,
|
options,
|
||||||
|
metrics: null,
|
||||||
NullLogger<ValkeyAdvisoryCacheService>.Instance);
|
NullLogger<ValkeyAdvisoryCacheService>.Instance);
|
||||||
|
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ public sealed class ExportRetentionService : IExportRetentionService
|
|||||||
{
|
{
|
||||||
private readonly IExportRetentionStore _retentionStore;
|
private readonly IExportRetentionStore _retentionStore;
|
||||||
private readonly ILogger<ExportRetentionService> _logger;
|
private readonly ILogger<ExportRetentionService> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ExportRetentionService(
|
public ExportRetentionService(
|
||||||
IExportRetentionStore retentionStore,
|
IExportRetentionStore retentionStore,
|
||||||
ILogger<ExportRetentionService> logger)
|
ILogger<ExportRetentionService> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_retentionStore = retentionStore ?? throw new ArgumentNullException(nameof(retentionStore));
|
_retentionStore = retentionStore ?? throw new ArgumentNullException(nameof(retentionStore));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -26,7 +29,7 @@ public sealed class ExportRetentionService : IExportRetentionService
|
|||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
var retention = request.OverrideRetention ?? new ExportRetentionConfig();
|
var retention = request.OverrideRetention ?? new ExportRetentionConfig();
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Starting retention prune for tenant {TenantId}, profile {ProfileId}, execute={Execute}",
|
"Starting retention prune for tenant {TenantId}, profile {ProfileId}, execute={Execute}",
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ public sealed class InMemoryExportScheduleStore : IExportScheduleStore
|
|||||||
private readonly ConcurrentDictionary<Guid, Guid> _runToProfile = new();
|
private readonly ConcurrentDictionary<Guid, Guid> _runToProfile = new();
|
||||||
private readonly ConcurrentDictionary<Guid, List<ScheduledProfileInfo>> _profilesByTenant = new();
|
private readonly ConcurrentDictionary<Guid, List<ScheduledProfileInfo>> _profilesByTenant = new();
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public InMemoryExportScheduleStore(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a profile for testing.
|
/// Adds a profile for testing.
|
||||||
@@ -106,7 +112,7 @@ public sealed class InMemoryExportScheduleStore : IExportScheduleStore
|
|||||||
{
|
{
|
||||||
if (_statusByProfile.TryGetValue(profileId, out var existing))
|
if (_statusByProfile.TryGetValue(profileId, out var existing))
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
var newFailureCount = success ? 0 : existing.ConsecutiveFailures + 1;
|
var newFailureCount = success ? 0 : existing.ConsecutiveFailures + 1;
|
||||||
|
|
||||||
_statusByProfile[profileId] = existing with
|
_statusByProfile[profileId] = existing with
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly ILogger<LineageEvidencePackService> _logger;
|
private readonly ILogger<LineageEvidencePackService> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly ConcurrentDictionary<Guid, CachedPack> _packCache = new();
|
private readonly ConcurrentDictionary<Guid, CachedPack> _packCache = new();
|
||||||
private readonly string _tempDirectory;
|
private readonly string _tempDirectory;
|
||||||
|
|
||||||
public LineageEvidencePackService(ILogger<LineageEvidencePackService> logger)
|
public LineageEvidencePackService(ILogger<LineageEvidencePackService> logger, TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
_tempDirectory = Path.Combine(Path.GetTempPath(), "stellaops-evidence-packs");
|
_tempDirectory = Path.Combine(Path.GetTempPath(), "stellaops-evidence-packs");
|
||||||
Directory.CreateDirectory(_tempDirectory);
|
Directory.CreateDirectory(_tempDirectory);
|
||||||
}
|
}
|
||||||
@@ -187,7 +189,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
Entries = entries.ToImmutableArray(),
|
Entries = entries.ToImmutableArray(),
|
||||||
TotalSizeBytes = entries.Sum(e => e.SizeBytes),
|
TotalSizeBytes = entries.Sum(e => e.SizeBytes),
|
||||||
FileCount = entries.Count,
|
FileCount = entries.Count,
|
||||||
CreatedAt = DateTimeOffset.UtcNow
|
CreatedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write manifest
|
// Write manifest
|
||||||
@@ -205,7 +207,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
VexVerdictDigests = vexDocuments.Select(v => v.Digest).ToImmutableArray(),
|
VexVerdictDigests = vexDocuments.Select(v => v.Digest).ToImmutableArray(),
|
||||||
PolicyVerdictDigest = policyVerdict?.Digest,
|
PolicyVerdictDigest = policyVerdict?.Digest,
|
||||||
ReplayHash = ComputeReplayHash(artifactDigest, sbomDigest, manifest.MerkleRoot),
|
ReplayHash = ComputeReplayHash(artifactDigest, sbomDigest, manifest.MerkleRoot),
|
||||||
GeneratedAt = DateTimeOffset.UtcNow,
|
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||||
Attestations = attestations.ToImmutableArray(),
|
Attestations = attestations.ToImmutableArray(),
|
||||||
SbomDocuments = sbomDocuments.ToImmutableArray(),
|
SbomDocuments = sbomDocuments.ToImmutableArray(),
|
||||||
VexDocuments = vexDocuments.ToImmutableArray(),
|
VexDocuments = vexDocuments.ToImmutableArray(),
|
||||||
@@ -224,7 +226,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
{
|
{
|
||||||
Pack = pack,
|
Pack = pack,
|
||||||
ZipPath = zipPath,
|
ZipPath = zipPath,
|
||||||
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24)
|
ExpiresAt = _timeProvider.GetUtcNow().AddHours(24)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clean up temp directory
|
// Clean up temp directory
|
||||||
@@ -246,7 +248,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
Success = true,
|
Success = true,
|
||||||
Pack = pack,
|
Pack = pack,
|
||||||
DownloadUrl = $"/api/v1/lineage/export/{packId}/download",
|
DownloadUrl = $"/api/v1/lineage/export/{packId}/download",
|
||||||
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24),
|
ExpiresAt = _timeProvider.GetUtcNow().AddHours(24),
|
||||||
SizeBytes = zipInfo.Length,
|
SizeBytes = zipInfo.Length,
|
||||||
Warnings = warnings.ToImmutableArray()
|
Warnings = warnings.ToImmutableArray()
|
||||||
};
|
};
|
||||||
@@ -268,7 +270,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
string tenantId,
|
string tenantId,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow)
|
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > _timeProvider.GetUtcNow())
|
||||||
{
|
{
|
||||||
if (cached.Pack.TenantId == tenantId)
|
if (cached.Pack.TenantId == tenantId)
|
||||||
{
|
{
|
||||||
@@ -285,7 +287,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
string tenantId,
|
string tenantId,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow)
|
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > _timeProvider.GetUtcNow())
|
||||||
{
|
{
|
||||||
if (cached.Pack.TenantId == tenantId && File.Exists(cached.ZipPath))
|
if (cached.Pack.TenantId == tenantId && File.Exists(cached.ZipPath))
|
||||||
{
|
{
|
||||||
@@ -347,7 +349,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
bomFormat = "CycloneDX",
|
bomFormat = "CycloneDX",
|
||||||
specVersion = "1.6",
|
specVersion = "1.6",
|
||||||
version = 1,
|
version = 1,
|
||||||
metadata = new { timestamp = DateTimeOffset.UtcNow.ToString("O") },
|
metadata = new { timestamp = _timeProvider.GetUtcNow().ToString("O") },
|
||||||
components = Array.Empty<object>()
|
components = Array.Empty<object>()
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
|
|
||||||
@@ -383,7 +385,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
dataLicense = "CC0-1.0",
|
dataLicense = "CC0-1.0",
|
||||||
name = artifactDigest,
|
name = artifactDigest,
|
||||||
documentNamespace = $"https://stellaops.io/spdx/{artifactDigest}",
|
documentNamespace = $"https://stellaops.io/spdx/{artifactDigest}",
|
||||||
creationInfo = new { created = DateTimeOffset.UtcNow.ToString("O") },
|
creationInfo = new { created = _timeProvider.GetUtcNow().ToString("O") },
|
||||||
packages = Array.Empty<object>()
|
packages = Array.Empty<object>()
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
|
|
||||||
@@ -418,7 +420,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
context = "https://openvex.dev/ns/v0.2.0",
|
context = "https://openvex.dev/ns/v0.2.0",
|
||||||
id = $"urn:stellaops:vex:{artifactDigest}",
|
id = $"urn:stellaops:vex:{artifactDigest}",
|
||||||
author = "StellaOps",
|
author = "StellaOps",
|
||||||
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
timestamp = _timeProvider.GetUtcNow().ToString("O"),
|
||||||
statements = Array.Empty<object>()
|
statements = Array.Empty<object>()
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
|
|
||||||
@@ -457,7 +459,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
tenantId,
|
tenantId,
|
||||||
verdict = "pass",
|
verdict = "pass",
|
||||||
policyVersion = "1.0.0",
|
policyVersion = "1.0.0",
|
||||||
evaluatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
evaluatedAt = _timeProvider.GetUtcNow().ToString("O"),
|
||||||
rules = new { total = 0, passed = 0, failed = 0, warned = 0 }
|
rules = new { total = 0, passed = 0, failed = 0, warned = 0 }
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
|
|
||||||
@@ -477,7 +479,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
RulesPassed = 0,
|
RulesPassed = 0,
|
||||||
RulesFailed = 0,
|
RulesFailed = 0,
|
||||||
RulesWarned = 0,
|
RulesWarned = 0,
|
||||||
EvaluatedAt = DateTimeOffset.UtcNow,
|
EvaluatedAt = _timeProvider.GetUtcNow(),
|
||||||
FileName = fileName
|
FileName = fileName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -528,9 +530,9 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
|||||||
return ComputeHash(combined);
|
return ComputeHash(combined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ComputeReplayHash(string artifactDigest, string sbomDigest, string merkleRoot)
|
private string ComputeReplayHash(string artifactDigest, string sbomDigest, string merkleRoot)
|
||||||
{
|
{
|
||||||
var input = $"{artifactDigest}|{sbomDigest}|{merkleRoot}|{DateTimeOffset.UtcNow:O}";
|
var input = $"{artifactDigest}|{sbomDigest}|{merkleRoot}|{_timeProvider.GetUtcNow():O}";
|
||||||
return $"sha256:{ComputeHash(input)}";
|
return $"sha256:{ComputeHash(input)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,14 +149,15 @@ public sealed record ExportVerificationResult
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// When verification was performed.
|
/// When verification was performed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
|
public required DateTimeOffset VerifiedAt { get; init; }
|
||||||
|
|
||||||
public static ExportVerificationResult Failed(Guid runId, params VerificationError[] errors)
|
public static ExportVerificationResult Failed(Guid runId, DateTimeOffset verifiedAt, params VerificationError[] errors)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
Status = VerificationStatus.Invalid,
|
Status = VerificationStatus.Invalid,
|
||||||
RunId = runId,
|
RunId = runId,
|
||||||
Errors = errors
|
Errors = errors,
|
||||||
|
VerifiedAt = verifiedAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,22 +14,26 @@ public sealed class ExportVerificationService : IExportVerificationService
|
|||||||
private readonly IExportArtifactStore _artifactStore;
|
private readonly IExportArtifactStore _artifactStore;
|
||||||
private readonly IPackRunAttestationStore? _packRunStore;
|
private readonly IPackRunAttestationStore? _packRunStore;
|
||||||
private readonly ILogger<ExportVerificationService> _logger;
|
private readonly ILogger<ExportVerificationService> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ExportVerificationService(
|
public ExportVerificationService(
|
||||||
IExportArtifactStore artifactStore,
|
IExportArtifactStore artifactStore,
|
||||||
ILogger<ExportVerificationService> logger)
|
ILogger<ExportVerificationService> logger,
|
||||||
: this(artifactStore, null, logger)
|
TimeProvider? timeProvider = null)
|
||||||
|
: this(artifactStore, null, logger, timeProvider)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExportVerificationService(
|
public ExportVerificationService(
|
||||||
IExportArtifactStore artifactStore,
|
IExportArtifactStore artifactStore,
|
||||||
IPackRunAttestationStore? packRunStore,
|
IPackRunAttestationStore? packRunStore,
|
||||||
ILogger<ExportVerificationService> logger)
|
ILogger<ExportVerificationService> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore));
|
_artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore));
|
||||||
_packRunStore = packRunStore;
|
_packRunStore = packRunStore;
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -52,6 +56,7 @@ public sealed class ExportVerificationService : IExportVerificationService
|
|||||||
{
|
{
|
||||||
return ExportVerificationResult.Failed(
|
return ExportVerificationResult.Failed(
|
||||||
request.RunId,
|
request.RunId,
|
||||||
|
_timeProvider.GetUtcNow(),
|
||||||
new VerificationError
|
new VerificationError
|
||||||
{
|
{
|
||||||
Code = VerificationErrorCodes.ManifestNotFound,
|
Code = VerificationErrorCodes.ManifestNotFound,
|
||||||
@@ -64,6 +69,7 @@ public sealed class ExportVerificationService : IExportVerificationService
|
|||||||
{
|
{
|
||||||
return ExportVerificationResult.Failed(
|
return ExportVerificationResult.Failed(
|
||||||
request.RunId,
|
request.RunId,
|
||||||
|
_timeProvider.GetUtcNow(),
|
||||||
new VerificationError
|
new VerificationError
|
||||||
{
|
{
|
||||||
Code = VerificationErrorCodes.TenantMismatch,
|
Code = VerificationErrorCodes.TenantMismatch,
|
||||||
@@ -234,7 +240,8 @@ public sealed class ExportVerificationService : IExportVerificationService
|
|||||||
Encryption = encryptionResult,
|
Encryption = encryptionResult,
|
||||||
Attestation = attestationStatus,
|
Attestation = attestationStatus,
|
||||||
Errors = errors,
|
Errors = errors,
|
||||||
Warnings = warnings
|
Warnings = warnings,
|
||||||
|
VerifiedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
private readonly IExceptionApplicationRepository _applicationRepository;
|
private readonly IExceptionApplicationRepository _applicationRepository;
|
||||||
private readonly ConcurrentDictionary<string, ReportJob> _jobs = new();
|
private readonly ConcurrentDictionary<string, ReportJob> _jobs = new();
|
||||||
private readonly ILogger<ExceptionReportGenerator> _logger;
|
private readonly ILogger<ExceptionReportGenerator> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@@ -30,11 +31,13 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
public ExceptionReportGenerator(
|
public ExceptionReportGenerator(
|
||||||
IExceptionRepository exceptionRepository,
|
IExceptionRepository exceptionRepository,
|
||||||
IExceptionApplicationRepository applicationRepository,
|
IExceptionApplicationRepository applicationRepository,
|
||||||
ILogger<ExceptionReportGenerator> logger)
|
ILogger<ExceptionReportGenerator> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_exceptionRepository = exceptionRepository;
|
_exceptionRepository = exceptionRepository;
|
||||||
_applicationRepository = applicationRepository;
|
_applicationRepository = applicationRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ExceptionReportJobResponse> CreateReportAsync(
|
public async Task<ExceptionReportJobResponse> CreateReportAsync(
|
||||||
@@ -42,7 +45,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var jobId = $"exc-rpt-{Guid.NewGuid():N}";
|
var jobId = $"exc-rpt-{Guid.NewGuid():N}";
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
var job = new ReportJob
|
var job = new ReportJob
|
||||||
{
|
{
|
||||||
@@ -151,7 +154,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
job.Status = "running";
|
job.Status = "running";
|
||||||
job.StartedAt = DateTimeOffset.UtcNow;
|
job.StartedAt = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
var filter = job.Request.Filter ?? new ExceptionFilter
|
var filter = job.Request.Filter ?? new ExceptionFilter
|
||||||
{
|
{
|
||||||
@@ -232,7 +235,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
var document = new ExceptionReportDocument
|
var document = new ExceptionReportDocument
|
||||||
{
|
{
|
||||||
ReportId = job.JobId,
|
ReportId = job.JobId,
|
||||||
GeneratedAt = DateTimeOffset.UtcNow,
|
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||||
TenantId = job.TenantId,
|
TenantId = job.TenantId,
|
||||||
RequesterId = job.RequesterId,
|
RequesterId = job.RequesterId,
|
||||||
Title = job.Request.Title ?? "Exception Report",
|
Title = job.Request.Title ?? "Exception Report",
|
||||||
@@ -289,7 +292,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
job.ContentHash = $"sha256:{Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant()}";
|
job.ContentHash = $"sha256:{Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant()}";
|
||||||
job.Progress = 100;
|
job.Progress = 100;
|
||||||
job.Status = "completed";
|
job.Status = "completed";
|
||||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
job.CompletedAt = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Completed exception report {JobId} with {Count} exceptions, {Size} bytes",
|
"Completed exception report {JobId} with {Count} exceptions, {Size} bytes",
|
||||||
@@ -300,7 +303,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
|||||||
_logger.LogError(ex, "Failed to generate exception report {JobId}", job.JobId);
|
_logger.LogError(ex, "Failed to generate exception report {JobId}", job.JobId);
|
||||||
job.Status = "failed";
|
job.Status = "failed";
|
||||||
job.ErrorMessage = ex.Message;
|
job.ErrorMessage = ex.Message;
|
||||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
job.CompletedAt = _timeProvider.GetUtcNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ namespace StellaOps.Findings.Ledger.WebService.Mappings;
|
|||||||
|
|
||||||
public static class LedgerEventMapping
|
public static class LedgerEventMapping
|
||||||
{
|
{
|
||||||
public static LedgerEventDraft ToDraft(this LedgerEventRequest request)
|
public static LedgerEventDraft ToDraft(this LedgerEventRequest request, TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
var recordedAt = (request.RecordedAt ?? DateTimeOffset.UtcNow).ToUniversalTime();
|
timeProvider ??= TimeProvider.System;
|
||||||
|
var recordedAt = (request.RecordedAt ?? timeProvider.GetUtcNow()).ToUniversalTime();
|
||||||
var payload = request.Payload is null ? new JsonObject() : (JsonObject)request.Payload.DeepClone();
|
var payload = request.Payload is null ? new JsonObject() : (JsonObject)request.Payload.DeepClone();
|
||||||
|
|
||||||
var eventObject = new JsonObject
|
var eventObject = new JsonObject
|
||||||
|
|||||||
@@ -1768,6 +1768,7 @@ app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task<Results<Ok<BundleVe
|
|||||||
[FromBody] BundleVerificationRequest request,
|
[FromBody] BundleVerificationRequest request,
|
||||||
[FromServices] IAlertService alertService,
|
[FromServices] IAlertService alertService,
|
||||||
[FromServices] IEvidenceBundleService bundleService,
|
[FromServices] IEvidenceBundleService bundleService,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken cancellationToken) =>
|
CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
var alert = await alertService.GetAlertAsync(alertId, cancellationToken).ConfigureAwait(false);
|
var alert = await alertService.GetAlertAsync(alertId, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -1786,7 +1787,7 @@ app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task<Results<Ok<BundleVe
|
|||||||
{
|
{
|
||||||
AlertId = alertId,
|
AlertId = alertId,
|
||||||
IsValid = result.IsValid,
|
IsValid = result.IsValid,
|
||||||
VerifiedAt = DateTimeOffset.UtcNow,
|
VerifiedAt = timeProvider.GetUtcNow(),
|
||||||
SignatureValid = result.SignatureValid,
|
SignatureValid = result.SignatureValid,
|
||||||
HashValid = result.HashValid,
|
HashValid = result.HashValid,
|
||||||
ChainValid = result.ChainValid,
|
ChainValid = result.ChainValid,
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder
|
|||||||
{
|
{
|
||||||
private readonly IEvidenceRepository _evidenceRepo;
|
private readonly IEvidenceRepository _evidenceRepo;
|
||||||
private readonly IAttestationVerifier _attestationVerifier;
|
private readonly IAttestationVerifier _attestationVerifier;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public EvidenceGraphBuilder(
|
public EvidenceGraphBuilder(
|
||||||
IEvidenceRepository evidenceRepo,
|
IEvidenceRepository evidenceRepo,
|
||||||
IAttestationVerifier attestationVerifier)
|
IAttestationVerifier attestationVerifier,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_evidenceRepo = evidenceRepo;
|
_evidenceRepo = evidenceRepo;
|
||||||
_attestationVerifier = attestationVerifier;
|
_attestationVerifier = attestationVerifier;
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EvidenceGraphResponse?> BuildAsync(
|
public async Task<EvidenceGraphResponse?> BuildAsync(
|
||||||
@@ -126,7 +129,7 @@ public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder
|
|||||||
Nodes = nodes,
|
Nodes = nodes,
|
||||||
Edges = edges,
|
Edges = edges,
|
||||||
RootNodeId = verdictNode.Id,
|
RootNodeId = verdictNode.Id,
|
||||||
GeneratedAt = DateTimeOffset.UtcNow
|
GeneratedAt = _timeProvider.GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public sealed class FindingWorkflowService : IFindingWorkflowService
|
|||||||
|
|
||||||
var payload = CreateBasePayload(request);
|
var payload = CreateBasePayload(request);
|
||||||
payload["action"] = "assign";
|
payload["action"] = "assign";
|
||||||
payload["assignee"] = BuildAssigneeNode(request.Assignee);
|
payload["assignee"] = BuildAssigneeNode(request.Assignee!);
|
||||||
AddComment(payload, request.Comment);
|
AddComment(payload, request.Comment);
|
||||||
ApplyStatus(payload, request.Status);
|
ApplyStatus(payload, request.Status);
|
||||||
ApplyAttachments(payload, request.Attachments);
|
ApplyAttachments(payload, request.Attachments);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
|
||||||
|
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Persistence/StellaOps.Notify.Persistence.csproj" />
|
||||||
|
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
|
||||||
|
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
|
||||||
|
<ProjectReference Include="../../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||||
|
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ namespace StellaOps.Notify.Storage.InMemory.Repositories;
|
|||||||
public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyChannelDocument> _channels = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyChannelDocument> _channels = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyChannelRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyChannelDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyChannelDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -34,7 +40,7 @@ public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
|||||||
|
|
||||||
public Task<NotifyChannelDocument> UpsertAsync(NotifyChannelDocument channel, CancellationToken cancellationToken = default)
|
public Task<NotifyChannelDocument> UpsertAsync(NotifyChannelDocument channel, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
channel.UpdatedAt = DateTimeOffset.UtcNow;
|
channel.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{channel.TenantId}:{channel.Id}";
|
var key = $"{channel.TenantId}:{channel.Id}";
|
||||||
_channels[key] = channel;
|
_channels[key] = channel;
|
||||||
return Task.FromResult(channel);
|
return Task.FromResult(channel);
|
||||||
@@ -59,6 +65,12 @@ public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
|||||||
public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyRuleDocument> _rules = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyRuleDocument> _rules = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyRuleRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyRuleDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyRuleDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -83,7 +95,7 @@ public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
|||||||
|
|
||||||
public Task<NotifyRuleDocument> UpsertAsync(NotifyRuleDocument rule, CancellationToken cancellationToken = default)
|
public Task<NotifyRuleDocument> UpsertAsync(NotifyRuleDocument rule, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
rule.UpdatedAt = DateTimeOffset.UtcNow;
|
rule.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{rule.TenantId}:{rule.Id}";
|
var key = $"{rule.TenantId}:{rule.Id}";
|
||||||
_rules[key] = rule;
|
_rules[key] = rule;
|
||||||
return Task.FromResult(rule);
|
return Task.FromResult(rule);
|
||||||
@@ -108,6 +120,12 @@ public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
|||||||
public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyTemplateDocument> _templates = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyTemplateDocument> _templates = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyTemplateRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyTemplateDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyTemplateDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -130,7 +148,7 @@ public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
|||||||
|
|
||||||
public Task<NotifyTemplateDocument> UpsertAsync(NotifyTemplateDocument template, CancellationToken cancellationToken = default)
|
public Task<NotifyTemplateDocument> UpsertAsync(NotifyTemplateDocument template, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
template.UpdatedAt = DateTimeOffset.UtcNow;
|
template.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{template.TenantId}:{template.Id}";
|
var key = $"{template.TenantId}:{template.Id}";
|
||||||
_templates[key] = template;
|
_templates[key] = template;
|
||||||
return Task.FromResult(template);
|
return Task.FromResult(template);
|
||||||
@@ -149,6 +167,12 @@ public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
|||||||
public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyDeliveryDocument> _deliveries = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyDeliveryDocument> _deliveries = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyDeliveryRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyDeliveryDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyDeliveryDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -166,7 +190,7 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
|||||||
|
|
||||||
public Task<NotifyDeliveryDocument> UpsertAsync(NotifyDeliveryDocument delivery, CancellationToken cancellationToken = default)
|
public Task<NotifyDeliveryDocument> UpsertAsync(NotifyDeliveryDocument delivery, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
delivery.UpdatedAt = DateTimeOffset.UtcNow;
|
delivery.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{delivery.TenantId}:{delivery.Id}";
|
var key = $"{delivery.TenantId}:{delivery.Id}";
|
||||||
_deliveries[key] = delivery;
|
_deliveries[key] = delivery;
|
||||||
return Task.FromResult(delivery);
|
return Task.FromResult(delivery);
|
||||||
@@ -179,7 +203,7 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
|||||||
{
|
{
|
||||||
doc.Status = status;
|
doc.Status = status;
|
||||||
doc.Error = error;
|
doc.Error = error;
|
||||||
doc.UpdatedAt = DateTimeOffset.UtcNow;
|
doc.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
@@ -199,6 +223,12 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
|||||||
public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository
|
public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyDigestDocument> _digests = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyDigestDocument> _digests = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyDigestRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyDigestDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyDigestDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -209,7 +239,7 @@ public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository
|
|||||||
|
|
||||||
public Task<NotifyDigestDocument> UpsertAsync(NotifyDigestDocument digest, CancellationToken cancellationToken = default)
|
public Task<NotifyDigestDocument> UpsertAsync(NotifyDigestDocument digest, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
digest.UpdatedAt = DateTimeOffset.UtcNow;
|
digest.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{digest.TenantId}:{digest.Id}";
|
var key = $"{digest.TenantId}:{digest.Id}";
|
||||||
_digests[key] = digest;
|
_digests[key] = digest;
|
||||||
return Task.FromResult(digest);
|
return Task.FromResult(digest);
|
||||||
@@ -257,10 +287,16 @@ public sealed class NotifyAuditRepositoryAdapter : INotifyAuditRepository
|
|||||||
public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, (string Owner, DateTimeOffset ExpiresAt)> _locks = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, (string Owner, DateTimeOffset ExpiresAt)> _locks = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyLockRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<bool> TryAcquireAsync(string lockKey, string owner, TimeSpan ttl, CancellationToken cancellationToken = default)
|
public Task<bool> TryAcquireAsync(string lockKey, string owner, TimeSpan ttl, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
// Clean up expired locks
|
// Clean up expired locks
|
||||||
foreach (var key in _locks.Keys.ToList())
|
foreach (var key in _locks.Keys.ToList())
|
||||||
@@ -288,7 +324,7 @@ public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
|||||||
{
|
{
|
||||||
if (_locks.TryGetValue(lockKey, out var value) && value.Owner == owner)
|
if (_locks.TryGetValue(lockKey, out var value) && value.Owner == owner)
|
||||||
{
|
{
|
||||||
var newExpiry = DateTimeOffset.UtcNow + ttl;
|
var newExpiry = _timeProvider.GetUtcNow() + ttl;
|
||||||
_locks[lockKey] = (owner, newExpiry);
|
_locks[lockKey] = (owner, newExpiry);
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
@@ -302,6 +338,12 @@ public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
|||||||
public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationPolicyRepository
|
public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationPolicyRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyEscalationPolicyDocument> _policies = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyEscalationPolicyDocument> _policies = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyEscalationPolicyRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyEscalationPolicyDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyEscalationPolicyDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -318,7 +360,7 @@ public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationP
|
|||||||
|
|
||||||
public Task<NotifyEscalationPolicyDocument> UpsertAsync(NotifyEscalationPolicyDocument policy, CancellationToken cancellationToken = default)
|
public Task<NotifyEscalationPolicyDocument> UpsertAsync(NotifyEscalationPolicyDocument policy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
policy.UpdatedAt = DateTimeOffset.UtcNow;
|
policy.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{policy.TenantId}:{policy.Id}";
|
var key = $"{policy.TenantId}:{policy.Id}";
|
||||||
_policies[key] = policy;
|
_policies[key] = policy;
|
||||||
return Task.FromResult(policy);
|
return Task.FromResult(policy);
|
||||||
@@ -331,6 +373,12 @@ public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationP
|
|||||||
public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationStateRepository
|
public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationStateRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyEscalationStateDocument> _states = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyEscalationStateDocument> _states = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyEscalationStateRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyEscalationStateDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyEscalationStateDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -341,7 +389,7 @@ public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationSt
|
|||||||
|
|
||||||
public Task<NotifyEscalationStateDocument> UpsertAsync(NotifyEscalationStateDocument state, CancellationToken cancellationToken = default)
|
public Task<NotifyEscalationStateDocument> UpsertAsync(NotifyEscalationStateDocument state, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
state.UpdatedAt = DateTimeOffset.UtcNow;
|
state.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{state.TenantId}:{state.Id}";
|
var key = $"{state.TenantId}:{state.Id}";
|
||||||
_states[key] = state;
|
_states[key] = state;
|
||||||
return Task.FromResult(state);
|
return Task.FromResult(state);
|
||||||
@@ -360,6 +408,12 @@ public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationSt
|
|||||||
public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallScheduleRepository
|
public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallScheduleRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyOnCallScheduleDocument> _schedules = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyOnCallScheduleDocument> _schedules = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyOnCallScheduleRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyOnCallScheduleDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyOnCallScheduleDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -376,7 +430,7 @@ public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallSchedul
|
|||||||
|
|
||||||
public Task<NotifyOnCallScheduleDocument> UpsertAsync(NotifyOnCallScheduleDocument schedule, CancellationToken cancellationToken = default)
|
public Task<NotifyOnCallScheduleDocument> UpsertAsync(NotifyOnCallScheduleDocument schedule, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
schedule.UpdatedAt = DateTimeOffset.UtcNow;
|
schedule.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{schedule.TenantId}:{schedule.Id}";
|
var key = $"{schedule.TenantId}:{schedule.Id}";
|
||||||
_schedules[key] = schedule;
|
_schedules[key] = schedule;
|
||||||
return Task.FromResult(schedule);
|
return Task.FromResult(schedule);
|
||||||
@@ -397,6 +451,12 @@ public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallSchedul
|
|||||||
public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursRepository
|
public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyQuietHoursDocument> _quietHours = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyQuietHoursDocument> _quietHours = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyQuietHoursRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyQuietHoursDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyQuietHoursDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -413,7 +473,7 @@ public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursReposit
|
|||||||
|
|
||||||
public Task<NotifyQuietHoursDocument> UpsertAsync(NotifyQuietHoursDocument quietHours, CancellationToken cancellationToken = default)
|
public Task<NotifyQuietHoursDocument> UpsertAsync(NotifyQuietHoursDocument quietHours, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
quietHours.UpdatedAt = DateTimeOffset.UtcNow;
|
quietHours.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{quietHours.TenantId}:{quietHours.Id}";
|
var key = $"{quietHours.TenantId}:{quietHours.Id}";
|
||||||
_quietHours[key] = quietHours;
|
_quietHours[key] = quietHours;
|
||||||
return Task.FromResult(quietHours);
|
return Task.FromResult(quietHours);
|
||||||
@@ -432,6 +492,12 @@ public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursReposit
|
|||||||
public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanceWindowRepository
|
public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanceWindowRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyMaintenanceWindowDocument> _windows = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyMaintenanceWindowDocument> _windows = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyMaintenanceWindowRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyMaintenanceWindowDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyMaintenanceWindowDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -454,7 +520,7 @@ public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanc
|
|||||||
|
|
||||||
public Task<NotifyMaintenanceWindowDocument> UpsertAsync(NotifyMaintenanceWindowDocument window, CancellationToken cancellationToken = default)
|
public Task<NotifyMaintenanceWindowDocument> UpsertAsync(NotifyMaintenanceWindowDocument window, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
window.UpdatedAt = DateTimeOffset.UtcNow;
|
window.UpdatedAt = _timeProvider.GetUtcNow();
|
||||||
var key = $"{window.TenantId}:{window.Id}";
|
var key = $"{window.TenantId}:{window.Id}";
|
||||||
_windows[key] = window;
|
_windows[key] = window;
|
||||||
return Task.FromResult(window);
|
return Task.FromResult(window);
|
||||||
@@ -473,6 +539,12 @@ public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanc
|
|||||||
public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository
|
public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, NotifyInboxDocument> _inbox = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, NotifyInboxDocument> _inbox = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public NotifyInboxRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<NotifyInboxDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
public Task<NotifyInboxDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -502,7 +574,7 @@ public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository
|
|||||||
if (_inbox.TryGetValue(key, out var doc))
|
if (_inbox.TryGetValue(key, out var doc))
|
||||||
{
|
{
|
||||||
doc.Read = true;
|
doc.Read = true;
|
||||||
doc.ReadAt = DateTimeOffset.UtcNow;
|
doc.ReadAt = _timeProvider.GetUtcNow();
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class LedgerExporter : ILedgerExporter
|
|||||||
private readonly ILedgerRepository _ledgerRepository;
|
private readonly ILedgerRepository _ledgerRepository;
|
||||||
private readonly ILedgerExportRepository _exportRepository;
|
private readonly ILedgerExportRepository _exportRepository;
|
||||||
private readonly ILogger<LedgerExporter> _logger;
|
private readonly ILogger<LedgerExporter> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@@ -32,11 +33,13 @@ public sealed class LedgerExporter : ILedgerExporter
|
|||||||
public LedgerExporter(
|
public LedgerExporter(
|
||||||
ILedgerRepository ledgerRepository,
|
ILedgerRepository ledgerRepository,
|
||||||
ILedgerExportRepository exportRepository,
|
ILedgerExportRepository exportRepository,
|
||||||
ILogger<LedgerExporter> logger)
|
ILogger<LedgerExporter> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_ledgerRepository = ledgerRepository;
|
_ledgerRepository = ledgerRepository;
|
||||||
_exportRepository = exportRepository;
|
_exportRepository = exportRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -44,7 +47,7 @@ public sealed class LedgerExporter : ILedgerExporter
|
|||||||
LedgerExport export,
|
LedgerExport export,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var startTime = DateTimeOffset.UtcNow;
|
var startTime = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -83,7 +86,7 @@ public sealed class LedgerExporter : ILedgerExporter
|
|||||||
export = export.Complete(outputUri, digest, sizeBytes, entries.Count);
|
export = export.Complete(outputUri, digest, sizeBytes, entries.Count);
|
||||||
export = await _exportRepository.UpdateAsync(export, cancellationToken);
|
export = await _exportRepository.UpdateAsync(export, cancellationToken);
|
||||||
|
|
||||||
var duration = DateTimeOffset.UtcNow - startTime;
|
var duration = _timeProvider.GetUtcNow() - startTime;
|
||||||
OrchestratorMetrics.LedgerExportCompleted(export.TenantId, export.Format);
|
OrchestratorMetrics.LedgerExportCompleted(export.TenantId, export.Format);
|
||||||
OrchestratorMetrics.RecordLedgerExportDuration(export.TenantId, export.Format, duration.TotalSeconds);
|
OrchestratorMetrics.RecordLedgerExportDuration(export.TenantId, export.Format, duration.TotalSeconds);
|
||||||
OrchestratorMetrics.RecordLedgerExportSize(export.TenantId, export.Format, sizeBytes);
|
OrchestratorMetrics.RecordLedgerExportSize(export.TenantId, export.Format, sizeBytes);
|
||||||
@@ -165,12 +168,12 @@ public sealed class LedgerExporter : ILedgerExporter
|
|||||||
return (content, digest);
|
return (content, digest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GenerateJson(IReadOnlyList<RunLedgerEntry> entries)
|
private string GenerateJson(IReadOnlyList<RunLedgerEntry> entries)
|
||||||
{
|
{
|
||||||
var exportData = new LedgerExportData
|
var exportData = new LedgerExportData
|
||||||
{
|
{
|
||||||
SchemaVersion = "1.0.0",
|
SchemaVersion = "1.0.0",
|
||||||
ExportedAt = DateTimeOffset.UtcNow,
|
ExportedAt = _timeProvider.GetUtcNow(),
|
||||||
EntryCount = entries.Count,
|
EntryCount = entries.Count,
|
||||||
Entries = entries.Select(MapEntry).ToList()
|
Entries = entries.Select(MapEntry).ToList()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,15 +56,18 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor
|
|||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly string _tenantId;
|
private readonly string _tenantId;
|
||||||
private readonly ILogger<PostgresDuplicateSuppressor> _logger;
|
private readonly ILogger<PostgresDuplicateSuppressor> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresDuplicateSuppressor(
|
public PostgresDuplicateSuppressor(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
string tenantId,
|
string tenantId,
|
||||||
ILogger<PostgresDuplicateSuppressor> logger)
|
ILogger<PostgresDuplicateSuppressor> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
|
_tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> HasProcessedAsync(string scopeKey, string eventKey, CancellationToken cancellationToken)
|
public async Task<bool> HasProcessedAsync(string scopeKey, string eventKey, CancellationToken cancellationToken)
|
||||||
@@ -125,7 +128,7 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor
|
|||||||
command.Parameters.AddWithValue("event_key", eventKey);
|
command.Parameters.AddWithValue("event_key", eventKey);
|
||||||
command.Parameters.AddWithValue("event_time", eventTime);
|
command.Parameters.AddWithValue("event_time", eventTime);
|
||||||
command.Parameters.AddWithValue("batch_id", (object?)batchId ?? DBNull.Value);
|
command.Parameters.AddWithValue("batch_id", (object?)batchId ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("expires_at", DateTimeOffset.UtcNow + ttl);
|
command.Parameters.AddWithValue("expires_at", _timeProvider.GetUtcNow() + ttl);
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -143,7 +146,7 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expiresAt = DateTimeOffset.UtcNow + ttl;
|
var expiresAt = _timeProvider.GetUtcNow() + ttl;
|
||||||
|
|
||||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -108,13 +108,16 @@ public sealed class PostgresJobRepository : IJobRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresJobRepository> _logger;
|
private readonly ILogger<PostgresJobRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresJobRepository(
|
public PostgresJobRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresJobRepository> logger)
|
ILogger<PostgresJobRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Job?> GetByIdAsync(string tenantId, Guid jobId, CancellationToken cancellationToken)
|
public async Task<Job?> GetByIdAsync(string tenantId, Guid jobId, CancellationToken cancellationToken)
|
||||||
@@ -228,8 +231,8 @@ public sealed class PostgresJobRepository : IJobRepository
|
|||||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||||
command.Parameters.AddWithValue("worker_id", workerId);
|
command.Parameters.AddWithValue("worker_id", workerId);
|
||||||
command.Parameters.AddWithValue("lease_until", leaseUntil);
|
command.Parameters.AddWithValue("lease_until", leaseUntil);
|
||||||
command.Parameters.AddWithValue("leased_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("leased_at", _timeProvider.GetUtcNow());
|
||||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
if (jobType != null)
|
if (jobType != null)
|
||||||
{
|
{
|
||||||
@@ -263,7 +266,7 @@ public sealed class PostgresJobRepository : IJobRepository
|
|||||||
command.Parameters.AddWithValue("job_id", jobId);
|
command.Parameters.AddWithValue("job_id", jobId);
|
||||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||||
command.Parameters.AddWithValue("new_lease_until", newLeaseUntil);
|
command.Parameters.AddWithValue("new_lease_until", newLeaseUntil);
|
||||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return rows > 0;
|
return rows > 0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
|||||||
{
|
{
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresPackRegistryRepository> _logger;
|
private readonly ILogger<PostgresPackRegistryRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
private const string PackColumns = """
|
private const string PackColumns = """
|
||||||
pack_id, tenant_id, project_id, name, display_name, description,
|
pack_id, tenant_id, project_id, name, display_name, description,
|
||||||
@@ -33,10 +34,12 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
|||||||
|
|
||||||
public PostgresPackRegistryRepository(
|
public PostgresPackRegistryRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresPackRegistryRepository> logger)
|
ILogger<PostgresPackRegistryRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pack CRUD
|
// Pack CRUD
|
||||||
@@ -264,7 +267,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
|||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("pack_id", packId);
|
command.Parameters.AddWithValue("pack_id", packId);
|
||||||
command.Parameters.AddWithValue("status", status.ToString().ToLowerInvariant());
|
command.Parameters.AddWithValue("status", status.ToString().ToLowerInvariant());
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow.UtcDateTime);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow().UtcDateTime);
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
command.Parameters.AddWithValue("published_at", (object?)publishedAt?.UtcDateTime ?? DBNull.Value);
|
command.Parameters.AddWithValue("published_at", (object?)publishedAt?.UtcDateTime ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("published_by", (object?)publishedBy ?? DBNull.Value);
|
command.Parameters.AddWithValue("published_by", (object?)publishedBy ?? DBNull.Value);
|
||||||
@@ -534,7 +537,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
|||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("pack_version_id", packVersionId);
|
command.Parameters.AddWithValue("pack_version_id", packVersionId);
|
||||||
command.Parameters.AddWithValue("status", status.ToString().ToLowerInvariant());
|
command.Parameters.AddWithValue("status", status.ToString().ToLowerInvariant());
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow.UtcDateTime);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow().UtcDateTime);
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
command.Parameters.AddWithValue("published_at", (object?)publishedAt?.UtcDateTime ?? DBNull.Value);
|
command.Parameters.AddWithValue("published_at", (object?)publishedAt?.UtcDateTime ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("published_by", (object?)publishedBy ?? DBNull.Value);
|
command.Parameters.AddWithValue("published_by", (object?)publishedBy ?? DBNull.Value);
|
||||||
@@ -574,7 +577,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
|||||||
command.Parameters.AddWithValue("signature_algorithm", signatureAlgorithm);
|
command.Parameters.AddWithValue("signature_algorithm", signatureAlgorithm);
|
||||||
command.Parameters.AddWithValue("signed_by", signedBy);
|
command.Parameters.AddWithValue("signed_by", signedBy);
|
||||||
command.Parameters.AddWithValue("signed_at", signedAt.UtcDateTime);
|
command.Parameters.AddWithValue("signed_at", signedAt.UtcDateTime);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow.UtcDateTime);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow().UtcDateTime);
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,11 +128,16 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresPackRunRepository> _logger;
|
private readonly ILogger<PostgresPackRunRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresPackRunRepository(OrchestratorDataSource dataSource, ILogger<PostgresPackRunRepository> logger)
|
public PostgresPackRunRepository(
|
||||||
|
OrchestratorDataSource dataSource,
|
||||||
|
ILogger<PostgresPackRunRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PackRun?> GetByIdAsync(string tenantId, Guid packRunId, CancellationToken cancellationToken)
|
public async Task<PackRun?> GetByIdAsync(string tenantId, Guid packRunId, CancellationToken cancellationToken)
|
||||||
@@ -244,7 +249,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
|||||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||||
await using var command = new NpgsqlCommand(sql, connection);
|
await using var command = new NpgsqlCommand(sql, connection);
|
||||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||||
command.Parameters.AddWithValue("task_runner_id", taskRunnerId);
|
command.Parameters.AddWithValue("task_runner_id", taskRunnerId);
|
||||||
@@ -275,7 +280,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
|||||||
command.Parameters.AddWithValue("pack_run_id", packRunId);
|
command.Parameters.AddWithValue("pack_run_id", packRunId);
|
||||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||||
command.Parameters.AddWithValue("new_lease_until", newLeaseUntil);
|
command.Parameters.AddWithValue("new_lease_until", newLeaseUntil);
|
||||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return rows > 0;
|
return rows > 0;
|
||||||
@@ -292,7 +297,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
|||||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||||
command.Parameters.AddWithValue("status", StatusToString(newStatus));
|
command.Parameters.AddWithValue("status", StatusToString(newStatus));
|
||||||
command.Parameters.AddWithValue("reason", (object?)reason ?? DBNull.Value);
|
command.Parameters.AddWithValue("reason", (object?)reason ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("completed_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("completed_at", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,13 +113,16 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresQuotaRepository> _logger;
|
private readonly ILogger<PostgresQuotaRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresQuotaRepository(
|
public PostgresQuotaRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresQuotaRepository> logger)
|
ILogger<PostgresQuotaRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Quota?> GetByIdAsync(string tenantId, Guid quotaId, CancellationToken cancellationToken)
|
public async Task<Quota?> GetByIdAsync(string tenantId, Guid quotaId, CancellationToken cancellationToken)
|
||||||
@@ -229,7 +232,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
|||||||
command.Parameters.AddWithValue("current_active", currentActive);
|
command.Parameters.AddWithValue("current_active", currentActive);
|
||||||
command.Parameters.AddWithValue("current_hour_count", currentHourCount);
|
command.Parameters.AddWithValue("current_hour_count", currentHourCount);
|
||||||
command.Parameters.AddWithValue("current_hour_start", currentHourStart);
|
command.Parameters.AddWithValue("current_hour_start", currentHourStart);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -245,7 +248,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
|||||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||||
command.Parameters.AddWithValue("pause_reason", reason);
|
command.Parameters.AddWithValue("pause_reason", reason);
|
||||||
command.Parameters.AddWithValue("quota_ticket", (object?)ticket ?? DBNull.Value);
|
command.Parameters.AddWithValue("quota_ticket", (object?)ticket ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
|
|
||||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -263,7 +266,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
|||||||
|
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
|
|
||||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -281,7 +284,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
|||||||
|
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -294,7 +297,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
|||||||
|
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,13 +69,16 @@ public sealed class PostgresRunRepository : IRunRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresRunRepository> _logger;
|
private readonly ILogger<PostgresRunRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresRunRepository(
|
public PostgresRunRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresRunRepository> logger)
|
ILogger<PostgresRunRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Run?> GetByIdAsync(string tenantId, Guid runId, CancellationToken cancellationToken)
|
public async Task<Run?> GetByIdAsync(string tenantId, Guid runId, CancellationToken cancellationToken)
|
||||||
@@ -149,7 +152,7 @@ public sealed class PostgresRunRepository : IRunRepository
|
|||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("run_id", runId);
|
command.Parameters.AddWithValue("run_id", runId);
|
||||||
command.Parameters.AddWithValue("succeeded", succeeded);
|
command.Parameters.AddWithValue("succeeded", succeeded);
|
||||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||||
|
|||||||
@@ -74,13 +74,16 @@ public sealed class PostgresSourceRepository : ISourceRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresSourceRepository> _logger;
|
private readonly ILogger<PostgresSourceRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresSourceRepository(
|
public PostgresSourceRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresSourceRepository> logger)
|
ILogger<PostgresSourceRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Source?> GetByIdAsync(string tenantId, Guid sourceId, CancellationToken cancellationToken)
|
public async Task<Source?> GetByIdAsync(string tenantId, Guid sourceId, CancellationToken cancellationToken)
|
||||||
@@ -175,7 +178,7 @@ public sealed class PostgresSourceRepository : ISourceRepository
|
|||||||
command.Parameters.AddWithValue("source_id", sourceId);
|
command.Parameters.AddWithValue("source_id", sourceId);
|
||||||
command.Parameters.AddWithValue("pause_reason", reason);
|
command.Parameters.AddWithValue("pause_reason", reason);
|
||||||
command.Parameters.AddWithValue("pause_ticket", (object?)ticket ?? DBNull.Value);
|
command.Parameters.AddWithValue("pause_ticket", (object?)ticket ?? DBNull.Value);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
|
|
||||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -193,7 +196,7 @@ public sealed class PostgresSourceRepository : ISourceRepository
|
|||||||
|
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("source_id", sourceId);
|
command.Parameters.AddWithValue("source_id", sourceId);
|
||||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||||
|
|
||||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -77,13 +77,16 @@ public sealed class PostgresThrottleRepository : IThrottleRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresThrottleRepository> _logger;
|
private readonly ILogger<PostgresThrottleRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresThrottleRepository(
|
public PostgresThrottleRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresThrottleRepository> logger)
|
ILogger<PostgresThrottleRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Throttle?> GetByIdAsync(string tenantId, Guid throttleId, CancellationToken cancellationToken)
|
public async Task<Throttle?> GetByIdAsync(string tenantId, Guid throttleId, CancellationToken cancellationToken)
|
||||||
@@ -110,7 +113,7 @@ public sealed class PostgresThrottleRepository : IThrottleRepository
|
|||||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("source_id", sourceId);
|
command.Parameters.AddWithValue("source_id", sourceId);
|
||||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var throttles = new List<Throttle>();
|
var throttles = new List<Throttle>();
|
||||||
@@ -128,7 +131,7 @@ public sealed class PostgresThrottleRepository : IThrottleRepository
|
|||||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
command.Parameters.AddWithValue("job_type", jobType);
|
command.Parameters.AddWithValue("job_type", jobType);
|
||||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var throttles = new List<Throttle>();
|
var throttles = new List<Throttle>();
|
||||||
|
|||||||
@@ -100,13 +100,16 @@ public sealed class PostgresWatermarkRepository : IWatermarkRepository
|
|||||||
|
|
||||||
private readonly OrchestratorDataSource _dataSource;
|
private readonly OrchestratorDataSource _dataSource;
|
||||||
private readonly ILogger<PostgresWatermarkRepository> _logger;
|
private readonly ILogger<PostgresWatermarkRepository> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public PostgresWatermarkRepository(
|
public PostgresWatermarkRepository(
|
||||||
OrchestratorDataSource dataSource,
|
OrchestratorDataSource dataSource,
|
||||||
ILogger<PostgresWatermarkRepository> logger)
|
ILogger<PostgresWatermarkRepository> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Watermark?> GetByScopeKeyAsync(string tenantId, string scopeKey, CancellationToken cancellationToken)
|
public async Task<Watermark?> GetByScopeKeyAsync(string tenantId, string scopeKey, CancellationToken cancellationToken)
|
||||||
@@ -271,7 +274,7 @@ public sealed class PostgresWatermarkRepository : IWatermarkRepository
|
|||||||
int limit,
|
int limit,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var thresholdTime = DateTimeOffset.UtcNow - lagThreshold;
|
var thresholdTime = _timeProvider.GetUtcNow() - lagThreshold;
|
||||||
|
|
||||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);
|
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);
|
||||||
await using var command = new NpgsqlCommand(SelectLaggingSql, connection);
|
await using var command = new NpgsqlCommand(SelectLaggingSql, connection);
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ public sealed record BackfillCheckpoint(
|
|||||||
int batchNumber,
|
int batchNumber,
|
||||||
DateTimeOffset batchStart,
|
DateTimeOffset batchStart,
|
||||||
DateTimeOffset batchEnd,
|
DateTimeOffset batchEnd,
|
||||||
int eventsInBatch)
|
int eventsInBatch,
|
||||||
|
DateTimeOffset startedAt)
|
||||||
{
|
{
|
||||||
return new BackfillCheckpoint(
|
return new BackfillCheckpoint(
|
||||||
CheckpointId: Guid.NewGuid(),
|
CheckpointId: Guid.NewGuid(),
|
||||||
@@ -166,7 +167,7 @@ public sealed record BackfillCheckpoint(
|
|||||||
EventsSkipped: 0,
|
EventsSkipped: 0,
|
||||||
EventsFailed: 0,
|
EventsFailed: 0,
|
||||||
BatchHash: null,
|
BatchHash: null,
|
||||||
StartedAt: DateTimeOffset.UtcNow,
|
StartedAt: startedAt,
|
||||||
CompletedAt: null,
|
CompletedAt: null,
|
||||||
ErrorMessage: null);
|
ErrorMessage: null);
|
||||||
}
|
}
|
||||||
@@ -174,7 +175,7 @@ public sealed record BackfillCheckpoint(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks the checkpoint as complete.
|
/// Marks the checkpoint as complete.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BackfillCheckpoint Complete(int processed, int skipped, int failed, string? batchHash)
|
public BackfillCheckpoint Complete(int processed, int skipped, int failed, string? batchHash, DateTimeOffset completedAt)
|
||||||
{
|
{
|
||||||
return this with
|
return this with
|
||||||
{
|
{
|
||||||
@@ -182,18 +183,18 @@ public sealed record BackfillCheckpoint(
|
|||||||
EventsSkipped = skipped,
|
EventsSkipped = skipped,
|
||||||
EventsFailed = failed,
|
EventsFailed = failed,
|
||||||
BatchHash = batchHash,
|
BatchHash = batchHash,
|
||||||
CompletedAt = DateTimeOffset.UtcNow
|
CompletedAt = completedAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks the checkpoint as failed.
|
/// Marks the checkpoint as failed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BackfillCheckpoint Fail(string error)
|
public BackfillCheckpoint Fail(string error, DateTimeOffset completedAt)
|
||||||
{
|
{
|
||||||
return this with
|
return this with
|
||||||
{
|
{
|
||||||
CompletedAt = DateTimeOffset.UtcNow,
|
CompletedAt = completedAt,
|
||||||
ErrorMessage = error
|
ErrorMessage = error
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,18 @@ public sealed class FirstSignalSnapshotWriter : BackgroundService
|
|||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly FirstSignalSnapshotWriterOptions _options;
|
private readonly FirstSignalSnapshotWriterOptions _options;
|
||||||
private readonly ILogger<FirstSignalSnapshotWriter> _logger;
|
private readonly ILogger<FirstSignalSnapshotWriter> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public FirstSignalSnapshotWriter(
|
public FirstSignalSnapshotWriter(
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
IOptions<FirstSignalOptions> options,
|
IOptions<FirstSignalOptions> options,
|
||||||
ILogger<FirstSignalSnapshotWriter> logger)
|
ILogger<FirstSignalSnapshotWriter> logger,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||||
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.SnapshotWriter;
|
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.SnapshotWriter;
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
@@ -76,7 +79,7 @@ public sealed class FirstSignalSnapshotWriter : BackgroundService
|
|||||||
var runRepository = scope.ServiceProvider.GetRequiredService<IRunRepository>();
|
var runRepository = scope.ServiceProvider.GetRequiredService<IRunRepository>();
|
||||||
var firstSignalService = scope.ServiceProvider.GetRequiredService<CoreServices.IFirstSignalService>();
|
var firstSignalService = scope.ServiceProvider.GetRequiredService<CoreServices.IFirstSignalService>();
|
||||||
|
|
||||||
var createdAfter = DateTimeOffset.UtcNow.Subtract(lookback);
|
var createdAfter = _timeProvider.GetUtcNow().Subtract(lookback);
|
||||||
|
|
||||||
var pending = await runRepository.ListAsync(
|
var pending = await runRepository.ListAsync(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -36,13 +36,14 @@ public static class HealthEndpoints
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IResult GetHealth()
|
private static IResult GetHealth([FromServices] TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
return Results.Ok(new HealthResponse("ok", DateTimeOffset.UtcNow));
|
return Results.Ok(new HealthResponse("ok", timeProvider.GetUtcNow()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetReadiness(
|
private static async Task<IResult> GetReadiness(
|
||||||
[FromServices] OrchestratorDataSource dataSource,
|
[FromServices] OrchestratorDataSource dataSource,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -53,14 +54,14 @@ public static class HealthEndpoints
|
|||||||
if (!dbHealthy)
|
if (!dbHealthy)
|
||||||
{
|
{
|
||||||
return Results.Json(
|
return Results.Json(
|
||||||
new ReadinessResponse("not_ready", DateTimeOffset.UtcNow, new Dictionary<string, string>
|
new ReadinessResponse("not_ready", timeProvider.GetUtcNow(), new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["database"] = "unhealthy"
|
["database"] = "unhealthy"
|
||||||
}),
|
}),
|
||||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Results.Ok(new ReadinessResponse("ready", DateTimeOffset.UtcNow, new Dictionary<string, string>
|
return Results.Ok(new ReadinessResponse("ready", timeProvider.GetUtcNow(), new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["database"] = "healthy"
|
["database"] = "healthy"
|
||||||
}));
|
}));
|
||||||
@@ -68,7 +69,7 @@ public static class HealthEndpoints
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Results.Json(
|
return Results.Json(
|
||||||
new ReadinessResponse("not_ready", DateTimeOffset.UtcNow, new Dictionary<string, string>
|
new ReadinessResponse("not_ready", timeProvider.GetUtcNow(), new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["database"] = $"error: {ex.Message}"
|
["database"] = $"error: {ex.Message}"
|
||||||
}),
|
}),
|
||||||
@@ -76,14 +77,15 @@ public static class HealthEndpoints
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IResult GetLiveness()
|
private static IResult GetLiveness([FromServices] TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
// Liveness just checks the process is alive
|
// Liveness just checks the process is alive
|
||||||
return Results.Ok(new HealthResponse("alive", DateTimeOffset.UtcNow));
|
return Results.Ok(new HealthResponse("alive", timeProvider.GetUtcNow()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetHealthDetails(
|
private static async Task<IResult> GetHealthDetails(
|
||||||
[FromServices] OrchestratorDataSource dataSource,
|
[FromServices] OrchestratorDataSource dataSource,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var checks = new Dictionary<string, HealthCheckResult>();
|
var checks = new Dictionary<string, HealthCheckResult>();
|
||||||
@@ -96,12 +98,12 @@ public static class HealthEndpoints
|
|||||||
checks["database"] = new HealthCheckResult(
|
checks["database"] = new HealthCheckResult(
|
||||||
dbHealthy ? "healthy" : "unhealthy",
|
dbHealthy ? "healthy" : "unhealthy",
|
||||||
dbHealthy ? null : "Connection test failed",
|
dbHealthy ? null : "Connection test failed",
|
||||||
DateTimeOffset.UtcNow);
|
timeProvider.GetUtcNow());
|
||||||
overallHealthy &= dbHealthy;
|
overallHealthy &= dbHealthy;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
checks["database"] = new HealthCheckResult("unhealthy", ex.Message, DateTimeOffset.UtcNow);
|
checks["database"] = new HealthCheckResult("unhealthy", ex.Message, timeProvider.GetUtcNow());
|
||||||
overallHealthy = false;
|
overallHealthy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ public static class HealthEndpoints
|
|||||||
checks["memory"] = new HealthCheckResult(
|
checks["memory"] = new HealthCheckResult(
|
||||||
memoryHealthy ? "healthy" : "degraded",
|
memoryHealthy ? "healthy" : "degraded",
|
||||||
$"Used: {memoryUsedMb:F2} MB",
|
$"Used: {memoryUsedMb:F2} MB",
|
||||||
DateTimeOffset.UtcNow);
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
// Thread pool check
|
// Thread pool check
|
||||||
ThreadPool.GetAvailableThreads(out var workerThreads, out var completionPortThreads);
|
ThreadPool.GetAvailableThreads(out var workerThreads, out var completionPortThreads);
|
||||||
@@ -124,11 +126,11 @@ public static class HealthEndpoints
|
|||||||
checks["threadPool"] = new HealthCheckResult(
|
checks["threadPool"] = new HealthCheckResult(
|
||||||
threadPoolHealthy ? "healthy" : "degraded",
|
threadPoolHealthy ? "healthy" : "degraded",
|
||||||
$"Worker threads available: {workerThreads}/{maxWorkerThreads}",
|
$"Worker threads available: {workerThreads}/{maxWorkerThreads}",
|
||||||
DateTimeOffset.UtcNow);
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
var response = new HealthDetailsResponse(
|
var response = new HealthDetailsResponse(
|
||||||
overallHealthy ? "healthy" : "unhealthy",
|
overallHealthy ? "healthy" : "unhealthy",
|
||||||
DateTimeOffset.UtcNow,
|
timeProvider.GetUtcNow(),
|
||||||
checks);
|
checks);
|
||||||
|
|
||||||
return overallHealthy
|
return overallHealthy
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ public static class KpiEndpoints
|
|||||||
[FromQuery] DateTimeOffset? to,
|
[FromQuery] DateTimeOffset? to,
|
||||||
[FromQuery] string? tenant,
|
[FromQuery] string? tenant,
|
||||||
[FromServices] IKpiCollector collector,
|
[FromServices] IKpiCollector collector,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var start = from ?? DateTimeOffset.UtcNow.AddDays(-7);
|
var now = timeProvider.GetUtcNow();
|
||||||
var end = to ?? DateTimeOffset.UtcNow;
|
var start = from ?? now.AddDays(-7);
|
||||||
|
var end = to ?? now;
|
||||||
|
|
||||||
var kpis = await collector.CollectAsync(start, end, tenant, ct);
|
var kpis = await collector.CollectAsync(start, end, tenant, ct);
|
||||||
return Results.Ok(kpis);
|
return Results.Ok(kpis);
|
||||||
@@ -69,11 +71,13 @@ public static class KpiEndpoints
|
|||||||
[FromQuery] DateTimeOffset? to,
|
[FromQuery] DateTimeOffset? to,
|
||||||
[FromQuery] string? tenant,
|
[FromQuery] string? tenant,
|
||||||
[FromServices] IKpiCollector collector,
|
[FromServices] IKpiCollector collector,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
var now = timeProvider.GetUtcNow();
|
||||||
var kpis = await collector.CollectAsync(
|
var kpis = await collector.CollectAsync(
|
||||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
from ?? now.AddDays(-7),
|
||||||
to ?? DateTimeOffset.UtcNow,
|
to ?? now,
|
||||||
tenant,
|
tenant,
|
||||||
ct);
|
ct);
|
||||||
return Results.Ok(kpis.Reachability);
|
return Results.Ok(kpis.Reachability);
|
||||||
@@ -84,11 +88,13 @@ public static class KpiEndpoints
|
|||||||
[FromQuery] DateTimeOffset? to,
|
[FromQuery] DateTimeOffset? to,
|
||||||
[FromQuery] string? tenant,
|
[FromQuery] string? tenant,
|
||||||
[FromServices] IKpiCollector collector,
|
[FromServices] IKpiCollector collector,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
var now = timeProvider.GetUtcNow();
|
||||||
var kpis = await collector.CollectAsync(
|
var kpis = await collector.CollectAsync(
|
||||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
from ?? now.AddDays(-7),
|
||||||
to ?? DateTimeOffset.UtcNow,
|
to ?? now,
|
||||||
tenant,
|
tenant,
|
||||||
ct);
|
ct);
|
||||||
return Results.Ok(kpis.Explainability);
|
return Results.Ok(kpis.Explainability);
|
||||||
@@ -99,11 +105,13 @@ public static class KpiEndpoints
|
|||||||
[FromQuery] DateTimeOffset? to,
|
[FromQuery] DateTimeOffset? to,
|
||||||
[FromQuery] string? tenant,
|
[FromQuery] string? tenant,
|
||||||
[FromServices] IKpiCollector collector,
|
[FromServices] IKpiCollector collector,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
var now = timeProvider.GetUtcNow();
|
||||||
var kpis = await collector.CollectAsync(
|
var kpis = await collector.CollectAsync(
|
||||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
from ?? now.AddDays(-7),
|
||||||
to ?? DateTimeOffset.UtcNow,
|
to ?? now,
|
||||||
tenant,
|
tenant,
|
||||||
ct);
|
ct);
|
||||||
return Results.Ok(kpis.Runtime);
|
return Results.Ok(kpis.Runtime);
|
||||||
@@ -114,11 +122,13 @@ public static class KpiEndpoints
|
|||||||
[FromQuery] DateTimeOffset? to,
|
[FromQuery] DateTimeOffset? to,
|
||||||
[FromQuery] string? tenant,
|
[FromQuery] string? tenant,
|
||||||
[FromServices] IKpiCollector collector,
|
[FromServices] IKpiCollector collector,
|
||||||
|
[FromServices] TimeProvider timeProvider,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
var now = timeProvider.GetUtcNow();
|
||||||
var kpis = await collector.CollectAsync(
|
var kpis = await collector.CollectAsync(
|
||||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
from ?? now.AddDays(-7),
|
||||||
to ?? DateTimeOffset.UtcNow,
|
to ?? now,
|
||||||
tenant,
|
tenant,
|
||||||
ct);
|
ct);
|
||||||
return Results.Ok(kpis.Replay);
|
return Results.Ok(kpis.Replay);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user