From dfab8a29c309e3f81885b901bc57248840a862c8 Mon Sep 17 00:00:00 2001 From: StellaOps Bot Date: Mon, 5 Jan 2026 09:35:33 +0200 Subject: [PATCH] docs re-org, audit fixes, build fixes --- CLAUDE.md | 11 +- docs/AGENTS.md | 2 +- ..._CLI_REFERENCE.md => API_CLI_REFERENCE.md} | 0 ...E_OVERVIEW.md => ARCHITECTURE_OVERVIEW.md} | 76 ++++++------- ...HITECTURE.md => ARCHITECTURE_REFERENCE.md} | 16 +-- docs/{11_AUTHORITY.md => AUTHORITY.md} | 0 ..._CODE_OF_CONDUCT.md => CODE_OF_CONDUCT.md} | 0 ...ODING_STANDARDS.md => CODING_STANDARDS.md} | 0 ...CKSTART.md => CONCELIER_CLI_QUICKSTART.md} | 8 +- docs/{11_DATA_SCHEMAS.md => DATA_SCHEMAS.md} | 0 docs/DEVELOPER_ONBOARDING.md | 10 +- docs/{23_FAQ_MATRIX.md => FAQ_MATRIX.md} | 18 ++-- ...04_FEATURE_MATRIX.md => FEATURE_MATRIX.md} | 0 docs/{14_GLOSSARY_OF_TERMS.md => GLOSSARY.md} | 0 docs/{11_GOVERNANCE.md => GOVERNANCE.md} | 6 +- .../{21_INSTALL_GUIDE.md => INSTALL_GUIDE.md} | 0 ...EGAL_COMPLIANCE.md => LEGAL_COMPLIANCE.md} | 0 ..._LEGAL_FAQ_QUOTA.md => LEGAL_FAQ_QUOTA.md} | 0 docs/{24_OFFLINE_KIT.md => OFFLINE_KIT.md} | 0 ...CE_WORKBOOK.md => PERFORMANCE_WORKBOOK.md} | 0 ...LUGIN_SDK_GUIDE.md => PLUGIN_SDK_GUIDE.md} | 0 ...OLICY_TEMPLATES.md => POLICY_TEMPLATES.md} | 0 ...ENT_FLOW1.md => QUOTA_ENFORCEMENT_FLOW.md} | 0 ...33_QUOTA_OVERVIEW.md => QUOTA_OVERVIEW.md} | 0 docs/README.md | 36 +++---- ...OOK.md => RELEASE_ENGINEERING_PLAYBOOK.md} | 0 docs/{05_ROADMAP.md => ROADMAP.md} | 0 ...G_GUIDE.md => SECURITY_HARDENING_GUIDE.md} | 0 ..._SECURITY_POLICY.md => SECURITY_POLICY.md} | 0 ...TS_SPEC.md => SYSTEM_REQUIREMENTS_SPEC.md} | 0 ...ITE_OVERVIEW.md => TEST_SUITE_OVERVIEW.md} | 7 +- docs/{15_UI_GUIDE.md => UI_GUIDE.md} | 0 ...SENSUS_GUIDE.md => VEX_CONSENSUS_GUIDE.md} | 0 docs/{03_VISION.md => VISION.md} | 0 ...IDE.md => VULNERABILITY_EXPLORER_GUIDE.md} | 0 .../canonicalization-determinism.md | 2 +- docs/full-features-list.md | 2 +- ...1_BE_determinism_timeprovider_injection.md | 10 ++ docs/install/docker.md | 4 +- docs/key-features.md | 8 +- docs/market/competitive-landscape.md | 4 +- docs/moat.md | 2 +- docs/modules/_template/AGENTS.md | 57 ++++++++++ docs/modules/_template/README.md | 62 +++++++++++ docs/modules/_template/architecture.md | 98 +++++++++++++++++ docs/modules/platform/README.md | 2 +- .../modules/platform/architecture-overview.md | 2 +- docs/modules/platform/architecture.md | 3 +- docs/modules/router/README.md | 12 ++- .../operations/entrypoint-lang-dotnet.md | 2 +- docs/router/README.md | 4 + docs/schemas/dotnet-il-metadata.schema.json | 4 +- docs/sdks/overview.md | 2 +- docs/technical/architecture/README.md | 8 +- docs/technical/architecture/component-map.md | 10 +- .../architecture/sbom-analyzer-inventory.md | 4 +- docs/technical/development/README.md | 12 +-- docs/technical/operations/README.md | 10 +- docs/technical/security/README.md | 16 +-- docs/technical/strategy/README.md | 24 ++--- docs/testing/README.md | 2 + docs/testing/ci-quality-gates.md | 4 +- docs/testing/testing-strategy-models.md | 4 +- .../Auth/HeaderScopeAuthenticationHandler.cs | 5 + .../AirGapIntegrationTests.cs | 18 ++-- .../BundleDeterminismTests.cs | 18 ++-- .../BundleExportImportTests.cs | 12 ++- .../BundleExportTests.cs | 39 ++++--- .../BundleManifestTests.cs | 3 +- .../StellaOps.Bench.ScannerAnalyzers.csproj | 18 ++++ .../Services/ForensicVerifier.cs | 8 +- .../Services/ImageAttestationVerifier.cs | 19 ++-- .../Internal/CertFrFeedClient.cs | 2 +- .../CachePerformanceBenchmarkTests.cs | 1 + .../Scheduling/ExportRetentionService.cs | 7 +- .../Scheduling/InMemorySchedulingStores.cs | 8 +- .../Services/LineageEvidencePackService.cs | 30 +++--- .../Verification/ExportVerificationModels.cs | 7 +- .../Verification/ExportVerificationService.cs | 15 ++- .../ExceptionReportGenerator.cs | 15 +-- .../Mappings/LedgerEventMapping.cs | 5 +- .../Program.cs | 3 +- .../Services/EvidenceGraphBuilder.cs | 7 +- .../Services/FindingWorkflowService.cs | 2 +- .../StellaOps.Notifier.Worker.csproj | 18 ++++ .../Repositories/InMemoryRepositories.cs | 100 +++++++++++++++--- .../Ledger/LedgerExporter.cs | 13 ++- .../Postgres/PostgresDuplicateSuppressor.cs | 9 +- .../Postgres/PostgresJobRepository.cs | 11 +- .../PostgresPackRegistryRepository.cs | 11 +- .../Postgres/PostgresPackRunRepository.cs | 13 ++- .../Postgres/PostgresQuotaRepository.cs | 15 +-- .../Postgres/PostgresRunRepository.cs | 7 +- .../Postgres/PostgresSourceRepository.cs | 9 +- .../Postgres/PostgresThrottleRepository.cs | 9 +- .../Postgres/PostgresWatermarkRepository.cs | 7 +- .../Repositories/IBackfillRepository.cs | 13 +-- .../Services/FirstSignalSnapshotWriter.cs | 7 +- .../Endpoints/HealthEndpoints.cs | 26 ++--- .../Endpoints/KpiEndpoints.cs | 30 ++++-- src/Policy/StellaOps.Policy.Engine/Program.cs | 4 +- .../Services/InMemoryPolicyPackRepository.cs | 18 ++-- .../InMemory/InMemoryExceptionRepository.cs | 10 +- .../Endpoints/ExceptionApprovalEndpoints.cs | 5 +- .../Endpoints/ExceptionEndpoints.cs | 26 +++-- .../Endpoints/GateEndpoints.cs | 8 +- .../Endpoints/GovernanceEndpoints.cs | 43 +++++--- .../Endpoints/RegistryWebhookEndpoints.cs | 9 +- .../Services/InMemoryGateEvaluationQueue.cs | 12 ++- .../Storage/InMemoryOverrideStore.cs | 10 +- .../Storage/InMemoryPolicyPackStore.cs | 14 ++- .../Storage/InMemorySnapshotStore.cs | 8 +- .../InMemoryVerificationPolicyStore.cs | 10 +- .../Storage/InMemoryViolationStore.cs | 10 +- .../UnknownsRepositoryTests.cs | 5 +- .../PolicyPreviewServiceTests.cs | 8 +- .../PolicySnapshotStoreTests.cs | 6 +- .../Replay/ReplayEngineTests.cs | 1 + .../EntrypointEndpointsTests.cs | 6 +- .../OrchestratorEndpointsTests.cs | 6 +- .../ProjectionEndpointTests.cs | 6 +- .../ResolverFeedExportTests.cs | 6 +- .../SbomAssetEventsTests.cs | 6 +- .../SbomEndpointsTests.cs | 6 +- .../SbomEventEndpointsTests.cs | 6 +- .../SbomInventoryEventsTests.cs | 6 +- .../SbomLedgerEndpointsTests.cs | 6 +- .../StellaOps.SbomService/Program.cs | 7 +- .../InMemoryOrchestratorRepository.cs | 6 +- .../StellaOps.SbomService/Services/Clock.cs | 21 +++- .../Services/IReplayVerificationService.cs | 8 +- .../Services/LineageCompareService.cs | 7 +- .../Services/LineageExportService.cs | 13 ++- .../Services/LineageHoverCache.cs | 8 +- .../Services/OrchestratorControlService.cs | 13 ++- .../Services/RegistrySourceService.cs | 23 ++-- .../Services/ReplayVerificationService.cs | 2 + .../Services/WatermarkService.cs | 10 +- .../StellaOps.SbomService.csproj | 1 + .../Repositories/SbomLineageEdgeRepository.cs | 7 +- .../Services/LineageGraphService.cs | 7 +- .../PostgresOrchestratorRepository.cs | 9 +- .../ISbomLineageEdgeRepository.cs | 4 +- .../ISbomVerdictLinkRepository.cs | 4 +- .../AdvisoryClient.cs | 2 +- .../Internal/PythonDistributionLoader.cs | 2 +- .../Risk/RiskScoreTests.cs | 2 +- .../SurfaceValidatorRunnerTests.cs | 8 +- .../StellaOps.VexHub.WebService/Program.cs | 8 +- .../VexExportCompatibilityTests.cs | 4 +- .../Api/NoiseGatingApiModels.cs | 4 +- .../NoiseGate/NoiseGateService.cs | 27 +++-- .../alert-destination-config.component.ts | 21 +++- .../exceptions/exception-manager.component.ts | 23 ++-- .../masked-value-display.component.ts | 7 +- .../secret-findings-list.component.ts | 29 +++-- .../revelation-policy-selector.component.ts | 9 +- .../secret-detection-settings.component.ts | 8 +- .../models/secret-detection.models.ts | 51 +++++++++ .../services/secret-exception.service.ts | 10 +- .../services/secret-findings.service.ts | 17 ++- .../RuntimeAdmissionPolicyServiceTests.cs | 2 +- .../AwsKmsClient.cs | 8 +- .../AwsKmsFacade.cs | 9 +- .../Fido2KmsClient.cs | 13 ++- .../Fido2Options.cs | 3 +- .../FileKmsClient.cs | 16 +-- .../GcpKmsClient.cs | 8 +- .../GcpKmsFacade.cs | 13 ++- .../Pkcs11Facade.cs | 6 +- .../Pkcs11KmsClient.cs | 8 +- .../PolicySimulationSmokeRunner.cs | 1 + .../MinimalProofExporterTests.cs | 2 + 173 files changed, 1276 insertions(+), 560 deletions(-) rename docs/{09_API_CLI_REFERENCE.md => API_CLI_REFERENCE.md} (100%) rename docs/{40_ARCHITECTURE_OVERVIEW.md => ARCHITECTURE_OVERVIEW.md} (96%) rename docs/{07_HIGH_LEVEL_ARCHITECTURE.md => ARCHITECTURE_REFERENCE.md} (92%) rename docs/{11_AUTHORITY.md => AUTHORITY.md} (100%) rename docs/{12_CODE_OF_CONDUCT.md => CODE_OF_CONDUCT.md} (100%) rename docs/{18_CODING_STANDARDS.md => CODING_STANDARDS.md} (100%) rename docs/{10_CONCELIER_CLI_QUICKSTART.md => CONCELIER_CLI_QUICKSTART.md} (92%) rename docs/{11_DATA_SCHEMAS.md => DATA_SCHEMAS.md} (100%) rename docs/{23_FAQ_MATRIX.md => FAQ_MATRIX.md} (78%) rename docs/{04_FEATURE_MATRIX.md => FEATURE_MATRIX.md} (100%) rename docs/{14_GLOSSARY_OF_TERMS.md => GLOSSARY.md} (100%) rename docs/{11_GOVERNANCE.md => GOVERNANCE.md} (91%) rename docs/{21_INSTALL_GUIDE.md => INSTALL_GUIDE.md} (100%) rename docs/{28_LEGAL_COMPLIANCE.md => LEGAL_COMPLIANCE.md} (100%) rename docs/{29_LEGAL_FAQ_QUOTA.md => LEGAL_FAQ_QUOTA.md} (100%) rename docs/{24_OFFLINE_KIT.md => OFFLINE_KIT.md} (100%) rename docs/{12_PERFORMANCE_WORKBOOK.md => PERFORMANCE_WORKBOOK.md} (100%) rename docs/{10_PLUGIN_SDK_GUIDE.md => PLUGIN_SDK_GUIDE.md} (100%) rename docs/{60_POLICY_TEMPLATES.md => POLICY_TEMPLATES.md} (100%) rename docs/{30_QUOTA_ENFORCEMENT_FLOW1.md => QUOTA_ENFORCEMENT_FLOW.md} (100%) rename docs/{33_333_QUOTA_OVERVIEW.md => QUOTA_OVERVIEW.md} (100%) rename docs/{13_RELEASE_ENGINEERING_PLAYBOOK.md => RELEASE_ENGINEERING_PLAYBOOK.md} (100%) rename docs/{05_ROADMAP.md => ROADMAP.md} (100%) rename docs/{17_SECURITY_HARDENING_GUIDE.md => SECURITY_HARDENING_GUIDE.md} (100%) rename docs/{13_SECURITY_POLICY.md => SECURITY_POLICY.md} (100%) rename docs/{05_SYSTEM_REQUIREMENTS_SPEC.md => SYSTEM_REQUIREMENTS_SPEC.md} (100%) rename docs/{19_TEST_SUITE_OVERVIEW.md => TEST_SUITE_OVERVIEW.md} (97%) rename docs/{15_UI_GUIDE.md => UI_GUIDE.md} (100%) rename docs/{16_VEX_CONSENSUS_GUIDE.md => VEX_CONSENSUS_GUIDE.md} (100%) rename docs/{03_VISION.md => VISION.md} (100%) rename docs/{20_VULNERABILITY_EXPLORER_GUIDE.md => VULNERABILITY_EXPLORER_GUIDE.md} (100%) create mode 100644 docs/modules/_template/AGENTS.md create mode 100644 docs/modules/_template/README.md create mode 100644 docs/modules/_template/architecture.md diff --git a/CLAUDE.md b/CLAUDE.md index d9f55d7ed..2d0a9c16c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -656,7 +656,7 @@ Always update task status in `docs/implplan/SPRINT_*.md`: Before coding, confirm required docs are read: - `docs/README.md` -- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/ARCHITECTURE_REFERENCE.md` - `docs/modules/platform/architecture-overview.md` - Relevant module dossier (e.g., `docs/modules//architecture.md`) - Module-specific `AGENTS.md` file @@ -674,13 +674,14 @@ Before coding, confirm required docs are read: ## 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//architecture.md` - **Database specification:** `docs/db/SPECIFICATION.md` - **PostgreSQL operations:** `docs/operations/postgresql-guide.md` -- **API/CLI reference:** `docs/09_API_CLI_REFERENCE.md` -- **Offline operation:** `docs/24_OFFLINE_KIT.md` -- **Quickstart:** `docs/10_CONCELIER_CLI_QUICKSTART.md` +- **API/CLI reference:** `docs/API_CLI_REFERENCE.md` +- **Offline operation:** `docs/OFFLINE_KIT.md` +- **Quickstart:** `docs/CONCELIER_CLI_QUICKSTART.md` - **Sprint planning:** `docs/implplan/SPRINT_*.md` ## CI/CD diff --git a/docs/AGENTS.md b/docs/AGENTS.md index dbda50f79..1c26633a8 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -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. ## 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`). - Active sprint file: `docs/implplan/SPRINT_0301_0001_0001_docs_md_i.md` (Docs Tasks Md.I). diff --git a/docs/09_API_CLI_REFERENCE.md b/docs/API_CLI_REFERENCE.md similarity index 100% rename from docs/09_API_CLI_REFERENCE.md rename to docs/API_CLI_REFERENCE.md diff --git a/docs/40_ARCHITECTURE_OVERVIEW.md b/docs/ARCHITECTURE_OVERVIEW.md similarity index 96% rename from docs/40_ARCHITECTURE_OVERVIEW.md rename to docs/ARCHITECTURE_OVERVIEW.md index 088391621..fcdfb8d52 100755 --- a/docs/40_ARCHITECTURE_OVERVIEW.md +++ b/docs/ARCHITECTURE_OVERVIEW.md @@ -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. - -For the full reference map (services, boundaries, detailed flows), see `docs/07_HIGH_LEVEL_ARCHITECTURE.md`. - -## Guiding Principles - -- **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). -- **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. -- **Offline-first:** the same workflow runs connected or air-gapped via Offline Kit snapshots and signed bundles. - + +For the full reference map (services, boundaries, detailed flows), see `docs/ARCHITECTURE_REFERENCE.md`. + +## Guiding Principles + +- **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). +- **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. +- **Offline-first:** the same workflow runs connected or air-gapped via Offline Kit snapshots and signed bundles. + ## 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) - **Scanning and SBOM:** Scanner WebService + Worker (facts generation) - **Advisories:** Concelier (ingest/normalize/export vulnerability sources) -- **VEX:** Excititor + VEX Lens (VEX observations/linksets and exploration) -- **Decisioning:** Policy Engine surfaces (lattice-style explainable policy) -- **Signing and transparency:** Signer + Attestor (DSSE/in-toto and optional transparency) +- **VEX:** Excititor + VEX Lens (VEX observations/linksets and exploration) +- **Decisioning:** Policy Engine surfaces (lattice-style explainable policy) +- **Signing and transparency:** Signer + Attestor (DSSE/in-toto and optional transparency) - **Orchestration and delivery:** Scheduler, Notify, Export Center - **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. | ## Infrastructure (What Is Required) - -**Required** - -- **PostgreSQL:** canonical persistent store for module schemas. -- **Valkey:** Redis-compatible cache/streams and DPoP nonce store. -- **RustFS (or equivalent S3-compatible store):** object storage for artifacts, bundles, and evidence. - -**Optional (deployment-dependent)** - -- **NATS JetStream:** optional messaging transport in some deployments. -- **Transparency log services:** Rekor mirror (and CA services) when transparency is enabled. - + +**Required** + +- **PostgreSQL:** canonical persistent store for module schemas. +- **Valkey:** Redis-compatible cache/streams and DPoP nonce store. +- **RustFS (or equivalent S3-compatible store):** object storage for artifacts, bundles, and evidence. + +**Optional (deployment-dependent)** + +- **NATS JetStream:** optional messaging transport in some deployments. +- **Transparency log services:** Rekor mirror (and CA services) when transparency is enabled. + ## End-to-End Flow (Typical) 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. 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. - + ## Extension Points (Where You Customize) - -- **Scanner analyzers** (restart-time plug-ins) for ecosystem-specific parsing and facts extraction. -- **Concelier connectors** for new advisory sources (preserving aggregation-only guardrails). -- **Policy packs** for organization-specific gating and waivers/justifications. + +- **Scanner analyzers** (restart-time plug-ins) for ecosystem-specific parsing and facts extraction. +- **Concelier connectors** for new advisory sources (preserving aggregation-only guardrails). +- **Policy packs** for organization-specific gating and waivers/justifications. - **Export profiles** for output formats and offline bundle shapes. ## 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. ## References - -- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` -- `docs/24_OFFLINE_KIT.md` -- `docs/09_API_CLI_REFERENCE.md` -- `docs/modules/platform/architecture-overview.md` + +- `docs/ARCHITECTURE_REFERENCE.md` +- `docs/OFFLINE_KIT.md` +- `docs/API_CLI_REFERENCE.md` +- `docs/modules/platform/architecture-overview.md` diff --git a/docs/07_HIGH_LEVEL_ARCHITECTURE.md b/docs/ARCHITECTURE_REFERENCE.md similarity index 92% rename from docs/07_HIGH_LEVEL_ARCHITECTURE.md rename to docs/ARCHITECTURE_REFERENCE.md index 75ad509fb..306c63d8d 100755 --- a/docs/07_HIGH_LEVEL_ARCHITECTURE.md +++ b/docs/ARCHITECTURE_REFERENCE.md @@ -3,7 +3,7 @@ This document is the canonical index for StellaOps architecture. 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 @@ -90,7 +90,7 @@ Tenancy and identity context are part of the platform contract: ## APIs and CLI reference 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 contracts and samples: `docs/api/` - CLI command guides: `docs/modules/cli/guides/commands/` @@ -98,15 +98,15 @@ Canonical entry points: ## Offline, verification, and operations Canonical entry points: -- Offline Kit: `docs/24_OFFLINE_KIT.md` -- Security hardening: `docs/17_SECURITY_HARDENING_GUIDE.md` -- Installation guide: `docs/21_INSTALL_GUIDE.md` +- Offline Kit: `docs/OFFLINE_KIT.md` +- Security hardening: `docs/SECURITY_HARDENING_GUIDE.md` +- Installation guide: `docs/INSTALL_GUIDE.md` - Ops and runbooks: `docs/operations/`, `docs/modules/*/operations/` ## Data and schemas 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/` - Events (schemas + samples): `docs/events/` @@ -114,5 +114,5 @@ Use these as the canonical map for schemas and contracts: - Product overview: `docs/overview.md` - Key features: `docs/key-features.md` -- Roadmap (internal): `docs/05_ROADMAP.md` -- Glossary: `docs/14_GLOSSARY_OF_TERMS.md` +- Roadmap (internal): `docs/ROADMAP.md` +- Glossary: `docs/GLOSSARY.md` diff --git a/docs/11_AUTHORITY.md b/docs/AUTHORITY.md similarity index 100% rename from docs/11_AUTHORITY.md rename to docs/AUTHORITY.md diff --git a/docs/12_CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from docs/12_CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/docs/18_CODING_STANDARDS.md b/docs/CODING_STANDARDS.md similarity index 100% rename from docs/18_CODING_STANDARDS.md rename to docs/CODING_STANDARDS.md diff --git a/docs/10_CONCELIER_CLI_QUICKSTART.md b/docs/CONCELIER_CLI_QUICKSTART.md similarity index 92% rename from docs/10_CONCELIER_CLI_QUICKSTART.md rename to docs/CONCELIER_CLI_QUICKSTART.md index 341c1d9cb..1ad183ba1 100644 --- a/docs/10_CONCELIER_CLI_QUICKSTART.md +++ b/docs/CONCELIER_CLI_QUICKSTART.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. ## 1) Prerequisites -- Deployment: follow `docs/21_INSTALL_GUIDE.md` (Compose profiles under `deploy/compose/`). -- Offline/air-gap: follow `docs/24_OFFLINE_KIT.md` and `docs/airgap/overview.md`. +- Deployment: follow `docs/INSTALL_GUIDE.md` (Compose profiles under `deploy/compose/`). +- Offline/air-gap: follow `docs/OFFLINE_KIT.md` and `docs/airgap/overview.md`. - Local dev (optional): .NET SDK version pinned by `global.json`. ## 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. Start here: -- `docs/21_INSTALL_GUIDE.md` +- `docs/INSTALL_GUIDE.md` - `docs/modules/concelier/operations/` ### 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 operations: `docs/modules/concelier/operations/` - 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` diff --git a/docs/11_DATA_SCHEMAS.md b/docs/DATA_SCHEMAS.md similarity index 100% rename from docs/11_DATA_SCHEMAS.md rename to docs/DATA_SCHEMAS.md diff --git a/docs/DEVELOPER_ONBOARDING.md b/docs/DEVELOPER_ONBOARDING.md index 483caf69a..296b75070 100644 --- a/docs/DEVELOPER_ONBOARDING.md +++ b/docs/DEVELOPER_ONBOARDING.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). ### Canonical references -- Architecture overview (10-minute tour): `docs/40_ARCHITECTURE_OVERVIEW.md` -- High-level reference map: `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- Architecture overview (10-minute tour): `docs/ARCHITECTURE_OVERVIEW.md` +- High-level reference map: `docs/ARCHITECTURE_REFERENCE.md` - Detailed architecture index: `docs/technical/architecture/README.md` - Topology: `docs/technical/architecture/platform-topology.md` - Infrastructure: `docs/technical/architecture/infrastructure-dependencies.md` @@ -170,7 +170,7 @@ Canonical guide: Related references: - Compose profiles: `deploy/compose/README.md` -- Install guide: `docs/21_INSTALL_GUIDE.md` +- Install guide: `docs/INSTALL_GUIDE.md` - Service-specific runbooks: `docs/modules//operations/` ## Service-by-Service Debugging Guide @@ -711,10 +711,10 @@ sudo docker compose -f docker-compose.dev.yaml up -d ### Key Documentation -- **Architecture:** `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- **Architecture:** `docs/ARCHITECTURE_REFERENCE.md` - **Build Commands:** `CLAUDE.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//architecture.md` ### Support diff --git a/docs/23_FAQ_MATRIX.md b/docs/FAQ_MATRIX.md similarity index 78% rename from docs/23_FAQ_MATRIX.md rename to docs/FAQ_MATRIX.md index 5e082e4fe..6909b3f52 100755 --- a/docs/23_FAQ_MATRIX.md +++ b/docs/FAQ_MATRIX.md @@ -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 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. | -| 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`. | | 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/`. | -| 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. | ## Further reading -- Vision: `docs/03_VISION.md` -- Feature matrix: `docs/04_FEATURE_MATRIX.md` -- Architecture overview: `docs/40_ARCHITECTURE_OVERVIEW.md` -- High-level architecture: `docs/07_HIGH_LEVEL_ARCHITECTURE.md` -- Offline kit: `docs/24_OFFLINE_KIT.md` -- Install guide: `docs/21_INSTALL_GUIDE.md` +- Vision: `docs/VISION.md` +- Feature matrix: `docs/FEATURE_MATRIX.md` +- Architecture overview: `docs/ARCHITECTURE_OVERVIEW.md` +- High-level architecture: `docs/ARCHITECTURE_REFERENCE.md` +- Offline kit: `docs/OFFLINE_KIT.md` +- Install guide: `docs/INSTALL_GUIDE.md` - Quickstart: `docs/quickstart.md` diff --git a/docs/04_FEATURE_MATRIX.md b/docs/FEATURE_MATRIX.md similarity index 100% rename from docs/04_FEATURE_MATRIX.md rename to docs/FEATURE_MATRIX.md diff --git a/docs/14_GLOSSARY_OF_TERMS.md b/docs/GLOSSARY.md similarity index 100% rename from docs/14_GLOSSARY_OF_TERMS.md rename to docs/GLOSSARY.md diff --git a/docs/11_GOVERNANCE.md b/docs/GOVERNANCE.md similarity index 91% rename from docs/11_GOVERNANCE.md rename to docs/GOVERNANCE.md index a2a16186a..28d428074 100755 --- a/docs/11_GOVERNANCE.md +++ b/docs/GOVERNANCE.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**. * 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. --- @@ -59,8 +59,8 @@ Approval is recorded via Git forge review or a signed commit trailer | Situation | Escalation | |-----------|------------| | Technical deadlock | **Maintainer Summit** (recorded & published) | -| Security bug | Follow [Security Policy](13_SECURITY_POLICY.md) | -| Code of Conduct violation | See `12_CODE_OF_CONDUCT.md` escalation ladder | +| Security bug | Follow [Security Policy](SECURITY_POLICY.md) | +| Code of Conduct violation | See `CODE_OF_CONDUCT.md` escalation ladder | --- diff --git a/docs/21_INSTALL_GUIDE.md b/docs/INSTALL_GUIDE.md similarity index 100% rename from docs/21_INSTALL_GUIDE.md rename to docs/INSTALL_GUIDE.md diff --git a/docs/28_LEGAL_COMPLIANCE.md b/docs/LEGAL_COMPLIANCE.md similarity index 100% rename from docs/28_LEGAL_COMPLIANCE.md rename to docs/LEGAL_COMPLIANCE.md diff --git a/docs/29_LEGAL_FAQ_QUOTA.md b/docs/LEGAL_FAQ_QUOTA.md similarity index 100% rename from docs/29_LEGAL_FAQ_QUOTA.md rename to docs/LEGAL_FAQ_QUOTA.md diff --git a/docs/24_OFFLINE_KIT.md b/docs/OFFLINE_KIT.md similarity index 100% rename from docs/24_OFFLINE_KIT.md rename to docs/OFFLINE_KIT.md diff --git a/docs/12_PERFORMANCE_WORKBOOK.md b/docs/PERFORMANCE_WORKBOOK.md similarity index 100% rename from docs/12_PERFORMANCE_WORKBOOK.md rename to docs/PERFORMANCE_WORKBOOK.md diff --git a/docs/10_PLUGIN_SDK_GUIDE.md b/docs/PLUGIN_SDK_GUIDE.md similarity index 100% rename from docs/10_PLUGIN_SDK_GUIDE.md rename to docs/PLUGIN_SDK_GUIDE.md diff --git a/docs/60_POLICY_TEMPLATES.md b/docs/POLICY_TEMPLATES.md similarity index 100% rename from docs/60_POLICY_TEMPLATES.md rename to docs/POLICY_TEMPLATES.md diff --git a/docs/30_QUOTA_ENFORCEMENT_FLOW1.md b/docs/QUOTA_ENFORCEMENT_FLOW.md similarity index 100% rename from docs/30_QUOTA_ENFORCEMENT_FLOW1.md rename to docs/QUOTA_ENFORCEMENT_FLOW.md diff --git a/docs/33_333_QUOTA_OVERVIEW.md b/docs/QUOTA_OVERVIEW.md similarity index 100% rename from docs/33_333_QUOTA_OVERVIEW.md rename to docs/QUOTA_OVERVIEW.md diff --git a/docs/README.md b/docs/README.md index c360d1afa..e1596f7dd 100755 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ StellaOps is a deterministic, offline-first container security platform: every v ## 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`. 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 | | --- | --- | -| Understand the product in 2 minutes | [overview.md](/docs/overview/) | -| Run a first scan (CLI) | [quickstart.md](/docs/quickstart/) | -| Browse capabilities | [key-features.md](/docs/key-features/) | -| Roadmap (priorities + definition of "done") | [05_ROADMAP.md](/docs/05_roadmap/) | -| Architecture: high-level overview | [40_ARCHITECTURE_OVERVIEW.md](/docs/40_architecture_overview/) | -| Architecture: full reference map | [07_HIGH_LEVEL_ARCHITECTURE.md](/docs/07_high_level_architecture/) | -| Architecture: user flows (UML) | [technical/architecture/user-flows.md](/docs/technical/architecture/user-flows/) | -| Architecture: module matrix (46 modules) | [technical/architecture/module-matrix.md](/docs/technical/architecture/module-matrix/) | -| Architecture: data flows | [technical/architecture/data-flows.md](/docs/technical/architecture/data-flows/) | -| Architecture: schema mapping | [technical/architecture/schema-mapping.md](/docs/technical/architecture/schema-mapping/) | -| Offline / air-gap operations | [24_OFFLINE_KIT.md](/docs/24_offline_kit/) | -| Security deployment hardening | [17_SECURITY_HARDENING_GUIDE.md](/docs/17_security_hardening_guide/) | -| Ingest advisories (Concelier + CLI) | [10_CONCELIER_CLI_QUICKSTART.md](/docs/10_concelier_cli_quickstart/) | -| Develop plugins/connectors | [10_PLUGIN_SDK_GUIDE.md](/docs/10_plugin_sdk_guide/) | -| Console (Web UI) operator guide | [15_UI_GUIDE.md](/docs/15_ui_guide/) | -| VEX consensus and issuer trust | [16_VEX_CONSENSUS_GUIDE.md](/docs/16_vex_consensus_guide/) | -| Vulnerability Explorer guide | [20_VULNERABILITY_EXPLORER_GUIDE.md](/docs/20_vulnerability_explorer_guide/) | +| Understand the product in 2 minutes | [overview.md](overview.md) | +| Run a first scan (CLI) | [quickstart.md](quickstart.md) | +| Browse capabilities | [key-features.md](key-features.md) | +| Roadmap (priorities + definition of "done") | [ROADMAP.md](ROADMAP.md) | +| Architecture: high-level overview | [ARCHITECTURE_OVERVIEW.md](ARCHITECTURE_OVERVIEW.md) | +| Architecture: full reference map | [ARCHITECTURE_REFERENCE.md](ARCHITECTURE_REFERENCE.md) | +| Architecture: user flows (UML) | [technical/architecture/user-flows.md](technical/architecture/user-flows.md) | +| Architecture: module matrix (46 modules) | [technical/architecture/module-matrix.md](technical/architecture/module-matrix.md) | +| Architecture: data flows | [technical/architecture/data-flows.md](technical/architecture/data-flows.md) | +| Architecture: schema mapping | [technical/architecture/schema-mapping.md](technical/architecture/schema-mapping.md) | +| Offline / air-gap operations | [OFFLINE_KIT.md](OFFLINE_KIT.md) | +| Security deployment hardening | [SECURITY_HARDENING_GUIDE.md](SECURITY_HARDENING_GUIDE.md) | +| Ingest advisories (Concelier + CLI) | [CONCELIER_CLI_QUICKSTART.md](CONCELIER_CLI_QUICKSTART.md) | +| Develop plugins/connectors | [PLUGIN_SDK_GUIDE.md](PLUGIN_SDK_GUIDE.md) | +| Console (Web UI) operator guide | [UI_GUIDE.md](UI_GUIDE.md) | +| VEX consensus and issuer trust | [VEX_CONSENSUS_GUIDE.md](VEX_CONSENSUS_GUIDE.md) | +| Vulnerability Explorer guide | [VULNERABILITY_EXPLORER_GUIDE.md](VULNERABILITY_EXPLORER_GUIDE.md) | ## Detailed Indexes diff --git a/docs/13_RELEASE_ENGINEERING_PLAYBOOK.md b/docs/RELEASE_ENGINEERING_PLAYBOOK.md similarity index 100% rename from docs/13_RELEASE_ENGINEERING_PLAYBOOK.md rename to docs/RELEASE_ENGINEERING_PLAYBOOK.md diff --git a/docs/05_ROADMAP.md b/docs/ROADMAP.md similarity index 100% rename from docs/05_ROADMAP.md rename to docs/ROADMAP.md diff --git a/docs/17_SECURITY_HARDENING_GUIDE.md b/docs/SECURITY_HARDENING_GUIDE.md similarity index 100% rename from docs/17_SECURITY_HARDENING_GUIDE.md rename to docs/SECURITY_HARDENING_GUIDE.md diff --git a/docs/13_SECURITY_POLICY.md b/docs/SECURITY_POLICY.md similarity index 100% rename from docs/13_SECURITY_POLICY.md rename to docs/SECURITY_POLICY.md diff --git a/docs/05_SYSTEM_REQUIREMENTS_SPEC.md b/docs/SYSTEM_REQUIREMENTS_SPEC.md similarity index 100% rename from docs/05_SYSTEM_REQUIREMENTS_SPEC.md rename to docs/SYSTEM_REQUIREMENTS_SPEC.md diff --git a/docs/19_TEST_SUITE_OVERVIEW.md b/docs/TEST_SUITE_OVERVIEW.md similarity index 97% rename from docs/19_TEST_SUITE_OVERVIEW.md rename to docs/TEST_SUITE_OVERVIEW.md index d01f3dbcd..ea7908bd8 100755 --- a/docs/19_TEST_SUITE_OVERVIEW.md +++ b/docs/TEST_SUITE_OVERVIEW.md @@ -242,7 +242,7 @@ flowchart LR 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`). -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`. 4. If the test requires network isolation, inherit from `NetworkIsolatedTestBase`. 5. If the test uses golden corpus, add cases to `bench/golden-corpus/`. @@ -251,11 +251,12 @@ flowchart LR ## Related Documentation -- [Sprint Epic 5100 - Testing Strategy](implplan/SPRINT_5100_0000_0000_epic_summary.md) - [Testing Strategy Models](testing/testing-strategy-models.md) - [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) -- [Offline Operation Guide](24_OFFLINE_KIT.md) +- [Offline Operation Guide](OFFLINE_KIT.md) - [Module Architecture Dossiers](modules/) --- diff --git a/docs/15_UI_GUIDE.md b/docs/UI_GUIDE.md similarity index 100% rename from docs/15_UI_GUIDE.md rename to docs/UI_GUIDE.md diff --git a/docs/16_VEX_CONSENSUS_GUIDE.md b/docs/VEX_CONSENSUS_GUIDE.md similarity index 100% rename from docs/16_VEX_CONSENSUS_GUIDE.md rename to docs/VEX_CONSENSUS_GUIDE.md diff --git a/docs/03_VISION.md b/docs/VISION.md similarity index 100% rename from docs/03_VISION.md rename to docs/VISION.md diff --git a/docs/20_VULNERABILITY_EXPLORER_GUIDE.md b/docs/VULNERABILITY_EXPLORER_GUIDE.md similarity index 100% rename from docs/20_VULNERABILITY_EXPLORER_GUIDE.md rename to docs/VULNERABILITY_EXPLORER_GUIDE.md diff --git a/docs/contributing/canonicalization-determinism.md b/docs/contributing/canonicalization-determinism.md index 8fc0ee1b5..4040804aa 100644 --- a/docs/contributing/canonicalization-determinism.md +++ b/docs/contributing/canonicalization-determinism.md @@ -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/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 --- diff --git a/docs/full-features-list.md b/docs/full-features-list.md index 8e5cfe886..5e35e4df3 100644 --- a/docs/full-features-list.md +++ b/docs/full-features-list.md @@ -3,7 +3,7 @@ > **Comprehensive table of every capability in the platform.** > > 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). --- diff --git a/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md b/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md index 82e641a04..bbe9469ad 100644 --- a/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md +++ b/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md @@ -142,6 +142,16 @@ services.AddSingleton(); | 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 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 - **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach. diff --git a/docs/install/docker.md b/docs/install/docker.md index 423ed1778..6570313b3 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -3,7 +3,7 @@ > **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. -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. - `/docs/deploy/console.md` – Detailed environment variables, CSP, health checks. - `/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. --- diff --git a/docs/key-features.md b/docs/key-features.md index d47fe9885..bbe464feb 100644 --- a/docs/key-features.md +++ b/docs/key-features.md @@ -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. -> **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. -**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 - **Competitive Landscape**: `docs/market/competitive-landscape.md` - **Moat Strategy**: `docs/market/moat-strategy-summary.md` - **Proof Architecture**: `docs/modules/platform/proof-driven-moats-architecture.md` -- **Vision**: `docs/03_VISION.md` -- **Architecture Overview**: `docs/40_ARCHITECTURE_OVERVIEW.md` +- **Vision**: `docs/VISION.md` +- **Architecture Overview**: `docs/ARCHITECTURE_OVERVIEW.md` - **Quickstart**: `docs/quickstart.md` diff --git a/docs/market/competitive-landscape.md b/docs/market/competitive-landscape.md index 07b6f7e2d..1406c21c5 100644 --- a/docs/market/competitive-landscape.md +++ b/docs/market/competitive-landscape.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. ## Cross-links -- Vision: `docs/03_VISION.md` (Moats section) -- Architecture: `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- Vision: `docs/VISION.md` (Moats section) +- Architecture: `docs/ARCHITECTURE_REFERENCE.md` - Reachability moat details: `docs/reachability/lead.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) diff --git a/docs/moat.md b/docs/moat.md index fd03a84e7..4aeeb2bf3 100644 --- a/docs/moat.md +++ b/docs/moat.md @@ -383,7 +383,7 @@ Produce **entrypoint‑aware differential SBOMs** and continually **re‑enrich* { "schema":"cyclonedx+stella-diff@1.0", "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"], "static_reachable": ["pkg:nuget/Kestrel@*", "pkg:nuget/System.Data@*"], "runtime_observed": ["pkg:rpm/openssl@3.0.9"], diff --git a/docs/modules/_template/AGENTS.md b/docs/modules/_template/AGENTS.md new file mode 100644 index 000000000..28c819180 --- /dev/null +++ b/docs/modules/_template/AGENTS.md @@ -0,0 +1,57 @@ +# - 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//__Libraries/StellaOps..Core/` | Core business logic | +| `src//StellaOps..WebService/Program.cs` | Service entry point | +| `src//__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 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 diff --git a/docs/modules/_template/README.md b/docs/modules/_template/README.md new file mode 100644 index 000000000..502880de4 --- /dev/null +++ b/docs/modules/_template/README.md @@ -0,0 +1,62 @@ +# + +> 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.AddServices(); +``` + +## Configuration + +```yaml +# Minimal configuration example +module_name: + setting_1: value + setting_2: value +``` + +## Related Documentation + +- [Implementation Guides](../..//) - If applicable +- [Related Module](../other-module/) - Cross-references + +## Notes + +Any important caveats, migration notes, or known issues. diff --git a/docs/modules/_template/architecture.md b/docs/modules/_template/architecture.md new file mode 100644 index 000000000..be197901a --- /dev/null +++ b/docs/modules/_template/architecture.md @@ -0,0 +1,98 @@ +# Architecture + +> Technical architecture specification for . + +## 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 + +``` +/ +├── __Libraries/ +│ ├── StellaOps..Core/ # Core business logic +│ ├── StellaOps..Storage/ # Data persistence (if applicable) +│ └── StellaOps..Client/ # Client SDK (if applicable) +├── StellaOps..WebService/ # HTTP API (if applicable) +├── StellaOps..Worker/ # Background processing (if applicable) +└── __Tests/ + └── StellaOps..*.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 +{ + Task DoSomethingAsync(Input input, CancellationToken ct); +} +``` + +Purpose and usage of this interface. + +### Interface 2 +```csharp +public interface I +{ + // ... +} +``` + +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/) diff --git a/docs/modules/platform/README.md b/docs/modules/platform/README.md index 37529f543..73941956b 100644 --- a/docs/modules/platform/README.md +++ b/docs/modules/platform/README.md @@ -17,7 +17,7 @@ Platform module describes cross-cutting architecture, contracts, and guardrails ## Key components - Architecture overview in `architecture-overview.md`. - Platform architecture summary in `architecture.md`. -- High-level reference: `../../07_HIGH_LEVEL_ARCHITECTURE.md`. +- High-level reference: `../../ARCHITECTURE_REFERENCE.md`. ## Integrations & dependencies - All StellaOps services via shared contracts (AOC, telemetry, security). diff --git a/docs/modules/platform/architecture-overview.md b/docs/modules/platform/architecture-overview.md index 2c7fe9dd5..e69dc8c09 100644 --- a/docs/modules/platform/architecture-overview.md +++ b/docs/modules/platform/architecture-overview.md @@ -2,7 +2,7 @@ > **Ownership:** Architecture Guild • Docs Guild > **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. diff --git a/docs/modules/platform/architecture.md b/docs/modules/platform/architecture.md index 71bf7efe3..b59627754 100644 --- a/docs/modules/platform/architecture.md +++ b/docs/modules/platform/architecture.md @@ -3,7 +3,8 @@ This module aggregates cross-cutting contracts and guardrails that every StellaOps service must follow. ## 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 service definition: `platform-service.md` - Aggregation-Only Contract: `../../aoc/aggregation-only-contract.md` (referenced across ingestion/observability docs) diff --git a/docs/modules/router/README.md b/docs/modules/router/README.md index 4b7e077f1..0a34a7b54 100644 --- a/docs/modules/router/README.md +++ b/docs/modules/router/README.md @@ -80,16 +80,26 @@ StellaOps.Router.slnx ## Key Documents +### Module Documentation (this directory) | Document | Purpose | |----------|---------| | [architecture.md](architecture.md) | Canonical specification and requirements | | [schema-validation.md](schema-validation.md) | JSON Schema validation feature | | [openapi-aggregation.md](openapi-aggregation.md) | OpenAPI document generation | | [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 | | [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 ### Gateway diff --git a/docs/modules/scanner/operations/entrypoint-lang-dotnet.md b/docs/modules/scanner/operations/entrypoint-lang-dotnet.md index 3d01ebccc..27ca99cfd 100644 --- a/docs/modules/scanner/operations/entrypoint-lang-dotnet.md +++ b/docs/modules/scanner/operations/entrypoint-lang-dotnet.md @@ -16,7 +16,7 @@ ## Evidence & scoring - 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. ## Edge cases diff --git a/docs/router/README.md b/docs/router/README.md index efe55eea7..6d4e16063 100644 --- a/docs/router/README.md +++ b/docs/router/README.md @@ -277,12 +277,16 @@ dotnet run --project src/Router/examples/Examples.OrderService ## Documentation +### Implementation Guides (this directory) - [Architecture Overview](./ARCHITECTURE.md) - [Getting Started Guide](./GETTING_STARTED.md) - [Transport Configuration](./transports/) - [Rate Limiting](./rate-limiting.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 - **Authority**: OAuth/OIDC integration for gateway authentication diff --git a/docs/schemas/dotnet-il-metadata.schema.json b/docs/schemas/dotnet-il-metadata.schema.json index 2962f855a..acbc63b42 100644 --- a/docs/schemas/dotnet-il-metadata.schema.json +++ b/docs/schemas/dotnet-il-metadata.schema.json @@ -21,7 +21,7 @@ "items": { "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": { "$ref": "#/definitions/AssemblyAnalysisConfig" @@ -1483,7 +1483,7 @@ { "config_id": "aspnet-core-analyzer", "version": "1.0.0", - "target_frameworks": ["net6.0", "net7.0", "net8.0"], + "target_frameworks": ["net8.0", "net9.0", "net10.0"], "assembly_analysis": { "enabled": true, "include_referenced_assemblies": true, diff --git a/docs/sdks/overview.md b/docs/sdks/overview.md index 24596cc4b..5d091c0ae 100644 --- a/docs/sdks/overview.md +++ b/docs/sdks/overview.md @@ -53,4 +53,4 @@ All SDKs and plugins must maintain determinism: - [Plugin Architecture](../plugins/ARCHITECTURE.md) - [Plugin Configuration](../plugins/CONFIGURATION.md) -- [Plugin SDK Guide](../10_PLUGIN_SDK_GUIDE.md) +- [Plugin SDK Guide](../PLUGIN_SDK_GUIDE.md) diff --git a/docs/technical/architecture/README.md b/docs/technical/architecture/README.md index 7c2265471..6986ea32c 100644 --- a/docs/technical/architecture/README.md +++ b/docs/technical/architecture/README.md @@ -3,11 +3,11 @@ Use this index to locate platform-level architecture references and per-module dossiers. ## Core views -- [Architecture overview (10-minute tour)](../../40_ARCHITECTURE_OVERVIEW.md) -- [High-level architecture (reference map)](../../07_HIGH_LEVEL_ARCHITECTURE.md) +- [Architecture overview (10-minute tour)](../../ARCHITECTURE_OVERVIEW.md) +- [High-level architecture (reference map)](../../ARCHITECTURE_REFERENCE.md) - [Scanner core contracts](../../scanner-core-contracts.md) -- [Authority (legacy overview)](../../11_AUTHORITY.md) -- [Console operator guide](../../15_UI_GUIDE.md) and deep dives under [console](../../console/) and [ux](../../ux/) +- [Authority (legacy overview)](../../AUTHORITY.md) +- [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/`) ## Detailed references diff --git a/docs/technical/architecture/component-map.md b/docs/technical/architecture/component-map.md index a0a630433..2681cbede 100644 --- a/docs/technical/architecture/component-map.md +++ b/docs/technical/architecture/component-map.md @@ -5,7 +5,7 @@ Concise descriptions of every top-level component under `src/`, summarising the ## 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`. - **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`). - **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`). @@ -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`. ## 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`). - **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`). @@ -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`). - **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. -- **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 -- **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/`). - **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). @@ -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. ## 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. 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. diff --git a/docs/technical/architecture/sbom-analyzer-inventory.md b/docs/technical/architecture/sbom-analyzer-inventory.md index c483a1d2d..331c6cf25 100644 --- a/docs/technical/architecture/sbom-analyzer-inventory.md +++ b/docs/technical/architecture/sbom-analyzer-inventory.md @@ -88,11 +88,11 @@ This document provides a complete inventory of all analyzers used in StellaOps S │ 2. Parse legacy packages.config XML │ │ 3. Parse *.deps.json for runtime dependencies │ │ 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: │ │ 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: │ │ • Framework-specific dependencies │ diff --git a/docs/technical/development/README.md b/docs/technical/development/README.md index 959b35478..42a5742b6 100644 --- a/docs/technical/development/README.md +++ b/docs/technical/development/README.md @@ -3,15 +3,15 @@ Resources for contributors building features, plug-ins, connectors, and tests. ## Engineering Standards & Quality -- [../18_CODING_STANDARDS.md](../../18_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. -- [../12_PERFORMANCE_WORKBOOK.md](../../12_PERFORMANCE_WORKBOOK.md) – benchmark targets and reference rigs. +- [../CODING_STANDARDS.md](../../CODING_STANDARDS.md) – language guidelines, project layout, review expectations. +- [../TEST_SUITE_OVERVIEW.md](../../TEST_SUITE_OVERVIEW.md) – unit, integration, golden, and determinism test strategy. +- [../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. - [../scanner-core-contracts.md](../../scanner-core-contracts.md) – DTO fixtures consumed by tests. ## Plug-ins, Connectors & Extensions -- [../10_PLUGIN_SDK_GUIDE.md](../../10_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. +- [../PLUGIN_SDK_GUIDE.md](../../PLUGIN_SDK_GUIDE.md) – plug-in lifecycle, manifests, packaging. +- [../CONCELIER_CLI_QUICKSTART.md](../../CONCELIER_CLI_QUICKSTART.md) – local Concelier + CLI workflow for advisory ingestion. - Developer guides under [../dev/](../../dev/): - 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`). @@ -20,7 +20,7 @@ Resources for contributors building features, plug-ins, connectors, and tests. - Operational templates and fixtures (`templates/`, `fixtures.md`). ## 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. - [../policy/gateway.md](../../policy/gateway.md) & [../policy/dsl.md](../../policy/dsl.md) – foundations for automating policy programs. diff --git a/docs/technical/operations/README.md b/docs/technical/operations/README.md index dd1ddf165..76f666d63 100644 --- a/docs/technical/operations/README.md +++ b/docs/technical/operations/README.md @@ -3,22 +3,22 @@ Deployment, runtime operations, and air-gap playbooks for running Stella Ops in production. ## 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. - [../deploy/containers.md](../../deploy/containers.md) – container deployment guidance for AOC environments. - [../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. ## Offline & Sovereign Operations - [../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. - [../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 -- [../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. - [../security/console-security.md](../../security/console-security.md) – console-specific controls. - [../security/authority-scopes.md](../../security/authority-scopes.md) – Authority scope model. diff --git a/docs/technical/security/README.md b/docs/technical/security/README.md index e8716373f..81d9da942 100644 --- a/docs/technical/security/README.md +++ b/docs/technical/security/README.md @@ -3,13 +3,13 @@ Authoritative sources for threat models, governance, compliance, and security operations. ## Policies & Governance -- [../13_SECURITY_POLICY.md](../../13_SECURITY_POLICY.md) – responsible disclosure, support windows. -- [../11_GOVERNANCE.md](../../11_GOVERNANCE.md) – project governance charter. -- [../12_CODE_OF_CONDUCT.md](../../12_CODE_OF_CONDUCT.md) – community expectations. -- [../17_SECURITY_HARDENING_GUIDE.md](../../17_SECURITY_HARDENING_GUIDE.md) – deployment hardening steps. +- [../SECURITY_POLICY.md](../../SECURITY_POLICY.md) – responsible disclosure, support windows. +- [../GOVERNANCE.md](../../GOVERNANCE.md) – project governance charter. +- [../CODE_OF_CONDUCT.md](../../CODE_OF_CONDUCT.md) – community expectations. +- [../SECURITY_HARDENING_GUIDE.md](../../SECURITY_HARDENING_GUIDE.md) – deployment hardening steps. - [../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. -- [../33_333_QUOTA_OVERVIEW.md](../../33_333_QUOTA_OVERVIEW.md) – quota policy reference. +- [../LEGAL_FAQ_QUOTA.md](../../LEGAL_FAQ_QUOTA.md) – legal interpretation of quota. +- [../QUOTA_OVERVIEW.md](../../QUOTA_OVERVIEW.md) – quota policy reference. - [../risk/risk-profiles.md](../../risk/risk-profiles.md) – organisational risk personas. ## 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/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. -- [../30_QUOTA_ENFORCEMENT_FLOW1.md](../../30_QUOTA_ENFORCEMENT_FLOW1.md) – quota enforcement sequence. -- [../24_OFFLINE_KIT.md](../../24_OFFLINE_KIT.md) – tamper-evident offline artefacts. +- [../QUOTA_ENFORCEMENT_FLOW.md](../../QUOTA_ENFORCEMENT_FLOW.md) – quota enforcement sequence. +- [../OFFLINE_KIT.md](../../OFFLINE_KIT.md) – tamper-evident offline artefacts. - [../security/](../../security/) – browse for additional deep dives (audit, scopes, rate limits). ## Supporting Material diff --git a/docs/technical/strategy/README.md b/docs/technical/strategy/README.md index d2f645d76..32c276b9b 100644 --- a/docs/technical/strategy/README.md +++ b/docs/technical/strategy/README.md @@ -2,19 +2,19 @@ Foundational, high-level documents that define StellaOps direction, scope, and differentiators. -- [Vision](../../03_VISION.md) — north-star goals, KPIs, and themes. -- [Feature matrix](../../04_FEATURE_MATRIX.md) — capability matrix by tier. -- [System requirements spec](../../05_SYSTEM_REQUIREMENTS_SPEC.md) — functional and non-functional requirements baseline. -- [Roadmap](../../05_ROADMAP.md) — date-free capability roadmap and definition of “done”. -- [Architecture overview](../../40_ARCHITECTURE_OVERVIEW.md) — platform principles and module map. +- [Vision](../../VISION.md) — north-star goals, KPIs, and themes. +- [Feature matrix](../../FEATURE_MATRIX.md) — capability matrix by tier. +- [System requirements spec](../../SYSTEM_REQUIREMENTS_SPEC.md) — functional and non-functional requirements baseline. +- [Roadmap](../../ROADMAP.md) — date-free capability roadmap and definition of "done". +- [Architecture overview](../../ARCHITECTURE_OVERVIEW.md) — platform principles and module map. - [Moat](../../moat.md) — differentiating workstreams (determinism, policy lattice, sovereign crypto readiness, attestation graph). -- [Offline Kit](../../24_OFFLINE_KIT.md) — offline story and workflows. -- [Security policy](../../13_SECURITY_POLICY.md) — disclosure and support expectations. -- [Glossary](../../14_GLOSSARY_OF_TERMS.md) — canonical vocabulary. -- [UI guide](../../15_UI_GUIDE.md) — console UX overview for evaluators. -- [FAQ matrix](../../23_FAQ_MATRIX.md) — stakeholder FAQ. +- [Offline Kit](../../OFFLINE_KIT.md) — offline story and workflows. +- [Security policy](../../SECURITY_POLICY.md) — disclosure and support expectations. +- [Glossary](../../GLOSSARY.md) — canonical vocabulary. +- [UI guide](../../UI_GUIDE.md) — console UX overview for evaluators. +- [FAQ matrix](../../FAQ_MATRIX.md) — stakeholder FAQ. ## Related concepts -- [Quota framing](../../33_333_QUOTA_OVERVIEW.md) and [enforcement flow](../../30_QUOTA_ENFORCEMENT_FLOW1.md) align business policy with enforcement diagrams. -- [Legal FAQ (quota)](../../29_LEGAL_FAQ_QUOTA.md) captures the AGPL-3.0 interpretation of quota enforcement. +- [Quota framing](../../QUOTA_OVERVIEW.md) and [enforcement flow](../../QUOTA_ENFORCEMENT_FLOW.md) align business policy with enforcement diagrams. +- [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. diff --git a/docs/testing/README.md b/docs/testing/README.md index 562418a71..218909a67 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -161,7 +161,9 @@ npm run storybook:build ## Related Documentation +- [Test Suite Overview](../TEST_SUITE_OVERVIEW.md) - High-level entry point for testing - [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) - [Path Filters](../cicd/path-filters.md) - [Test Infrastructure](../../src/__Tests/AGENTS.md) diff --git a/docs/testing/ci-quality-gates.md b/docs/testing/ci-quality-gates.md index a31ed81a9..bb9612466 100644 --- a/docs/testing/ci-quality-gates.md +++ b/docs/testing/ci-quality-gates.md @@ -149,9 +149,9 @@ If baselines become stale: ## Related Documentation -- [Test Suite Overview](../19_TEST_SUITE_OVERVIEW.md) +- [Test Suite Overview](../TEST_SUITE_OVERVIEW.md) - [Testing Strategy Models](./testing-strategy-models.md) - [Test Catalog](./TEST_CATALOG.yml) - [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) diff --git a/docs/testing/testing-strategy-models.md b/docs/testing/testing-strategy-models.md index 66d4a5829..b5d895829 100644 --- a/docs/testing/testing-strategy-models.md +++ b/docs/testing/testing-strategy-models.md @@ -41,12 +41,12 @@ Supersedes/extends: `docs/product-advisories/archived/2025-12-21-testing-strateg ## Documentation moments (when to update) - 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//`. - New fixtures or runnable harnesses: place under `docs/benchmarks/**` or `tests/**` and link here. ## Related artifacts - 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` - Code samples from the advisory: `docs/benchmarks/testing/better-testing-strategy-samples.md` diff --git a/src/AirGap/StellaOps.AirGap.Controller/Auth/HeaderScopeAuthenticationHandler.cs b/src/AirGap/StellaOps.AirGap.Controller/Auth/HeaderScopeAuthenticationHandler.cs index 77b0d1dff..24dfae0d3 100644 --- a/src/AirGap/StellaOps.AirGap.Controller/Auth/HeaderScopeAuthenticationHandler.cs +++ b/src/AirGap/StellaOps.AirGap.Controller/Auth/HeaderScopeAuthenticationHandler.cs @@ -72,6 +72,11 @@ public sealed class HeaderScopeAuthenticationHandler : AuthenticationHandler(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var bundleOutputPath = Path.Combine(_onlineEnvPath, "bundle"); @@ -120,7 +121,8 @@ public sealed class AirGapIntegrationTests : IDisposable DateTimeOffset.UtcNow.AddDays(30), 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 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()); var bundlePath = Path.Combine(_onlineEnvPath, "multi-bundle"); @@ -161,7 +163,8 @@ public sealed class AirGapIntegrationTests : IDisposable null, new[] { new FeedBuildConfig("feed", "nvd", "v1", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var bundlePath = Path.Combine(_onlineEnvPath, "corrupt-source"); var manifest = await builder.BuildAsync(request, bundlePath); @@ -219,7 +222,8 @@ public sealed class AirGapIntegrationTests : IDisposable null, Array.Empty(), new[] { new PolicyBuildConfig("security-policy", "security", "1.0", policyPath, "policies/security.rego", PolicyType.OpaRego) }, - Array.Empty()); + Array.Empty(), + Array.Empty()); 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-3", "policy3", "1.0", policy3Path, "policies/policy3.rego", PolicyType.OpaRego) }, - Array.Empty()); + Array.Empty(), + Array.Empty()); var bundlePath = Path.Combine(_onlineEnvPath, "multi-policy"); @@ -315,7 +320,8 @@ public sealed class AirGapIntegrationTests : IDisposable null, Array.Empty(), 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()); var bundlePath = Path.Combine(_onlineEnvPath, "signed-bundle"); diff --git a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleDeterminismTests.cs b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleDeterminismTests.cs index 99b9e2a5d..10b6c7fb0 100644 --- a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleDeterminismTests.cs +++ b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleDeterminismTests.cs @@ -142,7 +142,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act - First export 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var request2 = new BundleBuildRequest( "binary-test", @@ -343,7 +347,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime new FeedBuildConfig("f1", "binary", "v1", source2, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); } private BundleManifest CreateDeterministicManifest(string name) diff --git a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportImportTests.cs b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportImportTests.cs index be0a57e6a..5b0d2a130 100644 --- a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportImportTests.cs +++ b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportImportTests.cs @@ -259,7 +259,8 @@ public sealed class BundleExportImportTests : IDisposable null, new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile1, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var request2 = new BundleBuildRequest( "determinism-test", @@ -267,7 +268,8 @@ public sealed class BundleExportImportTests : IDisposable null, new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile2, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var outputPath1 = Path.Combine(_tempRoot, "determinism-output1"); var outputPath2 = Path.Combine(_tempRoot, "determinism-output2"); @@ -363,7 +365,8 @@ public sealed class BundleExportImportTests : IDisposable imported.Feeds[0].SnapshotAt, imported.Feeds[0].Format) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var bundlePath2 = Path.Combine(_tempRoot, "roundtrip2"); var manifest2 = await builder.BuildAsync(reexportRequest, bundlePath2); @@ -409,7 +412,8 @@ public sealed class BundleExportImportTests : IDisposable null, new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedSourcePath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); } private static BundleManifest CreateTestManifest() diff --git a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportTests.cs b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportTests.cs index d1813b1a9..6cec88f9c 100644 --- a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportTests.cs +++ b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleExportTests.cs @@ -49,7 +49,8 @@ public sealed class BundleExportTests : IAsyncLifetime null, Array.Empty(), Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -93,7 +94,8 @@ public sealed class BundleExportTests : IAsyncLifetime FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -139,7 +141,8 @@ public sealed class BundleExportTests : IAsyncLifetime "policies/default.rego", PolicyType.OpaRego) }, - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -182,7 +185,8 @@ public sealed class BundleExportTests : IAsyncLifetime "certs/root.pem", CryptoComponentType.TrustRoot, DateTimeOffset.UtcNow.AddYears(10)) - }); + }, + Array.Empty()); // Act 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) }, - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -328,7 +335,8 @@ public sealed class BundleExportTests : IAsyncLifetime new[] { new CryptoBuildConfig("c1", "root", certFile, "crypto/certs/ca/root.pem", CryptoComponentType.TrustRoot, null) - }); + }, + Array.Empty()); // Act 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) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act 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) }, - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -440,7 +450,8 @@ public sealed class BundleExportTests : IAsyncLifetime new[] { new CryptoBuildConfig("c1", "test", certFile, "certs/test", type, null) - }); + }, + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -468,7 +479,8 @@ public sealed class BundleExportTests : IAsyncLifetime expiresAt, Array.Empty(), Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); @@ -496,7 +508,8 @@ public sealed class BundleExportTests : IAsyncLifetime new[] { new CryptoBuildConfig("c1", "root", certFile, "certs/root.pem", CryptoComponentType.TrustRoot, componentExpiry) - }); + }, + Array.Empty()); // Act var manifest = await builder.BuildAsync(request, outputPath); diff --git a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleManifestTests.cs b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleManifestTests.cs index 67a0dbeda..ff0ae4114 100644 --- a/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleManifestTests.cs +++ b/src/AirGap/__Libraries/__Tests/StellaOps.AirGap.Bundle.Tests/BundleManifestTests.cs @@ -49,7 +49,8 @@ public class BundleManifestTests null, new[] { new FeedBuildConfig("feed-1", "nvd", "v1", sourceFile, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) }, Array.Empty(), - Array.Empty()); + Array.Empty(), + Array.Empty()); var outputPath = Path.Combine(tempRoot, "bundle"); var manifest = await builder.BuildAsync(request, outputPath); diff --git a/src/Bench/StellaOps.Bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj b/src/Bench/StellaOps.Bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj index e69de29bb..87d040caf 100644 --- a/src/Bench/StellaOps.Bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj +++ b/src/Bench/StellaOps.Bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj @@ -0,0 +1,18 @@ + + + Exe + net10.0 + preview + enable + enable + true + + + + + + + + + + diff --git a/src/Cli/StellaOps.Cli/Services/ForensicVerifier.cs b/src/Cli/StellaOps.Cli/Services/ForensicVerifier.cs index 5999a9cc3..5160a9556 100644 --- a/src/Cli/StellaOps.Cli/Services/ForensicVerifier.cs +++ b/src/Cli/StellaOps.Cli/Services/ForensicVerifier.cs @@ -26,10 +26,12 @@ internal sealed class ForensicVerifier : IForensicVerifier }; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public ForensicVerifier(ILogger logger) + public ForensicVerifier(ILogger logger, TimeProvider? timeProvider = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task VerifyBundleAsync( @@ -42,7 +44,7 @@ internal sealed class ForensicVerifier : IForensicVerifier var errors = new List(); var warnings = new List(); - var verifiedAt = DateTimeOffset.UtcNow; + var verifiedAt = _timeProvider.GetUtcNow(); _logger.LogDebug("Verifying forensic bundle at {BundlePath}", bundlePath); @@ -440,7 +442,7 @@ internal sealed class ForensicVerifier : IForensicVerifier matchingRoot.PublicKey); // Check time validity - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var timeValid = (!matchingRoot.NotBefore.HasValue || now >= matchingRoot.NotBefore.Value) && (!matchingRoot.NotAfter.HasValue || now <= matchingRoot.NotAfter.Value); diff --git a/src/Cli/StellaOps.Cli/Services/ImageAttestationVerifier.cs b/src/Cli/StellaOps.Cli/Services/ImageAttestationVerifier.cs index 115d22224..b64da12a5 100644 --- a/src/Cli/StellaOps.Cli/Services/ImageAttestationVerifier.cs +++ b/src/Cli/StellaOps.Cli/Services/ImageAttestationVerifier.cs @@ -17,17 +17,20 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier private readonly ITrustPolicyLoader _trustPolicyLoader; private readonly IDsseSignatureVerifier _dsseVerifier; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public ImageAttestationVerifier( IOciRegistryClient registryClient, ITrustPolicyLoader trustPolicyLoader, IDsseSignatureVerifier dsseVerifier, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _registryClient = registryClient ?? throw new ArgumentNullException(nameof(registryClient)); _trustPolicyLoader = trustPolicyLoader ?? throw new ArgumentNullException(nameof(trustPolicyLoader)); _dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task VerifyAsync( @@ -51,7 +54,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier ImageDigest = digest, Registry = reference.Registry, Repository = reference.Repository, - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = _timeProvider.GetUtcNow() }; OciReferrersResponse referrers; @@ -191,7 +194,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier Digest = candidate.Digest, SignerIdentity = verification.KeyId, Message = verification.Error ?? "Signature verification failed", - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = _timeProvider.GetUtcNow() }; } @@ -206,7 +209,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier Digest = candidate.Digest, SignerIdentity = signerKeyId, 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, SignerIdentity = signerKeyId, Message = "Rekor receipt missing", - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = _timeProvider.GetUtcNow() }; } if (policy.MaxAge.HasValue) { 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 { @@ -237,7 +240,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier Digest = candidate.Digest, SignerIdentity = signerKeyId, Message = "Attestation exceeded max age", - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = _timeProvider.GetUtcNow() }; } } @@ -250,7 +253,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier Digest = candidate.Digest, SignerIdentity = signerKeyId, Message = "Signature valid", - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = _timeProvider.GetUtcNow() }; } catch (Exception ex) diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrFeedClient.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrFeedClient.cs index 99280c617..db2fe75ad 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrFeedClient.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrFeedClient.cs @@ -76,7 +76,7 @@ public sealed class CertFrFeedClient } 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); diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Cache.Valkey.Tests/Performance/CachePerformanceBenchmarkTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Cache.Valkey.Tests/Performance/CachePerformanceBenchmarkTests.cs index 0d58cfd37..b4d923b58 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Cache.Valkey.Tests/Performance/CachePerformanceBenchmarkTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Cache.Valkey.Tests/Performance/CachePerformanceBenchmarkTests.cs @@ -73,6 +73,7 @@ public sealed class CachePerformanceBenchmarkTests : IAsyncLifetime _cacheService = new ValkeyAdvisoryCacheService( _connectionFactory, options, + metrics: null, NullLogger.Instance); await ValueTask.CompletedTask; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/ExportRetentionService.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/ExportRetentionService.cs index 019198b4e..eb6c81c80 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/ExportRetentionService.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/ExportRetentionService.cs @@ -9,13 +9,16 @@ public sealed class ExportRetentionService : IExportRetentionService { private readonly IExportRetentionStore _retentionStore; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public ExportRetentionService( IExportRetentionStore retentionStore, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _retentionStore = retentionStore ?? throw new ArgumentNullException(nameof(retentionStore)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -26,7 +29,7 @@ public sealed class ExportRetentionService : IExportRetentionService ArgumentNullException.ThrowIfNull(request); var retention = request.OverrideRetention ?? new ExportRetentionConfig(); - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); _logger.LogInformation( "Starting retention prune for tenant {TenantId}, profile {ProfileId}, execute={Execute}", diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/InMemorySchedulingStores.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/InMemorySchedulingStores.cs index b75c4dd58..715b0ef2b 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/InMemorySchedulingStores.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/InMemorySchedulingStores.cs @@ -11,6 +11,12 @@ public sealed class InMemoryExportScheduleStore : IExportScheduleStore private readonly ConcurrentDictionary _runToProfile = new(); private readonly ConcurrentDictionary> _profilesByTenant = new(); private readonly object _lock = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryExportScheduleStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } /// /// Adds a profile for testing. @@ -106,7 +112,7 @@ public sealed class InMemoryExportScheduleStore : IExportScheduleStore { if (_statusByProfile.TryGetValue(profileId, out var existing)) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var newFailureCount = success ? 0 : existing.ConsecutiveFailures + 1; _statusByProfile[profileId] = existing with diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/LineageEvidencePackService.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/LineageEvidencePackService.cs index 13957b7d8..40f323873 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/LineageEvidencePackService.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/LineageEvidencePackService.cs @@ -32,12 +32,14 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService }; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _packCache = new(); private readonly string _tempDirectory; - public LineageEvidencePackService(ILogger logger) + public LineageEvidencePackService(ILogger logger, TimeProvider? timeProvider = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; _tempDirectory = Path.Combine(Path.GetTempPath(), "stellaops-evidence-packs"); Directory.CreateDirectory(_tempDirectory); } @@ -187,7 +189,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService Entries = entries.ToImmutableArray(), TotalSizeBytes = entries.Sum(e => e.SizeBytes), FileCount = entries.Count, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = _timeProvider.GetUtcNow() }; // Write manifest @@ -205,7 +207,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService VexVerdictDigests = vexDocuments.Select(v => v.Digest).ToImmutableArray(), PolicyVerdictDigest = policyVerdict?.Digest, ReplayHash = ComputeReplayHash(artifactDigest, sbomDigest, manifest.MerkleRoot), - GeneratedAt = DateTimeOffset.UtcNow, + GeneratedAt = _timeProvider.GetUtcNow(), Attestations = attestations.ToImmutableArray(), SbomDocuments = sbomDocuments.ToImmutableArray(), VexDocuments = vexDocuments.ToImmutableArray(), @@ -224,7 +226,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService { Pack = pack, ZipPath = zipPath, - ExpiresAt = DateTimeOffset.UtcNow.AddHours(24) + ExpiresAt = _timeProvider.GetUtcNow().AddHours(24) }; // Clean up temp directory @@ -246,7 +248,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService Success = true, Pack = pack, DownloadUrl = $"/api/v1/lineage/export/{packId}/download", - ExpiresAt = DateTimeOffset.UtcNow.AddHours(24), + ExpiresAt = _timeProvider.GetUtcNow().AddHours(24), SizeBytes = zipInfo.Length, Warnings = warnings.ToImmutableArray() }; @@ -268,7 +270,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService string tenantId, 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) { @@ -285,7 +287,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService string tenantId, 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)) { @@ -347,7 +349,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService bomFormat = "CycloneDX", specVersion = "1.6", version = 1, - metadata = new { timestamp = DateTimeOffset.UtcNow.ToString("O") }, + metadata = new { timestamp = _timeProvider.GetUtcNow().ToString("O") }, components = Array.Empty() }, JsonOptions); @@ -383,7 +385,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService dataLicense = "CC0-1.0", name = artifactDigest, documentNamespace = $"https://stellaops.io/spdx/{artifactDigest}", - creationInfo = new { created = DateTimeOffset.UtcNow.ToString("O") }, + creationInfo = new { created = _timeProvider.GetUtcNow().ToString("O") }, packages = Array.Empty() }, JsonOptions); @@ -418,7 +420,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService context = "https://openvex.dev/ns/v0.2.0", id = $"urn:stellaops:vex:{artifactDigest}", author = "StellaOps", - timestamp = DateTimeOffset.UtcNow.ToString("O"), + timestamp = _timeProvider.GetUtcNow().ToString("O"), statements = Array.Empty() }, JsonOptions); @@ -457,7 +459,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService tenantId, verdict = "pass", policyVersion = "1.0.0", - evaluatedAt = DateTimeOffset.UtcNow.ToString("O"), + evaluatedAt = _timeProvider.GetUtcNow().ToString("O"), rules = new { total = 0, passed = 0, failed = 0, warned = 0 } }, JsonOptions); @@ -477,7 +479,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService RulesPassed = 0, RulesFailed = 0, RulesWarned = 0, - EvaluatedAt = DateTimeOffset.UtcNow, + EvaluatedAt = _timeProvider.GetUtcNow(), FileName = fileName }; } @@ -528,9 +530,9 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService 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)}"; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationModels.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationModels.cs index 175cb5081..935f29f79 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationModels.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationModels.cs @@ -149,14 +149,15 @@ public sealed record ExportVerificationResult /// /// When verification was performed. /// - 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() { Status = VerificationStatus.Invalid, RunId = runId, - Errors = errors + Errors = errors, + VerifiedAt = verifiedAt }; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationService.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationService.cs index 80647e66a..b818fc8ea 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationService.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Verification/ExportVerificationService.cs @@ -14,22 +14,26 @@ public sealed class ExportVerificationService : IExportVerificationService private readonly IExportArtifactStore _artifactStore; private readonly IPackRunAttestationStore? _packRunStore; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public ExportVerificationService( IExportArtifactStore artifactStore, - ILogger logger) - : this(artifactStore, null, logger) + ILogger logger, + TimeProvider? timeProvider = null) + : this(artifactStore, null, logger, timeProvider) { } public ExportVerificationService( IExportArtifactStore artifactStore, IPackRunAttestationStore? packRunStore, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore)); _packRunStore = packRunStore; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -52,6 +56,7 @@ public sealed class ExportVerificationService : IExportVerificationService { return ExportVerificationResult.Failed( request.RunId, + _timeProvider.GetUtcNow(), new VerificationError { Code = VerificationErrorCodes.ManifestNotFound, @@ -64,6 +69,7 @@ public sealed class ExportVerificationService : IExportVerificationService { return ExportVerificationResult.Failed( request.RunId, + _timeProvider.GetUtcNow(), new VerificationError { Code = VerificationErrorCodes.TenantMismatch, @@ -234,7 +240,8 @@ public sealed class ExportVerificationService : IExportVerificationService Encryption = encryptionResult, Attestation = attestationStatus, Errors = errors, - Warnings = warnings + Warnings = warnings, + VerifiedAt = _timeProvider.GetUtcNow() }; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs index ae49f3f35..2db7edd3a 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs @@ -19,6 +19,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator private readonly IExceptionApplicationRepository _applicationRepository; private readonly ConcurrentDictionary _jobs = new(); private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -30,11 +31,13 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator public ExceptionReportGenerator( IExceptionRepository exceptionRepository, IExceptionApplicationRepository applicationRepository, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _exceptionRepository = exceptionRepository; _applicationRepository = applicationRepository; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task CreateReportAsync( @@ -42,7 +45,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator CancellationToken cancellationToken = default) { var jobId = $"exc-rpt-{Guid.NewGuid():N}"; - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var job = new ReportJob { @@ -151,7 +154,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator try { job.Status = "running"; - job.StartedAt = DateTimeOffset.UtcNow; + job.StartedAt = _timeProvider.GetUtcNow(); var filter = job.Request.Filter ?? new ExceptionFilter { @@ -232,7 +235,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator var document = new ExceptionReportDocument { ReportId = job.JobId, - GeneratedAt = DateTimeOffset.UtcNow, + GeneratedAt = _timeProvider.GetUtcNow(), TenantId = job.TenantId, RequesterId = job.RequesterId, 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.Progress = 100; job.Status = "completed"; - job.CompletedAt = DateTimeOffset.UtcNow; + job.CompletedAt = _timeProvider.GetUtcNow(); _logger.LogInformation( "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); job.Status = "failed"; job.ErrorMessage = ex.Message; - job.CompletedAt = DateTimeOffset.UtcNow; + job.CompletedAt = _timeProvider.GetUtcNow(); } } diff --git a/src/Findings/StellaOps.Findings.Ledger.WebService/Mappings/LedgerEventMapping.cs b/src/Findings/StellaOps.Findings.Ledger.WebService/Mappings/LedgerEventMapping.cs index efb3db55d..e4840dc25 100644 --- a/src/Findings/StellaOps.Findings.Ledger.WebService/Mappings/LedgerEventMapping.cs +++ b/src/Findings/StellaOps.Findings.Ledger.WebService/Mappings/LedgerEventMapping.cs @@ -6,11 +6,12 @@ namespace StellaOps.Findings.Ledger.WebService.Mappings; public static class LedgerEventMapping { - public static LedgerEventDraft ToDraft(this LedgerEventRequest request) + public static LedgerEventDraft ToDraft(this LedgerEventRequest request, TimeProvider? timeProvider = null) { 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 eventObject = new JsonObject diff --git a/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs b/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs index f61de8d51..6d9794d90 100644 --- a/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs +++ b/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs @@ -1768,6 +1768,7 @@ app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task { var alert = await alertService.GetAlertAsync(alertId, cancellationToken).ConfigureAwait(false); @@ -1786,7 +1787,7 @@ app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task BuildAsync( @@ -126,7 +129,7 @@ public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder Nodes = nodes, Edges = edges, RootNodeId = verdictNode.Id, - GeneratedAt = DateTimeOffset.UtcNow + GeneratedAt = _timeProvider.GetUtcNow() }; } diff --git a/src/Findings/StellaOps.Findings.Ledger/Services/FindingWorkflowService.cs b/src/Findings/StellaOps.Findings.Ledger/Services/FindingWorkflowService.cs index 76119e486..5fb3fc044 100644 --- a/src/Findings/StellaOps.Findings.Ledger/Services/FindingWorkflowService.cs +++ b/src/Findings/StellaOps.Findings.Ledger/Services/FindingWorkflowService.cs @@ -66,7 +66,7 @@ public sealed class FindingWorkflowService : IFindingWorkflowService var payload = CreateBasePayload(request); payload["action"] = "assign"; - payload["assignee"] = BuildAssigneeNode(request.Assignee); + payload["assignee"] = BuildAssigneeNode(request.Assignee!); AddComment(payload, request.Comment); ApplyStatus(payload, request.Status); ApplyAttachments(payload, request.Attachments); diff --git a/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj b/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj index e69de29bb..3d032bd4d 100644 --- a/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj +++ b/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj @@ -0,0 +1,18 @@ + + + net10.0 + preview + enable + enable + true + + + + + + + + + + + diff --git a/src/Notify/__Libraries/StellaOps.Notify.Storage.InMemory/Repositories/InMemoryRepositories.cs b/src/Notify/__Libraries/StellaOps.Notify.Storage.InMemory/Repositories/InMemoryRepositories.cs index 960b722cc..9db12d8e2 100644 --- a/src/Notify/__Libraries/StellaOps.Notify.Storage.InMemory/Repositories/InMemoryRepositories.cs +++ b/src/Notify/__Libraries/StellaOps.Notify.Storage.InMemory/Repositories/InMemoryRepositories.cs @@ -9,6 +9,12 @@ namespace StellaOps.Notify.Storage.InMemory.Repositories; public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository { private readonly ConcurrentDictionary _channels = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyChannelRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -34,7 +40,7 @@ public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository public Task UpsertAsync(NotifyChannelDocument channel, CancellationToken cancellationToken = default) { - channel.UpdatedAt = DateTimeOffset.UtcNow; + channel.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{channel.TenantId}:{channel.Id}"; _channels[key] = channel; return Task.FromResult(channel); @@ -59,6 +65,12 @@ public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository { private readonly ConcurrentDictionary _rules = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyRuleRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -83,7 +95,7 @@ public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository public Task UpsertAsync(NotifyRuleDocument rule, CancellationToken cancellationToken = default) { - rule.UpdatedAt = DateTimeOffset.UtcNow; + rule.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{rule.TenantId}:{rule.Id}"; _rules[key] = rule; return Task.FromResult(rule); @@ -108,6 +120,12 @@ public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository { private readonly ConcurrentDictionary _templates = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyTemplateRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -130,7 +148,7 @@ public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository public Task UpsertAsync(NotifyTemplateDocument template, CancellationToken cancellationToken = default) { - template.UpdatedAt = DateTimeOffset.UtcNow; + template.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{template.TenantId}:{template.Id}"; _templates[key] = template; return Task.FromResult(template); @@ -149,6 +167,12 @@ public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository { private readonly ConcurrentDictionary _deliveries = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyDeliveryRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -166,7 +190,7 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository public Task UpsertAsync(NotifyDeliveryDocument delivery, CancellationToken cancellationToken = default) { - delivery.UpdatedAt = DateTimeOffset.UtcNow; + delivery.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{delivery.TenantId}:{delivery.Id}"; _deliveries[key] = delivery; return Task.FromResult(delivery); @@ -179,7 +203,7 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository { doc.Status = status; doc.Error = error; - doc.UpdatedAt = DateTimeOffset.UtcNow; + doc.UpdatedAt = _timeProvider.GetUtcNow(); return Task.FromResult(true); } return Task.FromResult(false); @@ -199,6 +223,12 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository { private readonly ConcurrentDictionary _digests = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyDigestRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -209,7 +239,7 @@ public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository public Task UpsertAsync(NotifyDigestDocument digest, CancellationToken cancellationToken = default) { - digest.UpdatedAt = DateTimeOffset.UtcNow; + digest.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{digest.TenantId}:{digest.Id}"; _digests[key] = digest; return Task.FromResult(digest); @@ -257,10 +287,16 @@ public sealed class NotifyAuditRepositoryAdapter : INotifyAuditRepository public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository { private readonly ConcurrentDictionary _locks = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyLockRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task TryAcquireAsync(string lockKey, string owner, TimeSpan ttl, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); // Clean up expired locks 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) { - var newExpiry = DateTimeOffset.UtcNow + ttl; + var newExpiry = _timeProvider.GetUtcNow() + ttl; _locks[lockKey] = (owner, newExpiry); return Task.FromResult(true); } @@ -302,6 +338,12 @@ public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationPolicyRepository { private readonly ConcurrentDictionary _policies = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyEscalationPolicyRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -318,7 +360,7 @@ public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationP public Task UpsertAsync(NotifyEscalationPolicyDocument policy, CancellationToken cancellationToken = default) { - policy.UpdatedAt = DateTimeOffset.UtcNow; + policy.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{policy.TenantId}:{policy.Id}"; _policies[key] = policy; return Task.FromResult(policy); @@ -331,6 +373,12 @@ public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationP public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationStateRepository { private readonly ConcurrentDictionary _states = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyEscalationStateRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -341,7 +389,7 @@ public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationSt public Task UpsertAsync(NotifyEscalationStateDocument state, CancellationToken cancellationToken = default) { - state.UpdatedAt = DateTimeOffset.UtcNow; + state.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{state.TenantId}:{state.Id}"; _states[key] = state; return Task.FromResult(state); @@ -360,6 +408,12 @@ public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationSt public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallScheduleRepository { private readonly ConcurrentDictionary _schedules = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyOnCallScheduleRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -376,7 +430,7 @@ public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallSchedul public Task UpsertAsync(NotifyOnCallScheduleDocument schedule, CancellationToken cancellationToken = default) { - schedule.UpdatedAt = DateTimeOffset.UtcNow; + schedule.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{schedule.TenantId}:{schedule.Id}"; _schedules[key] = schedule; return Task.FromResult(schedule); @@ -397,6 +451,12 @@ public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallSchedul public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursRepository { private readonly ConcurrentDictionary _quietHours = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyQuietHoursRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -413,7 +473,7 @@ public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursReposit public Task UpsertAsync(NotifyQuietHoursDocument quietHours, CancellationToken cancellationToken = default) { - quietHours.UpdatedAt = DateTimeOffset.UtcNow; + quietHours.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{quietHours.TenantId}:{quietHours.Id}"; _quietHours[key] = quietHours; return Task.FromResult(quietHours); @@ -432,6 +492,12 @@ public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursReposit public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanceWindowRepository { private readonly ConcurrentDictionary _windows = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyMaintenanceWindowRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -454,7 +520,7 @@ public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanc public Task UpsertAsync(NotifyMaintenanceWindowDocument window, CancellationToken cancellationToken = default) { - window.UpdatedAt = DateTimeOffset.UtcNow; + window.UpdatedAt = _timeProvider.GetUtcNow(); var key = $"{window.TenantId}:{window.Id}"; _windows[key] = window; return Task.FromResult(window); @@ -473,6 +539,12 @@ public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanc public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository { private readonly ConcurrentDictionary _inbox = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public NotifyInboxRepositoryAdapter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default) { @@ -502,7 +574,7 @@ public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository if (_inbox.TryGetValue(key, out var doc)) { doc.Read = true; - doc.ReadAt = DateTimeOffset.UtcNow; + doc.ReadAt = _timeProvider.GetUtcNow(); return Task.FromResult(true); } return Task.FromResult(false); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Ledger/LedgerExporter.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Ledger/LedgerExporter.cs index a658e6bd9..7a3fe63d5 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Ledger/LedgerExporter.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Ledger/LedgerExporter.cs @@ -16,6 +16,7 @@ public sealed class LedgerExporter : ILedgerExporter private readonly ILedgerRepository _ledgerRepository; private readonly ILedgerExportRepository _exportRepository; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -32,11 +33,13 @@ public sealed class LedgerExporter : ILedgerExporter public LedgerExporter( ILedgerRepository ledgerRepository, ILedgerExportRepository exportRepository, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _ledgerRepository = ledgerRepository; _exportRepository = exportRepository; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -44,7 +47,7 @@ public sealed class LedgerExporter : ILedgerExporter LedgerExport export, CancellationToken cancellationToken = default) { - var startTime = DateTimeOffset.UtcNow; + var startTime = _timeProvider.GetUtcNow(); try { @@ -83,7 +86,7 @@ public sealed class LedgerExporter : ILedgerExporter export = export.Complete(outputUri, digest, sizeBytes, entries.Count); export = await _exportRepository.UpdateAsync(export, cancellationToken); - var duration = DateTimeOffset.UtcNow - startTime; + var duration = _timeProvider.GetUtcNow() - startTime; OrchestratorMetrics.LedgerExportCompleted(export.TenantId, export.Format); OrchestratorMetrics.RecordLedgerExportDuration(export.TenantId, export.Format, duration.TotalSeconds); OrchestratorMetrics.RecordLedgerExportSize(export.TenantId, export.Format, sizeBytes); @@ -165,12 +168,12 @@ public sealed class LedgerExporter : ILedgerExporter return (content, digest); } - private static string GenerateJson(IReadOnlyList entries) + private string GenerateJson(IReadOnlyList entries) { var exportData = new LedgerExportData { SchemaVersion = "1.0.0", - ExportedAt = DateTimeOffset.UtcNow, + ExportedAt = _timeProvider.GetUtcNow(), EntryCount = entries.Count, Entries = entries.Select(MapEntry).ToList() }; diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresDuplicateSuppressor.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresDuplicateSuppressor.cs index 605c9911a..6a20f62be 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresDuplicateSuppressor.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresDuplicateSuppressor.cs @@ -56,15 +56,18 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor private readonly OrchestratorDataSource _dataSource; private readonly string _tenantId; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresDuplicateSuppressor( OrchestratorDataSource dataSource, string tenantId, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task 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_time", eventTime); 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); } @@ -143,7 +146,7 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor 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 transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresJobRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresJobRepository.cs index 5e993a728..ae25d083d 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresJobRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresJobRepository.cs @@ -108,13 +108,16 @@ public sealed class PostgresJobRepository : IJobRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresJobRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task 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("worker_id", workerId); command.Parameters.AddWithValue("lease_until", leaseUntil); - command.Parameters.AddWithValue("leased_at", DateTimeOffset.UtcNow); - command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow); + command.Parameters.AddWithValue("leased_at", _timeProvider.GetUtcNow()); + command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow()); if (jobType != null) { @@ -263,7 +266,7 @@ public sealed class PostgresJobRepository : IJobRepository command.Parameters.AddWithValue("job_id", jobId); command.Parameters.AddWithValue("lease_id", leaseId); 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); return rows > 0; diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRegistryRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRegistryRepository.cs index bec7a161e..a6153b4fd 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRegistryRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRegistryRepository.cs @@ -13,6 +13,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private const string PackColumns = """ pack_id, tenant_id, project_id, name, display_name, description, @@ -33,10 +34,12 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository public PostgresPackRegistryRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } // Pack CRUD @@ -264,7 +267,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository command.Parameters.AddWithValue("tenant_id", tenantId); command.Parameters.AddWithValue("pack_id", packId); 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("published_at", (object?)publishedAt?.UtcDateTime ?? 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("pack_version_id", packVersionId); 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("published_at", (object?)publishedAt?.UtcDateTime ?? 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("signed_by", signedBy); 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); } diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRunRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRunRepository.cs index 6d69b3d26..1c1e4e75f 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRunRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresPackRunRepository.cs @@ -128,11 +128,16 @@ public sealed class PostgresPackRunRepository : IPackRunRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public PostgresPackRunRepository(OrchestratorDataSource dataSource, ILogger logger) + public PostgresPackRunRepository( + OrchestratorDataSource dataSource, + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task 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 command = new NpgsqlCommand(sql, connection); command.CommandTimeout = _dataSource.CommandTimeoutSeconds; - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); command.Parameters.AddWithValue("tenant_id", tenantId); command.Parameters.AddWithValue("lease_id", leaseId); 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("lease_id", leaseId); 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); return rows > 0; @@ -292,7 +297,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository command.Parameters.AddWithValue("lease_id", leaseId); command.Parameters.AddWithValue("status", StatusToString(newStatus)); 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); } diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresQuotaRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresQuotaRepository.cs index 3cc77541d..a38389acd 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresQuotaRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresQuotaRepository.cs @@ -113,13 +113,16 @@ public sealed class PostgresQuotaRepository : IQuotaRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresQuotaRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task 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_hour_count", currentHourCount); 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); await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); @@ -245,7 +248,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository command.Parameters.AddWithValue("quota_id", quotaId); command.Parameters.AddWithValue("pause_reason", reason); 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); 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("quota_id", quotaId); - command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow); + command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow()); command.Parameters.AddWithValue("updated_by", updatedBy); 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("quota_id", quotaId); - command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow); + command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow()); await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } @@ -294,7 +297,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository command.Parameters.AddWithValue("tenant_id", tenantId); 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); } diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresRunRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresRunRepository.cs index 035cb5afb..70a79823c 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresRunRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresRunRepository.cs @@ -69,13 +69,16 @@ public sealed class PostgresRunRepository : IRunRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresRunRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task 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("run_id", runId); 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); if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresSourceRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresSourceRepository.cs index ca18adc2c..e2c713551 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresSourceRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresSourceRepository.cs @@ -74,13 +74,16 @@ public sealed class PostgresSourceRepository : ISourceRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresSourceRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task 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("pause_reason", reason); 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); 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("source_id", sourceId); - command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow); + command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow()); command.Parameters.AddWithValue("updated_by", updatedBy); var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresThrottleRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresThrottleRepository.cs index dd958d3e2..38c2c6558 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresThrottleRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresThrottleRepository.cs @@ -77,13 +77,16 @@ public sealed class PostgresThrottleRepository : IThrottleRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresThrottleRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task GetByIdAsync(string tenantId, Guid throttleId, CancellationToken cancellationToken) @@ -110,7 +113,7 @@ public sealed class PostgresThrottleRepository : IThrottleRepository command.CommandTimeout = _dataSource.CommandTimeoutSeconds; command.Parameters.AddWithValue("tenant_id", tenantId); 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); var throttles = new List(); @@ -128,7 +131,7 @@ public sealed class PostgresThrottleRepository : IThrottleRepository command.CommandTimeout = _dataSource.CommandTimeoutSeconds; command.Parameters.AddWithValue("tenant_id", tenantId); 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); var throttles = new List(); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresWatermarkRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresWatermarkRepository.cs index 1b87b9f03..c28b54d97 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresWatermarkRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Postgres/PostgresWatermarkRepository.cs @@ -100,13 +100,16 @@ public sealed class PostgresWatermarkRepository : IWatermarkRepository private readonly OrchestratorDataSource _dataSource; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public PostgresWatermarkRepository( OrchestratorDataSource dataSource, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task GetByScopeKeyAsync(string tenantId, string scopeKey, CancellationToken cancellationToken) @@ -271,7 +274,7 @@ public sealed class PostgresWatermarkRepository : IWatermarkRepository int limit, 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 command = new NpgsqlCommand(SelectLaggingSql, connection); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Repositories/IBackfillRepository.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Repositories/IBackfillRepository.cs index 13d036f46..7affdaebe 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Repositories/IBackfillRepository.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Repositories/IBackfillRepository.cs @@ -152,7 +152,8 @@ public sealed record BackfillCheckpoint( int batchNumber, DateTimeOffset batchStart, DateTimeOffset batchEnd, - int eventsInBatch) + int eventsInBatch, + DateTimeOffset startedAt) { return new BackfillCheckpoint( CheckpointId: Guid.NewGuid(), @@ -166,7 +167,7 @@ public sealed record BackfillCheckpoint( EventsSkipped: 0, EventsFailed: 0, BatchHash: null, - StartedAt: DateTimeOffset.UtcNow, + StartedAt: startedAt, CompletedAt: null, ErrorMessage: null); } @@ -174,7 +175,7 @@ public sealed record BackfillCheckpoint( /// /// Marks the checkpoint as complete. /// - 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 { @@ -182,18 +183,18 @@ public sealed record BackfillCheckpoint( EventsSkipped = skipped, EventsFailed = failed, BatchHash = batchHash, - CompletedAt = DateTimeOffset.UtcNow + CompletedAt = completedAt }; } /// /// Marks the checkpoint as failed. /// - public BackfillCheckpoint Fail(string error) + public BackfillCheckpoint Fail(string error, DateTimeOffset completedAt) { return this with { - CompletedAt = DateTimeOffset.UtcNow, + CompletedAt = completedAt, ErrorMessage = error }; } diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Services/FirstSignalSnapshotWriter.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Services/FirstSignalSnapshotWriter.cs index 31a05d7a9..7d9975d72 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Services/FirstSignalSnapshotWriter.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/Services/FirstSignalSnapshotWriter.cs @@ -14,15 +14,18 @@ public sealed class FirstSignalSnapshotWriter : BackgroundService private readonly IServiceScopeFactory _scopeFactory; private readonly FirstSignalSnapshotWriterOptions _options; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public FirstSignalSnapshotWriter( IServiceScopeFactory scopeFactory, IOptions options, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _options = (options ?? throw new ArgumentNullException(nameof(options))).Value.SnapshotWriter; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -76,7 +79,7 @@ public sealed class FirstSignalSnapshotWriter : BackgroundService var runRepository = scope.ServiceProvider.GetRequiredService(); var firstSignalService = scope.ServiceProvider.GetRequiredService(); - var createdAfter = DateTimeOffset.UtcNow.Subtract(lookback); + var createdAfter = _timeProvider.GetUtcNow().Subtract(lookback); var pending = await runRepository.ListAsync( tenantId, diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/HealthEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/HealthEndpoints.cs index 024b87537..514366d44 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/HealthEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/HealthEndpoints.cs @@ -36,13 +36,14 @@ public static class HealthEndpoints 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 GetReadiness( [FromServices] OrchestratorDataSource dataSource, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) { try @@ -53,14 +54,14 @@ public static class HealthEndpoints if (!dbHealthy) { return Results.Json( - new ReadinessResponse("not_ready", DateTimeOffset.UtcNow, new Dictionary + new ReadinessResponse("not_ready", timeProvider.GetUtcNow(), new Dictionary { ["database"] = "unhealthy" }), statusCode: StatusCodes.Status503ServiceUnavailable); } - return Results.Ok(new ReadinessResponse("ready", DateTimeOffset.UtcNow, new Dictionary + return Results.Ok(new ReadinessResponse("ready", timeProvider.GetUtcNow(), new Dictionary { ["database"] = "healthy" })); @@ -68,7 +69,7 @@ public static class HealthEndpoints catch (Exception ex) { return Results.Json( - new ReadinessResponse("not_ready", DateTimeOffset.UtcNow, new Dictionary + new ReadinessResponse("not_ready", timeProvider.GetUtcNow(), new Dictionary { ["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 - return Results.Ok(new HealthResponse("alive", DateTimeOffset.UtcNow)); + return Results.Ok(new HealthResponse("alive", timeProvider.GetUtcNow())); } private static async Task GetHealthDetails( [FromServices] OrchestratorDataSource dataSource, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) { var checks = new Dictionary(); @@ -96,12 +98,12 @@ public static class HealthEndpoints checks["database"] = new HealthCheckResult( dbHealthy ? "healthy" : "unhealthy", dbHealthy ? null : "Connection test failed", - DateTimeOffset.UtcNow); + timeProvider.GetUtcNow()); overallHealthy &= dbHealthy; } catch (Exception ex) { - checks["database"] = new HealthCheckResult("unhealthy", ex.Message, DateTimeOffset.UtcNow); + checks["database"] = new HealthCheckResult("unhealthy", ex.Message, timeProvider.GetUtcNow()); overallHealthy = false; } @@ -114,7 +116,7 @@ public static class HealthEndpoints checks["memory"] = new HealthCheckResult( memoryHealthy ? "healthy" : "degraded", $"Used: {memoryUsedMb:F2} MB", - DateTimeOffset.UtcNow); + timeProvider.GetUtcNow()); // Thread pool check ThreadPool.GetAvailableThreads(out var workerThreads, out var completionPortThreads); @@ -124,11 +126,11 @@ public static class HealthEndpoints checks["threadPool"] = new HealthCheckResult( threadPoolHealthy ? "healthy" : "degraded", $"Worker threads available: {workerThreads}/{maxWorkerThreads}", - DateTimeOffset.UtcNow); + timeProvider.GetUtcNow()); var response = new HealthDetailsResponse( overallHealthy ? "healthy" : "unhealthy", - DateTimeOffset.UtcNow, + timeProvider.GetUtcNow(), checks); return overallHealthy diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs index 5da313ace..c5728bb86 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs @@ -55,10 +55,12 @@ public static class KpiEndpoints [FromQuery] DateTimeOffset? to, [FromQuery] string? tenant, [FromServices] IKpiCollector collector, + [FromServices] TimeProvider timeProvider, CancellationToken ct) { - var start = from ?? DateTimeOffset.UtcNow.AddDays(-7); - var end = to ?? DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); + var start = from ?? now.AddDays(-7); + var end = to ?? now; var kpis = await collector.CollectAsync(start, end, tenant, ct); return Results.Ok(kpis); @@ -69,11 +71,13 @@ public static class KpiEndpoints [FromQuery] DateTimeOffset? to, [FromQuery] string? tenant, [FromServices] IKpiCollector collector, + [FromServices] TimeProvider timeProvider, CancellationToken ct) { + var now = timeProvider.GetUtcNow(); var kpis = await collector.CollectAsync( - from ?? DateTimeOffset.UtcNow.AddDays(-7), - to ?? DateTimeOffset.UtcNow, + from ?? now.AddDays(-7), + to ?? now, tenant, ct); return Results.Ok(kpis.Reachability); @@ -84,11 +88,13 @@ public static class KpiEndpoints [FromQuery] DateTimeOffset? to, [FromQuery] string? tenant, [FromServices] IKpiCollector collector, + [FromServices] TimeProvider timeProvider, CancellationToken ct) { + var now = timeProvider.GetUtcNow(); var kpis = await collector.CollectAsync( - from ?? DateTimeOffset.UtcNow.AddDays(-7), - to ?? DateTimeOffset.UtcNow, + from ?? now.AddDays(-7), + to ?? now, tenant, ct); return Results.Ok(kpis.Explainability); @@ -99,11 +105,13 @@ public static class KpiEndpoints [FromQuery] DateTimeOffset? to, [FromQuery] string? tenant, [FromServices] IKpiCollector collector, + [FromServices] TimeProvider timeProvider, CancellationToken ct) { + var now = timeProvider.GetUtcNow(); var kpis = await collector.CollectAsync( - from ?? DateTimeOffset.UtcNow.AddDays(-7), - to ?? DateTimeOffset.UtcNow, + from ?? now.AddDays(-7), + to ?? now, tenant, ct); return Results.Ok(kpis.Runtime); @@ -114,11 +122,13 @@ public static class KpiEndpoints [FromQuery] DateTimeOffset? to, [FromQuery] string? tenant, [FromServices] IKpiCollector collector, + [FromServices] TimeProvider timeProvider, CancellationToken ct) { + var now = timeProvider.GetUtcNow(); var kpis = await collector.CollectAsync( - from ?? DateTimeOffset.UtcNow.AddDays(-7), - to ?? DateTimeOffset.UtcNow, + from ?? now.AddDays(-7), + to ?? now, tenant, ct); return Results.Ok(kpis.Replay); diff --git a/src/Policy/StellaOps.Policy.Engine/Program.cs b/src/Policy/StellaOps.Policy.Engine/Program.cs index f848570a7..ca2ca7340 100644 --- a/src/Policy/StellaOps.Policy.Engine/Program.cs +++ b/src/Policy/StellaOps.Policy.Engine/Program.cs @@ -375,8 +375,8 @@ app.MapConflictsApi(); app.Run(); -// Make Program class partial to allow integration testing while keeping it minimal +// Make Program class internal to prevent type conflicts when referencing this assembly namespace StellaOps.Policy.Engine { - public partial class Program { } + internal partial class Program { } } diff --git a/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs b/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs index 916bbb8fb..5cc5c4bed 100644 --- a/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs +++ b/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs @@ -6,12 +6,18 @@ namespace StellaOps.Policy.Engine.Services; internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository { private readonly ConcurrentDictionary packs = new(StringComparer.OrdinalIgnoreCase); + private readonly TimeProvider _timeProvider; + + public InMemoryPolicyPackRepository(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task CreateAsync(string packId, string? displayName, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(packId); - var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, DateTimeOffset.UtcNow)); + var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, _timeProvider.GetUtcNow())); return Task.FromResult(created); } @@ -25,15 +31,15 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository public Task UpsertRevisionAsync(string packId, int version, bool requiresTwoPersonApproval, PolicyRevisionStatus initialStatus, CancellationToken cancellationToken) { - var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow)); + var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, _timeProvider.GetUtcNow())); int revisionVersion = version > 0 ? version : pack.GetNextVersion(); var revision = pack.GetOrAddRevision( revisionVersion, - v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, DateTimeOffset.UtcNow)); + v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, _timeProvider.GetUtcNow())); if (revision.Status != initialStatus) { - revision.SetStatus(initialStatus, DateTimeOffset.UtcNow); + revision.SetStatus(initialStatus, _timeProvider.GetUtcNow()); } return Task.FromResult(revision); @@ -95,9 +101,9 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository { ArgumentNullException.ThrowIfNull(bundle); - var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow)); + var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, _timeProvider.GetUtcNow())); var revision = pack.GetOrAddRevision(version > 0 ? version : pack.GetNextVersion(), - v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, DateTimeOffset.UtcNow)); + v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, _timeProvider.GetUtcNow())); revision.SetBundle(bundle); return Task.FromResult(bundle); diff --git a/src/Policy/StellaOps.Policy.Engine/Storage/InMemory/InMemoryExceptionRepository.cs b/src/Policy/StellaOps.Policy.Engine/Storage/InMemory/InMemoryExceptionRepository.cs index 8d1439d23..57442a1cb 100644 --- a/src/Policy/StellaOps.Policy.Engine/Storage/InMemory/InMemoryExceptionRepository.cs +++ b/src/Policy/StellaOps.Policy.Engine/Storage/InMemory/InMemoryExceptionRepository.cs @@ -13,6 +13,12 @@ namespace StellaOps.Policy.Engine.Storage.InMemory; public sealed class InMemoryExceptionRepository : IExceptionRepository { private readonly ConcurrentDictionary<(string Tenant, Guid Id), ExceptionEntity> _exceptions = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryExceptionRepository(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task CreateAsync(ExceptionEntity exception, CancellationToken cancellationToken = default) { @@ -123,7 +129,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository _exceptions[key] = Copy( existing, statusOverride: ExceptionStatus.Revoked, - revokedAtOverride: DateTimeOffset.UtcNow, + revokedAtOverride: _timeProvider.GetUtcNow(), revokedByOverride: revokedBy); return Task.FromResult(true); } @@ -133,7 +139,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository public Task ExpireAsync(string tenantId, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var normalizedTenant = Normalize(tenantId); var expired = 0; diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs index 1a3c33378..5d9137a01 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs @@ -89,6 +89,7 @@ public static class ExceptionApprovalEndpoints CreateApprovalRequestDto request, IExceptionApprovalRepository repository, IExceptionApprovalRulesService rulesService, + [FromServices] TimeProvider timeProvider, ILogger logger, CancellationToken cancellationToken) { @@ -110,7 +111,7 @@ public static class ExceptionApprovalEndpoints } // Generate request ID - var requestId = $"EAR-{DateTimeOffset.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}"; + var requestId = $"EAR-{timeProvider.GetUtcNow():yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}"; // Parse gate level if (!Enum.TryParse(request.GateLevel, ignoreCase: true, out var gateLevel)) @@ -139,7 +140,7 @@ public static class ExceptionApprovalEndpoints }); } - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); var entity = new ExceptionApprovalRequestEntity { Id = Guid.NewGuid(), diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs index 8267b7183..33128eadf 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs @@ -134,6 +134,7 @@ public static class ExceptionEndpoints CreateExceptionRequest request, HttpContext context, IExceptionRepository repository, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { if (request is null) @@ -146,7 +147,7 @@ public static class ExceptionEndpoints } // Validate expiry is in future - if (request.ExpiresAt <= DateTimeOffset.UtcNow) + if (request.ExpiresAt <= timeProvider.GetUtcNow()) { return Results.BadRequest(new ProblemDetails { @@ -157,7 +158,7 @@ public static class ExceptionEndpoints } // Validate expiry is not more than 1 year - if (request.ExpiresAt > DateTimeOffset.UtcNow.AddYears(1)) + if (request.ExpiresAt > timeProvider.GetUtcNow().AddYears(1)) { return Results.BadRequest(new ProblemDetails { @@ -188,8 +189,8 @@ public static class ExceptionEndpoints }, OwnerId = request.OwnerId, RequesterId = actorId, - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, + CreatedAt = timeProvider.GetUtcNow(), + UpdatedAt = timeProvider.GetUtcNow(), ExpiresAt = request.ExpiresAt, ReasonCode = ParseReasonRequired(request.ReasonCode), Rationale = request.Rationale, @@ -210,6 +211,7 @@ public static class ExceptionEndpoints UpdateExceptionRequest request, HttpContext context, IExceptionRepository repository, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { var existing = await repository.GetByIdAsync(id, cancellationToken); @@ -238,7 +240,7 @@ public static class ExceptionEndpoints var updated = existing with { Version = existing.Version + 1, - UpdatedAt = DateTimeOffset.UtcNow, + UpdatedAt = timeProvider.GetUtcNow(), Rationale = request.Rationale ?? existing.Rationale, EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? existing.EvidenceRefs, CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? existing.CompensatingControls, @@ -258,6 +260,7 @@ public static class ExceptionEndpoints ApproveExceptionRequest? request, HttpContext context, IExceptionRepository repository, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { var existing = await repository.GetByIdAsync(id, cancellationToken); @@ -294,8 +297,8 @@ public static class ExceptionEndpoints { Version = existing.Version + 1, Status = ExceptionStatus.Approved, - UpdatedAt = DateTimeOffset.UtcNow, - ApprovedAt = DateTimeOffset.UtcNow, + UpdatedAt = timeProvider.GetUtcNow(), + ApprovedAt = timeProvider.GetUtcNow(), ApproverIds = existing.ApproverIds.Add(actorId) }; @@ -310,6 +313,7 @@ public static class ExceptionEndpoints string id, HttpContext context, IExceptionRepository repository, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { var existing = await repository.GetByIdAsync(id, cancellationToken); @@ -335,7 +339,7 @@ public static class ExceptionEndpoints { Version = existing.Version + 1, Status = ExceptionStatus.Active, - UpdatedAt = DateTimeOffset.UtcNow + UpdatedAt = timeProvider.GetUtcNow() }; var result = await repository.UpdateAsync( @@ -350,6 +354,7 @@ public static class ExceptionEndpoints ExtendExceptionRequest request, HttpContext context, IExceptionRepository repository, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { var existing = await repository.GetByIdAsync(id, cancellationToken); @@ -384,7 +389,7 @@ public static class ExceptionEndpoints var updated = existing with { Version = existing.Version + 1, - UpdatedAt = DateTimeOffset.UtcNow, + UpdatedAt = timeProvider.GetUtcNow(), ExpiresAt = request.NewExpiresAt }; @@ -400,6 +405,7 @@ public static class ExceptionEndpoints [FromBody] RevokeExceptionRequest? request, HttpContext context, IExceptionRepository repository, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { var existing = await repository.GetByIdAsync(id, cancellationToken); @@ -425,7 +431,7 @@ public static class ExceptionEndpoints { Version = existing.Version + 1, Status = ExceptionStatus.Revoked, - UpdatedAt = DateTimeOffset.UtcNow + UpdatedAt = timeProvider.GetUtcNow() }; var result = await repository.UpdateAsync( diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs index 7bc1ae432..8b4ca55ca 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs @@ -39,6 +39,7 @@ public static class GateEndpoints IBaselineSelector baselineSelector, IGateBypassAuditor bypassAuditor, IMemoryCache cache, + [FromServices] TimeProvider timeProvider, ILogger logger, CancellationToken cancellationToken) => { @@ -79,12 +80,12 @@ public static class GateEndpoints return Results.Ok(new GateEvaluateResponse { - DecisionId = $"gate:{DateTimeOffset.UtcNow:yyyyMMddHHmmss}:{Guid.NewGuid():N}", + DecisionId = $"gate:{timeProvider.GetUtcNow():yyyyMMddHHmmss}:{Guid.NewGuid():N}", Status = GateStatus.Pass, ExitCode = GateExitCodes.Pass, ImageDigest = request.ImageDigest, BaselineRef = request.BaselineRef, - DecidedAt = DateTimeOffset.UtcNow, + DecidedAt = timeProvider.GetUtcNow(), Summary = "First build - no baseline for comparison", Advisory = "This appears to be a first build. Future builds will be compared against this baseline." }); @@ -224,7 +225,8 @@ public static class GateEndpoints .WithDescription("Retrieve a previous gate evaluation decision by ID"); // GET /api/v1/policy/gate/health - Health check for gate service - gates.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTimeOffset.UtcNow })) + gates.MapGet("/health", ([FromServices] TimeProvider timeProvider) => + Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() })) .WithName("GateHealth") .WithDescription("Health check for the gate evaluation service"); } diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs index 063d66f15..3aa7df6a6 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs @@ -100,6 +100,7 @@ public static class GovernanceEndpoints private static Task GetSealedModeStatusAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, [FromQuery] string? tenantId) { var tenant = tenantId ?? GetTenantId(httpContext) ?? "default"; @@ -118,7 +119,7 @@ public static class GovernanceEndpoints .Select(MapOverrideToResponse) .ToList(), VerificationStatus = "verified", - LastVerifiedAt = DateTimeOffset.UtcNow.ToString("O") + LastVerifiedAt = timeProvider.GetUtcNow().ToString("O") }; return Task.FromResult(Results.Ok(response)); @@ -140,11 +141,12 @@ public static class GovernanceEndpoints private static Task ToggleSealedModeAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, SealedModeToggleRequest request) { var tenant = GetTenantId(httpContext) ?? "default"; var actor = GetActorId(httpContext) ?? "system"; - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState()); @@ -173,7 +175,7 @@ public static class GovernanceEndpoints // Audit RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config", - $"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}"); + $"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider); var response = new SealedModeStatusResponse { @@ -193,11 +195,12 @@ public static class GovernanceEndpoints private static Task CreateSealedModeOverrideAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, SealedModeOverrideRequest request) { var tenant = GetTenantId(httpContext) ?? "default"; var actor = GetActorId(httpContext) ?? "system"; - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); var overrideId = $"override-{Guid.NewGuid():N}"; var entity = new SealedModeOverrideEntity @@ -217,13 +220,14 @@ public static class GovernanceEndpoints Overrides[overrideId] = entity; RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override", - $"Created override for {request.Target}: {request.Reason}"); + $"Created override for {request.Target}: {request.Reason}", timeProvider); return Task.FromResult(Results.Ok(MapOverrideToResponse(entity))); } private static Task RevokeSealedModeOverrideAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, string overrideId, RevokeOverrideRequest request) { @@ -243,7 +247,7 @@ public static class GovernanceEndpoints Overrides[overrideId] = entity; RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override", - $"Revoked override: {request.Reason}"); + $"Revoked override: {request.Reason}", timeProvider); return Task.FromResult(Results.NoContent()); } @@ -289,11 +293,12 @@ public static class GovernanceEndpoints private static Task CreateRiskProfileAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, CreateRiskProfileRequest request) { var tenant = GetTenantId(httpContext) ?? "default"; var actor = GetActorId(httpContext) ?? "system"; - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); var profileId = $"profile-{Guid.NewGuid():N}"; var entity = new RiskProfileEntity @@ -317,19 +322,20 @@ public static class GovernanceEndpoints RiskProfiles[profileId] = entity; RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile", - $"Created risk profile: {request.Name}"); + $"Created risk profile: {request.Name}", timeProvider); return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity))); } private static Task UpdateRiskProfileAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, string profileId, UpdateRiskProfileRequest request) { var tenant = GetTenantId(httpContext) ?? "default"; var actor = GetActorId(httpContext) ?? "system"; - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); if (!RiskProfiles.TryGetValue(profileId, out var existing)) { @@ -354,13 +360,14 @@ public static class GovernanceEndpoints RiskProfiles[profileId] = entity; RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile", - $"Updated risk profile: {entity.Name}"); + $"Updated risk profile: {entity.Name}", timeProvider); return Task.FromResult(Results.Ok(MapProfileToResponse(entity))); } private static Task DeleteRiskProfileAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, string profileId) { var tenant = GetTenantId(httpContext) ?? "default"; @@ -376,18 +383,19 @@ public static class GovernanceEndpoints } RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile", - $"Deleted risk profile: {removed.Name}"); + $"Deleted risk profile: {removed.Name}", timeProvider); return Task.FromResult(Results.NoContent()); } private static Task ActivateRiskProfileAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, string profileId) { var tenant = GetTenantId(httpContext) ?? "default"; var actor = GetActorId(httpContext) ?? "system"; - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); if (!RiskProfiles.TryGetValue(profileId, out var existing)) { @@ -408,19 +416,20 @@ public static class GovernanceEndpoints RiskProfiles[profileId] = entity; RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile", - $"Activated risk profile: {entity.Name}"); + $"Activated risk profile: {entity.Name}", timeProvider); return Task.FromResult(Results.Ok(MapProfileToResponse(entity))); } private static Task DeprecateRiskProfileAsync( HttpContext httpContext, + [FromServices] TimeProvider timeProvider, string profileId, DeprecateProfileRequest request) { var tenant = GetTenantId(httpContext) ?? "default"; var actor = GetActorId(httpContext) ?? "system"; - var now = DateTimeOffset.UtcNow; + var now = timeProvider.GetUtcNow(); if (!RiskProfiles.TryGetValue(profileId, out var existing)) { @@ -442,7 +451,7 @@ public static class GovernanceEndpoints RiskProfiles[profileId] = entity; RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile", - $"Deprecated risk profile: {entity.Name} - {request.Reason}"); + $"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider); return Task.FromResult(Results.Ok(MapProfileToResponse(entity))); } @@ -582,7 +591,7 @@ public static class GovernanceEndpoints ?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault(); } - private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary) + private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider) { var id = $"audit-{Guid.NewGuid():N}"; AuditEntries[id] = new GovernanceAuditEntry @@ -590,7 +599,7 @@ public static class GovernanceEndpoints Id = id, TenantId = tenantId, Type = eventType, - Timestamp = DateTimeOffset.UtcNow.ToString("O"), + Timestamp = timeProvider.GetUtcNow().ToString("O"), Actor = actor, ActorType = "user", TargetResource = targetId, diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs index 40d785545..c0cb2fe84 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs @@ -50,6 +50,7 @@ internal static class RegistryWebhookEndpoints private static async Task, ProblemHttpResult>> HandleDockerRegistryWebhook( [FromBody] DockerRegistryNotification notification, IGateEvaluationQueue evaluationQueue, + [FromServices] TimeProvider timeProvider, ILogger logger, CancellationToken ct) { @@ -77,7 +78,7 @@ internal static class RegistryWebhookEndpoints Tag = evt.Target.Tag, RegistryUrl = evt.Request?.Host, Source = "docker-registry", - Timestamp = evt.Timestamp ?? DateTimeOffset.UtcNow + Timestamp = evt.Timestamp ?? timeProvider.GetUtcNow() }, ct); jobs.Add(jobId); @@ -100,6 +101,7 @@ internal static class RegistryWebhookEndpoints private static async Task, ProblemHttpResult>> HandleHarborWebhook( [FromBody] HarborWebhookEvent notification, IGateEvaluationQueue evaluationQueue, + [FromServices] TimeProvider timeProvider, ILogger logger, CancellationToken ct) { @@ -136,7 +138,7 @@ internal static class RegistryWebhookEndpoints Tag = resource.Tag, RegistryUrl = notification.EventData.Repository?.RepoFullName, Source = "harbor", - Timestamp = notification.OccurAt ?? DateTimeOffset.UtcNow + Timestamp = notification.OccurAt ?? timeProvider.GetUtcNow() }, ct); jobs.Add(jobId); @@ -159,6 +161,7 @@ internal static class RegistryWebhookEndpoints private static async Task, ProblemHttpResult>> HandleGenericWebhook( [FromBody] GenericRegistryWebhook notification, IGateEvaluationQueue evaluationQueue, + [FromServices] TimeProvider timeProvider, ILogger logger, CancellationToken ct) { @@ -177,7 +180,7 @@ internal static class RegistryWebhookEndpoints RegistryUrl = notification.RegistryUrl, BaselineRef = notification.BaselineRef, Source = notification.Source ?? "generic", - Timestamp = DateTimeOffset.UtcNow + Timestamp = timeProvider.GetUtcNow() }, ct); logger.LogInformation( diff --git a/src/Policy/StellaOps.Policy.Gateway/Services/InMemoryGateEvaluationQueue.cs b/src/Policy/StellaOps.Policy.Gateway/Services/InMemoryGateEvaluationQueue.cs index 81b00a521..77aff90be 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Services/InMemoryGateEvaluationQueue.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Services/InMemoryGateEvaluationQueue.cs @@ -21,11 +21,15 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue { private readonly Channel _channel; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public InMemoryGateEvaluationQueue(ILogger logger) + public InMemoryGateEvaluationQueue( + ILogger logger, + TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(logger); _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; // Bounded channel to prevent unbounded memory growth _channel = Channel.CreateBounded(new BoundedChannelOptions(1000) @@ -46,7 +50,7 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue { JobId = jobId, Request = request, - QueuedAt = DateTimeOffset.UtcNow + QueuedAt = _timeProvider.GetUtcNow() }; await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false); @@ -65,10 +69,10 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue /// public ChannelReader Reader => _channel.Reader; - private static string GenerateJobId() + private string GenerateJobId() { // Format: gate-{timestamp}-{random} - var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); var random = Guid.NewGuid().ToString("N")[..8]; return $"gate-{timestamp}-{random}"; } diff --git a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryOverrideStore.cs b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryOverrideStore.cs index 5a18d6593..ca134ee22 100644 --- a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryOverrideStore.cs +++ b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryOverrideStore.cs @@ -9,6 +9,12 @@ namespace StellaOps.Policy.Registry.Storage; public sealed class InMemoryOverrideStore : IOverrideStore { private readonly ConcurrentDictionary<(Guid TenantId, Guid OverrideId), OverrideEntity> _overrides = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryOverrideStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task CreateAsync( Guid tenantId, @@ -16,7 +22,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore string? createdBy = null, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var overrideId = Guid.NewGuid(); var entity = new OverrideEntity @@ -73,7 +79,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore return Task.FromResult(null); } - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var updated = existing with { Status = OverrideStatus.Approved, diff --git a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryPolicyPackStore.cs b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryPolicyPackStore.cs index 010f6121a..2b3ee50b5 100644 --- a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryPolicyPackStore.cs +++ b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryPolicyPackStore.cs @@ -13,6 +13,12 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore { private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackEntity> _packs = new(); private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), List> _history = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryPolicyPackStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task CreateAsync( Guid tenantId, @@ -20,7 +26,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore string? createdBy = null, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var packId = Guid.NewGuid(); var entity = new PolicyPackEntity @@ -130,7 +136,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore Description = request.Description ?? existing.Description, Rules = request.Rules ?? existing.Rules, Metadata = request.Metadata ?? existing.Metadata, - UpdatedAt = DateTimeOffset.UtcNow, + UpdatedAt = _timeProvider.GetUtcNow(), UpdatedBy = updatedBy }; @@ -178,7 +184,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore return Task.FromResult(null); } - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var updated = existing with { Status = newStatus, @@ -228,7 +234,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore { PackId = packId, Action = action, - Timestamp = DateTimeOffset.UtcNow, + Timestamp = _timeProvider.GetUtcNow(), PerformedBy = performedBy, PreviousStatus = previousStatus, NewStatus = newStatus, diff --git a/src/Policy/StellaOps.Policy.Registry/Storage/InMemorySnapshotStore.cs b/src/Policy/StellaOps.Policy.Registry/Storage/InMemorySnapshotStore.cs index 9dfe1af7b..5e47b7c0e 100644 --- a/src/Policy/StellaOps.Policy.Registry/Storage/InMemorySnapshotStore.cs +++ b/src/Policy/StellaOps.Policy.Registry/Storage/InMemorySnapshotStore.cs @@ -12,6 +12,12 @@ namespace StellaOps.Policy.Registry.Storage; public sealed class InMemorySnapshotStore : ISnapshotStore { private readonly ConcurrentDictionary<(Guid TenantId, Guid SnapshotId), SnapshotEntity> _snapshots = new(); + private readonly TimeProvider _timeProvider; + + public InMemorySnapshotStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task CreateAsync( Guid tenantId, @@ -19,7 +25,7 @@ public sealed class InMemorySnapshotStore : ISnapshotStore string? createdBy = null, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var snapshotId = Guid.NewGuid(); // Compute digest from pack IDs and timestamp for uniqueness diff --git a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryVerificationPolicyStore.cs b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryVerificationPolicyStore.cs index da26a4bab..3824280e0 100644 --- a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryVerificationPolicyStore.cs +++ b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryVerificationPolicyStore.cs @@ -9,6 +9,12 @@ namespace StellaOps.Policy.Registry.Storage; public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore { private readonly ConcurrentDictionary<(Guid TenantId, string PolicyId), VerificationPolicyEntity> _policies = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryVerificationPolicyStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task CreateAsync( Guid tenantId, @@ -16,7 +22,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore string? createdBy = null, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var entity = new VerificationPolicyEntity { @@ -102,7 +108,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements, ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow, Metadata = request.Metadata ?? existing.Metadata, - UpdatedAt = DateTimeOffset.UtcNow, + UpdatedAt = _timeProvider.GetUtcNow(), UpdatedBy = updatedBy }; diff --git a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryViolationStore.cs b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryViolationStore.cs index 45e576e10..6889b3b90 100644 --- a/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryViolationStore.cs +++ b/src/Policy/StellaOps.Policy.Registry/Storage/InMemoryViolationStore.cs @@ -9,13 +9,19 @@ namespace StellaOps.Policy.Registry.Storage; public sealed class InMemoryViolationStore : IViolationStore { private readonly ConcurrentDictionary<(Guid TenantId, Guid ViolationId), ViolationEntity> _violations = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryViolationStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task AppendAsync( Guid tenantId, CreateViolationRequest request, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var violationId = Guid.NewGuid(); var entity = new ViolationEntity @@ -42,7 +48,7 @@ public sealed class InMemoryViolationStore : IViolationStore IReadOnlyList requests, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); int created = 0; int failed = 0; var errors = new List(); diff --git a/src/Policy/__Tests/StellaOps.Policy.Persistence.Tests/UnknownsRepositoryTests.cs b/src/Policy/__Tests/StellaOps.Policy.Persistence.Tests/UnknownsRepositoryTests.cs index 700bb94c1..2e695ca76 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Persistence.Tests/UnknownsRepositoryTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Persistence.Tests/UnknownsRepositoryTests.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.Policy.Persistence; using StellaOps.Policy.Persistence.Postgres; using StellaOps.Policy.Unknowns.Models; @@ -35,7 +36,7 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime public async Task CreateAndGetById_RoundTripsReasonCodeAndEvidence() { await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString()); - var repository = new UnknownsRepository(connection); + var repository = new UnknownsRepository(connection, TimeProvider.System, SystemGuidProvider.Instance); var now = new DateTimeOffset(2025, 1, 2, 3, 4, 5, TimeSpan.Zero); var unknown = CreateUnknown( @@ -65,7 +66,7 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime public async Task UpdateAsync_PersistsReasonCodeAndAssumptions() { await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString()); - var repository = new UnknownsRepository(connection); + var repository = new UnknownsRepository(connection, TimeProvider.System, SystemGuidProvider.Instance); var now = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero); var unknown = CreateUnknown( diff --git a/src/Policy/__Tests/StellaOps.Policy.Tests/PolicyPreviewServiceTests.cs b/src/Policy/__Tests/StellaOps.Policy.Tests/PolicyPreviewServiceTests.cs index 5bd7f70a9..b9bf0d47d 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Tests/PolicyPreviewServiceTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Tests/PolicyPreviewServiceTests.cs @@ -33,7 +33,7 @@ rules: var snapshotRepo = new InMemoryPolicySnapshotRepository(); var auditRepo = new InMemoryPolicyAuditRepository(); var timeProvider = new FakeTimeProvider(); - var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger.Instance); + var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger.Instance); await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None); @@ -80,7 +80,7 @@ rules: var snapshotRepo = new InMemoryPolicySnapshotRepository(); var auditRepo = new InMemoryPolicyAuditRepository(); - var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger.Instance); + var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger.Instance); var service = new PolicyPreviewService(store, NullLogger.Instance); var findings = ImmutableArray.Create( @@ -111,7 +111,7 @@ rules: { var snapshotRepo = new InMemoryPolicySnapshotRepository(); var auditRepo = new InMemoryPolicyAuditRepository(); - var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger.Instance); + var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger.Instance); var service = new PolicyPreviewService(store, NullLogger.Instance); const string invalid = "version: 1.0"; @@ -159,7 +159,7 @@ rules: Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet")); Assert.True(binding.Document.Rules[0].Action.Quiet); - var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger.Instance); + var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, null, NullLogger.Instance); await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None); var snapshot = await store.GetLatestAsync(); Assert.NotNull(snapshot); diff --git a/src/Policy/__Tests/StellaOps.Policy.Tests/PolicySnapshotStoreTests.cs b/src/Policy/__Tests/StellaOps.Policy.Tests/PolicySnapshotStoreTests.cs index aa2ad0927..d03859634 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Tests/PolicySnapshotStoreTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Tests/PolicySnapshotStoreTests.cs @@ -25,7 +25,7 @@ rules: var snapshotRepo = new InMemoryPolicySnapshotRepository(); var auditRepo = new InMemoryPolicyAuditRepository(); var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero)); - var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger.Instance); + var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger.Instance); var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null); @@ -56,7 +56,7 @@ rules: var snapshotRepo = new InMemoryPolicySnapshotRepository(); var auditRepo = new InMemoryPolicyAuditRepository(); var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero)); - var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger.Instance); + var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger.Instance); var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null); var first = await store.SaveAsync(content, CancellationToken.None); @@ -81,7 +81,7 @@ rules: { var snapshotRepo = new InMemoryPolicySnapshotRepository(); var auditRepo = new InMemoryPolicyAuditRepository(); - var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger.Instance); + var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger.Instance); const string invalidYaml = "version: '1.0'\nrules: []"; var content = new PolicySnapshotContent(invalidYaml, PolicyDocumentFormat.Yaml, null, null, null); diff --git a/src/Policy/__Tests/StellaOps.Policy.Tests/Replay/ReplayEngineTests.cs b/src/Policy/__Tests/StellaOps.Policy.Tests/Replay/ReplayEngineTests.cs index bc4627b88..085777574 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Tests/Replay/ReplayEngineTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Tests/Replay/ReplayEngineTests.cs @@ -32,6 +32,7 @@ public sealed class ReplayEngineTests _snapshotService, sourceResolver, verdictComparer, + null, NullLogger.Instance); } diff --git a/src/SbomService/StellaOps.SbomService.Tests/EntrypointEndpointsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/EntrypointEndpointsTests.cs index 29dbb09e0..4e6b81e5b 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/EntrypointEndpointsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/EntrypointEndpointsTests.cs @@ -7,11 +7,11 @@ using StellaOps.SbomService.Models; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class EntrypointEndpointsTests : IClassFixture> +public class EntrypointEndpointsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public EntrypointEndpointsTests(WebApplicationFactory factory) + public EntrypointEndpointsTests(WebApplicationFactory factory) { _factory = factory; } diff --git a/src/SbomService/StellaOps.SbomService.Tests/OrchestratorEndpointsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/OrchestratorEndpointsTests.cs index 43ea22ddf..0d8e9d441 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/OrchestratorEndpointsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/OrchestratorEndpointsTests.cs @@ -9,11 +9,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class OrchestratorEndpointsTests : IClassFixture> +public class OrchestratorEndpointsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public OrchestratorEndpointsTests(WebApplicationFactory factory) + public OrchestratorEndpointsTests(WebApplicationFactory factory) { _factory = factory; } diff --git a/src/SbomService/StellaOps.SbomService.Tests/ProjectionEndpointTests.cs b/src/SbomService/StellaOps.SbomService.Tests/ProjectionEndpointTests.cs index 14d3a608c..526a63278 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/ProjectionEndpointTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/ProjectionEndpointTests.cs @@ -13,11 +13,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class ProjectionEndpointTests : IClassFixture> +public class ProjectionEndpointTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public ProjectionEndpointTests(WebApplicationFactory factory) + public ProjectionEndpointTests(WebApplicationFactory factory) { var contentRoot = ResolveContentRoot(); _factory = factory.WithWebHostBuilder(builder => diff --git a/src/SbomService/StellaOps.SbomService.Tests/ResolverFeedExportTests.cs b/src/SbomService/StellaOps.SbomService.Tests/ResolverFeedExportTests.cs index 9c2835bca..1693bc8a3 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/ResolverFeedExportTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/ResolverFeedExportTests.cs @@ -8,11 +8,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class ResolverFeedExportTests : IClassFixture> +public class ResolverFeedExportTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public ResolverFeedExportTests(WebApplicationFactory factory) + public ResolverFeedExportTests(WebApplicationFactory factory) { _factory = factory; } diff --git a/src/SbomService/StellaOps.SbomService.Tests/SbomAssetEventsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/SbomAssetEventsTests.cs index bb658b479..aab0558bf 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/SbomAssetEventsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/SbomAssetEventsTests.cs @@ -8,11 +8,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class SbomAssetEventsTests : IClassFixture> +public class SbomAssetEventsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public SbomAssetEventsTests(WebApplicationFactory factory) + public SbomAssetEventsTests(WebApplicationFactory factory) { _factory = factory; } diff --git a/src/SbomService/StellaOps.SbomService.Tests/SbomEndpointsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/SbomEndpointsTests.cs index dc5761830..4c96cfa6e 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/SbomEndpointsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/SbomEndpointsTests.cs @@ -8,11 +8,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class SbomEndpointsTests : IClassFixture> +public class SbomEndpointsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public SbomEndpointsTests(WebApplicationFactory factory) + public SbomEndpointsTests(WebApplicationFactory factory) { _factory = factory.WithWebHostBuilder(_ => { }); } diff --git a/src/SbomService/StellaOps.SbomService.Tests/SbomEventEndpointsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/SbomEventEndpointsTests.cs index a76c490dd..e14deee31 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/SbomEventEndpointsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/SbomEventEndpointsTests.cs @@ -9,11 +9,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class SbomEventEndpointsTests : IClassFixture> +public class SbomEventEndpointsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public SbomEventEndpointsTests(WebApplicationFactory factory) + public SbomEventEndpointsTests(WebApplicationFactory factory) { _factory = factory.WithWebHostBuilder(_ => { }); } diff --git a/src/SbomService/StellaOps.SbomService.Tests/SbomInventoryEventsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/SbomInventoryEventsTests.cs index 96eb15959..a8cdbc418 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/SbomInventoryEventsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/SbomInventoryEventsTests.cs @@ -8,11 +8,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public class SbomInventoryEventsTests : IClassFixture> +public class SbomInventoryEventsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public SbomInventoryEventsTests(WebApplicationFactory factory) + public SbomInventoryEventsTests(WebApplicationFactory factory) { _factory = factory; } diff --git a/src/SbomService/StellaOps.SbomService.Tests/SbomLedgerEndpointsTests.cs b/src/SbomService/StellaOps.SbomService.Tests/SbomLedgerEndpointsTests.cs index d10299f6d..07634c951 100644 --- a/src/SbomService/StellaOps.SbomService.Tests/SbomLedgerEndpointsTests.cs +++ b/src/SbomService/StellaOps.SbomService.Tests/SbomLedgerEndpointsTests.cs @@ -10,11 +10,11 @@ using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; -public sealed class SbomLedgerEndpointsTests : IClassFixture> +public sealed class SbomLedgerEndpointsTests : IClassFixture> { - private readonly WebApplicationFactory _factory; + private readonly WebApplicationFactory _factory; - public SbomLedgerEndpointsTests(WebApplicationFactory factory) + public SbomLedgerEndpointsTests(WebApplicationFactory factory) { _factory = factory.WithWebHostBuilder(_ => { }); } diff --git a/src/SbomService/StellaOps.SbomService/Program.cs b/src/SbomService/StellaOps.SbomService/Program.cs index c66f90e08..eee5137bb 100644 --- a/src/SbomService/StellaOps.SbomService/Program.cs +++ b/src/SbomService/StellaOps.SbomService/Program.cs @@ -1311,5 +1311,8 @@ app.MapPost("/internal/orchestrator/watermarks", async Task ( app.Run(); -// Program class public for WebApplicationFactory -public partial class Program; +// Program class in namespace to avoid conflicts with other assemblies +namespace StellaOps.SbomService +{ + public partial class Program; +} diff --git a/src/SbomService/StellaOps.SbomService/Repositories/InMemoryOrchestratorRepository.cs b/src/SbomService/StellaOps.SbomService/Repositories/InMemoryOrchestratorRepository.cs index 4c310f8cb..529190d68 100644 --- a/src/SbomService/StellaOps.SbomService/Repositories/InMemoryOrchestratorRepository.cs +++ b/src/SbomService/StellaOps.SbomService/Repositories/InMemoryOrchestratorRepository.cs @@ -6,9 +6,11 @@ namespace StellaOps.SbomService.Repositories; internal sealed class InMemoryOrchestratorRepository : IOrchestratorRepository { private readonly ConcurrentDictionary> _sources = new(StringComparer.Ordinal); + private readonly TimeProvider _timeProvider; - public InMemoryOrchestratorRepository() + public InMemoryOrchestratorRepository(TimeProvider? timeProvider = null) { + _timeProvider = timeProvider ?? TimeProvider.System; Seed(); } @@ -37,7 +39,7 @@ internal sealed class InMemoryOrchestratorRepository : IOrchestratorRepository sourceId, request.ArtifactDigest.Trim(), request.SourceType.Trim(), - DateTimeOffset.UtcNow, + _timeProvider.GetUtcNow(), request.Metadata.Trim()); // Idempotent on (tenant, artifactDigest, sourceType) diff --git a/src/SbomService/StellaOps.SbomService/Services/Clock.cs b/src/SbomService/StellaOps.SbomService/Services/Clock.cs index 2fcc5bfdd..ca0ec7ca2 100644 --- a/src/SbomService/StellaOps.SbomService/Services/Clock.cs +++ b/src/SbomService/StellaOps.SbomService/Services/Clock.cs @@ -1,13 +1,28 @@ -using System; - namespace StellaOps.SbomService.Services; +/// +/// Provides the current time abstraction - delegates to TimeProvider. +/// +/// +/// Deprecated: Prefer injecting TimeProvider directly. This interface is kept +/// for backward compatibility during migration. +/// public interface IClock { DateTimeOffset UtcNow { get; } } +/// +/// Default IClock implementation that delegates to TimeProvider. +/// public sealed class SystemClock : IClock { - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + private readonly TimeProvider _timeProvider; + + public SystemClock(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } + + public DateTimeOffset UtcNow => _timeProvider.GetUtcNow(); } diff --git a/src/SbomService/StellaOps.SbomService/Services/IReplayVerificationService.cs b/src/SbomService/StellaOps.SbomService/Services/IReplayVerificationService.cs index 0499e0400..9d635043b 100644 --- a/src/SbomService/StellaOps.SbomService/Services/IReplayVerificationService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/IReplayVerificationService.cs @@ -136,9 +136,9 @@ public sealed record ReplayVerificationResult public ImmutableArray Drifts { get; init; } = ImmutableArray.Empty; /// - /// When the verification was performed. + /// When the verification was performed. Must be explicitly set by calling code. /// - public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow; + public required DateTimeOffset VerifiedAt { get; init; } /// /// Optional message with additional context. @@ -249,7 +249,7 @@ public sealed record ReplayDriftAnalysis public required string DriftSummary { get; init; } /// - /// When the analysis was performed. + /// When the analysis was performed. Must be explicitly set by calling code. /// - public DateTimeOffset AnalyzedAt { get; init; } = DateTimeOffset.UtcNow; + public required DateTimeOffset AnalyzedAt { get; init; } } diff --git a/src/SbomService/StellaOps.SbomService/Services/LineageCompareService.cs b/src/SbomService/StellaOps.SbomService/Services/LineageCompareService.cs index 927ba39a1..9dbd3437c 100644 --- a/src/SbomService/StellaOps.SbomService/Services/LineageCompareService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/LineageCompareService.cs @@ -26,19 +26,22 @@ internal sealed class LineageCompareService : ILineageCompareService private readonly IVexDeltaRepository? _vexDeltaRepository; private readonly ILineageCompareCache? _cache; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public LineageCompareService( ISbomLineageGraphService lineageService, ISbomLedgerService ledgerService, ILogger logger, IVexDeltaRepository? vexDeltaRepository = null, - ILineageCompareCache? cache = null) + ILineageCompareCache? cache = null, + TimeProvider? timeProvider = null) { _lineageService = lineageService ?? throw new ArgumentNullException(nameof(lineageService)); _ledgerService = ledgerService ?? throw new ArgumentNullException(nameof(ledgerService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _vexDeltaRepository = vexDeltaRepository; _cache = cache; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -139,7 +142,7 @@ internal sealed class LineageCompareService : ILineageCompareService FromDigest = fromDigest, ToDigest = toDigest, TenantId = tenantId, - ComputedAt = DateTimeOffset.UtcNow, + ComputedAt = _timeProvider.GetUtcNow(), FromArtifact = fromArtifact, ToArtifact = toArtifact, Summary = summary, diff --git a/src/SbomService/StellaOps.SbomService/Services/LineageExportService.cs b/src/SbomService/StellaOps.SbomService/Services/LineageExportService.cs index f4d5e034b..a8f4f9b60 100644 --- a/src/SbomService/StellaOps.SbomService/Services/LineageExportService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/LineageExportService.cs @@ -21,16 +21,19 @@ internal sealed class LineageExportService : ILineageExportService private readonly ISbomLineageGraphService _lineageService; private readonly IReplayHashService? _replayHashService; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private const long MaxExportSizeBytes = 50 * 1024 * 1024; // 50MB limit public LineageExportService( ISbomLineageGraphService lineageService, ILogger logger, - IReplayHashService? replayHashService = null) + IReplayHashService? replayHashService = null, + TimeProvider? timeProvider = null) { _lineageService = lineageService; _logger = logger; _replayHashService = replayHashService; + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task ExportAsync( @@ -59,7 +62,7 @@ internal sealed class LineageExportService : ILineageExportService Version = "1.0", FromDigest = request.FromDigest, ToDigest = request.ToDigest, - GeneratedAt = DateTimeOffset.UtcNow, + GeneratedAt = _timeProvider.GetUtcNow(), ReplayHash = diff.ReplayHash ?? ComputeFallbackHash(request.FromDigest, request.ToDigest), SbomDiff = request.IncludeSbomDiff ? diff.SbomDiff?.Summary : null, VexDeltas = request.IncludeVexDeltas ? diff.VexDiff : null, @@ -91,7 +94,7 @@ internal sealed class LineageExportService : ILineageExportService // Generate export ID and URL var exportId = Guid.NewGuid().ToString("N"); var downloadUrl = $"/api/v1/lineage/export/{exportId}/download"; - var expiresAt = DateTimeOffset.UtcNow.AddHours(24); + var expiresAt = _timeProvider.GetUtcNow().AddHours(24); // TODO: Store evidence pack for retrieval (file system, blob storage, etc.) // For now, return metadata only @@ -114,9 +117,9 @@ internal sealed class LineageExportService : ILineageExportService }; } - private static string ComputeFallbackHash(string fromDigest, string toDigest) + private string ComputeFallbackHash(string fromDigest, string toDigest) { - var input = $"{fromDigest}:{toDigest}:{DateTimeOffset.UtcNow:O}"; + var input = $"{fromDigest}:{toDigest}:{_timeProvider.GetUtcNow():O}"; var bytes = Encoding.UTF8.GetBytes(input); var hashBytes = SHA256.HashData(bytes); return Convert.ToHexString(hashBytes).ToLowerInvariant(); diff --git a/src/SbomService/StellaOps.SbomService/Services/LineageHoverCache.cs b/src/SbomService/StellaOps.SbomService/Services/LineageHoverCache.cs index b4c45d95f..8ca12c2f9 100644 --- a/src/SbomService/StellaOps.SbomService/Services/LineageHoverCache.cs +++ b/src/SbomService/StellaOps.SbomService/Services/LineageHoverCache.cs @@ -221,11 +221,13 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache { private readonly Dictionary _cache = new(); private readonly LineageHoverCacheOptions _options; + private readonly TimeProvider _timeProvider; private readonly object _lock = new(); - public InMemoryLineageHoverCache(LineageHoverCacheOptions? options = null) + public InMemoryLineageHoverCache(LineageHoverCacheOptions? options = null, TimeProvider? timeProvider = null) { _options = options ?? new LineageHoverCacheOptions(); + _timeProvider = timeProvider ?? TimeProvider.System; } public Task GetAsync(string fromDigest, string toDigest, string tenantId, CancellationToken ct = default) @@ -240,7 +242,7 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache { if (_cache.TryGetValue(key, out var entry)) { - if (entry.ExpiresAt > DateTimeOffset.UtcNow) + if (entry.ExpiresAt > _timeProvider.GetUtcNow()) { return Task.FromResult(entry.Card); } @@ -260,7 +262,7 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache } var key = BuildKey(fromDigest, toDigest, tenantId); - var expiresAt = DateTimeOffset.UtcNow.Add(_options.Ttl); + var expiresAt = _timeProvider.GetUtcNow().Add(_options.Ttl); lock (_lock) { diff --git a/src/SbomService/StellaOps.SbomService/Services/OrchestratorControlService.cs b/src/SbomService/StellaOps.SbomService/Services/OrchestratorControlService.cs index 054358593..bf51ef006 100644 --- a/src/SbomService/StellaOps.SbomService/Services/OrchestratorControlService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/OrchestratorControlService.cs @@ -11,8 +11,8 @@ public sealed record OrchestratorControlState( string Backpressure, DateTimeOffset UpdatedAtUtc) { - public static OrchestratorControlState Default(string tenantId) => - new(tenantId, false, 0, "normal", DateTimeOffset.UtcNow); + public static OrchestratorControlState Default(string tenantId, TimeProvider? timeProvider = null) => + new(tenantId, false, 0, "normal", (timeProvider ?? TimeProvider.System).GetUtcNow()); } public sealed record OrchestratorControlRequest( @@ -34,13 +34,18 @@ internal sealed class OrchestratorControlService : IOrchestratorControlService private readonly Counter _controlUpdates; private readonly ObservableGauge _throttleGauge; private readonly ObservableGauge _pausedGauge; + private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); - public OrchestratorControlService(IOrchestratorControlRepository repository, Meter meter) + public OrchestratorControlService( + IOrchestratorControlRepository repository, + Meter meter, + TimeProvider? timeProvider = null) { _repository = repository; _meter = meter; + _timeProvider = timeProvider ?? TimeProvider.System; _controlUpdates = meter.CreateCounter("sbom_orchestrator_control_updates"); _throttleGauge = meter.CreateObservableGauge("sbom_orchestrator_throttle_percent", ObserveThrottle); _pausedGauge = meter.CreateObservableGauge("sbom_orchestrator_paused", ObservePaused); @@ -66,7 +71,7 @@ internal sealed class OrchestratorControlService : IOrchestratorControlService Paused: request.Paused ?? current.Paused, ThrottlePercent: throttle, Backpressure: string.IsNullOrWhiteSpace(request.Backpressure) ? current.Backpressure : request.Backpressure!.Trim().ToLowerInvariant(), - UpdatedAtUtc: DateTimeOffset.UtcNow); + UpdatedAtUtc: _timeProvider.GetUtcNow()); await _repository.SetAsync(updated, cancellationToken); _cache[updated.TenantId] = updated; diff --git a/src/SbomService/StellaOps.SbomService/Services/RegistrySourceService.cs b/src/SbomService/StellaOps.SbomService/Services/RegistrySourceService.cs index 07e157474..dbb0c34fa 100644 --- a/src/SbomService/StellaOps.SbomService/Services/RegistrySourceService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/RegistrySourceService.cs @@ -30,15 +30,18 @@ public sealed class RegistrySourceService : IRegistrySourceService private readonly IRegistrySourceRepository _sourceRepository; private readonly IRegistrySourceRunRepository _runRepository; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public RegistrySourceService( IRegistrySourceRepository sourceRepository, IRegistrySourceRunRepository runRepository, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _sourceRepository = sourceRepository; _runRepository = runRepository; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -125,7 +128,7 @@ public sealed class RegistrySourceService : IRegistrySourceService if (request.Status.HasValue) source.Status = request.Status.Value; if (request.Tags is not null) source.Tags = request.Tags.ToList(); - source.UpdatedAt = DateTimeOffset.UtcNow; + source.UpdatedAt = _timeProvider.GetUtcNow(); source.UpdatedBy = userId; var updated = await _sourceRepository.UpdateAsync(source, cancellationToken); @@ -156,23 +159,23 @@ public sealed class RegistrySourceService : IRegistrySourceService var source = await _sourceRepository.GetByIdAsync(id, cancellationToken); if (source is null) { - return new TestRegistrySourceResponse(id, false, "Registry source not found", null, TimeSpan.Zero, DateTimeOffset.UtcNow); + return new TestRegistrySourceResponse(id, false, "Registry source not found", null, TimeSpan.Zero, _timeProvider.GetUtcNow()); } - var startTime = DateTimeOffset.UtcNow; + var startTime = _timeProvider.GetUtcNow(); // TODO: Implement actual registry connection test // For now, simulate a successful test await Task.Delay(100, cancellationToken); - var duration = DateTimeOffset.UtcNow - startTime; + var duration = _timeProvider.GetUtcNow() - startTime; // Update source status var newStatus = RegistrySourceStatus.Active; if (source.Status != newStatus) { source.Status = newStatus; - source.UpdatedAt = DateTimeOffset.UtcNow; + source.UpdatedAt = _timeProvider.GetUtcNow(); await _sourceRepository.UpdateAsync(source, cancellationToken); } @@ -188,7 +191,7 @@ public sealed class RegistrySourceService : IRegistrySourceService ["type"] = source.Type.ToString() }, duration, - DateTimeOffset.UtcNow); + _timeProvider.GetUtcNow()); } /// @@ -209,7 +212,7 @@ public sealed class RegistrySourceService : IRegistrySourceService Status = RegistryRunStatus.Queued, TriggerType = triggerType, TriggerMetadata = triggerMetadata, - StartedAt = DateTimeOffset.UtcNow + StartedAt = _timeProvider.GetUtcNow() }; var created = await _runRepository.CreateAsync(run, cancellationToken); @@ -229,7 +232,7 @@ public sealed class RegistrySourceService : IRegistrySourceService if (source is null) return null; source.Status = RegistrySourceStatus.Paused; - source.UpdatedAt = DateTimeOffset.UtcNow; + source.UpdatedAt = _timeProvider.GetUtcNow(); source.UpdatedBy = userId; var updated = await _sourceRepository.UpdateAsync(source, cancellationToken); @@ -252,7 +255,7 @@ public sealed class RegistrySourceService : IRegistrySourceService } source.Status = RegistrySourceStatus.Active; - source.UpdatedAt = DateTimeOffset.UtcNow; + source.UpdatedAt = _timeProvider.GetUtcNow(); source.UpdatedBy = userId; var updated = await _sourceRepository.UpdateAsync(source, cancellationToken); diff --git a/src/SbomService/StellaOps.SbomService/Services/ReplayVerificationService.cs b/src/SbomService/StellaOps.SbomService/Services/ReplayVerificationService.cs index 1276e7d5e..d7386a527 100644 --- a/src/SbomService/StellaOps.SbomService/Services/ReplayVerificationService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/ReplayVerificationService.cs @@ -74,6 +74,7 @@ internal sealed class ReplayVerificationService : IReplayVerificationService ExpectedHash = request.ReplayHash, ComputedHash = string.Empty, Status = ReplayVerificationStatus.InputsNotFound, + VerifiedAt = _clock.UtcNow, Error = "Unable to determine verification inputs. Provide explicit inputs or ensure hash is stored." }; } @@ -119,6 +120,7 @@ internal sealed class ReplayVerificationService : IReplayVerificationService ExpectedHash = request.ReplayHash, ComputedHash = string.Empty, Status = ReplayVerificationStatus.Error, + VerifiedAt = _clock.UtcNow, Error = ex.Message }; } diff --git a/src/SbomService/StellaOps.SbomService/Services/WatermarkService.cs b/src/SbomService/StellaOps.SbomService/Services/WatermarkService.cs index d56529294..07233dfbe 100644 --- a/src/SbomService/StellaOps.SbomService/Services/WatermarkService.cs +++ b/src/SbomService/StellaOps.SbomService/Services/WatermarkService.cs @@ -16,6 +16,12 @@ public interface IWatermarkService internal sealed class InMemoryWatermarkService : IWatermarkService { private readonly ConcurrentDictionary _watermarks = new(StringComparer.Ordinal); + private readonly TimeProvider _timeProvider; + + public InMemoryWatermarkService(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public Task GetAsync(string tenantId, CancellationToken cancellationToken) { @@ -24,14 +30,14 @@ internal sealed class InMemoryWatermarkService : IWatermarkService return Task.FromResult(state); } - var created = new WatermarkState(tenantId, string.Empty, DateTimeOffset.UtcNow); + var created = new WatermarkState(tenantId, string.Empty, _timeProvider.GetUtcNow()); _watermarks[tenantId] = created; return Task.FromResult(created); } public Task SetAsync(string tenantId, string watermark, CancellationToken cancellationToken) { - var state = new WatermarkState(tenantId, watermark, DateTimeOffset.UtcNow); + var state = new WatermarkState(tenantId, watermark, _timeProvider.GetUtcNow()); _watermarks[tenantId] = state; return Task.FromResult(state); } diff --git a/src/SbomService/StellaOps.SbomService/StellaOps.SbomService.csproj b/src/SbomService/StellaOps.SbomService/StellaOps.SbomService.csproj index d59473391..d7344d230 100644 --- a/src/SbomService/StellaOps.SbomService/StellaOps.SbomService.csproj +++ b/src/SbomService/StellaOps.SbomService/StellaOps.SbomService.csproj @@ -19,5 +19,6 @@ + diff --git a/src/SbomService/__Libraries/StellaOps.SbomService.Lineage/Repositories/SbomLineageEdgeRepository.cs b/src/SbomService/__Libraries/StellaOps.SbomService.Lineage/Repositories/SbomLineageEdgeRepository.cs index e760be260..154101906 100644 --- a/src/SbomService/__Libraries/StellaOps.SbomService.Lineage/Repositories/SbomLineageEdgeRepository.cs +++ b/src/SbomService/__Libraries/StellaOps.SbomService.Lineage/Repositories/SbomLineageEdgeRepository.cs @@ -13,12 +13,15 @@ public sealed class SbomLineageEdgeRepository : RepositoryBase logger) + ILogger logger, + TimeProvider? timeProvider = null) : base(dataSource, logger) { + _timeProvider = timeProvider ?? TimeProvider.System; } public async ValueTask GetGraphAsync( @@ -260,7 +263,7 @@ public sealed class SbomLineageEdgeRepository : RepositoryBase _logger; + private readonly TimeProvider _timeProvider; private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(10); private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); @@ -25,13 +26,15 @@ public sealed class LineageGraphService : ILineageGraphService IVexDeltaRepository deltaRepository, ISbomVerdictLinkRepository verdictRepository, ILogger logger, - IDistributedCache? cache = null) + IDistributedCache? cache = null, + TimeProvider? timeProvider = null) { _edgeRepository = edgeRepository; _deltaRepository = deltaRepository; _verdictRepository = verdictRepository; _cache = cache; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } public async ValueTask GetLineageAsync( @@ -184,7 +187,7 @@ public sealed class LineageGraphService : ILineageGraphService // Placeholder implementation var downloadUrl = $"https://evidence.stellaops.example/exports/{Guid.NewGuid()}.tar.gz"; - var expiresAt = DateTimeOffset.UtcNow.AddHours(24); + var expiresAt = _timeProvider.GetUtcNow().AddHours(24); return new ExportResult( DownloadUrl: downloadUrl, diff --git a/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Postgres/Repositories/PostgresOrchestratorRepository.cs b/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Postgres/Repositories/PostgresOrchestratorRepository.cs index ba5e0354b..8c537494a 100644 --- a/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Postgres/Repositories/PostgresOrchestratorRepository.cs +++ b/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Postgres/Repositories/PostgresOrchestratorRepository.cs @@ -11,11 +11,16 @@ namespace StellaOps.SbomService.Persistence.Postgres.Repositories; /// public sealed class PostgresOrchestratorRepository : RepositoryBase, IOrchestratorRepository { + private readonly TimeProvider _timeProvider; private bool _tableInitialized; - public PostgresOrchestratorRepository(SbomServiceDataSource dataSource, ILogger logger) + public PostgresOrchestratorRepository( + SbomServiceDataSource dataSource, + ILogger logger, + TimeProvider? timeProvider = null) : base(dataSource, logger) { + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task> ListAsync(string tenantId, CancellationToken cancellationToken) @@ -79,7 +84,7 @@ public sealed class PostgresOrchestratorRepository : RepositoryBase - /// When the edge was created. + /// When the edge was created. Must be explicitly set by calling code. /// - public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; + public required DateTimeOffset CreatedAt { get; init; } /// /// Optional metadata about the relationship. diff --git a/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Repositories/ISbomVerdictLinkRepository.cs b/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Repositories/ISbomVerdictLinkRepository.cs index 357720090..8d76054e7 100644 --- a/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Repositories/ISbomVerdictLinkRepository.cs +++ b/src/SbomService/__Libraries/StellaOps.SbomService.Persistence/Repositories/ISbomVerdictLinkRepository.cs @@ -94,9 +94,9 @@ public sealed record SbomVerdictLink public required string TenantId { get; init; } /// - /// When the link was created. + /// When the link was created. Must be explicitly set by calling code. /// - public DateTimeOffset LinkedAt { get; init; } = DateTimeOffset.UtcNow; + public required DateTimeOffset LinkedAt { get; init; } /// /// SBOM artifact digest for cross-reference. diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Advisory/AdvisoryClient.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Advisory/AdvisoryClient.cs index ea04bec0b..c2fa50a1e 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Advisory/AdvisoryClient.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Advisory/AdvisoryClient.cs @@ -44,7 +44,7 @@ public sealed class AdvisoryClient : IAdvisoryClient var normalized = cveId.Trim().ToUpperInvariant(); var cacheKey = $"advisory:cve:{normalized}"; - if (_cache.TryGetValue(cacheKey, out AdvisorySymbolMapping cached)) + if (_cache.TryGetValue(cacheKey, out AdvisorySymbolMapping? cached) && cached is not null) { return cached; } diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/Internal/PythonDistributionLoader.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/Internal/PythonDistributionLoader.cs index b1c0e8936..b5d5e6d12 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/Internal/PythonDistributionLoader.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/Internal/PythonDistributionLoader.cs @@ -249,7 +249,7 @@ internal static class PythonDistributionLoader return false; } - private static void AddFileEvidence(LanguageAnalyzerContext context, string path, string source, ICollection evidence) + private static void AddFileEvidence(LanguageAnalyzerContext context, string? path, string source, ICollection evidence) { if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { diff --git a/src/Scanner/__Tests/StellaOps.Scanner.EntryTrace.Tests/Risk/RiskScoreTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.EntryTrace.Tests/Risk/RiskScoreTests.cs index 7cc8a6982..ee054321a 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.EntryTrace.Tests/Risk/RiskScoreTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.EntryTrace.Tests/Risk/RiskScoreTests.cs @@ -14,7 +14,7 @@ public sealed class RiskScoreTests [Fact] public void RiskScore_Zero_ReturnsNegligibleLevel() { - var score = RiskScore.Zero; + var score = RiskScore.Zero(); Assert.Equal(0.0f, score.OverallScore); Assert.Equal(RiskCategory.Unknown, score.Category); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Surface.Validation.Tests/SurfaceValidatorRunnerTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Surface.Validation.Tests/SurfaceValidatorRunnerTests.cs index 17373008e..ab831ddc4 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Surface.Validation.Tests/SurfaceValidatorRunnerTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Surface.Validation.Tests/SurfaceValidatorRunnerTests.cs @@ -34,7 +34,7 @@ public sealed class SurfaceValidatorRunnerTests Array.Empty(), new SurfaceSecretsConfiguration("kubernetes", "", null, null, null, false), string.Empty, - new SurfaceTlsConfiguration(null, null, null)); + new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow }; var context = SurfaceValidationContext.Create(services, "TestComponent", environment); @@ -60,7 +60,7 @@ public sealed class SurfaceValidatorRunnerTests Array.Empty(), new SurfaceSecretsConfiguration("kubernetes", "tenant-a", null, "stellaops", null, false), "tenant-a", - new SurfaceTlsConfiguration(null, null, null)); + new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow }; var services = CreateServices(); var runner = services.GetRequiredService(); @@ -86,7 +86,7 @@ public sealed class SurfaceValidatorRunnerTests Array.Empty(), new SurfaceSecretsConfiguration("inline", "tenant-a", Root: null, Namespace: null, FallbackProvider: null, AllowInline: false), "tenant-a", - new SurfaceTlsConfiguration(null, null, null)); + new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow }; var services = CreateServices(); var runner = services.GetRequiredService(); @@ -118,7 +118,7 @@ public sealed class SurfaceValidatorRunnerTests Array.Empty(), new SurfaceSecretsConfiguration("file", "tenant-a", Root: missingRoot, Namespace: null, FallbackProvider: null, AllowInline: false), "tenant-a", - new SurfaceTlsConfiguration(null, null, null)); + new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow }; var services = CreateServices(); var runner = services.GetRequiredService(); diff --git a/src/VexHub/StellaOps.VexHub.WebService/Program.cs b/src/VexHub/StellaOps.VexHub.WebService/Program.cs index d96b48ad1..44116a6f9 100644 --- a/src/VexHub/StellaOps.VexHub.WebService/Program.cs +++ b/src/VexHub/StellaOps.VexHub.WebService/Program.cs @@ -49,7 +49,7 @@ builder.Services.AddOpenApi(); var routerOptions = builder.Configuration.GetSection("VexHub:Router").Get(); builder.Services.TryAddStellaRouter( serviceName: "vexhub", - version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0", + version: System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0", routerOptions: routerOptions); var app = builder.Build(); @@ -83,3 +83,9 @@ app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Service = "VexH app.TryRefreshStellaRouterEndpoints(routerOptions); app.Run(); + +// Make Program class explicit to avoid conflicts with imported types +namespace StellaOps.VexHub.WebService +{ + public partial class Program { } +} diff --git a/src/VexHub/__Tests/StellaOps.VexHub.WebService.Tests/Integration/VexExportCompatibilityTests.cs b/src/VexHub/__Tests/StellaOps.VexHub.WebService.Tests/Integration/VexExportCompatibilityTests.cs index 158c17c29..f4a8c76d3 100644 --- a/src/VexHub/__Tests/StellaOps.VexHub.WebService.Tests/Integration/VexExportCompatibilityTests.cs +++ b/src/VexHub/__Tests/StellaOps.VexHub.WebService.Tests/Integration/VexExportCompatibilityTests.cs @@ -10,11 +10,11 @@ namespace StellaOps.VexHub.WebService.Tests.Integration; /// Integration tests verifying VexHub API compatibility with Trivy and Grype. /// These tests ensure the API endpoints return valid OpenVEX format that can be consumed by scanning tools. /// -public sealed class VexExportCompatibilityTests : IClassFixture> +public sealed class VexExportCompatibilityTests : IClassFixture> { private readonly HttpClient _client; - public VexExportCompatibilityTests(WebApplicationFactory factory) + public VexExportCompatibilityTests(WebApplicationFactory factory) { _client = factory.CreateClient(); } diff --git a/src/VexLens/StellaOps.VexLens/Api/NoiseGatingApiModels.cs b/src/VexLens/StellaOps.VexLens/Api/NoiseGatingApiModels.cs index 349d103a0..fd37741d2 100644 --- a/src/VexLens/StellaOps.VexLens/Api/NoiseGatingApiModels.cs +++ b/src/VexLens/StellaOps.VexLens/Api/NoiseGatingApiModels.cs @@ -165,14 +165,14 @@ internal static class NoiseGatingApiMapper VulnerabilityId: entry.VulnerabilityId, ProductKey: entry.ProductKey, FromStatus: entry.FromStatus?.ToString(), - ToStatus: entry.ToStatus?.ToString(), + ToStatus: entry.ToStatus.ToString(), FromConfidence: entry.FromConfidence, ToConfidence: entry.ToConfidence, Justification: entry.Justification?.ToString(), FromRationaleClass: entry.FromRationaleClass, ToRationaleClass: entry.ToRationaleClass, Summary: entry.Summary, - ContributingSources: entry.ContributingSources?.ToList(), + ContributingSources: entry.ContributingSources.IsDefaultOrEmpty ? null : entry.ContributingSources.ToList(), CreatedAt: entry.Timestamp); } diff --git a/src/VexLens/StellaOps.VexLens/NoiseGate/NoiseGateService.cs b/src/VexLens/StellaOps.VexLens/NoiseGate/NoiseGateService.cs index d7072f41c..cc1dbcdd9 100644 --- a/src/VexLens/StellaOps.VexLens/NoiseGate/NoiseGateService.cs +++ b/src/VexLens/StellaOps.VexLens/NoiseGate/NoiseGateService.cs @@ -57,16 +57,27 @@ public sealed class NoiseGateService : INoiseGate if (!opts.EdgeDeduplicationEnabled || !opts.Enabled) { // Return edges without deduplication - create minimal deduplicated wrappers - var passthrough = edges.Select(e => new DeduplicatedEdgeBuilder( - e.From, e.To, e.Why.Type, e.Why.Loc) - .WithConfidence(e.Why.Confidence) - .Build()) - .ToList(); + var passthrough = edges.Select(e => + { + var key = new EdgeSemanticKey(e.From, e.To); + var builder = new DeduplicatedEdgeBuilder(key, e.From, e.To); + builder.AddSource( + "passthrough", + e.Why, + e.Why.Confidence, + _timeProvider.GetUtcNow()); + return builder.Build(); + }).ToList(); return Task.FromResult>(passthrough); } - var result = _edgeDeduplicator.Deduplicate(edges); + var result = _edgeDeduplicator.Deduplicate( + edges, + e => new EdgeSemanticKey(e.From, e.To), + e => "deduplicated", + e => e.Why.Confidence, + e => _timeProvider.GetUtcNow()); return Task.FromResult(result); } @@ -438,13 +449,13 @@ public sealed class NoiseGateService : INoiseGate // Add sorted edges var sortedEdges = edges - .OrderBy(e => e.SemanticKey, StringComparer.Ordinal) + .OrderBy(e => e.Key.ComputeKey(), StringComparer.Ordinal) .ToList(); foreach (var edge in sortedEdges) { sb.Append('|'); - sb.Append(edge.SemanticKey); + sb.Append(edge.Key.ComputeKey()); } // Add sorted verdicts diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/alerts/alert-destination-config.component.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/alerts/alert-destination-config.component.ts index 85b0bb639..e581bbecb 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/alerts/alert-destination-config.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/alerts/alert-destination-config.component.ts @@ -26,7 +26,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { inject } from '@angular/core'; -import { SecretAlertSettings, SecretAlertDestination } from '../../models'; +import { SecretAlertSettings, SecretAlertDestination, SecretSeverity } from '../../models'; import { SecretDetectionSettingsService } from '../../services'; @Component({ @@ -403,7 +403,16 @@ export class AlertDestinationConfigComponent { private readonly settingsService = inject(SecretDetectionSettingsService); private readonly snackBar = inject(MatSnackBar); - @Input() settings: SecretAlertSettings = { enabled: false, destinations: [] }; + @Input() settings: SecretAlertSettings = { + enabled: false, + destinations: [], + minimumAlertSeverity: 'High', + maxAlertsPerScan: 100, + deduplicationWindowHours: 24, + includeFilePath: true, + includeMaskedValue: false, + includeImageRef: true, + }; @Input() tenantId = ''; @Output() settingsChange = new EventEmitter(); @@ -421,7 +430,7 @@ export class AlertDestinationConfigComponent { this.emitChange(); } - onMinSeverityChange(severity: string): void { + onMinSeverityChange(severity: SecretSeverity): void { this.settings = { ...this.settings, minimumSeverity: severity }; this.emitChange(); } @@ -448,6 +457,8 @@ export class AlertDestinationConfigComponent { const newDest: SecretAlertDestination = { id: crypto.randomUUID(), name: '', + channelType: 'Webhook', + channelId: '', type: 'webhook', enabled: true, config: {}, @@ -466,7 +477,7 @@ export class AlertDestinationConfigComponent { this.updateDestination(index, { name: input.value }); } - onDestTypeChange(index: number, type: string): void { + onDestTypeChange(index: number, type: 'webhook' | 'slack' | 'email' | 'teams' | 'pagerduty'): void { this.updateDestination(index, { type, config: {} }); } @@ -478,7 +489,7 @@ export class AlertDestinationConfigComponent { }); } - onDestSeverityChange(index: number, severities: string[]): void { + onDestSeverityChange(index: number, severities: SecretSeverity[]): void { this.updateDestination(index, { severityFilter: severities }); } diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/exceptions/exception-manager.component.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/exceptions/exception-manager.component.ts index d9b4cbc4e..8985d796a 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/exceptions/exception-manager.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/exceptions/exception-manager.component.ts @@ -25,7 +25,7 @@ import { MatCardModule } from '@angular/material/card'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { SecretExceptionService } from '../../services'; -import { SecretExceptionPattern } from '../../models'; +import { SecretExceptionPattern, CreateExceptionRequest } from '../../models'; @Component({ selector: 'app-exception-manager', @@ -398,7 +398,7 @@ export class ExceptionManagerComponent implements OnInit { } loadExceptions(): void { - this.exceptionService.loadExceptions(this.tenantId).subscribe(); + this.exceptionService.listExceptions(this.tenantId).subscribe(); } openCreateDialog(): void { @@ -436,17 +436,18 @@ export class ExceptionManagerComponent implements OnInit { const formValue = this.exceptionForm.value; const existing = this.editingException(); - const payload: Partial = { - name: formValue.name, + const payload: CreateExceptionRequest = { + name: formValue.name ?? '', + pattern: formValue.pattern ?? '', + reason: formValue.description ?? 'Exception created via UI', + matchType: formValue.matchType ?? 'literal', + target: formValue.target ?? 'value', + enabled: formValue.enabled ?? true, description: formValue.description || undefined, - matchType: formValue.matchType, - pattern: formValue.pattern, - target: formValue.target, - enabled: formValue.enabled, }; const operation = existing - ? this.exceptionService.updateException(existing.id, payload) + ? this.exceptionService.updateException(this.tenantId, existing.id, payload) : this.exceptionService.createException(this.tenantId, payload); operation.subscribe({ @@ -464,7 +465,7 @@ export class ExceptionManagerComponent implements OnInit { } toggleEnabled(exception: SecretExceptionPattern): void { - this.exceptionService.updateException(exception.id, { + this.exceptionService.updateException(this.tenantId, exception.id, { enabled: !exception.enabled, }).subscribe({ next: () => { @@ -477,7 +478,7 @@ export class ExceptionManagerComponent implements OnInit { deleteException(exception: SecretExceptionPattern): void { if (!confirm(`Delete exception "${exception.name}"?`)) return; - this.exceptionService.deleteException(exception.id).subscribe({ + this.exceptionService.deleteException(this.tenantId, exception.id).subscribe({ next: () => { this.showSuccess('Exception deleted'); this.loadExceptions(); diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/masked-value-display.component.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/masked-value-display.component.ts index e26cd7478..b121eef07 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/masked-value-display.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/masked-value-display.component.ts @@ -121,6 +121,7 @@ export class MaskedValueDisplayComponent { @Input() value = ''; @Input() findingId = ''; + @Input() tenantId = ''; @Input() canReveal = false; @Output() revealed = new EventEmitter(); @@ -138,12 +139,12 @@ export class MaskedValueDisplayComponent { }; reveal(): void { - if (!this.canReveal || !this.findingId) return; + if (!this.canReveal || !this.findingId || !this.tenantId) return; this.revealing.set(true); - this.findingsService.revealValue(this.findingId).subscribe({ - next: (value) => { + this.findingsService.revealValue(this.tenantId, this.findingId).subscribe({ + next: (value: string) => { this.revealedValue.set(value); this.isRevealed.set(true); this.revealing.set(false); diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/secret-findings-list.component.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/secret-findings-list.component.ts index 79d838416..f20065663 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/secret-findings-list.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/findings/secret-findings-list.component.ts @@ -26,8 +26,8 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { MatMenuModule } from '@angular/material/menu'; import { SecretFindingsService } from '../../services'; -import { SecretFinding } from '../../models'; -import { MaskedValueDisplayComponent } from './masked-value-display.component'; +import { SecretFinding, SecretSeverity, SecretFindingStatus } from '../../models'; +import { MaskedValueDisplayComponent } from './'; @Component({ selector: 'app-secret-findings-list', @@ -426,7 +426,9 @@ export class SecretFindingsListComponent implements OnInit { // Expose service signals readonly findings = this.findingsService.findings; - readonly pagination = this.findingsService.pagination; + readonly page = this.findingsService.page; + readonly pageSize = this.findingsService.pageSize; + readonly total = this.findingsService.total; readonly loading = this.findingsService.loading; readonly error = this.findingsService.error; @@ -459,14 +461,11 @@ export class SecretFindingsListComponent implements OnInit { reload(): void { const tid = this.tenantId(); if (tid) { - this.findingsService.loadFindings(tid, { + this.findingsService.listFindings(tid, { page: 1, pageSize: 25, - search: this.searchQuery(), - severity: this.severityFilter(), - status: this.statusFilter(), - sortBy: this.sortField(), - sortDirection: this.sortDirection(), + severity: this.severityFilter() as SecretSeverity[], + status: this.statusFilter() ? [this.statusFilter() as SecretFindingStatus] : undefined, }).subscribe(); } } @@ -496,14 +495,11 @@ export class SecretFindingsListComponent implements OnInit { onPageChange(event: PageEvent): void { const tid = this.tenantId(); if (tid) { - this.findingsService.loadFindings(tid, { + this.findingsService.listFindings(tid, { page: event.pageIndex + 1, pageSize: event.pageSize, - search: this.searchQuery(), - severity: this.severityFilter(), - status: this.statusFilter(), - sortBy: this.sortField(), - sortDirection: this.sortDirection(), + severity: this.severityFilter() as SecretSeverity[], + status: this.statusFilter() ? [this.statusFilter() as SecretFindingStatus] : undefined, }).subscribe(); } } @@ -527,7 +523,8 @@ export class SecretFindingsListComponent implements OnInit { } markResolved(finding: SecretFinding): void { - this.findingsService.updateStatus(finding.id, 'resolved').subscribe({ + const tid = this.tenantId(); + this.findingsService.updateStatus(tid, finding.id, 'Resolved').subscribe({ next: () => this.reload(), }); } diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/revelation-policy-selector.component.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/revelation-policy-selector.component.ts index deb8d6837..e5b89a1f1 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/revelation-policy-selector.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/revelation-policy-selector.component.ts @@ -260,7 +260,14 @@ type RevelationMode = 'masked' | 'partial' | 'full' | 'redacted'; `] }) export class RevelationPolicySelectorComponent { - @Input() config: RevelationPolicyConfig = { mode: 'masked' }; + @Input() config: RevelationPolicyConfig = { + defaultPolicy: 'FullMask', + exportPolicy: 'FullMask', + logPolicy: 'FullMask', + fullRevealRoles: [], + partialRevealChars: 4, + mode: 'masked', + }; @Output() configChange = new EventEmitter(); private readonly sampleSecret = 'ghp_abc123XYZ789secret'; diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/secret-detection-settings.component.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/secret-detection-settings.component.ts index 418b87aed..4f22147bb 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/secret-detection-settings.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/components/settings/secret-detection-settings.component.ts @@ -19,10 +19,10 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { ActivatedRoute } from '@angular/router'; import { SecretDetectionSettingsService } from '../../services'; -import { RevelationPolicySelectorComponent } from '../settings/revelation-policy-selector.component'; -import { RuleCategoryTogglesComponent } from '../settings/rule-category-toggles.component'; -import { ExceptionManagerComponent } from '../exceptions/exception-manager.component'; -import { AlertDestinationConfigComponent } from '../alerts/alert-destination-config.component'; +import { RevelationPolicySelectorComponent } from './revelation-policy-selector.component'; +import { RuleCategoryTogglesComponent } from './rule-category-toggles.component'; +import { ExceptionManagerComponent } from '../exceptions'; +import { AlertDestinationConfigComponent } from '../alerts'; import { RevelationPolicyConfig, SecretAlertSettings } from '../../models'; @Component({ diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/models/secret-detection.models.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/models/secret-detection.models.ts index c455d6266..c9bc94705 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/models/secret-detection.models.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/models/secret-detection.models.ts @@ -24,6 +24,11 @@ export interface SecretDetectionSettings { */ export type SecretRevelationPolicy = 'FullMask' | 'PartialReveal' | 'FullReveal'; +/** + * Revelation mode for display settings. + */ +export type RevelationMode = 'masked' | 'partial' | 'full' | 'redacted'; + /** * Configuration for revelation policies by context. */ @@ -33,6 +38,18 @@ export interface RevelationPolicyConfig { logPolicy: SecretRevelationPolicy; fullRevealRoles: string[]; partialRevealChars: number; + /** Display mode for UI presentation */ + mode?: RevelationMode; + /** Character used for masking */ + maskChar?: string; + /** Length of the mask */ + maskLength?: number; + /** Number of characters to reveal at start */ + revealFirst?: number; + /** Number of characters to reveal at end */ + revealLast?: number; + /** Required permission for full reveal */ + requiredPermission?: string; } /** @@ -47,6 +64,16 @@ export interface SecretExceptionPattern { expiresAt?: string; ruleIds?: string[]; pathFilter?: string; + /** Display name for the exception */ + name?: string; + /** Description of why exception exists */ + description?: string; + /** How the pattern is matched */ + matchType?: 'literal' | 'regex' | 'glob' | 'prefix' | 'suffix'; + /** What the pattern applies to */ + target?: 'value' | 'path' | 'filename'; + /** Whether the exception is active */ + enabled?: boolean; } /** @@ -62,6 +89,12 @@ export interface SecretAlertSettings { includeMaskedValue: boolean; includeImageRef: boolean; alertMessagePrefix?: string; + /** Minimum severity to trigger alert (UI variant) */ + minimumSeverity?: SecretSeverity; + /** Rate limit per hour */ + rateLimitPerHour?: number; + /** Deduplication window in minutes (UI variant) */ + deduplicationWindowMinutes?: number; } /** @@ -74,6 +107,12 @@ export interface SecretAlertDestination { channelId: string; severityFilter?: SecretSeverity[]; ruleCategoryFilter?: string[]; + /** Destination type (UI variant) */ + type?: 'webhook' | 'slack' | 'email' | 'teams' | 'pagerduty'; + /** Configuration options */ + config?: Record; + /** Whether destination is enabled */ + enabled?: boolean; } /** @@ -120,6 +159,8 @@ export interface SecretRuleCategory { description: string; ruleCount: number; enabled: boolean; + /** Group for categorization */ + group?: string; } /** @@ -131,6 +172,16 @@ export interface CreateExceptionRequest { expiresAt?: string; ruleIds?: string[]; pathFilter?: string; + /** Display name for the exception */ + name?: string; + /** Description of why exception exists */ + description?: string; + /** How the pattern is matched */ + matchType?: 'literal' | 'regex' | 'glob' | 'prefix' | 'suffix'; + /** What the pattern applies to */ + target?: 'value' | 'path' | 'filename'; + /** Whether the exception is active */ + enabled?: boolean; } /** diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-exception.service.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-exception.service.ts index 5d998ac37..101e480d1 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-exception.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-exception.service.ts @@ -7,7 +7,7 @@ import { Injectable, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { catchError, tap } from 'rxjs/operators'; +import { catchError, tap, map } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; import { SecretExceptionPattern, @@ -47,12 +47,10 @@ export class SecretExceptionService { catchError(err => { this._error.set(err.message || 'Failed to load exceptions'); this._loading.set(false); - return of({ items: [] }); + return of({ items: [] as SecretExceptionPattern[] }); }), - // Transform response - tap(() => {}), - // Return just the items array - tap(response => this._exceptions.set(response.items)) + // Map to array + map(response => response.items) ); } diff --git a/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-findings.service.ts b/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-findings.service.ts index 71402c790..8c2dd04c6 100644 --- a/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-findings.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/secret-detection/services/secret-findings.service.ts @@ -7,7 +7,7 @@ import { Injectable, inject, signal, computed } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { catchError, tap } from 'rxjs/operators'; +import { catchError, tap, map } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; import { SecretFinding, @@ -157,6 +157,21 @@ export class SecretFindingsService { ); } + /** + * Reveals the secret value for a finding (requires appropriate permissions). + */ + revealValue(tenantId: string, findingId: string): Observable { + return this.http.get<{ value: string }>( + `${this.baseUrl}/${tenantId}/${findingId}/reveal` + ).pipe( + map(response => response.value), + catchError(err => { + this._error.set(err.message || 'Failed to reveal secret value'); + throw err; + }) + ); + } + /** * Selects a finding for detail view. */ diff --git a/src/Zastava/__Tests/StellaOps.Zastava.Webhook.Tests/Admission/RuntimeAdmissionPolicyServiceTests.cs b/src/Zastava/__Tests/StellaOps.Zastava.Webhook.Tests/Admission/RuntimeAdmissionPolicyServiceTests.cs index 1282242c5..ad71b9e03 100644 --- a/src/Zastava/__Tests/StellaOps.Zastava.Webhook.Tests/Admission/RuntimeAdmissionPolicyServiceTests.cs +++ b/src/Zastava/__Tests/StellaOps.Zastava.Webhook.Tests/Admission/RuntimeAdmissionPolicyServiceTests.cs @@ -249,7 +249,7 @@ public sealed class RuntimeAdmissionPolicyServiceTests new[] { "admission" }, new SurfaceSecretsConfiguration("inline", "default", null, null, null, true), "default", - new SurfaceTlsConfiguration(null, null, null)); + new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow }; private sealed class StubSurfaceFsClient : IWebhookSurfaceFsClient { diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsClient.cs b/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsClient.cs index 083bc6db9..febc80368 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsClient.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsClient.cs @@ -11,6 +11,7 @@ namespace StellaOps.Cryptography.Kms; public sealed class AwsKmsClient : IKmsClient, IDisposable { private readonly IAwsKmsFacade _facade; + private readonly TimeProvider _timeProvider; private readonly TimeSpan _metadataCacheDuration; private readonly TimeSpan _publicKeyCacheDuration; @@ -18,11 +19,12 @@ public sealed class AwsKmsClient : IKmsClient, IDisposable private readonly ConcurrentDictionary _publicKeyCache = new(StringComparer.Ordinal); private bool _disposed; - public AwsKmsClient(IAwsKmsFacade facade, AwsKmsOptions options) + public AwsKmsClient(IAwsKmsFacade facade, AwsKmsOptions options, TimeProvider? timeProvider = null) { _facade = facade ?? throw new ArgumentNullException(nameof(facade)); ArgumentNullException.ThrowIfNull(options); + _timeProvider = timeProvider ?? TimeProvider.System; _metadataCacheDuration = options.MetadataCacheDuration; _publicKeyCacheDuration = options.PublicKeyCacheDuration; } @@ -156,7 +158,7 @@ public sealed class AwsKmsClient : IKmsClient, IDisposable private async Task GetCachedMetadataAsync(string keyId, CancellationToken cancellationToken) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_metadataCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now) { return cached.Metadata; @@ -170,7 +172,7 @@ public sealed class AwsKmsClient : IKmsClient, IDisposable private async Task GetCachedPublicKeyAsync(string resource, CancellationToken cancellationToken) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_publicKeyCache.TryGetValue(resource, out var cached) && cached.ExpiresAt > now) { return cached.Material; diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsFacade.cs b/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsFacade.cs index 3bdda872d..13bc7e8be 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsFacade.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/AwsKmsFacade.cs @@ -37,10 +37,12 @@ internal sealed class AwsKmsFacade : IAwsKmsFacade { private readonly IAmazonKeyManagementService _client; private readonly bool _ownsClient; + private readonly TimeProvider _timeProvider; - public AwsKmsFacade(AwsKmsOptions options) + public AwsKmsFacade(AwsKmsOptions options, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(options); + _timeProvider = timeProvider ?? TimeProvider.System; var config = new AmazonKeyManagementServiceConfig(); if (!string.IsNullOrWhiteSpace(options.Region)) @@ -59,9 +61,10 @@ internal sealed class AwsKmsFacade : IAwsKmsFacade _ownsClient = true; } - public AwsKmsFacade(IAmazonKeyManagementService client) + public AwsKmsFacade(IAmazonKeyManagementService client, TimeProvider? timeProvider = null) { _client = client ?? throw new ArgumentNullException(nameof(client)); + _timeProvider = timeProvider ?? TimeProvider.System; _ownsClient = false; } @@ -116,7 +119,7 @@ internal sealed class AwsKmsFacade : IAwsKmsFacade }, cancellationToken).ConfigureAwait(false); var metadata = response.KeyMetadata ?? throw new InvalidOperationException($"Key '{keyId}' was not found."); - var createdAt = metadata.CreationDate?.ToUniversalTime() ?? DateTimeOffset.UtcNow; + var createdAt = metadata.CreationDate?.ToUniversalTime() ?? _timeProvider.GetUtcNow(); return new AwsKeyMetadata( metadata.KeyId ?? keyId, diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/Fido2KmsClient.cs b/src/__Libraries/StellaOps.Cryptography.Kms/Fido2KmsClient.cs index f00673275..8a86ae485 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/Fido2KmsClient.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/Fido2KmsClient.cs @@ -15,15 +15,17 @@ public sealed class Fido2KmsClient : IKmsClient private readonly byte[] _subjectPublicKeyInfo; private readonly TimeSpan _metadataCacheDuration; private readonly string _curveName; + private readonly TimeProvider _timeProvider; private KmsKeyMetadata? _cachedMetadata; private DateTimeOffset _metadataExpiresAt; private bool _disposed; - public Fido2KmsClient(IFido2Authenticator authenticator, Fido2Options options) + public Fido2KmsClient(IFido2Authenticator authenticator, Fido2Options options, TimeProvider? timeProvider = null) { _authenticator = authenticator ?? throw new ArgumentNullException(nameof(authenticator)); _options = options ?? throw new ArgumentNullException(nameof(options)); + _timeProvider = timeProvider ?? TimeProvider.System; if (string.IsNullOrWhiteSpace(_options.CredentialId)) { @@ -99,16 +101,17 @@ public sealed class Fido2KmsClient : IKmsClient { ThrowIfDisposed(); - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_cachedMetadata is not null && _metadataExpiresAt > now) { return Task.FromResult(_cachedMetadata); } + var createdAt = _options.CreatedAt ?? _timeProvider.GetUtcNow(); var version = new KmsKeyVersionMetadata( _options.CredentialId, KmsKeyState.Active, - _options.CreatedAt, + createdAt, null, Convert.ToBase64String(_subjectPublicKeyInfo), _curveName); @@ -117,7 +120,7 @@ public sealed class Fido2KmsClient : IKmsClient _options.CredentialId, KmsAlgorithms.Es256, KmsKeyState.Active, - _options.CreatedAt, + createdAt, ImmutableArray.Create(version)); _metadataExpiresAt = now.Add(_metadataCacheDuration); @@ -138,7 +141,7 @@ public sealed class Fido2KmsClient : IKmsClient Array.Empty(), _publicParameters.Q.X ?? throw new InvalidOperationException("FIDO2 public key missing X coordinate."), _publicParameters.Q.Y ?? throw new InvalidOperationException("FIDO2 public key missing Y coordinate."), - _options.CreatedAt); + _options.CreatedAt ?? _timeProvider.GetUtcNow()); } public Task RotateAsync(string keyId, CancellationToken cancellationToken = default) diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/Fido2Options.cs b/src/__Libraries/StellaOps.Cryptography.Kms/Fido2Options.cs index 252691b09..dab92fa54 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/Fido2Options.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/Fido2Options.cs @@ -24,8 +24,9 @@ public sealed class Fido2Options /// /// Gets or sets the timestamp when the credential was provisioned. + /// When not set, the Fido2KmsClient will use the current time via TimeProvider. /// - public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? CreatedAt { get; set; } /// /// Gets or sets the cache duration for metadata lookups. diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/FileKmsClient.cs b/src/__Libraries/StellaOps.Cryptography.Kms/FileKmsClient.cs index d8b7eddf5..40575917e 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/FileKmsClient.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/FileKmsClient.cs @@ -21,9 +21,10 @@ public sealed class FileKmsClient : IKmsClient, IDisposable private const int MinKeyDerivationIterations = 600_000; private readonly FileKmsOptions _options; + private readonly TimeProvider _timeProvider; private readonly SemaphoreSlim _mutex = new(1, 1); - public FileKmsClient(FileKmsOptions options) + public FileKmsClient(FileKmsOptions options, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(options); if (string.IsNullOrWhiteSpace(options.RootPath)) @@ -37,6 +38,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable } _options = options; + _timeProvider = timeProvider ?? TimeProvider.System; if (_options.KeyDerivationIterations < MinKeyDerivationIterations) { throw new ArgumentOutOfRangeException( @@ -202,7 +204,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable } var versionId = string.IsNullOrWhiteSpace(material.VersionId) - ? $"{DateTimeOffset.UtcNow:yyyyMMddTHHmmssfffZ}" + ? $"{_timeProvider.GetUtcNow():yyyyMMddTHHmmssfffZ}" : material.VersionId; if (record.Versions.Any(v => string.Equals(v.VersionId, versionId, StringComparison.Ordinal))) @@ -234,7 +236,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable existing.State = KmsKeyState.PendingRotation; } - var createdAt = material.CreatedAt == default ? DateTimeOffset.UtcNow : material.CreatedAt; + var createdAt = material.CreatedAt == default ? _timeProvider.GetUtcNow() : material.CreatedAt; var publicKey = CombinePublicCoordinates(material.Qx, material.Qy); record.Versions.Add(new KeyVersionRecord @@ -280,7 +282,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable throw new InvalidOperationException($"Key '{keyId}' has been revoked and cannot be rotated."); } - var timestamp = DateTimeOffset.UtcNow; + var timestamp = _timeProvider.GetUtcNow(); var versionId = $"{timestamp:yyyyMMddTHHmmssfffZ}"; var keyData = CreateKeyMaterial(record.Algorithm); @@ -334,7 +336,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: false).ConfigureAwait(false) ?? throw new InvalidOperationException($"Key '{keyId}' does not exist."); - var timestamp = DateTimeOffset.UtcNow; + var timestamp = _timeProvider.GetUtcNow(); record.State = KmsKeyState.Revoked; foreach (var version in record.Versions) { @@ -381,7 +383,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable KeyId = keyId, Algorithm = _options.Algorithm, State = KmsKeyState.Active, - CreatedAt = DateTimeOffset.UtcNow, + CreatedAt = _timeProvider.GetUtcNow(), }; await SaveMetadataAsync(record, cancellationToken).ConfigureAwait(false); @@ -645,7 +647,7 @@ public sealed class FileKmsClient : IKmsClient, IDisposable v.CurveName)) .ToImmutableArray(); - var createdAt = record.CreatedAt ?? (versions.Length > 0 ? versions.Min(v => v.CreatedAt) : DateTimeOffset.UtcNow); + var createdAt = record.CreatedAt ?? (versions.Length > 0 ? versions.Min(v => v.CreatedAt) : TimeProvider.System.GetUtcNow()); return new KmsKeyMetadata(record.KeyId, record.Algorithm, record.State, createdAt, versions); } diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsClient.cs b/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsClient.cs index f376586bc..392c7ae64 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsClient.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsClient.cs @@ -13,6 +13,7 @@ namespace StellaOps.Cryptography.Kms; public sealed class GcpKmsClient : IKmsClient, IDisposable { private readonly IGcpKmsFacade _facade; + private readonly TimeProvider _timeProvider; private readonly TimeSpan _metadataCacheDuration; private readonly TimeSpan _publicKeyCacheDuration; @@ -20,11 +21,12 @@ public sealed class GcpKmsClient : IKmsClient, IDisposable private readonly ConcurrentDictionary _publicKeyCache = new(StringComparer.Ordinal); private bool _disposed; - public GcpKmsClient(IGcpKmsFacade facade, GcpKmsOptions options) + public GcpKmsClient(IGcpKmsFacade facade, GcpKmsOptions options, TimeProvider? timeProvider = null) { _facade = facade ?? throw new ArgumentNullException(nameof(facade)); ArgumentNullException.ThrowIfNull(options); + _timeProvider = timeProvider ?? TimeProvider.System; _metadataCacheDuration = options.MetadataCacheDuration; _publicKeyCacheDuration = options.PublicKeyCacheDuration; } @@ -170,7 +172,7 @@ public sealed class GcpKmsClient : IKmsClient, IDisposable private async Task GetCachedMetadataAsync(string keyId, CancellationToken cancellationToken) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_metadataCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now) { return cached.Snapshot; @@ -186,7 +188,7 @@ public sealed class GcpKmsClient : IKmsClient, IDisposable private async Task GetCachedPublicKeyAsync(string versionName, CancellationToken cancellationToken) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_publicKeyCache.TryGetValue(versionName, out var cached) && cached.ExpiresAt > now) { return cached.Material; diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsFacade.cs b/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsFacade.cs index 094a86dbc..bb71cc792 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsFacade.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/GcpKmsFacade.cs @@ -44,10 +44,12 @@ internal sealed class GcpKmsFacade : IGcpKmsFacade { private readonly KeyManagementServiceClient _client; private readonly bool _ownsClient; + private readonly TimeProvider _timeProvider; - public GcpKmsFacade(GcpKmsOptions options) + public GcpKmsFacade(GcpKmsOptions options, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(options); + _timeProvider = timeProvider ?? TimeProvider.System; var builder = new KeyManagementServiceClientBuilder { Endpoint = string.IsNullOrWhiteSpace(options.Endpoint) @@ -59,9 +61,10 @@ internal sealed class GcpKmsFacade : IGcpKmsFacade _ownsClient = true; } - public GcpKmsFacade(KeyManagementServiceClient client) + public GcpKmsFacade(KeyManagementServiceClient client, TimeProvider? timeProvider = null) { _client = client ?? throw new ArgumentNullException(nameof(client)); + _timeProvider = timeProvider ?? TimeProvider.System; _ownsClient = false; } @@ -155,16 +158,16 @@ internal sealed class GcpKmsFacade : IGcpKmsFacade } } - private static DateTimeOffset ToDateTimeOffsetOrUtcNow(Timestamp? timestamp) + private DateTimeOffset ToDateTimeOffsetOrUtcNow(Timestamp? timestamp) { if (timestamp is null) { - return DateTimeOffset.UtcNow; + return _timeProvider.GetUtcNow(); } if (timestamp.Seconds == 0 && timestamp.Nanos == 0) { - return DateTimeOffset.UtcNow; + return _timeProvider.GetUtcNow(); } return timestamp.ToDateTimeOffset(); diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11Facade.cs b/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11Facade.cs index 56e3b475f..fcf61da5d 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11Facade.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11Facade.cs @@ -35,10 +35,12 @@ internal sealed class Pkcs11InteropFacade : IPkcs11Facade private readonly IPkcs11Library _library; private readonly ISlot _slot; private readonly ConcurrentDictionary _attributeCache = new(StringComparer.Ordinal); + private readonly TimeProvider _timeProvider; - public Pkcs11InteropFacade(Pkcs11Options options) + public Pkcs11InteropFacade(Pkcs11Options options, TimeProvider? timeProvider = null) { _options = options ?? throw new ArgumentNullException(nameof(options)); + _timeProvider = timeProvider ?? TimeProvider.System; if (string.IsNullOrWhiteSpace(_options.LibraryPath)) { throw new ArgumentException("PKCS#11 library path must be provided.", nameof(options)); @@ -66,7 +68,7 @@ internal sealed class Pkcs11InteropFacade : IPkcs11Facade return new Pkcs11KeyDescriptor( KeyId: label ?? privateHandle.ObjectId.ToString(), Label: label, - CreatedAt: DateTimeOffset.UtcNow); + CreatedAt: _timeProvider.GetUtcNow()); } public async Task GetPublicKeyAsync(CancellationToken cancellationToken) diff --git a/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11KmsClient.cs b/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11KmsClient.cs index 7540033ad..6e6d45f36 100644 --- a/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11KmsClient.cs +++ b/src/__Libraries/StellaOps.Cryptography.Kms/Pkcs11KmsClient.cs @@ -13,15 +13,17 @@ public sealed class Pkcs11KmsClient : IKmsClient private readonly IPkcs11Facade _facade; private readonly TimeSpan _metadataCacheDuration; private readonly TimeSpan _publicKeyCacheDuration; + private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _metadataCache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _publicKeyCache = new(StringComparer.Ordinal); private bool _disposed; - public Pkcs11KmsClient(IPkcs11Facade facade, Pkcs11Options options) + public Pkcs11KmsClient(IPkcs11Facade facade, Pkcs11Options options, TimeProvider? timeProvider = null) { _facade = facade ?? throw new ArgumentNullException(nameof(facade)); ArgumentNullException.ThrowIfNull(options); + _timeProvider = timeProvider ?? TimeProvider.System; _metadataCacheDuration = options.MetadataCacheDuration; _publicKeyCacheDuration = options.PublicKeyCacheDuration; @@ -169,7 +171,7 @@ public sealed class Pkcs11KmsClient : IKmsClient private async Task GetCachedMetadataAsync(string keyId, CancellationToken cancellationToken) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_metadataCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now) { return cached; @@ -183,7 +185,7 @@ public sealed class Pkcs11KmsClient : IKmsClient private async Task GetCachedPublicKeyAsync(string keyId, CancellationToken cancellationToken) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); if (_publicKeyCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now) { return cached; diff --git a/src/__Libraries/StellaOps.Policy.Tools/PolicySimulationSmokeRunner.cs b/src/__Libraries/StellaOps.Policy.Tools/PolicySimulationSmokeRunner.cs index 194e051e5..1a5660f94 100644 --- a/src/__Libraries/StellaOps.Policy.Tools/PolicySimulationSmokeRunner.cs +++ b/src/__Libraries/StellaOps.Policy.Tools/PolicySimulationSmokeRunner.cs @@ -62,6 +62,7 @@ public sealed class PolicySimulationSmokeRunner new NullPolicySnapshotRepository(), new NullPolicyAuditRepository(), timeProvider, + null, _loggerFactory.CreateLogger()); var previewService = new PolicyPreviewService(snapshotStore, _loggerFactory.CreateLogger()); diff --git a/src/__Libraries/__Tests/StellaOps.Provcache.Tests/MinimalProofExporterTests.cs b/src/__Libraries/__Tests/StellaOps.Provcache.Tests/MinimalProofExporterTests.cs index 0f6a16033..05eff2576 100644 --- a/src/__Libraries/__Tests/StellaOps.Provcache.Tests/MinimalProofExporterTests.cs +++ b/src/__Libraries/__Tests/StellaOps.Provcache.Tests/MinimalProofExporterTests.cs @@ -45,6 +45,7 @@ public sealed class MinimalProofExporterTests _mockChunkRepo.Object, signer: null, _timeProvider, + guidProvider: null, NullLogger.Instance); // Create test data @@ -435,6 +436,7 @@ public sealed class MinimalProofExporterTests _mockChunkRepo.Object, mockSigner.Object, _timeProvider, + guidProvider: null, NullLogger.Instance); var options = new MinimalProofExportOptions