From b1e78fe41273a80c09ca56e37f273fc64d2a0a32 Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 3 Nov 2025 10:02:29 +0200 Subject: [PATCH] feat: Implement vulnerability token signing and verification utilities - Added VulnTokenSigner for signing JWT tokens with specified algorithms and keys. - Introduced VulnTokenUtilities for resolving tenant and subject claims, and sanitizing context dictionaries. - Created VulnTokenVerificationUtilities for parsing tokens, verifying signatures, and deserializing payloads. - Developed VulnWorkflowAntiForgeryTokenIssuer for issuing anti-forgery tokens with configurable options. - Implemented VulnWorkflowAntiForgeryTokenVerifier for verifying anti-forgery tokens and validating payloads. - Added AuthorityVulnerabilityExplorerOptions to manage configuration for vulnerability explorer features. - Included tests for FilesystemPackRunDispatcher to ensure proper job handling under egress policy restrictions. --- NuGet.config | 5 +- deploy/compose/env/airgap.env.example | 1 + deploy/compose/env/dev.env.example | 1 + deploy/compose/env/prod.env.example | 1 + deploy/compose/env/stage.env.example | 1 + docs/11_AUTHORITY.md | 33 +- docs/11_DATA_SCHEMAS.md | 1100 ++-- docs/TASKS.md | 3 +- docs/airgap/airgap-mode.md | 8 +- docs/api/scanner/README.md | 18 + docs/api/scanner/windows-coverage.md | 50 + docs/api/scanner/windows-macos-summary.md | 37 + docs/api/sdk-openapi-program.md | 102 +- docs/benchmarks/scanner/README.md | 26 + docs/benchmarks/scanner/deep-dives/macos.md | 48 + docs/benchmarks/scanner/deep-dives/matrix.md | 3 +- .../scanner/deep-dives/os-packages.md | 8 + docs/benchmarks/scanner/deep-dives/secrets.md | 10 + docs/benchmarks/scanner/deep-dives/windows.md | 47 + ...ing-gaps-stella-misses-from-competitors.md | 136 +- .../scanner/windows-macos-demand.md | 16 + .../windows-macos-interview-template.md | 50 + ...GUIDE.md => 30_EXCITOR_CONNECTOR_GUIDE.md} | 440 +- .../manifest/connector.manifest.yaml | 8 + .../src/Excitor.MyConnector.csproj} | 2 +- .../src/MyConnector.cs | 6 +- .../src/MyConnectorOptions.cs | 2 +- .../src/MyConnectorOptionsValidator.cs | 4 +- .../src/MyConnectorPlugin.cs | 8 +- .../manifest/connector.manifest.yaml | 8 - docs/implplan/SPRINTS.md | 7 + docs/implplan/SPRINTS_PRIOR_20251021.md | 2 +- docs/implplan/SPRINTS_PRIOR_20251025.md | 2 +- docs/implplan/SPRINTS_PRIOR_20251027.md | 168 +- docs/implplan/SPRINTS_PRIOR_20251031.md | 2163 +++---- docs/implplan/SPRINT_100_identity_signing.md | 13 +- .../implplan/SPRINT_110_ingestion_evidence.md | 4 +- docs/implplan/SPRINT_120_policy_reasoning.md | 10 +- docs/implplan/SPRINT_130_scanner_surface.md | 20 +- .../SPRINT_170_notifications_telemetry.md | 1 + docs/implplan/SPRINT_190_ops_offline.md | 4 +- .../SPRINT_200_documentation_process.md | 3 +- docs/migration/no-merge.md | 141 + docs/modules/authority/architecture.md | 42 + docs/modules/{vexer => excitor}/AGENTS.md | 10 +- docs/modules/{vexer => excitor}/README.md | 68 +- docs/modules/excitor/TASKS.md | 9 + .../{vexer => excitor}/architecture.md | 926 +-- .../{vexer => excitor}/implementation_plan.md | 130 +- docs/modules/{vexer => excitor}/scoring.md | 166 +- .../export-center/provenance-and-signing.md | 300 +- .../operations/backup-restore.md | 3 +- .../issuer-directory/operations/deployment.md | 7 + .../operations/offline-kit.md | 2 + docs/modules/notify/architecture.md | 1009 +-- .../resources/schemas/notify-event@1.json | 112 +- docs/modules/policy/AGENTS.md | 2 + docs/modules/policy/README.md | 64 +- docs/modules/policy/TASKS.md | 20 +- .../policy/secret-leak-detection-readiness.md | 80 + .../policy/windows-package-readiness.md | 92 + docs/modules/scanner/AGENTS.md | 6 + docs/modules/scanner/README.md | 84 +- docs/modules/scanner/TASKS.md | 8 + docs/modules/scanner/design/README.md | 35 + docs/modules/scanner/design/macos-analyzer.md | 123 + .../scanner/design/windows-analyzer.md | 135 + .../scanner/operations/field-engagement.md | 30 + docs/modules/scheduler/architecture.md | 846 +-- docs/modules/scheduler/operations/worker.md | 164 +- docs/modules/vex-lens/implementation_plan.md | 126 +- docs/modules/vexer/TASKS.md | 9 - docs/modules/vuln-explorer/architecture.md | 31 +- .../vuln-explorer/implementation_plan.md | 140 +- docs/notifications/architecture.md | 11 +- docs/notifications/overview.md | 1 + docs/risk/risk-profiles.md | 114 +- docs/security/authority-scopes.md | 20 +- docs/technical/architecture/README.md | 2 +- docs/technical/development/README.md | 2 +- docs/ui/runs.md | 2 +- etc/authority.yaml.sample | 32 +- etc/issuer-directory.yaml.sample | 2 + ops/devops/TASKS.md | 2 + .../release/docker/Dockerfile.angular-ui | 2 +- .../release/docker/Dockerfile.dotnet-service | 104 +- samples/api/scheduler/run-summary.json | 202 +- samples/api/scheduler/run.json | 100 +- src/AdvisoryAI/StellaOps.AdvisoryAI/AGENTS.md | 2 +- .../StellaOps.AirGap.Importer/AGENTS.md | 2 +- .../HttpClientUsageAnalyzerTests.cs | 184 + ...laOps.AirGap.Policy.Analyzers.Tests.csproj | 28 + .../AnalyzerReleases.Unshipped.md | 5 + .../HttpClientUsageAnalyzer.cs | 98 + .../HttpClientUsageCodeFixProvider.cs | 79 + .../StellaOps.AirGap.Policy.Analyzers.csproj | 18 + .../EgressPolicyTests.cs | 229 + .../StellaOps.AirGap.Policy.Tests.csproj | 25 + .../AirGapEgressBlockedException.cs | 100 + .../StellaOps.AirGap.Policy/EgressDecision.cs | 43 + .../EgressHttpClientFactory.cs | 45 + .../StellaOps.AirGap.Policy/EgressPolicy.cs | 191 + .../EgressPolicyMode.cs | 17 + .../EgressPolicyOptions.cs | 99 + ...EgressPolicyServiceCollectionExtensions.cs | 282 + .../StellaOps.AirGap.Policy/EgressRequest.cs | 86 + .../StellaOps.AirGap.Policy/EgressRule.cs | 127 + .../EgressTransport.cs | 27 + .../StellaOps.AirGap.Policy/IEgressPolicy.cs | 49 + .../StellaOps.AirGap.Policy.csproj | 15 + src/AirGap/StellaOps.AirGap.Policy/TASKS.md | 38 +- .../authority/openapi.yaml | 30 +- src/Attestor/StellaOps.Attestor/TASKS.md | 2 +- .../authority/openapi.yaml | 18 +- .../StellaOpsScopesTests.cs | 21 +- .../StellaOpsClaimTypes.cs | 15 + .../StellaOpsScopes.cs | 27 + .../ServiceCollectionExtensionsTests.cs | 74 +- .../ServiceCollectionExtensions.cs | 28 + .../StellaOps.Auth.Client.csproj | 3 +- .../StellaOpsScopeAuthorizationHandler.cs | 7 + .../AuthorityServiceAccountDocument.cs | 3 + .../Documents/AuthorityTokenDocument.cs | 12 + .../Stores/AuthorityServiceAccountStore.cs | 74 + .../ServiceAccountAdminEndpointsTests.cs | 115 +- .../AuthorityWebApplicationFactory.cs | 55 +- .../NotifyAckTokenRotationEndpointTests.cs | 17 + .../ClientCredentialsAndTokenHandlersTests.cs | 332 +- .../Permalinks/VulnPermalinkServiceTests.cs | 11 +- .../VulnWorkflowTokenEndpointTests.cs | 407 ++ .../AuthorityOpenIddictConstants.cs | 17 + .../Handlers/ClientCredentialsHandlers.cs | 408 +- .../OpenIddict/Handlers/DiscoveryHandlers.cs | 8 + .../Handlers/TokenPersistenceHandlers.cs | 93 +- .../OpenIddict/TokenRequestTamperInspector.cs | 5 +- .../Permalinks/VulnPermalinkService.cs | 44 +- .../StellaOps.Authority/Program.cs | 5214 ++++++++------- .../ServiceAccountAdminContracts.cs | 3 +- .../StellaOps.Authority.csproj | 1 + .../Attachments/VulnAttachmentTokenIssuer.cs | 139 + .../Attachments/VulnAttachmentTokenModels.cs | 60 + .../Attachments/VulnAttachmentTokenPayload.cs | 21 + .../VulnAttachmentTokenVerifier.cs | 115 + .../Vulnerability/VulnTokenSigner.cs | 81 + .../Vulnerability/VulnTokenUtilities.cs | 110 + .../VulnTokenVerificationUtilities.cs | 115 + .../Workflow/VulnWorkflowAntiForgeryModels.cs | 55 + .../VulnWorkflowAntiForgeryPayload.cs | 27 + .../VulnWorkflowAntiForgeryTokenIssuer.cs | 230 + .../VulnWorkflowAntiForgeryTokenVerifier.cs | 122 + .../StellaOps.Authority/TASKS.completed.md | 2 +- src/Authority/StellaOps.Authority/TASKS.md | 9 +- .../StellaOps.Cartographer/AGENTS.md | 2 +- .../StellaOps.Concelier.Core/TASKS.md | 204 +- .../StellaOps.Concelier.Merge/TASKS.md | 5 +- .../StellaOps.EvidenceLocker/AGENTS.md | 2 +- .../Program.cs | 3 + .../StellaOps.ExportCenter.WebService.csproj | 1 + .../StellaOps.ExportCenter/TASKS.md | 8 +- .../StellaOps.Findings.Ledger/AGENTS.md | 4 +- src/Graph/StellaOps.Graph.Api/AGENTS.md | 2 +- src/Graph/StellaOps.Graph.Indexer/AGENTS.md | 6 +- .../StellaOps.IssuerDirectory/AGENTS.md | 4 +- .../StellaOps.IssuerDirectory/TASKS.md | 10 +- src/Notifier/StellaOps.Notifier/TASKS.md | 5 + .../NotifyEventKinds.cs | 30 +- src/Policy/StellaOps.Policy.Engine/Program.cs | 11 +- .../StellaOps.Policy.Engine.csproj | 3 +- .../StellaOps.Policy.Gateway/Program.cs | 36 +- .../StellaOps.Policy.Gateway.csproj | 3 +- .../__Libraries/StellaOps.Policy/AGENTS.md | 2 +- src/RiskEngine/StellaOps.RiskEngine/AGENTS.md | 2 +- src/RiskEngine/StellaOps.RiskEngine/TASKS.md | 64 +- .../TASKS.completed.md | 4 +- .../StellaOps.Scanner.WebService/TASKS.md | 6 +- src/Scanner/StellaOps.Scanner.Worker/TASKS.md | 6 +- .../EventWebhookEndpointExtensions.cs | 346 +- .../EventWebhooks/IInboundExportEventSink.cs | 22 +- .../EventWebhooks/LoggingExportEventSink.cs | 66 +- .../EventWebhooks/WebhookPayloads.cs | 212 +- .../Options/SchedulerEventsOptions.cs | 202 +- .../StellaOps.Scheduler.WebService/Program.cs | 392 +- .../TASKS.completed.md | 2 +- .../docs/SCHED-WEB-16-104-WEBHOOKS.md | 116 +- .../CanonicalJsonSerializer.cs | 464 +- .../StellaOps.Scheduler.Models/Enums.cs | 116 +- .../StellaOps.Scheduler.Models/Run.cs | 756 +-- .../Events/SchedulerEventPublisher.cs | 8 +- .../docs/SCHED-WORKER-16-204-EVENTS.md | 2 +- .../RunValidationTests.cs | 156 +- .../PlannerAndRunnerMessageTests.cs | 220 +- .../EventWebhookEndpointTests.cs | 256 +- .../SchedulerWebApplicationFactory.cs | 92 +- .../PlannerBackgroundServiceTests.cs | 822 +-- src/StellaOps.sln | 5637 +++++++++-------- .../Planning/TaskPackPlanner.cs | 399 +- .../StellaOps.TaskRunner.Core.csproj | 14 +- .../TaskPacks/TaskPackManifest.cs | 39 +- .../TaskPacks/TaskPackManifestValidator.cs | 56 +- .../Execution/FilesystemPackRunDispatcher.cs | 38 +- ...StellaOps.TaskRunner.Infrastructure.csproj | 41 +- .../FilesystemPackRunDispatcherTests.cs | 65 + .../StellaOps.TaskRunner.Tests.csproj | 3 +- .../TaskPackPlannerTests.cs | 77 +- .../TestManifests.cs | 91 +- .../StellaOps.TaskRunner.Worker/Program.cs | 31 +- src/TaskRunner/StellaOps.TaskRunner/TASKS.md | 2 +- src/VexLens/StellaOps.VexLens/AGENTS.md | 2 +- .../StellaOps.VulnExplorer.Api/AGENTS.md | 2 +- .../app/core/auth/dpop/dpop.service.spec.ts | 2 +- src/Web/StellaOps.Web/src/config/config.json | 2 +- .../src/config/config.sample.json | 2 +- src/Web/StellaOps.Web/tests/e2e/auth.spec.ts | 4 +- .../AuthorityVulnerabilityExplorerOptions.cs | 180 + .../StellaOpsAuthorityOptions.cs | 252 + 215 files changed, 19441 insertions(+), 12185 deletions(-) create mode 100644 docs/api/scanner/README.md create mode 100644 docs/api/scanner/windows-coverage.md create mode 100644 docs/api/scanner/windows-macos-summary.md create mode 100644 docs/benchmarks/scanner/README.md create mode 100644 docs/benchmarks/scanner/deep-dives/macos.md create mode 100644 docs/benchmarks/scanner/deep-dives/windows.md create mode 100644 docs/benchmarks/scanner/windows-macos-interview-template.md rename docs/dev/{30_VEXER_CONNECTOR_GUIDE.md => 30_EXCITOR_CONNECTOR_GUIDE.md} (77%) create mode 100644 docs/dev/templates/excitor-connector/manifest/connector.manifest.yaml rename docs/dev/templates/{vexer-connector/src/Vexer.MyConnector.csproj => excitor-connector/src/Excitor.MyConnector.csproj} (72%) rename docs/dev/templates/{vexer-connector => excitor-connector}/src/MyConnector.cs (95%) rename docs/dev/templates/{vexer-connector => excitor-connector}/src/MyConnectorOptions.cs (85%) rename docs/dev/templates/{vexer-connector => excitor-connector}/src/MyConnectorOptionsValidator.cs (81%) rename docs/dev/templates/{vexer-connector => excitor-connector}/src/MyConnectorPlugin.cs (83%) delete mode 100644 docs/dev/templates/vexer-connector/manifest/connector.manifest.yaml create mode 100644 docs/migration/no-merge.md rename docs/modules/{vexer => excitor}/AGENTS.md (85%) rename docs/modules/{vexer => excitor}/README.md (76%) create mode 100644 docs/modules/excitor/TASKS.md rename docs/modules/{vexer => excitor}/architecture.md (89%) rename docs/modules/{vexer => excitor}/implementation_plan.md (93%) rename docs/modules/{vexer => excitor}/scoring.md (70%) create mode 100644 docs/modules/policy/secret-leak-detection-readiness.md create mode 100644 docs/modules/policy/windows-package-readiness.md create mode 100644 docs/modules/scanner/design/README.md create mode 100644 docs/modules/scanner/design/macos-analyzer.md create mode 100644 docs/modules/scanner/design/windows-analyzer.md create mode 100644 docs/modules/scanner/operations/field-engagement.md delete mode 100644 docs/modules/vexer/TASKS.md create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/HttpClientUsageAnalyzerTests.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/StellaOps.AirGap.Policy.Analyzers.Tests.csproj create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageAnalyzer.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageCodeFixProvider.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/StellaOps.AirGap.Policy.Analyzers.csproj create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/StellaOps.AirGap.Policy.Tests.csproj create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/AirGapEgressBlockedException.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressDecision.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicy.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyMode.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyOptions.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyServiceCollectionExtensions.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRequest.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRule.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressTransport.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/IEgressPolicy.cs create mode 100644 src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Vulnerability/VulnWorkflowTokenEndpointTests.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenIssuer.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenModels.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenPayload.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenVerifier.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenSigner.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenUtilities.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenVerificationUtilities.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryModels.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryPayload.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenIssuer.cs create mode 100644 src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenVerifier.cs create mode 100644 src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/FilesystemPackRunDispatcherTests.cs create mode 100644 src/__Libraries/StellaOps.Configuration/AuthorityVulnerabilityExplorerOptions.cs diff --git a/NuGet.config b/NuGet.config index 024f15b2..2b46621d 100644 --- a/NuGet.config +++ b/NuGet.config @@ -29,10 +29,7 @@ - - - - + diff --git a/deploy/compose/env/airgap.env.example b/deploy/compose/env/airgap.env.example index cfc22ef3..73beab11 100644 --- a/deploy/compose/env/airgap.env.example +++ b/deploy/compose/env/airgap.env.example @@ -10,6 +10,7 @@ AUTHORITY_PORT=8440 SIGNER_POE_INTROSPECT_URL=file:///offline/poe/introspect.json SIGNER_PORT=8441 ATTESTOR_PORT=8442 +# Secrets for Issuer Directory are provided via issuer-directory.mongo.env (see etc/secrets/issuer-directory.mongo.secret.example). ISSUER_DIRECTORY_PORT=8447 ISSUER_DIRECTORY_MONGO_CONNECTION_STRING=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017 ISSUER_DIRECTORY_SEED_CSAF=true diff --git a/deploy/compose/env/dev.env.example b/deploy/compose/env/dev.env.example index f822a6dc..ed81829c 100644 --- a/deploy/compose/env/dev.env.example +++ b/deploy/compose/env/dev.env.example @@ -10,6 +10,7 @@ AUTHORITY_PORT=8440 SIGNER_POE_INTROSPECT_URL=https://licensing.svc.local/introspect SIGNER_PORT=8441 ATTESTOR_PORT=8442 +# Secrets for Issuer Directory are provided via issuer-directory.mongo.env (see etc/secrets/issuer-directory.mongo.secret.example). ISSUER_DIRECTORY_PORT=8447 ISSUER_DIRECTORY_MONGO_CONNECTION_STRING=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017 ISSUER_DIRECTORY_SEED_CSAF=true diff --git a/deploy/compose/env/prod.env.example b/deploy/compose/env/prod.env.example index d7974731..2f43d9c1 100644 --- a/deploy/compose/env/prod.env.example +++ b/deploy/compose/env/prod.env.example @@ -12,6 +12,7 @@ AUTHORITY_PORT=8440 SIGNER_POE_INTROSPECT_URL=https://licensing.prod.stella-ops.org/introspect SIGNER_PORT=8441 ATTESTOR_PORT=8442 +# Secrets for Issuer Directory are provided via issuer-directory.mongo.env (see etc/secrets/issuer-directory.mongo.secret.example). ISSUER_DIRECTORY_PORT=8447 ISSUER_DIRECTORY_MONGO_CONNECTION_STRING=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017 ISSUER_DIRECTORY_SEED_CSAF=true diff --git a/deploy/compose/env/stage.env.example b/deploy/compose/env/stage.env.example index 60362532..e7f652da 100644 --- a/deploy/compose/env/stage.env.example +++ b/deploy/compose/env/stage.env.example @@ -10,6 +10,7 @@ AUTHORITY_PORT=8440 SIGNER_POE_INTROSPECT_URL=https://licensing.stage.stella-ops.internal/introspect SIGNER_PORT=8441 ATTESTOR_PORT=8442 +# Secrets for Issuer Directory are provided via issuer-directory.mongo.env (see etc/secrets/issuer-directory.mongo.secret.example). ISSUER_DIRECTORY_PORT=8447 ISSUER_DIRECTORY_MONGO_CONNECTION_STRING=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017 ISSUER_DIRECTORY_SEED_CSAF=true diff --git a/docs/11_AUTHORITY.md b/docs/11_AUTHORITY.md index c0bedc6c..c5670a8d 100644 --- a/docs/11_AUTHORITY.md +++ b/docs/11_AUTHORITY.md @@ -45,7 +45,7 @@ Authority persists every issued token in MongoDB so operators can audit or revok - **Client ID**: `console-web` - **Grants**: `authorization_code` (PKCE required), `refresh_token` - **Audience**: `console` -- **Scopes**: `openid`, `profile`, `email`, `advisory:read`, `advisory-ai:view`, `vex:read`, `aoc:verify`, `findings:read`, `orch:read`, `vuln:read` +- **Scopes**: `openid`, `profile`, `email`, `advisory:read`, `advisory-ai:view`, `vex:read`, `aoc:verify`, `findings:read`, `orch:read`, `vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit` - **Redirect URIs** (defaults): `https://console.stella-ops.local/oidc/callback` - **Post-logout redirect**: `https://console.stella-ops.local/` - **Tokens**: Access tokens inherit the global 2 minute lifetime; refresh tokens remain short-lived (30 days) and can be exchanged silently via `/token`. @@ -111,7 +111,7 @@ Resource servers (Concelier WebService, Backend, Agent) **must not** assume in-m | `export-center-admin` | Export Center administrative automation | `export.viewer`, `export.operator`, `export.admin` | `dpop` | `tenant-default` | | `notify-service` | Notify WebService API | `notify.viewer`, `notify.operator` | `dpop` | `tenant-default` | | `notify-admin` | Notify administrative automation | `notify.viewer`, `notify.operator`, `notify.admin` | `dpop` | `tenant-default` | -| `vuln-explorer-ui` | Vuln Explorer UI/API | `vuln:read` | `dpop` | `tenant-default` | +| `vuln-explorer-ui` | Vuln Explorer UI/API | `vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit` | `dpop` | `tenant-default` | | `signals-uploader` | Reachability sensor ingestion | `signals:write`, `signals:read`, `aoc:verify` | `dpop` | `tenant-default` | > **Secret hygiene (2025‑10‑27):** The repository includes a convenience `etc/authority.yaml` for compose/helm smoke tests. Every entry’s `secretFile` points to `etc/secrets/*.secret`, which ship with `*-change-me` placeholders—replace them with strong values (and wire them through your vault/secret manager) before issuing tokens in CI, staging, or production. @@ -153,11 +153,18 @@ Graph Explorer introduces dedicated scopes: `graph:write` for Cartographer build - **Administrative controls** – Changes to channel secrets, quiet hours, or escalation policies require `notify.admin`. Authority logs these operations and surfaces `authority.notify.scope_violation` when tokens omit the scope or tenant. - **Least privilege** – Assign `notify.admin` sparingly (platform operations, DR automation). Day-to-day rule editing should rely on `notify.operator` scoped per tenant. -#### Vuln Explorer permalinks +#### Vuln Explorer scopes, ABAC, and permalinks -- **Scope** – `vuln:read` authorises Vuln Explorer to fetch advisory/linkset evidence and issue shareable links. Assign it only to front-end/API clients that must render vulnerability details. -- **Signed links** – `POST /permalinks/vuln` (requires `vuln:read`) accepts `{ "tenant": "tenant-a", "resourceKind": "vulnerability", "state": { ... }, "expiresInSeconds": 86400 }` and returns a JWT (`token`) plus `issuedAt`/`expiresAt`. The token embeds the tenant, requested state, and `vuln:read` scope and is signed with the same Authority signing keys published via `/jwks`. -- **Validation** – Resource servers verify the permalink using cached JWKS: check signature, ensure the tenant matches the current request context, honour the expiry, and enforce the contained `vuln:read` scope. The payload’s `resource.state` block is opaque JSON so UIs can round-trip filters/search terms without new schema changes. +- **Scopes** – `vuln:view` unlocks read-only access and permalink issuance, `vuln:investigate` allows triage actions (assignment, comments, remediation notes), `vuln:operate` unlocks state transitions and workflow execution, and `vuln:audit` exposes immutable ledgers/exports. The legacy `vuln:read` scope is still emitted for backward compatibility but new clients should request the granular scopes. +- **ABAC attributes** – Tenant roles can project attribute filters (`env`, `owner`, `business_tier`) via the `attributes` block in `authority.yaml` (see the sample `role/vuln-*` definitions). Authority now enforces the same filters on token issuance: client-credential requests must supply `vuln_env`, `vuln_owner`, and `vuln_business_tier` parameters when multiple values are configured, and the values must match the configured allow-list (or `*`). The accepted value pattern is `[a-z0-9:_-]{1,128}`. Issued tokens embed the resolved filters as `stellaops:vuln_env`, `stellaops:vuln_owner`, and `stellaops:vuln_business_tier` claims, and Authority persists the resulting actor chain plus service-account metadata in Mongo for auditability. +- **Service accounts** – Delegated Vuln Explorer identities (`svc-vuln-*`) should include the attribute filters in their seed definition. Authority enforces the supplied `attributes` during issuance and stores the selected values on the delegation token, making downstream revocation/audit exports aware of the effective ABAC envelope. +- **Token request parameters** – Minimum metadata for Vuln Explorer service accounts: + - `service_account`: requested service-account id (still required). + - `vuln_env`: single value or `*` (required when multiple environments are configured). + - `vuln_owner`: owning team/code; optional only when the service account allows a single value. + - `vuln_business_tier`: business criticality tier (`tier-1`, `tier-2`, etc.). + Authority rejects missing parameters with `invalid_request` and records the violation via `authority.vuln_attr_*` audit properties. +- **Signed links** – `POST /permalinks/vuln` requires `vuln:view`. The resulting JWT carries `vuln:view` plus the transitional `vuln:read` scope to preserve older consumers. Validation remains unchanged: verify the signature against `/jwks`, confirm tenant alignment, honour expiry, and enforce the scopes before honouring the permalink payload. ## 4. Revocation Pipeline Authority centralises revocation in `authority_revocations` with deterministic categories: @@ -453,6 +460,20 @@ tenants: * `serviceAccounts[].authorizedClients` restricts which OAuth clients may assume the delegate. Leave empty to allow any tenant client. * `tenants[].delegation.maxActiveTokens` optionally overrides the quota for a specific tenant. +**Bootstrap administration** + +Bootstrap operators can inspect and rotate delegated identities via the internal API group: + +```text +GET /internal/service-accounts?tenant={tenantId} +GET /internal/service-accounts/{accountId}/tokens +POST /internal/service-accounts/{accountId}/revocations +``` + +Requests must include the bootstrap API key header (`X-StellaOps-Bootstrap-Key`). Listing returns the seeded accounts with their configuration; the token listing call shows currently active delegation tokens (status, client, scopes, actor chain) and the revocation endpoint supports bulk or targeted token revocation with audit logging. + +Bootstrap seeding reuses the existing Mongo `_id`/`createdAt` values. When Authority restarts with updated configuration it upserts documents without mutating immutable fields, avoiding duplicate or conflicting service-account records. + **Requesting a delegated token** ```bash diff --git a/docs/11_DATA_SCHEMAS.md b/docs/11_DATA_SCHEMAS.md index 4e32cf4e..b4f84a6f 100755 --- a/docs/11_DATA_SCHEMAS.md +++ b/docs/11_DATA_SCHEMAS.md @@ -1,550 +1,550 @@ -# Data Schemas & Persistence Contracts - -*Audience* – backend developers, plug‑in authors, DB admins. -*Scope* – describes **Redis**, **MongoDB** (optional), and on‑disk blob shapes that power Stella Ops. - ---- - -## 0 Document Conventions - -* **CamelCase** for JSON. -* All timestamps are **RFC 3339 / ISO 8601** with `Z` (UTC). -* `⭑` = planned but *not* shipped yet (kept on Feature Matrix “To Do”). - ---- - -## 1 SBOM Wrapper Envelope - -Every SBOM blob (regardless of format) is stored on disk or in object storage with a *sidecar* JSON file that indexes it for the scanners. - -#### 1.1 JSON Shape - -```jsonc -{ - "id": "sha256:417f…", // digest of the SBOM *file* itself - "imageDigest": "sha256:e2b9…", // digest of the original container image - "created": "2025-07-14T07:02:13Z", - "format": "trivy-json-v2", // NEW enum: trivy-json-v2 | spdx-json | cyclonedx-json - "layers": [ - "sha256:d38b…", // layer digests (ordered) - "sha256:af45…" - ], - "partial": false, // true => delta SBOM (only some layers) - "provenanceId": "prov_0291" // ⭑ link to SLSA attestation (Q1‑2026) -} -``` - -*`format`* **NEW** – added to support **multiple SBOM formats**. -*`partial`* **NEW** – true when generated via the **delta SBOM** flow (§1.3). - -#### 1.2 File‑system Layout - -``` -blobs/ - ├─ 417f… # digest prefix - │   ├─ sbom.json # payload (any format) - │   └─ sbom.meta.json # wrapper (shape above) -``` - -> **Note** – blob storage can point at S3, MinIO, or plain disk; driver plug‑ins adapt. - -#### 1.3 Delta SBOM Extension - -When `partial: true`, *only* the missing layers have been scanned. -Merging logic inside `scanning` module stitches new data onto the cached full SBOM in Redis. - ---- - -## 2 Redis Keyspace - -| Key pattern | Type | TTL | Purpose | -|-------------------------------------|---------|------|--------------------------------------------------| -| `scan:<digest>` | string | ∞ | Last scan JSON result (as returned by `/scan`) | -| `layers:<digest>` | set | 90d | Layers already possessing SBOMs (delta cache) | -| `policy:active` | string | ∞ | YAML **or** Rego ruleset | -| `quota:<token>` | string | *until next UTC midnight* | Per‑token scan counter for Free tier ({{ quota_token }} scans). | -| `policy:history` | list | ∞ | Change audit IDs (see Mongo) | -| `feed:nvd:json` | string | 24h | Normalised feed snapshot | -| `locator:<imageDigest>` | string | 30d | Maps image digest → sbomBlobId | -| `metrics:…` | various | — | Prom / OTLP runtime metrics | - -> **Delta SBOM** uses `layers:*` to skip work in <20 ms. -> **Quota enforcement** increments `quota:` atomically; when {{ quota_token }} the API returns **429**. - ---- - -## 3 MongoDB Collections (Optional) - -Only enabled when `MONGO_URI` is supplied (for long‑term audit). - -| Collection | Shape (summary) | Indexes | -|--------------------|------------------------------------------------------------|-------------------------------------| -| `sbom_history` | Wrapper JSON + `replaceTs` on overwrite | `{imageDigest}` `{created}` | -| `policy_versions` | `{_id, yaml, rego, authorId, created}` | `{created}` | -| `attestations` ⭑ | SLSA provenance doc + Rekor log pointer | `{imageDigest}` | -| `audit_log` | Fully rendered RFC 5424 entries (UI & CLI actions) | `{userId}` `{ts}` | - -Schema detail for **policy_versions**: - -Samples live under `samples/api/scheduler/` (e.g., `schedule.json`, `run.json`, `impact-set.json`, `audit.json`) and mirror the canonical serializer output shown below. - -```jsonc -{ - "_id": "6619e90b8c5e1f76", - "yaml": "version: 1.0\nrules:\n - …", - "rego": null, // filled when Rego uploaded - "authorId": "u_1021", - "created": "2025-07-14T08:15:04Z", - "comment": "Imported via API" -} -``` - -### 3.1 Scheduler Sprints 16 Artifacts - -**Collections.** `schedules`, `runs`, `impact_snapshots`, `audit` (module‑local). All documents reuse the canonical JSON emitted by `StellaOps.Scheduler.Models` so agents and fixtures remain deterministic. - -#### 3.1.1 Schedule (`schedules`) - -```jsonc -{ - "_id": "sch_20251018a", - "tenantId": "tenant-alpha", - "name": "Nightly Prod", - "enabled": true, - "cronExpression": "0 2 * * *", - "timezone": "UTC", - "mode": "analysis-only", - "selection": { - "scope": "by-namespace", - "namespaces": ["team-a", "team-b"], - "repositories": ["app/service-api"], - "includeTags": ["canary", "prod"], - "labels": [{"key": "env", "values": ["prod", "staging"]}], - "resolvesTags": true - }, - "onlyIf": {"lastReportOlderThanDays": 7, "policyRevision": "policy@42"}, - "notify": {"onNewFindings": true, "minSeverity": "high", "includeKev": true}, - "limits": {"maxJobs": 1000, "ratePerSecond": 25, "parallelism": 4}, - "subscribers": ["notify.ops"], - "createdAt": "2025-10-18T22:00:00Z", - "createdBy": "svc_scheduler", - "updatedAt": "2025-10-18T22:00:00Z", - "updatedBy": "svc_scheduler" -} -``` - -*Constraints*: arrays are alphabetically sorted; `selection.tenantId` is optional but when present must match `tenantId`. Cron expressions are validated for newline/length, timezones are validated via `TimeZoneInfo`. - -#### 3.1.2 Run (`runs`) - -```jsonc -{ - "_id": "run_20251018_0001", - "tenantId": "tenant-alpha", - "scheduleId": "sch_20251018a", - "trigger": "feedser", - "state": "running", - "stats": { - "candidates": 1280, - "deduped": 910, - "queued": 624, - "completed": 310, - "deltas": 42, - "newCriticals": 7, - "newHigh": 11, - "newMedium": 18, - "newLow": 6 - }, - "reason": {"feedserExportId": "exp-20251018-03"}, - "createdAt": "2025-10-18T22:03:14Z", - "startedAt": "2025-10-18T22:03:20Z", - "finishedAt": null, - "error": null, - "deltas": [ - { - "imageDigest": "sha256:a1b2c3", - "newFindings": 3, - "newCriticals": 1, - "newHigh": 1, - "newMedium": 1, - "newLow": 0, - "kevHits": ["CVE-2025-0002"], - "topFindings": [ - { - "purl": "pkg:rpm/openssl@3.0.12-5.el9", - "vulnerabilityId": "CVE-2025-0002", - "severity": "critical", - "link": "https://ui.internal/scans/sha256:a1b2c3" - } - ], - "attestation": {"uuid": "rekor-314", "verified": true}, - "detectedAt": "2025-10-18T22:03:21Z" - } - ] -} -``` - -Counters are clamped to ≥0, timestamps are converted to UTC, and delta arrays are sorted (critical → info severity precedence, then vulnerability id). Missing `deltas` implies "no change" snapshots. - -#### 3.1.3 Impact Snapshot (`impact_snapshots`) - -```jsonc -{ - "selector": { - "scope": "all-images", - "tenantId": "tenant-alpha" - }, - "images": [ - { - "imageDigest": "sha256:f1e2d3", - "registry": "registry.internal", - "repository": "app/api", - "namespaces": ["team-a"], - "tags": ["prod"], - "usedByEntrypoint": true, - "labels": {"env": "prod"} - } - ], - "usageOnly": true, - "generatedAt": "2025-10-18T22:02:58Z", - "total": 412, - "snapshotId": "impact-20251018-1" -} -``` - -Images are deduplicated and sorted by digest. Label keys are normalised to lowercase to avoid case‑sensitive duplicates during reconciliation. `snapshotId` enables run planners to compare subsequent snapshots for drift. - -#### 3.1.4 Audit (`audit`) - -```jsonc -{ - "_id": "audit_169754", - "tenantId": "tenant-alpha", - "category": "scheduler", - "action": "pause", - "occurredAt": "2025-10-18T22:10:00Z", - "actor": {"actorId": "user_admin", "displayName": "Cluster Admin", "kind": "user"}, - "scheduleId": "sch_20251018a", - "correlationId": "corr-123", - "metadata": {"details": "schedule paused", "reason": "maintenance"}, - "message": "Paused via API" -} -``` - -Metadata keys are lowercased, first‑writer wins (duplicates with different casing are ignored), and optional IDs (`scheduleId`, `runId`) are trimmed when empty. Use the canonical serializer when emitting events so audit digests remain reproducible. - -#### 3.1.5 Run Summary (`run_summaries`) - -Materialized view powering the Scheduler UI dashboards. Stores the latest roll-up per schedule/tenant, enabling quick “last run” banners and sparkline counters without scanning the full `runs` collection. - -```jsonc -{ - "tenantId": "tenant-alpha", - "scheduleId": "sch_20251018a", - "updatedAt": "2025-10-18T22:10:10Z", - "lastRun": { - "runId": "run_20251018_0001", - "trigger": "feedser", - "state": "completed", - "createdAt": "2025-10-18T22:03:14Z", - "startedAt": "2025-10-18T22:03:20Z", - "finishedAt": "2025-10-18T22:08:45Z", - "stats": { - "candidates": 1280, - "deduped": 910, - "queued": 0, - "completed": 910, - "deltas": 42, - "newCriticals": 7, - "newHigh": 11, - "newMedium": 18, - "newLow": 6 - }, - "error": null - }, - "recent": [ - { - "runId": "run_20251018_0001", - "trigger": "feedser", - "state": "completed", - "createdAt": "2025-10-18T22:03:14Z", - "startedAt": "2025-10-18T22:03:20Z", - "finishedAt": "2025-10-18T22:08:45Z", - "stats": { - "candidates": 1280, - "deduped": 910, - "queued": 0, - "completed": 910, - "deltas": 42, - "newCriticals": 7, - "newHigh": 11, - "newMedium": 18, - "newLow": 6 - }, - "error": null - }, - { - "runId": "run_20251017_0003", - "trigger": "cron", - "state": "error", - "createdAt": "2025-10-17T22:01:02Z", - "startedAt": "2025-10-17T22:01:08Z", - "finishedAt": "2025-10-17T22:04:11Z", - "stats": { - "candidates": 1040, - "deduped": 812, - "queued": 0, - "completed": 640, - "deltas": 18, - "newCriticals": 2, - "newHigh": 4, - "newMedium": 7, - "newLow": 3 - }, - "error": "scanner timeout" - } - ], - "counters": { - "total": 3, - "planning": 0, - "queued": 0, - "running": 0, - "completed": 1, - "error": 1, - "cancelled": 1, - "totalDeltas": 60, - "totalNewCriticals": 9, - "totalNewHigh": 15, - "totalNewMedium": 25, - "totalNewLow": 9 - } -} -``` - -- `_id` combines `tenantId` and `scheduleId` (`tenant:schedule`). -- `recent` contains the 20 most recent runs ordered by `createdAt` (UTC). Updates replace the existing entry for a run to respect state transitions. -- `counters` aggregate over the retained window (20 runs) for quick trend indicators. Totals are recomputed after every update. -- Schedulers should call the projection service after every run state change so the cache mirrors planner/runner progress. - -Sample file: `samples/api/scheduler/run-summary.json`. - ---- - -## 4 Policy Schema (YAML v1.0) - -Minimal viable grammar (subset of OSV‑SCHEMA ideas). - -```yaml -version: "1.0" -rules: - - name: Block Critical - severity: [Critical] - action: block - - name: Ignore Low Dev - severity: [Low, None] - environments: [dev, staging] - action: ignore - expires: "2026-01-01" - - name: Escalate RegionalFeed High - sources: [NVD, CNNVD, CNVD, ENISA, JVN, BDU] - severity: [High, Critical] - action: escalate -``` - -Validation is performed by `policy:mapping.yaml` JSON‑Schema embedded in backend. - -Canonical schema source: `src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-schema@1.json` (embedded into `StellaOps.Policy`). -`PolicyValidationCli` (see `src/Policy/__Libraries/StellaOps.Policy/PolicyValidationCli.cs`) provides the reusable command handler that the main CLI wires up; in the interim it can be invoked from a short host like: - -```csharp -await new PolicyValidationCli().RunAsync(new PolicyValidationCliOptions -{ - Inputs = new[] { "policies/root.yaml" }, - Strict = true, -}); -``` - -### 4.1 Rego Variant (Advanced – TODO) - -*Accepted but stored as‑is in `rego` field.* -Evaluated via internal **OPA** side‑car once feature graduates from TODO list. - -### 4.2 Policy Scoring Config (JSON) - -*Schema id.* `https://schemas.stella-ops.org/policy/policy-scoring-schema@1.json` -*Source.* `src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-scoring-schema@1.json` (embedded in `StellaOps.Policy`), default fixture at `src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-scoring-default.json`. - -```jsonc -{ - "version": "1.0", - "severityWeights": {"Critical": 90, "High": 75, "Unknown": 60, "...": 0}, - "quietPenalty": 45, - "warnPenalty": 15, - "ignorePenalty": 35, - "trustOverrides": {"vendor": 1.0, "distro": 0.85}, - "reachabilityBuckets": {"entrypoint": 1.0, "direct": 0.85, "runtime": 0.45, "unknown": 0.5}, - "unknownConfidence": { - "initial": 0.8, - "decayPerDay": 0.05, - "floor": 0.2, - "bands": [ - {"name": "high", "min": 0.65}, - {"name": "medium", "min": 0.35}, - {"name": "low", "min": 0.0} - ] - } -} -``` - -Validation occurs alongside policy binding (`PolicyScoringConfigBinder`), producing deterministic digests via `PolicyScoringConfigDigest`. Bands are ordered descending by `min` so consumers can resolve confidence tiers deterministically. Reachability buckets are case-insensitive keys (`entrypoint`, `direct`, `indirect`, `runtime`, `unreachable`, `unknown`) with numeric multipliers (default ≤1.0). - -**Runtime usage** -- `trustOverrides` are matched against `finding.tags` (`trust:`) first, then `finding.source`/`finding.vendor`; missing keys default to `1.0`. -- `reachabilityBuckets` consume `finding.tags` with prefix `reachability:` (fallback `usage:` or `unknown`). Missing buckets fall back to `unknown` weight when present, otherwise `1.0`. -- Policy verdicts expose scoring inputs (`severityWeight`, `trustWeight`, `reachabilityWeight`, `baseScore`, penalties) plus unknown-state metadata (`unknownConfidence`, `unknownAgeDays`, `confidenceBand`) for auditability. See `samples/policy/policy-preview-unknown.json` and `samples/policy/policy-report-unknown.json` for offline reference payloads validated against the published schemas below. - -Validate the samples locally with **Ajv** before publishing changes: - -```bash -# install once per checkout (offline-safe): -npm install --no-save ajv-cli@5 ajv-formats@2 - -npx ajv validate --spec=draft2020 -c ajv-formats \ - -s docs/schemas/policy-preview-sample@1.json \ - -d samples/policy/policy-preview-unknown.json - -npx ajv validate --spec=draft2020 -c ajv-formats \ - -s docs/schemas/policy-report-sample@1.json \ - -d samples/policy/policy-report-unknown.json -``` -- Unknown confidence derives from `unknown-age-days:` (preferred) or `unknown-since:` + `observed-at:` tags; with no hints the engine keeps `initial` confidence. Values decay by `decayPerDay` down to `floor`, then resolve to the first matching `bands[].name`. - ---- - -## 5 SLSA Attestation Schema ⭑ - -Planned for Q1‑2026 (kept here for early plug‑in authors). - -```jsonc -{ - "id": "prov_0291", - "imageDigest": "sha256:e2b9…", - "buildType": "https://slsa.dev/container/v1", - "builder": { - "id": "https://git.stella-ops.ru/ci/stella-runner@sha256:f7b7…" - }, - "metadata": { - "invocation": { - "parameters": {"GIT_SHA": "f6a1…"}, - "buildStart": "2025-07-14T06:59:17Z", - "buildEnd": "2025-07-14T07:01:22Z" - }, - "completeness": {"parameters": true} - }, - "materials": [ - {"uri": "git+https://git…", "digest": {"sha1": "f6a1…"}} - ], - "rekorLogIndex": 99817 // entry in local Rekor mirror -} -``` - ---- - -## 6 Notify Foundations (Rule · Channel · Event) - -*Sprint 15 target* – canonically describe the Notify data shapes that UI, workers, and storage consume. JSON Schemas live under `docs/modules/notify/resources/schemas/` and deterministic fixtures under `docs/modules/notify/resources/samples/`. - -| Artifact | Schema | Sample | -|----------|--------|--------| -| **Rule** (catalogued routing logic) | `docs/modules/notify/resources/schemas/notify-rule@1.json` | `docs/modules/notify/resources/samples/notify-rule@1.sample.json` | -| **Channel** (delivery endpoint definition) | `docs/modules/notify/resources/schemas/notify-channel@1.json` | `docs/modules/notify/resources/samples/notify-channel@1.sample.json` | -| **Template** (rendering payload) | `docs/modules/notify/resources/schemas/notify-template@1.json` | `docs/modules/notify/resources/samples/notify-template@1.sample.json` | -| **Event envelope** (Notify ingest surface) | `docs/modules/notify/resources/schemas/notify-event@1.json` | `docs/modules/notify/resources/samples/notify-event@1.sample.json` | - -### 6.1 Rule highlights (`notify-rule@1`) - -* Keys are lower‑cased camelCase. `schemaVersion` (`notify.rule@1`), `ruleId`, `tenantId`, `name`, `match`, `actions`, `createdAt`, and `updatedAt` are mandatory. -* `match.eventKinds`, `match.verdicts`, and other array selectors are pre‑sorted and case‑normalized (e.g. `scanner.report.ready`). -* `actions[].throttle` serialises as ISO 8601 duration (`PT5M`), mirroring worker backoff guardrails. -* `vex` gates let operators exclude accepted/not‑affected justifications; omit the block to inherit default behaviour. -* Use `StellaOps.Notify.Models.NotifySchemaMigration.UpgradeRule(JsonNode)` when deserialising legacy payloads that might lack `schemaVersion` or retain older revisions. -* Soft deletions persist `deletedAt` in Mongo (and disable the rule); repository queries automatically filter them. - -### 6.2 Channel highlights (`notify-channel@1`) - -* `schemaVersion` is pinned to `notify.channel@1` and must accompany persisted documents. -* `type` matches plug‑in identifiers (`slack`, `teams`, `email`, `webhook`, `custom`). -* `config.secretRef` stores an external secret handle (Authority, Vault, K8s). Notify never persists raw credentials. -* Optional `config.limits.timeout` uses ISO 8601 durations identical to rule throttles; concurrency/RPM defaults apply when absent. -* `StellaOps.Notify.Models.NotifySchemaMigration.UpgradeChannel(JsonNode)` backfills the schema version when older documents omit it. -* Channels share the same soft-delete marker (`deletedAt`) so operators can restore prior configuration without purging history. - -### 6.3 Event envelope (`notify-event@1`) - -* Aligns with the platform event contract—`eventId` UUID, RFC 3339 `ts`, tenant isolation enforced. -* Enumerated `kind` covers the initial Notify surface (`scanner.report.ready`, `scheduler.rescan.delta`, `zastava.admission`, etc.). -* `scope.labels`/`scope.attributes` and top-level `attributes` mirror the metadata dictionaries workers surface for templating and audits. -* Notify workers use the same migration helper to wrap event payloads before template rendering, so schema additions remain additive. - -### 6.4 Template highlights (`notify-template@1`) - -* Carries the presentation key (`channelType`, `key`, `locale`) and the raw template body; `schemaVersion` is fixed to `notify.template@1`. -* `renderMode` enumerates supported engines (`markdown`, `html`, `adaptiveCard`, `plainText`, `json`) aligning with `NotifyTemplateRenderMode`. -* `format` signals downstream connector expectations (`slack`, `teams`, `email`, `webhook`, `json`). -* Upgrade legacy definitions with `NotifySchemaMigration.UpgradeTemplate(JsonNode)` to auto-apply the new schema version and ordering. -* Templates also record soft deletes via `deletedAt`; UI/API skip them by default while retaining revision history. - -**Validation loop:** - -```bash -# Validate Notify schemas and samples (matches Docs CI) -for schema in docs/modules/notify/resources/schemas/*.json; do - npx ajv compile -c ajv-formats -s "$schema" -done - -for sample in docs/modules/notify/resources/samples/*.sample.json; do - schema="docs/modules/notify/resources/schemas/$(basename "${sample%.sample.json}").json" - npx ajv validate -c ajv-formats -s "$schema" -d "$sample" -done -``` - -Integration tests can embed the sample fixtures to guarantee deterministic serialisation from the `StellaOps.Notify.Models` DTOs introduced in Sprint 15. - ---- - -## 6 Validator Contracts - -* For SBOM wrapper – `ISbomValidator` (DLL plug‑in) must return *typed* error list. -* For YAML policies – JSON‑Schema at `/schemas/policy‑v1.json`. -* For Rego – OPA `opa eval --fail-defined` under the hood. -* For **Free‑tier quotas** – `IQuotaService` integration tests ensure `quota:` resets at UTC midnight and produces correct `Retry‑After` headers. - ---- - -## 7 Migration Notes - -1. **Add `format` column** to existing SBOM wrappers; default to `trivy-json-v2`. -2. **Populate `layers` & `partial`** via backfill script (ship with `stellopsctl migrate` wizard). -3. Policy YAML previously stored in Redis → copy to Mongo if persistence enabled. -4. Prepare `attestations` collection (empty) – safe to create in advance. - ---- - -## 8 Open Questions / Future Work - -* How to de‑duplicate *identical* Rego policies differing only in whitespace? -* Embed *GOST 34.11‑2018* digests when users enable Russian crypto suite? -* Should enterprise tiers share the same Redis quota keys or switch to JWT claim `tier != Free` bypass? -* Evaluate sliding‑window quota instead of strict daily reset. -* Consider rate‑limit for `/layers/missing` to avoid brute‑force enumeration. - ---- - -## 9 Change Log - -| Date | Note | -|------------|--------------------------------------------------------------------------------| -| 2025‑07‑14 | **Added:** `format`, `partial`, delta cache keys, YAML policy schema v1.0. | -| 2025‑07‑12 | **Initial public draft** – SBOM wrapper, Redis keyspace, audit collections. | - ---- +# Data Schemas & Persistence Contracts + +*Audience* – backend developers, plug‑in authors, DB admins. +*Scope* – describes **Redis**, **MongoDB** (optional), and on‑disk blob shapes that power Stella Ops. + +--- + +## 0 Document Conventions + +* **CamelCase** for JSON. +* All timestamps are **RFC 3339 / ISO 8601** with `Z` (UTC). +* `⭑` = planned but *not* shipped yet (kept on Feature Matrix “To Do”). + +--- + +## 1 SBOM Wrapper Envelope + +Every SBOM blob (regardless of format) is stored on disk or in object storage with a *sidecar* JSON file that indexes it for the scanners. + +#### 1.1 JSON Shape + +```jsonc +{ + "id": "sha256:417f…", // digest of the SBOM *file* itself + "imageDigest": "sha256:e2b9…", // digest of the original container image + "created": "2025-07-14T07:02:13Z", + "format": "trivy-json-v2", // NEW enum: trivy-json-v2 | spdx-json | cyclonedx-json + "layers": [ + "sha256:d38b…", // layer digests (ordered) + "sha256:af45…" + ], + "partial": false, // true => delta SBOM (only some layers) + "provenanceId": "prov_0291" // ⭑ link to SLSA attestation (Q1‑2026) +} +``` + +*`format`* **NEW** – added to support **multiple SBOM formats**. +*`partial`* **NEW** – true when generated via the **delta SBOM** flow (§1.3). + +#### 1.2 File‑system Layout + +``` +blobs/ + ├─ 417f… # digest prefix + │   ├─ sbom.json # payload (any format) + │   └─ sbom.meta.json # wrapper (shape above) +``` + +> **Note** – blob storage can point at S3, MinIO, or plain disk; driver plug‑ins adapt. + +#### 1.3 Delta SBOM Extension + +When `partial: true`, *only* the missing layers have been scanned. +Merging logic inside `scanning` module stitches new data onto the cached full SBOM in Redis. + +--- + +## 2 Redis Keyspace + +| Key pattern | Type | TTL | Purpose | +|-------------------------------------|---------|------|--------------------------------------------------| +| `scan:<digest>` | string | ∞ | Last scan JSON result (as returned by `/scan`) | +| `layers:<digest>` | set | 90d | Layers already possessing SBOMs (delta cache) | +| `policy:active` | string | ∞ | YAML **or** Rego ruleset | +| `quota:<token>` | string | *until next UTC midnight* | Per‑token scan counter for Free tier ({{ quota_token }} scans). | +| `policy:history` | list | ∞ | Change audit IDs (see Mongo) | +| `feed:nvd:json` | string | 24h | Normalised feed snapshot | +| `locator:<imageDigest>` | string | 30d | Maps image digest → sbomBlobId | +| `metrics:…` | various | — | Prom / OTLP runtime metrics | + +> **Delta SBOM** uses `layers:*` to skip work in <20 ms. +> **Quota enforcement** increments `quota:` atomically; when {{ quota_token }} the API returns **429**. + +--- + +## 3 MongoDB Collections (Optional) + +Only enabled when `MONGO_URI` is supplied (for long‑term audit). + +| Collection | Shape (summary) | Indexes | +|--------------------|------------------------------------------------------------|-------------------------------------| +| `sbom_history` | Wrapper JSON + `replaceTs` on overwrite | `{imageDigest}` `{created}` | +| `policy_versions` | `{_id, yaml, rego, authorId, created}` | `{created}` | +| `attestations` ⭑ | SLSA provenance doc + Rekor log pointer | `{imageDigest}` | +| `audit_log` | Fully rendered RFC 5424 entries (UI & CLI actions) | `{userId}` `{ts}` | + +Schema detail for **policy_versions**: + +Samples live under `samples/api/scheduler/` (e.g., `schedule.json`, `run.json`, `impact-set.json`, `audit.json`) and mirror the canonical serializer output shown below. + +```jsonc +{ + "_id": "6619e90b8c5e1f76", + "yaml": "version: 1.0\nrules:\n - …", + "rego": null, // filled when Rego uploaded + "authorId": "u_1021", + "created": "2025-07-14T08:15:04Z", + "comment": "Imported via API" +} +``` + +### 3.1 Scheduler Sprints 16 Artifacts + +**Collections.** `schedules`, `runs`, `impact_snapshots`, `audit` (module‑local). All documents reuse the canonical JSON emitted by `StellaOps.Scheduler.Models` so agents and fixtures remain deterministic. + +#### 3.1.1 Schedule (`schedules`) + +```jsonc +{ + "_id": "sch_20251018a", + "tenantId": "tenant-alpha", + "name": "Nightly Prod", + "enabled": true, + "cronExpression": "0 2 * * *", + "timezone": "UTC", + "mode": "analysis-only", + "selection": { + "scope": "by-namespace", + "namespaces": ["team-a", "team-b"], + "repositories": ["app/service-api"], + "includeTags": ["canary", "prod"], + "labels": [{"key": "env", "values": ["prod", "staging"]}], + "resolvesTags": true + }, + "onlyIf": {"lastReportOlderThanDays": 7, "policyRevision": "policy@42"}, + "notify": {"onNewFindings": true, "minSeverity": "high", "includeKev": true}, + "limits": {"maxJobs": 1000, "ratePerSecond": 25, "parallelism": 4}, + "subscribers": ["notify.ops"], + "createdAt": "2025-10-18T22:00:00Z", + "createdBy": "svc_scheduler", + "updatedAt": "2025-10-18T22:00:00Z", + "updatedBy": "svc_scheduler" +} +``` + +*Constraints*: arrays are alphabetically sorted; `selection.tenantId` is optional but when present must match `tenantId`. Cron expressions are validated for newline/length, timezones are validated via `TimeZoneInfo`. + +#### 3.1.2 Run (`runs`) + +```jsonc +{ + "_id": "run_20251018_0001", + "tenantId": "tenant-alpha", + "scheduleId": "sch_20251018a", + "trigger": "conselier", + "state": "running", + "stats": { + "candidates": 1280, + "deduped": 910, + "queued": 624, + "completed": 310, + "deltas": 42, + "newCriticals": 7, + "newHigh": 11, + "newMedium": 18, + "newLow": 6 + }, + "reason": {"conselierExportId": "exp-20251018-03"}, + "createdAt": "2025-10-18T22:03:14Z", + "startedAt": "2025-10-18T22:03:20Z", + "finishedAt": null, + "error": null, + "deltas": [ + { + "imageDigest": "sha256:a1b2c3", + "newFindings": 3, + "newCriticals": 1, + "newHigh": 1, + "newMedium": 1, + "newLow": 0, + "kevHits": ["CVE-2025-0002"], + "topFindings": [ + { + "purl": "pkg:rpm/openssl@3.0.12-5.el9", + "vulnerabilityId": "CVE-2025-0002", + "severity": "critical", + "link": "https://ui.internal/scans/sha256:a1b2c3" + } + ], + "attestation": {"uuid": "rekor-314", "verified": true}, + "detectedAt": "2025-10-18T22:03:21Z" + } + ] +} +``` + +Counters are clamped to ≥0, timestamps are converted to UTC, and delta arrays are sorted (critical → info severity precedence, then vulnerability id). Missing `deltas` implies "no change" snapshots. + +#### 3.1.3 Impact Snapshot (`impact_snapshots`) + +```jsonc +{ + "selector": { + "scope": "all-images", + "tenantId": "tenant-alpha" + }, + "images": [ + { + "imageDigest": "sha256:f1e2d3", + "registry": "registry.internal", + "repository": "app/api", + "namespaces": ["team-a"], + "tags": ["prod"], + "usedByEntrypoint": true, + "labels": {"env": "prod"} + } + ], + "usageOnly": true, + "generatedAt": "2025-10-18T22:02:58Z", + "total": 412, + "snapshotId": "impact-20251018-1" +} +``` + +Images are deduplicated and sorted by digest. Label keys are normalised to lowercase to avoid case‑sensitive duplicates during reconciliation. `snapshotId` enables run planners to compare subsequent snapshots for drift. + +#### 3.1.4 Audit (`audit`) + +```jsonc +{ + "_id": "audit_169754", + "tenantId": "tenant-alpha", + "category": "scheduler", + "action": "pause", + "occurredAt": "2025-10-18T22:10:00Z", + "actor": {"actorId": "user_admin", "displayName": "Cluster Admin", "kind": "user"}, + "scheduleId": "sch_20251018a", + "correlationId": "corr-123", + "metadata": {"details": "schedule paused", "reason": "maintenance"}, + "message": "Paused via API" +} +``` + +Metadata keys are lowercased, first‑writer wins (duplicates with different casing are ignored), and optional IDs (`scheduleId`, `runId`) are trimmed when empty. Use the canonical serializer when emitting events so audit digests remain reproducible. + +#### 3.1.5 Run Summary (`run_summaries`) + +Materialized view powering the Scheduler UI dashboards. Stores the latest roll-up per schedule/tenant, enabling quick “last run” banners and sparkline counters without scanning the full `runs` collection. + +```jsonc +{ + "tenantId": "tenant-alpha", + "scheduleId": "sch_20251018a", + "updatedAt": "2025-10-18T22:10:10Z", + "lastRun": { + "runId": "run_20251018_0001", + "trigger": "conselier", + "state": "completed", + "createdAt": "2025-10-18T22:03:14Z", + "startedAt": "2025-10-18T22:03:20Z", + "finishedAt": "2025-10-18T22:08:45Z", + "stats": { + "candidates": 1280, + "deduped": 910, + "queued": 0, + "completed": 910, + "deltas": 42, + "newCriticals": 7, + "newHigh": 11, + "newMedium": 18, + "newLow": 6 + }, + "error": null + }, + "recent": [ + { + "runId": "run_20251018_0001", + "trigger": "conselier", + "state": "completed", + "createdAt": "2025-10-18T22:03:14Z", + "startedAt": "2025-10-18T22:03:20Z", + "finishedAt": "2025-10-18T22:08:45Z", + "stats": { + "candidates": 1280, + "deduped": 910, + "queued": 0, + "completed": 910, + "deltas": 42, + "newCriticals": 7, + "newHigh": 11, + "newMedium": 18, + "newLow": 6 + }, + "error": null + }, + { + "runId": "run_20251017_0003", + "trigger": "cron", + "state": "error", + "createdAt": "2025-10-17T22:01:02Z", + "startedAt": "2025-10-17T22:01:08Z", + "finishedAt": "2025-10-17T22:04:11Z", + "stats": { + "candidates": 1040, + "deduped": 812, + "queued": 0, + "completed": 640, + "deltas": 18, + "newCriticals": 2, + "newHigh": 4, + "newMedium": 7, + "newLow": 3 + }, + "error": "scanner timeout" + } + ], + "counters": { + "total": 3, + "planning": 0, + "queued": 0, + "running": 0, + "completed": 1, + "error": 1, + "cancelled": 1, + "totalDeltas": 60, + "totalNewCriticals": 9, + "totalNewHigh": 15, + "totalNewMedium": 25, + "totalNewLow": 9 + } +} +``` + +- `_id` combines `tenantId` and `scheduleId` (`tenant:schedule`). +- `recent` contains the 20 most recent runs ordered by `createdAt` (UTC). Updates replace the existing entry for a run to respect state transitions. +- `counters` aggregate over the retained window (20 runs) for quick trend indicators. Totals are recomputed after every update. +- Schedulers should call the projection service after every run state change so the cache mirrors planner/runner progress. + +Sample file: `samples/api/scheduler/run-summary.json`. + +--- + +## 4 Policy Schema (YAML v1.0) + +Minimal viable grammar (subset of OSV‑SCHEMA ideas). + +```yaml +version: "1.0" +rules: + - name: Block Critical + severity: [Critical] + action: block + - name: Ignore Low Dev + severity: [Low, None] + environments: [dev, staging] + action: ignore + expires: "2026-01-01" + - name: Escalate RegionalFeed High + sources: [NVD, CNNVD, CNVD, ENISA, JVN, BDU] + severity: [High, Critical] + action: escalate +``` + +Validation is performed by `policy:mapping.yaml` JSON‑Schema embedded in backend. + +Canonical schema source: `src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-schema@1.json` (embedded into `StellaOps.Policy`). +`PolicyValidationCli` (see `src/Policy/__Libraries/StellaOps.Policy/PolicyValidationCli.cs`) provides the reusable command handler that the main CLI wires up; in the interim it can be invoked from a short host like: + +```csharp +await new PolicyValidationCli().RunAsync(new PolicyValidationCliOptions +{ + Inputs = new[] { "policies/root.yaml" }, + Strict = true, +}); +``` + +### 4.1 Rego Variant (Advanced – TODO) + +*Accepted but stored as‑is in `rego` field.* +Evaluated via internal **OPA** side‑car once feature graduates from TODO list. + +### 4.2 Policy Scoring Config (JSON) + +*Schema id.* `https://schemas.stella-ops.org/policy/policy-scoring-schema@1.json` +*Source.* `src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-scoring-schema@1.json` (embedded in `StellaOps.Policy`), default fixture at `src/Policy/__Libraries/StellaOps.Policy/Schemas/policy-scoring-default.json`. + +```jsonc +{ + "version": "1.0", + "severityWeights": {"Critical": 90, "High": 75, "Unknown": 60, "...": 0}, + "quietPenalty": 45, + "warnPenalty": 15, + "ignorePenalty": 35, + "trustOverrides": {"vendor": 1.0, "distro": 0.85}, + "reachabilityBuckets": {"entrypoint": 1.0, "direct": 0.85, "runtime": 0.45, "unknown": 0.5}, + "unknownConfidence": { + "initial": 0.8, + "decayPerDay": 0.05, + "floor": 0.2, + "bands": [ + {"name": "high", "min": 0.65}, + {"name": "medium", "min": 0.35}, + {"name": "low", "min": 0.0} + ] + } +} +``` + +Validation occurs alongside policy binding (`PolicyScoringConfigBinder`), producing deterministic digests via `PolicyScoringConfigDigest`. Bands are ordered descending by `min` so consumers can resolve confidence tiers deterministically. Reachability buckets are case-insensitive keys (`entrypoint`, `direct`, `indirect`, `runtime`, `unreachable`, `unknown`) with numeric multipliers (default ≤1.0). + +**Runtime usage** +- `trustOverrides` are matched against `finding.tags` (`trust:`) first, then `finding.source`/`finding.vendor`; missing keys default to `1.0`. +- `reachabilityBuckets` consume `finding.tags` with prefix `reachability:` (fallback `usage:` or `unknown`). Missing buckets fall back to `unknown` weight when present, otherwise `1.0`. +- Policy verdicts expose scoring inputs (`severityWeight`, `trustWeight`, `reachabilityWeight`, `baseScore`, penalties) plus unknown-state metadata (`unknownConfidence`, `unknownAgeDays`, `confidenceBand`) for auditability. See `samples/policy/policy-preview-unknown.json` and `samples/policy/policy-report-unknown.json` for offline reference payloads validated against the published schemas below. + +Validate the samples locally with **Ajv** before publishing changes: + +```bash +# install once per checkout (offline-safe): +npm install --no-save ajv-cli@5 ajv-formats@2 + +npx ajv validate --spec=draft2020 -c ajv-formats \ + -s docs/schemas/policy-preview-sample@1.json \ + -d samples/policy/policy-preview-unknown.json + +npx ajv validate --spec=draft2020 -c ajv-formats \ + -s docs/schemas/policy-report-sample@1.json \ + -d samples/policy/policy-report-unknown.json +``` +- Unknown confidence derives from `unknown-age-days:` (preferred) or `unknown-since:` + `observed-at:` tags; with no hints the engine keeps `initial` confidence. Values decay by `decayPerDay` down to `floor`, then resolve to the first matching `bands[].name`. + +--- + +## 5 SLSA Attestation Schema ⭑ + +Planned for Q1‑2026 (kept here for early plug‑in authors). + +```jsonc +{ + "id": "prov_0291", + "imageDigest": "sha256:e2b9…", + "buildType": "https://slsa.dev/container/v1", + "builder": { + "id": "https://git.stella-ops.ru/ci/stella-runner@sha256:f7b7…" + }, + "metadata": { + "invocation": { + "parameters": {"GIT_SHA": "f6a1…"}, + "buildStart": "2025-07-14T06:59:17Z", + "buildEnd": "2025-07-14T07:01:22Z" + }, + "completeness": {"parameters": true} + }, + "materials": [ + {"uri": "git+https://git…", "digest": {"sha1": "f6a1…"}} + ], + "rekorLogIndex": 99817 // entry in local Rekor mirror +} +``` + +--- + +## 6 Notify Foundations (Rule · Channel · Event) + +*Sprint 15 target* – canonically describe the Notify data shapes that UI, workers, and storage consume. JSON Schemas live under `docs/modules/notify/resources/schemas/` and deterministic fixtures under `docs/modules/notify/resources/samples/`. + +| Artifact | Schema | Sample | +|----------|--------|--------| +| **Rule** (catalogued routing logic) | `docs/modules/notify/resources/schemas/notify-rule@1.json` | `docs/modules/notify/resources/samples/notify-rule@1.sample.json` | +| **Channel** (delivery endpoint definition) | `docs/modules/notify/resources/schemas/notify-channel@1.json` | `docs/modules/notify/resources/samples/notify-channel@1.sample.json` | +| **Template** (rendering payload) | `docs/modules/notify/resources/schemas/notify-template@1.json` | `docs/modules/notify/resources/samples/notify-template@1.sample.json` | +| **Event envelope** (Notify ingest surface) | `docs/modules/notify/resources/schemas/notify-event@1.json` | `docs/modules/notify/resources/samples/notify-event@1.sample.json` | + +### 6.1 Rule highlights (`notify-rule@1`) + +* Keys are lower‑cased camelCase. `schemaVersion` (`notify.rule@1`), `ruleId`, `tenantId`, `name`, `match`, `actions`, `createdAt`, and `updatedAt` are mandatory. +* `match.eventKinds`, `match.verdicts`, and other array selectors are pre‑sorted and case‑normalized (e.g. `scanner.report.ready`). +* `actions[].throttle` serialises as ISO 8601 duration (`PT5M`), mirroring worker backoff guardrails. +* `vex` gates let operators exclude accepted/not‑affected justifications; omit the block to inherit default behaviour. +* Use `StellaOps.Notify.Models.NotifySchemaMigration.UpgradeRule(JsonNode)` when deserialising legacy payloads that might lack `schemaVersion` or retain older revisions. +* Soft deletions persist `deletedAt` in Mongo (and disable the rule); repository queries automatically filter them. + +### 6.2 Channel highlights (`notify-channel@1`) + +* `schemaVersion` is pinned to `notify.channel@1` and must accompany persisted documents. +* `type` matches plug‑in identifiers (`slack`, `teams`, `email`, `webhook`, `custom`). +* `config.secretRef` stores an external secret handle (Authority, Vault, K8s). Notify never persists raw credentials. +* Optional `config.limits.timeout` uses ISO 8601 durations identical to rule throttles; concurrency/RPM defaults apply when absent. +* `StellaOps.Notify.Models.NotifySchemaMigration.UpgradeChannel(JsonNode)` backfills the schema version when older documents omit it. +* Channels share the same soft-delete marker (`deletedAt`) so operators can restore prior configuration without purging history. + +### 6.3 Event envelope (`notify-event@1`) + +* Aligns with the platform event contract—`eventId` UUID, RFC 3339 `ts`, tenant isolation enforced. +* Enumerated `kind` covers the initial Notify surface (`scanner.report.ready`, `scheduler.rescan.delta`, `zastava.admission`, etc.). +* `scope.labels`/`scope.attributes` and top-level `attributes` mirror the metadata dictionaries workers surface for templating and audits. +* Notify workers use the same migration helper to wrap event payloads before template rendering, so schema additions remain additive. + +### 6.4 Template highlights (`notify-template@1`) + +* Carries the presentation key (`channelType`, `key`, `locale`) and the raw template body; `schemaVersion` is fixed to `notify.template@1`. +* `renderMode` enumerates supported engines (`markdown`, `html`, `adaptiveCard`, `plainText`, `json`) aligning with `NotifyTemplateRenderMode`. +* `format` signals downstream connector expectations (`slack`, `teams`, `email`, `webhook`, `json`). +* Upgrade legacy definitions with `NotifySchemaMigration.UpgradeTemplate(JsonNode)` to auto-apply the new schema version and ordering. +* Templates also record soft deletes via `deletedAt`; UI/API skip them by default while retaining revision history. + +**Validation loop:** + +```bash +# Validate Notify schemas and samples (matches Docs CI) +for schema in docs/modules/notify/resources/schemas/*.json; do + npx ajv compile -c ajv-formats -s "$schema" +done + +for sample in docs/modules/notify/resources/samples/*.sample.json; do + schema="docs/modules/notify/resources/schemas/$(basename "${sample%.sample.json}").json" + npx ajv validate -c ajv-formats -s "$schema" -d "$sample" +done +``` + +Integration tests can embed the sample fixtures to guarantee deterministic serialisation from the `StellaOps.Notify.Models` DTOs introduced in Sprint 15. + +--- + +## 6 Validator Contracts + +* For SBOM wrapper – `ISbomValidator` (DLL plug‑in) must return *typed* error list. +* For YAML policies – JSON‑Schema at `/schemas/policy‑v1.json`. +* For Rego – OPA `opa eval --fail-defined` under the hood. +* For **Free‑tier quotas** – `IQuotaService` integration tests ensure `quota:` resets at UTC midnight and produces correct `Retry‑After` headers. + +--- + +## 7 Migration Notes + +1. **Add `format` column** to existing SBOM wrappers; default to `trivy-json-v2`. +2. **Populate `layers` & `partial`** via backfill script (ship with `stellopsctl migrate` wizard). +3. Policy YAML previously stored in Redis → copy to Mongo if persistence enabled. +4. Prepare `attestations` collection (empty) – safe to create in advance. + +--- + +## 8 Open Questions / Future Work + +* How to de‑duplicate *identical* Rego policies differing only in whitespace? +* Embed *GOST 34.11‑2018* digests when users enable Russian crypto suite? +* Should enterprise tiers share the same Redis quota keys or switch to JWT claim `tier != Free` bypass? +* Evaluate sliding‑window quota instead of strict daily reset. +* Consider rate‑limit for `/layers/missing` to avoid brute‑force enumeration. + +--- + +## 9 Change Log + +| Date | Note | +|------------|--------------------------------------------------------------------------------| +| 2025‑07‑14 | **Added:** `format`, `partial`, delta cache keys, YAML policy schema v1.0. | +| 2025‑07‑12 | **Initial public draft** – SBOM wrapper, Redis keyspace, audit collections. | + +--- diff --git a/docs/TASKS.md b/docs/TASKS.md index 80885c10..5d218b72 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -129,7 +129,8 @@ | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| | DOCS-LNM-22-007 | TODO | Docs Guild, Observability Guild | CONCELIER-LNM-21-005, EXCITITOR-LNM-21-005, DEVOPS-LNM-22-002 | Publish `/docs/observability/aggregation.md` with metrics/traces/logs/SLOs. | Observability doc merged; dashboards referenced; checklist appended. | -| DOCS-LNM-22-008 | TODO | Docs Guild, DevOps Guild | MERGE-LNM-21-001, CONCELIER-LNM-21-102 | Write `/docs/migration/no-merge.md` describing migration plan, backfill steps, rollback, feature flags. | Migration doc approved by stakeholders; checklist appended. | +| DOCS-LNM-22-008 | DONE (2025-11-03) | Docs Guild, DevOps Guild | MERGE-LNM-21-001, CONCELIER-LNM-21-102 | Write `/docs/migration/no-merge.md` describing migration plan, backfill steps, rollback, feature flags. | Migration doc approved by stakeholders; checklist appended. | +> 2025-11-03: Moved to DOING to align playbook structure, then published `docs/migration/no-merge.md` with rollout/backfill/rollback guidance plus readiness checklist; awaiting stakeholder approval note. ## Policy Engine + Editor v1 diff --git a/docs/airgap/airgap-mode.md b/docs/airgap/airgap-mode.md index 91a59f58..1b244319 100644 --- a/docs/airgap/airgap-mode.md +++ b/docs/airgap/airgap-mode.md @@ -6,7 +6,7 @@ Air-Gapped Mode is the supported operating profile for deployments with **zero external egress**. All inputs arrive via signed mirror bundles, and every surface (CLI, Console, APIs, schedulers, scanners) operates under sealed-network constraints while preserving Aggregation-Only Contract invariants. -- **Primary components:** Web Services API, Console, CLI, Orchestrator, Task Runner, Conseiller (Feedser), Excitator (VEXer), Policy Engine, Findings Ledger, Export Center, Authority & Tenancy, Notifications, Observability & Forensics. +- **Primary components:** Web Services API, Console, CLI, Orchestrator, Task Runner, Conselier (formerly Feedser), Excitor (formerly Vexer), Policy Engine, Findings Ledger, Export Center, Authority & Tenancy, Notifications, Observability & Forensics. - **Surfaces:** offline bootstrap, mirror ingestion, deterministic jobs, offline advisories/VEX/policy packs/notifications, evidence exports. - **Dependencies:** Export Center, Containerized Distribution, Authority-backed scopes & tenancy, Observability & Forensics, Policy Studio. @@ -15,7 +15,7 @@ Air-Gapped Mode is the supported operating profile for deployments with **zero e 1. **Zero egress:** all outbound network calls are disabled unless explicitly allowed. Any feature requiring online data must degrade gracefully with clear UX messaging. 2. **Deterministic inputs:** the platform accepts only signed Mirror Bundles (advisories, VEX, policy packs, vendor feeds, images, dashboards). Bundles carry provenance attestations and chain-of-custody manifests. 3. **Auditable exchange:** every import/export records provenance, signatures, and operator identity. Evidence bundles and reports remain verifiable offline. -4. **Aggregation-Only Contract compliance:** Conseiller and Excitator continue to aggregate without mutating source records, even when ingesting mirrored feeds. +4. **Aggregation-Only Contract compliance:** Conseiller and Excitor continue to aggregate without mutating source records, even when ingesting mirrored feeds. 5. **Operator ergonomics:** offline bootstrap, upgrade, and verification steps are reproducible and scripted. ## Lifecycle & modes @@ -44,7 +44,7 @@ Air-Gapped Mode is the supported operating profile for deployments with **zero e | --- | --- | | Export Center | Produce full/delta mirror bundles, signed manifests, provenance attestations. | | Authority & Tenancy | Provide offline scope enforcement, short-lived tokens, revocation via local CRLs. | -| Conseiller / Excitator | Ingest mirrored advisories/VEX, enforce AOC, versioned observations. | +| Conseiller / Excitor | Ingest mirrored advisories/VEX, enforce AOC, versioned observations. | | Policy Engine & Findings Ledger | Replay evaluations using offline feeds, emit explain traces, support sealed-mode hints. | | Notifications | Deliver locally via approved channels (email relay, webhook proxies) or queue for manual export. | | Observability | Collect metrics/logs/traces locally, generate forensic bundles for external analysis. | @@ -57,6 +57,8 @@ Air-Gapped Mode is the supported operating profile for deployments with **zero e - **Key rotation:** plan for offline key ceremonies; Export Center and Authority document rotation playbooks. - **Authority scopes:** enforce `airgap:status:read`, `airgap:import`, and `airgap:seal` via tenant-scoped roles; require operator reason/ticket metadata for sealing. - **Incident response:** maintain scripts for replaying imports, regenerating manifests, and exporting forensic data without egress. +- **EgressPolicy facade:** all services route outbound calls through `StellaOps.AirGap.Policy`. In sealed mode `EgressPolicy` enforces the `airgap.egressAllowlist`, auto-permits loopback targets, and raises `AIRGAP_EGRESS_BLOCKED` exceptions with remediation text (add host to allowlist or coordinate break-glass). Unsealed mode logs intents but does not block, giving operators a single toggle for rehearsals. Task Runner now feeds every `run.egress` declaration and runtime network hint into the shared policy during planning, preventing sealed-mode packs from executing unless destinations are declared and allow-listed. +- **Linting/CI:** enable the `StellaOps.AirGap.Policy.Analyzers` package in solution-level analyzers so CI fails on raw `HttpClient` usage. The analyzer emits `AIRGAP001` and the bundled code fix rewrites to `EgressHttpClientFactory.Create(...)`; treat analyzer warnings as errors in sealed-mode pipelines. ## Testing & verification diff --git a/docs/api/scanner/README.md b/docs/api/scanner/README.md new file mode 100644 index 00000000..827861cc --- /dev/null +++ b/docs/api/scanner/README.md @@ -0,0 +1,18 @@ +# Scanner API Docs — Windows/macOS Coverage (Draft) + +This directory collects interim artefacts tracking customer demand and roadmap readiness for extending Scanner coverage to Windows and macOS. + +## Files +- `windows-coverage.md` — narrative summary of customer signals and required artefacts. +- `windows-macos-summary.md` — dashboard-style snapshot (counts, cross-references) updated after each discovery cycle. + +## Related resources +- `../../modules/scanner/design/README.md` +- `../../benchmarks/scanner/windows-macos-demand.md` +- `../../benchmarks/scanner/windows-macos-interview-template.md` +- `../../modules/scanner/design/macos-analyzer.md` +- `../../modules/scanner/design/windows-analyzer.md` +- `../../modules/policy/windows-package-readiness.md` +- `../../modules/policy/secret-leak-detection-readiness.md` + +> Note: replace these working notes with formal API documentation once Windows/macOS analyzer endpoints are defined. diff --git a/docs/api/scanner/windows-coverage.md b/docs/api/scanner/windows-coverage.md new file mode 100644 index 00000000..08cfa21c --- /dev/null +++ b/docs/api/scanner/windows-coverage.md @@ -0,0 +1,50 @@ +# Scanner API — Windows/macOS Coverage Signals (Draft) + +> Audience: Solutions engineers, product managers, guild leads coordinating Windows/macOS roadmap +> Status: Informational; update as interviews conclude + +## Summary +| Region | Accounts referenced | Primary workloads | Demand strength (1-5) | Blocking? | Notes | +| --- | --- | --- | --- | --- | --- | +| North America | Northwind Health Services; FinSecure Corp | macOS CI runners; Windows Server 2019 workloads | 4-5 | macOS: evaluation; Windows: blocking | Demo 2025-11-10; Security decision due 2025-11-07. | +| EMEA | — | — | — | — | — | +| APAC | — | — | — | — | — | +| Gov / Regulated | — | — | — | — | — | + +### Key drivers +- Customers with regulated Windows Server/desktop estates lack deterministic SBOM coverage and provenance. +- macOS development shops (Mobile, Gaming) require entitlements/notarization evidence for compliance. +- Offline/air-gapped environments need signed rule bundles and feed mirrors for Windows/macOS ecosystems. + +### Competitive landscape +- Trivy/Grype/Snyk remain Linux-focused; Windows/macOS features are roadmap items or SaaS-only. +- Opportunity to differentiate via deterministic evidence, policy integration, and offline parity. + +### Design references +- `../../modules/scanner/design/macos-analyzer.md` +- `../../modules/scanner/design/windows-analyzer.md` + +### Action items +- Maintain region rows using interview summaries from `docs/benchmarks/scanner/windows-macos-demand.md` (last update 2025-11-03; capture via the interview template). +- Track readiness decisions by updating POLICY-READINESS-0001/0002 status and recording outcomes in the summary table. +- Align backlog references (`SCANNER-ENG-0020..0027`, `DOCS-SCANNER-BENCH-62-016`) with product prioritisation after each roadmap review. + +### Open blockers +- FinSecure PCI audit pending POLICY-READINESS-0002 decision (due 2025-11-07); unblock Windows analyzer spike scheduling. +- Northwind macOS readiness workshop scheduled 2025-11-10; capture masking/telemetry decisions for POLICY-READINESS-0001. + +## Interview log (selected) +| Date | Customer | Platform focus | Signal summary | Strength (1-5) | Follow-up | +| --- | --- | --- | --- | --- | --- | +| 2025-11-03 | Northwind Health Services | macOS | Needs notarization/entitlement visibility for CI runners | 4 | Demo 2025-11-10 with Product; feed findings into POLICY-READINESS-0001. | +| 2025-11-03 | FinSecure Corp | Windows | Requires MSI/WinSxS SBOM + signed driver attestations for PCI audit | 5 | Security guild to resolve Authenticode posture (POLICY-READINESS-0002) by 2025-11-07. | + +## Required artefacts +- Maintain interview notes using `docs/benchmarks/scanner/windows-macos-interview-template.md`. +- Update demand tracker tables in `docs/benchmarks/scanner/windows-macos-demand.md`. +- Sync backlog entries in `docs/modules/scanner/TASKS.md` and `docs/scanner/design/*.md`. + +## Next steps +1. Collect at least three qualified Windows and macOS requests; update summary table. +2. Present findings to Scanner Guild for prioritisation (target Sprint 133 design spike). +3. Coordinate policy readiness briefs (`docs/modules/policy/windows-package-readiness.md`) and design docs (`design/macos-analyzer.md`, `design/windows-analyzer.md`). diff --git a/docs/api/scanner/windows-macos-summary.md b/docs/api/scanner/windows-macos-summary.md new file mode 100644 index 00000000..57554b6c --- /dev/null +++ b/docs/api/scanner/windows-macos-summary.md @@ -0,0 +1,37 @@ +# Scanner API — Windows/macOS Coverage Dashboard (Draft) + +> Owners: Product Guild, Scanner Guild • Status: living document updated every sprint + +## At-a-glance metrics (Sprint 132 intake) +- macOS demand entries logged: 1 (Northwind Health Services, 2025-11-03) +- Windows demand entries logged: 1 (FinSecure Corp, 2025-11-03) +- Qualified customers awaiting roadmap response: 1 (FinSecure PCI blocker) +- Open policy readiness items: POLICY-READINESS-0001, POLICY-READINESS-0002 + +## Cross-reference +| Resource | Purpose | +| --- | --- | +| docs/benchmarks/scanner/windows-macos-demand.md | Signal log & next actions | +| docs/benchmarks/scanner/windows-macos-interview-template.md | Interview capture template | +| docs/benchmarks/scanner/deep-dives/macos.md | macOS implementation roadmap | +| docs/benchmarks/scanner/deep-dives/windows.md | Windows implementation roadmap | +| docs/modules/scanner/design/macos-analyzer.md | Detailed macOS design | +| docs/modules/scanner/design/windows-analyzer.md | Detailed Windows design | +| docs/modules/policy/windows-package-readiness.md | Policy readiness for Windows packages | +| docs/modules/policy/secret-leak-detection-readiness.md | Policy readiness for secrets | +| docs/modules/scanner/TASKS.md | Engineering backlog (SCANNER-ENG-0020..0027) | +| docs/modules/policy/TASKS.md | Policy readiness tasks | +| docs/api/scanner/windows-coverage.md | Narrative summary | + +## Maintenance cadence +- Update metrics and cross-links after each customer signal or roadmap checkpoint. +- Ensure DOCS-SCANNER-BENCH-62-002/016 status mirrors demand tracker progress. + +## Upcoming milestones +- 2025-11-07: POLICY-READINESS-0002 Authenticode/feed decision for FinSecure (unblocks Windows analyzer spike). +- 2025-11-10: POLICY-READINESS-0001 workshop during Northwind demo to finalise masking/telemetry posture. + +## Recent updates +- 2025-11-03: Logged Northwind Health Services (macOS) & FinSecure Corp (Windows); awaiting POLICY-READINESS-0001/0002 decisions before scheduling analyzer spikes. + +Last updated: 2025-11-03 (initial demand entries logged). diff --git a/docs/api/sdk-openapi-program.md b/docs/api/sdk-openapi-program.md index bf8e016b..ff86f17a 100644 --- a/docs/api/sdk-openapi-program.md +++ b/docs/api/sdk-openapi-program.md @@ -1,51 +1,51 @@ -# SDK & OpenAPI Program - -> Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. - -## Overview - -The SDK & OpenAPI program delivers canonical OpenAPI 3.1 contracts for every Stella Ops surface, plus officially supported SDKs (TypeScript/Node, Python, Go, Java, C#). It ensures backwards-compatible evolution, documentation, and offline availability. - -- **Primary components:** API Gateway, Web Services, Policy Engine, Conseiller, Excitator, Orchestrator, Findings Ledger, Export Center, Authority & Tenancy, Console, CLI. -- **Surfaces:** OpenAPI specs, language SDKs, developer portal, examples, mock server, conformance tests, changelog feeds, deprecation notices. -- **Dependencies:** Authority scopes/tenancy, CLI parity, Export Center, Notifications, Air-Gapped Mode, Observability. - -## Program pillars - -1. **Contract-first:** treat OpenAPI specs as the source of truth. CI validates schemas, compatibility, and documentation generation. -2. **SDK parity:** language SDKs cover the same surfaces with deterministic clients, pagination helpers, and typed models mirroring Aggregation-Only Contract semantics. -3. **Version discipline:** semantic versioning for specs and SDKs, release notes, deprecation windows, and automated change alerts via Notifications. -4. **Offline readiness:** specs and SDK bundles ship in Mirror Bundles for air-gapped environments; examples include smoke tests. -5. **Observability:** telemetry around SDK usage, spec download metrics, and error reporting funnels back into product decisions. - -## Deliverables - -| Workstream | Deliverable | -| --- | --- | -| Spec authoring | Unified OpenAPI 3.1 documents per service plus aggregate spec; lint rules; schema registries. | -| SDK generation | Language-specific clients with idiomatic ergonomics, retries, pagination, long-running operation helpers, unit + integration tests. | -| Dev portal | Consolidated documentation, guides, changelog, copy/paste examples, quickstart scripts. | -| Testing | Contract tests against staging, mock server for integration tests, compatibility verification per release. | -| Release ops | Automated CI pipelines, version bump workflows, release notes, deprecation policies. | - -## Guardrails - -- **Aggregation-Only Contract compliance:** SDKs expose raw advisory/VEX objects without hidden merges; all derived fields require explicit Policy Engine calls. -- **Security:** enforce scopes via SDK configuration; redact secrets; support DPoP/mTLS and offline token provisioning. -- **Compatibility:** maintain backwards-compatible paths for at least two minor releases; log warnings on deprecated endpoints. -- **Documentation:** publish examples for common workflows (scan, policy evaluate, export, attestation) with language parity. - -## Roadmap checkpoints - -1. Baseline OpenAPI specs extracted from gateway, validated, and published. -2. TypeScript/Node SDK as pilot, followed by Python and Go. -3. Developer portal launch with SDK docs, quickstarts, and mock server. -4. Offline kit integration (mirror bundles include specs + SDK tarballs). -5. Runtime alerting for breaking changes and dependency vulnerabilities. - -## References - -- API gateway integration: `docs/modules/platform/architecture-overview.md` -- Policy/Findings models: `docs/modules/policy/architecture.md`, `docs/modules/vuln-explorer/architecture.md` -- Export bundle distribution: `docs/modules/export-center/overview.md` -- Offline workflows: `docs/airgap/airgap-mode.md` +# SDK & OpenAPI Program + +> Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. + +## Overview + +The SDK & OpenAPI program delivers canonical OpenAPI 3.1 contracts for every Stella Ops surface, plus officially supported SDKs (TypeScript/Node, Python, Go, Java, C#). It ensures backwards-compatible evolution, documentation, and offline availability. + +- **Primary components:** API Gateway, Web Services, Policy Engine, Conseiller, Excitor, Orchestrator, Findings Ledger, Export Center, Authority & Tenancy, Console, CLI. +- **Surfaces:** OpenAPI specs, language SDKs, developer portal, examples, mock server, conformance tests, changelog feeds, deprecation notices. +- **Dependencies:** Authority scopes/tenancy, CLI parity, Export Center, Notifications, Air-Gapped Mode, Observability. + +## Program pillars + +1. **Contract-first:** treat OpenAPI specs as the source of truth. CI validates schemas, compatibility, and documentation generation. +2. **SDK parity:** language SDKs cover the same surfaces with deterministic clients, pagination helpers, and typed models mirroring Aggregation-Only Contract semantics. +3. **Version discipline:** semantic versioning for specs and SDKs, release notes, deprecation windows, and automated change alerts via Notifications. +4. **Offline readiness:** specs and SDK bundles ship in Mirror Bundles for air-gapped environments; examples include smoke tests. +5. **Observability:** telemetry around SDK usage, spec download metrics, and error reporting funnels back into product decisions. + +## Deliverables + +| Workstream | Deliverable | +| --- | --- | +| Spec authoring | Unified OpenAPI 3.1 documents per service plus aggregate spec; lint rules; schema registries. | +| SDK generation | Language-specific clients with idiomatic ergonomics, retries, pagination, long-running operation helpers, unit + integration tests. | +| Dev portal | Consolidated documentation, guides, changelog, copy/paste examples, quickstart scripts. | +| Testing | Contract tests against staging, mock server for integration tests, compatibility verification per release. | +| Release ops | Automated CI pipelines, version bump workflows, release notes, deprecation policies. | + +## Guardrails + +- **Aggregation-Only Contract compliance:** SDKs expose raw advisory/VEX objects without hidden merges; all derived fields require explicit Policy Engine calls. +- **Security:** enforce scopes via SDK configuration; redact secrets; support DPoP/mTLS and offline token provisioning. +- **Compatibility:** maintain backwards-compatible paths for at least two minor releases; log warnings on deprecated endpoints. +- **Documentation:** publish examples for common workflows (scan, policy evaluate, export, attestation) with language parity. + +## Roadmap checkpoints + +1. Baseline OpenAPI specs extracted from gateway, validated, and published. +2. TypeScript/Node SDK as pilot, followed by Python and Go. +3. Developer portal launch with SDK docs, quickstarts, and mock server. +4. Offline kit integration (mirror bundles include specs + SDK tarballs). +5. Runtime alerting for breaking changes and dependency vulnerabilities. + +## References + +- API gateway integration: `docs/modules/platform/architecture-overview.md` +- Policy/Findings models: `docs/modules/policy/architecture.md`, `docs/modules/vuln-explorer/architecture.md` +- Export bundle distribution: `docs/modules/export-center/overview.md` +- Offline workflows: `docs/airgap/airgap-mode.md` diff --git a/docs/benchmarks/scanner/README.md b/docs/benchmarks/scanner/README.md new file mode 100644 index 00000000..53e2fd08 --- /dev/null +++ b/docs/benchmarks/scanner/README.md @@ -0,0 +1,26 @@ +# Scanner Benchmarks & Coverage Dossiers + +This directory aggregates competitive analyses, deep dives, and demand signals that inform the Scanner roadmap. + +## Structure +- `../../scanner-feature-comparison-*.md` — high-level feature comparisons versus Trivy, Grype, Snyk. +- `deep-dives/` — ecosystem-specific implementation deep dives (languages, OS packages, secrets, macOS, Windows, etc.). +- `scanning-gaps-stella-misses-from-competitors.md` — gap analysis with scorecards, implementation plans, and backlog references. +- `windows-macos-demand.md` — demand tracker and next actions for Windows/macOS coverage. +- `windows-macos-interview-template.md` — structured questionnaire for customer discovery. + +## Quick links +- macOS: `deep-dives/macos.md`, `../../modules/scanner/design/macos-analyzer.md`. +- Windows: `deep-dives/windows.md`, `../../modules/scanner/design/windows-analyzer.md`. +- Policy readiness: `../../modules/policy/secret-leak-detection-readiness.md`, `../../modules/policy/windows-package-readiness.md`. +- API dashboards: `../../api/scanner/windows-coverage.md`, `../../api/scanner/windows-macos-summary.md`. + +## Maintenance tips +- When new interview signals are logged, update `windows-macos-demand.md`, the API dashboards, and cross-link relevant design docs. +- Ensure gap scorecards reflect the latest findings and backlog IDs. +- Keep feature comparison docs aligned with deep dive updates. + +## Backlog references +- Engineering: see `../../modules/scanner/TASKS.md` (SCANNER-ENG-0020..0027). +- Docs: `../../docs/TASKS.md` (DOCS-SCANNER-BENCH-62-016). +- Policy: `../../modules/policy/TASKS.md` (POLICY-READINESS-0001/0002). diff --git a/docs/benchmarks/scanner/deep-dives/macos.md b/docs/benchmarks/scanner/deep-dives/macos.md new file mode 100644 index 00000000..61ad29de --- /dev/null +++ b/docs/benchmarks/scanner/deep-dives/macos.md @@ -0,0 +1,48 @@ +# macOS Package Ecosystem — Coverage Deep Dive + +## Competitor snapshot +- **Trivy**: Official coverage tables list only Linux distributions (Alpine/Wolfi, Debian/Ubuntu, RHEL family, SUSE, Photon, Amazon, Bottlerocket). No analyzer exists for Homebrew, Mac App Store receipts, or `.app` bundles. +- **Grype**: Mirrors Syft catalogers that target Linux package managers; no matchers for Homebrew formulae, pkgutil receipts, or macOS frameworks. +- **Snyk CLI**: Container and open-source scans are routed through the SaaS backend; macOS host package analysis is not advertised or documented. + +## Proposed StellaOps roadmap +See also: `../../modules/scanner/design/macos-analyzer.md` for the in-depth design brief. +1. **Homebrew cellar collector** + - Parse `/usr/local/Cellar` and `/opt/homebrew/Cellar` manifest files plus taps metadata to enumerate installed formulae/casks. + - Normalize tap origin, version, revision, and bottle source; map to `pkg:brew/{tap}/{name}@{version}` PURLs. +2. **System receipt inventory** + - Use `pkgutil --pkgs --pkg-info-plist` style parsing against `/var/db/receipts/*.bom` to capture Apple installer packages and third-party pkg receipts. + - Record CFBundleIdentifier, install timestamps, and BOM file hashes to feed deterministic provenance. +3. **Application bundle inspection** + - Scan `/Applications`, `/System/Applications`, and `/Users/*/Applications` for `.app` bundles. + - Extract Info.plist data (bundle id/version), embedded frameworks, entitlements, and code signing certificates. +4. **Policy alignment** + - Emit capability hints (network, TCC categories) for Policy Engine gating. + - Provide allow lists for signed Apple system components versus third-party apps. +5. **Offline parity** + - Mirror Homebrew tap metadata snapshots and entitlements schema inside Offline Kit. + - Package notarization certificate bundles and CRL/OCSP cache guidance for air-gapped verification. + +## Detection technique comparison +| Technique | Artifacts | Merge strategy | Notes | +| --- | --- | --- | --- | +| Homebrew cellar parsing | Cellar manifests, `INSTALL_RECEIPT.json`, tap metadata | Produce per-formula component records keyed by tap + version; merge duplicates by canonical tap origin; attach bottle SHA256 for provenance. | Requires tap snapshot to ensure deterministic version resolution. | +| pkgutil receipt parsing | `/var/db/receipts/*.plist` + `.bom` files | Build component entries for Apple/third-party installer packages; merge with bundle evidence when matching CFBundleIdentifier. | Provides authoritative install-time metadata including install source. | +| `.app` bundle inspection | Info.plist, CodeResources, entitlements, embedded frameworks | Emit capability records (e.g., `tcc.camera`, `network.client`); merge with receipts when bundle id matches. | Must preserve signing chain and team identifier; store hash of CodeDirectory. | +| Launch daemon/service mapping | `/Library/LaunchDaemons`, `/Library/LaunchAgents`, `launchctl print` exports | (Planned) Attach runtime usage hints for Policy Engine and EntryTrace cross-module alignment. | Helps differentiate dormant apps from active services. | +| Competitor baseline | — | No competitor evidence to merge; Trivy/Grype/Snyk lack macOS analyzers. | Presents differentiation opportunity once demand justifies investment. | + +## Backlog / coordination +- Keep demand capture in `docs/benchmarks/scanner/windows-macos-demand.md` up to date; once thresholds are met, open: + - `SCANNER-ENG-00xx` (macOS cellar/receipt analyzer design spike). + - `DOCS-SCANNER-BENCH-62-002` (this doc’s parent task) — exit criteria: demand summary + design scope recommendation. + - Policy task to define macOS capability predicates (entitlements, notarization state). +- Align with Offline Kit guild on notarization/CRL packaging and tap mirroring strategy. + +## Open design questions +| Topic | Question | Owner | +| --- | --- | --- | +| Notarization verification | Should scanner verify Apple notarization tickets or delegate to Policy Engine? | Security Guild | +| Entitlement taxonomy | How granular should capability predicates be (per entitlement vs grouped categories)? | Policy Guild | +| User-space scope | Do we scan per-user Homebrew installs and sandboxed app containers? | Scanner Guild | +| Performance | Do we cache receipts/bundle metadata between scans given large `.app` trees? | Scanner + Surface Guilds | diff --git a/docs/benchmarks/scanner/deep-dives/matrix.md b/docs/benchmarks/scanner/deep-dives/matrix.md index b3de9976..461a98d3 100644 --- a/docs/benchmarks/scanner/deep-dives/matrix.md +++ b/docs/benchmarks/scanner/deep-dives/matrix.md @@ -13,7 +13,8 @@ | Rust | Binary heuristics | StellaOps | Fingerprint coverage incomplete for niche toolchains. | Unmatched binaries ignored; no fallback crates. | No fallback for binaries lacking Cargo metadata; depends on Syft crate data. | No Rust/Cargo support in CLI plugins. | Execute Rust fingerprint plan (`scanning-gaps-stella-misses-from-competitors.md`) and update analyzer backlog. | [rust.md](rust.md) | | OS packages | Linux distro coverage & provenance | Tie (StellaOps / Grype) | Requires RustFS/object store deployment for full replay; Windows packaging still out of scope. | No per-layer fragment storage; provenance limited; Windows support likewise minimal. | No per-layer provenance; shares Syft catalog and Anchore DB only. | Snyk Container scanning depends on SaaS API; no per-layer provenance. | Document RustFS dependency & offline alternatives in ops backlog; evaluate Windows pkg roadmap. | [os-packages.md](os-packages.md) | | OS packages | Linux flavor support (Alpine/Wolfi/Chainguard, Debian/Ubuntu, RHEL/Alma/Rocky, SUSE, Amazon/Bottlerocket) | Tie (Trivy / Snyk) | Windows/macOS package ecosystems still pending. | Coverage relies on package DB adapters; per-distro nuances (e.g., Chainguard signatures) not attested. | Supports major Linux feeds but no Windows/macOS package analyzers. | Supports documented distro list via Snyk Container but requires cloud connectivity. | Track demand for non-Linux package analyzers; document distro mapping in os-packages deep dive. | [os-packages.md](os-packages.md) | -| OS packages | Windows/macOS coverage | — | No Windows/macOS analyzer; backlog item for offline parity. | Coverage docs enumerate Linux distributions only; Windows/macOS packages unsupported. | Syft matchers focus on Linux ecosystems; Windows/macOS packages unsupported. | Coverage depends on Snyk’s SaaS service; no offline assurance for Windows/macOS packages. | Capture demand for Windows/macOS analyzers (see `docs/benchmarks/scanner/windows-macos-demand.md`) and scope feasibility. | [os-packages.md](os-packages.md) | +| macOS packages | Coverage & provenance | StellaOps (future) | macOS analyzer not implemented yet; design captured in deep dive. | No macOS analyzers (Linux only). | No macOS analyzers (Linux only). | No macOS host scanning; SaaS container focus only. | Advance demand capture (`windows-macos-demand.md`); open macOS design spike once threshold met. | [macos.md](macos.md) | +| Windows packages | Coverage & provenance | StellaOps (future) | Windows analyzer family not yet implemented; design brief in progress. | No Windows coverage beyond containers. | Syft/Grype lack Windows collectors; Anchore feeds Linux-only. | SaaS host scanning only; no offline or deterministic evidence. | Capture demand signals; scope Windows analyzer spike when thresholds met. | [windows.md](windows.md) | | Secrets | Handling posture | StellaOps | No leak scanning by design; Surface.Secrets manages retrieval/rotation with tenant scopes. | Leak detections lack governance hooks; operators must track rule updates. | No secret management abstraction; credentials configured manually. | Requires SaaS backend for secret scanning; no offline posture or secret storage guidance. | Document governance patterns for Surface.Secrets users and recommended companion tooling. | [secrets.md](secrets.md) | | Secrets | Detection technique | Trivy | No content scanning; relies on Surface.Secrets integrations. | Regex/entropy detectors with configurable allow/deny lists across files/bytecode. | No detector available; Syft/Grype skip leak scanning entirely. | Snyk Code/Snyk secrets require uploading code to SaaS; offline detection unavailable. | Execute secrets leak detection plan (`scanning-gaps-stella-misses-from-competitors.md`) and plan policy templates. | [secrets.md](secrets.md) | | EntryTrace | Runtime command resolution | StellaOps | Shell/language launcher coverage needs continuous tuning. | Not supported. | Not available. | Not available. | Maintain EntryTrace plan (`scanning-gaps-stella-misses-from-competitors.md`) and backlog cadence. | — | diff --git a/docs/benchmarks/scanner/deep-dives/os-packages.md b/docs/benchmarks/scanner/deep-dives/os-packages.md index 94f768bb..a4cae0ca 100644 --- a/docs/benchmarks/scanner/deep-dives/os-packages.md +++ b/docs/benchmarks/scanner/deep-dives/os-packages.md @@ -29,6 +29,14 @@ - **Metadata depth**: StellaOps records extensive vendor metadata and file evidence for replay; Trivy, Snyk, and Grype focus on match-relevant fields from their feeds. - **Provenance**: StellaOps’ outputs integrate directly with attestation/diff pipelines, while Trivy, Snyk, and Grype assume downstream tools consume package lists without replay requirements. +### Detection technique comparison +| Tool | Detection technique(s) | Merge / result handling | Notes | +| --- | --- | --- | --- | +| **StellaOps** | Deterministic DB parsing via `StellaOps.Scanner.Analyzers.OS.(Apk|Dpkg|Rpm)`; fragment mapping via `OsComponentMapper`; provenance persisted in Surface.FS. | Analyzer outputs become `LayerComponentFragment`s stored in `ScanAnalysisStore`; SBOM assembly composes inventory/usage views while preserving layer digests and metadata for diffs + attestations. | Supports offline replay, DSSE binding, and policy joins with Concelier advisories. | +| **Trivy** | Layer walkers + distro parsers in `pkg/fanal/analyzer/pkg/*`; optional manifest enrichment using vendor feeds. | Packages aggregated per artifact; provenance/layer context not persisted, leaving downstream tooling to interpret results. | Coverage matrix enumerated in `docs/docs/coverage/os/*.md`. | +| **Grype** | Syft catalogers + matchers in `grype/matcher/{apk,dpkg,rpm}` leveraging Anchore feeds. | Matchers run against Syft inventory and Anchore DB; no layer fragments retained. | Determinism depends on feed snapshots. | +| **Snyk** | Container scans upload image metadata to SaaS for analysis. | Results surfaced via SaaS dashboards/API; no local merge or provenance data. | Requires network connectivity; offline unsupported. | + ### References - [t1] `/tmp/trivy-src/docs/docs/coverage/os/index.md` - [s1] `/tmp/snyk-cli/README.md` diff --git a/docs/benchmarks/scanner/deep-dives/secrets.md b/docs/benchmarks/scanner/deep-dives/secrets.md index 1b1dd82e..c11bd5ec 100644 --- a/docs/benchmarks/scanner/deep-dives/secrets.md +++ b/docs/benchmarks/scanner/deep-dives/secrets.md @@ -1,6 +1,7 @@ # Secret Handling ## StellaOps approach +- Detailed Policy/Security briefing: `../../modules/policy/secret-leak-detection-readiness.md`. - Secrets treated as operational inputs delivered through Surface.Secrets (`src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets` and documented in `docs/modules/scanner/design/surface-secrets.md`). - Providers support Kubernetes Secrets, file-based bundles, and inline definitions; configuration resolved via Surface.Env with validation gates from Surface.Validation. - Secret placeholders (`secret://type/name`) are resolved before analyzers execute, with results wrapped in secure handles and rotation metadata checked at startup. @@ -26,6 +27,15 @@ - **Workflow**: StellaOps’ secret lifecycle is pre-scan configuration; Trivy and Snyk analyse content at scan time (Snyk requiring SaaS connectivity), and Grype requires external tooling for leak detection. - **Determinism**: StellaOps avoids non-deterministic leak scans; Trivy and Snyk’s detectors may evolve with rule updates; Grype remains deterministic by not attempting secret discovery. +### Detection technique comparison +| Tool | Detection technique(s) | Merge / result handling | Notes | +| --- | --- | --- | --- | +| **StellaOps (current)** | `Surface.Secrets` providers fetch credentials at runtime; no leak scanning today. | Secrets resolve to opaque handles stored in scan metadata; no SBOM entries emitted. | Deterministic and explainable; avoids exposing payloads. | +| **StellaOps (planned)** | `StellaOps.Scanner.Analyzers.Secrets` plug-in executing signed rule bundles. | Findings inserted into `ScanAnalysisStore` as `secret.leak` evidence; Policy Engine merges with component context and lattice scores. | Rules packaged offline; CLI/reporting masks payloads while surfacing rule IDs. | +| **Trivy** | Regex + entropy detectors under `pkg/fanal/secret` (configurable via `trivy-secret.yaml`). | Detectors aggregate per file; results exported alongside vulnerability findings without provenance binding. | Ships built-in rule sets; users can add allow/block lists. | +| **Snyk** | Snyk Code SaaS classifiers invoked by CLI plugin (`src/lib/plugins/sast`). | Source uploaded to SaaS; issues returned with severity + remediation; no offline merge with SBOM data. | Requires authenticated cloud access; rules evolve server-side. | +| **Grype** | None (focuses on vulnerability matching). | — | Operators must integrate separate tooling for leak detection. | + ### References - [s1] `/tmp/snyk-cli/src/lib/plugins/sast` - [s2] `/tmp/snyk-cli/README.md` diff --git a/docs/benchmarks/scanner/deep-dives/windows.md b/docs/benchmarks/scanner/deep-dives/windows.md new file mode 100644 index 00000000..5b73578d --- /dev/null +++ b/docs/benchmarks/scanner/deep-dives/windows.md @@ -0,0 +1,47 @@ +# Windows Package Ecosystem — Coverage Deep Dive + +## Competitor snapshot +- **Trivy**: No analyzers for Windows Installer (MSI), WinSxS manifests, or Chocolatey feeds. Official coverage tables enumerate Linux distributions only. +- **Grype**: Syft catalogers focus on Linux ecosystems; no Windows package collectors or matchers are available. +- **Snyk CLI**: Container/open-source scans rely on SaaS services; Windows host-level package scanning is not advertised. Snyk AppRisk references depend on SaaS integrations rather than offline collectors. + +## Proposed StellaOps roadmap +In-depth design detail lives in `../../modules/scanner/design/windows-analyzer.md`. +1. **MSI/WinSxS collector** + - Traverse `Windows/WinSxS/Manifests` and `Windows/Installer` to harvest component manifests, linking to installed products via the installer database. + - Parse MSI tables (Product, Component, File) to emit authoritative component records with product codes, upgrade codes, and install context. +2. **Chocolatey/NuGet-based packages** + - Inspect `ProgramData/Chocolatey/lib` and `ProgramData/Chocolatey/packages.config` for package metadata, including embedded nuspec files and checksums. + - Resolve package sources and cache feed metadata to support offline replay. +3. **Registry-backed inventory** + - Query `HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall` and equivalent Wow6432Node path to capture legacy installers (EXE/PDB) with version/signature hints. + - Record install source, publisher, and install location for cross-reference with filesystem collectors. +4. **Capability signals** + - Detect installed services (`System32/config/SYSTEM` registry hive), scheduled tasks, and driver packages to feed Policy Engine capability predicates. + - Capture Authenticode signatures and catalog (.cat) references to support trust enforcement. +5. **Offline parity** + - Package MSI schema definitions, Chocolatey feed snapshots, and Windows update catalog indexes into Offline Kit bundles. + - Provide hashing guidance for large binaries to avoid excessive Offline Kit growth (use external CAS references where possible). + +## Detection technique comparison +| Technique | Artifacts | Merge strategy | Notes | +| --- | --- | --- | --- | +| MSI database parsing | `Windows/Installer/*.msi` database tables (Product, Component, File) | Emit component records keyed by ProductCode/ComponentCode; merge with WinSxS manifests using file hashes. | Requires custom MSI reader (Jet/COM-free) compatible with offline bundles. | +| WinSxS manifest enumeration | `Windows/WinSxS/Manifests/*.manifest` | Map assemblies to catalog signatures and MSP patches; merge with MSI output for provenance. | Provides side-by-side assembly version info and language resources. | +| Chocolatey package inspection | `ProgramData/Chocolatey/lib/*/tools` & nuspec metadata | Produce package records with source feed URL, checksum, install scripts. | Feed metadata snapshot required to resolve dependencies offline. | +| Registry uninstall keys | `HKLM/HKCU ... /Uninstall/*` | Fill gaps for legacy installers (non-MSI); merge by install path and display name with file system evidence. | Use hive exports during scan to avoid registry API dependencies. | +| Service/driver mapping | `System32/config/SYSTEM` hive, `System32/DriverStore/FileRepository` | Generate capability overlays (services, drivers) for Policy Engine gating. | Helps differentiate drivers requiring elevated scrutiny (e.g., kernel-mode). | +| Competitor baseline | — | No competitor tool offers deterministic Windows package coverage with offline support. | Opportunity to differentiate once demand justifies scope. | + +## Backlog / coordination +- Follow demand capture in `docs/benchmarks/scanner/windows-macos-demand.md`. Once Windows signals meet thresholds, open engineering backlog (see proposed IDs below). +- Coordinate with Offline Kit guild on distributing MSI schema, Chocolatey feed snapshots, and driver catalog signatures. +- Engage Policy guild on required predicates (Authenticode trust, driver risk classes, service start modes). + +## Open design questions +| Topic | Question | Owner | +| --- | --- | --- | +| MSI parsing library | Build custom reader or embed open-source MSI parser? Must be AGPL-compatible and offline-ready. | Scanner Guild | +| Driver risk classification | Should Policy Engine treat kernel-mode drivers differently by default? | Policy Guild | +| Authenticodes & catalogs | Where do we verify signature/certificate revocation (scanner vs policy)? | Security Guild | +| Registry access | Will scanner access registry hives directly or require pre-extracted exports? | Scanner + Ops Guild | diff --git a/docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md b/docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md index 63d3ff99..652d2c37 100644 --- a/docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md +++ b/docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md @@ -232,12 +232,108 @@ - **Policy considerations**: Policies would need repository allowlists, signing requirements, and OS-specific mitigations; defer concrete templates until design spike completes. - **Next actions**: Execute tasks DOCS-SCANNER-BENCH-62-002/003/004/005/006 as demand signals accrue; only open engineering backlog after demand review approves scope expansion. +### Implementation details +- **StellaOps**: + - Linux analyzers live under `StellaOps.Scanner.Analyzers.OS.(Apk|Dpkg|Rpm)` and inherit from `OsPackageAnalyzerBase`, which normalises package metadata, file evidence, and vendor fields before persisting content-addressed fragments via Surface.FS. + - Analyzer results are converted to `LayerComponentFragment`s by `OsComponentMapper` and cached inside the worker `ScanAnalysisStore`; downstream SBOM assembly keeps layer digests and provenance so usage/inventory views can be replayed deterministically. + - Export Center binds OS fragments to DSSE attestations through Signer/Attestor so operators can prove provenance for every package list in offline bundles. +- **Trivy**: + - Package analyzers under `pkg/fanal/analyzer/pkg/(apk|dpkg|rpm)` walk layer file systems and emit `types.Package` results; the upstream coverage matrix in `docs/docs/coverage/os/*.md` drives which distro manifests and feeds ship with Trivy releases. + - Results are flattened per artifact, leaving layer attribution to Syft-style catalog walks; provenance and diffing are delegated to downstream tooling. +- **Grype**: + - Delegates cataloguing to Syft, then applies matchers in `grype/matcher/{apk,dpkg,rpm}` that resolve distro namespaces and fix statuses using Anchore feeds. + - Evidence is matched at scan time without long-lived fragments, so reproducibility depends on feed snapshots rather than packaged artifacts. +- **Snyk**: + - `snyk container test` uploads image metadata to Snyk SaaS; coverage is dictated by the hosted service and cannot run offline. + - No layer-aware evidence is returned; operators rely on SaaS reports for package presence and risk. + ### Detection techniques | Technique | Artifacts | Analyzer / Module | Merge strategy | |-----------|-----------|-------------------|----------------| -| Layer package DB parsing | apk, dpkg, rpm status/databases per layer | StellaOps.Scanner.Analyzers.OS.* with RustFS CAS | Produces per-layer fragments keyed by layer digest; composed into inventory/usage SBOM with provenance pointers. | -| Manifest + attestation binding | Distro manifest attestations, vendor signatures | Export Center + Signer/Attestor hand-off | Binds package fragments to DSSE attestations; policy consumes provenance metadata for trust weighting. | -| External SBOM import (interim) | Third-party SBOMs for Windows/macOS | Scanner SBOM import API (planned) | Imports produce declared-only entries flagged for policy review until native analyzers exist. | +| Layer package DB parsing | apk, dpkg, rpm status/databases per layer | `StellaOps.Scanner.Analyzers.OS.(Apk|Dpkg|Rpm)` + Surface.FS | Emits `OSPackageRecord` fragments tagged with layer digest; `OsComponentMapper` writes `LayerComponentFragment`s that SBOM assembly uses to build inventory/usage views with provenance. | +| Manifest + attestation binding | Distro manifest attestations, vendor signatures | Export Center + Signer/Attestor hand-off | Couples OS fragments with DSSE/Rekor proofs; Policy Engine verifies signatures before promotion gates. | +| Linux distro enrichment | Vendor advisory feeds, package-to-CVE mapping | Concelier + Policy Engine | Concelier ingests advisory metadata; Policy joins advisories with package fragments to produce lattice-scored findings. | +| External SBOM import (interim) | Third-party SBOMs for Windows/macOS | Scanner SBOM import API (planned) | Declared-only entries are merged with runtime evidence; policy downgrades unmatched declarations until native analyzers exist. | + +## macOS package coverage (Trivy, Grype, Snyk) +### Scorecard +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Customer demand | 2 | macOS coverage requests surface intermittently; demand capture ongoing via `windows-macos-demand.md`. | +| Competitive risk | 2 | Competitors lack macOS analyzers today, but roadmap announcements could erode differentiation quickly. | +| Engineering effort | 4 | Requires new collectors (Homebrew, pkgutil receipts, `.app` bundles) plus Offline Kit tap mirroring. | +| Policy/config impact | 4 | Policies must reason over entitlements, notarization, and signed bundle provenance. | +| Offline/air-gap impact | 4 | Tap metadata, notarization caches, and entitlement schemas must ship offline. | + +- **Competitor capability**: None of Trivy, Grype, or Snyk provide macOS host/package coverage; their documentation lists Linux-only ecosystems. +- **StellaOps gap**: No macOS analyzers exist; design work is captured in `docs/benchmarks/scanner/deep-dives/macos.md` but implementation/backlog stories are not yet opened. +- **Proposed plan**: + 1. Finalise demand assessment (DOCS-SCANNER-BENCH-62-002) using customer interviews, sales telemetry, and support tags. + 2. Scope macOS analyzer design spike covering Homebrew cellar parsing, pkgutil receipt ingestion, and `.app` bundle inspection; include entitlements/notarization strategy. + 3. Define Policy Engine predicates for entitlements, notarization status, bundle signing chain, and tenant allow lists. + 4. Coordinate with Offline Kit guild on mirroring tap metadata, notarization caches, and CRL/OCSP content for air-gapped operation. +- **Milestones**: Northwind Health Services demo (2025-11-10) doubles as POLICY-READINESS-0001 workshop to finalise masking/telemetry defaults before spike approval. +- **Policy considerations**: Need predicates for `macos.entitlement`, `macos.notarized`, `macos.bundle.teamId`, and severity rules for unsigned or unnotarized software; waivers should bind to bundle hash + signer. +- **Next actions**: Keep `windows-macos-demand.md` updated, prepare design brief once threshold met, and log engineering backlog items (SCANNER-ENG-00xx) for macOS collectors and policy integration. + +### Implementation details +- **Design references**: `docs/benchmarks/scanner/deep-dives/macos.md` captures proposed collectors (Homebrew, receipts, bundles) and open questions for Security/Policy guilds. +- **Collector outline**: + - Homebrew collector enumerates Cellar manifests under `/usr/local/Cellar` and `/opt/homebrew/Cellar`, mapping taps to PURLs and retaining bottle SHA256 for provenance. + - pkgutil collector parses `/var/db/receipts/*.plist` and `.bom` files to record installer package metadata with deterministic hashes. + - Bundle inspector walks `.app` directories, extracting Info.plist, entitlements, embedded frameworks, and code signing chains. +- **Merge strategy**: Collectors will emit component fragments keyed by bundle/tap identifiers; planned aggregator merges receipts and cellular data where identifiers match, tagging capabilities for Policy Engine. +- **Offline requirements**: Bundle signed rule packs (tap metadata snapshots, entitlements schema) with Offline Kit; ensure notarization cache instructions are documented. + +### Detection techniques +| Technique | Artifacts | Analyzer / Module | Merge strategy | +|-----------|-----------|-------------------|----------------| +| Homebrew cellar parsing (planned) | Cellar manifests, `INSTALL_RECEIPT.json`, tap metadata | Planned `StellaOps.Scanner.Analyzers.OS.Mac.Homebrew` | Emits component records keyed by tap + version; merges duplicates and attaches bottle hashes for provenance. | +| pkgutil receipt parsing (planned) | `/var/db/receipts/*.plist` and `.bom` | Planned `StellaOps.Scanner.Analyzers.OS.Mac.Receipts` | Records installer packages with bundle identifiers; merges with bundle evidence when identifiers align. | +| `.app` bundle inspection (planned) | Info.plist, CodeResources, entitlements, signing certs | Planned `StellaOps.Scanner.Analyzers.OS.Mac.Bundles` | Produces capability evidence (entitlements, hardened runtime) and links to receipts/Homebrew entries via bundle id. | +| Launch agent/daemon analysis (planned) | `/Library/Launch*` manifests, `launchctl` exports | Planned runtime usage mapper | Augments EntryTrace usage hints to distinguish active services. | +| Competitor baseline | — | Trivy/Grype/Snyk | No macOS host analyzers; coverage limited to Linux/container contexts. | + +## Windows package coverage (Trivy, Grype, Snyk) +### Scorecard +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Customer demand | 3 | Windows Server/container adopters continue to request evidence parity; demand captured via `windows-macos-demand.md`. | +| Competitive risk | 2 | Competitors lack Windows analyzers today but could narrow the gap with roadmap announcements. | +| Engineering effort | 5 | Requires MSI/WinSxS parsers, registry collectors, Chocolatey handling, and extensive Offline Kit work. | +| Policy/config impact | 4 | Policies must interpret Authenticode trust, driver/service posture, and legacy installer artefacts. | +| Offline/air-gap impact | 5 | Need to package MSI schemas, feed snapshots, and certificate bundles; storage overhead is significant. | + +- **Competitor capability**: Trivy, Grype, and Snyk do not ship Windows host analyzers; tooling focuses on Linux ecosystems and SaaS flows. +- **StellaOps gap**: No Windows analyzers exist yet; design outline under `docs/benchmarks/scanner/deep-dives/windows.md`. +- **Proposed plan**: + 1. Complete demand validation and prioritisation (DOCS-SCANNER-BENCH-62-002) alongside macOS signals. + 2. Execute engineering spike covering MSI/WinSxS parsing, Chocolatey inventory, and registry-based fallbacks. + 3. Define Policy Engine predicates for Authenticode, driver risk, service start mode, and Chocolatey provenance. + 4. Coordinate Offline Kit packaging strategy for MSI schemas, feed snapshots, and certificate revocation caches. +- **Policy considerations**: Introduce predicates such as `windows.package.signed(teamId?)`, `windows.driver.kernelMode`, `windows.service.startType`, and waivers tied to product code + signature thumbprint. +- **Next actions**: Maintain demand tracker, execute DOCS-SCANNER-BENCH-62-016, refine `docs/modules/scanner/design/windows-analyzer.md`, and open SCANNER-ENG-0024..0027 backlog tickets for collector and policy work once demand threshold is confirmed. +- **Milestones**: POLICY-READINESS-0002 Authenticode/feed decision due 2025-11-07 (FinSecure PCI blocker) gates Windows analyzer spikes. + +### Implementation details +- **Design references**: `docs/benchmarks/scanner/deep-dives/windows.md`, `docs/modules/scanner/design/windows-analyzer.md`, `docs/api/scanner/windows-coverage.md`. +- **Policy readiness**: see `docs/modules/policy/windows-package-readiness.md` for predicate requirements, waiver model, and offline guidance. +- **Collector outline**: + - MSI/WinSxS collector to parse installer databases/manifests and correlate via file hashes. + - Chocolatey collector to read nuspec metadata and install scripts, retaining feed provenance. + - Registry collector to harvest uninstall/service keys, linking to filesystem artefacts and signatures. +- **Merge strategy**: Planned `WindowsComponentMapper` merges MSI, WinSxS, Chocolatey, and registry evidence into unified fragments with provenance metadata; capability overlays capture services/drivers. +- **Offline requirements**: Bundle MSI schema definitions, Chocolatey feed snapshots, Windows Update catalog hashes, and certificate chains; document cache priming steps for air-gapped environments. + +### Detection techniques +| Technique | Artifacts | Analyzer / Module | Merge strategy | +|-----------|-----------|-------------------|----------------| +| MSI database parsing (planned) | `Windows/Installer/*.msi` Product/Component/File tables | Planned `StellaOps.Scanner.Analyzers.OS.Windows.Msi` | Emits component records keyed by product/component codes; merges with WinSxS manifests via file hashes. | +| WinSxS manifest parsing (planned) | `Windows/WinSxS/Manifests/*.manifest`, catalog files | Planned `StellaOps.Scanner.Analyzers.OS.Windows.WinSxS` | Maps assemblies to catalogs and MSP patches; merges with MSI output for provenance. | +| Chocolatey package parsing (planned) | `ProgramData/Chocolatey/lib/*`, nuspec metadata | Planned `StellaOps.Scanner.Analyzers.OS.Windows.Choco` | Records package evidence with feed provenance; merges with registry uninstall data. | +| Registry fallback (planned) | Exported uninstall/service hives | Planned `StellaOps.Scanner.Analyzers.OS.Windows.Registry` | Fills gaps for legacy installers and services; merges evidence by install path/signature. | +| Service/driver capability mapping (planned) | SYSTEM hive, DriverStore manifest | Planned capability overlay | Emits runtime capability records (drivers/services) for Policy Engine gating. | +| Competitor baseline | — | Trivy/Grype/Snyk | No Windows host analyzers; operators depend on external tooling. | ## Secrets leak detection (Trivy, Snyk) ### Scorecard @@ -258,14 +354,29 @@ 4. Offer CLI verb (`stella secrets scan`) and integration into existing scan workflows behind an opt-in flag. 5. Expose explain traces detailing rule IDs, masked snippets, and remediation guidance while upholding privacy constraints. - **Policy considerations**: Deliver policy templates for severity gating, rule packs per tenant, and privacy controls; lattice logic should discount low-confidence matches. -- **Next actions**: open analyzer/CLI backlog work, coordinate with Docs Guild on policy templates, and bundle signed rule packs for Offline Kit distribution. +- **Next actions**: Track execution via SCANNER-ENG-0007 (design/implementation) and DOCS-SCANNER-BENCH-62-007 (policy/docs); bundle signed rule packs for Offline Kit distribution once analyzer stories land. + +### Implementation details +- **StellaOps**: + - Operational secret retrieval flows through `Surface.Secrets` providers (Kubernetes, file bundle, inline) with validation policies enforced by `Surface.Validation`; handles remain opaque to analyzers. + - Planned `StellaOps.Scanner.Analyzers.Secrets` plug-in will execute deterministic rule bundles signed by the Export Center signing stack; findings land in `ScanAnalysisStore` alongside component fragments. + - Policy Engine will ingest `secret.leak` evidence with lattice hints (`confidence`, `rule.id`, `masking.applied`) so tenants can tune severities and approvals. +- **Trivy**: + - Secret detection is implemented in `pkg/fanal/secret` with detectors combining regex and entropy heuristics; rules merge into `Result` objects per file with severity weighting. + - Configuration is provided via `trivy-secret.yaml`, enabling per-rule enable/disable and allow lists. +- **Snyk**: + - CLI delegates to Snyk Code (`src/lib/plugins/sast`) which uploads source or image contents to SaaS for analysis; results stream back as issue JSON with remediation tips. + - Offline execution is unsupported; rule updates ship server-side. +- **Grype**: + - No leak detection analyzer; secrets are only used for registry authentication options. ### Detection techniques | Technique | Artifacts | Analyzer / Module | Merge strategy | |-----------|-----------|-------------------|----------------| -| Operational secret retrieval | secret:// references resolved via Surface.Secrets providers | Surface.Secrets, Surface.Validation | Injects secrets at runtime; no SBOM entry created; policy ensures provenance of retrieved credentials. | -| Deterministic leak detection (planned) | File content, archives, bytecode | StellaOps.Scanner.Analyzers.Secrets (planned) | Emits secret.leak evidence with masked snippets; Policy Engine merges with package evidence using VEX gating. | -| Competitor leak scanning | Regex/entropy rulesets (Trivy pkg/fanal/secret), Snyk Code SaaS service | Trivy secret analyzer, Snyk Code API | Findings remain separate from SBOM data; StellaOps will map to policy evidence types once analyzer ships. | +| Operational secret retrieval | `secret://` references resolved via `Surface.Secrets` providers | Surface.Secrets, Surface.Validation | Injects handles at runtime only; provenance recorded in scan metadata, nothing added to SBOM inventory. | +| Deterministic leak detection (planned) | File contents, archives, bytecode, container layers | `StellaOps.Scanner.Analyzers.Secrets` plug-in | Produces `secret.leak` records stored in `ScanAnalysisStore`; Policy Engine correlates with component metadata for context-aware enforcement. | +| Policy gating and reporting | Secret evidence + policy templates | Policy Engine, CLI/Export Center | Lattice scores combine confidence + severity; CLI/report output masks payloads while referencing rule IDs for explainability. | +| Competitor leak scanning | Regex/entropy rule sets, SaaS classifiers | Trivy `pkg/fanal/secret`; Snyk Code service | Trivy merges detectors per file; Snyk relies on SaaS analysis; neither binds results to SBOM evidence or deterministic attestations. | ## EntryTrace runtime command resolution (Trivy, Grype, Snyk) ### Scorecard @@ -314,12 +425,23 @@ - **Policy considerations**: Provide policy predicates for attestation presence, Rekor inclusion, and proof expiry to enforce promotion gates. - **Next actions**: Track via DOCS-SCANNER-BENCH-62-015 and SCANNER-ENG-0015 for playbook plus tooling updates. +### Implementation details +- **StellaOps**: + - `StellaOps.Signer` generates DSSE envelopes for SBOMs/reports using PoE-scoped keys and forwards them to `StellaOps.Attestor`, which handles Rekor v2 submissions with retries and proof caching. + - Export Center profiles bundle DSSE payloads, Rekor inclusion proofs, and any external attestations into offline-ready archives; CLI and Policy Engine verify proofs before release. + - Notify + Scanner.WebService emit attestation health telemetry so operators can quickly spot and remediate failed Rekor submissions. +- **Trivy / Grype / Snyk**: + - Trivy supports optional Cosign signing but leaves Rekor submission manual; proofs are not bundled with scanner outputs. + - Grype leans on Syft for SBOM export and does not sign outputs. + - Snyk Container/Snyk CLI rely on SaaS-managed signing and do not expose DSSE workflows or offline proof packaging. + ### Detection techniques | Technique | Artifacts | Analyzer / Module | Merge strategy | |-----------|-----------|-------------------|----------------| | SBOM emission | CycloneDX/SPDX payloads per scan | Scanner emit pipelines | Generates inventory/usage BOMs stored with CAS hashes for attestation. | | DSSE signing | DSSE bundles, signing keys | StellaOps.Signer + StellaOps.Attestor | Signs SBOM/report metadata, forwards to Rekor v2, records proof identifiers. | | Rekor proof packaging | Rekor inclusion proofs, bundle metadata | Export Center attestation packager | Bundles proofs into Offline Kit/export artifacts; Policy verifies before release. | +| Policy enforcement | Attestation & proof evidence | Policy Engine, Scheduler gates | Policies require successful DSSE/Rekor entries before promotion; Scheduler blocks exports lacking proofs. | | Competitor approach | CLI or SaaS-managed signing | Trivy Cosign integration, Snyk SaaS, Grype none | Operators must integrate manually; no default policy enforcement. | ## Ruby analyzer parity (Trivy, Grype, Snyk) diff --git a/docs/benchmarks/scanner/windows-macos-demand.md b/docs/benchmarks/scanner/windows-macos-demand.md index e6fdb243..2a9de867 100644 --- a/docs/benchmarks/scanner/windows-macos-demand.md +++ b/docs/benchmarks/scanner/windows-macos-demand.md @@ -10,8 +10,24 @@ 2. **Sales & SE feedback loop** – capture any RFP items referencing Windows/macOS scanning and log them in the Scanner guild tracker (SCANNER-ANALYZERS-OS-*). 3. **Support telemetry** – review ticket tags for “windows”, “macos”, “dotnet framework” to quantify inbound demand. 4. **Community landscape** – monitor Trivy/Grype/Snyk release notes for Windows/macOS announcements; update this note and the feature matrix when competitors change posture. +5. **Interview discipline** – use the structured questionnaire in `windows-macos-interview-template.md` to ensure consistent scoring and capture follow-up actions. + +## Signals log — macOS +| Date (YYYY-MM-DD) | Source / Account | Use case | Demand strength (1-5) | Notes / follow-up | +|-------------------|------------------|----------|------------------------|-------------------| +| 2025-11-03 | Northwind Health Services (NA) | macOS CI runners require notarization evidence for release sign-off | 4 | Demo deterministic bundle inspection w/ Product on 2025-11-10; capture entitlements policy requirements. | + +## Signals log — Windows +| Date (YYYY-MM-DD) | Source / Account | Use case | Demand strength (1-5) | Notes / follow-up | +|-------------------|------------------|----------|------------------------|-------------------| +| 2025-11-03 | FinSecure Corp (NA) | Windows Server 2019 images need MSI/WinSxS SBOM + signed driver attestations for PCI audit | 5 | Blocking go-live; Security guild to confirm Authenticode posture (POLICY-READINESS-0002) by 2025-11-07. | ## Next actions - Coordinate with Product Marketing to add Windows/macOS discovery prompts into upcoming customer advisory sessions (target: Sprint 132 intake). - Instrument the scanner roadmap intake form with explicit checkboxes for Windows/macOS package ecosystems. - If three or more qualified customers flag Windows/macOS coverage as a blocking requirement, open a design spike under the Scanner Analyzer Guild with scope/time estimates and Offline Kit considerations. +- Keep the macOS deep dive (`docs/benchmarks/scanner/deep-dives/macos.md`) in sync with demand findings so engineering can move from design sketch to formal backlog when thresholds are met. +- Update the Windows deep dive (`docs/benchmarks/scanner/deep-dives/windows.md`) and associated design briefs (`docs/modules/scanner/design/windows-analyzer.md`) as new signals arrive. +- Refresh API dashboards (`docs/api/scanner/windows-macos-summary.md`, `docs/api/scanner/windows-coverage.md`) after each update to keep Product and Field teams aligned. +- Drive POLICY-READINESS-0002 Authenticode/feed decision by 2025-11-07 (FinSecure PCI blocker); log outcome in dashboards and design briefs. +- Prepare POLICY-READINESS-0001 workshop aligned with Northwind demo on 2025-11-10, updating policy briefs with masking/telemetry decisions. diff --git a/docs/benchmarks/scanner/windows-macos-interview-template.md b/docs/benchmarks/scanner/windows-macos-interview-template.md new file mode 100644 index 00000000..aa65d119 --- /dev/null +++ b/docs/benchmarks/scanner/windows-macos-interview-template.md @@ -0,0 +1,50 @@ +# Windows / macOS Analyzer Demand — Interview Template + +Use this template during customer interviews, SE discovery calls, or product advisory meetings. Copy the table into your meeting notes and fill in the responses. Summaries should be pushed back into `windows-macos-demand.md`. + +## Interview metadata +| Field | Notes | +| --- | --- | +| Date | `YYYY-MM-DD` | +| Interviewer(s) | | +| Customer / Account | | +| Participant roles | (e.g., platform lead, security architect) | +| Workload context | (container images, VMs, desktop fleets, CI pipelines, etc.) | + +## Current state +1. **Operating systems in scope** + - Which Windows or macOS versions/images are mission critical? + - Container vs VM vs bare-metal distribution? +2. **Existing tooling** + - What scanners or inventory tools are used today (e.g., SCCM, Tanium, Trivy, Snyk, custom scripts)? + - Pain points / gaps they experience (offline support, provenance, coverage, explainability). +3. **Regulatory / compliance drivers** + - Any specific frameworks (PCI, FedRAMP, DISA, internal policies) mandating Windows/macOS SBOM or attestation? + +## Desired capabilities (score 1–5 per feature) +| Capability | Score | Notes | +| --- | --- | --- | +| MSI / WinSxS package inventory | | | +| Chocolatey / third-party feed tracking | | | +| macOS Homebrew / pkgutil receipts | | | +| `.app` bundle inspection (signing, entitlements) | | | +| Driver / service posture | | | +| Authenticode / notarization verification | | | +| Offline/air-gap parity | | | +| Policy integration (e.g., block unsigned driver) | | | + +## Operational requirements +- **Offline expectations**: Do they require artefact mirroring? Which feeds? +- **Performance**: Time budget for scans? Incremental vs full? +- **Evidence formats**: Preferred SBOM types, attestation needs, API endpoints. +- **Secrets / credentials**: Any constraints for registry/hive exports or feed mirrors? + +## Success metrics +- How will the customer judge success? (e.g., number of workloads covered, audit findings reduced, ability to prove provenance). +- Timeline expectations for pilot vs GA? + +## Follow-up actions +- What next steps were promised (POC, roadmap update, integration with other guilds)? +- Owners + due dates. + +> After the interview: convert highlights into a concise row in `windows-macos-demand.md` and, if needed, create Jira/backlog items for SCANNER-ENG-0020..0027 or DOCS-SCANNER-BENCH-62-016 with the captured context. diff --git a/docs/dev/30_VEXER_CONNECTOR_GUIDE.md b/docs/dev/30_EXCITOR_CONNECTOR_GUIDE.md similarity index 77% rename from docs/dev/30_VEXER_CONNECTOR_GUIDE.md rename to docs/dev/30_EXCITOR_CONNECTOR_GUIDE.md index 8c7d0890..f18dec16 100644 --- a/docs/dev/30_VEXER_CONNECTOR_GUIDE.md +++ b/docs/dev/30_EXCITOR_CONNECTOR_GUIDE.md @@ -1,220 +1,220 @@ -# Vexer Connector Packaging Guide - -> **Audience:** teams implementing new Vexer provider plug‑ins (CSAF feeds, -> OpenVEX attestations, etc.) -> **Prerequisites:** read `docs/modules/vexer/architecture.md` and the module -> `AGENTS.md` in `src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/`. - -The Vexer connector SDK gives you: - -- `VexConnectorBase` – deterministic logging, SHA‑256 helpers, time provider. -- `VexConnectorOptionsBinder` – strongly typed YAML/JSON configuration binding. -- `IVexConnectorOptionsValidator` – custom validation hooks (offline defaults, auth invariants). -- `VexConnectorDescriptor` & metadata helpers for consistent telemetry. - -This guide explains how to package a connector so the Vexer Worker/WebService -can load it via the plugin host. - ---- - -## 1. Project layout - -Start from the template under -`docs/dev/templates/vexer-connector/`. It contains: - -``` -Vexer.MyConnector/ -├── src/ -│ ├── Vexer.MyConnector.csproj -│ ├── MyConnectorOptions.cs -│ ├── MyConnector.cs -│ └── MyConnectorPlugin.cs -└── manifest/ - └── connector.manifest.yaml -``` - -Key points: - -- Target `net10.0`, enable `TreatWarningsAsErrors`, reference the - `StellaOps.Vexer.Connectors.Abstractions` project (or NuGet once published). -- Keep project ID prefix `StellaOps.Vexer.Connectors.` so the - plugin loader can discover it with the default search pattern. - -### 1.1 csproj snippet - -```xml - - - net10.0 - enable - enable - true - - - - - -``` - -Adjust the `ProjectReference` for your checkout (or switch to a NuGet package -once published). - ---- - -## 2. Implement the connector - -1. **Options model** – create an options POCO with data-annotation attributes. - Bind it via `VexConnectorOptionsBinder.Bind` in your connector - constructor or `ValidateAsync`. -2. **Validator** – implement `IVexConnectorOptionsValidator` to add - complex checks (e.g., ensure both `clientId` and `clientSecret` are present). -3. **Connector** – inherit from `VexConnectorBase`. Implement: - - `ValidateAsync` – run binder/validators, log configuration summary. - - `FetchAsync` – stream raw documents to `context.RawSink`. - - `NormalizeAsync` – convert raw documents into `VexClaimBatch` via - format-specific normalizers (`context.Normalizers`). -4. **Plugin adapter** – expose the connector via a plugin entry point so the - host can instantiate it. - -### 2.1 Options binding example - -```csharp -public sealed class MyConnectorOptions -{ - [Required] - [Url] - public string CatalogUri { get; set; } = default!; - - [Required] - public string ApiKey { get; set; } = default!; - - [Range(1, 64)] - public int MaxParallelRequests { get; set; } = 4; -} - -public sealed class MyConnectorOptionsValidator : IVexConnectorOptionsValidator -{ - public void Validate(VexConnectorDescriptor descriptor, MyConnectorOptions options, IList errors) - { - if (!options.CatalogUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - errors.Add("CatalogUri must use HTTPS."); - } - } -} -``` - -Bind inside the connector: - -```csharp -private readonly MyConnectorOptions _options; - -public MyConnector(VexConnectorDescriptor descriptor, ILogger logger, TimeProvider timeProvider) - : base(descriptor, logger, timeProvider) -{ - // `settings` comes from the orchestrator; validators registered via DI. - _options = VexConnectorOptionsBinder.Bind( - descriptor, - VexConnectorSettings.Empty, - validators: new[] { new MyConnectorOptionsValidator() }); -} -``` - -Replace `VexConnectorSettings.Empty` with the actual settings from context -inside `ValidateAsync`. - ---- - -## 3. Plugin adapter & manifest - -Create a simple plugin class that implements -`StellaOps.Plugin.IConnectorPlugin`. The Worker/WebService plugin host uses -this contract today. - -```csharp -public sealed class MyConnectorPlugin : IConnectorPlugin -{ - private static readonly VexConnectorDescriptor Descriptor = - new("vexer:my-provider", VexProviderKind.Vendor, "My Provider VEX"); - - public string Name => Descriptor.DisplayName; - - public bool IsAvailable(IServiceProvider services) => true; // inject feature flags if needed - - public IFeedConnector Create(IServiceProvider services) - { - var logger = services.GetRequiredService>(); - var timeProvider = services.GetRequiredService(); - return new MyConnector(Descriptor, logger, timeProvider); - } -} -``` - -> **Note:** the Vexer Worker currently instantiates connectors through the -> shared `IConnectorPlugin` contract. Once a dedicated Vexer plugin interface -> lands you simply swap the base interface; the descriptor/connector code -> remains unchanged. - -Provide a manifest describing the assembly for operational tooling: - -```yaml -# manifest/connector.manifest.yaml -id: vexer-my-provider -assembly: StellaOps.Vexer.Connectors.MyProvider.dll -entryPoint: StellaOps.Vexer.Connectors.MyProvider.MyConnectorPlugin -description: > - Official VEX feed for ExampleCorp products (CSAF JSON, daily updates). -tags: - - vexer - - csaf - - vendor -``` - -Store manifests under `/opt/stella/vexer/plugins//manifest/` in -production so the deployment tooling can inventory and verify plug‑ins. - ---- - -## 4. Packaging workflow - -1. `dotnet publish -c Release` → copy the published DLLs to - `/opt/stella/vexer/plugins//`. -2. Place `connector.manifest.yaml` next to the binaries. -3. Restart the Vexer Worker or WebService (hot reload not supported yet). -4. Verify logs: `VEX-ConnectorLoader` should list the connector descriptor. - -### 4.1 Offline kits - -- Add the connector folder (binaries + manifest) to the Offline Kit bundle. -- Include a `settings.sample.yaml` demonstrating offline-friendly defaults. -- Document any external dependencies (e.g., SHA mirrors) in the manifest `notes` - field. - ---- - -## 5. Testing checklist - -- Unit tests around options binding & validators. -- Integration tests (future `StellaOps.Vexer.Connectors.Abstractions.Tests`) - verifying deterministic logging scopes: - `logger.BeginScope` should produce `vex.connector.id`, `vex.connector.kind`, - and `vex.connector.operation`. -- Deterministic SHA tests: repeated `CreateRawDocument` calls with identical - content must return the same digest. - ---- - -## 6. Reference template - -See `docs/dev/templates/vexer-connector/` for the full quick‑start including: - -- Sample options class + validator. -- Connector implementation inheriting from `VexConnectorBase`. -- Plugin adapter + manifest. - -Copy the directory, rename namespaces/IDs, then iterate on provider-specific -logic. - ---- - -*Last updated: 2025-10-17* +# Excitor Connector Packaging Guide + +> **Audience:** teams implementing new Excitor provider plug‑ins (CSAF feeds, +> OpenVEX attestations, etc.) +> **Prerequisites:** read `docs/modules/excitor/architecture.md` and the module +> `AGENTS.md` in `src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/`. + +The Excitor connector SDK gives you: + +- `VexConnectorBase` – deterministic logging, SHA‑256 helpers, time provider. +- `VexConnectorOptionsBinder` – strongly typed YAML/JSON configuration binding. +- `IVexConnectorOptionsValidator` – custom validation hooks (offline defaults, auth invariants). +- `VexConnectorDescriptor` & metadata helpers for consistent telemetry. + +This guide explains how to package a connector so the Excitor Worker/WebService +can load it via the plugin host. + +--- + +## 1. Project layout + +Start from the template under +`docs/dev/templates/excitor-connector/`. It contains: + +``` +Excitor.MyConnector/ +├── src/ +│ ├── Excitor.MyConnector.csproj +│ ├── MyConnectorOptions.cs +│ ├── MyConnector.cs +│ └── MyConnectorPlugin.cs +└── manifest/ + └── connector.manifest.yaml +``` + +Key points: + +- Target `net10.0`, enable `TreatWarningsAsErrors`, reference the + `StellaOps.Excitor.Connectors.Abstractions` project (or NuGet once published). +- Keep project ID prefix `StellaOps.Excitor.Connectors.` so the + plugin loader can discover it with the default search pattern. + +### 1.1 csproj snippet + +```xml + + + net10.0 + enable + enable + true + + + + + +``` + +Adjust the `ProjectReference` for your checkout (or switch to a NuGet package +once published). + +--- + +## 2. Implement the connector + +1. **Options model** – create an options POCO with data-annotation attributes. + Bind it via `VexConnectorOptionsBinder.Bind` in your connector + constructor or `ValidateAsync`. +2. **Validator** – implement `IVexConnectorOptionsValidator` to add + complex checks (e.g., ensure both `clientId` and `clientSecret` are present). +3. **Connector** – inherit from `VexConnectorBase`. Implement: + - `ValidateAsync` – run binder/validators, log configuration summary. + - `FetchAsync` – stream raw documents to `context.RawSink`. + - `NormalizeAsync` – convert raw documents into `VexClaimBatch` via + format-specific normalizers (`context.Normalizers`). +4. **Plugin adapter** – expose the connector via a plugin entry point so the + host can instantiate it. + +### 2.1 Options binding example + +```csharp +public sealed class MyConnectorOptions +{ + [Required] + [Url] + public string CatalogUri { get; set; } = default!; + + [Required] + public string ApiKey { get; set; } = default!; + + [Range(1, 64)] + public int MaxParallelRequests { get; set; } = 4; +} + +public sealed class MyConnectorOptionsValidator : IVexConnectorOptionsValidator +{ + public void Validate(VexConnectorDescriptor descriptor, MyConnectorOptions options, IList errors) + { + if (!options.CatalogUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + errors.Add("CatalogUri must use HTTPS."); + } + } +} +``` + +Bind inside the connector: + +```csharp +private readonly MyConnectorOptions _options; + +public MyConnector(VexConnectorDescriptor descriptor, ILogger logger, TimeProvider timeProvider) + : base(descriptor, logger, timeProvider) +{ + // `settings` comes from the orchestrator; validators registered via DI. + _options = VexConnectorOptionsBinder.Bind( + descriptor, + VexConnectorSettings.Empty, + validators: new[] { new MyConnectorOptionsValidator() }); +} +``` + +Replace `VexConnectorSettings.Empty` with the actual settings from context +inside `ValidateAsync`. + +--- + +## 3. Plugin adapter & manifest + +Create a simple plugin class that implements +`StellaOps.Plugin.IConnectorPlugin`. The Worker/WebService plugin host uses +this contract today. + +```csharp +public sealed class MyConnectorPlugin : IConnectorPlugin +{ + private static readonly VexConnectorDescriptor Descriptor = + new("excitor:my-provider", VexProviderKind.Vendor, "My Provider VEX"); + + public string Name => Descriptor.DisplayName; + + public bool IsAvailable(IServiceProvider services) => true; // inject feature flags if needed + + public IFeedConnector Create(IServiceProvider services) + { + var logger = services.GetRequiredService>(); + var timeProvider = services.GetRequiredService(); + return new MyConnector(Descriptor, logger, timeProvider); + } +} +``` + +> **Note:** the Excitor Worker currently instantiates connectors through the +> shared `IConnectorPlugin` contract. Once a dedicated Excitor plugin interface +> lands you simply swap the base interface; the descriptor/connector code +> remains unchanged. + +Provide a manifest describing the assembly for operational tooling: + +```yaml +# manifest/connector.manifest.yaml +id: excitor-my-provider +assembly: StellaOps.Excitor.Connectors.MyProvider.dll +entryPoint: StellaOps.Excitor.Connectors.MyProvider.MyConnectorPlugin +description: > + Official VEX feed for ExampleCorp products (CSAF JSON, daily updates). +tags: + - excitor + - csaf + - vendor +``` + +Store manifests under `/opt/stella/excitor/plugins//manifest/` in +production so the deployment tooling can inventory and verify plug‑ins. + +--- + +## 4. Packaging workflow + +1. `dotnet publish -c Release` → copy the published DLLs to + `/opt/stella/excitor/plugins//`. +2. Place `connector.manifest.yaml` next to the binaries. +3. Restart the Excitor Worker or WebService (hot reload not supported yet). +4. Verify logs: `VEX-ConnectorLoader` should list the connector descriptor. + +### 4.1 Offline kits + +- Add the connector folder (binaries + manifest) to the Offline Kit bundle. +- Include a `settings.sample.yaml` demonstrating offline-friendly defaults. +- Document any external dependencies (e.g., SHA mirrors) in the manifest `notes` + field. + +--- + +## 5. Testing checklist + +- Unit tests around options binding & validators. +- Integration tests (future `StellaOps.Excitor.Connectors.Abstractions.Tests`) + verifying deterministic logging scopes: + `logger.BeginScope` should produce `vex.connector.id`, `vex.connector.kind`, + and `vex.connector.operation`. +- Deterministic SHA tests: repeated `CreateRawDocument` calls with identical + content must return the same digest. + +--- + +## 6. Reference template + +See `docs/dev/templates/excitor-connector/` for the full quick‑start including: + +- Sample options class + validator. +- Connector implementation inheriting from `VexConnectorBase`. +- Plugin adapter + manifest. + +Copy the directory, rename namespaces/IDs, then iterate on provider-specific +logic. + +--- + +*Last updated: 2025-10-17* diff --git a/docs/dev/templates/excitor-connector/manifest/connector.manifest.yaml b/docs/dev/templates/excitor-connector/manifest/connector.manifest.yaml new file mode 100644 index 00000000..3471094f --- /dev/null +++ b/docs/dev/templates/excitor-connector/manifest/connector.manifest.yaml @@ -0,0 +1,8 @@ +id: excitor-my-provider +assembly: StellaOps.Excitor.Connectors.MyProvider.dll +entryPoint: StellaOps.Excitor.Connectors.MyProvider.MyConnectorPlugin +description: | + Example connector template. Replace metadata before shipping. +tags: + - excitor + - template diff --git a/docs/dev/templates/vexer-connector/src/Vexer.MyConnector.csproj b/docs/dev/templates/excitor-connector/src/Excitor.MyConnector.csproj similarity index 72% rename from docs/dev/templates/vexer-connector/src/Vexer.MyConnector.csproj rename to docs/dev/templates/excitor-connector/src/Excitor.MyConnector.csproj index 3f18fde3..4ad4b7ee 100644 --- a/docs/dev/templates/vexer-connector/src/Vexer.MyConnector.csproj +++ b/docs/dev/templates/excitor-connector/src/Excitor.MyConnector.csproj @@ -7,6 +7,6 @@ - + diff --git a/docs/dev/templates/vexer-connector/src/MyConnector.cs b/docs/dev/templates/excitor-connector/src/MyConnector.cs similarity index 95% rename from docs/dev/templates/vexer-connector/src/MyConnector.cs rename to docs/dev/templates/excitor-connector/src/MyConnector.cs index d7a8eccd..932a2224 100644 --- a/docs/dev/templates/vexer-connector/src/MyConnector.cs +++ b/docs/dev/templates/excitor-connector/src/MyConnector.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -using StellaOps.Vexer.Connectors.Abstractions; -using StellaOps.Vexer.Core; +using StellaOps.Excitor.Connectors.Abstractions; +using StellaOps.Excitor.Core; -namespace StellaOps.Vexer.Connectors.MyProvider; +namespace StellaOps.Excitor.Connectors.MyProvider; public sealed class MyConnector : VexConnectorBase { diff --git a/docs/dev/templates/vexer-connector/src/MyConnectorOptions.cs b/docs/dev/templates/excitor-connector/src/MyConnectorOptions.cs similarity index 85% rename from docs/dev/templates/vexer-connector/src/MyConnectorOptions.cs rename to docs/dev/templates/excitor-connector/src/MyConnectorOptions.cs index c8cd857e..04d7756a 100644 --- a/docs/dev/templates/vexer-connector/src/MyConnectorOptions.cs +++ b/docs/dev/templates/excitor-connector/src/MyConnectorOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace StellaOps.Vexer.Connectors.MyProvider; +namespace StellaOps.Excitor.Connectors.MyProvider; public sealed class MyConnectorOptions { diff --git a/docs/dev/templates/vexer-connector/src/MyConnectorOptionsValidator.cs b/docs/dev/templates/excitor-connector/src/MyConnectorOptionsValidator.cs similarity index 81% rename from docs/dev/templates/vexer-connector/src/MyConnectorOptionsValidator.cs rename to docs/dev/templates/excitor-connector/src/MyConnectorOptionsValidator.cs index 04723e5b..8bdf151b 100644 --- a/docs/dev/templates/vexer-connector/src/MyConnectorOptionsValidator.cs +++ b/docs/dev/templates/excitor-connector/src/MyConnectorOptionsValidator.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using StellaOps.Vexer.Connectors.Abstractions; +using StellaOps.Excitor.Connectors.Abstractions; -namespace StellaOps.Vexer.Connectors.MyProvider; +namespace StellaOps.Excitor.Connectors.MyProvider; public sealed class MyConnectorOptionsValidator : IVexConnectorOptionsValidator { diff --git a/docs/dev/templates/vexer-connector/src/MyConnectorPlugin.cs b/docs/dev/templates/excitor-connector/src/MyConnectorPlugin.cs similarity index 83% rename from docs/dev/templates/vexer-connector/src/MyConnectorPlugin.cs rename to docs/dev/templates/excitor-connector/src/MyConnectorPlugin.cs index d12b573d..a50f56d8 100644 --- a/docs/dev/templates/vexer-connector/src/MyConnectorPlugin.cs +++ b/docs/dev/templates/excitor-connector/src/MyConnectorPlugin.cs @@ -1,15 +1,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Plugin; -using StellaOps.Vexer.Connectors.Abstractions; -using StellaOps.Vexer.Core; +using StellaOps.Excitor.Connectors.Abstractions; +using StellaOps.Excitor.Core; -namespace StellaOps.Vexer.Connectors.MyProvider; +namespace StellaOps.Excitor.Connectors.MyProvider; public sealed class MyConnectorPlugin : IConnectorPlugin { private static readonly VexConnectorDescriptor Descriptor = new( - id: "vexer:my-provider", + id: "excitor:my-provider", kind: VexProviderKind.Vendor, displayName: "My Provider VEX"); diff --git a/docs/dev/templates/vexer-connector/manifest/connector.manifest.yaml b/docs/dev/templates/vexer-connector/manifest/connector.manifest.yaml deleted file mode 100644 index 307b4bd5..00000000 --- a/docs/dev/templates/vexer-connector/manifest/connector.manifest.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: vexer-my-provider -assembly: StellaOps.Vexer.Connectors.MyProvider.dll -entryPoint: StellaOps.Vexer.Connectors.MyProvider.MyConnectorPlugin -description: | - Example connector template. Replace metadata before shipping. -tags: - - vexer - - template diff --git a/docs/implplan/SPRINTS.md b/docs/implplan/SPRINTS.md index 9688ef6c..6682615a 100644 --- a/docs/implplan/SPRINTS.md +++ b/docs/implplan/SPRINTS.md @@ -14,6 +14,13 @@ Follow the sprint files below in order. Update task status in both `SPRINTS` and - [Ops & Offline](./SPRINT_190_ops_offline.md) - [Documentation & Process](./SPRINT_200_documentation_process.md) +> 2025-11-03: AIRGAP-POL-57-002 confirmed DOING (AirGap Policy Guild, Task Runner Guild) – continuing Task Runner sealed-mode egress validation and test sweep. +> 2025-11-03: AIRGAP-POL-57-002 marked DONE (AirGap Policy Guild, Task Runner Guild) – worker now injects `IEgressPolicy`, filesystem dispatcher enforces sealed-mode egress, planner grants normalized, sealed-mode dispatcher test added; follow-up queued to lift remaining dispatchers/executors onto the shared policy before sealing the full worker loop. +> 2025-11-03: MERGE-LNM-21-001 moved to DOING (BE-Merge, Architecture Guild) – drafting `no-merge` migration playbook outline and capturing rollout/backfill checkpoints. +> 2025-11-03: MERGE-LNM-21-001 marked DONE – published `docs/migration/no-merge.md` with rollout, backfill, validation, and rollback guidance for the LNM cutover. +> 2025-11-03: MERGE-LNM-21-002 moved to DOING (BE-Merge) – auditing `AdvisoryMergeService` call sites to scope removal and analyzer enforcement. +> 2025-11-03: DOCS-LNM-22-008 moved to DOING (Docs Guild, DevOps Guild) – aligning migration playbook structure and readiness checklist. +> 2025-11-03: DOCS-LNM-22-008 marked DONE – `/docs/migration/no-merge.md` published for DevOps/Export Center planning with checklist for cutover readiness. > 2025-11-01: SCANNER-ANALYZERS-LANG-10-308R marked DONE (Language Analyzer Guild) – heuristics fixtures, benchmarks, and coverage comparison published. > 2025-11-01: SCANNER-ANALYZERS-LANG-10-309R marked DONE (Language Analyzer Guild) – Rust analyzer packaged with offline kit smoke tests and docs. > 2025-11-01: ENTRYTRACE-SURFACE-01 moved to DOING (EntryTrace Guild) – wiring Surface.Validation and Surface.FS reuse ahead of EntryTrace runs. diff --git a/docs/implplan/SPRINTS_PRIOR_20251021.md b/docs/implplan/SPRINTS_PRIOR_20251021.md index 33eb1cc5..8e53cd08 100644 --- a/docs/implplan/SPRINTS_PRIOR_20251021.md +++ b/docs/implplan/SPRINTS_PRIOR_20251021.md @@ -24,7 +24,7 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation | Sprint 8 | Plugin Infrastructure | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-20) | Authority Core, Plugin Platform Guild | AUTH-PLUGIN-COORD-08-002 | Coordinate scoped-service adoption for Authority plug-in registrars
Workshop notes and follow-up backlog captured 2025-10-20 in `docs/dev/authority-plugin-di-coordination.md`. | | Sprint 9 | Scanner Core Foundations | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-WEB-09-103 | Progress streaming (SSE/JSONL) with correlation IDs and ISO-8601 UTC timestamps, documented in API reference. | | Sprint 9 | Scanner Core Foundations | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-105 | Policy snapshot loader + schema + OpenAPI (YAML ignore rules, VEX include/exclude, vendor precedence). | -| Sprint 9 | Scanner Core Foundations | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-106 | `/reports` verdict assembly (Feedser+Vexer+Policy) + signed response envelope. | +| Sprint 9 | Scanner Core Foundations | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-106 | `/reports` verdict assembly (Conselier+Excitor+Policy) + signed response envelope. | | Sprint 9 | Scanner Core Foundations | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-107 | Expose score inputs, config version, and quiet provenance in `/reports` JSON and signed payload. | | Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-21) | DevOps Guild, Scanner WebService Guild | DEVOPS-SCANNER-09-204 | Surface `SCANNER__EVENTS__*` env config across Compose/Helm and document overrides. | | Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-21) | DevOps Guild, Notify Guild | DEVOPS-SCANNER-09-205 | Notify smoke job validates Redis stream + Notify deliveries after staging deploys. | diff --git a/docs/implplan/SPRINTS_PRIOR_20251025.md b/docs/implplan/SPRINTS_PRIOR_20251025.md index bdeac463..e9f68890 100644 --- a/docs/implplan/SPRINTS_PRIOR_20251025.md +++ b/docs/implplan/SPRINTS_PRIOR_20251025.md @@ -20,7 +20,7 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation | Sprint 12 | Runtime Guardrails | src/Zastava/StellaOps.Zastava.Webhook/TASKS.md | DONE (2025-10-24) | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-103 | Caching, fail-open/closed toggles, metrics/logging for admission decisions. | | Sprint 12 | Runtime Guardrails | src/Zastava/StellaOps.Zastava.Webhook/TASKS.md | DONE (2025-10-24) | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-104 | Wire `/admission` endpoint to runtime policy client and emit allow/deny envelopes. | | Sprint 12 | Runtime Guardrails | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | `/policy/runtime` endpoint joining SBOM baseline + policy verdict, returning admission guidance. | -| Sprint 12 | Runtime Guardrails | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-303 | Align `/policy/runtime` verdicts with canonical policy evaluation (Feedser/Vexer). | +| Sprint 12 | Runtime Guardrails | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-303 | Align `/policy/runtime` verdicts with canonical policy evaluation (Conselier/Excitor). | | Sprint 12 | Runtime Guardrails | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-304 | Integrate attestation verification into runtime policy metadata. | | Sprint 12 | Runtime Guardrails | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-305 | Deliver shared fixtures + e2e validation with Zastava/CLI teams. | | Sprint 13 | UX & CLI Experience | src/UI/StellaOps.UI/TASKS.md | DONE (2025-10-23) | UI Guild | UI-AUTH-13-001 | Integrate Authority OIDC + DPoP flows with session management. | diff --git a/docs/implplan/SPRINTS_PRIOR_20251027.md b/docs/implplan/SPRINTS_PRIOR_20251027.md index f12a30f2..267eb702 100644 --- a/docs/implplan/SPRINTS_PRIOR_20251027.md +++ b/docs/implplan/SPRINTS_PRIOR_20251027.md @@ -1,84 +1,84 @@ -This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not). - -| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description | -| --- | --- | --- | --- | --- | --- | --- | -| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-NUGET-13-002 | Ensure all solutions/projects prioritize `local-nuget` before public feeds and add restore-order validation. | -| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild, Platform Leads | DEVOPS-NUGET-13-003 | Upgrade `Microsoft.*` dependencies pinned to 8.* to their latest .NET 10 (or 9.x) releases and refresh guidance. | -| Sprint 14 | Release & Offline Ops | ops/deployment/TASKS.md | DONE (2025-10-26) | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. | -| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. | -| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild, Scanner Guild | DEVOPS-REL-14-004 | Extend release/offline smoke jobs to cover Python analyzer plug-ins (warm/cold, determinism, signing). | -| Sprint 14 | Release & Offline Ops | ops/licensing/TASKS.md | DONE (2025-10-26) | Licensing Guild | DEVOPS-LIC-14-004 | Registry token service tied to Authority, plan gating, revocation handling, monitoring. | -| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | DONE (2025-10-26) | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. | -| Sprint 15 | Benchmarks | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild, Notify Team | BENCH-NOTIFY-15-001 | Notify dispatch throughput bench with results CSV. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-19) | Scheduler Models Guild | SCHED-MODELS-16-101 | Define Scheduler DTOs & validation. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-19) | Scheduler Models Guild | SCHED-MODELS-16-102 | Publish schema docs/sample payloads. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Storage.Mongo/TASKS.md | DONE (2025-10-19) | Scheduler Storage Guild | SCHED-STORAGE-16-201 | Mongo schemas/indexes for Scheduler state. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Storage.Mongo/TASKS.md | DONE (2025-10-26) | Scheduler Storage Guild | SCHED-STORAGE-16-202 | Repositories with tenant scoping, TTL, causal consistency. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Storage.Mongo/TASKS.md | DONE (2025-10-26) | Scheduler Storage Guild | SCHED-STORAGE-16-203 | Audit/run stats materialization for UI. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.ImpactIndex/TASKS.md | DONE (2025-10-26) | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-302 | Query APIs for ResolveByPurls/ResolveByVulns/ResolveAll. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.ImpactIndex/TASKS.md | DONE (2025-10-26) | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-301 | Ingest BOM-Index into roaring bitmap store. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-16-102 | Schedules CRUD (cron validation, pause/resume, audit). | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-16-103 | Runs API (list/detail/cancel) + impact previews. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-WEB-16-104 | Feedser/Vexer webhook handlers with security enforcement. | -| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. | -| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. | -| Sprint 17 | Symbol Intelligence & Forensics | ops/offline-kit/TASKS.md | DONE (2025-10-26) | Offline Kit Guild, DevOps Guild | DEVOPS-OFFLINE-17-003 | Mirror release debug-store artefacts into Offline Kit packaging and document validation. | -| Sprint 17 | Symbol Intelligence & Forensics | src/Scanner/__Libraries/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-26) | Emit Guild | SCANNER-EMIT-17-701 | Record GNU build-id for ELF components and surface it in SBOM/diff outputs. | -| Sprint 18 | Launch Readiness | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-LAUNCH-18-001 | Production launch cutover rehearsal and runbook publication. | -| Sprint 18 | Launch Readiness | ops/offline-kit/TASKS.md | DONE (2025-10-26) | Offline Kit Guild, Scanner Guild | DEVOPS-OFFLINE-18-005 | Rebuild Offline Kit with Python analyzer artefacts and refreshed manifest/signature pair. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-AOC-19-001 | Publish aggregation-only contract reference documentation. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Architecture Guild | DOCS-AOC-19-002 | Update architecture overview with AOC boundary diagrams. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Policy Guild | DOCS-AOC-19-003 | Refresh policy engine doc with raw ingestion constraints. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, UI Guild | DOCS-AOC-19-004 | Document console AOC dashboard and drill-down flow. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, CLI Guild | DOCS-AOC-19-005 | Document CLI AOC commands and exit codes. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Observability Guild | DOCS-AOC-19-006 | Document new AOC metrics, traces, and logs. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Authority Core | DOCS-AOC-19-007 | Document new Authority scopes and tenancy enforcement. | -| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, DevOps Guild | DOCS-AOC-19-008 | Update deployment guide with validator enablement and verify user guidance. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce new ingestion/auth scopes across Authority. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-POLICY-20-001 | Publish `/docs/policy/overview.md` with compliance checklist. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-POLICY-20-002 | Document DSL grammar + examples in `/docs/policy/dsl.md`. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Authority Core | DOCS-POLICY-20-003 | Write `/docs/policy/lifecycle.md` covering workflow + roles. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Scheduler Guild | DOCS-POLICY-20-004 | Document policy run modes + cursors in `/docs/policy/runs.md`. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Platform Guild | DOCS-POLICY-20-005 | Produce `/docs/api/policy.md` with endpoint schemas + errors. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, CLI Guild | DOCS-POLICY-20-006 | Author `/docs/modules/cli/guides/policy.md` with commands, exit codes, JSON output. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, UI Guild | DOCS-POLICY-20-007 | Create `/docs/ui/policy-editor.md` covering editor, simulation, approvals. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Architecture Guild | DOCS-POLICY-20-008 | Publish `/docs/modules/policy/architecture.md` with sequence diagrams. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Observability Guild | DOCS-POLICY-20-009 | Document metrics/traces/logs in `/docs/observability/policy.md`. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Security Guild | DOCS-POLICY-20-010 | Publish `/docs/security/policy-governance.md` for scopes + approvals. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Policy Guild | DOCS-POLICY-20-011 | Add example policies under `/docs/examples/policies/` with commentary. | -| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Support Guild | DOCS-POLICY-20-012 | Draft `/docs/faq/policy-faq.md` covering conflicts, determinism, pitfalls. | -| Sprint 20 | Policy Engine v2 | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-POLICY-20-001 | Add DSL lint + compile checks to CI pipelines. | -| Sprint 20 | Policy Engine v2 | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild, QA Guild | DEVOPS-POLICY-20-003 | Add determinism CI job diffing repeated policy runs. | -| Sprint 20 | Policy Engine v2 | samples/TASKS.md | DONE (2025-10-26) | Samples Guild, Policy Guild | SAMPLES-POLICY-20-001 | Commit baseline/serverless/internal-only policy samples + fixtures. | -| Sprint 20 | Policy Engine v2 | samples/TASKS.md | DONE (2025-10-26) | Samples Guild, UI Guild | SAMPLES-POLICY-20-002 | Produce simulation diff fixtures for UI/CLI tests. | -| Sprint 20 | Policy Engine v2 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-POLICY-20-001 | Add new policy scopes (`policy:*`, `findings:read`, `effective:write`). | -| Sprint 20 | Policy Engine v2 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-POLICY-20-002 | Enforce Policy Engine service identity and scope checks at gateway. | -| Sprint 20 | Policy Engine v2 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Docs Guild | AUTH-POLICY-20-003 | Update Authority docs/config samples for policy scopes + workflows. | -| Sprint 20 | Policy Engine v2 | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild, Policy Guild | BENCH-POLICY-20-001 | Create policy evaluation benchmark suite + baseline metrics. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | DONE (2025-10-26) | Policy Guild, Platform Guild | POLICY-ENGINE-20-000 | Spin up new Policy Engine service host with DI bootstrap and Authority wiring. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | DONE (2025-10-26) | Policy Guild | POLICY-ENGINE-20-001 | Deliver `stella-dsl@1` parser + IR compiler with diagnostics and checksums. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-26) | Scheduler Models Guild | SCHED-MODELS-20-001 | Define policy run/diff DTOs + validation helpers. | -| Sprint 21 | Graph Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core Guild | AUTH-GRAPH-21-001 | Introduce graph scopes (`graph:*`) with configuration binding and defaults. | -| Sprint 21 | Graph Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core Guild | AUTH-GRAPH-21-002 | Enforce graph scopes/identities at gateway with tenant propagation. | -| Sprint 21 | Graph Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Docs Guild | AUTH-GRAPH-21-003 | Update security docs/config samples for graph access and least privilege. | -| Sprint 21 | Graph Explorer v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-26) | Scheduler Models Guild | SCHED-MODELS-21-001 | Define job DTOs for graph builds/overlay refresh (`GraphBuildJob`, `GraphOverlayJob`) with deterministic serialization and status enums; document in `src/Scheduler/__Libraries/StellaOps.Scheduler.Models/docs/SCHED-MODELS-21-001-GRAPH-JOBS.md`. | -| Sprint 21 | Graph Explorer v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-26) | Scheduler Models Guild | SCHED-MODELS-21-002 | Publish schema docs/sample payloads for graph job lifecycle. | -| Sprint 22 | Link-Not-Merge v1 | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild | BENCH-LNM-22-001 | Benchmark advisory observation ingest/correlation throughput. | -| Sprint 22 | Link-Not-Merge v1 | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild | BENCH-LNM-22-002 | Benchmark VEX ingest/correlation latency and event emission. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-001 | Publish `/docs/ui/console-overview.md` (IA, tenant model, filters, AOC alignment). | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-002 | Author `/docs/ui/navigation.md` with route map, filters, keyboard shortcuts, deep links. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-003 | Document `/docs/ui/sbom-explorer.md` covering catalog, graph, overlays, exports. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-004 | Produce `/docs/ui/advisories-and-vex.md` detailing aggregation-not-merge UX. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-005 | Write `/docs/ui/findings.md` with filters, explain, exports, CLI parity notes. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-006 | Publish `/docs/ui/policies.md` (editor, simulation, approvals, RBAC). | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-007 | Document `/docs/ui/runs.md` with SSE monitoring, diff, retries, evidence downloads. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-008 | Draft `/docs/ui/admin.md` covering tenants, roles, tokens, integrations, fresh-auth. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-27) | Docs Guild | DOCS-CONSOLE-23-009 | Publish `/docs/ui/downloads.md` aligning manifest with commands and offline flow. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-27) | Docs Guild, Deployment Guild, Console Guild | DOCS-CONSOLE-23-010 | Write `/docs/deploy/console.md` (Helm, ingress, TLS, env vars, health checks). | -| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-21-001 | Provide graph build/overlay job APIs; see `docs/SCHED-WEB-21-001-GRAPH-APIS.md`. | -| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-21-002 | Provide overlay lag metrics endpoint/webhook; see `docs/SCHED-WEB-21-001-GRAPH-APIS.md`. | -| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild, Authority Core Guild | SCHED-WEB-21-003 | Replace header auth with Authority scopes using `StellaOpsScopes`; dev fallback only when `Scheduler:Authority:Enabled=false`. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-OBS-50-001 | Deploy default OpenTelemetry collector manifests with secure OTLP pipeline. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-OBS-50-003 | Package telemetry stack configs for offline/air-gapped installs with signatures. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-WEB-16-101 | Minimal API host with Authority enforcement. | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | DONE (2025-10-27) | Scheduler Worker Guild | SCHED-WORKER-16-202 | ImpactIndex targeting and shard planning. | +This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not). + +| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description | +| --- | --- | --- | --- | --- | --- | --- | +| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-NUGET-13-002 | Ensure all solutions/projects prioritize `local-nuget` before public feeds and add restore-order validation. | +| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild, Platform Leads | DEVOPS-NUGET-13-003 | Upgrade `Microsoft.*` dependencies pinned to 8.* to their latest .NET 10 (or 9.x) releases and refresh guidance. | +| Sprint 14 | Release & Offline Ops | ops/deployment/TASKS.md | DONE (2025-10-26) | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. | +| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. | +| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild, Scanner Guild | DEVOPS-REL-14-004 | Extend release/offline smoke jobs to cover Python analyzer plug-ins (warm/cold, determinism, signing). | +| Sprint 14 | Release & Offline Ops | ops/licensing/TASKS.md | DONE (2025-10-26) | Licensing Guild | DEVOPS-LIC-14-004 | Registry token service tied to Authority, plan gating, revocation handling, monitoring. | +| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | DONE (2025-10-26) | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. | +| Sprint 15 | Benchmarks | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild, Notify Team | BENCH-NOTIFY-15-001 | Notify dispatch throughput bench with results CSV. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-19) | Scheduler Models Guild | SCHED-MODELS-16-101 | Define Scheduler DTOs & validation. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-19) | Scheduler Models Guild | SCHED-MODELS-16-102 | Publish schema docs/sample payloads. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Storage.Mongo/TASKS.md | DONE (2025-10-19) | Scheduler Storage Guild | SCHED-STORAGE-16-201 | Mongo schemas/indexes for Scheduler state. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Storage.Mongo/TASKS.md | DONE (2025-10-26) | Scheduler Storage Guild | SCHED-STORAGE-16-202 | Repositories with tenant scoping, TTL, causal consistency. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Storage.Mongo/TASKS.md | DONE (2025-10-26) | Scheduler Storage Guild | SCHED-STORAGE-16-203 | Audit/run stats materialization for UI. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.ImpactIndex/TASKS.md | DONE (2025-10-26) | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-302 | Query APIs for ResolveByPurls/ResolveByVulns/ResolveAll. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.ImpactIndex/TASKS.md | DONE (2025-10-26) | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-301 | Ingest BOM-Index into roaring bitmap store. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-16-102 | Schedules CRUD (cron validation, pause/resume, audit). | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-16-103 | Runs API (list/detail/cancel) + impact previews. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-WEB-16-104 | Conselier/Excitor webhook handlers with security enforcement. | +| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. | +| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. | +| Sprint 17 | Symbol Intelligence & Forensics | ops/offline-kit/TASKS.md | DONE (2025-10-26) | Offline Kit Guild, DevOps Guild | DEVOPS-OFFLINE-17-003 | Mirror release debug-store artefacts into Offline Kit packaging and document validation. | +| Sprint 17 | Symbol Intelligence & Forensics | src/Scanner/__Libraries/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-26) | Emit Guild | SCANNER-EMIT-17-701 | Record GNU build-id for ELF components and surface it in SBOM/diff outputs. | +| Sprint 18 | Launch Readiness | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-LAUNCH-18-001 | Production launch cutover rehearsal and runbook publication. | +| Sprint 18 | Launch Readiness | ops/offline-kit/TASKS.md | DONE (2025-10-26) | Offline Kit Guild, Scanner Guild | DEVOPS-OFFLINE-18-005 | Rebuild Offline Kit with Python analyzer artefacts and refreshed manifest/signature pair. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-AOC-19-001 | Publish aggregation-only contract reference documentation. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Architecture Guild | DOCS-AOC-19-002 | Update architecture overview with AOC boundary diagrams. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Policy Guild | DOCS-AOC-19-003 | Refresh policy engine doc with raw ingestion constraints. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, UI Guild | DOCS-AOC-19-004 | Document console AOC dashboard and drill-down flow. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, CLI Guild | DOCS-AOC-19-005 | Document CLI AOC commands and exit codes. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Observability Guild | DOCS-AOC-19-006 | Document new AOC metrics, traces, and logs. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Authority Core | DOCS-AOC-19-007 | Document new Authority scopes and tenancy enforcement. | +| Sprint 19 | Aggregation-Only Contract Enforcement | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, DevOps Guild | DOCS-AOC-19-008 | Update deployment guide with validator enablement and verify user guidance. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce new ingestion/auth scopes across Authority. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-POLICY-20-001 | Publish `/docs/policy/overview.md` with compliance checklist. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-POLICY-20-002 | Document DSL grammar + examples in `/docs/policy/dsl.md`. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Authority Core | DOCS-POLICY-20-003 | Write `/docs/policy/lifecycle.md` covering workflow + roles. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Scheduler Guild | DOCS-POLICY-20-004 | Document policy run modes + cursors in `/docs/policy/runs.md`. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Platform Guild | DOCS-POLICY-20-005 | Produce `/docs/api/policy.md` with endpoint schemas + errors. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, CLI Guild | DOCS-POLICY-20-006 | Author `/docs/modules/cli/guides/policy.md` with commands, exit codes, JSON output. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, UI Guild | DOCS-POLICY-20-007 | Create `/docs/ui/policy-editor.md` covering editor, simulation, approvals. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Architecture Guild | DOCS-POLICY-20-008 | Publish `/docs/modules/policy/architecture.md` with sequence diagrams. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Observability Guild | DOCS-POLICY-20-009 | Document metrics/traces/logs in `/docs/observability/policy.md`. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Security Guild | DOCS-POLICY-20-010 | Publish `/docs/security/policy-governance.md` for scopes + approvals. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Policy Guild | DOCS-POLICY-20-011 | Add example policies under `/docs/examples/policies/` with commentary. | +| Sprint 20 | Policy Engine v2 | docs/TASKS.md | DONE (2025-10-26) | Docs Guild, Support Guild | DOCS-POLICY-20-012 | Draft `/docs/faq/policy-faq.md` covering conflicts, determinism, pitfalls. | +| Sprint 20 | Policy Engine v2 | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-POLICY-20-001 | Add DSL lint + compile checks to CI pipelines. | +| Sprint 20 | Policy Engine v2 | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild, QA Guild | DEVOPS-POLICY-20-003 | Add determinism CI job diffing repeated policy runs. | +| Sprint 20 | Policy Engine v2 | samples/TASKS.md | DONE (2025-10-26) | Samples Guild, Policy Guild | SAMPLES-POLICY-20-001 | Commit baseline/serverless/internal-only policy samples + fixtures. | +| Sprint 20 | Policy Engine v2 | samples/TASKS.md | DONE (2025-10-26) | Samples Guild, UI Guild | SAMPLES-POLICY-20-002 | Produce simulation diff fixtures for UI/CLI tests. | +| Sprint 20 | Policy Engine v2 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-POLICY-20-001 | Add new policy scopes (`policy:*`, `findings:read`, `effective:write`). | +| Sprint 20 | Policy Engine v2 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-POLICY-20-002 | Enforce Policy Engine service identity and scope checks at gateway. | +| Sprint 20 | Policy Engine v2 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Docs Guild | AUTH-POLICY-20-003 | Update Authority docs/config samples for policy scopes + workflows. | +| Sprint 20 | Policy Engine v2 | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild, Policy Guild | BENCH-POLICY-20-001 | Create policy evaluation benchmark suite + baseline metrics. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | DONE (2025-10-26) | Policy Guild, Platform Guild | POLICY-ENGINE-20-000 | Spin up new Policy Engine service host with DI bootstrap and Authority wiring. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | DONE (2025-10-26) | Policy Guild | POLICY-ENGINE-20-001 | Deliver `stella-dsl@1` parser + IR compiler with diagnostics and checksums. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-26) | Scheduler Models Guild | SCHED-MODELS-20-001 | Define policy run/diff DTOs + validation helpers. | +| Sprint 21 | Graph Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core Guild | AUTH-GRAPH-21-001 | Introduce graph scopes (`graph:*`) with configuration binding and defaults. | +| Sprint 21 | Graph Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core Guild | AUTH-GRAPH-21-002 | Enforce graph scopes/identities at gateway with tenant propagation. | +| Sprint 21 | Graph Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-10-26) | Authority Core & Docs Guild | AUTH-GRAPH-21-003 | Update security docs/config samples for graph access and least privilege. | +| Sprint 21 | Graph Explorer v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-26) | Scheduler Models Guild | SCHED-MODELS-21-001 | Define job DTOs for graph builds/overlay refresh (`GraphBuildJob`, `GraphOverlayJob`) with deterministic serialization and status enums; document in `src/Scheduler/__Libraries/StellaOps.Scheduler.Models/docs/SCHED-MODELS-21-001-GRAPH-JOBS.md`. | +| Sprint 21 | Graph Explorer v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | DONE (2025-10-26) | Scheduler Models Guild | SCHED-MODELS-21-002 | Publish schema docs/sample payloads for graph job lifecycle. | +| Sprint 22 | Link-Not-Merge v1 | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild | BENCH-LNM-22-001 | Benchmark advisory observation ingest/correlation throughput. | +| Sprint 22 | Link-Not-Merge v1 | src/Bench/StellaOps.Bench/TASKS.md | DONE (2025-10-26) | Bench Guild | BENCH-LNM-22-002 | Benchmark VEX ingest/correlation latency and event emission. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-001 | Publish `/docs/ui/console-overview.md` (IA, tenant model, filters, AOC alignment). | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-002 | Author `/docs/ui/navigation.md` with route map, filters, keyboard shortcuts, deep links. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-003 | Document `/docs/ui/sbom-explorer.md` covering catalog, graph, overlays, exports. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-004 | Produce `/docs/ui/advisories-and-vex.md` detailing aggregation-not-merge UX. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-005 | Write `/docs/ui/findings.md` with filters, explain, exports, CLI parity notes. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-006 | Publish `/docs/ui/policies.md` (editor, simulation, approvals, RBAC). | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-007 | Document `/docs/ui/runs.md` with SSE monitoring, diff, retries, evidence downloads. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-26) | Docs Guild | DOCS-CONSOLE-23-008 | Draft `/docs/ui/admin.md` covering tenants, roles, tokens, integrations, fresh-auth. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-27) | Docs Guild | DOCS-CONSOLE-23-009 | Publish `/docs/ui/downloads.md` aligning manifest with commands and offline flow. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | DONE (2025-10-27) | Docs Guild, Deployment Guild, Console Guild | DOCS-CONSOLE-23-010 | Write `/docs/deploy/console.md` (Helm, ingress, TLS, env vars, health checks). | +| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-21-001 | Provide graph build/overlay job APIs; see `docs/SCHED-WEB-21-001-GRAPH-APIS.md`. | +| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-21-002 | Provide overlay lag metrics endpoint/webhook; see `docs/SCHED-WEB-21-001-GRAPH-APIS.md`. | +| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-26) | Scheduler WebService Guild, Authority Core Guild | SCHED-WEB-21-003 | Replace header auth with Authority scopes using `StellaOpsScopes`; dev fallback only when `Scheduler:Authority:Enabled=false`. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-OBS-50-001 | Deploy default OpenTelemetry collector manifests with secure OTLP pipeline. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | ops/devops/TASKS.md | DONE (2025-10-26) | DevOps Guild | DEVOPS-OBS-50-003 | Package telemetry stack configs for offline/air-gapped installs with signatures. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-WEB-16-101 | Minimal API host with Authority enforcement. | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | DONE (2025-10-27) | Scheduler Worker Guild | SCHED-WORKER-16-202 | ImpactIndex targeting and shard planning. | diff --git a/docs/implplan/SPRINTS_PRIOR_20251031.md b/docs/implplan/SPRINTS_PRIOR_20251031.md index 63673ae4..03084b88 100644 --- a/docs/implplan/SPRINTS_PRIOR_20251031.md +++ b/docs/implplan/SPRINTS_PRIOR_20251031.md @@ -1,1099 +1,1100 @@ -This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not). - -| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description | -| --- | --- | --- | --- | --- | --- | --- | -| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | DOING (2025-10-27) | Scheduler Worker Guild | SCHED-WORKER-16-201 | Planner loop (cron/event triggers, leases, fairness). | -| Sprint 17 | Symbol Intelligence & Forensics | ops/offline-kit/TASKS.md | BLOCKED (2025-10-26) | Offline Kit Guild, DevOps Guild | DEVOPS-OFFLINE-17-004 | Run mirror_debug_store.py once release artefacts exist and archive verification evidence with the Offline Kit. | -| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-REL-17-004 | Ensure release workflow publishes `out/release/debug` (build-id tree + manifest) and fails when symbols are missing. | -> DOCS-AOC-19-004: Architecture overview & policy-engine docs refreshed 2025-10-26 — reuse new AOC boundary diagram + metrics guidance. -> DOCS-AOC-19-005: Link to the new AOC reference and architecture overview; include exit code table sourced from those docs. -| Sprint 19 | Aggregation-Only Contract Enforcement | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild, Platform Guild | DEVOPS-AOC-19-001 | Integrate AOC analyzer/guard enforcement into CI pipelines. | -| Sprint 19 | Aggregation-Only Contract Enforcement | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-AOC-19-002 | Add CI stage running `stella aoc verify` against seeded snapshots. | -| Sprint 19 | Aggregation-Only Contract Enforcement | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild, QA Guild | DEVOPS-AOC-19-003 | Enforce guard coverage thresholds and export metrics to dashboards. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Cli/StellaOps.Cli/TASKS.md | DOING (2025-10-27) | DevEx/CLI Guild | CLI-AOC-19-001 | Implement `stella sources ingest --dry-run` command. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AOC-19-002 | Implement `stella aoc verify` command with exit codes. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Cli/StellaOps.Cli/TASKS.md | TODO | Docs/CLI Guild | CLI-AOC-19-003 | Update CLI reference and quickstart docs for new AOC commands. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-001 | Implement AOC repository guard rejecting forbidden fields. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-002 | Deliver deterministic linkset extraction for advisories. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-003 | Enforce idempotent append-only upsert with supersedes pointers. | +This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not). + +| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description | +| --- | --- | --- | --- | --- | --- | --- | +| Sprint 16 | Scheduler Intelligence | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | DOING (2025-10-27) | Scheduler Worker Guild | SCHED-WORKER-16-201 | Planner loop (cron/event triggers, leases, fairness). | +| Sprint 17 | Symbol Intelligence & Forensics | ops/offline-kit/TASKS.md | BLOCKED (2025-10-26) | Offline Kit Guild, DevOps Guild | DEVOPS-OFFLINE-17-004 | Run mirror_debug_store.py once release artefacts exist and archive verification evidence with the Offline Kit. | +| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-REL-17-004 | Ensure release workflow publishes `out/release/debug` (build-id tree + manifest) and fails when symbols are missing. | +> DOCS-AOC-19-004: Architecture overview & policy-engine docs refreshed 2025-10-26 — reuse new AOC boundary diagram + metrics guidance. +> DOCS-AOC-19-005: Link to the new AOC reference and architecture overview; include exit code table sourced from those docs. +| Sprint 19 | Aggregation-Only Contract Enforcement | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild, Platform Guild | DEVOPS-AOC-19-001 | Integrate AOC analyzer/guard enforcement into CI pipelines. | +| Sprint 19 | Aggregation-Only Contract Enforcement | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-AOC-19-002 | Add CI stage running `stella aoc verify` against seeded snapshots. | +| Sprint 19 | Aggregation-Only Contract Enforcement | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild, QA Guild | DEVOPS-AOC-19-003 | Enforce guard coverage thresholds and export metrics to dashboards. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Cli/StellaOps.Cli/TASKS.md | DOING (2025-10-27) | DevEx/CLI Guild | CLI-AOC-19-001 | Implement `stella sources ingest --dry-run` command. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AOC-19-002 | Implement `stella aoc verify` command with exit codes. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Cli/StellaOps.Cli/TASKS.md | TODO | Docs/CLI Guild | CLI-AOC-19-003 | Update CLI reference and quickstart docs for new AOC commands. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-001 | Implement AOC repository guard rejecting forbidden fields. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-002 | Deliver deterministic linkset extraction for advisories. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-003 | Enforce idempotent append-only upsert with supersedes pointers. | | Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | DOING (2025-10-28) | Concelier Core Guild | CONCELIER-CORE-AOC-19-004 | Remove ingestion normalization; defer derived logic to Policy Engine. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-013 | Extend smoke coverage to validate tenant-scoped Authority tokens and cross-tenant rejection. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-STORE-AOC-19-001 | Add Mongo schema validator for `advisory_raw`. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-STORE-AOC-19-002 | Create idempotency unique index backed by migration scripts. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-STORE-AOC-19-003 | Deliver append-only migration/backfill plan with supersedes chaining. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild, DevOps Guild | CONCELIER-STORE-AOC-19-004 | Document validator deployment steps for online/offline clusters. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-WEB-AOC-19-002 | Emit AOC observability metrics, traces, and structured logs. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | QA Guild | CONCELIER-WEB-AOC-19-003 | Add schema/guard unit tests covering AOC error codes. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-004 | Build integration suite validating deterministic ingest under load. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-001 | Introduce VEX repository guard enforcing AOC invariants. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-002 | Build deterministic VEX linkset extraction. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-003 | Enforce append-only idempotent VEX raw upserts. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-004 | Remove ingestion consensus logic; rely on Policy Engine. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-013 | Update smoke suites to enforce tenant-scoped Authority tokens and cross-tenant VEX rejection. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-STORE-AOC-19-001 | Add Mongo schema validator for `vex_raw`. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-STORE-AOC-19-002 | Create idempotency unique index for VEX raw documents. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-STORE-AOC-19-003 | Deliver append-only migration/backfill for VEX raw collections. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild, DevOps Guild | EXCITITOR-STORE-AOC-19-004 | Document validator deployment for Excititor clusters/offline kit. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-AOC-19-001 | Implement raw VEX ingestion and AOC verifier endpoints. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-WEB-AOC-19-002 | Emit AOC metrics/traces/logging for Excititor ingestion. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | QA Guild | EXCITITOR-WEB-AOC-19-003 | Add AOC guard test harness for VEX schemas. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-013 | Extend smoke coverage to validate tenant-scoped Authority tokens and cross-tenant rejection. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-STORE-AOC-19-001 | Add Mongo schema validator for `advisory_raw`. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-STORE-AOC-19-002 | Create idempotency unique index backed by migration scripts. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-STORE-AOC-19-003 | Deliver append-only migration/backfill plan with supersedes chaining. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild, DevOps Guild | CONCELIER-STORE-AOC-19-004 | Document validator deployment steps for online/offline clusters. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-WEB-AOC-19-002 | Emit AOC observability metrics, traces, and structured logs. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | QA Guild | CONCELIER-WEB-AOC-19-003 | Add schema/guard unit tests covering AOC error codes. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-004 | Build integration suite validating deterministic ingest under load. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-001 | Introduce VEX repository guard enforcing AOC invariants. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-002 | Build deterministic VEX linkset extraction. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-003 | Enforce append-only idempotent VEX raw upserts. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-004 | Remove ingestion consensus logic; rely on Policy Engine. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-CORE-AOC-19-013 | Update smoke suites to enforce tenant-scoped Authority tokens and cross-tenant VEX rejection. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-STORE-AOC-19-001 | Add Mongo schema validator for `vex_raw`. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-STORE-AOC-19-002 | Create idempotency unique index for VEX raw documents. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-STORE-AOC-19-003 | Deliver append-only migration/backfill for VEX raw collections. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild, DevOps Guild | EXCITITOR-STORE-AOC-19-004 | Document validator deployment for Excititor clusters/offline kit. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-AOC-19-001 | Implement raw VEX ingestion and AOC verifier endpoints. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-WEB-AOC-19-002 | Emit AOC metrics/traces/logging for Excititor ingestion. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | QA Guild | EXCITITOR-WEB-AOC-19-003 | Add AOC guard test harness for VEX schemas. | | Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild, QA Guild | EXCITITOR-WEB-AOC-19-004 | Validate large VEX ingest runs and CLI verification parity. | | Sprint 41 | Surface Sharing Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS/TASKS.md | TODO | Scanner Guild, Zastava Guild | SURFACE-FS-01 | Author Surface.FS cache specification and cross-module contract. | | Sprint 41 | Surface Sharing Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Surface.Env/TASKS.md | TODO | Scanner Guild, Ops Guild, Zastava Guild | SURFACE-ENV-01 | Draft Surface.Env variable matrix for Scanner/Zastava deployments. | | Sprint 41 | Surface Sharing Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/TASKS.md | TODO | Scanner Guild, Security Guild, Zastava Guild | SURFACE-SECRETS-01 | Define Surface.Secrets schema and rotation guidance. | | Sprint 41 | Surface Sharing Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Surface.Validation/TASKS.md | TODO | Scanner Guild, Security Guild | SURFACE-VAL-01 | Design validator framework for shared Surface checks and extensibility. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-WORKER-AOC-19-001 | Rewire worker to persist raw VEX docs with guard enforcement. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-WORKER-AOC-19-002 | Enforce signature/checksum verification prior to raw writes. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-AOC-19-001 | Add lint preventing ingestion modules from referencing Policy-only helpers. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild, Security Guild | POLICY-AOC-19-002 | Enforce Policy-only writes to `effective_finding_*` collections. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-AOC-19-003 | Update Policy readers to consume only raw document fields. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild, QA Guild | POLICY-AOC-19-004 | Add determinism tests for raw-driven policy recomputation. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AOC-19-001 | Add Sources dashboard tiles surfacing AOC status and violations. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AOC-19-002 | Build violation drill-down view for offending documents. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AOC-19-003 | Wire "Verify last 24h" action and CLI parity messaging. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Web/StellaOps.Web/TASKS.md | DOING (2025-10-26) | BE-Base Platform Guild | WEB-AOC-19-001 | Provide shared AOC forbidden key set and guard middleware. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AOC-19-002 | Ship provenance builder and signature helpers for ingestion services. | -| Sprint 19 | Aggregation-Only Contract Enforcement | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, QA Guild | WEB-AOC-19-003 | Author analyzer + shared test fixtures for guard compliance. | -| Sprint 20 | Policy Engine v2 | ops/devops/TASKS.md | BLOCKED (waiting on POLICY-ENGINE-20-006) | DevOps Guild | DEVOPS-POLICY-20-002 | Run `stella policy simulate` CI stage against golden SBOMs. | -| Sprint 20 | Policy Engine v2 | src/Bench/StellaOps.Bench/TASKS.md | BLOCKED (waiting on SCHED-WORKER-20-302) | Bench Guild, Scheduler Guild | BENCH-POLICY-20-002 | Add incremental run benchmark capturing delta SLA compliance. | -| Sprint 20 | Policy Engine v2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild, Docs Guild | CLI-POLICY-20-003 | Extend `stella findings` commands with policy filters and explain view. | -> 2025-10-27: Backend helpers drafted but command integration/tests pending; task reset to TODO awaiting follow-up. -| Sprint 20 | Policy Engine v2 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-POLICY-20-002 | Strengthen linkset builders with equivalence tables + range parsing. | -| Sprint 20 | Policy Engine v2 | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-POLICY-20-003 | Add advisory selection cursors + change-stream checkpoints for policy runs. | -| Sprint 20 | Policy Engine v2 | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-POLICY-20-001 | Provide advisory selection endpoints for policy engine (batch PURL/ID). | -| Sprint 20 | Policy Engine v2 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-POLICY-20-002 | Enhance VEX linkset scope + version resolution for policy accuracy. | -| Sprint 20 | Policy Engine v2 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-POLICY-20-003 | Introduce VEX selection cursors + change-stream checkpoints. | -| Sprint 20 | Policy Engine v2 | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-POLICY-20-001 | Ship VEX selection APIs aligned with policy join requirements. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | BLOCKED (2025-10-26) | Policy Guild | POLICY-ENGINE-20-002 | Implement deterministic rule evaluator with priority/first-match semantics. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Concelier Core, Excititor Core | POLICY-ENGINE-20-003 | Build SBOM↔advisory↔VEX linkset joiners with deterministic batching. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Storage Guild | POLICY-ENGINE-20-004 | Materialize effective findings with append-only history and tenant scoping. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Security Guild | POLICY-ENGINE-20-005 | Enforce determinism guard banning wall-clock, RNG, and network usage. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Scheduler Guild | POLICY-ENGINE-20-006 | Implement incremental orchestrator reacting to change streams. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Observability Guild | POLICY-ENGINE-20-007 | Emit policy metrics, traces, and sampled rule-hit logs. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, QA Guild | POLICY-ENGINE-20-008 | Add unit/property/golden/perf suites verifying determinism + SLA. | -| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Storage Guild | POLICY-ENGINE-20-009 | Define Mongo schemas/indexes + migrations for policies/runs/findings. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-20-002 | Update schema docs with policy run lifecycle samples. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-20-001 | Expose policy run scheduling APIs with scope enforcement. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-20-002 | Provide simulation trigger endpoint returning diff metadata. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-20-301 | Schedule policy runs via API with idempotent job tracking. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-20-302 | Implement delta targeting leveraging change streams + policy metadata. | -| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild, Observability Guild | SCHED-WORKER-20-303 | Expose policy scheduling metrics/logs with policy/run identifiers. | -| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-POLICY-20-001 | Ship Monaco-based policy editor with inline diagnostics + checklists. | -| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-POLICY-20-002 | Build simulation panel with deterministic diff rendering + virtualization. | -| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild, Product Ops | UI-POLICY-20-003 | Implement submit/review/approve workflow with RBAC + audit trail. | -| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild, Observability Guild | UI-POLICY-20-004 | Add run dashboards (heatmap/VEX wins/suppressions) with export. | -| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-20-001 | Implement Policy CRUD/compile/run/simulate/findings/explain endpoints. | -| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-20-002 | Add pagination, filters, deterministic ordering to policy listings. | -| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, QA Guild | WEB-POLICY-20-003 | Map engine errors to `ERR_POL_*` responses with contract tests. | -| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | Platform Reliability Guild | WEB-POLICY-20-004 | Introduce rate limits/quotas + metrics for simulation endpoints. | -| Sprint 21 | Graph Explorer v1 | src/Bench/StellaOps.Bench/TASKS.md | BLOCKED (2025-10-27) | Bench Guild, Graph Platform Guild | BENCH-GRAPH-21-001 | Graph viewport/path perf harness (50k/100k nodes) measuring Graph API/Indexer latency and cache hit rates. Executed within Sprint 28 Graph program. Upstream Graph API/indexer contracts (`GRAPH-API-28-003`, `GRAPH-INDEX-28-006`) still pending, so benchmarks cannot target stable endpoints yet. | -| Sprint 21 | Graph Explorer v1 | src/Bench/StellaOps.Bench/TASKS.md | BLOCKED (2025-10-27) | Bench Guild, UI Guild | BENCH-GRAPH-21-002 | Headless UI load benchmark for graph canvas interactions (Playwright) tracking render FPS budgets. Executed within Sprint 28 Graph program. Depends on BENCH-GRAPH-21-001 and UI Graph Explorer (`UI-GRAPH-24-001`), both pending. | -| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | BLOCKED (2025-10-27) | Concelier Core Guild | CONCELIER-GRAPH-21-001 | Enrich SBOM normalization with relationships, scopes, entrypoint annotations for Cartographer. Requires finalized schemas from `CONCELIER-POLICY-20-002` and Cartographer event contract (`CARTO-GRAPH-21-002`). | -| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | BLOCKED (2025-10-27) | Concelier Core & Scheduler Guilds | CONCELIER-GRAPH-21-002 | Publish SBOM change events with tenant metadata for graph builds. Awaiting projection schema from `CONCELIER-GRAPH-21-001` and Cartographer webhook expectations. | -| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | BLOCKED (2025-10-27) | Excititor Core Guild | EXCITITOR-GRAPH-21-001 | Deliver batched VEX/advisory fetch helpers for inspector linkouts. Waiting on linkset enrichment (`EXCITITOR-POLICY-20-002`) and Cartographer inspector contract (`CARTO-GRAPH-21-005`). | -| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | BLOCKED (2025-10-27) | Excititor Core Guild | EXCITITOR-GRAPH-21-002 | Enrich overlay metadata with VEX justification summaries for graph overlays. Depends on `EXCITITOR-GRAPH-21-001` and Policy overlay schema (`POLICY-ENGINE-30-001`). | -| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | BLOCKED (2025-10-27) | Excititor Storage Guild | EXCITITOR-GRAPH-21-005 | Create indexes/materialized views for VEX lookups by PURL/policy. Awaiting access pattern specs from `EXCITITOR-GRAPH-21-001`. | -| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service Guild | SBOM-SERVICE-21-001 | Expose normalized SBOM projection API with relationships, scopes, entrypoints. Waiting on Concelier projection schema (`CONCELIER-GRAPH-21-001`). | -| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service & Scheduler Guilds | SBOM-SERVICE-21-002 | Emit SBOM version change events for Cartographer build queue. Depends on SBOM projection API (`SBOM-SERVICE-21-001`) and Scheduler contracts. | -| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service Guild | SBOM-SERVICE-21-003 | Provide entrypoint management API with tenant overrides. Blocked by SBOM projection API contract. | -| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service & Observability Guilds | SBOM-SERVICE-21-004 | Add metrics/traces/logs for SBOM projections. Requires projection pipeline from `SBOM-SERVICE-21-001`. | -| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base Platform Guild | WEB-GRAPH-21-001 | Add gateway routes for graph APIs with scope enforcement and streaming. Upstream Graph API (`GRAPH-API-28-003`) and Authority scope work (`AUTH-VULN-24-001`) pending. | -| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base Platform Guild | WEB-GRAPH-21-002 | Implement bbox/zoom/path validation and pagination for graph endpoints. Depends on core proxy routes. | -| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base Platform & QA Guilds | WEB-GRAPH-21-003 | Map graph errors to `ERR_Graph_*` and support export streaming. Requires `WEB-GRAPH-21-001`. | -| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base & Policy Guilds | WEB-GRAPH-21-004 | Wire Policy Engine simulation overlays into graph responses. Waiting on Graph routes and Policy overlay schema (`POLICY-ENGINE-30-002`). | -| Sprint 22 | Link-Not-Merge v1 | docs/TASKS.md | BLOCKED (2025-10-27) | Docs Guild | DOCS-LNM-22-001 | Publish advisories aggregation doc with observation/linkset philosophy. | -> Blocked by `CONCELIER-LNM-21-001..003`; draft doc exists but final alignment waits for schema/API delivery. -| Sprint 22 | Link-Not-Merge v1 | docs/TASKS.md | BLOCKED (2025-10-27) | Docs Guild | DOCS-LNM-22-002 | Publish VEX aggregation doc describing observation/linkset flow. | -> Blocked by `EXCITITOR-LNM-21-001..003`; draft doc staged pending observation/linkset implementation. -| Sprint 22 | Link-Not-Merge v1 | docs/TASKS.md | BLOCKED (2025-10-27) | Docs Guild | DOCS-LNM-22-005 | Document UI evidence panel with conflict badges/AOC drill-down. | -> Blocked by `UI-LNM-22-001..003`; need shipping UI to capture screenshots and finalize guidance. -| Sprint 22 | Link-Not-Merge v1 | ops/devops/TASKS.md | BLOCKED (2025-10-27) | DevOps Guild | DEVOPS-LNM-22-001 | Execute advisory observation/linkset migration/backfill and automation. | -| Sprint 22 | Link-Not-Merge v1 | ops/devops/TASKS.md | BLOCKED (2025-10-27) | DevOps Guild | DEVOPS-LNM-22-002 | Run VEX observation/linkset migration/backfill with monitoring/runbook. | -| Sprint 22 | Link-Not-Merge v1 | samples/TASKS.md | BLOCKED (2025-10-27) | Samples Guild | SAMPLES-LNM-22-001 | Add advisory observation/linkset fixtures with conflicts. | -| Sprint 22 | Link-Not-Merge v1 | samples/TASKS.md | BLOCKED (2025-10-27) | Samples Guild | SAMPLES-LNM-22-002 | Add VEX observation/linkset fixtures with status disagreements. | -| Sprint 22 | Link-Not-Merge v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-AOC-22-001 | Roll out new advisory/vex ingest/read scopes. | -| Sprint 22 | Link-Not-Merge v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-LNM-22-001 | Implement advisory observation/linkset CLI commands with JSON/OSV export. | -| Sprint 22 | Link-Not-Merge v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-LNM-22-002 | Implement VEX observation/linkset CLI commands. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-LNM-21-001 | Define immutable advisory observation schema with AOC metadata. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild, Data Science Guild | CONCELIER-LNM-21-002 | Implement advisory linkset builder with correlation signals/conflicts. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md | TODO | BE-Merge | MERGE-LNM-21-002 | Deprecate merge service and enforce observation-only pipeline. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-LNM-21-101 | Provision observations/linksets collections and indexes. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage & DevOps Guilds | CONCELIER-LNM-21-102 | Backfill legacy merged advisories into observations/linksets with rollback tooling. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-LNM-21-201 | Ship advisory observation read APIs with pagination/RBAC. | -| Sprint 22 | Link-Not-Merge v1 | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-LNM-21-202 | Implement advisory linkset read/export/evidence endpoints mapped to `ERR_AGG_*`. | -| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-LNM-21-001 | Define immutable VEX observation model. | -| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-LNM-21-002 | Build VEX linkset correlator with confidence/conflict recording. | -| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-LNM-21-101 | Provision VEX observation/linkset collections and indexes. | -| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage & DevOps Guilds | EXCITITOR-LNM-21-102 | Backfill legacy VEX data into observations/linksets with rollback scripts. | -| Sprint 22 | Link-Not-Merge v1 | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-201 | Expose VEX observation APIs with filters/pagination and RBAC. | -| Sprint 22 | Link-Not-Merge v1 | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-202 | Implement VEX linkset endpoints + exports with evidence payloads. | -| Sprint 22 | Link-Not-Merge v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-40-001 | Update severity selection to handle multiple source severities per linkset. | -| Sprint 22 | Link-Not-Merge v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Excititor Guild | POLICY-ENGINE-40-002 | Integrate VEX linkset conflicts into effective findings/explain traces. | -| Sprint 22 | Link-Not-Merge v1 | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-LNM-21-001 | Update report/runtime payloads to consume linksets and surface source evidence. | -| Sprint 22 | Link-Not-Merge v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-LNM-22-001 | Deliver Evidence panel with policy banner and source observations. | -| Sprint 22 | Link-Not-Merge v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-LNM-22-003 | Add VEX evidence tab with conflict indicators and exports. | -| Sprint 22 | Link-Not-Merge v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-LNM-21-001 | Surface advisory observation/linkset APIs through gateway with RBAC. | -| Sprint 22 | Link-Not-Merge v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-LNM-21-002 | Expose VEX observation/linkset endpoints with export handling. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-23-015 | Produce `/docs/architecture/console.md` describing packages, data flow, SSE design. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-23-017 | Create `/docs/examples/ui-tours.md` walkthroughs with annotated screenshots/GIFs. | -| Sprint 23 | StellaOps Console | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-23-018 | Execute console security checklist and record Security Guild sign-off. | -| Sprint 23 | StellaOps Console | ops/deployment/TASKS.md | TODO | Deployment Guild | DOWNLOADS-CONSOLE-23-001 | Maintain signed downloads manifest pipeline feeding Console + docs parity checks. | -| Sprint 23 | StellaOps Console | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-CONSOLE-23-001 | Stand up console CI pipeline (pnpm cache, lint, tests, Playwright, Lighthouse, offline runners). | -| Sprint 23 | StellaOps Console | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONSOLE-23-002 | Deliver `stella-console` container + Helm overlays with SBOM/provenance and offline packaging. | -| Sprint 23 | StellaOps Console | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-CONSOLE-23-001 | Register Console OIDC client with PKCE, scopes, short-lived tokens, and offline defaults. | -| Sprint 23 | StellaOps Console | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-CONSOLE-23-002 | Provide tenant catalog/user profile endpoints with audit logging and fresh-auth requirements. | -| Sprint 23 | StellaOps Console | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-CONSOLE-23-003 | Update security docs/sample configs for Console flows, CSP, and session policies. | -| Sprint 23 | StellaOps Console | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001 | Surface `/console/advisories` aggregation views with per-source metadata and filters. | -| Sprint 23 | StellaOps Console | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-002 | Provide advisory delta metrics API for dashboard + live status ticker. | -| Sprint 23 | StellaOps Console | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-003 | Add search helpers for CVE/GHSA/PURL lookups returning evidence fragments. | -| Sprint 23 | StellaOps Console | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-001 | Expose `/console/vex` aggregation endpoints with precedence and provenance. | -| Sprint 23 | StellaOps Console | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-002 | Publish VEX override delta metrics feeding dashboard/status ticker. | -| Sprint 23 | StellaOps Console | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-003 | Implement VEX search helpers for global search and explain drill-downs. | -| Sprint 23 | StellaOps Console | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Scheduler Guild | EXPORT-CONSOLE-23-001 | Implement evidence bundle/export generator with signed manifests and telemetry. | -| Sprint 23 | StellaOps Console | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-CONSOLE-23-001 | Optimize findings/explain APIs for Console filters, aggregation hints, and provenance traces. | -| Sprint 23 | StellaOps Console | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Product Ops | POLICY-CONSOLE-23-002 | Expose simulation diff + approval state metadata for policy workspace scenarios. | -| Sprint 23 | StellaOps Console | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-CONSOLE-23-001 | Deliver Console SBOM catalog API with filters, evaluation metadata, and raw projections. | -| Sprint 23 | StellaOps Console | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-CONSOLE-23-002 | Provide component lookup/neighborhood endpoints for global search and overlays. | -| Sprint 23 | StellaOps Console | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-CONSOLE-23-001 | Extend runs API with SSE progress, queue lag summaries, RBAC actions, and history pagination. | -| Sprint 23 | StellaOps Console | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-CONSOLE-23-201 | Stream run progress events with heartbeat/dedupe for Console SSE consumers. | -| Sprint 23 | StellaOps Console | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-CONSOLE-23-202 | Coordinate evidence bundle job queueing, status tracking, cancellation, and retention. | -| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONSOLE-23-001 | Ship `/console/dashboard` + `/console/filters` aggregates with tenant scoping and deterministic totals. | -| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, Scheduler Guild | WEB-CONSOLE-23-002 | Provide `/console/status` polling and `/console/runs/{id}/stream` SSE proxy with heartbeat/backoff. | -| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, Policy Guild | WEB-CONSOLE-23-003 | Expose `/console/exports` orchestration for evidence bundles, CSV/JSON streaming, manifest retrieval. | -| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONSOLE-23-004 | Implement `/console/search` fan-out router for CVE/GHSA/PURL/SBOM lookups with caching and RBAC. | -| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, DevOps Guild | WEB-CONSOLE-23-005 | Serve `/console/downloads` manifest with signed image metadata and offline guidance. | -| Sprint 24 | Graph & Vuln Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-VULN-24-001 | Extend scopes (`vuln:read`) and signed permalinks. | -> 2025-10-27: Scope enforcement spike paused; no production change landed. -| Sprint 24 | Graph & Vuln Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-GRAPH-24-001 | Surface raw advisory observations/linksets for overlay services (no derived aggregation in ingestion). | -> 2025-10-27: Prototype not merged (query layer + CLI consumer under review); resetting to TODO. -| Sprint 24 | Graph & Vuln Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-GRAPH-24-001 | Surface raw VEX statements/linksets for overlay services (no suppression/precedence logic here). | -| Sprint 24 | Graph & Vuln Explorer v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-60-001 | Maintain Redis effective decision maps for overlays. | -| Sprint 24 | Graph & Vuln Explorer v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-60-002 | Provide simulation bridge for graph what-if APIs. | -| Sprint 24 | Graph & Vuln Explorer v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-GRAPH-24-001 | Build Graph Explorer canvas with virtualization. | -| Sprint 24 | Graph & Vuln Explorer v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-GRAPH-24-002 | Implement overlays (Policy/Evidence/License/Exposure). | -| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-001 | Document exception governance concepts/workflow. | -| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-002 | Document approvals routing / MFA requirements. | -| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-003 | Publish API documentation for exceptions endpoints. | -| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-005 | Document UI exception center + badges. | -| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-006 | Update CLI docs for exception commands. | -| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-007 | Write migration guide for governed exceptions. | -| Sprint 25 | Exceptions v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-EXC-25-001 | Introduce exception scopes and routing matrix with MFA. | -| Sprint 25 | Exceptions v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-EXC-25-002 | Update docs/config samples for exception governance. | -| Sprint 25 | Exceptions v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXC-25-001 | Implement CLI exception workflow commands. | -| Sprint 25 | Exceptions v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXC-25-002 | Extend policy simulate with exception overrides. | -| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-002 | Create exception collections/bindings storage + repos. | -| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-003 | Implement Redis exception cache + invalidation. | -| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-004 | Add metrics/tracing/logging for exception application. | -| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-005 | Hook workers/events for activation/expiry. | -| Sprint 25 | Exceptions v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-25-101 | Implement exception lifecycle worker for activation/expiry. | -| Sprint 25 | Exceptions v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-25-102 | Add expiring notification job & metrics. | -| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-001 | Deliver Exception Center (list/kanban) with workflows. | -| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-002 | Build exception creation wizard with scope/timebox guardrails. | -| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-003 | Add inline exception drafting/proposing from explorers. | -| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-004 | Surface badges/countdowns/explain integration. | -| Sprint 25 | Exceptions v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXC-25-001 | Ship exception CRUD + workflow API endpoints. | -| Sprint 25 | Exceptions v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXC-25-002 | Extend policy endpoints to include exception metadata. | -| Sprint 25 | Exceptions v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXC-25-003 | Emit exception events/notifications with rate limits. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-001 | Document reachability concepts and scoring. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-002 | Document callgraph formats. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-003 | Document runtime facts ingestion. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-004 | Document policy weighting for signals. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-005 | Document UI overlays/timelines. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-006 | Document CLI reachability commands. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-007 | Publish API docs for signals endpoints. | -| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-008 | Write migration guide for enabling reachability. | -| Sprint 26 | Reachability v1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-SIG-26-001 | Provision pipelines/deployments for Signals service. | -| Sprint 26 | Reachability v1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-SIG-26-002 | Add dashboards/alerts for reachability metrics. | -| Sprint 26 | Reachability v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-SIG-26-001 | Add signals scopes/roles + AOC requirements. | -| Sprint 26 | Reachability v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SIG-26-001 | Implement reachability CLI commands (upload/list/explain). | -| Sprint 26 | Reachability v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SIG-26-002 | Add reachability overrides to policy simulate. | -| Sprint 26 | Reachability v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-SIG-26-001 | Expose advisory symbol metadata for signals scoring. | -| Sprint 26 | Reachability v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-SIG-26-001 | Surface vendor exploitability hints to Signals. | -| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-001 | Integrate reachability inputs into policy evaluation and explainers. | -| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-002 | Optimize reachability fact retrieval + cache. | -| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-003 | Update SPL compiler for reachability predicates. | -| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-004 | Emit reachability metrics/traces. | -| Sprint 26 | Reachability v1 | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-SPL-24-001 | Extend SPL schema with reachability predicates/actions. | -| Sprint 26 | Reachability v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-26-201 | Implement reachability joiner worker. | -| Sprint 26 | Reachability v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-26-202 | Implement staleness monitor + notifications. | -| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild, Authority Guild | SIGNALS-24-001 | Stand up Signals API skeleton with RBAC + health checks. Host scaffold ready, waiting on `AUTH-SIG-26-001` to finalize scope issuance and tenant enforcement. | -| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-002 | Implement callgraph ingestion/normalization pipeline. Waiting on SIGNALS-24-001 skeleton deployment. | -| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-003 | Ingest runtime facts and persist context data with AOC provenance. Depends on SIGNALS-24-001 base host. | -| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-004 | Deliver reachability scoring engine writing reachability facts. Blocked until ingestion pipelines unblock. | -| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-005 | Implement caches + signals events. Downstream of SIGNALS-24-004. | -| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-001 | Add reachability columns/badges to Vulnerability Explorer. | -| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-002 | Enhance Why drawer with call path/timeline. | -| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-003 | Add reachability overlay/time slider to SBOM Graph. | -| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-004 | Build Reachability Center + missing sensor view. | -| Sprint 26 | Reachability v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-SIG-26-001 | Expose signals proxy endpoints with pagination and RBAC. | -| Sprint 26 | Reachability v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-SIG-26-002 | Join reachability data into policy/vuln responses. | -| Sprint 26 | Reachability v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-SIG-26-003 | Support reachability overrides in simulate APIs. | -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Guilds | DOCS-POLICY-27-001 | Publish `/docs/policy/studio-overview.md` with lifecycle + roles. | -> Blocked by `REGISTRY-API-27-001` and `POLICY-ENGINE-27-001`; revisit once spec and compile enrichments land. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Console Guilds | DOCS-POLICY-27-002 | Write `/docs/policy/authoring.md` with templates/snippets/lint rules. | -> Blocked by `CONSOLE-STUDIO-27-001` pending; waiting on Studio authoring UX. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Registry Guilds | DOCS-POLICY-27-003 | Document `/docs/policy/versioning-and-publishing.md`. | -> Blocked by `REGISTRY-API-27-007` pending publish/sign pipeline. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Scheduler Guilds | DOCS-POLICY-27-004 | Publish `/docs/policy/simulation.md` with quick vs batch guidance. | -> Blocked by `REGISTRY-API-27-005`/`SCHED-WORKER-27-301` pending batch simulation. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Product Ops | DOCS-POLICY-27-005 | Author `/docs/policy/review-and-approval.md`. | -> Blocked by `REGISTRY-API-27-006` review workflow outstanding. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Guilds | DOCS-POLICY-27-006 | Publish `/docs/policy/promotion.md` covering canary + rollback. | -> Blocked by `REGISTRY-API-27-008` promotion APIs not ready. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & DevEx/CLI Guilds | DOCS-POLICY-27-007 | Update `/docs/policy/cli.md` with new commands + JSON schemas. | -> Blocked by `CLI-POLICY-27-001..004` CLI commands missing. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Registry Guilds | DOCS-POLICY-27-008 | Publish `/docs/policy/api.md` aligning with Registry OpenAPI. | -> Blocked by Registry OpenAPI (`REGISTRY-API-27-001..008`) incomplete. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Security Guilds | DOCS-POLICY-27-009 | Create `/docs/security/policy-attestations.md`. | -> Blocked by `AUTH-POLICY-27-002` signing integration pending. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Architecture Guilds | DOCS-POLICY-27-010 | Write `/docs/architecture/policy-registry.md`. | -> Blocked by `REGISTRY-API-27-001` & `SCHED-WORKER-27-301` not delivered. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Observability Guilds | DOCS-POLICY-27-011 | Publish `/docs/observability/policy-telemetry.md`. | -> Blocked by `DEVOPS-POLICY-27-004` observability work outstanding. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Ops Guilds | DOCS-POLICY-27-012 | Write `/docs/runbooks/policy-incident.md`. | -> Blocked by `DEPLOY-POLICY-27-002` ops playbooks pending. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Guilds | DOCS-POLICY-27-013 | Update `/docs/examples/policy-templates.md`. | -> Blocked by `CONSOLE-STUDIO-27-001`/`REGISTRY-API-27-002` templates missing. -| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Registry Guilds | DOCS-POLICY-27-014 | Refresh `/docs/aoc/aoc-guardrails.md` with Studio guardrails. | -> Blocked by `REGISTRY-API-27-003` & `WEB-POLICY-27-001` guardrails not implemented. -| Sprint 27 | Policy Studio | ops/deployment/TASKS.md | TODO | Deployment & Policy Registry Guilds | DEPLOY-POLICY-27-001 | Create Helm/Compose overlays for Policy Registry + workers with signing config. | -| Sprint 27 | Policy Studio | ops/deployment/TASKS.md | TODO | Deployment & Policy Guilds | DEPLOY-POLICY-27-002 | Document policy rollout/rollback playbooks in runbook. | -| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-POLICY-27-001 | Add CI stage for policy lint/compile/test + secret scanning and artifacts. | -| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps & Policy Registry Guilds | DEVOPS-POLICY-27-002 | Provide optional batch simulation CI job with drift gating + PR comment. | -| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps & Security Guilds | DEVOPS-POLICY-27-003 | Manage signing keys + attestation verification in pipelines. | -| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps & Observability Guilds | DEVOPS-POLICY-27-004 | Build dashboards/alerts for compile latency, queue depth, approvals, promotions. | -| Sprint 27 | Policy Studio | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-POLICY-27-001 | Define Policy Studio roles/scopes for author/review/approve/operate/audit. | -| Sprint 27 | Policy Studio | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guilds | AUTH-POLICY-27-002 | Wire signing service + fresh-auth enforcement for publish/promote. | -| Sprint 27 | Policy Studio | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-POLICY-27-003 | Update authority configuration/docs for Policy Studio roles & signing. | -| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-001 | Implement policy workspace CLI commands (init, lint, compile, test). | -| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-002 | Add version bump, submit, review/approve CLI workflow commands. | -| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-003 | Extend simulate command for quick/batch runs, manifests, CI reports. | -| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-004 | Implement publish/promote/rollback/sign CLI lifecycle commands. | -| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI & Docs Guilds | CLI-POLICY-27-005 | Update CLI docs/reference for Policy Studio commands and schemas. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-001 | Return rule coverage, symbol table, docs, hashes from compile endpoint. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-002 | Enhance simulate outputs with heatmap, explain traces, delta summaries. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-003 | Enforce complexity/time limits with diagnostics. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-004 | Update tests/fixtures for coverage, symbol table, explain, complexity. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-001 | Define Policy Registry OpenAPI spec for workspaces, versions, reviews, simulations, promotions, attestations. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-002 | Implement workspace storage + CRUD with tenant retention policies. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-003 | Integrate compile pipeline storing diagnostics, symbol tables, complexity metrics. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-004 | Deliver quick simulation API with limits and deterministic outputs. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & Scheduler Guilds | REGISTRY-API-27-005 | Build batch simulation orchestration, reduction, and evidence bundle storage. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-006 | Implement review workflow with comments, required approvers, webhooks. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & Security Guilds | REGISTRY-API-27-007 | Ship publish/sign pipeline with attestations, immutable versions. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-008 | Implement promotion/canary bindings per tenant/environment with rollback. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & Observability Guilds | REGISTRY-API-27-009 | Instrument metrics/logs/traces for compile, simulation, approval latency. | -| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & QA Guilds | REGISTRY-API-27-010 | Build unit/integration/load test suites and seeded fixtures. | -| Sprint 27 | Policy Studio | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-CONSOLE-27-001 | Provide policy simulation orchestration endpoints with SSE + RBAC. | -| Sprint 27 | Policy Studio | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService & Observability Guilds | SCHED-CONSOLE-27-002 | Emit policy simulation telemetry endpoints/metrics + webhooks. | -| Sprint 27 | Policy Studio | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-27-301 | Implement batch simulation worker sharding SBOMs with retries/backoff. | -| Sprint 27 | Policy Studio | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-27-302 | Build reducer job aggregating shard outputs into manifests with checksums. | -| Sprint 27 | Policy Studio | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker & Security Guilds | SCHED-WORKER-27-303 | Enforce tenant isolation/attestation integration and secret scanning for jobs. | -| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-27-001 | Proxy Policy Registry APIs with tenant scoping, RBAC, evidence streaming. | -| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-27-002 | Implement review lifecycle routes with audit logs and webhooks. | -| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Scheduler Guilds | WEB-POLICY-27-003 | Expose quick/batch simulation endpoints with SSE progress + manifests. | -| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Security Guilds | WEB-POLICY-27-004 | Add publish/promote/rollback endpoints with canary + signing enforcement. | -| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Observability Guilds | WEB-POLICY-27-005 | Instrument Policy Studio metrics/logs for dashboards. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & SBOM Guilds | DOCS-GRAPH-28-001 | Publish `/docs/sbom/graph-explorer-overview.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Console Guilds | DOCS-GRAPH-28-002 | Write `/docs/sbom/graph-using-the-console.md` with walkthrough + accessibility tips. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Graph API Guilds | DOCS-GRAPH-28-003 | Document `/docs/sbom/graph-query-language.md` (JSON schema, cost rules). | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Graph API Guilds | DOCS-GRAPH-28-004 | Publish `/docs/sbom/graph-api.md` endpoints + streaming guidance. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & CLI Guilds | DOCS-GRAPH-28-005 | Produce `/docs/sbom/graph-cli.md` command reference. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Policy Guilds | DOCS-GRAPH-28-006 | Publish `/docs/policy/graph-overlays.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Excitator Guilds | DOCS-GRAPH-28-007 | Document `/docs/vex/graph-integration.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Concelier Guilds | DOCS-GRAPH-28-008 | Document `/docs/advisories/graph-integration.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Architecture Guilds | DOCS-GRAPH-28-009 | Author `/docs/architecture/graph-services.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Observability Guilds | DOCS-GRAPH-28-010 | Publish `/docs/observability/graph-telemetry.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Ops Guilds | DOCS-GRAPH-28-011 | Write `/docs/runbooks/graph-incidents.md`. | -| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Security Guilds | DOCS-GRAPH-28-012 | Create `/docs/security/graph-rbac.md`. | -| Sprint 28 | Graph Explorer | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-GRAPH-28-001 | Provide deployment/offline instructions for Graph Indexer/API, including cache seeds. | -| Sprint 28 | Graph Explorer | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-GRAPH-28-001 | Configure load/perf tests, query budget alerts, and CI smoke for graph APIs. | -| Sprint 28 | Graph Explorer | ops/devops/TASKS.md | TODO | DevOps & Security Guilds | DEVOPS-GRAPH-28-002 | Implement caching/backpressure limits, rate limiting configs, and runaway query kill switches. | -| Sprint 28 | Graph Explorer | ops/devops/TASKS.md | TODO | DevOps & Observability Guilds | DEVOPS-GRAPH-28-003 | Build dashboards/alerts for tile latency, query denials, memory pressure. | -| Sprint 28 | Graph Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-GRAPH-28-001 | Ship `stella sbom graph` subcommands (search, query, paths, diff, impacted, export) with JSON output + exit codes. | -| Sprint 28 | Graph Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-GRAPH-28-002 | Add saved query management + deep link helpers to CLI. | -| Sprint 28 | Graph Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-GRAPH-28-003 | Update CLI docs/examples for Graph Explorer commands. | -| Sprint 28 | Graph Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-GRAPH-24-101 | Deliver advisory summary API feeding graph tooltips. | -| Sprint 28 | Graph Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-GRAPH-28-102 | Add batch fetch for advisory observations/linksets keyed by component sets to feed Graph overlay tooltips efficiently. | -| Sprint 28 | Graph Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | WEB-LNM-21-001 | Provide advisory observation endpoints optimized for graph overlays. | -| Sprint 28 | Graph Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-GRAPH-24-101 | Provide VEX summary API for Graph Explorer inspector overlays. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-001 | Publish Graph API OpenAPI + JSON schemas for queries/tiles. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-002 | Implement `/graph/search` with caching and RBAC. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-003 | Build query planner + streaming tile pipeline with budgets. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-004 | Deliver `/graph/paths` with depth limits and policy overlay support. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-005 | Implement `/graph/diff` streaming adds/removes/changes for SBOM snapshots. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-006 | Compose advisory/VEX/policy overlays with caching + explain sampling. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-007 | Provide export jobs (GraphML/CSV/NDJSON/PNG/SVG) with manifests. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & Authority Guilds | GRAPH-API-28-008 | Enforce RBAC scopes, tenant headers, audit logging, rate limits. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & Observability Guilds | GRAPH-API-28-009 | Instrument metrics/logs/traces; publish dashboards. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & QA Guilds | GRAPH-API-28-010 | Build unit/integration/load tests with synthetic datasets. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & DevOps Guilds | GRAPH-API-28-011 | Ship deployment/offline manifests + gateway integration docs. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-001 | Define node/edge schemas, identity rules, and fixtures for graph ingestion. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-002 | Implement SBOM ingest consumer generating artifact/package/file nodes & edges. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-003 | Serve advisory overlay tiles from Conseiller linksets (no mutation of raw node/edge stores). | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-004 | Integrate VEX statements for `vex_exempts` edges with precedence metadata. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & Policy Guilds | GRAPH-INDEX-28-005 | Hydrate policy overlay nodes/edges referencing determinations + explains. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-006 | Produce graph snapshots per SBOM with lineage for diff jobs. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & Observability Guilds | GRAPH-INDEX-28-007 | Run clustering/centrality background jobs and persist cluster ids. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-008 | Build incremental/backfill pipeline with change streams, retries, backlog metrics. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & QA Guilds | GRAPH-INDEX-28-009 | Extend tests/perf fixtures ensuring determinism on large graphs. | -| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & DevOps Guilds | GRAPH-INDEX-28-010 | Provide deployment/offline artifacts and docs for Graph Indexer. | -| Sprint 28 | Graph Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-30-001 | Finalize graph overlay contract + projection API. | -| Sprint 28 | Graph Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-30-002 | Implement simulation overlay bridge for Graph Explorer queries. | -| Sprint 28 | Graph Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy & Scheduler Guilds | POLICY-ENGINE-30-003 | Emit change events for effective findings supporting graph overlays. | -| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DOING (2025-10-26) | Scheduler WebService Guild, Scheduler Storage Guild | SCHED-WEB-21-004 | Persist graph jobs + emit completion events/webhook. | -| Sprint 28 | Graph Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-21-201 | Run graph build worker for SBOM snapshots with retries/backoff. | -| Sprint 28 | Graph Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-21-202 | Execute overlay refresh worker subscribing to change events. | -| Sprint 28 | Graph Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker & Observability Guilds | SCHED-WORKER-21-203 | Emit metrics/logs for graph build/overlay jobs. | -| Sprint 28 | Graph Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-GRAPH-24-001 | Route `/graph/*` APIs through gateway with tenant scoping and RBAC. | -| Sprint 28 | Graph Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-GRAPH-24-002 | Maintain overlay proxy routes to dedicated services (Policy/Vuln API), ensuring caching + RBAC only. | -| Sprint 28 | Graph Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Observability Guilds | WEB-GRAPH-24-004 | Add Graph Explorer telemetry endpoints and metrics aggregation. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs Guild | DOCS-VULN-29-001 | Publish `/docs/vuln/explorer-overview.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Console Guilds | DOCS-VULN-29-002 | Write `/docs/vuln/explorer-using-console.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs Guild | DOCS-VULN-29-003 | Author `/docs/vuln/explorer-api.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs Guild | DOCS-VULN-29-004 | Publish `/docs/vuln/explorer-cli.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Ledger Guilds | DOCS-VULN-29-005 | Document Findings Ledger (`/docs/vuln/findings-ledger.md`). | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Policy Guilds | DOCS-VULN-29-006 | Update `/docs/policy/vuln-determinations.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Excititor Guilds | DOCS-VULN-29-007 | Publish `/docs/vex/explorer-integration.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Concelier Guilds | DOCS-VULN-29-008 | Publish `/docs/advisories/explorer-integration.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & SBOM Guilds | DOCS-VULN-29-009 | Publish `/docs/sbom/vuln-resolution.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Observability Guilds | DOCS-VULN-29-010 | Publish `/docs/observability/vuln-telemetry.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Security Guilds | DOCS-VULN-29-011 | Publish `/docs/security/vuln-rbac.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Ops Guilds | DOCS-VULN-29-012 | Publish `/docs/runbooks/vuln-ops.md`. | -| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Deployment Guilds | DOCS-VULN-29-013 | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API. | -| Sprint 29 | Vulnerability Explorer | ops/deployment/TASKS.md | TODO | Deployment & Findings Ledger Guilds | DEPLOY-VULN-29-001 | Provide deployments for Findings Ledger/projector with migrations/backups. | -| Sprint 29 | Vulnerability Explorer | ops/deployment/TASKS.md | TODO | Deployment & Vuln Explorer API Guilds | DEPLOY-VULN-29-002 | Package Vuln Explorer API deployments/health checks/offline kit notes. | -| Sprint 29 | Vulnerability Explorer | ops/devops/TASKS.md | TODO | DevOps & Findings Ledger Guilds | DEVOPS-VULN-29-001 | Set up CI/backups/anchoring monitoring for Findings Ledger. | -| Sprint 29 | Vulnerability Explorer | ops/devops/TASKS.md | TODO | DevOps & Vuln Explorer API Guilds | DEVOPS-VULN-29-002 | Configure Vuln Explorer perf tests, budgets, dashboards, alerts. | -| Sprint 29 | Vulnerability Explorer | ops/devops/TASKS.md | TODO | DevOps & Console Guilds | DEVOPS-VULN-29-003 | Integrate Vuln Explorer telemetry pipeline with privacy safeguards + dashboards. | -| Sprint 29 | Vulnerability Explorer | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Vuln Explorer RBAC/ABAC scopes and issuer metadata. | -| Sprint 29 | Vulnerability Explorer | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-VULN-29-002 | Enforce CSRF, attachment signing, and audit logging referencing ledger hashes. | -| Sprint 29 | Vulnerability Explorer | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-VULN-29-003 | Update docs/config samples for Vuln Explorer roles and security posture. | -| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-001 | Implement `stella vuln list` with grouping, filters, JSON/CSV output. | -| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-002 | Implement `stella vuln show` with evidence/policy/path display. | -| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-003 | Add workflow CLI commands (assign/comment/accept-risk/verify-fix/target-fix/reopen). | -| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-004 | Implement `stella vuln simulate` producing diff summaries/Markdown. | -| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-005 | Implement `stella vuln export` and bundle signature verification. | -| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI & Docs Guilds | CLI-VULN-29-006 | Update CLI docs/examples for Vulnerability Explorer commands. | -| Sprint 29 | Vulnerability Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-VULN-29-001 | Canonicalize (lossless) advisory identifiers, persist `links[]`, backfill, and expose raw payload snapshots (no merge/derived fields). | -| Sprint 29 | Vulnerability Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-VULN-29-002 | Provide advisory evidence retrieval endpoint for Vuln Explorer. | -| Sprint 29 | Vulnerability Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService & Observability Guilds | CONCELIER-VULN-29-004 | Add metrics/logs/events for advisory normalization supporting resolver. | -| Sprint 29 | Vulnerability Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001 | Canonicalize (lossless) VEX keys and product scopes with backfill + links (no merge/suppression). | -| Sprint 29 | Vulnerability Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-002 | Expose VEX evidence retrieval endpoint for Explorer evidence tabs. | -| Sprint 29 | Vulnerability Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService & Observability Guilds | EXCITITOR-VULN-29-004 | Instrument metrics/logs for VEX normalization and suppression events. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-29-001 | Design ledger & projection schemas, hashing strategy, and migrations for Findings Ledger. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-29-002 | Implement ledger write API with hash chaining and Merkle root anchoring job. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Scheduler Guilds | LEDGER-29-003 | Build projector worker deriving `findings_projection` with idempotent replay. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Policy Guilds | LEDGER-29-004 | Integrate Policy Engine batch evaluation into projector with rationale caching. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-29-005 | Implement workflow mutation endpoints producing ledger events (assign/comment/accept-risk/etc.). | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Security Guilds | LEDGER-29-006 | Add attachment encryption, signed URLs, and CSRF protections for workflow endpoints. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Observability Guilds | LEDGER-29-007 | Instrument ledger metrics/logs/alerts (write latency, projection lag, anchoring). | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & QA Guilds | LEDGER-29-008 | Provide replay/determinism/load tests for ledger/projector pipelines. | -| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & DevOps Guilds | LEDGER-29-009 | Deliver deployment/offline artefacts, backup/restore, Merkle anchoring guidance. | -| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-29-001 | Implement policy batch evaluation endpoint returning determinations + rationale. | -| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-29-002 | Provide simulation diff API for Vuln Explorer comparisons. | -| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-29-003 | Include path/scope annotations in determinations for Explorer. | -| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild & Observability Guild | POLICY-ENGINE-29-004 | Add telemetry for batch evaluation + simulation jobs. | -| Sprint 29 | Vulnerability Explorer | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-VULN-29-001 | Emit inventory evidence with scope/runtime/path/safe version hints; publish change events. | -| Sprint 29 | Vulnerability Explorer | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service & Findings Ledger Guilds | SBOM-VULN-29-002 | Provide resolver feed for candidate generation with idempotent delivery. | -| Sprint 29 | Vulnerability Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-VULN-29-001 | Expose resolver job APIs + status monitoring for Vuln Explorer recomputation. | -| Sprint 29 | Vulnerability Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService & Observability Guilds | SCHED-VULN-29-002 | Provide projector lag metrics endpoint + webhook notifications. | -| Sprint 29 | Vulnerability Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-29-001 | Implement resolver worker applying ecosystem version semantics and path scope. | -| Sprint 29 | Vulnerability Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-29-002 | Implement evaluation worker invoking Policy Engine and updating ledger queues. | -| Sprint 29 | Vulnerability Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker & Observability Guilds | SCHED-WORKER-29-003 | Add monitoring for resolver/evaluation backlog and SLA alerts. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-001 | Publish Vuln Explorer OpenAPI + query schemas. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-002 | Implement list/query endpoints with grouping, paging, cost budgets. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-003 | Implement detail endpoint combining evidence, policy rationale, paths, history. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Findings Ledger Guilds | VULN-API-29-004 | Expose workflow APIs writing ledger events with validation + idempotency. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Policy Guilds | VULN-API-29-005 | Implement policy simulation endpoint producing diffs without side effects. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-006 | Integrate Graph Explorer paths metadata and deep-link parameters. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Security Guilds | VULN-API-29-007 | Enforce RBAC/ABAC, CSRF, attachment security, and audit logging. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-008 | Provide evidence bundle export job with signing + manifests. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Observability Guilds | VULN-API-29-009 | Instrument API telemetry (latency, workflow counts, exports). | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & QA Guilds | VULN-API-29-010 | Deliver unit/integration/perf/determinism tests for Vuln Explorer API. | -| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & DevOps Guilds | VULN-API-29-011 | Ship deployment/offline manifests, health checks, scaling docs. | -| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-VULN-29-001 | Route `/vuln/*` APIs with tenant RBAC, ABAC, anti-forgery enforcement. | -| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-VULN-29-002 | Proxy workflow calls to Findings Ledger with correlation IDs + retries. | -| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-VULN-29-003 | Expose simulation/export orchestration with SSE/progress + signed links. | -| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Observability Guilds | WEB-VULN-29-004 | Aggregate Vuln Explorer telemetry (latency, errors, exports). | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-001 | Publish `/docs/vex/consensus-overview.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-002 | Write `/docs/vex/consensus-algorithm.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-003 | Document `/docs/vex/issuer-directory.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-004 | Publish `/docs/vex/consensus-api.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-005 | Create `/docs/vex/consensus-console.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-006 | Add `/docs/policy/vex-trust-model.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-007 | Author `/docs/sbom/vex-mapping.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-008 | Publish `/docs/security/vex-signatures.md`. | -| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-009 | Write `/docs/runbooks/vex-ops.md`. | -| Sprint 30 | VEX Lens | ops/devops/TASKS.md | TODO | DevOps Guild | VEXLENS-30-009, ISSUER-30-005 | Set up CI/perf/telemetry dashboards for VEX Lens and Issuer Directory. | -| Sprint 30 | VEX Lens | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | VEXLENS-30-007 | Implement `stella vex consensus` CLI commands with list/show/simulate/export. | -| Sprint 30 | VEX Lens | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild, VEX Lens Guild | CONCELIER-VEXLENS-30-001 | Guarantee advisory key consistency and provide cross-links for consensus rationale (VEX Lens). | -| Sprint 30 | VEX Lens | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001 | Ensure VEX evidence includes issuer hints, signatures, product trees for Lens consumption. | -| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory Guild | ISSUER-30-001 | Implement issuer CRUD API with RBAC and audit logs. | -| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & Security Guilds | ISSUER-30-002 | Implement key management endpoints with expiry enforcement. | -| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & Policy Guilds | ISSUER-30-003 | Provide trust weight override APIs with audit trails. | -| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & VEX Lens Guilds | ISSUER-30-004 | Integrate issuer data into signature verification clients. | -| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & Observability Guilds | ISSUER-30-005 | Instrument issuer change metrics/logs and dashboards. | -| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & DevOps Guilds | ISSUER-30-006 | Provide deployment/backup/offline docs for Issuer Directory. | -| Sprint 30 | VEX Lens | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-30-101 | Surface trust weighting configuration (issuer weights, modifiers, decay) for VEX Lens via Policy Studio/API. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-001 | Implement VEX normalization pipeline (CSAF, OpenVEX, CycloneDX) with deterministic outputs. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-002 | Build product mapping library aligning CSAF product trees to purls/versions with scope scoring. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Issuer Directory Guilds | VEXLENS-30-003 | Integrate signature verification using issuer keys; annotate evidence. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Policy Guilds | VEXLENS-30-004 | Implement trust weighting functions configurable via policy. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-005 | Implement consensus algorithm producing state, confidence, rationale, and quorum. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Findings Ledger Guilds | VEXLENS-30-006 | Materialize consensus projections and change events. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-007 | Deliver query/detail/simulation/export APIs with budgets and OpenAPI docs. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Policy Guilds | VEXLENS-30-008 | Integrate consensus signals with Policy Engine and Vuln Explorer. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Observability Guilds | VEXLENS-30-009 | Instrument metrics/logs/traces; publish dashboards/alerts. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & QA Guilds | VEXLENS-30-010 | Build unit/property/integration/load tests and determinism harness. | -| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & DevOps Guilds | VEXLENS-30-011 | Provide deployment manifests, scaling guides, offline seeds, runbooks. | -| Sprint 30 | VEX Lens | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, VEX Lens Guild | WEB-VEX-30-007 | Route `/vex/consensus` APIs via gateway with RBAC/ABAC, caching, and telemetry (proxy-only). | -| Sprint 31 | Advisory AI | docs/TASKS.md | TODO | Docs Guild | DOCS-AIAI-31-001 | Publish Advisory AI overview doc. | -| Sprint 31 | Advisory AI | docs/TASKS.md | TODO | Docs Guild | DOCS-AIAI-31-002 | Publish architecture doc for Advisory AI. | -| Sprint 31 | Advisory AI | docs/TASKS.md | TODO | Docs Guild | DOCS-AIAI-31-003..009 | Complete API/Console/CLI/Policy/Security/SBOM/Runbook docs. | -| Sprint 31 | Advisory AI | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-AIAI-31-001 | Provide Advisory AI deployment/offline guidance. | -| Sprint 31 | Advisory AI | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIAI-31-001 | Provision CI/perf/telemetry for Advisory AI. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-001 | Implement advisory/VEX retrievers with paragraph anchors and citations. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-002 | Build SBOM context retriever and blast radius estimator. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-003 | Deliver deterministic toolset (version checks, dependency analysis, policy lookup). | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-004 | Orchestrator with task templates, tool chaining, caching. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & Security Guilds | AIAI-31-005 | Guardrails (redaction, injection defense, output validation). | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-006 | Expose REST/batch APIs with RBAC and OpenAPI. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & Observability Guilds | AIAI-31-007 | Instrument metrics/logs/traces and dashboards. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & DevOps Guilds | AIAI-31-008 | Package inference + deployment manifests/flags. | -| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & QA Guilds | AIAI-31-009 | Build golden/injection/perf tests ensuring determinism. | -| Sprint 31 | Advisory AI | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-AIAI-31-001 | Define Advisory AI scopes and remote inference toggles. | -| Sprint 31 | Advisory AI | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-AIAI-31-002 | Enforce prompt logging and consent/audit flows. | -| Sprint 31 | Advisory AI | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIAI-31-001 | Implement `stella advise *` CLI commands leveraging Advisory AI orchestration and policy scopes. | -| Sprint 31 | Advisory AI | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-AIAI-31-001 | Expose advisory chunk API with paragraph anchors. | -| Sprint 31 | Advisory AI | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-AIAI-31-001 | Provide VEX chunks with justifications and signatures. | -| Sprint 31 | Advisory AI | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-31-001 | Provide policy knobs for Advisory AI. | -| Sprint 31 | Advisory AI | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-AIAI-31-001 | Deliver SBOM path/timeline endpoints for Advisory AI. | -| Sprint 31 | Advisory AI | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-AIAI-31-001 | Expose enriched rationale API for conflict explanations. | -| Sprint 31 | Advisory AI | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-AIAI-31-002 | Provide batching/caching hooks for Advisory AI. | -| Sprint 31 | Advisory AI | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AIAI-31-001 | Route `/advisory/ai/*` APIs with RBAC/telemetry. | -| Sprint 31 | Advisory AI | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AIAI-31-002 | Provide batch orchestration and retry handling for Advisory AI. | -| Sprint 31 | Advisory AI | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AIAI-31-003 | Emit Advisory AI gateway telemetry/audit logs. | -| Sprint 32 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-32-001 | Author `/docs/orchestrator/overview.md` covering mission, roles, AOC alignment, and imposed rule reminder. | -| Sprint 32 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-32-002 | Author `/docs/orchestrator/architecture.md` detailing scheduler, DAGs, rate limits, and data model. | -| Sprint 32 | Orchestrator Dashboard | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ORCH-32-001 | Provision staging Postgres/message-bus charts, CI smoke deploy, and baseline dashboards for queue depth and inflight jobs. | -| Sprint 32 | Orchestrator Dashboard | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-ORCH-32-001 | Introduce `orch:read` scope and `Orch.Viewer` role with metadata, discovery docs, and offline defaults. | -| Sprint 32 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-32-001 | Register Concelier sources with orchestrator, publish schedules/rate policies, and seed metadata. | -| Sprint 32 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-32-002 | Embed worker SDK into Concelier ingestion loops emitting progress, heartbeats, and artifact hashes. | -| Sprint 32 | Orchestrator Dashboard | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-ORCH-32-001 | Adopt worker SDK in Excititor worker with job claim/heartbeat and artifact summary emission. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-32-001 | Bootstrap Go worker SDK (client config, job claim, acknowledgement flow) with integration tests. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-32-002 | Add heartbeat/progress helpers, structured logging, and default metrics exporters to Go SDK. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-32-001 | Bootstrap Python async SDK with job claim/config adapters and sample worker. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-32-002 | Implement heartbeat/progress helpers and logging/metrics instrumentation for Python workers. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-001 | Bootstrap orchestrator service with Postgres schema/migrations for sources, runs, jobs, dag_edges, artifacts, quotas, schedules. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-002 | Implement scheduler DAG planner, dependency resolver, and job state machine for read-only tracking. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-003 | Expose read-only REST APIs (sources, runs, jobs, DAG) with OpenAPI + validation. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-004 | Ship WebSocket/SSE live update stream and metrics counters/histograms for job lifecycle. | -| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-005 | Deliver worker claim/heartbeat/progress endpoints capturing artifact metadata and checksums. | -| Sprint 32 | Orchestrator Dashboard | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-32-101 | Define orchestrator `policy_eval` job contract, idempotency keys, and enqueue hooks for change events. | -| Sprint 32 | Orchestrator Dashboard | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-ORCH-32-001 | Integrate orchestrator job IDs into SBOM ingest/index pipelines with artifact hashing and status updates. | -| Sprint 32 | Orchestrator Dashboard | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-ORCH-32-001 | Expose read-only orchestrator APIs via gateway with tenant scoping, caching headers, and rate limits. | -| Sprint 33 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-33-001 | Author `/docs/orchestrator/api.md` with endpoints, WebSocket events, error codes, and imposed rule reminder. | -| Sprint 33 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-33-002 | Author `/docs/orchestrator/console.md` covering screens, accessibility, and live updates. | -| Sprint 33 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-33-003 | Author `/docs/orchestrator/cli.md` with command reference, examples, and exit codes. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-WORKER-AOC-19-001 | Rewire worker to persist raw VEX docs with guard enforcement. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-WORKER-AOC-19-002 | Enforce signature/checksum verification prior to raw writes. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-AOC-19-001 | Add lint preventing ingestion modules from referencing Policy-only helpers. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild, Security Guild | POLICY-AOC-19-002 | Enforce Policy-only writes to `effective_finding_*` collections. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-AOC-19-003 | Update Policy readers to consume only raw document fields. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild, QA Guild | POLICY-AOC-19-004 | Add determinism tests for raw-driven policy recomputation. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AOC-19-001 | Add Sources dashboard tiles surfacing AOC status and violations. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AOC-19-002 | Build violation drill-down view for offending documents. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AOC-19-003 | Wire "Verify last 24h" action and CLI parity messaging. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Web/StellaOps.Web/TASKS.md | DOING (2025-10-26) | BE-Base Platform Guild | WEB-AOC-19-001 | Provide shared AOC forbidden key set and guard middleware. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AOC-19-002 | Ship provenance builder and signature helpers for ingestion services. | +| Sprint 19 | Aggregation-Only Contract Enforcement | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, QA Guild | WEB-AOC-19-003 | Author analyzer + shared test fixtures for guard compliance. | +| Sprint 20 | Policy Engine v2 | ops/devops/TASKS.md | BLOCKED (waiting on POLICY-ENGINE-20-006) | DevOps Guild | DEVOPS-POLICY-20-002 | Run `stella policy simulate` CI stage against golden SBOMs. | +| Sprint 20 | Policy Engine v2 | src/Bench/StellaOps.Bench/TASKS.md | BLOCKED (waiting on SCHED-WORKER-20-302) | Bench Guild, Scheduler Guild | BENCH-POLICY-20-002 | Add incremental run benchmark capturing delta SLA compliance. | +| Sprint 20 | Policy Engine v2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild, Docs Guild | CLI-POLICY-20-003 | Extend `stella findings` commands with policy filters and explain view. | +> 2025-10-27: Backend helpers drafted but command integration/tests pending; task reset to TODO awaiting follow-up. +| Sprint 20 | Policy Engine v2 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-POLICY-20-002 | Strengthen linkset builders with equivalence tables + range parsing. | +| Sprint 20 | Policy Engine v2 | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-POLICY-20-003 | Add advisory selection cursors + change-stream checkpoints for policy runs. | +| Sprint 20 | Policy Engine v2 | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-POLICY-20-001 | Provide advisory selection endpoints for policy engine (batch PURL/ID). | +| Sprint 20 | Policy Engine v2 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-POLICY-20-002 | Enhance VEX linkset scope + version resolution for policy accuracy. | +| Sprint 20 | Policy Engine v2 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-POLICY-20-003 | Introduce VEX selection cursors + change-stream checkpoints. | +| Sprint 20 | Policy Engine v2 | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-POLICY-20-001 | Ship VEX selection APIs aligned with policy join requirements. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | BLOCKED (2025-10-26) | Policy Guild | POLICY-ENGINE-20-002 | Implement deterministic rule evaluator with priority/first-match semantics. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Concelier Core, Excititor Core | POLICY-ENGINE-20-003 | Build SBOM↔advisory↔VEX linkset joiners with deterministic batching. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Storage Guild | POLICY-ENGINE-20-004 | Materialize effective findings with append-only history and tenant scoping. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Security Guild | POLICY-ENGINE-20-005 | Enforce determinism guard banning wall-clock, RNG, and network usage. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Scheduler Guild | POLICY-ENGINE-20-006 | Implement incremental orchestrator reacting to change streams. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Observability Guild | POLICY-ENGINE-20-007 | Emit policy metrics, traces, and sampled rule-hit logs. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, QA Guild | POLICY-ENGINE-20-008 | Add unit/property/golden/perf suites verifying determinism + SLA. | +| Sprint 20 | Policy Engine v2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Storage Guild | POLICY-ENGINE-20-009 | Define Mongo schemas/indexes + migrations for policies/runs/findings. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-20-002 | Update schema docs with policy run lifecycle samples. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-20-001 | Expose policy run scheduling APIs with scope enforcement. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-20-002 | Provide simulation trigger endpoint returning diff metadata. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-20-301 | Schedule policy runs via API with idempotent job tracking. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-20-302 | Implement delta targeting leveraging change streams + policy metadata. | +| Sprint 20 | Policy Engine v2 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild, Observability Guild | SCHED-WORKER-20-303 | Expose policy scheduling metrics/logs with policy/run identifiers. | +| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-POLICY-20-001 | Ship Monaco-based policy editor with inline diagnostics + checklists. | +| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-POLICY-20-002 | Build simulation panel with deterministic diff rendering + virtualization. | +| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild, Product Ops | UI-POLICY-20-003 | Implement submit/review/approve workflow with RBAC + audit trail. | +| Sprint 20 | Policy Engine v2 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild, Observability Guild | UI-POLICY-20-004 | Add run dashboards (heatmap/VEX wins/suppressions) with export. | +| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-20-001 | Implement Policy CRUD/compile/run/simulate/findings/explain endpoints. | +| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-20-002 | Add pagination, filters, deterministic ordering to policy listings. | +| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, QA Guild | WEB-POLICY-20-003 | Map engine errors to `ERR_POL_*` responses with contract tests. | +| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web/TASKS.md | TODO | Platform Reliability Guild | WEB-POLICY-20-004 | Introduce rate limits/quotas + metrics for simulation endpoints. | +| Sprint 21 | Graph Explorer v1 | src/Bench/StellaOps.Bench/TASKS.md | BLOCKED (2025-10-27) | Bench Guild, Graph Platform Guild | BENCH-GRAPH-21-001 | Graph viewport/path perf harness (50k/100k nodes) measuring Graph API/Indexer latency and cache hit rates. Executed within Sprint 28 Graph program. Upstream Graph API/indexer contracts (`GRAPH-API-28-003`, `GRAPH-INDEX-28-006`) still pending, so benchmarks cannot target stable endpoints yet. | +| Sprint 21 | Graph Explorer v1 | src/Bench/StellaOps.Bench/TASKS.md | BLOCKED (2025-10-27) | Bench Guild, UI Guild | BENCH-GRAPH-21-002 | Headless UI load benchmark for graph canvas interactions (Playwright) tracking render FPS budgets. Executed within Sprint 28 Graph program. Depends on BENCH-GRAPH-21-001 and UI Graph Explorer (`UI-GRAPH-24-001`), both pending. | +| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | BLOCKED (2025-10-27) | Concelier Core Guild | CONCELIER-GRAPH-21-001 | Enrich SBOM normalization with relationships, scopes, entrypoint annotations for Cartographer. Requires finalized schemas from `CONCELIER-POLICY-20-002` and Cartographer event contract (`CARTO-GRAPH-21-002`). | +| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | BLOCKED (2025-10-27) | Concelier Core & Scheduler Guilds | CONCELIER-GRAPH-21-002 | Publish SBOM change events with tenant metadata for graph builds. Awaiting projection schema from `CONCELIER-GRAPH-21-001` and Cartographer webhook expectations. | +| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | BLOCKED (2025-10-27) | Excititor Core Guild | EXCITITOR-GRAPH-21-001 | Deliver batched VEX/advisory fetch helpers for inspector linkouts. Waiting on linkset enrichment (`EXCITITOR-POLICY-20-002`) and Cartographer inspector contract (`CARTO-GRAPH-21-005`). | +| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | BLOCKED (2025-10-27) | Excititor Core Guild | EXCITITOR-GRAPH-21-002 | Enrich overlay metadata with VEX justification summaries for graph overlays. Depends on `EXCITITOR-GRAPH-21-001` and Policy overlay schema (`POLICY-ENGINE-30-001`). | +| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | BLOCKED (2025-10-27) | Excititor Storage Guild | EXCITITOR-GRAPH-21-005 | Create indexes/materialized views for VEX lookups by PURL/policy. Awaiting access pattern specs from `EXCITITOR-GRAPH-21-001`. | +| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service Guild | SBOM-SERVICE-21-001 | Expose normalized SBOM projection API with relationships, scopes, entrypoints. Waiting on Concelier projection schema (`CONCELIER-GRAPH-21-001`). | +| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service & Scheduler Guilds | SBOM-SERVICE-21-002 | Emit SBOM version change events for Cartographer build queue. Depends on SBOM projection API (`SBOM-SERVICE-21-001`) and Scheduler contracts. | +| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service Guild | SBOM-SERVICE-21-003 | Provide entrypoint management API with tenant overrides. Blocked by SBOM projection API contract. | +| Sprint 21 | Graph Explorer v1 | src/SbomService/StellaOps.SbomService/TASKS.md | BLOCKED (2025-10-27) | SBOM Service & Observability Guilds | SBOM-SERVICE-21-004 | Add metrics/traces/logs for SBOM projections. Requires projection pipeline from `SBOM-SERVICE-21-001`. | +| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base Platform Guild | WEB-GRAPH-21-001 | Add gateway routes for graph APIs with scope enforcement and streaming. Upstream Graph API (`GRAPH-API-28-003`) and Authority scope work (`AUTH-VULN-24-001`) pending. | +| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base Platform Guild | WEB-GRAPH-21-002 | Implement bbox/zoom/path validation and pagination for graph endpoints. Depends on core proxy routes. | +| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base Platform & QA Guilds | WEB-GRAPH-21-003 | Map graph errors to `ERR_Graph_*` and support export streaming. Requires `WEB-GRAPH-21-001`. | +| Sprint 21 | Graph Explorer v1 | src/Web/StellaOps.Web/TASKS.md | BLOCKED (2025-10-27) | BE-Base & Policy Guilds | WEB-GRAPH-21-004 | Wire Policy Engine simulation overlays into graph responses. Waiting on Graph routes and Policy overlay schema (`POLICY-ENGINE-30-002`). | +| Sprint 22 | Link-Not-Merge v1 | docs/TASKS.md | BLOCKED (2025-10-27) | Docs Guild | DOCS-LNM-22-001 | Publish advisories aggregation doc with observation/linkset philosophy. | +> Blocked by `CONCELIER-LNM-21-001..003`; draft doc exists but final alignment waits for schema/API delivery. +| Sprint 22 | Link-Not-Merge v1 | docs/TASKS.md | BLOCKED (2025-10-27) | Docs Guild | DOCS-LNM-22-002 | Publish VEX aggregation doc describing observation/linkset flow. | +> Blocked by `EXCITITOR-LNM-21-001..003`; draft doc staged pending observation/linkset implementation. +| Sprint 22 | Link-Not-Merge v1 | docs/TASKS.md | BLOCKED (2025-10-27) | Docs Guild | DOCS-LNM-22-005 | Document UI evidence panel with conflict badges/AOC drill-down. | +> Blocked by `UI-LNM-22-001..003`; need shipping UI to capture screenshots and finalize guidance. +| Sprint 22 | Link-Not-Merge v1 | ops/devops/TASKS.md | BLOCKED (2025-10-27) | DevOps Guild | DEVOPS-LNM-22-001 | Execute advisory observation/linkset migration/backfill and automation. | +| Sprint 22 | Link-Not-Merge v1 | ops/devops/TASKS.md | BLOCKED (2025-10-27) | DevOps Guild | DEVOPS-LNM-22-002 | Run VEX observation/linkset migration/backfill with monitoring/runbook. | +| Sprint 22 | Link-Not-Merge v1 | samples/TASKS.md | BLOCKED (2025-10-27) | Samples Guild | SAMPLES-LNM-22-001 | Add advisory observation/linkset fixtures with conflicts. | +| Sprint 22 | Link-Not-Merge v1 | samples/TASKS.md | BLOCKED (2025-10-27) | Samples Guild | SAMPLES-LNM-22-002 | Add VEX observation/linkset fixtures with status disagreements. | +| Sprint 22 | Link-Not-Merge v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-AOC-22-001 | Roll out new advisory/vex ingest/read scopes. | +| Sprint 22 | Link-Not-Merge v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-LNM-22-001 | Implement advisory observation/linkset CLI commands with JSON/OSV export. | +| Sprint 22 | Link-Not-Merge v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-LNM-22-002 | Implement VEX observation/linkset CLI commands. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-LNM-21-001 | Define immutable advisory observation schema with AOC metadata. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild, Data Science Guild | CONCELIER-LNM-21-002 | Implement advisory linkset builder with correlation signals/conflicts. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md | TODO | BE-Merge | MERGE-LNM-21-002 | Deprecate merge service and enforce observation-only pipeline. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage Guild | CONCELIER-LNM-21-101 | Provision observations/linksets collections and indexes. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Concelier Storage & DevOps Guilds | CONCELIER-LNM-21-102 | Backfill legacy merged advisories into observations/linksets with rollback tooling. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-LNM-21-201 | Ship advisory observation read APIs with pagination/RBAC. | +| Sprint 22 | Link-Not-Merge v1 | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-LNM-21-202 | Implement advisory linkset read/export/evidence endpoints mapped to `ERR_AGG_*`. | +| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-LNM-21-001 | Define immutable VEX observation model. | +| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-LNM-21-002 | Build VEX linkset correlator with confidence/conflict recording. | +| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage Guild | EXCITITOR-LNM-21-101 | Provision VEX observation/linkset collections and indexes. | +| Sprint 22 | Link-Not-Merge v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Excititor Storage & DevOps Guilds | EXCITITOR-LNM-21-102 | Backfill legacy VEX data into observations/linksets with rollback scripts. | +| Sprint 22 | Link-Not-Merge v1 | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-201 | Expose VEX observation APIs with filters/pagination and RBAC. | +| Sprint 22 | Link-Not-Merge v1 | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-202 | Implement VEX linkset endpoints + exports with evidence payloads. | +| Sprint 22 | Link-Not-Merge v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-40-001 | Update severity selection to handle multiple source severities per linkset. | +| Sprint 22 | Link-Not-Merge v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Excititor Guild | POLICY-ENGINE-40-002 | Integrate VEX linkset conflicts into effective findings/explain traces. | +| Sprint 22 | Link-Not-Merge v1 | src/Scanner/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-LNM-21-001 | Update report/runtime payloads to consume linksets and surface source evidence. | +| Sprint 22 | Link-Not-Merge v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-LNM-22-001 | Deliver Evidence panel with policy banner and source observations. | +| Sprint 22 | Link-Not-Merge v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-LNM-22-003 | Add VEX evidence tab with conflict indicators and exports. | +| Sprint 22 | Link-Not-Merge v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-LNM-21-001 | Surface advisory observation/linkset APIs through gateway with RBAC. | +| Sprint 22 | Link-Not-Merge v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-LNM-21-002 | Expose VEX observation/linkset endpoints with export handling. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-23-015 | Produce `/docs/architecture/console.md` describing packages, data flow, SSE design. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-23-017 | Create `/docs/examples/ui-tours.md` walkthroughs with annotated screenshots/GIFs. | +| Sprint 23 | StellaOps Console | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-23-018 | Execute console security checklist and record Security Guild sign-off. | +| Sprint 23 | StellaOps Console | ops/deployment/TASKS.md | TODO | Deployment Guild | DOWNLOADS-CONSOLE-23-001 | Maintain signed downloads manifest pipeline feeding Console + docs parity checks. | +| Sprint 23 | StellaOps Console | ops/devops/TASKS.md | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-CONSOLE-23-001 | Stand up console CI pipeline (pnpm cache, lint, tests, Playwright, Lighthouse, offline runners). | +| Sprint 23 | StellaOps Console | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONSOLE-23-002 | Deliver `stella-console` container + Helm overlays with SBOM/provenance and offline packaging. | +| Sprint 23 | StellaOps Console | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-CONSOLE-23-001 | Register Console OIDC client with PKCE, scopes, short-lived tokens, and offline defaults. | +| Sprint 23 | StellaOps Console | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-CONSOLE-23-002 | Provide tenant catalog/user profile endpoints with audit logging and fresh-auth requirements. | +| Sprint 23 | StellaOps Console | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-CONSOLE-23-003 | Update security docs/sample configs for Console flows, CSP, and session policies. | +| Sprint 23 | StellaOps Console | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001 | Surface `/console/advisories` aggregation views with per-source metadata and filters. | +| Sprint 23 | StellaOps Console | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-002 | Provide advisory delta metrics API for dashboard + live status ticker. | +| Sprint 23 | StellaOps Console | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-003 | Add search helpers for CVE/GHSA/PURL lookups returning evidence fragments. | +| Sprint 23 | StellaOps Console | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-001 | Expose `/console/vex` aggregation endpoints with precedence and provenance. | +| Sprint 23 | StellaOps Console | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-002 | Publish VEX override delta metrics feeding dashboard/status ticker. | +| Sprint 23 | StellaOps Console | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-003 | Implement VEX search helpers for global search and explain drill-downs. | +| Sprint 23 | StellaOps Console | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Scheduler Guild | EXPORT-CONSOLE-23-001 | Implement evidence bundle/export generator with signed manifests and telemetry. | +| Sprint 23 | StellaOps Console | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-CONSOLE-23-001 | Optimize findings/explain APIs for Console filters, aggregation hints, and provenance traces. | +| Sprint 23 | StellaOps Console | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild, Product Ops | POLICY-CONSOLE-23-002 | Expose simulation diff + approval state metadata for policy workspace scenarios. | +| Sprint 23 | StellaOps Console | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-CONSOLE-23-001 | Deliver Console SBOM catalog API with filters, evaluation metadata, and raw projections. | +| Sprint 23 | StellaOps Console | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-CONSOLE-23-002 | Provide component lookup/neighborhood endpoints for global search and overlays. | +| Sprint 23 | StellaOps Console | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-CONSOLE-23-001 | Extend runs API with SSE progress, queue lag summaries, RBAC actions, and history pagination. | +| Sprint 23 | StellaOps Console | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-CONSOLE-23-201 | Stream run progress events with heartbeat/dedupe for Console SSE consumers. | +| Sprint 23 | StellaOps Console | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-CONSOLE-23-202 | Coordinate evidence bundle job queueing, status tracking, cancellation, and retention. | +| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONSOLE-23-001 | Ship `/console/dashboard` + `/console/filters` aggregates with tenant scoping and deterministic totals. | +| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, Scheduler Guild | WEB-CONSOLE-23-002 | Provide `/console/status` polling and `/console/runs/{id}/stream` SSE proxy with heartbeat/backoff. | +| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, Policy Guild | WEB-CONSOLE-23-003 | Expose `/console/exports` orchestration for evidence bundles, CSV/JSON streaming, manifest retrieval. | +| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONSOLE-23-004 | Implement `/console/search` fan-out router for CVE/GHSA/PURL/SBOM lookups with caching and RBAC. | +| Sprint 23 | StellaOps Console | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, DevOps Guild | WEB-CONSOLE-23-005 | Serve `/console/downloads` manifest with signed image metadata and offline guidance. | +| Sprint 24 | Graph & Vuln Explorer v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-VULN-24-001 | Extend scopes (`vuln:view`/`vuln:investigate`/`vuln:operate`/`vuln:audit`) and signed permalinks. | +> 2025-10-27: Scope enforcement spike paused; no production change landed. +| Sprint 24 | Graph & Vuln Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-GRAPH-24-001 | Surface raw advisory observations/linksets for overlay services (no derived aggregation in ingestion). | +> 2025-10-27: Prototype not merged (query layer + CLI consumer under review); resetting to TODO. +| Sprint 24 | Graph & Vuln Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-GRAPH-24-001 | Surface raw VEX statements/linksets for overlay services (no suppression/precedence logic here). | +| Sprint 24 | Graph & Vuln Explorer v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-60-001 | Maintain Redis effective decision maps for overlays. | +| Sprint 24 | Graph & Vuln Explorer v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-60-002 | Provide simulation bridge for graph what-if APIs. | +| Sprint 24 | Graph & Vuln Explorer v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-GRAPH-24-001 | Build Graph Explorer canvas with virtualization. | +| Sprint 24 | Graph & Vuln Explorer v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-GRAPH-24-002 | Implement overlays (Policy/Evidence/License/Exposure). | +| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-001 | Document exception governance concepts/workflow. | +| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-002 | Document approvals routing / MFA requirements. | +| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-003 | Publish API documentation for exceptions endpoints. | +| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-005 | Document UI exception center + badges. | +| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-006 | Update CLI docs for exception commands. | +| Sprint 25 | Exceptions v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXC-25-007 | Write migration guide for governed exceptions. | +| Sprint 25 | Exceptions v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-EXC-25-001 | Introduce exception scopes and routing matrix with MFA. | +| Sprint 25 | Exceptions v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-EXC-25-002 | Update docs/config samples for exception governance. | +| Sprint 25 | Exceptions v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXC-25-001 | Implement CLI exception workflow commands. | +| Sprint 25 | Exceptions v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXC-25-002 | Extend policy simulate with exception overrides. | +| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-002 | Create exception collections/bindings storage + repos. | +| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-003 | Implement Redis exception cache + invalidation. | +| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-004 | Add metrics/tracing/logging for exception application. | +| Sprint 25 | Exceptions v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-70-005 | Hook workers/events for activation/expiry. | +| Sprint 25 | Exceptions v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-25-101 | Implement exception lifecycle worker for activation/expiry. | +| Sprint 25 | Exceptions v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-25-102 | Add expiring notification job & metrics. | +| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-001 | Deliver Exception Center (list/kanban) with workflows. | +| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-002 | Build exception creation wizard with scope/timebox guardrails. | +| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-003 | Add inline exception drafting/proposing from explorers. | +| Sprint 25 | Exceptions v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-EXC-25-004 | Surface badges/countdowns/explain integration. | +| Sprint 25 | Exceptions v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXC-25-001 | Ship exception CRUD + workflow API endpoints. | +| Sprint 25 | Exceptions v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXC-25-002 | Extend policy endpoints to include exception metadata. | +| Sprint 25 | Exceptions v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXC-25-003 | Emit exception events/notifications with rate limits. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-001 | Document reachability concepts and scoring. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-002 | Document callgraph formats. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-003 | Document runtime facts ingestion. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-004 | Document policy weighting for signals. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-005 | Document UI overlays/timelines. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-006 | Document CLI reachability commands. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-007 | Publish API docs for signals endpoints. | +| Sprint 26 | Reachability v1 | docs/TASKS.md | TODO | Docs Guild | DOCS-SIG-26-008 | Write migration guide for enabling reachability. | +| Sprint 26 | Reachability v1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-SIG-26-001 | Provision pipelines/deployments for Signals service. | +| Sprint 26 | Reachability v1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-SIG-26-002 | Add dashboards/alerts for reachability metrics. | +| Sprint 26 | Reachability v1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-SIG-26-001 | Add signals scopes/roles + AOC requirements. | +| Sprint 26 | Reachability v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SIG-26-001 | Implement reachability CLI commands (upload/list/explain). | +| Sprint 26 | Reachability v1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SIG-26-002 | Add reachability overrides to policy simulate. | +| Sprint 26 | Reachability v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-SIG-26-001 | Expose advisory symbol metadata for signals scoring. | +| Sprint 26 | Reachability v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-SIG-26-001 | Surface vendor exploitability hints to Signals. | +| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-001 | Integrate reachability inputs into policy evaluation and explainers. | +| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-002 | Optimize reachability fact retrieval + cache. | +| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-003 | Update SPL compiler for reachability predicates. | +| Sprint 26 | Reachability v1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-80-004 | Emit reachability metrics/traces. | +| Sprint 26 | Reachability v1 | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-SPL-24-001 | Extend SPL schema with reachability predicates/actions. | +| Sprint 26 | Reachability v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-26-201 | Implement reachability joiner worker. | +| Sprint 26 | Reachability v1 | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-26-202 | Implement staleness monitor + notifications. | +| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild, Authority Guild | SIGNALS-24-001 | Stand up Signals API skeleton with RBAC + health checks. Host scaffold ready, waiting on `AUTH-SIG-26-001` to finalize scope issuance and tenant enforcement. | +| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-002 | Implement callgraph ingestion/normalization pipeline. Waiting on SIGNALS-24-001 skeleton deployment. | +| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-003 | Ingest runtime facts and persist context data with AOC provenance. Depends on SIGNALS-24-001 base host. | +| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-004 | Deliver reachability scoring engine writing reachability facts. Blocked until ingestion pipelines unblock. | +| Sprint 26 | Reachability v1 | src/Signals/StellaOps.Signals/TASKS.md | BLOCKED (2025-10-27) | Signals Guild | SIGNALS-24-005 | Implement caches + signals events. Downstream of SIGNALS-24-004. | +| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-001 | Add reachability columns/badges to Vulnerability Explorer. | +| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-002 | Enhance Why drawer with call path/timeline. | +| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-003 | Add reachability overlay/time slider to SBOM Graph. | +| Sprint 26 | Reachability v1 | src/UI/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SIG-26-004 | Build Reachability Center + missing sensor view. | +| Sprint 26 | Reachability v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-SIG-26-001 | Expose signals proxy endpoints with pagination and RBAC. | +| Sprint 26 | Reachability v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-SIG-26-002 | Join reachability data into policy/vuln responses. | +| Sprint 26 | Reachability v1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-SIG-26-003 | Support reachability overrides in simulate APIs. | +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Guilds | DOCS-POLICY-27-001 | Publish `/docs/policy/studio-overview.md` with lifecycle + roles. | +> Blocked by `REGISTRY-API-27-001` and `POLICY-ENGINE-27-001`; revisit once spec and compile enrichments land. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Console Guilds | DOCS-POLICY-27-002 | Write `/docs/policy/authoring.md` with templates/snippets/lint rules. | +> Blocked by `CONSOLE-STUDIO-27-001` pending; waiting on Studio authoring UX. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Registry Guilds | DOCS-POLICY-27-003 | Document `/docs/policy/versioning-and-publishing.md`. | +> Blocked by `REGISTRY-API-27-007` pending publish/sign pipeline. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Scheduler Guilds | DOCS-POLICY-27-004 | Publish `/docs/policy/simulation.md` with quick vs batch guidance. | +> Blocked by `REGISTRY-API-27-005`/`SCHED-WORKER-27-301` pending batch simulation. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Product Ops | DOCS-POLICY-27-005 | Author `/docs/policy/review-and-approval.md`. | +> Blocked by `REGISTRY-API-27-006` review workflow outstanding. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Guilds | DOCS-POLICY-27-006 | Publish `/docs/policy/promotion.md` covering canary + rollback. | +> Blocked by `REGISTRY-API-27-008` promotion APIs not ready. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & DevEx/CLI Guilds | DOCS-POLICY-27-007 | Update `/docs/policy/cli.md` with new commands + JSON schemas. | +> Blocked by `CLI-POLICY-27-001..004` CLI commands missing. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Registry Guilds | DOCS-POLICY-27-008 | Publish `/docs/policy/api.md` aligning with Registry OpenAPI. | +> Blocked by Registry OpenAPI (`REGISTRY-API-27-001..008`) incomplete. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Security Guilds | DOCS-POLICY-27-009 | Create `/docs/security/policy-attestations.md`. | +> Blocked by `AUTH-POLICY-27-002` signing integration pending. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Architecture Guilds | DOCS-POLICY-27-010 | Write `/docs/architecture/policy-registry.md`. | +> Blocked by `REGISTRY-API-27-001` & `SCHED-WORKER-27-301` not delivered. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Observability Guilds | DOCS-POLICY-27-011 | Publish `/docs/observability/policy-telemetry.md`. | +> Blocked by `DEVOPS-POLICY-27-004` observability work outstanding. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Ops Guilds | DOCS-POLICY-27-012 | Write `/docs/runbooks/policy-incident.md`. | +> Blocked by `DEPLOY-POLICY-27-002` ops playbooks pending. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Guilds | DOCS-POLICY-27-013 | Update `/docs/examples/policy-templates.md`. | +> Blocked by `CONSOLE-STUDIO-27-001`/`REGISTRY-API-27-002` templates missing. +| Sprint 27 | Policy Studio | docs/TASKS.md | BLOCKED (2025-10-27) | Docs & Policy Registry Guilds | DOCS-POLICY-27-014 | Refresh `/docs/aoc/aoc-guardrails.md` with Studio guardrails. | +> Blocked by `REGISTRY-API-27-003` & `WEB-POLICY-27-001` guardrails not implemented. +| Sprint 27 | Policy Studio | ops/deployment/TASKS.md | TODO | Deployment & Policy Registry Guilds | DEPLOY-POLICY-27-001 | Create Helm/Compose overlays for Policy Registry + workers with signing config. | +| Sprint 27 | Policy Studio | ops/deployment/TASKS.md | TODO | Deployment & Policy Guilds | DEPLOY-POLICY-27-002 | Document policy rollout/rollback playbooks in runbook. | +| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-POLICY-27-001 | Add CI stage for policy lint/compile/test + secret scanning and artifacts. | +| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps & Policy Registry Guilds | DEVOPS-POLICY-27-002 | Provide optional batch simulation CI job with drift gating + PR comment. | +| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps & Security Guilds | DEVOPS-POLICY-27-003 | Manage signing keys + attestation verification in pipelines. | +| Sprint 27 | Policy Studio | ops/devops/TASKS.md | TODO | DevOps & Observability Guilds | DEVOPS-POLICY-27-004 | Build dashboards/alerts for compile latency, queue depth, approvals, promotions. | +| Sprint 27 | Policy Studio | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core Guild | AUTH-POLICY-27-001 | Define Policy Studio roles/scopes for author/review/approve/operate/audit. | +| Sprint 27 | Policy Studio | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guilds | AUTH-POLICY-27-002 | Wire signing service + fresh-auth enforcement for publish/promote. | +| Sprint 27 | Policy Studio | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-POLICY-27-003 | Update authority configuration/docs for Policy Studio roles & signing. | +| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-001 | Implement policy workspace CLI commands (init, lint, compile, test). | +| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-002 | Add version bump, submit, review/approve CLI workflow commands. | +| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-003 | Extend simulate command for quick/batch runs, manifests, CI reports. | +| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-POLICY-27-004 | Implement publish/promote/rollback/sign CLI lifecycle commands. | +| Sprint 27 | Policy Studio | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI & Docs Guilds | CLI-POLICY-27-005 | Update CLI docs/reference for Policy Studio commands and schemas. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-001 | Return rule coverage, symbol table, docs, hashes from compile endpoint. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-002 | Enhance simulate outputs with heatmap, explain traces, delta summaries. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-003 | Enforce complexity/time limits with diagnostics. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-27-004 | Update tests/fixtures for coverage, symbol table, explain, complexity. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-001 | Define Policy Registry OpenAPI spec for workspaces, versions, reviews, simulations, promotions, attestations. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-002 | Implement workspace storage + CRUD with tenant retention policies. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-003 | Integrate compile pipeline storing diagnostics, symbol tables, complexity metrics. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-004 | Deliver quick simulation API with limits and deterministic outputs. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & Scheduler Guilds | REGISTRY-API-27-005 | Build batch simulation orchestration, reduction, and evidence bundle storage. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-006 | Implement review workflow with comments, required approvers, webhooks. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & Security Guilds | REGISTRY-API-27-007 | Ship publish/sign pipeline with attestations, immutable versions. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry Guild | REGISTRY-API-27-008 | Implement promotion/canary bindings per tenant/environment with rollback. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & Observability Guilds | REGISTRY-API-27-009 | Instrument metrics/logs/traces for compile, simulation, approval latency. | +| Sprint 27 | Policy Studio | src/Policy/StellaOps.Policy.Registry/TASKS.md | TODO | Policy Registry & QA Guilds | REGISTRY-API-27-010 | Build unit/integration/load test suites and seeded fixtures. | +| Sprint 27 | Policy Studio | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-CONSOLE-27-001 | Provide policy simulation orchestration endpoints with SSE + RBAC. | +| Sprint 27 | Policy Studio | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService & Observability Guilds | SCHED-CONSOLE-27-002 | Emit policy simulation telemetry endpoints/metrics + webhooks. | +| Sprint 27 | Policy Studio | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-27-301 | Implement batch simulation worker sharding SBOMs with retries/backoff. | +| Sprint 27 | Policy Studio | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-27-302 | Build reducer job aggregating shard outputs into manifests with checksums. | +| Sprint 27 | Policy Studio | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker & Security Guilds | SCHED-WORKER-27-303 | Enforce tenant isolation/attestation integration and secret scanning for jobs. | +| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-27-001 | Proxy Policy Registry APIs with tenant scoping, RBAC, evidence streaming. | +| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-POLICY-27-002 | Implement review lifecycle routes with audit logs and webhooks. | +| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Scheduler Guilds | WEB-POLICY-27-003 | Expose quick/batch simulation endpoints with SSE progress + manifests. | +| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Security Guilds | WEB-POLICY-27-004 | Add publish/promote/rollback endpoints with canary + signing enforcement. | +| Sprint 27 | Policy Studio | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Observability Guilds | WEB-POLICY-27-005 | Instrument Policy Studio metrics/logs for dashboards. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & SBOM Guilds | DOCS-GRAPH-28-001 | Publish `/docs/sbom/graph-explorer-overview.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Console Guilds | DOCS-GRAPH-28-002 | Write `/docs/sbom/graph-using-the-console.md` with walkthrough + accessibility tips. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Graph API Guilds | DOCS-GRAPH-28-003 | Document `/docs/sbom/graph-query-language.md` (JSON schema, cost rules). | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Graph API Guilds | DOCS-GRAPH-28-004 | Publish `/docs/sbom/graph-api.md` endpoints + streaming guidance. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & CLI Guilds | DOCS-GRAPH-28-005 | Produce `/docs/sbom/graph-cli.md` command reference. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Policy Guilds | DOCS-GRAPH-28-006 | Publish `/docs/policy/graph-overlays.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Excitor Guilds | DOCS-GRAPH-28-007 | Document `/docs/vex/graph-integration.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Concelier Guilds | DOCS-GRAPH-28-008 | Document `/docs/advisories/graph-integration.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Architecture Guilds | DOCS-GRAPH-28-009 | Author `/docs/architecture/graph-services.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Observability Guilds | DOCS-GRAPH-28-010 | Publish `/docs/observability/graph-telemetry.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Ops Guilds | DOCS-GRAPH-28-011 | Write `/docs/runbooks/graph-incidents.md`. | +| Sprint 28 | Graph Explorer | docs/TASKS.md | TODO | Docs & Security Guilds | DOCS-GRAPH-28-012 | Create `/docs/security/graph-rbac.md`. | +| Sprint 28 | Graph Explorer | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-GRAPH-28-001 | Provide deployment/offline instructions for Graph Indexer/API, including cache seeds. | +| Sprint 28 | Graph Explorer | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-GRAPH-28-001 | Configure load/perf tests, query budget alerts, and CI smoke for graph APIs. | +| Sprint 28 | Graph Explorer | ops/devops/TASKS.md | TODO | DevOps & Security Guilds | DEVOPS-GRAPH-28-002 | Implement caching/backpressure limits, rate limiting configs, and runaway query kill switches. | +| Sprint 28 | Graph Explorer | ops/devops/TASKS.md | TODO | DevOps & Observability Guilds | DEVOPS-GRAPH-28-003 | Build dashboards/alerts for tile latency, query denials, memory pressure. | +| Sprint 28 | Graph Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-GRAPH-28-001 | Ship `stella sbom graph` subcommands (search, query, paths, diff, impacted, export) with JSON output + exit codes. | +| Sprint 28 | Graph Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-GRAPH-28-002 | Add saved query management + deep link helpers to CLI. | +| Sprint 28 | Graph Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-GRAPH-28-003 | Update CLI docs/examples for Graph Explorer commands. | +| Sprint 28 | Graph Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-GRAPH-24-101 | Deliver advisory summary API feeding graph tooltips. | +| Sprint 28 | Graph Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-GRAPH-28-102 | Add batch fetch for advisory observations/linksets keyed by component sets to feed Graph overlay tooltips efficiently. | +| Sprint 28 | Graph Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | WEB-LNM-21-001 | Provide advisory observation endpoints optimized for graph overlays. | +| Sprint 28 | Graph Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-GRAPH-24-101 | Provide VEX summary API for Graph Explorer inspector overlays. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-001 | Publish Graph API OpenAPI + JSON schemas for queries/tiles. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-002 | Implement `/graph/search` with caching and RBAC. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-003 | Build query planner + streaming tile pipeline with budgets. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-004 | Deliver `/graph/paths` with depth limits and policy overlay support. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-005 | Implement `/graph/diff` streaming adds/removes/changes for SBOM snapshots. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-006 | Compose advisory/VEX/policy overlays with caching + explain sampling. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API Guild | GRAPH-API-28-007 | Provide export jobs (GraphML/CSV/NDJSON/PNG/SVG) with manifests. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & Authority Guilds | GRAPH-API-28-008 | Enforce RBAC scopes, tenant headers, audit logging, rate limits. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & Observability Guilds | GRAPH-API-28-009 | Instrument metrics/logs/traces; publish dashboards. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & QA Guilds | GRAPH-API-28-010 | Build unit/integration/load tests with synthetic datasets. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Api/TASKS.md | TODO | Graph API & DevOps Guilds | GRAPH-API-28-011 | Ship deployment/offline manifests + gateway integration docs. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-001 | Define node/edge schemas, identity rules, and fixtures for graph ingestion. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-002 | Implement SBOM ingest consumer generating artifact/package/file nodes & edges. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-003 | Serve advisory overlay tiles from Conseiller linksets (no mutation of raw node/edge stores). | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-004 | Integrate VEX statements for `vex_exempts` edges with precedence metadata. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & Policy Guilds | GRAPH-INDEX-28-005 | Hydrate policy overlay nodes/edges referencing determinations + explains. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-006 | Produce graph snapshots per SBOM with lineage for diff jobs. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & Observability Guilds | GRAPH-INDEX-28-007 | Run clustering/centrality background jobs and persist cluster ids. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer Guild | GRAPH-INDEX-28-008 | Build incremental/backfill pipeline with change streams, retries, backlog metrics. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & QA Guilds | GRAPH-INDEX-28-009 | Extend tests/perf fixtures ensuring determinism on large graphs. | +| Sprint 28 | Graph Explorer | src/Graph/StellaOps.Graph.Indexer/TASKS.md | TODO | Graph Indexer & DevOps Guilds | GRAPH-INDEX-28-010 | Provide deployment/offline artifacts and docs for Graph Indexer. | +| Sprint 28 | Graph Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-30-001 | Finalize graph overlay contract + projection API. | +| Sprint 28 | Graph Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-30-002 | Implement simulation overlay bridge for Graph Explorer queries. | +| Sprint 28 | Graph Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy & Scheduler Guilds | POLICY-ENGINE-30-003 | Emit change events for effective findings supporting graph overlays. | +| Sprint 28 | Graph Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | DOING (2025-10-26) | Scheduler WebService Guild, Scheduler Storage Guild | SCHED-WEB-21-004 | Persist graph jobs + emit completion events/webhook. | +| Sprint 28 | Graph Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-21-201 | Run graph build worker for SBOM snapshots with retries/backoff. | +| Sprint 28 | Graph Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-21-202 | Execute overlay refresh worker subscribing to change events. | +| Sprint 28 | Graph Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker & Observability Guilds | SCHED-WORKER-21-203 | Emit metrics/logs for graph build/overlay jobs. | +| Sprint 28 | Graph Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-GRAPH-24-001 | Route `/graph/*` APIs through gateway with tenant scoping and RBAC. | +| Sprint 28 | Graph Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-GRAPH-24-002 | Maintain overlay proxy routes to dedicated services (Policy/Vuln API), ensuring caching + RBAC only. | +| Sprint 28 | Graph Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Observability Guilds | WEB-GRAPH-24-004 | Add Graph Explorer telemetry endpoints and metrics aggregation. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs Guild | DOCS-VULN-29-001 | Publish `/docs/vuln/explorer-overview.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Console Guilds | DOCS-VULN-29-002 | Write `/docs/vuln/explorer-using-console.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs Guild | DOCS-VULN-29-003 | Author `/docs/vuln/explorer-api.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs Guild | DOCS-VULN-29-004 | Publish `/docs/vuln/explorer-cli.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Ledger Guilds | DOCS-VULN-29-005 | Document Findings Ledger (`/docs/vuln/findings-ledger.md`). | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Policy Guilds | DOCS-VULN-29-006 | Update `/docs/policy/vuln-determinations.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Excititor Guilds | DOCS-VULN-29-007 | Publish `/docs/vex/explorer-integration.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Concelier Guilds | DOCS-VULN-29-008 | Publish `/docs/advisories/explorer-integration.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & SBOM Guilds | DOCS-VULN-29-009 | Publish `/docs/sbom/vuln-resolution.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Observability Guilds | DOCS-VULN-29-010 | Publish `/docs/observability/vuln-telemetry.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Security Guilds | DOCS-VULN-29-011 | Publish `/docs/security/vuln-rbac.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Ops Guilds | DOCS-VULN-29-012 | Publish `/docs/runbooks/vuln-ops.md`. | +| Sprint 29 | Vulnerability Explorer | docs/TASKS.md | TODO | Docs & Deployment Guilds | DOCS-VULN-29-013 | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API. | +| Sprint 29 | Vulnerability Explorer | ops/deployment/TASKS.md | TODO | Deployment & Findings Ledger Guilds | DEPLOY-VULN-29-001 | Provide deployments for Findings Ledger/projector with migrations/backups. | +| Sprint 29 | Vulnerability Explorer | ops/deployment/TASKS.md | TODO | Deployment & Vuln Explorer API Guilds | DEPLOY-VULN-29-002 | Package Vuln Explorer API deployments/health checks/offline kit notes. | +| Sprint 29 | Vulnerability Explorer | ops/devops/TASKS.md | TODO | DevOps & Findings Ledger Guilds | DEVOPS-VULN-29-001 | Set up CI/backups/anchoring monitoring for Findings Ledger. | +| Sprint 29 | Vulnerability Explorer | ops/devops/TASKS.md | TODO | DevOps & Vuln Explorer API Guilds | DEVOPS-VULN-29-002 | Configure Vuln Explorer perf tests, budgets, dashboards, alerts. | +| Sprint 29 | Vulnerability Explorer | ops/devops/TASKS.md | TODO | DevOps & Console Guilds | DEVOPS-VULN-29-003 | Integrate Vuln Explorer telemetry pipeline with privacy safeguards + dashboards. | +| Sprint 29 | Vulnerability Explorer | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Vuln Explorer RBAC/ABAC scopes and issuer metadata. | +| Sprint 29 | Vulnerability Explorer | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-VULN-29-002 | Enforce CSRF, attachment signing, and audit logging referencing ledger hashes. | +| Sprint 29 | Vulnerability Explorer | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Docs Guild | AUTH-VULN-29-003 | Update docs/config samples for Vuln Explorer roles and security posture. | +| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-001 | Implement `stella vuln list` with grouping, filters, JSON/CSV output. | +| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-002 | Implement `stella vuln show` with evidence/policy/path display. | +| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-003 | Add workflow CLI commands (assign/comment/accept-risk/verify-fix/target-fix/reopen). | +| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-004 | Implement `stella vuln simulate` producing diff summaries/Markdown. | +| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-VULN-29-005 | Implement `stella vuln export` and bundle signature verification. | +| Sprint 29 | Vulnerability Explorer | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI & Docs Guilds | CLI-VULN-29-006 | Update CLI docs/examples for Vulnerability Explorer commands. | +| Sprint 29 | Vulnerability Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-VULN-29-001 | Canonicalize (lossless) advisory identifiers, persist `links[]`, backfill, and expose raw payload snapshots (no merge/derived fields). | +| Sprint 29 | Vulnerability Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-VULN-29-002 | Provide advisory evidence retrieval endpoint for Vuln Explorer. | +| Sprint 29 | Vulnerability Explorer | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService & Observability Guilds | CONCELIER-VULN-29-004 | Add metrics/logs/events for advisory normalization supporting resolver. | +| Sprint 29 | Vulnerability Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001 | Canonicalize (lossless) VEX keys and product scopes with backfill + links (no merge/suppression). | +| Sprint 29 | Vulnerability Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-002 | Expose VEX evidence retrieval endpoint for Explorer evidence tabs. | +| Sprint 29 | Vulnerability Explorer | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService & Observability Guilds | EXCITITOR-VULN-29-004 | Instrument metrics/logs for VEX normalization and suppression events. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-29-001 | Design ledger & projection schemas, hashing strategy, and migrations for Findings Ledger. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-29-002 | Implement ledger write API with hash chaining and Merkle root anchoring job. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Scheduler Guilds | LEDGER-29-003 | Build projector worker deriving `findings_projection` with idempotent replay. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Policy Guilds | LEDGER-29-004 | Integrate Policy Engine batch evaluation into projector with rationale caching. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-29-005 | Implement workflow mutation endpoints producing ledger events (assign/comment/accept-risk/etc.). | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Security Guilds | LEDGER-29-006 | Add attachment encryption, signed URLs, and CSRF protections for workflow endpoints. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & Observability Guilds | LEDGER-29-007 | Instrument ledger metrics/logs/alerts (write latency, projection lag, anchoring). | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & QA Guilds | LEDGER-29-008 | Provide replay/determinism/load tests for ledger/projector pipelines. | +| Sprint 29 | Vulnerability Explorer | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger & DevOps Guilds | LEDGER-29-009 | Deliver deployment/offline artefacts, backup/restore, Merkle anchoring guidance. | +| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-29-001 | Implement policy batch evaluation endpoint returning determinations + rationale. | +| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-29-002 | Provide simulation diff API for Vuln Explorer comparisons. | +| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-29-003 | Include path/scope annotations in determinations for Explorer. | +| Sprint 29 | Vulnerability Explorer | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild & Observability Guild | POLICY-ENGINE-29-004 | Add telemetry for batch evaluation + simulation jobs. | +| Sprint 29 | Vulnerability Explorer | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-VULN-29-001 | Emit inventory evidence with scope/runtime/path/safe version hints; publish change events. | +| Sprint 29 | Vulnerability Explorer | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service & Findings Ledger Guilds | SBOM-VULN-29-002 | Provide resolver feed for candidate generation with idempotent delivery. | +| Sprint 29 | Vulnerability Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-VULN-29-001 | Expose resolver job APIs + status monitoring for Vuln Explorer recomputation. | +| Sprint 29 | Vulnerability Explorer | src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService & Observability Guilds | SCHED-VULN-29-002 | Provide projector lag metrics endpoint + webhook notifications. | +| Sprint 29 | Vulnerability Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-29-001 | Implement resolver worker applying ecosystem version semantics and path scope. | +| Sprint 29 | Vulnerability Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-29-002 | Implement evaluation worker invoking Policy Engine and updating ledger queues. | +| Sprint 29 | Vulnerability Explorer | src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker & Observability Guilds | SCHED-WORKER-29-003 | Add monitoring for resolver/evaluation backlog and SLA alerts. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-001 | Publish Vuln Explorer OpenAPI + query schemas. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-002 | Implement list/query endpoints with grouping, paging, cost budgets. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-003 | Implement detail endpoint combining evidence, policy rationale, paths, history. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Findings Ledger Guilds | VULN-API-29-004 | Expose workflow APIs writing ledger events with validation + idempotency. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Policy Guilds | VULN-API-29-005 | Implement policy simulation endpoint producing diffs without side effects. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-006 | Integrate Graph Explorer paths metadata and deep-link parameters. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Security Guilds | VULN-API-29-007 | Enforce RBAC/ABAC, CSRF, attachment security, and audit logging. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API Guild | VULN-API-29-008 | Provide evidence bundle export job with signing + manifests. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & Observability Guilds | VULN-API-29-009 | Instrument API telemetry (latency, workflow counts, exports). | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & QA Guilds | VULN-API-29-010 | Deliver unit/integration/perf/determinism tests for Vuln Explorer API. | +| Sprint 29 | Vulnerability Explorer | src/VulnExplorer/StellaOps.VulnExplorer.Api/TASKS.md | TODO | Vuln Explorer API & DevOps Guilds | VULN-API-29-011 | Ship deployment/offline manifests, health checks, scaling docs. | +| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-VULN-29-001 | Route `/vuln/*` APIs with tenant RBAC, ABAC, anti-forgery enforcement. | +| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-VULN-29-002 | Proxy workflow calls to Findings Ledger with correlation IDs + retries. | +| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-VULN-29-003 | Expose simulation/export orchestration with SSE/progress + signed links. | +| Sprint 29 | Vulnerability Explorer | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform & Observability Guilds | WEB-VULN-29-004 | Aggregate Vuln Explorer telemetry (latency, errors, exports). | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-001 | Publish `/docs/vex/consensus-overview.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-002 | Write `/docs/vex/consensus-algorithm.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-003 | Document `/docs/vex/issuer-directory.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-004 | Publish `/docs/vex/consensus-api.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-005 | Create `/docs/vex/consensus-console.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-006 | Add `/docs/policy/vex-trust-model.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-007 | Author `/docs/sbom/vex-mapping.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-008 | Publish `/docs/security/vex-signatures.md`. | +| Sprint 30 | VEX Lens | docs/TASKS.md | TODO | Docs Guild | DOCS-VEX-30-009 | Write `/docs/runbooks/vex-ops.md`. | +| Sprint 30 | VEX Lens | ops/devops/TASKS.md | TODO | DevOps Guild | VEXLENS-30-009, ISSUER-30-005 | Set up CI/perf/telemetry dashboards for VEX Lens and Issuer Directory. | +| Sprint 30 | VEX Lens | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | VEXLENS-30-007 | Implement `stella vex consensus` CLI commands with list/show/simulate/export. | +| Sprint 30 | VEX Lens | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild, VEX Lens Guild | CONCELIER-VEXLENS-30-001 | Guarantee advisory key consistency and provide cross-links for consensus rationale (VEX Lens). | +| Sprint 30 | VEX Lens | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001 | Ensure VEX evidence includes issuer hints, signatures, product trees for Lens consumption. | +| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory Guild | ISSUER-30-001 | Implement issuer CRUD API with RBAC and audit logs. | +| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & Security Guilds | ISSUER-30-002 | Implement key management endpoints with expiry enforcement. | +| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & Policy Guilds | ISSUER-30-003 | Provide trust weight override APIs with audit trails. | +| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & VEX Lens Guilds | ISSUER-30-004 | Integrate issuer data into signature verification clients. | +| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & Observability Guilds | ISSUER-30-005 | Instrument issuer change metrics/logs and dashboards. | +| Sprint 30 | VEX Lens | src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md | TODO | Issuer Directory & DevOps Guilds | ISSUER-30-006 | Provide deployment/backup/offline docs for Issuer Directory. | +| Sprint 30 | VEX Lens | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-30-101 | Surface trust weighting configuration (issuer weights, modifiers, decay) for VEX Lens via Policy Studio/API. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-001 | Implement VEX normalization pipeline (CSAF, OpenVEX, CycloneDX) with deterministic outputs. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-002 | Build product mapping library aligning CSAF product trees to purls/versions with scope scoring. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Issuer Directory Guilds | VEXLENS-30-003 | Integrate signature verification using issuer keys; annotate evidence. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Policy Guilds | VEXLENS-30-004 | Implement trust weighting functions configurable via policy. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-005 | Implement consensus algorithm producing state, confidence, rationale, and quorum. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Findings Ledger Guilds | VEXLENS-30-006 | Materialize consensus projections and change events. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-30-007 | Deliver query/detail/simulation/export APIs with budgets and OpenAPI docs. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Policy Guilds | VEXLENS-30-008 | Integrate consensus signals with Policy Engine and Vuln Explorer. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & Observability Guilds | VEXLENS-30-009 | Instrument metrics/logs/traces; publish dashboards/alerts. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & QA Guilds | VEXLENS-30-010 | Build unit/property/integration/load tests and determinism harness. | +| Sprint 30 | VEX Lens | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens & DevOps Guilds | VEXLENS-30-011 | Provide deployment manifests, scaling guides, offline seeds, runbooks. | +| Sprint 30 | VEX Lens | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild, VEX Lens Guild | WEB-VEX-30-007 | Route `/vex/consensus` APIs via gateway with RBAC/ABAC, caching, and telemetry (proxy-only). | +| Sprint 31 | Advisory AI | docs/TASKS.md | TODO | Docs Guild | DOCS-AIAI-31-001 | Publish Advisory AI overview doc. | +| Sprint 31 | Advisory AI | docs/TASKS.md | TODO | Docs Guild | DOCS-AIAI-31-002 | Publish architecture doc for Advisory AI. | +| Sprint 31 | Advisory AI | docs/TASKS.md | TODO | Docs Guild | DOCS-AIAI-31-003..009 | Complete API/Console/CLI/Policy/Security/SBOM/Runbook docs. | +| Sprint 31 | Advisory AI | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-AIAI-31-001 | Provide Advisory AI deployment/offline guidance. | +| Sprint 31 | Advisory AI | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIAI-31-001 | Provision CI/perf/telemetry for Advisory AI. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-001 | Implement advisory/VEX retrievers with paragraph anchors and citations. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-002 | Build SBOM context retriever and blast radius estimator. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-003 | Deliver deterministic toolset (version checks, dependency analysis, policy lookup). | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-004 | Orchestrator with task templates, tool chaining, caching. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & Security Guilds | AIAI-31-005 | Guardrails (redaction, injection defense, output validation). | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI Guild | AIAI-31-006 | Expose REST/batch APIs with RBAC and OpenAPI. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & Observability Guilds | AIAI-31-007 | Instrument metrics/logs/traces and dashboards. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & DevOps Guilds | AIAI-31-008 | Package inference + deployment manifests/flags. | +| Sprint 31 | Advisory AI | src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md | TODO | Advisory AI & QA Guilds | AIAI-31-009 | Build golden/injection/perf tests ensuring determinism. | +| Sprint 31 | Advisory AI | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-AIAI-31-001 | Define Advisory AI scopes and remote inference toggles. | +| Sprint 31 | Advisory AI | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-AIAI-31-002 | Enforce prompt logging and consent/audit flows. | +| Sprint 31 | Advisory AI | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIAI-31-001 | Implement `stella advise *` CLI commands leveraging Advisory AI orchestration and policy scopes. | +| Sprint 31 | Advisory AI | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-AIAI-31-001 | Expose advisory chunk API with paragraph anchors. | +| Sprint 31 | Advisory AI | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-AIAI-31-001 | Provide VEX chunks with justifications and signatures. | +| Sprint 31 | Advisory AI | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-31-001 | Provide policy knobs for Advisory AI. | +| Sprint 31 | Advisory AI | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-AIAI-31-001 | Deliver SBOM path/timeline endpoints for Advisory AI. | +| Sprint 31 | Advisory AI | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-AIAI-31-001 | Expose enriched rationale API for conflict explanations. | +| Sprint 31 | Advisory AI | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-AIAI-31-002 | Provide batching/caching hooks for Advisory AI. | +| Sprint 31 | Advisory AI | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AIAI-31-001 | Route `/advisory/ai/*` APIs with RBAC/telemetry. | +| Sprint 31 | Advisory AI | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AIAI-31-002 | Provide batch orchestration and retry handling for Advisory AI. | +| Sprint 31 | Advisory AI | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-AIAI-31-003 | Emit Advisory AI gateway telemetry/audit logs. | +| Sprint 32 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-32-001 | Author `/docs/orchestrator/overview.md` covering mission, roles, AOC alignment, and imposed rule reminder. | +| Sprint 32 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-32-002 | Author `/docs/orchestrator/architecture.md` detailing scheduler, DAGs, rate limits, and data model. | +| Sprint 32 | Orchestrator Dashboard | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ORCH-32-001 | Provision staging Postgres/message-bus charts, CI smoke deploy, and baseline dashboards for queue depth and inflight jobs. | +| Sprint 32 | Orchestrator Dashboard | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-ORCH-32-001 | Introduce `orch:read` scope and `Orch.Viewer` role with metadata, discovery docs, and offline defaults. | +| Sprint 32 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-32-001 | Register Concelier sources with orchestrator, publish schedules/rate policies, and seed metadata. | +| Sprint 32 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-32-002 | Embed worker SDK into Concelier ingestion loops emitting progress, heartbeats, and artifact hashes. | +| Sprint 32 | Orchestrator Dashboard | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-ORCH-32-001 | Adopt worker SDK in Excititor worker with job claim/heartbeat and artifact summary emission. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-32-001 | Bootstrap Go worker SDK (client config, job claim, acknowledgement flow) with integration tests. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-32-002 | Add heartbeat/progress helpers, structured logging, and default metrics exporters to Go SDK. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-32-001 | Bootstrap Python async SDK with job claim/config adapters and sample worker. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-32-002 | Implement heartbeat/progress helpers and logging/metrics instrumentation for Python workers. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-001 | Bootstrap orchestrator service with Postgres schema/migrations for sources, runs, jobs, dag_edges, artifacts, quotas, schedules. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-002 | Implement scheduler DAG planner, dependency resolver, and job state machine for read-only tracking. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-003 | Expose read-only REST APIs (sources, runs, jobs, DAG) with OpenAPI + validation. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-004 | Ship WebSocket/SSE live update stream and metrics counters/histograms for job lifecycle. | +| Sprint 32 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-32-005 | Deliver worker claim/heartbeat/progress endpoints capturing artifact metadata and checksums. | +| Sprint 32 | Orchestrator Dashboard | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-32-101 | Define orchestrator `policy_eval` job contract, idempotency keys, and enqueue hooks for change events. | +| Sprint 32 | Orchestrator Dashboard | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-ORCH-32-001 | Integrate orchestrator job IDs into SBOM ingest/index pipelines with artifact hashing and status updates. | +| Sprint 32 | Orchestrator Dashboard | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-ORCH-32-001 | Expose read-only orchestrator APIs via gateway with tenant scoping, caching headers, and rate limits. | +| Sprint 33 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-33-001 | Author `/docs/orchestrator/api.md` with endpoints, WebSocket events, error codes, and imposed rule reminder. | +| Sprint 33 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-33-002 | Author `/docs/orchestrator/console.md` covering screens, accessibility, and live updates. | +| Sprint 33 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-33-003 | Author `/docs/orchestrator/cli.md` with command reference, examples, and exit codes. | | Sprint 33 | Governance & Rules | ops/devops/TASKS.md | REVIEW (2025-10-30) | DevOps Guild, Platform Leads | DEVOPS-RULES-33-001 | Contracts & Rules anchor (gateway proxy-only; Policy Engine overlays/simulations; AOC ingestion canonicalization; Graph Indexer + Graph API as sole platform). | -| Sprint 33 | Orchestrator Dashboard | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ORCH-33-001 | Publish Grafana dashboards for rate-limit/backpressure/error clustering and configure alert rules with runbooks. | -| Sprint 33 | Orchestrator Dashboard | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-ORCH-33-001 | Add `Orch.Operator` role, control action scopes, and enforce reason/ticket field capture. | -| Sprint 33 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-33-001 | Wire orchestrator control hooks (pause, throttle, retry) into Concelier workers with safe checkpoints. | -| Sprint 33 | Orchestrator Dashboard | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-ORCH-33-001 | Honor orchestrator throttles, classify VEX errors, and emit retry-safe checkpoints in Excititor worker. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-33-001 | Add artifact upload helpers (object store + checksum) and idempotency guard to Go SDK. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-33-002 | Implement error classification/retry helper and structured failure report in Go SDK. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-33-001 | Add artifact publish/idempotency features to Python SDK with object store integration. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-33-002 | Expose error classification/retry/backoff helpers in Python SDK with structured logging. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-001 | Enable source/job control actions (test, pause/resume, retry/cancel/prioritize) with RBAC and audit hooks. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-002 | Implement adaptive token-bucket rate limiter and concurrency caps reacting to upstream 429/503 signals. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-003 | Add watermark/backfill manager with event-time windows, duplicate suppression, and preview API. | -| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-004 | Deliver dead-letter storage, replay endpoints, and surfaced error classes with remediation hints. | -| Sprint 33 | Orchestrator Dashboard | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-33-101 | Implement orchestrator-driven policy evaluation workers with heartbeats, SLO metrics, and rate limit awareness. | -| Sprint 33 | Orchestrator Dashboard | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-ORCH-33-001 | Report SBOM ingest backpressure metrics and support orchestrator pause/resume/backfill signals. | -| Sprint 33 | Orchestrator Dashboard | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-ORCH-33-001 | Expose `consensus_compute` orchestrator job type and integrate VEX Lens worker for diff batches. | -| Sprint 33 | Orchestrator Dashboard | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-ORCH-33-001 | Add control endpoints (actions/backfill) and SSE bridging with permission checks and error mapping. | -| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-001 | Author `/docs/orchestrator/run-ledger.md` describing provenance export format and audits. | -| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-002 | Author `/docs/security/secrets-handling.md` covering KMS refs, redaction, and operator hygiene. | -| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-003 | Author `/docs/operations/orchestrator-runbook.md` (failures, backfill guide, circuit breakers). | -| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-004 | Author `/docs/schemas/artifacts.md` detailing artifact kinds, schema versions, hashing, storage layout. | -| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-005 | Author `/docs/slo/orchestrator-slo.md` defining SLOs, burn alerts, and measurement strategy. | -| Sprint 34 | Orchestrator Dashboard | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-ORCH-34-001 | Provide Helm/Compose manifests, scaling defaults, and offline kit instructions for orchestrator service. | -| Sprint 34 | Orchestrator Dashboard | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ORCH-34-001 | Harden production dashboards/alerts, synthetic probes, and incident response playbooks for orchestrator. | -| Sprint 34 | Orchestrator Dashboard | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-34-006 | Bundle orchestrator service, worker SDK samples, and Postgres snapshot into Offline Kit with integrity checks. | -| Sprint 34 | Orchestrator Dashboard | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-ORCH-34-001 | Add `Orch.Admin` role for quotas/backfills, enforce audit reason requirements, update docs and offline defaults. | -| Sprint 34 | Orchestrator Dashboard | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-ORCH-34-001 | Implement backfill wizard and quota management commands with dry-run preview and guardrails. | -| Sprint 34 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-34-001 | Implement orchestrator-driven backfills for advisory sources with idempotent artifact reuse and ledger linkage. | -| Sprint 34 | Orchestrator Dashboard | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-ORCH-34-001 | Support orchestrator backfills and circuit breaker resets for Excititor sources with auditing. | -| Sprint 34 | Orchestrator Dashboard | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-34-101 | Link orchestrator run ledger entries into Findings Ledger provenance export and audit queries. | -| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-34-001 | Add backfill range execution, watermark handshake, and artifact dedupe verification to Go SDK. | -| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-34-001 | Add backfill support and deterministic artifact dedupe validation to Python SDK. | -| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-001 | Implement quota management APIs, SLO burn-rate computation, and alert budget tracking. | -| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-002 | Build audit log and immutable run ledger export with signed manifest support. | -| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-003 | Run perf/scale validation (10k jobs, dispatch <150 ms) and add autoscaling hooks. | -| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-004 | Package orchestrator container, Helm overlays, offline bundle seeds, and provenance attestations. | -| Sprint 34 | Orchestrator Dashboard | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-34-101 | Expose policy eval run ledger exports and SLO burn metrics to orchestrator. | -| Sprint 34 | Orchestrator Dashboard | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-ORCH-34-001 | Enable SBOM backfill and watermark reconciliation; emit coverage metrics and flood guard. | -| Sprint 34 | Orchestrator Dashboard | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-ORCH-34-001 | Integrate consensus compute completion events with orchestrator ledger and provenance outputs. | -| Sprint 34 | Orchestrator Dashboard | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-ORCH-34-001 | Expose quotas/backfill/queue metrics endpoints, throttle toggles, and error clustering APIs. | -| Sprint 35 | EPDR Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild | SCANNER-ANALYZERS-LANG-11-001 | Build entrypoint resolver (identity + environment profiles) and emit normalized entrypoint records. | -| Sprint 35 | EPDR Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild | SCANNER-ANALYZERS-LANG-11-002 | Static IL/reflection/ALC heuristics producing dependency edges with reason codes and confidence. | -| Sprint 35 | EPDR Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild, Signals Guild | SCANNER-ANALYZERS-LANG-11-003 | Runtime loader/PInvoke signal ingestion merged with static/declared edges (confidence & explain). | -| Sprint 35 | Export Center Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-35-001 | Author `/docs/modules/export-center/overview.md` with purpose, profiles, security, and imposed rule reminder. | -| Sprint 35 | Export Center Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-35-002 | Author `/docs/modules/export-center/architecture.md` detailing service components, adapters, manifests, signing, and distribution. | -| Sprint 35 | Export Center Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-35-003 | Publish `/docs/modules/export-center/profiles.md` covering schemas, examples, and compatibility. | -| Sprint 35 | Export Center Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-EXPORT-35-001 | Package exporter service/worker containers, Helm overlays (download-only), and rollout guide. | -| Sprint 35 | Export Center Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-EXPORT-35-001 | Create exporter CI pipeline (lint/test/perf smoke), object storage fixtures, and initial Grafana dashboards. | -| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-001 | Bootstrap exporter service, configuration, and migrations for export profiles/runs/inputs/distributions with tenant scopes. | -| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-002 | Implement planner resolving filters to iterators and orchestrator job contract with deterministic sampling. | -| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-003 | Deliver JSON adapters (raw/policy) with canonical normalization, redaction enforcement, and zstd writers. | -| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-004 | Build mirror (full) adapter producing filesystem layout, manifests, and bundle assembly for download profile. | -| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-005 | Implement manifest/provenance writer and KMS signing/attestation for export bundles. | -| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-006 | Expose Export API (profiles, runs, download) with SSE updates, concurrency controls, and audit logging. | -| Sprint 35 | Export Center Phase 1 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-EXPORT-35-001 | Provide paginated streaming endpoints for advisories, VEX, SBOMs, and findings filtered by scope selectors. | -| Sprint 35 | Export Center Phase 1 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-35-101 | Register export job type, quotas, and rate policies; surface export job telemetry for scheduler. | -| Sprint 35 | Export Center Phase 1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-35-201 | Expose deterministic policy snapshot + evaluated findings endpoint aligned with Export Center requirements. | -| Sprint 35 | Export Center Phase 1 | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-EXPORT-35-001 | Publish consensus snapshot API delivering deterministic JSON for export consumption. | -| Sprint 35 | Export Center Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXPORT-35-001 | Route Export Center APIs through gateway with tenant scoping, viewer/operator scopes, and streaming downloads. | -| Sprint 36 | EPDR Observations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild, SBOM Service Guild | SCANNER-ANALYZERS-LANG-11-004 | Normalize EPDR output to Scanner observation writer (entrypoints + edges + env profiles). | -| Sprint 36 | EPDR Observations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild, QA Guild | SCANNER-ANALYZERS-LANG-11-005 | End-to-end fixtures/benchmarks covering publish modes, RIDs, trimming, NativeAOT with explain traces. | -| Sprint 36 | Export Center Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-36-004 | Author `/docs/modules/export-center/api.md` with endpoint examples and imposed rule note. | -| Sprint 36 | Export Center Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-36-005 | Publish `/docs/modules/export-center/cli.md` covering commands, scripts, verification, and imposed rule reminder. | -| Sprint 36 | Export Center Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-36-006 | Write `/docs/modules/export-center/trivy-adapter.md` detailing mappings, compatibility, and test matrix. | -| Sprint 36 | Export Center Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-EXPORT-36-001 | Document registry credentials, OCI push workflows, and automation for export distributions. | -| Sprint 36 | Export Center Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-EXPORT-36-001 | Integrate Trivy compatibility validation, OCI push smoke tests, and metrics dashboards for export throughput. | -| Sprint 36 | Export Center Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXPORT-36-001 | Add `stella export distribute` (OCI/objstore), `run download --resume`, and status polling enhancements. | -| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-001 | Implement Trivy DB adapter (core) with schema mapping, validation, and compatibility gating. | -| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-002 | Add Trivy Java DB variant, shared manifest entries, and adapter regression tests. | -| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-003 | Build OCI distribution engine for exports with descriptor annotations and registry auth handling. | -| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-004 | Extend planner/run lifecycle for OCI/object storage distributions with retry + idempotency. | -| Sprint 36 | Export Center Phase 2 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-36-101 | Add distribution job follow-ups, retention metadata, and metrics for export runs. | -| Sprint 36 | Export Center Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXPORT-36-001 | Expose distribution endpoints (OCI/object storage) and manifest/provenance download proxies with RBAC. | -| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-001 | Publish `/docs/modules/export-center/mirror-bundles.md` detailing layouts, deltas, encryption, imposed rule reminder. | -| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-002 | Publish `/docs/modules/export-center/provenance-and-signing.md` covering manifests, attestation, verification. | -| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-003 | Publish `/docs/operations/export-runbook.md` for failures, tuning, capacity, with imposed rule note. | -| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-004 | Publish `/docs/security/export-hardening.md` covering RBAC, isolation, encryption, and imposed rule. | -| Sprint 37 | Export Center Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-EXPORT-37-001 | Finalize dashboards/alerts for exports (failure, verify), retention jobs, and chaos testing harness. | -| Sprint 37 | Export Center Phase 3 | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-37-001 | Package Export Center mirror bundles + verification tooling into Offline Kit with manifest/signature updates. | -| Sprint 37 | Export Center Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-EXPORT-37-001 | Add `Export.Admin` scope enforcement for retention, encryption keys, and scheduling APIs. | -| Sprint 37 | Export Center Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXPORT-37-001 | Implement `stella export schedule`, `run verify`, and bundle verification tooling with signature/hash checks. | -| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-001 | Implement mirror delta adapter, base export linkage, and content-addressed reuse. | -| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-002 | Add bundle encryption, key wrapping with KMS, and verification tooling for encrypted exports. | -| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-003 | Deliver scheduling/retention engine (cron/event triggers), audit trails, and retry idempotency enhancements. | -| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-004 | Provide export verification API and CLI integration, including hash/signature validation endpoints. | -| Sprint 37 | Export Center Phase 3 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-37-101 | Enable scheduled export runs, retention pruning hooks, and failure alerting integration. | -| Sprint 37 | Export Center Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXPORT-37-001 | Surface scheduling, retention, and verification endpoints plus encryption parameter handling. | -| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-001 | Format detector & binary identity for ELF/PE/Mach-O (multi-slice) with stable entrypoint IDs. | -| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-002 | ELF dynamic parser emitting dtneeded edges, runpath metadata, symbol version needs. | -| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-003 | PE import + delay-load + SxS manifest parsing producing reason-coded edges. | -| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-004 | Mach-O load command parsing with @rpath expansion and slice handling. | -| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-005 | Cross-platform resolver engine modeling search order/explain traces for ELF/PE/Mach-O. | -| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-006 | Heuristic scanner for dlopen/LoadLibrary strings, plugin configs, ecosystem hints with confidence tags. | -| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-007 | Serialize entrypoints/edges/env profiles to Scanner writer (AOC-compliant observations). | -| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild, QA Guild | SCANNER-ANALYZERS-NATIVE-20-008 | Fixture suite + determinism benchmarks for native analyzer across linux/windows/macos. | -| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild, Signals Guild | SCANNER-ANALYZERS-NATIVE-20-009 | Optional runtime capture adapters (eBPF/ETW/dyld) producing runtime-load edges with redaction. | -| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild, DevOps Guild | SCANNER-ANALYZERS-NATIVE-20-010 | Package native analyzer plug-in + Offline Kit updates and restart-time loading. | -| Sprint 38 | Notifications Studio Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-NOTIFY-38-001 | Publish `/docs/notifications/overview.md` and `/docs/notifications/architecture.md` ending with imposed rule statement. | -| Sprint 38 | Notifications Studio Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-NOTIFY-38-001 | Package notifier API/worker Helm overlays (email/chat/webhook), secrets templates, rollout guide. | -| Sprint 38 | Notifications Studio Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NOTIFY-38-001 | Stand up notifier CI pipelines, event bus fixtures, base dashboards for events/notifications latency. | -| Sprint 38 | Notifications Studio Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-NOTIFY-38-001 | Implement `stella notify` rule/template/incident commands (list/create/test/ack) with file-based inputs. | -| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-001 | Bootstrap notifier service, migrations for notif tables, event ingestion, and rule engine foundation (policy violations + job failures). | -| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-002 | Implement channel adapters (email, chat-webhook, generic webhook) with retry and audit logging. | -| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-003 | Deliver template service (versioning, preview), rendering pipeline with redaction, and provenance links. | -| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-004 | Expose initial API (rules CRUD, templates, incidents list, ack) and live feed WS stream. | -| Sprint 38 | Notifications Studio Phase 1 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-38-101 | Standardize event envelope publication (policy/export/job lifecycle) with idempotency keys for notifier ingestion. | -| Sprint 38 | Notifications Studio Phase 1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-38-201 | Emit enriched violation events including rationale IDs via orchestrator bus. | -| Sprint 38 | Notifications Studio Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-NOTIFY-38-001 | Route notifier APIs through gateway with tenant scoping and operator scopes. | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-001 | Java input normalizer (jar/war/ear/fat/jmod/jimage) with MR overlay selection. | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-002 | Module/classpath builder with duplicate & split-package detection. | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-003 | SPI scanner & provider selection with warnings. | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | DONE | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-004 | Reflection/TCCL heuristics emitting reason-coded edges. | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-005 | Framework config extraction (Spring, Jakarta, MicroProfile, logging, Graal configs). | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-006 | JNI/native hint detection for Java artifacts. | -| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-007 | Manifest/signature metadata collector (main/start/agent classes, signers). | -| Sprint 39 | Notifications Studio Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-NOTIFY-39-002 | Publish `/docs/notifications/rules.md`, `/templates.md`, `/digests.md` with imposed rule reminder. | -| Sprint 39 | Notifications Studio Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NOTIFY-39-002 | Add throttling/quiet-hours dashboards, digest job monitoring, and storm breaker alerts. | -| Sprint 39 | Notifications Studio Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-NOTIFY-39-001 | Add simulation/digest CLI verbs and advanced filtering for incidents. | -| Sprint 39 | Notifications Studio Phase 2 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-NOTIFY-39-001 | Optimize digest queries and provide API for notifier to fetch unresolved policy violations/SBOM deltas. | -| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-001 | Implement correlation engine, throttling, quiet hours/maintenance evaluator, and incident state machine. | -| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-002 | Add digests generator with Findings Ledger queries and distribution (email/chat). | -| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-003 | Provide simulation engine and API for rule dry-run against historical events. | -| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-004 | Integrate quiet hours calendars and default throttles with audit logging. | -| Sprint 39 | Notifications Studio Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-NOTIFY-39-001 | Surface digest scheduling, simulation, and throttle management endpoints via gateway. | -| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-008 | Observation writer producing entrypoints/components/edges with warnings. | -| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild, QA Guild | SCANNER-ANALYZERS-JAVA-21-009 | Fixture suite + determinism/perf benchmarks for Java analyzer. | -| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild, Signals Guild | SCANNER-ANALYZERS-JAVA-21-010 | Optional runtime ingestion via agent/JFR producing runtime edges. | -| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild, DevOps Guild | SCANNER-ANALYZERS-JAVA-21-011 | Package Java analyzer plug-in + Offline Kit/CLI updates. | -| Sprint 40 | Notifications Studio Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-NOTIFY-40-001 | Publish `/docs/notifications/channels.md`, `/escalations.md`, `/api.md`, `/operations/notifier-runbook.md`, `/security/notifications-hardening.md` with imposed rule lines. | -| Sprint 40 | Notifications Studio Phase 3 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-NOTIFY-40-001 | Package notifier escalations + localization deployment overlays, signed ack token rotation scripts, and rollback guidance. | -| Sprint 40 | Notifications Studio Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NOTIFY-40-001 | Finalize notifier dashboards/alerts (escalation failures, ack latency), chaos testing harness, and channel health monitoring. | -| Sprint 40 | Notifications Studio Phase 3 | ops/offline-kit/TASKS.md | CARRY (no scope change) | Offline Kit Guild | DEVOPS-OFFLINE-37-002 | Carry from Sprint 37: Notifier offline packs (sample configs, template/digest packs, dry-run harness) with integrity checks. | -| Sprint 40 | Notifications Studio Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Enforce ack token signing/rotation, webhook allowlists, and admin-only escalation settings. | -| Sprint 40 | Notifications Studio Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-NOTIFY-40-001 | Implement ack token redemption, escalation management, localization previews. | -| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-001 | Implement escalations, on-call schedules, ack bridge, PagerDuty/OpsGenie adapters, and localization bundles. | -| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-002 | Add CLI inbox/in-app feed channels and summary storm breaker notifications. | -| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-003 | Harden security: signed ack links, webhook HMAC/IP allowlists, tenant isolation fuzzing, localization fallback. | -| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-004 | Finalize observability (incident metrics, escalation latency) and chaos tests for channel outages. | -| Sprint 40 | Notifications Studio Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-NOTIFY-40-001 | Expose escalation, localization, channel health endpoints and verification of signed links. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-41-001 | Publish `/docs/modules/cli/guides/overview.md`, `/cli/configuration.md`, `/cli/output-and-exit-codes.md` (with imposed rule). | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-CLI-41-001 | Package CLI release artifacts (tarballs, completions, container image) with distribution docs. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CLI-41-001 | Establish CLI build pipeline (multi-platform binaries, SBOM, checksums) and parity matrix CI enforcement. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-PACKS-41-001 | Define CLI SSO scopes and Packs (`Packs.Read/Write/Run/Approve`) roles; update discovery/offline defaults. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-CORE-41-001 | Implement CLI config/auth foundation, global flags, output renderer, and error/exit code mapping. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PARITY-41-001 | Deliver parity command groups (`policy`, `sbom`, `vuln`, `vex`, `advisory`, `export`, `orchestrator`) with JSON/table outputs and `--explain`. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PARITY-41-002 | Implement `notify`, `aoc`, `auth` command groups, idempotency keys, completions, and parity matrix export. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-41-101 | Register `pack-run` job type, integrate logs/artifacts, expose pack run metadata. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/PacksRegistry/StellaOps.PacksRegistry/TASKS.md | TODO | Packs Registry Guild | PACKS-REG-41-001 | Implement packs index API, signature verification, provenance storage, and RBAC. | -| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-41-001 | Bootstrap Task Runner service, migrations, run API, local executor, approvals pause, artifact capture. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-42-001 | Publish `/docs/modules/cli/guides/parity-matrix.md`, `/cli/commands/*.md`, `/docs/task-packs/spec.md` (imposed rule). | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CLI-42-001 | Add CLI golden output tests, parity diff automation, and pack run CI harness. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PACKS-42-001 | Implement Task Pack CLI commands (`pack plan/run/push/pull/verify`) with plan/simulate engine and expression sandbox. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PARITY-41-001..002 | Close parity gaps for Notifications, Policy Studio advanced features, SBOM graph, Vuln Explorer; parity matrix green. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-PACKS-42-001 | Expose snapshot/time-travel APIs for CLI offline mode and pack simulation. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-42-101 | Stream pack run logs via SSE/WS, expose artifact manifests, enforce pack run quotas. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/PacksRegistry/StellaOps.PacksRegistry/TASKS.md | TODO | Packs Registry Guild | PACKS-REG-42-001 | Support pack version lifecycle, tenant allowlists, provenance export, signature rotation. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-42-201 | Provide stable rationale IDs/APIs for CLI `--explain` and pack policy gates. | -| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-42-001 | Add loops, conditionals, `maxParallel`, outputs, simulation mode, policy gates in Task Runner. | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-PACKS-43-001 | Publish `/docs/task-packs/authoring-guide.md`, `/registry.md`, `/runbook.md`, `/security/pack-signing-and-rbac.md`, `/operations/cli-release-and-packaging.md` (imposed rule). | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CLI-43-001 | Finalize multi-platform release automation, SBOM signing, parity gate enforcement, pack run chaos tests. | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-PACKS-41-001 | Enforce pack signing policies, approval RBAC, CLI token scopes for CI headless runs. | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PACKS-42-001 | Deliver advanced pack features (approvals pause/resume, remote streaming, secret injection), localization, man pages. | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-005, PACKS-REG-41-001 | Integrate pack run manifests into export bundles and CLI verify flows. | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/PacksRegistry/StellaOps.PacksRegistry/TASKS.md | TODO | Packs Registry Guild | PACKS-REG-42-001 | Enforce pack signing policies, audit trails, registry mirroring, Offline Kit support. | -| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-42-001 | Implement approvals workflow, notifications integration, remote artifact uploads, chaos resilience. | -| Sprint 44 | Containerized Distribution Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-44-001 | Publish install overview + Compose Quickstart docs (imposed rule). | -| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | COMPOSE-44-001 | Deliver Quickstart Compose stack with seed data and quickstart script. | -| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | COMPOSE-44-002 | Provide backup/reset scripts with guardrails and documentation. | -| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | COMPOSE-44-003 | Implement seed job and onboarding wizard toggle (`QUICKSTART_MODE`). | -| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-COMPOSE-44-001 | Finalize Quickstart scripts and README. | -| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONTAINERS-44-001 | Automate multi-arch builds with SBOM/signature pipeline. | -| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DOCKER-44-001 | Author multi-stage Dockerfiles with non-root users, read-only FS, and health scripts for all services. | -| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DOCKER-44-002 | Generate SBOMs and cosign attestations for each image; integrate signature verification in CI. | -| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DOCKER-44-003 | Ensure `/health/*`, `/version`, `/metrics`, and capability endpoints (`merge=false`) are exposed across services. | -| Sprint 44 | Containerized Distribution Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONTAINERS-44-001 | Expose config discovery and quickstart handling with health/version endpoints. | -| Sprint 45 | Containerized Distribution Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-45-001 | Publish Helm production + configuration reference docs (imposed rule). | -| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-HELM-45-001 | Publish Helm install guide and sample values. | -| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | HELM-45-001 | Scaffold Helm chart with component toggles and pinned digests. | -| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | HELM-45-002 | Add security features (TLS, NetworkPolicy, Secrets integration). | -| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | HELM-45-003 | Implement HPA, PDB, readiness gates, and observability hooks. | -| Sprint 45 | Containerized Distribution Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONTAINERS-45-001 | Add Compose/Helm smoke tests to CI. | -| Sprint 45 | Containerized Distribution Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONTAINERS-45-001 | Ensure readiness endpoints and config toggles support Helm deployments. | -| Sprint 46 | Containerized Distribution Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-46-001 | Publish air-gap, supply chain, health/readiness, image catalog, console onboarding docs (imposed rule). | -| Sprint 46 | Containerized Distribution Phase 3 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-AIRGAP-46-001 | Provide air-gap load script and docs. | -| Sprint 46 | Containerized Distribution Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONTAINERS-46-001 | Build signed air-gap bundle and verify in CI. | -| Sprint 46 | Containerized Distribution Phase 3 | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | OFFLINE-CONTAINERS-46-001 | Include air-gap bundle and instructions in Offline Kit. | -| Sprint 46 | Containerized Distribution Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONTAINERS-46-001 | Harden offline mode and document fallback behavior. | -| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-TEN-47-001 | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` (imposed rule). | -| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-TEN-47-001 | Integrate JWKS caching, signature verification tests, and auth regression suite into CI. | -| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement unified JWT/ODIC config, scope grammar, tenant/project claims, and JWKS caching in Authority. | -| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-TEN-47-001 | Ship `stella login`, `whoami`, `tenants list`, and tenant flag persistence with secure token storage. | -| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-TEN-47-001 | Add auth middleware (token verification, tenant activation, scope checks) and structured 403 responses. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-TEN-48-001 | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md` (imposed rule). | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-TEN-48-001 | Write integration tests for RLS enforcement, tenant audit stream, and object store prefix checks. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-TEN-48-001 | Ensure advisory linkers operate per tenant with RLS, enforce aggregation-only capability endpoint. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-TEN-48-001 | Same as above for VEX linkers; enforce capability endpoint `merge=false`. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-TEN-48-001 | Add tenant prefixes to manifests/artifacts, enforce scope checks, and block cross-tenant exports by default. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-TEN-48-001 | Partition findings by tenant/project, enable RLS, and update queries/events to include tenant context. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-TEN-48-001 | Tenant-scope notification rules, incidents, and outbound channels; update storage schemas. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-TEN-48-001 | Stamp jobs with tenant/project, set DB session context, and reject jobs without context. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-TEN-48-001 | Add `tenant_id`/`project_id` to policy data, enable Postgres RLS, and expose rationale IDs with tenant context. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-TEN-48-001 | Propagate tenant/project to all steps, enforce object store prefix, and validate before execution. | -| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-TEN-48-001 | Enforce tenant context through persistence (DB GUC, object store prefix), add request annotations, and emit audit events. | -| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-TEN-49-001 | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, `/docs/install/configuration-reference.md` updates (imposed rule). | -| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-TEN-49-001 | Implement audit log pipeline, monitor scope usage, chaos tests for JWKS outage, and tenant load/perf tests. | -| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-TEN-49-001 | Implement service accounts, delegation tokens (`act` chain), per-tenant quotas, and audit log streaming. | -| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-TEN-49-001 | Add service account token minting, delegation, and `--impersonate` banner/controls. | -| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-TEN-49-001 | Integrate ABAC policy overlay (optional), expose audit API, and support service token minting endpoints. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-50-001 | Add `/docs/install/telemetry-stack.md` for collector deployment and offline packaging. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | BLOCKED (2025-10-26) | Docs Guild | DOCS-OBS-50-001 | Author `/docs/observability/overview.md` with imposed rule banner and architecture context. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-50-002 | Document telemetry standards (fields, scrubbing, sampling) under `/docs/observability/telemetry-standards.md`. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-50-003 | Publish structured logging guide `/docs/observability/logging.md` with examples and imposed rule banner. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-50-004 | Publish tracing guide `/docs/observability/tracing.md` covering context propagation and sampling. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-SEC-OBS-50-001 | Update `/docs/security/redaction-and-privacy.md` for telemetry privacy controls. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | ops/devops/TASKS.md | DOING (2025-10-26) | DevOps Guild | DEVOPS-OBS-50-002 | Stand up multi-tenant metrics/logs/traces backends with retention and isolation. | -> Staging rollout plan recorded in `docs/modules/telemetry/operations/storage.md`; waiting on Authority-issued tokens and namespace bootstrap. +| Sprint 33 | Orchestrator Dashboard | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ORCH-33-001 | Publish Grafana dashboards for rate-limit/backpressure/error clustering and configure alert rules with runbooks. | +| Sprint 33 | Orchestrator Dashboard | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-ORCH-33-001 | Add `Orch.Operator` role, control action scopes, and enforce reason/ticket field capture. | +| Sprint 33 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-33-001 | Wire orchestrator control hooks (pause, throttle, retry) into Concelier workers with safe checkpoints. | +| Sprint 33 | Orchestrator Dashboard | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-ORCH-33-001 | Honor orchestrator throttles, classify VEX errors, and emit retry-safe checkpoints in Excititor worker. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-33-001 | Add artifact upload helpers (object store + checksum) and idempotency guard to Go SDK. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-33-002 | Implement error classification/retry helper and structured failure report in Go SDK. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-33-001 | Add artifact publish/idempotency features to Python SDK with object store integration. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-33-002 | Expose error classification/retry/backoff helpers in Python SDK with structured logging. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-001 | Enable source/job control actions (test, pause/resume, retry/cancel/prioritize) with RBAC and audit hooks. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-002 | Implement adaptive token-bucket rate limiter and concurrency caps reacting to upstream 429/503 signals. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-003 | Add watermark/backfill manager with event-time windows, duplicate suppression, and preview API. | +| Sprint 33 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-33-004 | Deliver dead-letter storage, replay endpoints, and surfaced error classes with remediation hints. | +| Sprint 33 | Orchestrator Dashboard | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-33-101 | Implement orchestrator-driven policy evaluation workers with heartbeats, SLO metrics, and rate limit awareness. | +| Sprint 33 | Orchestrator Dashboard | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-ORCH-33-001 | Report SBOM ingest backpressure metrics and support orchestrator pause/resume/backfill signals. | +| Sprint 33 | Orchestrator Dashboard | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-ORCH-33-001 | Expose `consensus_compute` orchestrator job type and integrate VEX Lens worker for diff batches. | +| Sprint 33 | Orchestrator Dashboard | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-ORCH-33-001 | Add control endpoints (actions/backfill) and SSE bridging with permission checks and error mapping. | +| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-001 | Author `/docs/orchestrator/run-ledger.md` describing provenance export format and audits. | +| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-002 | Author `/docs/security/secrets-handling.md` covering KMS refs, redaction, and operator hygiene. | +| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-003 | Author `/docs/operations/orchestrator-runbook.md` (failures, backfill guide, circuit breakers). | +| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-004 | Author `/docs/schemas/artifacts.md` detailing artifact kinds, schema versions, hashing, storage layout. | +| Sprint 34 | Orchestrator Dashboard | docs/TASKS.md | TODO | Docs Guild | DOCS-ORCH-34-005 | Author `/docs/slo/orchestrator-slo.md` defining SLOs, burn alerts, and measurement strategy. | +| Sprint 34 | Orchestrator Dashboard | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-ORCH-34-001 | Provide Helm/Compose manifests, scaling defaults, and offline kit instructions for orchestrator service. | +| Sprint 34 | Orchestrator Dashboard | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ORCH-34-001 | Harden production dashboards/alerts, synthetic probes, and incident response playbooks for orchestrator. | +| Sprint 34 | Orchestrator Dashboard | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-34-006 | Bundle orchestrator service, worker SDK samples, and Postgres snapshot into Offline Kit with integrity checks. | +| Sprint 34 | Orchestrator Dashboard | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-ORCH-34-001 | Add `Orch.Admin` role for quotas/backfills, enforce audit reason requirements, update docs and offline defaults. | +| Sprint 34 | Orchestrator Dashboard | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-ORCH-34-001 | Implement backfill wizard and quota management commands with dry-run preview and guardrails. | +| Sprint 34 | Orchestrator Dashboard | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-ORCH-34-001 | Implement orchestrator-driven backfills for advisory sources with idempotent artifact reuse and ledger linkage. | +| Sprint 34 | Orchestrator Dashboard | src/Excititor/StellaOps.Excititor.Worker/TASKS.md | TODO | Excititor Worker Guild | EXCITITOR-ORCH-34-001 | Support orchestrator backfills and circuit breaker resets for Excititor sources with auditing. | +| Sprint 34 | Orchestrator Dashboard | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-34-101 | Link orchestrator run ledger entries into Findings Ledger provenance export and audit queries. | +| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md | TODO | Worker SDK Guild | WORKER-GO-34-001 | Add backfill range execution, watermark handshake, and artifact dedupe verification to Go SDK. | +| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Python/TASKS.md | TODO | Worker SDK Guild | WORKER-PY-34-001 | Add backfill support and deterministic artifact dedupe validation to Python SDK. | +| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-001 | Implement quota management APIs, SLO burn-rate computation, and alert budget tracking. | +| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-002 | Build audit log and immutable run ledger export with signed manifest support. | +| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-003 | Run perf/scale validation (10k jobs, dispatch <150 ms) and add autoscaling hooks. | +| Sprint 34 | Orchestrator Dashboard | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-34-004 | Package orchestrator container, Helm overlays, offline bundle seeds, and provenance attestations. | +| Sprint 34 | Orchestrator Dashboard | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-34-101 | Expose policy eval run ledger exports and SLO burn metrics to orchestrator. | +| Sprint 34 | Orchestrator Dashboard | src/SbomService/StellaOps.SbomService/TASKS.md | TODO | SBOM Service Guild | SBOM-ORCH-34-001 | Enable SBOM backfill and watermark reconciliation; emit coverage metrics and flood guard. | +| Sprint 34 | Orchestrator Dashboard | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-ORCH-34-001 | Integrate consensus compute completion events with orchestrator ledger and provenance outputs. | +| Sprint 34 | Orchestrator Dashboard | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-ORCH-34-001 | Expose quotas/backfill/queue metrics endpoints, throttle toggles, and error clustering APIs. | +| Sprint 35 | EPDR Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild | SCANNER-ANALYZERS-LANG-11-001 | Build entrypoint resolver (identity + environment profiles) and emit normalized entrypoint records. | +| Sprint 35 | EPDR Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild | SCANNER-ANALYZERS-LANG-11-002 | Static IL/reflection/ALC heuristics producing dependency edges with reason codes and confidence. | +| Sprint 35 | EPDR Foundations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild, Signals Guild | SCANNER-ANALYZERS-LANG-11-003 | Runtime loader/PInvoke signal ingestion merged with static/declared edges (confidence & explain). | +| Sprint 35 | Export Center Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-35-001 | Author `/docs/modules/export-center/overview.md` with purpose, profiles, security, and imposed rule reminder. | +| Sprint 35 | Export Center Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-35-002 | Author `/docs/modules/export-center/architecture.md` detailing service components, adapters, manifests, signing, and distribution. | +| Sprint 35 | Export Center Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-35-003 | Publish `/docs/modules/export-center/profiles.md` covering schemas, examples, and compatibility. | +| Sprint 35 | Export Center Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-EXPORT-35-001 | Package exporter service/worker containers, Helm overlays (download-only), and rollout guide. | +| Sprint 35 | Export Center Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-EXPORT-35-001 | Create exporter CI pipeline (lint/test/perf smoke), object storage fixtures, and initial Grafana dashboards. | +| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-001 | Bootstrap exporter service, configuration, and migrations for export profiles/runs/inputs/distributions with tenant scopes. | +| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-002 | Implement planner resolving filters to iterators and orchestrator job contract with deterministic sampling. | +| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-003 | Deliver JSON adapters (raw/policy) with canonical normalization, redaction enforcement, and zstd writers. | +| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-004 | Build mirror (full) adapter producing filesystem layout, manifests, and bundle assembly for download profile. | +| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-005 | Implement manifest/provenance writer and KMS signing/attestation for export bundles. | +| Sprint 35 | Export Center Phase 1 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-006 | Expose Export API (profiles, runs, download) with SSE updates, concurrency controls, and audit logging. | +| Sprint 35 | Export Center Phase 1 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-EXPORT-35-001 | Provide paginated streaming endpoints for advisories, VEX, SBOMs, and findings filtered by scope selectors. | +| Sprint 35 | Export Center Phase 1 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-35-101 | Register export job type, quotas, and rate policies; surface export job telemetry for scheduler. | +| Sprint 35 | Export Center Phase 1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-35-201 | Expose deterministic policy snapshot + evaluated findings endpoint aligned with Export Center requirements. | +| Sprint 35 | Export Center Phase 1 | src/VexLens/StellaOps.VexLens/TASKS.md | TODO | VEX Lens Guild | VEXLENS-EXPORT-35-001 | Publish consensus snapshot API delivering deterministic JSON for export consumption. | +| Sprint 35 | Export Center Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXPORT-35-001 | Route Export Center APIs through gateway with tenant scoping, viewer/operator scopes, and streaming downloads. | +| Sprint 36 | EPDR Observations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild, SBOM Service Guild | SCANNER-ANALYZERS-LANG-11-004 | Normalize EPDR output to Scanner observation writer (entrypoints + edges + env profiles). | +| Sprint 36 | EPDR Observations | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md | TODO | Scanner EPDR Guild, QA Guild | SCANNER-ANALYZERS-LANG-11-005 | End-to-end fixtures/benchmarks covering publish modes, RIDs, trimming, NativeAOT with explain traces. | +| Sprint 36 | Export Center Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-36-004 | Author `/docs/modules/export-center/api.md` with endpoint examples and imposed rule note. | +| Sprint 36 | Export Center Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-36-005 | Publish `/docs/modules/export-center/cli.md` covering commands, scripts, verification, and imposed rule reminder. | +| Sprint 36 | Export Center Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-36-006 | Write `/docs/modules/export-center/trivy-adapter.md` detailing mappings, compatibility, and test matrix. | +| Sprint 36 | Export Center Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-EXPORT-36-001 | Document registry credentials, OCI push workflows, and automation for export distributions. | +| Sprint 36 | Export Center Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-EXPORT-36-001 | Integrate Trivy compatibility validation, OCI push smoke tests, and metrics dashboards for export throughput. | +| Sprint 36 | Export Center Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXPORT-36-001 | Add `stella export distribute` (OCI/objstore), `run download --resume`, and status polling enhancements. | +| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-001 | Implement Trivy DB adapter (core) with schema mapping, validation, and compatibility gating. | +| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-002 | Add Trivy Java DB variant, shared manifest entries, and adapter regression tests. | +| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-003 | Build OCI distribution engine for exports with descriptor annotations and registry auth handling. | +| Sprint 36 | Export Center Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-36-004 | Extend planner/run lifecycle for OCI/object storage distributions with retry + idempotency. | +| Sprint 36 | Export Center Phase 2 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-36-101 | Add distribution job follow-ups, retention metadata, and metrics for export runs. | +| Sprint 36 | Export Center Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXPORT-36-001 | Expose distribution endpoints (OCI/object storage) and manifest/provenance download proxies with RBAC. | +| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-001 | Publish `/docs/modules/export-center/mirror-bundles.md` detailing layouts, deltas, encryption, imposed rule reminder. | +| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-002 | Publish `/docs/modules/export-center/provenance-and-signing.md` covering manifests, attestation, verification. | +| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-003 | Publish `/docs/operations/export-runbook.md` for failures, tuning, capacity, with imposed rule note. | +| Sprint 37 | Export Center Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-EXPORT-37-004 | Publish `/docs/security/export-hardening.md` covering RBAC, isolation, encryption, and imposed rule. | +| Sprint 37 | Export Center Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-EXPORT-37-001 | Finalize dashboards/alerts for exports (failure, verify), retention jobs, and chaos testing harness. | +| Sprint 37 | Export Center Phase 3 | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-37-001 | Package Export Center mirror bundles + verification tooling into Offline Kit with manifest/signature updates. | +| Sprint 37 | Export Center Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-EXPORT-37-001 | Add `Export.Admin` scope enforcement for retention, encryption keys, and scheduling APIs. | +| Sprint 37 | Export Center Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-EXPORT-37-001 | Implement `stella export schedule`, `run verify`, and bundle verification tooling with signature/hash checks. | +| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-001 | Implement mirror delta adapter, base export linkage, and content-addressed reuse. | +| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-002 | Add bundle encryption, key wrapping with KMS, and verification tooling for encrypted exports. | +| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-003 | Deliver scheduling/retention engine (cron/event triggers), audit trails, and retry idempotency enhancements. | +| Sprint 37 | Export Center Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-37-004 | Provide export verification API and CLI integration, including hash/signature validation endpoints. | +| Sprint 37 | Export Center Phase 3 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-37-101 | Enable scheduled export runs, retention pruning hooks, and failure alerting integration. | +| Sprint 37 | Export Center Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-EXPORT-37-001 | Surface scheduling, retention, and verification endpoints plus encryption parameter handling. | +| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-001 | Format detector & binary identity for ELF/PE/Mach-O (multi-slice) with stable entrypoint IDs. | +| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-002 | ELF dynamic parser emitting dtneeded edges, runpath metadata, symbol version needs. | +| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-003 | PE import + delay-load + SxS manifest parsing producing reason-coded edges. | +| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-004 | Mach-O load command parsing with @rpath expansion and slice handling. | +| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-005 | Cross-platform resolver engine modeling search order/explain traces for ELF/PE/Mach-O. | +| Sprint 37 | Native Analyzer Core | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-006 | Heuristic scanner for dlopen/LoadLibrary strings, plugin configs, ecosystem hints with confidence tags. | +| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild | SCANNER-ANALYZERS-NATIVE-20-007 | Serialize entrypoints/edges/env profiles to Scanner writer (AOC-compliant observations). | +| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild, QA Guild | SCANNER-ANALYZERS-NATIVE-20-008 | Fixture suite + determinism benchmarks for native analyzer across linux/windows/macos. | +| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild, Signals Guild | SCANNER-ANALYZERS-NATIVE-20-009 | Optional runtime capture adapters (eBPF/ETW/dyld) producing runtime-load edges with redaction. | +| Sprint 38 | Native Observation Pipeline | src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md | TODO | Native Analyzer Guild, DevOps Guild | SCANNER-ANALYZERS-NATIVE-20-010 | Package native analyzer plug-in + Offline Kit updates and restart-time loading. | +| Sprint 38 | Notifications Studio Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-NOTIFY-38-001 | Publish `/docs/notifications/overview.md` and `/docs/notifications/architecture.md` ending with imposed rule statement. | +| Sprint 38 | Notifications Studio Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-NOTIFY-38-001 | Package notifier API/worker Helm overlays (email/chat/webhook), secrets templates, rollout guide. | +| Sprint 38 | Notifications Studio Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NOTIFY-38-001 | Stand up notifier CI pipelines, event bus fixtures, base dashboards for events/notifications latency. | +| Sprint 38 | Notifications Studio Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-NOTIFY-38-001 | Implement `stella notify` rule/template/incident commands (list/create/test/ack) with file-based inputs. | +| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-001 | Bootstrap notifier service, migrations for notif tables, event ingestion, and rule engine foundation (policy violations + job failures). | +| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-002 | Implement channel adapters (email, chat-webhook, generic webhook) with retry and audit logging. | +| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-003 | Deliver template service (versioning, preview), rendering pipeline with redaction, and provenance links. | +| Sprint 38 | Notifications Studio Phase 1 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-38-004 | Expose initial API (rules CRUD, templates, incidents list, ack) and live feed WS stream. | +| Sprint 38 | Notifications Studio Phase 1 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-38-101 | Standardize event envelope publication (policy/export/job lifecycle) with idempotency keys for notifier ingestion. | +| Sprint 38 | Notifications Studio Phase 1 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-38-201 | Emit enriched violation events including rationale IDs via orchestrator bus. | +| Sprint 38 | Notifications Studio Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-NOTIFY-38-001 | Route notifier APIs through gateway with tenant scoping and operator scopes. | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-001 | Java input normalizer (jar/war/ear/fat/jmod/jimage) with MR overlay selection. | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-002 | Module/classpath builder with duplicate & split-package detection. | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-003 | SPI scanner & provider selection with warnings. | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | DONE | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-004 | Reflection/TCCL heuristics emitting reason-coded edges. | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-005 | Framework config extraction (Spring, Jakarta, MicroProfile, logging, Graal configs). | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-006 | JNI/native hint detection for Java artifacts. | +| Sprint 39 | Java Analyzer Core | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-007 | Manifest/signature metadata collector (main/start/agent classes, signers). | +| Sprint 39 | Notifications Studio Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-NOTIFY-39-002 | Publish `/docs/notifications/rules.md`, `/templates.md`, `/digests.md` with imposed rule reminder. | +| Sprint 39 | Notifications Studio Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NOTIFY-39-002 | Add throttling/quiet-hours dashboards, digest job monitoring, and storm breaker alerts. | +| Sprint 39 | Notifications Studio Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-NOTIFY-39-001 | Add simulation/digest CLI verbs and advanced filtering for incidents. | +| Sprint 39 | Notifications Studio Phase 2 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-NOTIFY-39-001 | Optimize digest queries and provide API for notifier to fetch unresolved policy violations/SBOM deltas. | +| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-001 | Implement correlation engine, throttling, quiet hours/maintenance evaluator, and incident state machine. | +| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-002 | Add digests generator with Findings Ledger queries and distribution (email/chat). | +| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-003 | Provide simulation engine and API for rule dry-run against historical events. | +| Sprint 39 | Notifications Studio Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-39-004 | Integrate quiet hours calendars and default throttles with audit logging. | +| Sprint 39 | Notifications Studio Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-NOTIFY-39-001 | Surface digest scheduling, simulation, and throttle management endpoints via gateway. | +| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild | SCANNER-ANALYZERS-JAVA-21-008 | Observation writer producing entrypoints/components/edges with warnings. | +| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild, QA Guild | SCANNER-ANALYZERS-JAVA-21-009 | Fixture suite + determinism/perf benchmarks for Java analyzer. | +| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild, Signals Guild | SCANNER-ANALYZERS-JAVA-21-010 | Optional runtime ingestion via agent/JFR producing runtime edges. | +| Sprint 40 | Java Observation & Runtime | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/TASKS.md | TODO | Java Analyzer Guild, DevOps Guild | SCANNER-ANALYZERS-JAVA-21-011 | Package Java analyzer plug-in + Offline Kit/CLI updates. | +| Sprint 40 | Notifications Studio Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-NOTIFY-40-001 | Publish `/docs/notifications/channels.md`, `/escalations.md`, `/api.md`, `/operations/notifier-runbook.md`, `/security/notifications-hardening.md` with imposed rule lines. | +| Sprint 40 | Notifications Studio Phase 3 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-NOTIFY-40-001 | Package notifier escalations + localization deployment overlays, signed ack token rotation scripts, and rollback guidance. | +| Sprint 40 | Notifications Studio Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NOTIFY-40-001 | Finalize notifier dashboards/alerts (escalation failures, ack latency), chaos testing harness, and channel health monitoring. | +| Sprint 40 | Notifications Studio Phase 3 | ops/offline-kit/TASKS.md | CARRY (no scope change) | Offline Kit Guild | DEVOPS-OFFLINE-37-002 | Carry from Sprint 37: Notifier offline packs (sample configs, template/digest packs, dry-run harness) with integrity checks. | +| Sprint 40 | Notifications Studio Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Enforce ack token signing/rotation, webhook allowlists, and admin-only escalation settings. | +| Sprint 40 | Notifications Studio Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-NOTIFY-40-001 | Implement ack token redemption, escalation management, localization previews. | +| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-001 | Implement escalations, on-call schedules, ack bridge, PagerDuty/OpsGenie adapters, and localization bundles. | +| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-002 | Add CLI inbox/in-app feed channels and summary storm breaker notifications. | +| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-003 | Harden security: signed ack links, webhook HMAC/IP allowlists, tenant isolation fuzzing, localization fallback. | +| Sprint 40 | Notifications Studio Phase 3 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-SVC-40-004 | Finalize observability (incident metrics, escalation latency) and chaos tests for channel outages. | +| Sprint 40 | Notifications Studio Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-NOTIFY-40-001 | Expose escalation, localization, channel health endpoints and verification of signed links. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-41-001 | Publish `/docs/modules/cli/guides/overview.md`, `/cli/configuration.md`, `/cli/output-and-exit-codes.md` (with imposed rule). | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-CLI-41-001 | Package CLI release artifacts (tarballs, completions, container image) with distribution docs. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CLI-41-001 | Establish CLI build pipeline (multi-platform binaries, SBOM, checksums) and parity matrix CI enforcement. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-PACKS-41-001 | Define CLI SSO scopes and Packs (`Packs.Read/Write/Run/Approve`) roles; update discovery/offline defaults. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-CORE-41-001 | Implement CLI config/auth foundation, global flags, output renderer, and error/exit code mapping. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PARITY-41-001 | Deliver parity command groups (`policy`, `sbom`, `vuln`, `vex`, `advisory`, `export`, `orchestrator`) with JSON/table outputs and `--explain`. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PARITY-41-002 | Implement `notify`, `aoc`, `auth` command groups, idempotency keys, completions, and parity matrix export. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-41-101 | Register `pack-run` job type, integrate logs/artifacts, expose pack run metadata. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/PacksRegistry/StellaOps.PacksRegistry/TASKS.md | TODO | Packs Registry Guild | PACKS-REG-41-001 | Implement packs index API, signature verification, provenance storage, and RBAC. | +| Sprint 41 | CLI Parity & Task Packs Phase 1 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-41-001 | Bootstrap Task Runner service, migrations, run API, local executor, approvals pause, artifact capture. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-42-001 | Publish `/docs/modules/cli/guides/parity-matrix.md`, `/cli/commands/*.md`, `/docs/task-packs/spec.md` (imposed rule). | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CLI-42-001 | Add CLI golden output tests, parity diff automation, and pack run CI harness. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PACKS-42-001 | Implement Task Pack CLI commands (`pack plan/run/push/pull/verify`) with plan/simulate engine and expression sandbox. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PARITY-41-001..002 | Close parity gaps for Notifications, Policy Studio advanced features, SBOM graph, Vuln Explorer; parity matrix green. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-PACKS-42-001 | Expose snapshot/time-travel APIs for CLI offline mode and pack simulation. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-SVC-42-101 | Stream pack run logs via SSE/WS, expose artifact manifests, enforce pack run quotas. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/PacksRegistry/StellaOps.PacksRegistry/TASKS.md | TODO | Packs Registry Guild | PACKS-REG-42-001 | Support pack version lifecycle, tenant allowlists, provenance export, signature rotation. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ENGINE-42-201 | Provide stable rationale IDs/APIs for CLI `--explain` and pack policy gates. | +| Sprint 42 | CLI Parity & Task Packs Phase 2 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-42-001 | Add loops, conditionals, `maxParallel`, outputs, simulation mode, policy gates in Task Runner. | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-PACKS-43-001 | Publish `/docs/task-packs/authoring-guide.md`, `/registry.md`, `/runbook.md`, `/security/pack-signing-and-rbac.md`, `/operations/cli-release-and-packaging.md` (imposed rule). | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CLI-43-001 | Finalize multi-platform release automation, SBOM signing, parity gate enforcement, pack run chaos tests. | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-PACKS-41-001 | Enforce pack signing policies, approval RBAC, CLI token scopes for CI headless runs. | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-PACKS-42-001 | Deliver advanced pack features (approvals pause/resume, remote streaming, secret injection), localization, man pages. | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-SVC-35-005, PACKS-REG-41-001 | Integrate pack run manifests into export bundles and CLI verify flows. | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/PacksRegistry/StellaOps.PacksRegistry/TASKS.md | TODO | Packs Registry Guild | PACKS-REG-42-001 | Enforce pack signing policies, audit trails, registry mirroring, Offline Kit support. | +| Sprint 43 | CLI Parity & Task Packs Phase 3 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-42-001 | Implement approvals workflow, notifications integration, remote artifact uploads, chaos resilience. | +| Sprint 44 | Containerized Distribution Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-44-001 | Publish install overview + Compose Quickstart docs (imposed rule). | +| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | COMPOSE-44-001 | Deliver Quickstart Compose stack with seed data and quickstart script. | +| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | COMPOSE-44-002 | Provide backup/reset scripts with guardrails and documentation. | +| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | COMPOSE-44-003 | Implement seed job and onboarding wizard toggle (`QUICKSTART_MODE`). | +| Sprint 44 | Containerized Distribution Phase 1 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-COMPOSE-44-001 | Finalize Quickstart scripts and README. | +| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONTAINERS-44-001 | Automate multi-arch builds with SBOM/signature pipeline. | +| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DOCKER-44-001 | Author multi-stage Dockerfiles with non-root users, read-only FS, and health scripts for all services. | +| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DOCKER-44-002 | Generate SBOMs and cosign attestations for each image; integrate signature verification in CI. | +| Sprint 44 | Containerized Distribution Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DOCKER-44-003 | Ensure `/health/*`, `/version`, `/metrics`, and capability endpoints (`merge=false`) are exposed across services. | +| Sprint 44 | Containerized Distribution Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONTAINERS-44-001 | Expose config discovery and quickstart handling with health/version endpoints. | +| Sprint 45 | Containerized Distribution Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-45-001 | Publish Helm production + configuration reference docs (imposed rule). | +| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-HELM-45-001 | Publish Helm install guide and sample values. | +| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | HELM-45-001 | Scaffold Helm chart with component toggles and pinned digests. | +| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | HELM-45-002 | Add security features (TLS, NetworkPolicy, Secrets integration). | +| Sprint 45 | Containerized Distribution Phase 2 | ops/deployment/TASKS.md | TODO | Deployment Guild | HELM-45-003 | Implement HPA, PDB, readiness gates, and observability hooks. | +| Sprint 45 | Containerized Distribution Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONTAINERS-45-001 | Add Compose/Helm smoke tests to CI. | +| Sprint 45 | Containerized Distribution Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONTAINERS-45-001 | Ensure readiness endpoints and config toggles support Helm deployments. | +| Sprint 46 | Containerized Distribution Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-46-001 | Publish air-gap, supply chain, health/readiness, image catalog, console onboarding docs (imposed rule). | +| Sprint 46 | Containerized Distribution Phase 3 | ops/deployment/TASKS.md | TODO | Deployment Guild | DEPLOY-AIRGAP-46-001 | Provide air-gap load script and docs. | +| Sprint 46 | Containerized Distribution Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-CONTAINERS-46-001 | Build signed air-gap bundle and verify in CI. | +| Sprint 46 | Containerized Distribution Phase 3 | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | OFFLINE-CONTAINERS-46-001 | Include air-gap bundle and instructions in Offline Kit. | +| Sprint 46 | Containerized Distribution Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-CONTAINERS-46-001 | Harden offline mode and document fallback behavior. | +| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | docs/TASKS.md | TODO | Docs Guild | DOCS-TEN-47-001 | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` (imposed rule). | +| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-TEN-47-001 | Integrate JWKS caching, signature verification tests, and auth regression suite into CI. | +| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement unified JWT/ODIC config, scope grammar, tenant/project claims, and JWKS caching in Authority. | +| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-TEN-47-001 | Ship `stella login`, `whoami`, `tenants list`, and tenant flag persistence with secure token storage. | +| Sprint 47 | Authority-Backed Scopes & Tenancy Phase 1 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-TEN-47-001 | Add auth middleware (token verification, tenant activation, scope checks) and structured 403 responses. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | docs/TASKS.md | TODO | Docs Guild | DOCS-TEN-48-001 | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md` (imposed rule). | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-TEN-48-001 | Write integration tests for RLS enforcement, tenant audit stream, and object store prefix checks. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-TEN-48-001 | Ensure advisory linkers operate per tenant with RLS, enforce aggregation-only capability endpoint. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-TEN-48-001 | Same as above for VEX linkers; enforce capability endpoint `merge=false`. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-TEN-48-001 | Add tenant prefixes to manifests/artifacts, enforce scope checks, and block cross-tenant exports by default. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-TEN-48-001 | Partition findings by tenant/project, enable RLS, and update queries/events to include tenant context. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-TEN-48-001 | Tenant-scope notification rules, incidents, and outbound channels; update storage schemas. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-TEN-48-001 | Stamp jobs with tenant/project, set DB session context, and reject jobs without context. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-TEN-48-001 | Add `tenant_id`/`project_id` to policy data, enable Postgres RLS, and expose rationale IDs with tenant context. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-TEN-48-001 | Propagate tenant/project to all steps, enforce object store prefix, and validate before execution. | +| Sprint 48 | Authority-Backed Scopes & Tenancy Phase 2 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-TEN-48-001 | Enforce tenant context through persistence (DB GUC, object store prefix), add request annotations, and emit audit events. | +| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | docs/TASKS.md | TODO | Docs Guild | DOCS-TEN-49-001 | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, `/docs/install/configuration-reference.md` updates (imposed rule). | +| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-TEN-49-001 | Implement audit log pipeline, monitor scope usage, chaos tests for JWKS outage, and tenant load/perf tests. | +| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-TEN-49-001 | Implement service accounts, delegation tokens (`act` chain), per-tenant quotas, and audit log streaming. | +| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-TEN-49-001 | Add service account token minting, delegation, and `--impersonate` banner/controls. | +| Sprint 49 | Authority-Backed Scopes & Tenancy Phase 3 | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-TEN-49-001 | Integrate ABAC policy overlay (optional), expose audit API, and support service token minting endpoints. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-INSTALL-50-001 | Add `/docs/install/telemetry-stack.md` for collector deployment and offline packaging. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | BLOCKED (2025-10-26) | Docs Guild | DOCS-OBS-50-001 | Author `/docs/observability/overview.md` with imposed rule banner and architecture context. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-50-002 | Document telemetry standards (fields, scrubbing, sampling) under `/docs/observability/telemetry-standards.md`. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-50-003 | Publish structured logging guide `/docs/observability/logging.md` with examples and imposed rule banner. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-50-004 | Publish tracing guide `/docs/observability/tracing.md` covering context propagation and sampling. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | docs/TASKS.md | TODO | Docs Guild | DOCS-SEC-OBS-50-001 | Update `/docs/security/redaction-and-privacy.md` for telemetry privacy controls. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | ops/devops/TASKS.md | DOING (2025-10-26) | DevOps Guild | DEVOPS-OBS-50-002 | Stand up multi-tenant metrics/logs/traces backends with retention and isolation. | +> Staging rollout plan recorded in `docs/modules/telemetry/operations/storage.md`; waiting on Authority-issued tokens and namespace bootstrap. | Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Authority/StellaOps.Authority/TASKS.md | DOING (2025-11-01) | Authority Core & Security Guild | AUTH-OBS-50-001 | Introduce observability/timeline/evidence/attestation scopes and update discovery metadata. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-50-001 | Propagate trace headers from CLI commands and print correlation IDs. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-50-001 | Replace ad-hoc logging with telemetry core across advisory ingestion/linking. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001 | Adopt telemetry core in Concelier APIs and surface correlation IDs. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-50-001 | Integrate telemetry core into VEX ingestion/linking with scope metadata. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-50-001 | Add telemetry core to VEX APIs and emit trace headers. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-50-001 | Enable telemetry core in export planner/workers capturing bundle metadata. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-50-001 | Wire telemetry core through ledger writer/projector for append/replay operations. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-50-001 | Instrument orchestrator scheduler/control APIs with telemetry core spans/logs. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-50-001 | Instrument policy compile/evaluate flows with telemetry core spans/logs. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-50-001 | Adopt telemetry core in Task Runner host and workers with scrubbed transcripts. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-50-001 | Bootstrap telemetry core library with structured logging, OTLP exporters, and deterministic bootstrap. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-50-002 | Deliver context propagation middleware for HTTP/gRPC/jobs/CLI carrying trace + tenant metadata. | -| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-50-001 | Integrate telemetry core into gateway and emit structured traces/logs for all routes. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-51-001 | Publish `/docs/observability/metrics-and-slos.md` with alert policies. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-51-001 | Deploy SLO evaluator service, dashboards, and alert routing. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-51-001 | Implement `stella obs top` streaming health metrics command. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-51-001 | Emit ingest latency metrics + SLO thresholds for advisories. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-51-001 | Provide VEX ingest metrics and SLO burn-rate automation. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-51-001 | Capture export planner/bundle latency metrics and SLOs. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-51-001 | Add ledger/projector metrics dashboards and burn-rate policies. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OBS-51-001 | Ingest SLO burn-rate webhooks and deliver observability alerts. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-51-001 | Publish orchestration metrics, SLOs, and burn-rate alerts. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-51-001 | Publish policy evaluation metrics + dashboards meeting SLO targets. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-51-001 | Emit task runner golden-signal metrics and SLO alerts. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-51-001 | Ship metrics helpers + exemplar guards for golden signals. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Security Guild | TELEMETRY-OBS-51-002 | Implement logging scrubbing and tenant debug override controls. | -| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-51-001 | Expose `/obs/health` and `/obs/slo` aggregations for services. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-OBS-52-001 | Document `stella obs` CLI commands and scripting patterns. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-OBS-52-001 | Document Console observability hub and trace/log search workflows. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-OBS-52-002 | Publish Console forensics/timeline guidance with imposed rule banner. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-52-001 | Configure streaming pipelines and schema validation for timeline events. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-52-001 | Add `stella obs trace` + log commands correlating timeline data. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-52-001 | Emit advisory ingest/link timeline events with provenance metadata. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-52-001 | Provide SSE bridge for advisory timeline events. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-52-001 | Emit VEX ingest/link timeline events with justification info. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-52-001 | Stream VEX timeline updates to clients with tenant filters. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-52-001 | Publish export lifecycle events into timeline. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-52-001 | Record ledger append/projection events into timeline stream. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-52-001 | Emit job lifecycle timeline events with tenant/project metadata. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-52-001 | Emit policy decision timeline events with rule summaries and trace IDs. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-52-001 | Emit pack run timeline events and dedupe logic. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-52-001 | Bootstrap timeline indexer service and schema with RLS scaffolding. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-52-002 | Implement event ingestion pipeline with ordering and dedupe. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-52-003 | Expose timeline query APIs with tenant filters and pagination. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Security Guild | TIMELINE-OBS-52-004 | Finalize RLS + scope enforcement and audit logging for timeline reads. | -| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-52-001 | Provide trace/log proxy endpoints bridging to timeline + log store. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-FORENSICS-53-001 | Document `stella forensic` CLI workflows with sample bundles. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | docs/TASKS.md | TODO | Docs Guild | DOCS-FORENSICS-53-001 | Publish `/docs/forensics/evidence-locker.md` covering bundles, WORM, legal holds. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | docs/TASKS.md | TODO | Docs Guild | DOCS-FORENSICS-53-003 | Publish `/docs/forensics/timeline.md` with schema and query examples. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-53-001 | Provision WORM-capable storage, legal hold automation, and backup/restore scripts for evidence locker. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-FORENSICS-53-001 | Ship `stella forensic snapshot` commands invoking evidence locker. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-53-001 | Generate advisory evidence payloads (raw doc, linkset diff) for locker. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-53-001 | Add `/evidence/advisories/*` gateway endpoints consuming locker APIs. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-53-001 | Bootstrap evidence locker service with schema, storage abstraction, and RLS. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-53-002 | Implement bundle builders for evaluation, job, and export snapshots. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-53-003 | Expose evidence APIs (create/get/verify/hold) with audit + quotas. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-53-001 | Produce VEX evidence payloads and push to locker. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-53-001 | Expose `/evidence/vex/*` endpoints retrieving locker bundles. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-53-001 | Store export manifests + transcripts within evidence bundles. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-53-001 | Persist evidence bundle references alongside ledger entries and expose lookup API. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-53-001 | Attach job capsules + manifests to evidence locker snapshots. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-53-001 | Build evaluation evidence bundles (inputs, rule traces, engine version). | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-53-001 | Capture step transcripts and manifests into evidence bundles. | -| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-53-001 | Link timeline events to evidence bundle digests and expose evidence lookup endpoint. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | docs/TASKS.md | TODO | Docs Guild | DOCS-FORENSICS-53-002 | Publish `/docs/forensics/provenance-attestation.md` covering signing + verification. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-54-001 | Manage provenance signing infrastructure (KMS keys, timestamp authority) and CI verification. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-FORENSICS-54-001 | Implement `stella forensic verify` command verifying bundles + signatures. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-FORENSICS-54-002 | Add `stella forensic attest show` command with signer/timestamp details. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-54-001 | Sign advisory batches with DSSE attestations and expose verification. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-54-001 | Add `/attestations/advisories/*` endpoints surfacing verification metadata. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-54-001 | Attach DSSE signing/timestamping to evidence bundles and emit timeline hooks. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-54-002 | Provide bundle packaging + offline verification fixtures. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-54-001 | Produce VEX batch attestations linking to timeline/ledger. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-54-001 | Expose `/attestations/vex/*` endpoints with verification summaries. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-54-001 | Produce export attestation manifests and CLI verification hooks. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-54-001 | Produce DSSE attestations for jobs and surface verification endpoint. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-54-001 | Generate DSSE attestations for policy evaluations and expose verification API. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild | PROV-OBS-53-001 | Implement DSSE/SLSA models with deterministic serializer + test vectors. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild | PROV-OBS-53-002 | Build signer abstraction (cosign/KMS/offline) with policy enforcement. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild | PROV-OBS-54-001 | Deliver verification library validating DSSE signatures + Merkle roots. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild, DevEx/CLI Guild | PROV-OBS-54-002 | Package provenance verification tool for CLI integration and offline use. | -| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-54-001 | Generate pack run attestations and link to timeline/evidence. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNBOOK-55-001 | Publish `/docs/runbooks/incidents.md` covering activation, escalation, and verification checklist. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-55-001 | Automate incident mode activation via SLO alerts, retention override management, and reset job. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-50-001 | Propagate trace headers from CLI commands and print correlation IDs. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-50-001 | Replace ad-hoc logging with telemetry core across advisory ingestion/linking. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001 | Adopt telemetry core in Concelier APIs and surface correlation IDs. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-50-001 | Integrate telemetry core into VEX ingestion/linking with scope metadata. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-50-001 | Add telemetry core to VEX APIs and emit trace headers. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-50-001 | Enable telemetry core in export planner/workers capturing bundle metadata. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-50-001 | Wire telemetry core through ledger writer/projector for append/replay operations. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-50-001 | Instrument orchestrator scheduler/control APIs with telemetry core spans/logs. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-50-001 | Instrument policy compile/evaluate flows with telemetry core spans/logs. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-50-001 | Adopt telemetry core in Task Runner host and workers with scrubbed transcripts. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-50-001 | Bootstrap telemetry core library with structured logging, OTLP exporters, and deterministic bootstrap. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-50-002 | Deliver context propagation middleware for HTTP/gRPC/jobs/CLI carrying trace + tenant metadata. | +| Sprint 50 | Observability & Forensics Phase 1 – Baseline Telemetry | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-50-001 | Integrate telemetry core into gateway and emit structured traces/logs for all routes. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | docs/TASKS.md | TODO | Docs Guild | DOCS-OBS-51-001 | Publish `/docs/observability/metrics-and-slos.md` with alert policies. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-51-001 | Deploy SLO evaluator service, dashboards, and alert routing. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-51-001 | Implement `stella obs top` streaming health metrics command. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-51-001 | Emit ingest latency metrics + SLO thresholds for advisories. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-51-001 | Provide VEX ingest metrics and SLO burn-rate automation. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-51-001 | Capture export planner/bundle latency metrics and SLOs. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-51-001 | Add ledger/projector metrics dashboards and burn-rate policies. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OBS-51-001 | Ingest SLO burn-rate webhooks and deliver observability alerts. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-51-001 | Publish orchestration metrics, SLOs, and burn-rate alerts. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-51-001 | Publish policy evaluation metrics + dashboards meeting SLO targets. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-51-001 | Emit task runner golden-signal metrics and SLO alerts. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-51-001 | Ship metrics helpers + exemplar guards for golden signals. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Security Guild | TELEMETRY-OBS-51-002 | Implement logging scrubbing and tenant debug override controls. | +| Sprint 51 | Observability & Forensics Phase 2 – SLOs & Dashboards | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-51-001 | Expose `/obs/health` and `/obs/slo` aggregations for services. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-OBS-52-001 | Document `stella obs` CLI commands and scripting patterns. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-OBS-52-001 | Document Console observability hub and trace/log search workflows. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | docs/TASKS.md | TODO | Docs Guild | DOCS-CONSOLE-OBS-52-002 | Publish Console forensics/timeline guidance with imposed rule banner. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-52-001 | Configure streaming pipelines and schema validation for timeline events. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-52-001 | Add `stella obs trace` + log commands correlating timeline data. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-52-001 | Emit advisory ingest/link timeline events with provenance metadata. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-52-001 | Provide SSE bridge for advisory timeline events. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-52-001 | Emit VEX ingest/link timeline events with justification info. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-52-001 | Stream VEX timeline updates to clients with tenant filters. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-52-001 | Publish export lifecycle events into timeline. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-52-001 | Record ledger append/projection events into timeline stream. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-52-001 | Emit job lifecycle timeline events with tenant/project metadata. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-52-001 | Emit policy decision timeline events with rule summaries and trace IDs. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-52-001 | Emit pack run timeline events and dedupe logic. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-52-001 | Bootstrap timeline indexer service and schema with RLS scaffolding. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-52-002 | Implement event ingestion pipeline with ordering and dedupe. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-52-003 | Expose timeline query APIs with tenant filters and pagination. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Security Guild | TIMELINE-OBS-52-004 | Finalize RLS + scope enforcement and audit logging for timeline reads. | +| Sprint 52 | Observability & Forensics Phase 3 – Timeline & Decision Logs | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-52-001 | Provide trace/log proxy endpoints bridging to timeline + log store. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | docs/TASKS.md | TODO | Docs Guild | DOCS-CLI-FORENSICS-53-001 | Document `stella forensic` CLI workflows with sample bundles. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | docs/TASKS.md | TODO | Docs Guild | DOCS-FORENSICS-53-001 | Publish `/docs/forensics/evidence-locker.md` covering bundles, WORM, legal holds. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | docs/TASKS.md | TODO | Docs Guild | DOCS-FORENSICS-53-003 | Publish `/docs/forensics/timeline.md` with schema and query examples. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-53-001 | Provision WORM-capable storage, legal hold automation, and backup/restore scripts for evidence locker. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-FORENSICS-53-001 | Ship `stella forensic snapshot` commands invoking evidence locker. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-53-001 | Generate advisory evidence payloads (raw doc, linkset diff) for locker. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-53-001 | Add `/evidence/advisories/*` gateway endpoints consuming locker APIs. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-53-001 | Bootstrap evidence locker service with schema, storage abstraction, and RLS. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-53-002 | Implement bundle builders for evaluation, job, and export snapshots. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-53-003 | Expose evidence APIs (create/get/verify/hold) with audit + quotas. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-53-001 | Produce VEX evidence payloads and push to locker. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-53-001 | Expose `/evidence/vex/*` endpoints retrieving locker bundles. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-53-001 | Store export manifests + transcripts within evidence bundles. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-53-001 | Persist evidence bundle references alongside ledger entries and expose lookup API. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-53-001 | Attach job capsules + manifests to evidence locker snapshots. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-53-001 | Build evaluation evidence bundles (inputs, rule traces, engine version). | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-53-001 | Capture step transcripts and manifests into evidence bundles. | +| Sprint 53 | Observability & Forensics Phase 4 – Evidence Locker | src/TimelineIndexer/StellaOps.TimelineIndexer/TASKS.md | TODO | Timeline Indexer Guild | TIMELINE-OBS-53-001 | Link timeline events to evidence bundle digests and expose evidence lookup endpoint. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | docs/TASKS.md | TODO | Docs Guild | DOCS-FORENSICS-53-002 | Publish `/docs/forensics/provenance-attestation.md` covering signing + verification. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-54-001 | Manage provenance signing infrastructure (KMS keys, timestamp authority) and CI verification. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-FORENSICS-54-001 | Implement `stella forensic verify` command verifying bundles + signatures. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-FORENSICS-54-002 | Add `stella forensic attest show` command with signer/timestamp details. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-54-001 | Sign advisory batches with DSSE attestations and expose verification. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-54-001 | Add `/attestations/advisories/*` endpoints surfacing verification metadata. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-54-001 | Attach DSSE signing/timestamping to evidence bundles and emit timeline hooks. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-54-002 | Provide bundle packaging + offline verification fixtures. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-54-001 | Produce VEX batch attestations linking to timeline/ledger. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-54-001 | Expose `/attestations/vex/*` endpoints with verification summaries. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-54-001 | Produce export attestation manifests and CLI verification hooks. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-54-001 | Produce DSSE attestations for jobs and surface verification endpoint. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-54-001 | Generate DSSE attestations for policy evaluations and expose verification API. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild | PROV-OBS-53-001 | Implement DSSE/SLSA models with deterministic serializer + test vectors. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild | PROV-OBS-53-002 | Build signer abstraction (cosign/KMS/offline) with policy enforcement. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild | PROV-OBS-54-001 | Deliver verification library validating DSSE signatures + Merkle roots. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/Provenance/StellaOps.Provenance.Attestation/TASKS.md | TODO | Provenance Guild, DevEx/CLI Guild | PROV-OBS-54-002 | Package provenance verification tool for CLI integration and offline use. | +| Sprint 54 | Observability & Forensics Phase 5 – Provenance & Verification | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-54-001 | Generate pack run attestations and link to timeline/evidence. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNBOOK-55-001 | Publish `/docs/runbooks/incidents.md` covering activation, escalation, and verification checklist. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OBS-55-001 | Automate incident mode activation via SLO alerts, retention override management, and reset job. | | Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Authority/StellaOps.Authority/TASKS.md | DOING (2025-11-01) | Authority Core & Security Guild | AUTH-OBS-55-001 | Enforce `obs:incident` scope with fresh-auth requirement and audit export for toggles. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-55-001 | Ship `stella obs incident-mode` commands with safeguards and audit logging. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-55-001 | Increase sampling and raw payload retention under incident mode with redaction guards. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-55-001 | Provide incident mode toggle endpoints and propagate to services. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-55-001 | Extend evidence retention + activation events for incident windows. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-55-001 | Enable incident sampling + retention overrides for VEX pipelines. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-55-001 | Add incident mode APIs for VEX services with audit + guardrails. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-55-001 | Increase export telemetry + debug retention during incident mode and emit events. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-55-001 | Extend retention and diagnostics capture during incident mode. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OBS-55-001 | Send incident mode start/stop notifications with quick links to evidence/timeline. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-55-001 | Increase telemetry + evidence capture during incident mode and emit activation events. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-55-001 | Capture full rule traces + retention bump on incident activation with timeline events. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-55-001 | Capture extra debug data + notifications for incident mode runs. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-55-001 | Implement incident mode sampling toggle API with activation audit trail. | -| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-55-001 | Deliver `/obs/incident-mode` control endpoints with audit + retention previews. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-001 | Publish `/docs/airgap/overview.md`. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-002 | Document sealing and egress controls. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-003 | Publish mirror bundles guide. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-004 | Publish bootstrap pack guide. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-56-001 | Publish deny-all egress policies and verification script for sealed environments. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-56-002 | Provide bundle staging/import scripts for air-gapped object stores. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-56-003 | Build Bootstrap Pack pipeline bundling images/charts with checksums. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Controller/TASKS.md | TODO | AirGap Controller Guild | AIRGAP-CTL-56-001 | Implement sealing state machine, persistence, and RBAC scopes for air-gapped status. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Controller/TASKS.md | TODO | AirGap Controller Guild | AIRGAP-CTL-56-002 | Expose seal/status APIs with policy hash validation and staleness placeholders. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-56-001 | Implement DSSE/TUF/Merkle verification helpers. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-56-002 | Enforce root rotation policy for bundles. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-56-001 | Ship `EgressPolicy` facade with sealed/unsealed enforcement and remediation errors. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-56-002 | Deliver Roslyn analyzer blocking raw HTTP clients; wire into CI. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-56-001 | Implement mirror create/verify and airgap verify commands. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-50-001 | Ensure telemetry propagation for sealed logging. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-AIRGAP-56-001 | Add mirror ingestion adapters preserving source metadata. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-AIRGAP-56-001 | Add VEX mirror ingestion adapters. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-56-001 | Extend export center to build mirror bundles. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Mirror/StellaOps.Mirror.Creator/TASKS.md | TODO | Mirror Creator Guild | MIRROR-CRT-56-001 | Build deterministic bundle assembler (advisories/vex/policy). | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-56-001 | Validate jobs against sealed-mode restrictions. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-56-001 | Accept policy packs from bundles with provenance tracking. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-56-001 | Enforce sealed-mode plan validation for network calls. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-56-001 | (Carry) Extend telemetry core with sealed-mode hooks before integration. | -| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-56-001 | Extend telemetry core usage for sealed-mode status surfaces (seal/unseal dashboards, drift signals). | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-001 | Publish staleness/time doc. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-002 | Publish console airgap doc. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-003 | Publish CLI airgap doc. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-004 | Publish airgap operations runbook. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-57-001 | Automate mirror bundle creation with approvals. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-57-002 | Run sealed-mode CI suite enforcing zero egress. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-57-001 | Implement bundle catalog with RLS + migrations. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-57-002 | Load artifacts into object store with checksum verification. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-57-001 | Adopt EgressPolicy in core services. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-57-002 | Enforce Task Runner job plan validation. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Time/TASKS.md | TODO | AirGap Time Guild | AIRGAP-TIME-57-001 | Parse signed time tokens and expose normalized anchors. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-57-001 | Complete airgap import CLI with diff preview. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-57-002 | Ship seal/status CLI commands. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-56-002 | Deliver bootstrap pack artifacts. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Mirror/StellaOps.Mirror.Creator/TASKS.md | TODO | Mirror Creator Guild | MIRROR-CRT-57-001 | Add OCI image support to mirror bundles. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Mirror/StellaOps.Mirror.Creator/TASKS.md | TODO | Mirror Creator Guild | MIRROR-CRT-57-002 | Embed signed time anchors in bundles. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-56-001 | Lock notifications to enclave-safe channels. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-56-002 | Integrate sealing status + staleness into scheduling. | -| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-56-002 | Provide bundle ingestion helper steps. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-001 | Publish degradation matrix doc. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-002 | Update trust & signing doc for DSSE/TUF roots. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-003 | Publish developer airgap contracts doc. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-004 | Document portable evidence workflows. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Controller/TASKS.md | TODO | AirGap Controller Guild | AIRGAP-CTL-58-001 | Persist time anchor data and expose drift metrics. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-58-001 | Disable remote observability exporters in sealed mode. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-58-002 | Add CLI sealed-mode guard. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Time/TASKS.md | TODO | AirGap Time Guild | AIRGAP-TIME-58-001 | Compute drift/staleness metrics and surface via controller status. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Time/TASKS.md | TODO | AirGap Time Guild | AIRGAP-TIME-58-002 | Emit notifications/events for staleness budgets. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-58-001 | Ship portable evidence export helper. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-AIRGAP-57-002 | Annotate advisories with staleness metadata. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-AIRGAP-57-002 | Annotate VEX statements with staleness metadata. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-57-001 | Add portable evidence export integration. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-57-001 | Notify on drift/staleness thresholds. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-58-001 | Link import/export jobs to timeline/evidence. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-57-002 | Show degradation fallback info in explain traces. | -| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-58-001 | Capture import job evidence transcripts. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-AIRGAP-57-001 | Map sealed-mode violations to standard errors. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-AIRGAP-57-001 | Map sealed-mode violations to standard errors. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-58-001 | Emit notifications/timeline for bundle readiness. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-AIRGAP-56-002 | Enforce staleness thresholds for findings exports. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-58-001 | Notify on portable evidence exports. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-57-001 | Automate mirror bundle job scheduling with audit provenance. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-57-001 | Enforce sealed-mode guardrails inside evaluation engine. | -| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-57-001 | Block execution when seal state mismatched; emit timeline events. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-004 | Document portable evidence workflows. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-58-001 | Finalize portable evidence CLI workflow with verification. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-AIRGAP-58-001 | Emit timeline events for bundle imports. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-60-001 | Deliver portable evidence export flow for sealed environments with checksum manifest and offline verification script. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-AIRGAP-58-001 | Emit timeline events for VEX bundle imports. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-AIRGAP-57-001 | Link findings to portable evidence bundles. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-58-001 | (Carry) Portable evidence notifications. | -| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-58-001 | Notify on stale policy packs and guide remediation. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-61-001 | Publish `/docs/api/overview.md`. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-61-002 | Publish `/docs/api/conventions.md`. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-61-003 | Publish `/docs/api/versioning.md`. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OAS-61-001 | Add OAS lint/validation/diff stages to CI. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-61-001 | Configure lint rules and CI enforcement. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-61-002 | Enforce example coverage in CI. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-61-001 | Scaffold per-service OpenAPI skeletons with shared components. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-61-002 | Build aggregate composer and integrate into CI. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-OAS-61-001 | Document Authority authentication APIs in OAS. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-OAS-61-002 | Provide Authority discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-61-001 | Update advisory OAS coverage. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-61-002 | Populate advisory examples. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OAS-61-001 | Implement Concelier discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OAS-61-002 | Standardize error envelope. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-61-001 | Update VEX OAS coverage. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-61-002 | Provide VEX examples. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OAS-61-001 | Implement discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OAS-61-002 | Migrate errors to standard envelope. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-61-001 | Update Exporter spec coverage. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-61-002 | Implement Exporter discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-61-001 | Expand Findings Ledger spec coverage. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-61-002 | Provide ledger discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-61-001 | Update notifier spec coverage. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-61-002 | Implement notifier discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OAS-61-001 | Extend Orchestrator spec coverage. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OAS-61-002 | Provide orchestrator discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-61-001 | Document Task Runner APIs in OAS. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-61-002 | Expose Task Runner discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-61-001 | Implement gateway discovery endpoint. | -| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-61-002 | Standardize error envelope across gateway. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-CONTRIB-62-001 | Publish API contracts contributing guide. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-DEVPORT-62-001 | Document dev portal publishing. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-62-001 | Deploy `/docs/api/reference/` generated site. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-SDK-62-001 | Publish SDK overview + language guides. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-SEC-62-001 | Update auth scopes documentation. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-TEST-62-001 | Publish contract testing doc. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-62-001 | Implement compatibility diff tool. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-62-001 | Populate examples for top endpoints. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-OAS-62-001 | Provide SDK auth helpers/tests. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-62-001 | Migrate CLI to official SDK. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-62-002 | Update CLI error handling for new envelope. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-62-001 | Add SDK smoke tests for advisory APIs. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OAS-62-001 | Add advisory API examples. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-62-001 | Build static generator with nav/search. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-62-002 | Add schema viewer, examples, version selector. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-62-001 | Add SDK tests for VEX APIs. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OAS-62-001 | Provide VEX API examples. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-62-001 | Ensure SDK streaming helpers for exports. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-62-001 | Provide SDK tests for ledger APIs. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-62-001 | Provide SDK examples for notifier APIs. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-62-001 | Establish generator framework. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-62-002 | Implement shared post-processing helpers. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-62-001 | Provide SDK examples for pack runs. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-62-001 | Align pagination/idempotency behaviors. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-62-001 | Generate mock server fixtures. | -| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-62-002 | Integrate mock server into CI. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | docs/TASKS.md | TODO | Docs Guild | DOCS-TEST-62-001 | (Carry) ensure contract testing doc final. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-63-001 | Integrate compatibility diff gating. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-63-001 | Compatibility diff support. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-63-002 | Define discovery schema metadata. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-63-001 | Add CLI spec download command. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-63-001 | Add Try-It console. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-63-002 | Embed SDK snippets/quick starts. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-001 | Release TypeScript SDK alpha. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-002 | Release Python SDK alpha. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-003 | Release Go SDK alpha. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-004 | Release Java SDK alpha. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-63-001 | Configure SDK release pipelines. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-63-002 | Automate changelogs from OAS diffs. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-63-001 | Build replay harness for drift detection. | -| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-63-002 | Emit contract testing metrics. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-DEVPORT-64-001 | Document devportal offline usage. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-DEVPORT-63-001 | Automate developer portal pipeline. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-DEVPORT-64-001 | Schedule offline bundle builds. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-64-001 | Offline portal build. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-64-002 | Add accessibility/performance checks. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/ExportCenter/StellaOps.ExportCenter.DevPortalOffline/TASKS.md | TODO | DevPortal Offline Guild | DVOFF-64-001 | Implement devportal offline export job. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/ExportCenter/StellaOps.ExportCenter.DevPortalOffline/TASKS.md | TODO | DevPortal Offline Guild | DVOFF-64-002 | Provide verification CLI. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-64-001 | Migrate CLI to SDK. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-64-002 | Integrate SDKs into Console. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-64-001 | Hook SDK releases to Notifications. | -| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-64-002 | Produce devportal offline bundle. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-DEVPORT-64-001 | (Carry) ensure offline doc published; update as necessary. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-63-001 | (Carry) compatibility gating monitoring. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-55-001 | Ship `stella obs incident-mode` commands with safeguards and audit logging. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OBS-55-001 | Increase sampling and raw payload retention under incident mode with redaction guards. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-55-001 | Provide incident mode toggle endpoints and propagate to services. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-55-001 | Extend evidence retention + activation events for incident windows. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OBS-55-001 | Enable incident sampling + retention overrides for VEX pipelines. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-55-001 | Add incident mode APIs for VEX services with audit + guardrails. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OBS-55-001 | Increase export telemetry + debug retention during incident mode and emit events. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OBS-55-001 | Extend retention and diagnostics capture during incident mode. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OBS-55-001 | Send incident mode start/stop notifications with quick links to evidence/timeline. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OBS-55-001 | Increase telemetry + evidence capture during incident mode and emit activation events. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-OBS-55-001 | Capture full rule traces + retention bump on incident activation with timeline events. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OBS-55-001 | Capture extra debug data + notifications for incident mode runs. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-55-001 | Implement incident mode sampling toggle API with activation audit trail. | +| Sprint 55 | Observability & Forensics Phase 6 – Incident Mode | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-55-001 | Deliver `/obs/incident-mode` control endpoints with audit + retention previews. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-001 | Publish `/docs/airgap/overview.md`. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-002 | Document sealing and egress controls. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-003 | Publish mirror bundles guide. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-56-004 | Publish bootstrap pack guide. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-56-001 | Publish deny-all egress policies and verification script for sealed environments. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-56-002 | Provide bundle staging/import scripts for air-gapped object stores. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-56-003 | Build Bootstrap Pack pipeline bundling images/charts with checksums. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Controller/TASKS.md | TODO | AirGap Controller Guild | AIRGAP-CTL-56-001 | Implement sealing state machine, persistence, and RBAC scopes for air-gapped status. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Controller/TASKS.md | TODO | AirGap Controller Guild | AIRGAP-CTL-56-002 | Expose seal/status APIs with policy hash validation and staleness placeholders. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-56-001 | Implement DSSE/TUF/Merkle verification helpers. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-56-002 | Enforce root rotation policy for bundles. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-56-001 | Ship `EgressPolicy` facade with sealed/unsealed enforcement and remediation errors. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-56-002 | Deliver Roslyn analyzer blocking raw HTTP clients; wire into CI. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-56-001 | Implement mirror create/verify and airgap verify commands. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-OBS-50-001 | Ensure telemetry propagation for sealed logging. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-AIRGAP-56-001 | Add mirror ingestion adapters preserving source metadata. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-AIRGAP-56-001 | Add VEX mirror ingestion adapters. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-56-001 | Extend export center to build mirror bundles. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Mirror/StellaOps.Mirror.Creator/TASKS.md | TODO | Mirror Creator Guild | MIRROR-CRT-56-001 | Build deterministic bundle assembler (advisories/vex/policy). | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-56-001 | Validate jobs against sealed-mode restrictions. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-56-001 | Accept policy packs from bundles with provenance tracking. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-56-001 | Enforce sealed-mode plan validation for network calls. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Telemetry/StellaOps.Telemetry.Core/TASKS.md | TODO | Observability Guild | TELEMETRY-OBS-56-001 | (Carry) Extend telemetry core with sealed-mode hooks before integration. | +| Sprint 56 | Air-Gapped Mode Phase 1 – Sealing Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OBS-56-001 | Extend telemetry core usage for sealed-mode status surfaces (seal/unseal dashboards, drift signals). | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-001 | Publish staleness/time doc. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-002 | Publish console airgap doc. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-003 | Publish CLI airgap doc. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-57-004 | Publish airgap operations runbook. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-57-001 | Automate mirror bundle creation with approvals. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-AIRGAP-57-002 | Run sealed-mode CI suite enforcing zero egress. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-57-001 | Implement bundle catalog with RLS + migrations. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Importer/TASKS.md | TODO | AirGap Importer Guild | AIRGAP-IMP-57-002 | Load artifacts into object store with checksum verification. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-57-001 | Adopt EgressPolicy in core services. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-57-002 | Enforce Task Runner job plan validation. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/AirGap/StellaOps.AirGap.Time/TASKS.md | TODO | AirGap Time Guild | AIRGAP-TIME-57-001 | Parse signed time tokens and expose normalized anchors. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-57-001 | Complete airgap import CLI with diff preview. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-57-002 | Ship seal/status CLI commands. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-56-002 | Deliver bootstrap pack artifacts. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Mirror/StellaOps.Mirror.Creator/TASKS.md | TODO | Mirror Creator Guild | MIRROR-CRT-57-001 | Add OCI image support to mirror bundles. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Mirror/StellaOps.Mirror.Creator/TASKS.md | TODO | Mirror Creator Guild | MIRROR-CRT-57-002 | Embed signed time anchors in bundles. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-56-001 | Lock notifications to enclave-safe channels. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-56-002 | Integrate sealing status + staleness into scheduling. | +| Sprint 57 | Air-Gapped Mode Phase 2 – Mirror Bundles & Imports | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-56-002 | Provide bundle ingestion helper steps. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-001 | Publish degradation matrix doc. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-002 | Update trust & signing doc for DSSE/TUF roots. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-003 | Publish developer airgap contracts doc. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-004 | Document portable evidence workflows. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Controller/TASKS.md | TODO | AirGap Controller Guild | AIRGAP-CTL-58-001 | Persist time anchor data and expose drift metrics. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-58-001 | Disable remote observability exporters in sealed mode. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Policy/TASKS.md | TODO | AirGap Policy Guild | AIRGAP-POL-58-002 | Add CLI sealed-mode guard. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Time/TASKS.md | TODO | AirGap Time Guild | AIRGAP-TIME-58-001 | Compute drift/staleness metrics and surface via controller status. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/AirGap/StellaOps.AirGap.Time/TASKS.md | TODO | AirGap Time Guild | AIRGAP-TIME-58-002 | Emit notifications/events for staleness budgets. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-58-001 | Ship portable evidence export helper. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-AIRGAP-57-002 | Annotate advisories with staleness metadata. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-AIRGAP-57-002 | Annotate VEX statements with staleness metadata. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-57-001 | Add portable evidence export integration. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-57-001 | Notify on drift/staleness thresholds. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-58-001 | Link import/export jobs to timeline/evidence. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-57-002 | Show degradation fallback info in explain traces. | +| Sprint 58 | Air-Gapped Mode Phase 3 – Staleness & Enforcement | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-58-001 | Capture import job evidence transcripts. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-AIRGAP-57-001 | Map sealed-mode violations to standard errors. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-AIRGAP-57-001 | Map sealed-mode violations to standard errors. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-AIRGAP-58-001 | Emit notifications/timeline for bundle readiness. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-AIRGAP-56-002 | Enforce staleness thresholds for findings exports. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-58-001 | Notify on portable evidence exports. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-AIRGAP-57-001 | Automate mirror bundle job scheduling with audit provenance. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-57-001 | Enforce sealed-mode guardrails inside evaluation engine. | +| Sprint 59 | Air-Gapped Mode Phase 4 – Deterministic Jobs & Enforcement | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-AIRGAP-57-001 | Block execution when seal state mismatched; emit timeline events. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-58-004 | Document portable evidence workflows. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-AIRGAP-58-001 | Finalize portable evidence CLI workflow with verification. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-AIRGAP-58-001 | Emit timeline events for bundle imports. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/EvidenceLocker/StellaOps.EvidenceLocker/TASKS.md | TODO | Evidence Locker Guild | EVID-OBS-60-001 | Deliver portable evidence export flow for sealed environments with checksum manifest and offline verification script. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-AIRGAP-58-001 | Emit timeline events for VEX bundle imports. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-AIRGAP-57-001 | Link findings to portable evidence bundles. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-AIRGAP-58-001 | (Carry) Portable evidence notifications. | +| Sprint 60 | Air-Gapped Mode Phase 5 – Evidence Portability & UX | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-AIRGAP-58-001 | Notify on stale policy packs and guide remediation. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-61-001 | Publish `/docs/api/overview.md`. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-61-002 | Publish `/docs/api/conventions.md`. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-61-003 | Publish `/docs/api/versioning.md`. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-OAS-61-001 | Add OAS lint/validation/diff stages to CI. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-61-001 | Configure lint rules and CI enforcement. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-61-002 | Enforce example coverage in CI. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-61-001 | Scaffold per-service OpenAPI skeletons with shared components. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-61-002 | Build aggregate composer and integrate into CI. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-OAS-61-001 | Document Authority authentication APIs in OAS. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-OAS-61-002 | Provide Authority discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-61-001 | Update advisory OAS coverage. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-61-002 | Populate advisory examples. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OAS-61-001 | Implement Concelier discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OAS-61-002 | Standardize error envelope. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-61-001 | Update VEX OAS coverage. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-61-002 | Provide VEX examples. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OAS-61-001 | Implement discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OAS-61-002 | Migrate errors to standard envelope. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-61-001 | Update Exporter spec coverage. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-61-002 | Implement Exporter discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-61-001 | Expand Findings Ledger spec coverage. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-61-002 | Provide ledger discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-61-001 | Update notifier spec coverage. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-61-002 | Implement notifier discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OAS-61-001 | Extend Orchestrator spec coverage. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OAS-61-002 | Provide orchestrator discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-61-001 | Document Task Runner APIs in OAS. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-61-002 | Expose Task Runner discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-61-001 | Implement gateway discovery endpoint. | +| Sprint 61 | SDKs & OpenAPI Phase 1 – Contract Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-61-002 | Standardize error envelope across gateway. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-CONTRIB-62-001 | Publish API contracts contributing guide. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-DEVPORT-62-001 | Document dev portal publishing. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-OAS-62-001 | Deploy `/docs/api/reference/` generated site. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-SDK-62-001 | Publish SDK overview + language guides. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-SEC-62-001 | Update auth scopes documentation. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | docs/TASKS.md | TODO | Docs Guild | DOCS-TEST-62-001 | Publish contract testing doc. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-62-001 | Implement compatibility diff tool. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-62-001 | Populate examples for top endpoints. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Authority/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-OAS-62-001 | Provide SDK auth helpers/tests. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-62-001 | Migrate CLI to official SDK. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-62-002 | Update CLI error handling for new envelope. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-62-001 | Add SDK smoke tests for advisory APIs. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Concelier/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-OAS-62-001 | Add advisory API examples. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-62-001 | Build static generator with nav/search. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-62-002 | Add schema viewer, examples, version selector. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-62-001 | Add SDK tests for VEX APIs. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Excititor/StellaOps.Excititor.WebService/TASKS.md | TODO | Excititor WebService Guild | EXCITITOR-WEB-OAS-62-001 | Provide VEX API examples. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-62-001 | Ensure SDK streaming helpers for exports. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-62-001 | Provide SDK tests for ledger APIs. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-62-001 | Provide SDK examples for notifier APIs. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-62-001 | Establish generator framework. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-62-002 | Implement shared post-processing helpers. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-62-001 | Provide SDK examples for pack runs. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-62-001 | Align pagination/idempotency behaviors. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-62-001 | Generate mock server fixtures. | +| Sprint 62 | SDKs & OpenAPI Phase 2 – Examples & Portal | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-62-002 | Integrate mock server into CI. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | docs/TASKS.md | TODO | Docs Guild | DOCS-TEST-62-001 | (Carry) ensure contract testing doc final. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-63-001 | Integrate compatibility diff gating. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-63-001 | Compatibility diff support. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Api/StellaOps.Api.OpenApi/TASKS.md | TODO | API Contracts Guild | OAS-63-002 | Define discovery schema metadata. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-63-001 | Add CLI spec download command. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-63-001 | Add Try-It console. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-63-002 | Embed SDK snippets/quick starts. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-001 | Release TypeScript SDK alpha. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-002 | Release Python SDK alpha. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-003 | Release Go SDK alpha. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-63-004 | Release Java SDK alpha. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-63-001 | Configure SDK release pipelines. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-63-002 | Automate changelogs from OAS diffs. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-63-001 | Build replay harness for drift detection. | +| Sprint 63 | SDKs & OpenAPI Phase 3 – SDK Alpha & Try-It | test/contract/TASKS.md | TODO | Contract Testing Guild | CONTR-63-002 | Emit contract testing metrics. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-DEVPORT-64-001 | Document devportal offline usage. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-DEVPORT-63-001 | Automate developer portal pipeline. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-DEVPORT-64-001 | Schedule offline bundle builds. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-64-001 | Offline portal build. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/DevPortal/StellaOps.DevPortal.Site/TASKS.md | TODO | Developer Portal Guild | DEVPORT-64-002 | Add accessibility/performance checks. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/ExportCenter/StellaOps.ExportCenter.DevPortalOffline/TASKS.md | TODO | DevPortal Offline Guild | DVOFF-64-001 | Implement devportal offline export job. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/ExportCenter/StellaOps.ExportCenter.DevPortalOffline/TASKS.md | TODO | DevPortal Offline Guild | DVOFF-64-002 | Provide verification CLI. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-64-001 | Migrate CLI to SDK. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Generator/TASKS.md | TODO | SDK Generator Guild | SDKGEN-64-002 | Integrate SDKs into Console. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-64-001 | Hook SDK releases to Notifications. | +| Sprint 64 | SDKs & OpenAPI Phase 4 – Harden & Offline Bundles | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-64-002 | Produce devportal offline bundle. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | docs/TASKS.md | TODO | Docs Guild | DOCS-AIRGAP-DEVPORT-64-001 | (Carry) ensure offline doc published; update as necessary. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Api/StellaOps.Api.Governance/TASKS.md | TODO | API Governance Guild | APIGOV-63-001 | (Carry) compatibility gating monitoring. | | Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Authority/StellaOps.Authority/TASKS.md | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-OAS-63-001 | Deprecation headers for auth endpoints. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-64-001 | SDK update awareness command. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-63-001 | Deprecation metadata for Concelier APIs. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-63-001 | Deprecation metadata for VEX APIs. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-63-001 | Deprecation headers for exporter APIs. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-63-001 | Deprecation headers for ledger APIs. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-63-001 | Emit deprecation notifications. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OAS-63-001 | Add orchestrator deprecation headers. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-64-001 | Production rollout of notifications feed. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-63-001 | Add Task Runner deprecation headers. | -| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-63-001 | Implement deprecation headers in gateway. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-001 | Publish `/docs/risk/overview.md`. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-002 | Publish `/docs/risk/profiles.md`. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-003 | Publish `/docs/risk/factors.md`. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-004 | Publish `/docs/risk/formulas.md`. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-66-001 | Implement CLI profile management commands. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-66-002 | Implement CLI simulation command. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-RISK-66-001 | Expose CVSS/KEV provider data. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-RISK-66-002 | Provide fix availability signals. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-RISK-66-001 | Supply VEX gating data to risk engine. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-RISK-66-002 | Provide reachability inputs. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-66-001 | Add risk scoring columns/indexes. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-66-002 | Implement deterministic scoring upserts. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-66-001 | Create risk severity alert templates. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-66-003 | Integrate schema validation into Policy Engine. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-66-001 | Deliver RiskProfile schema + validators. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-66-002 | Implement inheritance/merge and hashing. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-RISK-66-004 | Extend Policy libraries for RiskProfile handling. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-66-001 | Scaffold risk engine queue/worker/registry. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-66-002 | Implement transforms/gates/contribution calculator. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-66-001 | Expose risk API routing in gateway. | -| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-66-002 | Handle explainability downloads. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-001 | Publish explainability doc. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-002 | Publish risk API doc. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-003 | Publish console risk UI doc. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-004 | Publish CLI risk doc. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-67-001 | Provide risk results query command. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-RISK-67-001 | Add source consensus metrics. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-RISK-67-001 | Add VEX explainability metadata. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-67-001 | Notify on profile publish/deprecate. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | (Prep) risk routing settings seeds. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-67-001 | Enqueue scoring on new findings. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-67-002 | Deliver profile lifecycle APIs. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-67-001 | Integrate profiles into policy store lifecycle. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-67-002 | Publish schema endpoint + validation tooling. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-RISK-67-003 | Provide simulation orchestration APIs. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-67-001 | Integrate CVSS/KEV providers. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-67-002 | Integrate VEX gate provider. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-67-003 | Add fix availability/criticality/exposure providers. | -| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-67-001 | Provide risk status endpoint. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-68-001 | Publish risk bundle doc. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-68-002 | Update AOC invariants doc. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-68-001 | Add risk bundle verification command. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-67-001 | Provide scored findings query API. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-68-001 | Enable scored findings export. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | Configure risk notification routing UI/logic. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-68-001 | Ship simulation API endpoint. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-RISK-68-002 | Support profile export/import. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-68-001 | Persist scoring results & explanations. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-68-002 | Expose jobs/results/explanations APIs. | -| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-68-001 | Emit severity transition events via gateway. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-001..004 | (Carry) ensure docs updated from simulation release. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-69-001 | Build risk bundle. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-69-002 | Integrate bundle into pipelines. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-RISK-69-002 | Enable simulation report exports. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-66-001 | (Completion) finalize severity alert templates. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-69-001 | Implement simulation mode. | -| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-69-002 | Add telemetry/metrics dashboards. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-68-001 | (Carry) finalize risk bundle doc after verification CLI. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-70-001 | Provide bundle verification CLI. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-70-002 | Publish documentation. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-RISK-70-001 | Integrate risk bundle into offline kit. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | Finalize risk alert routing UI. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-70-001 | Support offline provider bundles. | -| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-70-002 | Integrate runtime/reachability providers. | -| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-001..68-002 | Final editorial pass on risk documentation set. | -| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-66-001..68-001 | Harden CLI commands with integration tests and error handling. | -| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-69-001 | Finalize dashboards and alerts for scoring latency. | -| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | Tune routing/quiet hour dedupe for risk alerts. | -| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-69-002 | Optimize performance, cache, and incremental scoring; validate SLOs. | -| Sprint 72 | Attestor Console Phase 1 – Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-73-001 | (Prep) align CI secrets for Attestor service. | -| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-72-001 | Implement DSSE canonicalization and hashing helpers. | -| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-72-002 | Support compact/expanded output and detached payloads. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-SDK-64-001 | SDK update awareness command. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-OAS-63-001 | Deprecation metadata for Concelier APIs. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-OAS-63-001 | Deprecation metadata for VEX APIs. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-OAS-63-001 | Deprecation headers for exporter APIs. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-OAS-63-001 | Deprecation headers for ledger APIs. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-OAS-63-001 | Emit deprecation notifications. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Orchestrator/StellaOps.Orchestrator/TASKS.md | TODO | Orchestrator Service Guild | ORCH-OAS-63-001 | Add orchestrator deprecation headers. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Sdk/StellaOps.Sdk.Release/TASKS.md | TODO | SDK Release Guild | SDKREL-64-001 | Production rollout of notifications feed. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/TaskRunner/StellaOps.TaskRunner/TASKS.md | TODO | Task Runner Guild | TASKRUN-OAS-63-001 | Add Task Runner deprecation headers. | +| Sprint 65 | SDKs & OpenAPI Phase 5 – Deprecation & Notifications | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-OAS-63-001 | Implement deprecation headers in gateway. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-001 | Publish `/docs/risk/overview.md`. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-002 | Publish `/docs/risk/profiles.md`. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-003 | Publish `/docs/risk/factors.md`. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-66-004 | Publish `/docs/risk/formulas.md`. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-66-001 | Implement CLI profile management commands. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-66-002 | Implement CLI simulation command. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-RISK-66-001 | Expose CVSS/KEV provider data. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-RISK-66-002 | Provide fix availability signals. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-RISK-66-001 | Supply VEX gating data to risk engine. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-RISK-66-002 | Provide reachability inputs. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-66-001 | Add risk scoring columns/indexes. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-66-002 | Implement deterministic scoring upserts. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-66-001 | Create risk severity alert templates. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-66-003 | Integrate schema validation into Policy Engine. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-66-001 | Deliver RiskProfile schema + validators. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-66-002 | Implement inheritance/merge and hashing. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-RISK-66-004 | Extend Policy libraries for RiskProfile handling. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-66-001 | Scaffold risk engine queue/worker/registry. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-66-002 | Implement transforms/gates/contribution calculator. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-66-001 | Expose risk API routing in gateway. | +| Sprint 66 | Risk Profiles Phase 1 – Foundations | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-66-002 | Handle explainability downloads. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-001 | Publish explainability doc. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-002 | Publish risk API doc. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-003 | Publish console risk UI doc. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-004 | Publish CLI risk doc. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-67-001 | Provide risk results query command. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md | TODO | Concelier Core Guild | CONCELIER-RISK-67-001 | Add source consensus metrics. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md | TODO | Excititor Core Guild | EXCITITOR-RISK-67-001 | Add VEX explainability metadata. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-67-001 | Notify on profile publish/deprecate. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | (Prep) risk routing settings seeds. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-67-001 | Enqueue scoring on new findings. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-67-002 | Deliver profile lifecycle APIs. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-67-001 | Integrate profiles into policy store lifecycle. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/StellaOps.Policy.RiskProfile/TASKS.md | TODO | Risk Profile Schema Guild | POLICY-RISK-67-002 | Publish schema endpoint + validation tooling. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-RISK-67-003 | Provide simulation orchestration APIs. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-67-001 | Integrate CVSS/KEV providers. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-67-002 | Integrate VEX gate provider. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-67-003 | Add fix availability/criticality/exposure providers. | +| Sprint 67 | Risk Profiles Phase 2 – Providers & Lifecycle | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-67-001 | Provide risk status endpoint. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-68-001 | Publish risk bundle doc. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-68-002 | Update AOC invariants doc. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-68-001 | Add risk bundle verification command. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-67-001 | Provide scored findings query API. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-68-001 | Enable scored findings export. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | Configure risk notification routing UI/logic. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-RISK-68-001 | Ship simulation API endpoint. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Policy/__Libraries/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-RISK-68-002 | Support profile export/import. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-68-001 | Persist scoring results & explanations. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-68-002 | Expose jobs/results/explanations APIs. | +| Sprint 68 | Risk Profiles Phase 3 – APIs & Ledger | src/Web/StellaOps.Web/TASKS.md | TODO | BE-Base Platform Guild | WEB-RISK-68-001 | Emit severity transition events via gateway. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-001..004 | (Carry) ensure docs updated from simulation release. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-69-001 | Build risk bundle. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-69-002 | Integrate bundle into pipelines. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-RISK-69-002 | Enable simulation report exports. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-66-001 | (Completion) finalize severity alert templates. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-69-001 | Implement simulation mode. | +| Sprint 69 | Risk Profiles Phase 4 – Simulation & Reporting | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-69-002 | Add telemetry/metrics dashboards. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-68-001 | (Carry) finalize risk bundle doc after verification CLI. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-70-001 | Provide bundle verification CLI. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/TASKS.md | TODO | Risk Bundle Export Guild | RISK-BUNDLE-70-002 | Publish documentation. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/ExportCenter/StellaOps.ExportCenter/TASKS.md | TODO | Exporter Service Guild | EXPORT-RISK-70-001 | Integrate risk bundle into offline kit. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | Finalize risk alert routing UI. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-70-001 | Support offline provider bundles. | +| Sprint 70 | Risk Profiles Phase 5 – Air-Gap & Advanced Factors | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-70-002 | Integrate runtime/reachability providers. | +| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | docs/TASKS.md | TODO | Docs Guild | DOCS-RISK-67-001..68-002 | Final editorial pass on risk documentation set. | +| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/Cli/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI Guild | CLI-RISK-66-001..68-001 | Harden CLI commands with integration tests and error handling. | +| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/Findings/StellaOps.Findings.Ledger/TASKS.md | TODO | Findings Ledger Guild | LEDGER-RISK-69-001 | Finalize dashboards and alerts for scoring latency. | +| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-RISK-68-001 | Tune routing/quiet hour dedupe for risk alerts. | +| Sprint 71 | Risk Profiles Phase 6 – Quality & Performance | src/RiskEngine/StellaOps.RiskEngine/TASKS.md | TODO | Risk Engine Guild | RISK-ENGINE-69-002 | Optimize performance, cache, and incremental scoring; validate SLOs. | +| Sprint 72 | Attestor Console Phase 1 – Foundations | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-73-001 | (Prep) align CI secrets for Attestor service. | +| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-72-001 | Implement DSSE canonicalization and hashing helpers. | +| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-72-002 | Support compact/expanded output and detached payloads. | | Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor.Types/TASKS.md | DONE | Attestation Payloads Guild | ATTEST-TYPES-72-001 | Draft schemas for all attestation payload types. | | Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor.Types/TASKS.md | DONE | Attestation Payloads Guild | ATTEST-TYPES-72-002 | Generate models/validators from schemas. | -| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-72-001 | Scaffold attestor service skeleton. | -| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-72-002 | Implement attestation store + storage integration. | | Sprint 72 | Attestor Console Phase 1 – Foundations | src/__Libraries/StellaOps.Cryptography.Kms/TASKS.md | DONE | KMS Guild | KMS-72-001 | Implement KMS interface + file driver. | -| Sprint 73 | Attestor CLI Phase 2 – Signing & Policies | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-73-001 | Implement `stella attest sign` (payload selection, subject digest, key reference, output format) using official SDK transport. | -| Sprint 73 | Attestor CLI Phase 2 – Signing & Policies | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-73-002 | Implement `stella attest verify` with policy selection, explainability output, and JSON/table formatting. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-73-001 | Publish attestor overview. | +| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-72-001 | Scaffold attestor service skeleton. | +| Sprint 72 | Attestor Console Phase 1 – Foundations | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-72-002 | Implement attestation store + storage integration. | +| Sprint 72 | Attestor Console Phase 1 – Foundations | src/__Libraries/StellaOps.Cryptography.Kms/TASKS.md | DONE | KMS Guild | KMS-72-001 | Implement KMS interface + file driver. | +| Sprint 73 | Attestor CLI Phase 2 – Signing & Policies | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-73-001 | Implement `stella attest sign` (payload selection, subject digest, key reference, output format) using official SDK transport. | +| Sprint 73 | Attestor CLI Phase 2 – Signing & Policies | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-73-002 | Implement `stella attest verify` with policy selection, explainability output, and JSON/table formatting. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-73-001 | Publish attestor overview. | | Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | DONE | Docs Guild | DOCS-ATTEST-73-002 | Publish payload docs. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-73-003 | Publish policies doc. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-73-004 | Publish workflows doc. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-73-001 | Add signing/verification helpers with KMS integration. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-73-003 | Publish policies doc. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-73-004 | Publish workflows doc. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-73-001 | Add signing/verification helpers with KMS integration. | | Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor.Types/TASKS.md | DONE | Attestation Payloads Guild | ATTEST-TYPES-73-001 | Create golden payload fixtures. | | Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor/TASKS.md | DOING | Attestor Service Guild | ATTESTOR-73-001 | Ship signing endpoint. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-73-002 | Ship verification pipeline and reports. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-73-003 | Implement list/fetch APIs. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-73-002 | Ship verification pipeline and reports. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-73-003 | Implement list/fetch APIs. | | Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/__Libraries/StellaOps.Cryptography.Kms/TASKS.md | DONE (2025-10-30) | KMS Guild | KMS-72-002 | CLI support for key import/export. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ATTEST-73-001 | Implement VerificationPolicy lifecycle. | -| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ATTEST-73-002 | Surface policies in Policy Studio. | -| Sprint 74 | Attestor CLI Phase 3 – Transparency & Chain of Custody | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-74-001 | Implement `stella attest list` with filters (subject, type, issuer, scope) and pagination. | -| Sprint 74 | Attestor CLI Phase 3 – Transparency & Chain of Custody | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-74-002 | Implement `stella attest fetch` to download envelopes and payloads to disk. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-001 | Publish keys & issuers doc. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-002 | Publish transparency doc. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-003 | Publish console attestor UI doc. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-004 | Publish CLI attest doc. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-74-001 | Deploy transparency witness infra. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-73-002 | Run fuzz tests for envelope handling. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor.Verify/TASKS.md | TODO | Verification Guild | ATTEST-VERIFY-74-001 | Add telemetry for verification pipeline. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor.Verify/TASKS.md | TODO | Verification Guild | ATTEST-VERIFY-74-002 | Document verification explainability. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ATTEST-73-001 | Implement VerificationPolicy lifecycle. | +| Sprint 73 | Attestor Console Phase 2 – Signing & Policies | src/Policy/StellaOps.Policy.Engine/TASKS.md | TODO | Policy Guild | POLICY-ATTEST-73-002 | Surface policies in Policy Studio. | +| Sprint 74 | Attestor CLI Phase 3 – Transparency & Chain of Custody | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-74-001 | Implement `stella attest list` with filters (subject, type, issuer, scope) and pagination. | +| Sprint 74 | Attestor CLI Phase 3 – Transparency & Chain of Custody | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild | CLI-ATTEST-74-002 | Implement `stella attest fetch` to download envelopes and payloads to disk. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-001 | Publish keys & issuers doc. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-002 | Publish transparency doc. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-003 | Publish console attestor UI doc. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-74-004 | Publish CLI attest doc. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-74-001 | Deploy transparency witness infra. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor.Envelope/TASKS.md | TODO | Envelope Guild | ATTEST-ENVELOPE-73-002 | Run fuzz tests for envelope handling. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor.Verify/TASKS.md | TODO | Verification Guild | ATTEST-VERIFY-74-001 | Add telemetry for verification pipeline. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor.Verify/TASKS.md | TODO | Verification Guild | ATTEST-VERIFY-74-002 | Document verification explainability. | | Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor/TASKS.md | DOING | Attestor Service Guild | ATTESTOR-74-001 | Integrate transparency witness client. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-74-002 | Implement bulk verification worker. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/ExportCenter/StellaOps.ExportCenter.AttestationBundles/TASKS.md | TODO | Attestation Bundle Guild | EXPORT-ATTEST-74-001 | Build attestation bundle export job. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-ATTEST-74-001 | Add verification/key notifications. | -| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-ATTEST-74-002 | Notify key rotation/revocation. | -| Sprint 75 | Attestor CLI Phase 4 – Air Gap & Bulk | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild, Export Guild | CLI-ATTEST-75-002 | Add support for building/verifying attestation bundles in CLI. | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-75-001 | Publish attestor airgap doc. | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-75-002 | Update AOC invariants for attestations. | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-74-002 | Integrate bundle builds into release/offline pipelines. | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-75-001 | Dashboards/alerts for attestor metrics. | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-75-001 | Support attestation bundle export/import for air gap. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-74-002 | Implement bulk verification worker. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/ExportCenter/StellaOps.ExportCenter.AttestationBundles/TASKS.md | TODO | Attestation Bundle Guild | EXPORT-ATTEST-74-001 | Build attestation bundle export job. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-ATTEST-74-001 | Add verification/key notifications. | +| Sprint 74 | Attestor Console Phase 3 – Transparency & Chain of Custody | src/Notifier/StellaOps.Notifier/TASKS.md | TODO | Notifications Service Guild | NOTIFY-ATTEST-74-002 | Notify key rotation/revocation. | +| Sprint 75 | Attestor CLI Phase 4 – Air Gap & Bulk | src/Cli/StellaOps.Cli/TASKS.md | TODO | CLI Attestor Guild, Export Guild | CLI-ATTEST-75-002 | Add support for building/verifying attestation bundles in CLI. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-75-001 | Publish attestor airgap doc. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | docs/TASKS.md | TODO | Docs Guild | DOCS-ATTEST-75-002 | Update AOC invariants for attestations. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-74-002 | Integrate bundle builds into release/offline pipelines. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-ATTEST-75-001 | Dashboards/alerts for attestor metrics. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/Attestor/StellaOps.Attestor/TASKS.md | TODO | Attestor Service Guild | ATTESTOR-75-001 | Support attestation bundle export/import for air gap. | | Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/Attestor/StellaOps.Attestor/TASKS.md | DONE | Attestor Service Guild | ATTESTOR-75-002 | Harden APIs (rate limits, fuzz tests, threat model actions). | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/ExportCenter/StellaOps.ExportCenter.AttestationBundles/TASKS.md | TODO | Attestation Bundle Guild | EXPORT-ATTEST-75-001 | CLI bundle verify/import. | -| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/ExportCenter/StellaOps.ExportCenter.AttestationBundles/TASKS.md | TODO | Attestation Bundle Guild | EXPORT-ATTEST-75-002 | Document attestor airgap workflow. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/ExportCenter/StellaOps.ExportCenter.AttestationBundles/TASKS.md | TODO | Attestation Bundle Guild | EXPORT-ATTEST-75-001 | CLI bundle verify/import. | +| Sprint 75 | Attestor Console Phase 4 – Air Gap & Bulk | src/ExportCenter/StellaOps.ExportCenter.AttestationBundles/TASKS.md | TODO | Attestation Bundle Guild | EXPORT-ATTEST-75-002 | Document attestor airgap workflow. | diff --git a/docs/implplan/SPRINT_100_identity_signing.md b/docs/implplan/SPRINT_100_identity_signing.md index 9d5d4aa5..1ac6e7d3 100644 --- a/docs/implplan/SPRINT_100_identity_signing.md +++ b/docs/implplan/SPRINT_100_identity_signing.md @@ -84,9 +84,12 @@ AUTH-POLICY-27-002 | DONE (2025-11-02) | Provide attestation signing service bin AUTH-POLICY-27-003 | DOING (2025-11-02) | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. Dependencies: AUTH-POLICY-27-001, AUTH-POLICY-27-002. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-TEN-49-001 | DOING (2025-11-02) | Implement service accounts & delegation tokens (`act` chain), per-tenant quotas, audit stream of auth decisions, and revocation APIs. Dependencies: AUTH-TEN-47-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) > 2025-11-02: Service account store + configuration wired, delegation quotas enforced, token persistence extended with `serviceAccountId`/`tokenKind`/`actorChain`, docs & samples refreshed, and new tests cover delegated issuance/persistence. -AUTH-VULN-29-001 | TODO | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. Dependencies: AUTH-POLICY-27-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) -AUTH-VULN-29-002 | TODO | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. Dependencies: AUTH-VULN-29-001, LEDGER-29-002. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) -AUTH-VULN-29-003 | TODO | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. Dependencies: AUTH-VULN-29-001..002. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md) +> 2025-11-02: Updated bootstrap test fixtures to use AuthorityDelegation seed types and verified `/internal/service-accounts` endpoints respond as expected via targeted Authority tests. +> 2025-11-02: Documented bootstrap admin API usage (`/internal/service-accounts/**`) and clarified that repeated seeding preserves Mongo `_id`/`createdAt` values to avoid immutable field errors. +AUTH-VULN-29-001 | DONE (2025-11-03) | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. Dependencies: AUTH-POLICY-27-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) +AUTH-VULN-29-002 | DONE (2025-11-03) | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. Dependencies: AUTH-VULN-29-001, LEDGER-29-002. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) +AUTH-VULN-29-003 | DOING (2025-11-03) | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. Dependencies: AUTH-VULN-29-001..002. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md) +> 2025-11-03: Workflow anti-forgery and attachment token endpoints merged with audit trails; negative-path coverage added (`VulnWorkflowTokenEndpointTests`). Full Authority test suite still running; follow-up execution required after dependency build completes. PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | BE-Auth Plugin, Docs Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) PLG6.DIAGRAM | TODO | Export final sequence/component diagrams for the developer guide and add offline-friendly assets under `docs/assets/authority`. | Docs Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) PLG7.RFC | REVIEW | Socialize LDAP plugin RFC (`docs/rfcs/authority-plugin-ldap.md`) and capture guild feedback. | BE-Auth Plugin, Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) @@ -102,9 +105,9 @@ Task ID | State | Task description | Owners (Source) ISSUER-30-001 | DONE (2025-11-01) | Implement issuer CRUD API with RBAC, audit logging, and tenant scoping; seed CSAF publisher metadata. | Issuer Directory Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) ISSUER-30-002 | DONE (2025-11-01) | Implement key management endpoints (add/rotate/revoke keys), enforce expiry, validate formats (Ed25519, X.509, DSSE). Dependencies: ISSUER-30-001. | Issuer Directory Guild, Security Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) ISSUER-30-003 | DOING | Provide trust weight APIs and tenant overrides with validation (+/- bounds) and audit trails. Dependencies: ISSUER-30-001. | Issuer Directory Guild, Policy Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) -ISSUER-30-004 | DONE (2025-11-01) | Integrate with VEX Lens and Excitator signature verification (client SDK, caching, retries). Dependencies: ISSUER-30-001..003. | Issuer Directory Guild, VEX Lens Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) +ISSUER-30-004 | DONE (2025-11-01) | Integrate with VEX Lens and Excitor signature verification (client SDK, caching, retries). Dependencies: ISSUER-30-001..003. | Issuer Directory Guild, VEX Lens Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) ISSUER-30-005 | DONE (2025-11-01) | Instrument metrics/logs (issuer changes, key rotation, verification failures) and dashboards/alerts. Dependencies: ISSUER-30-001..004. | Issuer Directory Guild, Observability Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) -ISSUER-30-006 | DOING (2025-11-02) | Provide deployment manifests, backup/restore, secure secret storage, and offline kit instructions. Dependencies: ISSUER-30-001..005. | Issuer Directory Guild, DevOps Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) +ISSUER-30-006 | DONE (2025-11-02) | Provide deployment manifests, backup/restore, secure secret storage, and offline kit instructions. Dependencies: ISSUER-30-001..005. | Issuer Directory Guild, DevOps Guild (src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md) [Identity & Signing] 100.D) __Libraries diff --git a/docs/implplan/SPRINT_110_ingestion_evidence.md b/docs/implplan/SPRINT_110_ingestion_evidence.md index 84283bff..2291c3a2 100644 --- a/docs/implplan/SPRINT_110_ingestion_evidence.md +++ b/docs/implplan/SPRINT_110_ingestion_evidence.md @@ -158,7 +158,7 @@ FEEDMERGE-COORD-02-902 ICS-CISA version comparison support | BE-Merge, Models | FEEDMERGE-COORD-02-903 KISA firmware scheme review | BE-Merge, Models | **TODO (due 2025-10-24)** – Pair with KISA team on proposed firmware comparison helper (`kisa.build` or variant), ensure observation mapper alignment, and open Models ticket only if a new comparator is required. Log the final helper signature and observation coverage metrics in coordination docs + tracker files. Dependencies: FEEDMERGE-COORD-02-902. | FEEDMERGE-COORD-02-900 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) Fixture validation sweep | QA | **DOING (2025-10-19)** – Prereqs confirmed none; continuing RHSA fixture regeneration and diff review alongside mapper provenance updates.
2025-10-29: Added `scripts/update-redhat-fixtures.sh` to regenerate golden snapshots with `UPDATE_GOLDENS=1`; run it before reviews to capture CSAF contract deltas. | None (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/TASKS.md) Link-Not-Merge version provenance coordination | BE-Merge | **DOING** – Coordinate remaining connectors (`Acsc`, `Cccs`, `CertBund`, `CertCc`, `Cve`, `Ghsa`, `Ics.Cisa`, `Kisa`, `Ru.Bdu`, `Ru.Nkcki`, `Vndr.Apple`, `Vndr.Cisco`, `Vndr.Msrc`) so they emit `advisory_observations.affected.versions[]` entries with provenance tags and deterministic comparison keys. Track rollout status in `docs/dev/normalized-rule-recipes.md` (now updated for Link-Not-Merge) and retire the legacy merge counters as coverage transitions to linkset validation metrics.
2025-10-29: Added new guidance in the doc for recording observation version metadata and logging gaps via `LinksetVersionCoverage` warnings to replace prior `concelier.merge.normalized_rules*` alerts. Dependencies: CONCELIER-LNM-21-203. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) -MERGE-LNM-21-001 Migration plan authoring | BE-Merge, Architecture Guild | Draft `no-merge` migration playbook, documenting backfill strategy, feature flag rollout, and rollback steps for legacy merge pipeline deprecation. | CONCELIER-LNM-21-101 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) +MERGE-LNM-21-001 | DONE (2025-11-03) | Draft `no-merge` migration playbook, documenting backfill strategy, feature flag rollout, and rollback steps for legacy merge pipeline deprecation.
2025-11-03: Authored `docs/migration/no-merge.md` covering rollout phases, backfill/validation checklists, and rollback guidance; shared artefact owners. | BE-Merge, Architecture Guild (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) [Ingestion & Evidence] 110.B) Concelier.VII @@ -166,7 +166,7 @@ Depends on: Sprint 110.B - Concelier.VI Summary: Ingestion & Evidence focus on Concelier (phase VII). Task ID | State | Task description | Owners (Source) --- | --- | --- | --- -MERGE-LNM-21-002 Merge service deprecation | BE-Merge | Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage. Dependencies: MERGE-LNM-21-001. | MERGE-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) +MERGE-LNM-21-002 | DOING (2025-11-03) | Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage.
2025-11-03: Began dependency audit and call-site inventory ahead of deprecation plan; cataloging service registrations/tests referencing merge APIs. | BE-Merge (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) MERGE-LNM-21-003 Determinism/test updates | QA Guild, BE-Merge | Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible. Dependencies: MERGE-LNM-21-002. | MERGE-LNM-21-002 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) diff --git a/docs/implplan/SPRINT_120_policy_reasoning.md b/docs/implplan/SPRINT_120_policy_reasoning.md index 99ab8ee2..13bc2d02 100644 --- a/docs/implplan/SPRINT_120_policy_reasoning.md +++ b/docs/implplan/SPRINT_120_policy_reasoning.md @@ -5,10 +5,10 @@ Depends on: Sprint 110.A - AdvisoryAI Summary: Policy & Reasoning focus on AirGap). Task ID | State | Task description | Owners (Source) --- | --- | --- | --- -AIRGAP-POL-56-001 | TODO | Implement `StellaOps.AirGap.Policy` package exposing `EgressPolicy` facade with sealed/unsealed branches and remediation-friendly errors. | AirGap Policy Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) -AIRGAP-POL-56-002 | TODO | Create Roslyn analyzer/code fix warning on raw `HttpClient` usage outside approved wrappers; add CI integration. Dependencies: AIRGAP-POL-56-001. | AirGap Policy Guild, DevEx Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) -AIRGAP-POL-57-001 | TODO | Update core web services (Web, Exporter, Policy, Findings, Authority) to use `EgressPolicy`; ensure configuration wiring for sealed mode. Dependencies: AIRGAP-POL-56-002. | AirGap Policy Guild, BE-Base Platform Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) -AIRGAP-POL-57-002 | TODO | Implement Task Runner job plan validator rejecting network steps unless marked internal allow-list. Dependencies: AIRGAP-POL-57-001. | AirGap Policy Guild, Task Runner Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) +AIRGAP-POL-56-001 | DONE | Implement `StellaOps.AirGap.Policy` package exposing `EgressPolicy` facade with sealed/unsealed branches and remediation-friendly errors. | AirGap Policy Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) +AIRGAP-POL-56-002 | DONE | Create Roslyn analyzer/code fix warning on raw `HttpClient` usage outside approved wrappers; add CI integration. Dependencies: AIRGAP-POL-56-001. | AirGap Policy Guild, DevEx Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) +AIRGAP-POL-57-001 | DONE (2025-11-03) | Update core web services (Web, Exporter, Policy, Findings, Authority) to use `EgressPolicy`; ensure configuration wiring for sealed mode. Dependencies: AIRGAP-POL-56-002. | AirGap Policy Guild, BE-Base Platform Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) +AIRGAP-POL-57-002 | DONE (2025-11-03) | Implement Task Runner job plan validator rejecting network steps unless marked internal allow-list.
2025-11-03: Worker wiring pulls `IEgressPolicy`, filesystem dispatcher enforces sealed-mode egress, dispatcher test + grant normalization landed, package versions aligned to rc.2.
Next: ensure other dispatchers/executors reuse the injected policy before enabling sealed-mode runs in worker service. Dependencies: AIRGAP-POL-57-001. | AirGap Policy Guild, Task Runner Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) AIRGAP-POL-58-001 | TODO | Ensure Observability exporters only target local endpoints in sealed mode; disable remote sinks with warning. Dependencies: AIRGAP-POL-57-002. | AirGap Policy Guild, Observability Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) AIRGAP-POL-58-002 | TODO | Add CLI sealed-mode guard that refuses commands needing egress and surfaces remediation. Dependencies: AIRGAP-POL-58-001. | AirGap Policy Guild, CLI Guild (src/AirGap/StellaOps.AirGap.Policy/TASKS.md) @@ -226,7 +226,7 @@ Task ID | State | Task description | Owners (Source) RISK-ENGINE-66-001 | TODO | Scaffold scoring service (job queue, worker loop, provider registry) with deterministic execution harness. | Risk Engine Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) RISK-ENGINE-66-002 | TODO | Implement default transforms (linear, minmax, logistic, piecewise), clamping, gating, and contribution calculator. Dependencies: RISK-ENGINE-66-001. | Risk Engine Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) RISK-ENGINE-67-001 | TODO | Integrate CVSS and KEV providers pulling data from Conseiller; implement reducers (`max`, `any`, `consensus`). Dependencies: RISK-ENGINE-66-002. | Risk Engine Guild, Concelier Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) -RISK-ENGINE-67-002 | TODO | Integrate VEX gate provider and ensure gating short-circuits scoring as configured. Dependencies: RISK-ENGINE-67-001. | Risk Engine Guild, Excitator Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) +RISK-ENGINE-67-002 | TODO | Integrate VEX gate provider and ensure gating short-circuits scoring as configured. Dependencies: RISK-ENGINE-67-001. | Risk Engine Guild, Excitor Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) RISK-ENGINE-67-003 | TODO | Add fix availability, asset criticality, and internet exposure providers with caching + TTL enforcement. Dependencies: RISK-ENGINE-67-002. | Risk Engine Guild, Policy Engine Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) RISK-ENGINE-68-001 | TODO | Persist scoring results + explanation pointers to Findings Ledger; handle incremental updates via input hash. Dependencies: RISK-ENGINE-67-003. | Risk Engine Guild, Findings Ledger Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) RISK-ENGINE-68-002 | TODO | Expose APIs (`/risk/jobs`, `/risk/results`, `/risk/results/{id}/explanation`); include pagination, filtering, error codes. Dependencies: RISK-ENGINE-68-001. | Risk Engine Guild, API Guild (src/RiskEngine/StellaOps.RiskEngine/TASKS.md) diff --git a/docs/implplan/SPRINT_130_scanner_surface.md b/docs/implplan/SPRINT_130_scanner_surface.md index c74320c1..a3110225 100644 --- a/docs/implplan/SPRINT_130_scanner_surface.md +++ b/docs/implplan/SPRINT_130_scanner_surface.md @@ -134,19 +134,27 @@ Summary: Scanner & Surface focus on Scanner (phase VII). Task ID | State | Task description | Owners (Source) --- | --- | --- | --- SCANNER-ENTRYTRACE-18-504 | TODO | Emit EntryTrace AOC NDJSON (`entrytrace.entry/node/edge/target/warning/capability`) and wire CLI/service streaming outputs. Dependencies: SCANNER-ENTRYTRACE-18-503. | EntryTrace Guild (src/Scanner/__Libraries/StellaOps.Scanner.EntryTrace/TASKS.md) -SCANNER-ENV-01 | TODO | Replace ad-hoc environment reads with `StellaOps.Scanner.Surface.Env` helpers for cache roots and CAS endpoints. | Scanner Worker Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md) -SCANNER-ENV-02 | TODO | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration. Dependencies: SCANNER-ENV-01. | Scanner WebService Guild, Ops Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) +SCANNER-ENV-01 | DOING (2025-11-02) | Replace ad-hoc environment reads with `StellaOps.Scanner.Surface.Env` helpers for cache roots and CAS endpoints.
2025-11-02: Env helper wiring drafted for Worker startup; initial tests validate cache root resolution. | Scanner Worker Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md) +SCANNER-ENV-02 | DOING (2025-11-02) | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration. Dependencies: SCANNER-ENV-01.
2025-11-02: WebService bootstrap now consumes Surface.Env helpers for cache roots and feature flag toggles; configuration doc draft pending. | Scanner WebService Guild, Ops Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-ENV-03 | TODO | Adopt Surface.Env helpers for plugin configuration (cache roots, CAS endpoints, feature toggles). Dependencies: SCANNER-ENV-02. | BuildX Plugin Guild (src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md) SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-EVENTS-16-302 | DOING (2025-10-26) | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console. Dependencies: SCANNER-EVENTS-16-301. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-GRAPH-21-001 | TODO | Provide webhook/REST endpoint for Cartographer to request policy overlays and runtime evidence for graph nodes, ensuring determinism and tenant scoping. | Scanner WebService Guild, Cartographer Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-LNM-21-001 | TODO | Update `/reports` and `/policy/runtime` payloads to consume advisory/vex linksets, exposing source severity arrays and conflict summaries alongside effective verdicts. | Scanner WebService Guild, Policy Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-LNM-21-002 | TODO | Add evidence endpoint for Console to fetch linkset summaries with policy overlay for a component/SBOM, including AOC references. Dependencies: SCANNER-LNM-21-001. | Scanner WebService Guild, UI Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) -SCANNER-SECRETS-01 | TODO | Adopt `StellaOps.Scanner.Surface.Secrets` for registry/CAS credentials during scan execution. | Scanner Worker Guild, Security Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md) -SCANNER-SECRETS-02 | TODO | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens). Dependencies: SCANNER-SECRETS-01. | Scanner WebService Guild, Security Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) +SCANNER-SECRETS-01 | DOING (2025-11-02) | Adopt `StellaOps.Scanner.Surface.Secrets` for registry/CAS credentials during scan execution.
2025-11-02: Worker integration tests added for CAS token retrieval via Surface.Secrets abstraction; refactor under review. | Scanner Worker Guild, Security Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md) +SCANNER-SECRETS-02 | DOING (2025-11-02) | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens). Dependencies: SCANNER-SECRETS-01.
2025-11-02: WebService export path now resolves registry credentials via Surface.Secrets stub; CI pipeline hook in progress. | Scanner WebService Guild, Security Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-SECRETS-03 | TODO | Use Surface.Secrets to retrieve registry credentials when interacting with CAS/referrers. Dependencies: SCANNER-SECRETS-02. | BuildX Plugin Guild, Security Guild (src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md) -SCANNER-SURFACE-01 | TODO | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments. | Scanner Worker Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md) -SCANNER-SURFACE-02 | TODO | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata. Dependencies: SCANNER-SURFACE-01. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) +SCANNER-ENG-0020 | TODO | Implement Homebrew collector & fragment mapper per `design/macos-analyzer.md` §3.1. | Scanner Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0021 | TODO | Implement pkgutil receipt collector per `design/macos-analyzer.md` §3.2. | Scanner Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0022 | TODO | Implement macOS bundle inspector & capability overlays per `design/macos-analyzer.md` §3.3. | Scanner Guild, Policy Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0023 | TODO | Deliver macOS policy/offline integration per `design/macos-analyzer.md` §5–6. | Scanner Guild, Offline Kit Guild, Policy Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0024 | TODO | Implement Windows MSI collector per `design/windows-analyzer.md` §3.1. | Scanner Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0025 | TODO | Implement WinSxS manifest collector per `design/windows-analyzer.md` §3.2. | Scanner Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0026 | TODO | Implement Windows Chocolatey & registry collectors per `design/windows-analyzer.md` §3.3–3.4. | Scanner Guild (docs/modules/scanner/TASKS.md) +SCANNER-ENG-0027 | TODO | Deliver Windows policy/offline integration per `design/windows-analyzer.md` §5–6. | Scanner Guild, Policy Guild, Offline Kit Guild (docs/modules/scanner/TASKS.md) +SCANNER-SURFACE-01 | DOING (2025-11-02) | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments.
2025-11-02: Worker pipeline emitting draft Surface.FS manifests for sample scans; determinism checks running. | Scanner Worker Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md) +SCANNER-SURFACE-02 | DOING (2025-11-02) | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata. Dependencies: SCANNER-SURFACE-01.
2025-11-02: WebService responses now include preview CAS URIs; attestation metadata updates staged for review. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md) SCANNER-SURFACE-03 | TODO | Push layer manifests and entry fragments into Surface.FS during build-time SBOM generation. Dependencies: SCANNER-SURFACE-02. | BuildX Plugin Guild (src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md) [Scanner & Surface] 130.A) Scanner.VIII diff --git a/docs/implplan/SPRINT_170_notifications_telemetry.md b/docs/implplan/SPRINT_170_notifications_telemetry.md index 3074ca6a..178e5ff1 100644 --- a/docs/implplan/SPRINT_170_notifications_telemetry.md +++ b/docs/implplan/SPRINT_170_notifications_telemetry.md @@ -5,6 +5,7 @@ Depends on: Sprint 150.A - Orchestrator Summary: Notifications & Telemetry focus on Notifier (phase I). Task ID | State | Task description | Owners (Source) --- | --- | --- | --- +NOTIFY-DOC-70-001 | DONE | Record architecture decision to keep `src/Notify` (shared libraries) and `src/Notifier` (host runtime) separate; capture rationale in notifications docs. Notes added 2025-11-02. | Notifications Service Guild (src/Notifier/StellaOps.Notifier/TASKS.md) NOTIFY-AIRGAP-56-001 | TODO | Disable external webhook targets in sealed mode, default to enclave-safe channels (SMTP relay, syslog, file sink), and surface remediation guidance. | Notifications Service Guild (src/Notifier/StellaOps.Notifier/TASKS.md) NOTIFY-AIRGAP-56-002 | TODO | Provide local notifier configurations bundled within Bootstrap Pack with deterministic secrets handling. Dependencies: NOTIFY-AIRGAP-56-001. | Notifications Service Guild, DevOps Guild (src/Notifier/StellaOps.Notifier/TASKS.md) NOTIFY-AIRGAP-57-001 | TODO | Send staleness drift and bundle import notifications with remediation steps. Dependencies: NOTIFY-AIRGAP-56-002. | Notifications Service Guild, AirGap Time Guild (src/Notifier/StellaOps.Notifier/TASKS.md) diff --git a/docs/implplan/SPRINT_190_ops_offline.md b/docs/implplan/SPRINT_190_ops_offline.md index 9d29ecc5..9f199bbe 100644 --- a/docs/implplan/SPRINT_190_ops_offline.md +++ b/docs/implplan/SPRINT_190_ops_offline.md @@ -136,9 +136,9 @@ DEVOPS-VEX-30-001 | TODO | Provision CI, load tests, dashboards, alerts for VEX DEVOPS-VULN-29-001 | TODO | Provision CI jobs for ledger projector (replay, determinism), set up backups, monitor Merkle anchoring, and automate verification. | DevOps Guild, Findings Ledger Guild (ops/devops/TASKS.md) DEVOPS-VULN-29-002 | TODO | Configure load/perf tests (5M findings/tenant), query budget enforcement, API SLO dashboards, and alerts for `vuln_list_latency` and `projection_lag`. Dependencies: DEVOPS-VULN-29-001. | DevOps Guild, Vuln Explorer API Guild (ops/devops/TASKS.md) DEVOPS-VULN-29-003 | TODO | Instrument analytics pipeline for Vuln Explorer (telemetry ingestion, query hashes), ensure compliance with privacy/PII guardrails, and update observability docs. Dependencies: DEVOPS-VULN-29-002. | DevOps Guild, Console Guild (ops/devops/TASKS.md) -DOCKER-44-001 | TODO | Author multi-stage Dockerfiles for all core services (API, Console, Orchestrator, Task Runner, Conseiller, Excitator, Policy, Notify, Export, AI) with non-root users, read-only file systems, and health scripts. | DevOps Guild, Service Owners (ops/devops/TASKS.md) +DOCKER-44-001 | TODO | Author multi-stage Dockerfiles for all core services (API, Console, Orchestrator, Task Runner, Conseiller, Excitor, Policy, Notify, Export, AI) with non-root users, read-only file systems, and health scripts. | DevOps Guild, Service Owners (ops/devops/TASKS.md) DOCKER-44-002 | TODO | Generate SBOMs and cosign attestations for each image and integrate verification into CI. Dependencies: DOCKER-44-001. | DevOps Guild (ops/devops/TASKS.md) -DOCKER-44-003 | TODO | Implement `/health/liveness`, `/health/readiness`, `/version`, `/metrics`, and ensure capability endpoint returns `merge=false` for Conseiller/Excitator. Dependencies: DOCKER-44-002. | DevOps Guild (ops/devops/TASKS.md) +DOCKER-44-003 | TODO | Implement `/health/liveness`, `/health/readiness`, `/version`, `/metrics`, and ensure capability endpoint returns `merge=false` for Conseiller/Excitor. Dependencies: DOCKER-44-002. | DevOps Guild (ops/devops/TASKS.md) OPS-ENV-01 | TODO | Update deployment manifests (Helm/Compose) and configuration docs to include Surface.Env variables for Scanner and Zastava services. | DevOps Guild, Scanner Guild (ops/devops/TASKS.md) OPS-SECRETS-01 | TODO | Define secret provisioning workflow (Kubernetes, Compose, Offline Kit) for Surface.Secrets references and update runbooks. | DevOps Guild, Security Guild (ops/devops/TASKS.md) OPS-SECRETS-02 | TODO | Embed Surface.Secrets material (encrypted bundles, manifests) into offline kit packaging scripts. Dependencies: OPS-SECRETS-01. | DevOps Guild, Offline Kit Guild (ops/devops/TASKS.md) diff --git a/docs/implplan/SPRINT_200_documentation_process.md b/docs/implplan/SPRINT_200_documentation_process.md index 05f7f3e2..bebd813b 100644 --- a/docs/implplan/SPRINT_200_documentation_process.md +++ b/docs/implplan/SPRINT_200_documentation_process.md @@ -103,7 +103,8 @@ DOCS-LNM-22-003 | BLOCKED (2025-10-27) | Update `/docs/api/advisories.md` and `/ DOCS-LNM-22-004 | TODO | Create `/docs/policy/effective-severity.md` detailing severity selection strategies from multiple sources. Dependencies: DOCS-LNM-22-003. | Docs Guild, Policy Guild (docs/TASKS.md) DOCS-LNM-22-005 | BLOCKED (2025-10-27) | Document `/docs/ui/evidence-panel.md` with screenshots, conflict badges, accessibility guidance. Dependencies: DOCS-LNM-22-004. | Docs Guild, UI Guild (docs/TASKS.md) DOCS-LNM-22-007 | TODO | Publish `/docs/observability/aggregation.md` with metrics/traces/logs/SLOs. Dependencies: DOCS-LNM-22-005. | Docs Guild, Observability Guild (docs/TASKS.md) -DOCS-LNM-22-008 | TODO | Write `/docs/migration/no-merge.md` describing migration plan, backfill steps, rollback, feature flags. Dependencies: DOCS-LNM-22-007. | Docs Guild, DevOps Guild (docs/TASKS.md) +DOCS-LNM-22-008 | DONE (2025-11-03) | Write `/docs/migration/no-merge.md` describing migration plan, backfill steps, rollback, feature flags. Dependencies: DOCS-LNM-22-007. | Docs Guild, DevOps Guild (docs/TASKS.md) +> 2025-11-03: Drafted and published `docs/migration/no-merge.md` covering rollout phases, backfill/validation workflow, rollback plan, and readiness checklist. DOCS-NOTIFY-40-001 | TODO | Publish `/docs/notifications/channels.md`, `/docs/notifications/escalations.md`, `/docs/notifications/api.md`, `/docs/operations/notifier-runbook.md`, `/docs/security/notifications-hardening.md`; each ends with imposed rule line. | Docs Guild, Security Guild (docs/TASKS.md) DOCS-OAS-61-001 | TODO | Publish `/docs/api/overview.md` covering auth, tenancy, pagination, idempotency, rate limits with banner. | Docs Guild, API Contracts Guild (docs/TASKS.md) DOCS-OAS-61-002 | TODO | Author `/docs/api/conventions.md` capturing naming, errors, filters, sorting, examples. Dependencies: DOCS-OAS-61-001. | Docs Guild, API Governance Guild (docs/TASKS.md) diff --git a/docs/migration/no-merge.md b/docs/migration/no-merge.md new file mode 100644 index 00000000..5ba6cd43 --- /dev/null +++ b/docs/migration/no-merge.md @@ -0,0 +1,141 @@ +# No-Merge Migration Playbook + +_Last updated: 2025-11-03_ + +This playbook guides the full retirement of the legacy Merge service (`AdvisoryMergeService`) in favour of Link-Not-Merge (LNM) observations plus linksets. It is written for the BE-Merge, Architecture, DevOps, and Docs guilds coordinating Sprint 110 (Ingestion & Evidence) deliverables, and it feeds CONCELIER-LNM-21-101 / MERGE-LNM-21-001 and downstream DOCS-LNM-22-008. + +## 0. Scope & objectives + +- **Primary goal:** cut over all advisory pipelines to Link-Not-Merge with no residual dependencies on `AdvisoryMergeService`. +- **Secondary goals:** maintain deterministic evidence, zero data loss, and reversible deployment across online and offline tenants. +- **Success criteria:** + - All connectors emit observation `affected.versions[]` with provenance and pass LNM guardrails. + - Linkset dashboards show zero `missing_version_entries_total` and no `Normalized version rules missing…` warnings. + - Policy, Export Center, and CLI consumers operate solely on observations/linksets. + - Rollback playbook validated and rehearsed in staging. + +## 1. Prerequisites checklist + +| Item | Owner | Notes | +| --- | --- | --- | +| Normalized version ranges emitted for all Sprint 110 connectors (`Acsc`, `Cccs`, `CertBund`, `CertCc`, `Cve`, `Ghsa`, `Ics.Cisa`, `Kisa`, `Ru.Bdu`, `Ru.Nkcki`, `Vndr.Apple`, `Vndr.Cisco`, `Vndr.Msrc`). | Connector guilds | Follow `docs/dev/normalized-rule-recipes.md`; update fixtures with `UPDATE_*_FIXTURES=1`. | +| Metrics dashboards (`LinksetVersionCoverage`, `Normalized version rules missing`) available in Grafana/CI snapshots. | Observability guild | Publish baseline before shadow rollout. | +| Concelier WebService exposes `linkset` and `observation` read APIs for policy/CLI consumers. | BE-Merge / Platform | Confirm contract parity with Merge outputs. | +| Export Center / Offline Kit aware of new manifests. | Export Center guild | Provide beta bundle for QA verification. | +| Docs guild aligned on public migration messaging. | Docs guild | Update `docs/dev`, `docs/modules/concelier`, and release notes once cutover date is locked. | + +Do not proceed to Phase 1 until all prerequisites are checked or explicitly waived by Architecture guild. + +## 2. Feature flag & configuration plan + +| Toggle | Default | Purpose | Notes | +| --- | --- | --- | --- | +| `concelier:features:noMergeEnabled` | `false` | Master switch to disable legacy Merge job scheduling/execution. | Applies to WebService + Worker; gate `AdvisoryMergeService` DI registration. | +| `concelier:features:lnmShadowWrites` | `true` | Enables dual-write of linksets while Merge remains active. | Keep enabled throughout Phase 0–1 to validate parity. | +| `concelier:jobs:merge:allowlist` | `[]` | Explicit allowlist for Merge jobs when noMergeEnabled is `false`. | Set to empty during Phase 2+ to prevent accidental restarts. | +| `policy:overlays:requireLinksetEvidence` | `false` | Policy engine safety net to require linkset-backed findings. | Flip to `true` only after cutover (Phase 2). | + +> **Configuration hygiene:** Document the toggle values per environment in `ops/devops/configuration/staging.md` and `ops/devops/configuration/production.md`. Air-gapped customers receive defaults through the Offline Kit release notes. + +## 3. Rollout phases + +| Phase | Goal | Duration | Key actions | +| --- | --- | --- | --- | +| **0 – Preparation** | Ensure readiness | 2–3 days | Finalise prerequisites, snapshot Merge metrics, dry-run backfill scripts in dev. | +| **1 – Shadow / Dual Write** | Validate parity | 5–7 days | Enable `lnmShadowWrites`, keep Merge primary. Compare linkset vs merged outputs using `stella concelier diff-merge --snapshot `; fix discrepancies. | +| **2 – Cutover** | Switch to LNM | 1 day (per env) | Enable `noMergeEnabled`, disable Merge job schedules, update Policy/Export configs, run post-cutover smoke tests. | +| **3 – Harden** | Decommission Merge | 2–3 days | Remove Merge background services, delete `merge_event` retention jobs, clean dashboards, notify operators. | + +### 3.1 Environment sequencing + +1. **Dev/Test clusters:** Validate all automation. Run full regression suite (`dotnet test src/Concelier/...`). +2. **Staging:** Execute complete backfill (see §4) and collect 24 h of telemetry before sign-off. +3. **Production:** Perform cutover during low-ingest window; communicate via Slack/email + status page two days in advance. +4. **Offline kit:** Package new Observer snapshots with LNM-only data; ensure instructions cover flag toggles for air-gapped deployments. + +### 3.2 Smoke test matrix + +- `stella concelier status --include linkset` returns healthy and shows zero Merge workers. +- `stella policy evaluate` against sample tenants produces identical findings pre/post cutover. +- Export Center bundle diff shows only expected metadata changes (manifest ID, timestamps). +- Grafana dashboards: `linkset_insert_duration_ms` steady, `merge.identity.conflicts` flatlined. + +## 4. Backfill strategy + +1. **Freeze Merge writes:** Pause Merge job scheduler (`MergeJobScheduler.PauseAsync`) to prevent new merge events while snapshots are taken. +2. **Generate linkset baseline:** Run `dotnet run --project src/Concelier/StellaOps.Concelier.WebService -- linkset backfill --from 2024-01-01` (or equivalent CLI job) to rebuild linksets from `advisory_raw`. Capture job output artefacts and attach to the sprint issue. +3. **Validate parity:** Use the internal diff tool (`tools/concelier/compare-linkset-merge.ps1`) to compare sample advisories. Any diffs must be triaged before production cutover. +4. **Publish evidence:** For air-gapped tenants, create a one-off Offline Kit slice (`export profile linkset-backfill`) and push to staging mirror. +5. **Tag snapshot:** Record Mongo `oplog` timestamp and S3/object storage manifests in `ops/devops/runbooks/concelier/no-merge.md` (new section) so rollback knows the safe point. + +> **Determinism:** rerunning the backfill with identical inputs must produce byte-identical linkset documents. Use the `--verify-determinism` flag where available and archive the checksum report under `artifacts/lnm-backfill//`. + +## 5. Validation gates + +- **Metrics:** `linkset_insert_duration_ms`, `linkset_documents_total`, `normalized_version_rules_missing`, `merge.identity.conflicts`. + - Gate: `normalized_version_rules_missing == 0` for 48 h before enabling `noMergeEnabled`. +- **Logs:** Ensure no occurrences of `Fallbacking to merge service` after cutover. +- **Change streams:** Policy and Scheduler should observe only `advisory.linkset.updated` events; monitor for stragglers referencing merge IDs. +- **QA:** Golden tests in `StellaOps.Concelier.Merge.Tests` updated to assert absence of merge outputs, plus integration tests verifying LNM-only exports. + +Capture validation evidence in the sprint journal (attach Grafana screenshots + CLI output). + +## 6. Rollback plan + +1. **Toggle sequence:** + - Set `concelier:features:noMergeEnabled=false`. + - Re-enable Merge job schedules (`concelier:jobs:merge:allowlist=["merge:default"]`). + - Disable `policy:overlays:requireLinksetEvidence`. +2. **Data considerations:** + - Linkset writes continue, so no data is lost; ensure Policy consumers ignore linkset-only fields during rollback window. + - If Merge pipeline was fully removed (Phase 3 complete), redeploy the Merge service container image from the `rollback` tag published before cutover. +3. **Verification:** + - Run `stella concelier status` to confirm Merge workers active. + - Monitor `merge.identity.conflicts` for spikes; if present, roll forward and re-open incident with Architecture guild. +4. **Communication:** + - Post incident note in #release-infra and customer status page. + - Log rollback reason, window, and configs in `ops/devops/incidents/-no-merge.md`. + +Rollback window should not exceed 4 hours; beyond that, plan to roll forward with a hotfix rather than reintroducing Merge. + +## 7. Documentation & communications + +- Update `docs/modules/concelier/architecture.md` appendix to mark Merge deprecated and link back to this playbook. +- Coordinate with Docs guild to publish operator-facing guidance (`docs/releases/2025-q4.md`) and update CLI help text. +- Notify product/CS teams with a short FAQ covering timelines, customer impact, and steps for self-hosted installations. + +## 8. Responsibilities matrix + +| Area | Lead guild(s) | Supporting | +| --- | --- | --- | +| Feature flags & config | BE-Merge | DevOps | +| Backfill scripting | BE-Merge | Tools | +| Observability dashboards | Observability | QA | +| Offline kit packaging | Export Center | AirGap | +| Customer comms | Docs | Product, Support | + +## 9. Deliverables & artefacts + +- Config diff per environment (stored in GitOps repo). +- Backfill checksum report (`artifacts/lnm-backfill//checksums.json`). +- Grafana export (PDF) showing validation metrics. +- QA test run attesting to LNM-only regressions passing. +- Updated runbook entry in `ops/devops/runbooks/concelier/`. + +--- + +## 10. Migration readiness checklist + +| Item | Primary owner | Status notes | +| --- | --- | --- | +| Capture Linkset coverage baselines (`version_entries_total`, `missing_version_entries_total`) and archive Grafana export. | Observability Guild | [ ] Pending | +| Stage and verify linkset backfill using `linkset backfill` job; store checksum report under `artifacts/lnm-backfill//`. | BE-Merge, DevOps Guild | [ ] Pending | +| Confirm feature flags per environment (`noMergeEnabled`, `lnmShadowWrites`, `policy:overlays:requireLinksetEvidence`) match Phase 0–3 plan. | DevOps Guild | [ ] Pending | +| Publish operator comms (status page, Slack/email) with cutover + rollback windows. | Docs Guild, Product | [ ] Pending | +| Execute rollback rehearsal in staging and log results in `ops/devops/incidents/-no-merge.md`. | DevOps Guild, Architecture Guild | [ ] Pending | + +> Update the checklist as each item completes; completion of every row is required before moving to Phase 2 (Cutover). + +--- + +With this playbook completed, proceed to MERGE-LNM-21-002 to remove the Merge service code paths and enforce compile-time analyzers that block new merge dependencies. diff --git a/docs/modules/authority/architecture.md b/docs/modules/authority/architecture.md index bb206cf2..4a27ab61 100644 --- a/docs/modules/authority/architecture.md +++ b/docs/modules/authority/architecture.md @@ -146,6 +146,48 @@ plan? = // optional hint for UIs; not used for e --- +### 3.5 Vuln Explorer workflow safeguards + +* **Anti-forgery flow** — Vuln Explorer’s mutation verbs call + * `POST /vuln/workflow/anti-forgery/issue` + * `POST /vuln/workflow/anti-forgery/verify` + + Callers must hold `vuln:operate` scopes. Issued tokens embed the actor, tenant, whitelisted actions, ABAC selectors (environment/owner/business tier), and optional context key/value pairs. Tokens are EdDSA/ES256 signed via the primary Authority signing key and default to a 10‑minute TTL (cap: 30 minutes). Verification enforces nonce reuse prevention, tenant match, and action membership before forwarding the request to Vuln Explorer. + +* **Attachment access** — Evidence bundles and attachments reference a ledger hash. Vuln Explorer obtains a scoped download token through: + * `POST /vuln/attachments/tokens/issue` + * `POST /vuln/attachments/tokens/verify` + + These tokens bind the ledger event hash, attachment identifier, optional finding/content metadata, and the actor. They default to a 30‑minute TTL (cap: 4 hours) and require `vuln:investigate`. + +* **Audit trail** — Both flows emit `vuln.workflow.csrf.*` and `vuln.attachment.token.*` audit records with tenant, actor, ledger hash, nonce, and filtered context metadata so Offline Kit operators can reconcile actions against ledger entries. + +* **Configuration** + + ```yaml + authority: + vulnerabilityExplorer: + workflow: + antiForgery: + enabled: true + audience: "stellaops:vuln-workflow" + defaultLifetime: "00:10:00" + maxLifetime: "00:30:00" + maxContextEntries: 16 + maxContextValueLength: 256 + attachments: + enabled: true + defaultLifetime: "00:30:00" + maxLifetime: "04:00:00" + payloadType: "application/vnd.stellaops.vuln-attachment-token+json" + maxMetadataEntries: 16 + maxMetadataValueLength: 512 + ``` + + Air-gapped bundles include the signing key material and policy snapshots required to validate these tokens offline. + +--- + ## 4) Audiences, scopes & RBAC ### 4.1 Audiences diff --git a/docs/modules/vexer/AGENTS.md b/docs/modules/excitor/AGENTS.md similarity index 85% rename from docs/modules/vexer/AGENTS.md rename to docs/modules/excitor/AGENTS.md index 67a49e5a..57826c5c 100644 --- a/docs/modules/vexer/AGENTS.md +++ b/docs/modules/excitor/AGENTS.md @@ -1,7 +1,7 @@ -# Vexer agent guide +# Excitor agent guide ## Mission -Vexer computes deterministic consensus across VEX claims, preserving conflicts and producing attestable evidence for policy suppression. +Excitor computes deterministic consensus across VEX claims, preserving conflicts and producing attestable evidence for policy suppression. ## Key docs - [Module README](./README.md) @@ -21,9 +21,9 @@ Vexer computes deterministic consensus across VEX claims, preserving conflicts a - Keep Offline Kit parity in mind—document air-gapped workflows for any new feature. - Update runbooks/observability assets when operational characteristics change. ## Required Reading -- `docs/modules/vexer/README.md` -- `docs/modules/vexer/architecture.md` -- `docs/modules/vexer/implementation_plan.md` +- `docs/modules/excitor/README.md` +- `docs/modules/excitor/architecture.md` +- `docs/modules/excitor/implementation_plan.md` - `docs/modules/platform/architecture-overview.md` ## Working Agreement diff --git a/docs/modules/vexer/README.md b/docs/modules/excitor/README.md similarity index 76% rename from docs/modules/vexer/README.md rename to docs/modules/excitor/README.md index dce1601a..59e40ec5 100644 --- a/docs/modules/vexer/README.md +++ b/docs/modules/excitor/README.md @@ -1,34 +1,34 @@ -# StellaOps Vexer - -Vexer computes deterministic consensus across VEX claims, preserving conflicts and producing attestable evidence for policy suppression. - -## Responsibilities -- Ingest Excititor observations and compute per-product consensus snapshots. -- Provide APIs for querying canonical VEX positions and conflict sets. -- Publish exports and DSSE-ready digests for downstream consumption. -- Keep provenance weights and disagreement metadata. - -## Key components -- Consensus engine and API host in `StellaOps.Vexer.*` (to-be-implemented). -- Storage schema for consensus graphs. -- Integration hooks for Policy Engine suppression logic. - -## Integrations & dependencies -- Excititor for raw observations. -- Policy Engine and UI for suppression stories. -- CLI for evidence inspection. - -## Operational notes -- Deterministic consensus algorithms (see architecture). -- Planned telemetry for disagreement counts and freshness. -- Offline exports aligning with Concelier/Excititor timelines. - -## Related resources -- ./scoring.md - -## Backlog references -- DOCS-VEXER backlog referenced in architecture doc. -- CLI parity tracked in ../../TASKS.md (CLI-GRAPH/VEX stories). - -## Epic alignment -- **Epic 7 – VEX Consensus Lens:** deliver trust-weighted consensus snapshots, disagreement metadata, and explain APIs. +# StellaOps Excitor + +Excitor computes deterministic consensus across VEX claims, preserving conflicts and producing attestable evidence for policy suppression. + +## Responsibilities +- Ingest Excititor observations and compute per-product consensus snapshots. +- Provide APIs for querying canonical VEX positions and conflict sets. +- Publish exports and DSSE-ready digests for downstream consumption. +- Keep provenance weights and disagreement metadata. + +## Key components +- Consensus engine and API host in `StellaOps.Excitor.*` (to-be-implemented). +- Storage schema for consensus graphs. +- Integration hooks for Policy Engine suppression logic. + +## Integrations & dependencies +- Excititor for raw observations. +- Policy Engine and UI for suppression stories. +- CLI for evidence inspection. + +## Operational notes +- Deterministic consensus algorithms (see architecture). +- Planned telemetry for disagreement counts and freshness. +- Offline exports aligning with Concelier/Excititor timelines. + +## Related resources +- ./scoring.md + +## Backlog references +- DOCS-EXCITOR backlog referenced in architecture doc. +- CLI parity tracked in ../../TASKS.md (CLI-GRAPH/VEX stories). + +## Epic alignment +- **Epic 7 – VEX Consensus Lens:** deliver trust-weighted consensus snapshots, disagreement metadata, and explain APIs. diff --git a/docs/modules/excitor/TASKS.md b/docs/modules/excitor/TASKS.md new file mode 100644 index 00000000..bf5d0255 --- /dev/null +++ b/docs/modules/excitor/TASKS.md @@ -0,0 +1,9 @@ +# Task board — Excitor + +> Local tasks should link back to ./AGENTS.md and mirror status updates into ../../TASKS.md when applicable. + +| ID | Status | Owner(s) | Description | Notes | +|----|--------|----------|-------------|-------| +| EXCITOR-DOCS-0001 | DOING (2025-10-29) | Docs Guild | Validate that ./README.md aligns with the latest release notes. | See ./AGENTS.md | +| EXCITOR-OPS-0001 | TODO | Ops Guild | Review runbooks/observability assets after next sprint demo. | Sync outcomes back to ../../TASKS.md | +| EXCITOR-ENG-0001 | TODO | Module Team | Cross-check implementation plan milestones against ../../implplan/SPRINTS.md. | Update status via ./AGENTS.md workflow | diff --git a/docs/modules/vexer/architecture.md b/docs/modules/excitor/architecture.md similarity index 89% rename from docs/modules/vexer/architecture.md rename to docs/modules/excitor/architecture.md index e12e864f..124624b3 100644 --- a/docs/modules/vexer/architecture.md +++ b/docs/modules/excitor/architecture.md @@ -1,465 +1,465 @@ -# component_architecture_vexer.md — **Stella Ops Vexer** (2025Q4) +# component_architecture_excitor.md — **Stella Ops Excitor** (2025Q4) > Built to satisfy Epic 7 – VEX Consensus Lens requirements. - -> **Scope.** This document specifies the **Vexer** service: its purpose, trust model, data structures, APIs, plug‑in contracts, storage schema, normalization/consensus algorithms, performance budgets, testing matrix, and how it integrates with Scanner, Policy, Feedser, and the attestation chain. It is implementation‑ready. - ---- - -## 0) Mission & role in the platform - -**Mission.** Convert heterogeneous **VEX** statements (OpenVEX, CSAF VEX, CycloneDX VEX; vendor/distro/platform sources) into **canonical, queryable claims**; compute **deterministic consensus** per *(vuln, product)*; preserve **conflicts with provenance**; publish **stable, attestable exports** that the backend uses to suppress non‑exploitable findings, prioritize remaining risk, and explain decisions. - -**Boundaries.** - -* Vexer **does not** decide PASS/FAIL. It supplies **evidence** (statuses + justifications + provenance weights). -* Vexer preserves **conflicting claims** unchanged; consensus encodes how we would pick, but the raw set is always exportable. -* VEX consumption is **backend‑only**: Scanner never applies VEX. The backend’s **Policy Engine** asks Vexer for status evidence and then decides what to show. - ---- - -## 1) Inputs, outputs & canonical domain - -### 1.1 Accepted input formats (ingest) - -* **OpenVEX** JSON documents (attested or raw). -* **CSAF VEX** 2.x (vendor PSIRTs and distros commonly publish CSAF). -* **CycloneDX VEX** 1.4+ (standalone VEX or embedded VEX blocks). -* **OCI‑attached attestations** (VEX statements shipped as OCI referrers) — optional connectors. - -All connectors register **source metadata**: provider identity, trust tier, signature expectations (PGP/cosign/PKI), fetch windows, rate limits, and time anchors. - -### 1.2 Canonical model (normalized) - -Every incoming statement becomes a set of **VexClaim** records: - -``` -VexClaim -- providerId // 'redhat', 'suse', 'ubuntu', 'github', 'vendorX' -- vulnId // 'CVE-2025-12345', 'GHSA-xxxx', canonicalized -- productKey // canonical product identity (see §2.2) -- status // affected | not_affected | fixed | under_investigation -- justification? // for 'not_affected'/'affected' where provided -- introducedVersion? // semantics per provider (range or exact) -- fixedVersion? // where provided (range or exact) -- lastObserved // timestamp from source or fetch time -- provenance // doc digest, signature status, fetch URI, line/offset anchors -- evidence[] // raw source snippets for explainability -- supersedes? // optional cross-doc chain (docDigest → docDigest) -``` - -### 1.3 Exports (consumption) - -* **VexConsensus** per `(vulnId, productKey)` with: - - * `rollupStatus` (after policy weights/justification gates), - * `sources[]` (winning + losing claims with weights & reasons), - * `policyRevisionId` (identifier of the Vexer policy used), - * `consensusDigest` (stable SHA‑256 over canonical JSON). -* **Raw claims** export for auditing (unchanged, with provenance). -* **Provider snapshots** (per source, last N days) for operator debugging. -* **Index** optimized for backend joins: `(productKey, vulnId) → (status, confidence, sourceSet)`. - -All exports are **deterministic**, and (optionally) **attested** via DSSE and logged to Rekor v2. - ---- - -## 2) Identity model — products & joins - -### 2.1 Vuln identity - -* Accepts **CVE**, **GHSA**, vendor IDs (MSRC, RHSA…), distro IDs (DSA/USN/RHSA…) — normalized to `vulnId` with alias sets. -* **Alias graph** maintained (from Feedser) to map vendor/distro IDs → CVE (primary) and to **GHSA** where applicable. - -### 2.2 Product identity (`productKey`) - -* **Primary:** `purl` (Package URL). -* **Secondary links:** `cpe`, **OS package NVRA/EVR**, NuGet/Maven/Golang identity, and **OS package name** when purl unavailable. -* **Fallback:** `oci:/@` for image‑level VEX. -* **Special cases:** kernel modules, firmware, platforms → provider‑specific mapping helpers (connector captures provider’s product taxonomy → canonical `productKey`). - -> Vexer does not invent identities. If a provider cannot be mapped to purl/CPE/NVRA deterministically, we keep the native **product string** and mark the claim as **non‑joinable**; the backend will ignore it unless a policy explicitly whitelists that provider mapping. - ---- - -## 3) Storage schema (MongoDB) - -Database: `vexer` - -### 3.1 Collections - -**`vex.providers`** - -``` -_id: providerId -name, homepage, contact -trustTier: enum {vendor, distro, platform, hub, attestation} -signaturePolicy: { type: pgp|cosign|x509|none, keys[], certs[], cosignKeylessRoots[] } -fetch: { baseUrl, kind: http|oci|file, rateLimit, etagSupport, windowDays } -enabled: bool -createdAt, modifiedAt -``` - -**`vex.raw`** (immutable raw documents) - -``` -_id: sha256(doc bytes) -providerId -uri -ingestedAt -contentType -sig: { verified: bool, method: pgp|cosign|x509|none, keyId|certSubject, bundle? } -payload: GridFS pointer (if large) -disposition: kept|replaced|superseded -correlation: { replaces?: sha256, replacedBy?: sha256 } -``` - -**`vex.claims`** (normalized rows; dedupe on providerId+vulnId+productKey+docDigest) - -``` -_id -providerId -vulnId -productKey -status -justification? -introducedVersion? -fixedVersion? -lastObserved -docDigest -provenance { uri, line?, pointer?, signatureState } -evidence[] { key, value, locator } -indices: - - {vulnId:1, productKey:1} - - {providerId:1, lastObserved:-1} - - {status:1} - - text index (optional) on evidence.value for debugging -``` - -**`vex.consensus`** (rollups) - -``` -_id: sha256(canonical(vulnId, productKey, policyRevision)) -vulnId -productKey -rollupStatus -sources[]: [ - { providerId, status, justification?, weight, lastObserved, accepted:bool, reason } -] -policyRevisionId -evaluatedAt -consensusDigest // same as _id -indices: - - {vulnId:1, productKey:1} - - {policyRevisionId:1, evaluatedAt:-1} -``` - -**`vex.exports`** (manifest of emitted artifacts) - -``` -_id -querySignature -format: raw|consensus|index -artifactSha256 -rekor { uuid, index, url }? -createdAt -policyRevisionId -cacheable: bool -``` - -**`vex.cache`** - -``` -querySignature -> exportId (for fast reuse) -ttl, hits -``` - -**`vex.migrations`** - -* ordered migrations applied at bootstrap to ensure indexes. - -### 3.2 Indexing strategy - -* Hot path queries use exact `(vulnId, productKey)` and time‑bounded windows; compound indexes cover both. -* Providers list view by `lastObserved` for monitoring staleness. -* `vex.consensus` keyed by `(vulnId, productKey, policyRevision)` for deterministic reuse. - ---- - -## 4) Ingestion pipeline - -### 4.1 Connector contract - -```csharp -public interface IVexConnector -{ - string ProviderId { get; } - Task FetchAsync(VexConnectorContext ctx, CancellationToken ct); // raw docs - Task NormalizeAsync(VexConnectorContext ctx, CancellationToken ct); // raw -> VexClaim[] -} -``` - -* **Fetch** must implement: window scheduling, conditional GET (ETag/If‑Modified‑Since), rate limiting, retry/backoff. -* **Normalize** parses the format, validates schema, maps product identities deterministically, emits `VexClaim` records with **provenance**. - -### 4.2 Signature verification (per provider) - -* **cosign (keyless or keyful)** for OCI referrers or HTTP‑served JSON with Sigstore bundles. -* **PGP** (provider keyrings) for distro/vendor feeds that sign docs. -* **x509** (mutual TLS / provider‑pinned certs) where applicable. -* Signature state is stored on **vex.raw.sig** and copied into **provenance.signatureState** on claims. - -> Claims from sources failing signature policy are marked `"signatureState.verified=false"` and **policy** can down‑weight or ignore them. - -### 4.3 Time discipline - -* For each doc, prefer **provider’s document timestamp**; if absent, use fetch time. -* Claims carry `lastObserved` which drives **tie‑breaking** within equal weight tiers. - ---- - -## 5) Normalization: product & status semantics - -### 5.1 Product mapping - -* **purl** first; **cpe** second; OS package NVRA/EVR mapping helpers (distro connectors) produce purls via canonical tables (e.g., rpm→purl:rpm, deb→purl:deb). -* Where a provider publishes **platform‑level** VEX (e.g., “RHEL 9 not affected”), connectors expand to known product inventory rules (e.g., map to sets of packages/components shipped in the platform). Expansion tables are versioned and kept per provider; every expansion emits **evidence** indicating the rule applied. -* If expansion would be speculative, the claim remains **platform‑scoped** with `productKey="platform:redhat:rhel:9"` and is flagged **non‑joinable**; backend can decide to use platform VEX only when Scanner proves the platform runtime. - -### 5.2 Status + justification mapping - -* Canonical **status**: `affected | not_affected | fixed | under_investigation`. -* **Justifications** normalized to a controlled vocabulary (CISA‑aligned), e.g.: - - * `component_not_present` - * `vulnerable_code_not_in_execute_path` - * `vulnerable_configuration_unused` - * `inline_mitigation_applied` - * `fix_available` (with `fixedVersion`) - * `under_investigation` -* Providers with free‑text justifications are mapped by deterministic tables; raw text preserved as `evidence`. - ---- - -## 6) Consensus algorithm - -**Goal:** produce a **stable**, explainable `rollupStatus` per `(vulnId, productKey)` given possibly conflicting claims. - -### 6.1 Inputs - -* Set **S** of `VexClaim` for the key. -* **Vexer policy snapshot**: - - * **weights** per provider tier and per provider overrides. - * **justification gates** (e.g., require justification for `not_affected` to be acceptable). - * **minEvidence** rules (e.g., `not_affected` must come from ≥1 vendor or 2 distros). - * **signature requirements** (e.g., require verified signature for ‘fixed’ to be considered). - -### 6.2 Steps - -1. **Filter invalid** claims by signature policy & justification gates → set `S'`. -2. **Score** each claim: - `score = weight(provider) * freshnessFactor(lastObserved)` where freshnessFactor ∈ [0.8, 1.0] for staleness decay (configurable; small effect). -3. **Aggregate** scores per status: `W(status) = Σ score(claims with that status)`. -4. **Pick** `rollupStatus = argmax_status W(status)`. -5. **Tie‑breakers** (in order): - - * Higher **max single** provider score wins (vendor > distro > platform > hub). - * More **recent** lastObserved wins. - * Deterministic lexicographic order of status (`fixed` > `not_affected` > `under_investigation` > `affected`) as final tiebreaker. -6. **Explain**: mark accepted sources (`accepted=true; reason="weight"`/`"freshness"`), mark rejected sources with explicit `reason` (`"insufficient_justification"`, `"signature_unverified"`, `"lower_weight"`). - -> The algorithm is **pure** given S and policy snapshot; result is reproducible and hashed into `consensusDigest`. - ---- - -## 7) Query & export APIs - -All endpoints are versioned under `/api/v1/vex`. - -### 7.1 Query (online) - -``` -POST /claims/search - body: { vulnIds?: string[], productKeys?: string[], providers?: string[], since?: timestamp, limit?: int, pageToken?: string } - → { claims[], nextPageToken? } - -POST /consensus/search - body: { vulnIds?: string[], productKeys?: string[], policyRevisionId?: string, since?: timestamp, limit?: int, pageToken?: string } - → { entries[], nextPageToken? } - -POST /excititor/resolve (scope: vex.read) - body: { productKeys?: string[], purls?: string[], vulnerabilityIds: string[], policyRevisionId?: string } - → { policy, resolvedAt, results: [ { vulnerabilityId, productKey, status, sources[], conflicts[], decisions[], signals?, summary?, envelope: { artifact, contentSignature?, attestation?, attestationEnvelope?, attestationSignature? } } ] } -``` - -### 7.2 Exports (cacheable snapshots) - -``` -POST /exports - body: { signature: { vulnFilter?, productFilter?, providers?, since? }, format: raw|consensus|index, policyRevisionId?: string, force?: bool } - → { exportId, artifactSha256, rekor? } - -GET /exports/{exportId} → bytes (application/json or binary index) -GET /exports/{exportId}/meta → { signature, policyRevisionId, createdAt, artifactSha256, rekor? } -``` - -### 7.3 Provider operations - -``` -GET /providers → provider list & signature policy -POST /providers/{id}/refresh → trigger fetch/normalize window -GET /providers/{id}/status → last fetch, doc counts, signature stats -``` - -**Auth:** service‑to‑service via Authority tokens; operator operations via UI/CLI with RBAC. - ---- - -## 8) Attestation integration - -* Exports can be **DSSE‑signed** via **Signer** and logged to **Rekor v2** via **Attestor** (optional but recommended for regulated pipelines). -* `vex.exports.rekor` stores `{uuid, index, url}` when present. -* **Predicate type**: `https://stella-ops.org/attestations/vex-export/1` with fields: - - * `querySignature`, `policyRevisionId`, `artifactSha256`, `createdAt`. - ---- - -## 9) Configuration (YAML) - -```yaml -vexer: - mongo: { uri: "mongodb://mongo/vexer" } - s3: - endpoint: http://minio:9000 - bucket: stellaops - policy: - weights: - vendor: 1.0 - distro: 0.9 - platform: 0.7 - hub: 0.5 - attestation: 0.6 - providerOverrides: - redhat: 1.0 - suse: 0.95 - requireJustificationForNotAffected: true - signatureRequiredForFixed: true - minEvidence: - not_affected: - vendorOrTwoDistros: true - connectors: - - providerId: redhat - kind: csaf - baseUrl: https://access.redhat.com/security/data/csaf/v2/ - signaturePolicy: { type: pgp, keys: [ "…redhat-pgp-key…" ] } - windowDays: 7 - - providerId: suse - kind: csaf - baseUrl: https://ftp.suse.com/pub/projects/security/csaf/ - signaturePolicy: { type: pgp, keys: [ "…suse-pgp-key…" ] } - - providerId: ubuntu - kind: openvex - baseUrl: https://…/vex/ - signaturePolicy: { type: none } - - providerId: vendorX - kind: cyclonedx-vex - ociRef: ghcr.io/vendorx/vex@sha256:… - signaturePolicy: { type: cosign, cosignKeylessRoots: [ "sigstore-root" ] } -``` - ---- - -## 10) Security model - -* **Input signature verification** enforced per provider policy (PGP, cosign, x509). -* **Connector allowlists**: outbound fetch constrained to configured domains. -* **Tenant isolation**: per‑tenant DB prefixes or separate DBs; per‑tenant S3 prefixes; per‑tenant policies. -* **AuthN/Z**: Authority‑issued OpToks; RBAC roles (`vex.read`, `vex.admin`, `vex.export`). -* **No secrets in logs**; deterministic logging contexts include providerId, docDigest, claim keys. - ---- - -## 11) Performance & scale - -* **Targets:** - - * Normalize 10k VEX claims/minute/core. - * Consensus compute ≤ 50 ms for 1k unique `(vuln, product)` pairs in hot cache. - * Export (consensus) 1M rows in ≤ 60 s on 8 cores with streaming writer. - -* **Scaling:** - - * WebService handles control APIs; **Worker** background services (same image) execute fetch/normalize in parallel with rate‑limits; Mongo writes batched; upserts by natural keys. - * Exports stream straight to S3 (MinIO) with rolling buffers. - -* **Caching:** - - * `vex.cache` maps query signatures → export; TTL to avoid stampedes; optimistic reuse unless `force`. - ---- - -## 12) Observability - -* **Metrics:** - - * `vex.ingest.docs_total{provider}` - * `vex.normalize.claims_total{provider}` - * `vex.signature.failures_total{provider,method}` - * `vex.consensus.conflicts_total{vulnId}` - * `vex.exports.bytes{format}` / `vex.exports.latency_seconds` -* **Tracing:** spans for fetch, verify, parse, map, consensus, export. -* **Dashboards:** provider staleness, top conflicting vulns/components, signature posture, export cache hit‑rate. - ---- - -## 13) Testing matrix - -* **Connectors:** golden raw docs → deterministic claims (fixtures per provider/format). -* **Signature policies:** valid/invalid PGP/cosign/x509 samples; ensure rejects are recorded but not accepted. -* **Normalization edge cases:** platform‑only claims, free‑text justifications, non‑purl products. -* **Consensus:** conflict scenarios across tiers; check tie‑breakers; justification gates. -* **Performance:** 1M‑row export timing; memory ceilings; stream correctness. -* **Determinism:** same inputs + policy → identical `consensusDigest` and export bytes. -* **API contract tests:** pagination, filters, RBAC, rate limits. - ---- - -## 14) Integration points - -* **Backend Policy Engine** (in Scanner.WebService): calls `POST /excititor/resolve` (scope `vex.read`) with batched `(purl, vulnId)` pairs to fetch `rollupStatus + sources`. -* **Feedser**: provides alias graph (CVE↔vendor IDs) and may supply VEX‑adjacent metadata (e.g., KEV flag) for policy escalation. -* **UI**: VEX explorer screens use `/claims/search` and `/consensus/search`; show conflicts & provenance. -* **CLI**: `stellaops vex export --consensus --since 7d --out vex.json` for audits. - ---- - -## 15) Failure modes & fallback - -* **Provider unreachable:** stale thresholds trigger warnings; policy can down‑weight stale providers automatically (freshness factor). -* **Signature outage:** continue to ingest but mark `signatureState.verified=false`; consensus will likely exclude or down‑weight per policy. -* **Schema drift:** unknown fields are preserved as `evidence`; normalization rejects only on **invalid identity** or **status**. - ---- - -## 16) Rollout plan (incremental) - -1. **MVP**: OpenVEX + CSAF connectors for 3 major providers (e.g., Red Hat/SUSE/Ubuntu), normalization + consensus + `/excititor/resolve`. -2. **Signature policies**: PGP for distros; cosign for OCI. -3. **Exports + optional attestation**. -4. **CycloneDX VEX** connectors; platform claim expansion tables; UI explorer. -5. **Scale hardening**: export indexes; conflict analytics. - ---- - -## 17) Appendix — canonical JSON (stable ordering) - -All exports and consensus entries are serialized via `VexCanonicalJsonSerializer`: - -* UTF‑8 without BOM; -* keys sorted (ASCII); -* arrays sorted by `(providerId, vulnId, productKey, lastObserved)` unless semantic order mandated; -* timestamps in `YYYY‑MM‑DDThh:mm:ssZ`; -* no insignificant whitespace. - + +> **Scope.** This document specifies the **Excitor** service: its purpose, trust model, data structures, APIs, plug‑in contracts, storage schema, normalization/consensus algorithms, performance budgets, testing matrix, and how it integrates with Scanner, Policy, Conselier, and the attestation chain. It is implementation‑ready. + +--- + +## 0) Mission & role in the platform + +**Mission.** Convert heterogeneous **VEX** statements (OpenVEX, CSAF VEX, CycloneDX VEX; vendor/distro/platform sources) into **canonical, queryable claims**; compute **deterministic consensus** per *(vuln, product)*; preserve **conflicts with provenance**; publish **stable, attestable exports** that the backend uses to suppress non‑exploitable findings, prioritize remaining risk, and explain decisions. + +**Boundaries.** + +* Excitor **does not** decide PASS/FAIL. It supplies **evidence** (statuses + justifications + provenance weights). +* Excitor preserves **conflicting claims** unchanged; consensus encodes how we would pick, but the raw set is always exportable. +* VEX consumption is **backend‑only**: Scanner never applies VEX. The backend’s **Policy Engine** asks Excitor for status evidence and then decides what to show. + +--- + +## 1) Inputs, outputs & canonical domain + +### 1.1 Accepted input formats (ingest) + +* **OpenVEX** JSON documents (attested or raw). +* **CSAF VEX** 2.x (vendor PSIRTs and distros commonly publish CSAF). +* **CycloneDX VEX** 1.4+ (standalone VEX or embedded VEX blocks). +* **OCI‑attached attestations** (VEX statements shipped as OCI referrers) — optional connectors. + +All connectors register **source metadata**: provider identity, trust tier, signature expectations (PGP/cosign/PKI), fetch windows, rate limits, and time anchors. + +### 1.2 Canonical model (normalized) + +Every incoming statement becomes a set of **VexClaim** records: + +``` +VexClaim +- providerId // 'redhat', 'suse', 'ubuntu', 'github', 'vendorX' +- vulnId // 'CVE-2025-12345', 'GHSA-xxxx', canonicalized +- productKey // canonical product identity (see §2.2) +- status // affected | not_affected | fixed | under_investigation +- justification? // for 'not_affected'/'affected' where provided +- introducedVersion? // semantics per provider (range or exact) +- fixedVersion? // where provided (range or exact) +- lastObserved // timestamp from source or fetch time +- provenance // doc digest, signature status, fetch URI, line/offset anchors +- evidence[] // raw source snippets for explainability +- supersedes? // optional cross-doc chain (docDigest → docDigest) +``` + +### 1.3 Exports (consumption) + +* **VexConsensus** per `(vulnId, productKey)` with: + + * `rollupStatus` (after policy weights/justification gates), + * `sources[]` (winning + losing claims with weights & reasons), + * `policyRevisionId` (identifier of the Excitor policy used), + * `consensusDigest` (stable SHA‑256 over canonical JSON). +* **Raw claims** export for auditing (unchanged, with provenance). +* **Provider snapshots** (per source, last N days) for operator debugging. +* **Index** optimized for backend joins: `(productKey, vulnId) → (status, confidence, sourceSet)`. + +All exports are **deterministic**, and (optionally) **attested** via DSSE and logged to Rekor v2. + +--- + +## 2) Identity model — products & joins + +### 2.1 Vuln identity + +* Accepts **CVE**, **GHSA**, vendor IDs (MSRC, RHSA…), distro IDs (DSA/USN/RHSA…) — normalized to `vulnId` with alias sets. +* **Alias graph** maintained (from Conselier) to map vendor/distro IDs → CVE (primary) and to **GHSA** where applicable. + +### 2.2 Product identity (`productKey`) + +* **Primary:** `purl` (Package URL). +* **Secondary links:** `cpe`, **OS package NVRA/EVR**, NuGet/Maven/Golang identity, and **OS package name** when purl unavailable. +* **Fallback:** `oci:/@` for image‑level VEX. +* **Special cases:** kernel modules, firmware, platforms → provider‑specific mapping helpers (connector captures provider’s product taxonomy → canonical `productKey`). + +> Excitor does not invent identities. If a provider cannot be mapped to purl/CPE/NVRA deterministically, we keep the native **product string** and mark the claim as **non‑joinable**; the backend will ignore it unless a policy explicitly whitelists that provider mapping. + +--- + +## 3) Storage schema (MongoDB) + +Database: `excitor` + +### 3.1 Collections + +**`vex.providers`** + +``` +_id: providerId +name, homepage, contact +trustTier: enum {vendor, distro, platform, hub, attestation} +signaturePolicy: { type: pgp|cosign|x509|none, keys[], certs[], cosignKeylessRoots[] } +fetch: { baseUrl, kind: http|oci|file, rateLimit, etagSupport, windowDays } +enabled: bool +createdAt, modifiedAt +``` + +**`vex.raw`** (immutable raw documents) + +``` +_id: sha256(doc bytes) +providerId +uri +ingestedAt +contentType +sig: { verified: bool, method: pgp|cosign|x509|none, keyId|certSubject, bundle? } +payload: GridFS pointer (if large) +disposition: kept|replaced|superseded +correlation: { replaces?: sha256, replacedBy?: sha256 } +``` + +**`vex.claims`** (normalized rows; dedupe on providerId+vulnId+productKey+docDigest) + +``` +_id +providerId +vulnId +productKey +status +justification? +introducedVersion? +fixedVersion? +lastObserved +docDigest +provenance { uri, line?, pointer?, signatureState } +evidence[] { key, value, locator } +indices: + - {vulnId:1, productKey:1} + - {providerId:1, lastObserved:-1} + - {status:1} + - text index (optional) on evidence.value for debugging +``` + +**`vex.consensus`** (rollups) + +``` +_id: sha256(canonical(vulnId, productKey, policyRevision)) +vulnId +productKey +rollupStatus +sources[]: [ + { providerId, status, justification?, weight, lastObserved, accepted:bool, reason } +] +policyRevisionId +evaluatedAt +consensusDigest // same as _id +indices: + - {vulnId:1, productKey:1} + - {policyRevisionId:1, evaluatedAt:-1} +``` + +**`vex.exports`** (manifest of emitted artifacts) + +``` +_id +querySignature +format: raw|consensus|index +artifactSha256 +rekor { uuid, index, url }? +createdAt +policyRevisionId +cacheable: bool +``` + +**`vex.cache`** + +``` +querySignature -> exportId (for fast reuse) +ttl, hits +``` + +**`vex.migrations`** + +* ordered migrations applied at bootstrap to ensure indexes. + +### 3.2 Indexing strategy + +* Hot path queries use exact `(vulnId, productKey)` and time‑bounded windows; compound indexes cover both. +* Providers list view by `lastObserved` for monitoring staleness. +* `vex.consensus` keyed by `(vulnId, productKey, policyRevision)` for deterministic reuse. + +--- + +## 4) Ingestion pipeline + +### 4.1 Connector contract + +```csharp +public interface IVexConnector +{ + string ProviderId { get; } + Task FetchAsync(VexConnectorContext ctx, CancellationToken ct); // raw docs + Task NormalizeAsync(VexConnectorContext ctx, CancellationToken ct); // raw -> VexClaim[] +} +``` + +* **Fetch** must implement: window scheduling, conditional GET (ETag/If‑Modified‑Since), rate limiting, retry/backoff. +* **Normalize** parses the format, validates schema, maps product identities deterministically, emits `VexClaim` records with **provenance**. + +### 4.2 Signature verification (per provider) + +* **cosign (keyless or keyful)** for OCI referrers or HTTP‑served JSON with Sigstore bundles. +* **PGP** (provider keyrings) for distro/vendor feeds that sign docs. +* **x509** (mutual TLS / provider‑pinned certs) where applicable. +* Signature state is stored on **vex.raw.sig** and copied into **provenance.signatureState** on claims. + +> Claims from sources failing signature policy are marked `"signatureState.verified=false"` and **policy** can down‑weight or ignore them. + +### 4.3 Time discipline + +* For each doc, prefer **provider’s document timestamp**; if absent, use fetch time. +* Claims carry `lastObserved` which drives **tie‑breaking** within equal weight tiers. + +--- + +## 5) Normalization: product & status semantics + +### 5.1 Product mapping + +* **purl** first; **cpe** second; OS package NVRA/EVR mapping helpers (distro connectors) produce purls via canonical tables (e.g., rpm→purl:rpm, deb→purl:deb). +* Where a provider publishes **platform‑level** VEX (e.g., “RHEL 9 not affected”), connectors expand to known product inventory rules (e.g., map to sets of packages/components shipped in the platform). Expansion tables are versioned and kept per provider; every expansion emits **evidence** indicating the rule applied. +* If expansion would be speculative, the claim remains **platform‑scoped** with `productKey="platform:redhat:rhel:9"` and is flagged **non‑joinable**; backend can decide to use platform VEX only when Scanner proves the platform runtime. + +### 5.2 Status + justification mapping + +* Canonical **status**: `affected | not_affected | fixed | under_investigation`. +* **Justifications** normalized to a controlled vocabulary (CISA‑aligned), e.g.: + + * `component_not_present` + * `vulnerable_code_not_in_execute_path` + * `vulnerable_configuration_unused` + * `inline_mitigation_applied` + * `fix_available` (with `fixedVersion`) + * `under_investigation` +* Providers with free‑text justifications are mapped by deterministic tables; raw text preserved as `evidence`. + +--- + +## 6) Consensus algorithm + +**Goal:** produce a **stable**, explainable `rollupStatus` per `(vulnId, productKey)` given possibly conflicting claims. + +### 6.1 Inputs + +* Set **S** of `VexClaim` for the key. +* **Excitor policy snapshot**: + + * **weights** per provider tier and per provider overrides. + * **justification gates** (e.g., require justification for `not_affected` to be acceptable). + * **minEvidence** rules (e.g., `not_affected` must come from ≥1 vendor or 2 distros). + * **signature requirements** (e.g., require verified signature for ‘fixed’ to be considered). + +### 6.2 Steps + +1. **Filter invalid** claims by signature policy & justification gates → set `S'`. +2. **Score** each claim: + `score = weight(provider) * freshnessFactor(lastObserved)` where freshnessFactor ∈ [0.8, 1.0] for staleness decay (configurable; small effect). +3. **Aggregate** scores per status: `W(status) = Σ score(claims with that status)`. +4. **Pick** `rollupStatus = argmax_status W(status)`. +5. **Tie‑breakers** (in order): + + * Higher **max single** provider score wins (vendor > distro > platform > hub). + * More **recent** lastObserved wins. + * Deterministic lexicographic order of status (`fixed` > `not_affected` > `under_investigation` > `affected`) as final tiebreaker. +6. **Explain**: mark accepted sources (`accepted=true; reason="weight"`/`"freshness"`), mark rejected sources with explicit `reason` (`"insufficient_justification"`, `"signature_unverified"`, `"lower_weight"`). + +> The algorithm is **pure** given S and policy snapshot; result is reproducible and hashed into `consensusDigest`. + +--- + +## 7) Query & export APIs + +All endpoints are versioned under `/api/v1/vex`. + +### 7.1 Query (online) + +``` +POST /claims/search + body: { vulnIds?: string[], productKeys?: string[], providers?: string[], since?: timestamp, limit?: int, pageToken?: string } + → { claims[], nextPageToken? } + +POST /consensus/search + body: { vulnIds?: string[], productKeys?: string[], policyRevisionId?: string, since?: timestamp, limit?: int, pageToken?: string } + → { entries[], nextPageToken? } + +POST /excititor/resolve (scope: vex.read) + body: { productKeys?: string[], purls?: string[], vulnerabilityIds: string[], policyRevisionId?: string } + → { policy, resolvedAt, results: [ { vulnerabilityId, productKey, status, sources[], conflicts[], decisions[], signals?, summary?, envelope: { artifact, contentSignature?, attestation?, attestationEnvelope?, attestationSignature? } } ] } +``` + +### 7.2 Exports (cacheable snapshots) + +``` +POST /exports + body: { signature: { vulnFilter?, productFilter?, providers?, since? }, format: raw|consensus|index, policyRevisionId?: string, force?: bool } + → { exportId, artifactSha256, rekor? } + +GET /exports/{exportId} → bytes (application/json or binary index) +GET /exports/{exportId}/meta → { signature, policyRevisionId, createdAt, artifactSha256, rekor? } +``` + +### 7.3 Provider operations + +``` +GET /providers → provider list & signature policy +POST /providers/{id}/refresh → trigger fetch/normalize window +GET /providers/{id}/status → last fetch, doc counts, signature stats +``` + +**Auth:** service‑to‑service via Authority tokens; operator operations via UI/CLI with RBAC. + +--- + +## 8) Attestation integration + +* Exports can be **DSSE‑signed** via **Signer** and logged to **Rekor v2** via **Attestor** (optional but recommended for regulated pipelines). +* `vex.exports.rekor` stores `{uuid, index, url}` when present. +* **Predicate type**: `https://stella-ops.org/attestations/vex-export/1` with fields: + + * `querySignature`, `policyRevisionId`, `artifactSha256`, `createdAt`. + +--- + +## 9) Configuration (YAML) + +```yaml +excitor: + mongo: { uri: "mongodb://mongo/excitor" } + s3: + endpoint: http://minio:9000 + bucket: stellaops + policy: + weights: + vendor: 1.0 + distro: 0.9 + platform: 0.7 + hub: 0.5 + attestation: 0.6 + providerOverrides: + redhat: 1.0 + suse: 0.95 + requireJustificationForNotAffected: true + signatureRequiredForFixed: true + minEvidence: + not_affected: + vendorOrTwoDistros: true + connectors: + - providerId: redhat + kind: csaf + baseUrl: https://access.redhat.com/security/data/csaf/v2/ + signaturePolicy: { type: pgp, keys: [ "…redhat-pgp-key…" ] } + windowDays: 7 + - providerId: suse + kind: csaf + baseUrl: https://ftp.suse.com/pub/projects/security/csaf/ + signaturePolicy: { type: pgp, keys: [ "…suse-pgp-key…" ] } + - providerId: ubuntu + kind: openvex + baseUrl: https://…/vex/ + signaturePolicy: { type: none } + - providerId: vendorX + kind: cyclonedx-vex + ociRef: ghcr.io/vendorx/vex@sha256:… + signaturePolicy: { type: cosign, cosignKeylessRoots: [ "sigstore-root" ] } +``` + +--- + +## 10) Security model + +* **Input signature verification** enforced per provider policy (PGP, cosign, x509). +* **Connector allowlists**: outbound fetch constrained to configured domains. +* **Tenant isolation**: per‑tenant DB prefixes or separate DBs; per‑tenant S3 prefixes; per‑tenant policies. +* **AuthN/Z**: Authority‑issued OpToks; RBAC roles (`vex.read`, `vex.admin`, `vex.export`). +* **No secrets in logs**; deterministic logging contexts include providerId, docDigest, claim keys. + +--- + +## 11) Performance & scale + +* **Targets:** + + * Normalize 10k VEX claims/minute/core. + * Consensus compute ≤ 50 ms for 1k unique `(vuln, product)` pairs in hot cache. + * Export (consensus) 1M rows in ≤ 60 s on 8 cores with streaming writer. + +* **Scaling:** + + * WebService handles control APIs; **Worker** background services (same image) execute fetch/normalize in parallel with rate‑limits; Mongo writes batched; upserts by natural keys. + * Exports stream straight to S3 (MinIO) with rolling buffers. + +* **Caching:** + + * `vex.cache` maps query signatures → export; TTL to avoid stampedes; optimistic reuse unless `force`. + +--- + +## 12) Observability + +* **Metrics:** + + * `vex.ingest.docs_total{provider}` + * `vex.normalize.claims_total{provider}` + * `vex.signature.failures_total{provider,method}` + * `vex.consensus.conflicts_total{vulnId}` + * `vex.exports.bytes{format}` / `vex.exports.latency_seconds` +* **Tracing:** spans for fetch, verify, parse, map, consensus, export. +* **Dashboards:** provider staleness, top conflicting vulns/components, signature posture, export cache hit‑rate. + +--- + +## 13) Testing matrix + +* **Connectors:** golden raw docs → deterministic claims (fixtures per provider/format). +* **Signature policies:** valid/invalid PGP/cosign/x509 samples; ensure rejects are recorded but not accepted. +* **Normalization edge cases:** platform‑only claims, free‑text justifications, non‑purl products. +* **Consensus:** conflict scenarios across tiers; check tie‑breakers; justification gates. +* **Performance:** 1M‑row export timing; memory ceilings; stream correctness. +* **Determinism:** same inputs + policy → identical `consensusDigest` and export bytes. +* **API contract tests:** pagination, filters, RBAC, rate limits. + +--- + +## 14) Integration points + +* **Backend Policy Engine** (in Scanner.WebService): calls `POST /excititor/resolve` (scope `vex.read`) with batched `(purl, vulnId)` pairs to fetch `rollupStatus + sources`. +* **Conselier**: provides alias graph (CVE↔vendor IDs) and may supply VEX‑adjacent metadata (e.g., KEV flag) for policy escalation. +* **UI**: VEX explorer screens use `/claims/search` and `/consensus/search`; show conflicts & provenance. +* **CLI**: `stellaops vex export --consensus --since 7d --out vex.json` for audits. + +--- + +## 15) Failure modes & fallback + +* **Provider unreachable:** stale thresholds trigger warnings; policy can down‑weight stale providers automatically (freshness factor). +* **Signature outage:** continue to ingest but mark `signatureState.verified=false`; consensus will likely exclude or down‑weight per policy. +* **Schema drift:** unknown fields are preserved as `evidence`; normalization rejects only on **invalid identity** or **status**. + +--- + +## 16) Rollout plan (incremental) + +1. **MVP**: OpenVEX + CSAF connectors for 3 major providers (e.g., Red Hat/SUSE/Ubuntu), normalization + consensus + `/excititor/resolve`. +2. **Signature policies**: PGP for distros; cosign for OCI. +3. **Exports + optional attestation**. +4. **CycloneDX VEX** connectors; platform claim expansion tables; UI explorer. +5. **Scale hardening**: export indexes; conflict analytics. + +--- + +## 17) Appendix — canonical JSON (stable ordering) + +All exports and consensus entries are serialized via `VexCanonicalJsonSerializer`: + +* UTF‑8 without BOM; +* keys sorted (ASCII); +* arrays sorted by `(providerId, vulnId, productKey, lastObserved)` unless semantic order mandated; +* timestamps in `YYYY‑MM‑DDThh:mm:ssZ`; +* no insignificant whitespace. + diff --git a/docs/modules/vexer/implementation_plan.md b/docs/modules/excitor/implementation_plan.md similarity index 93% rename from docs/modules/vexer/implementation_plan.md rename to docs/modules/excitor/implementation_plan.md index e0fe452f..e2a43c89 100644 --- a/docs/modules/vexer/implementation_plan.md +++ b/docs/modules/excitor/implementation_plan.md @@ -1,65 +1,65 @@ -# Implementation plan — Vexer - -## Delivery phases -- **Phase 1 – Connectors & normalization** - Build connectors for OpenVEX, CSAF VEX, CycloneDX VEX, OCI attestations; capture provenance, signatures, and source metadata; normalise into `VexClaim`. -- **Phase 2 – Mapping & trust registry** - Implement product mapping (CPE → purl/version), issuer registry (trust tiers, signatures), scope scoring, and justification taxonomy. -- **Phase 3 – Consensus & projections** - Deliver consensus computation, conflict preservation, projections (`vex_consensus`, history, provider snapshots), and DSSE events. -- **Phase 4 – APIs & integrations** - Expose REST/CLI endpoints for claims, consensus, conflicts, exports; integrate Policy Engine, Vuln Explorer, Advisory AI, Export Center. -- **Phase 5 – Observability & offline** - Ship metrics, logs, traces, dashboards, incident runbooks, Offline Kit bundles, and performance tuning (10M claims/tenant). - -## Work breakdown -- **Connectors** - - Fetchers for vendor feeds, CSAF repositories, OpenVEX docs, OCI referrers. - - Signature verification (PGP, cosign, PKI) per source; schema validation; rate limiting. - - Source configuration (trust tier, fetch cadence, blackout windows) stored in metadata registry. -- **Normalization** - - Canonical `VexClaim` schema with deterministic IDs, provenance, supersedes chains. - - Product tree parsing, mapping to canonical product keys and environments. - - Justification and scope scoring derived from source semantics. -- **Consensus & projections** - - Lattice join with precedence rules, conflict tracking, confidence scores, recency decay. - - Append-only history, conflict queue, DSSE events (`vex.consensus.updated`). - - Export-ready JSONL & DSSE bundles for Offline Kit and Export Center. -- **APIs & UX** - - REST endpoints (`/claims`, `/consensus`, `/conflicts`, `/providers`) with tenant RBAC. - - CLI commands `stella vex claims|consensus|conflicts|export`. - - Console modules (list/detail, conflict diagnostics, provider health, simulation hooks). -- **Integrations** - - Policy Engine trust knobs, Vuln Explorer consensus badges, Advisory AI narrative generation, Notify alerts for conflicts. - - Orchestrator jobs for recompute/backfill triggered by Excitator deltas. -- **Observability & Ops** - - Metrics (ingest latency, signature failure rate, conflict rate, consensus latency). - - Logs/traces with tenant/issuer/provenance context. - - Runbooks for mapping failures, signature errors, recompute storms, quota exhaustion. - -## Acceptance criteria -- Connectors ingest validated VEX statements with signed provenance, deterministic mapping, and tenant isolation. -- Consensus outputs reproducible, include conflicts, and integrate with Policy Engine/Vuln Explorer/Export Center. -- CLI/Console provide evidence inspection, conflict analysis, and exports; Offline Kit bundles replay verification offline. -- Observability dashboards/alerts capture ingest health, trust anomalies, conflict spikes, and performance budgets. -- Recompute pipeline handles policy changes and new evidence without dropping deterministic outcomes. - -## Risks & mitigations -- **Mapping ambiguity:** maintain scope scores, manual overrides, highlight warnings. -- **Signature trust gaps:** issuer registry with auditing, fallback trust policies, tenant overrides. -- **Evidence surges:** orchestrator backpressure, prioritised queues, shardable workers. -- **Performance regressions:** indexing, caching, load tests, budget enforcement. -- **Tenant leakage:** strict RBAC/filters, fuzz tests, compliance reviews. - -## Test strategy -- **Unit:** connector parsers, normalization, mapping conversions, lattice operations. -- **Property:** randomised evidence ensuring commutative consensus and deterministic digests. -- **Integration:** end-to-end pipeline from Excitator to consensus export, policy simulation, conflict handling. -- **Performance:** large feed ingestion, recompute stress, CLI export throughput. -- **Security:** signature tampering, issuer revocation, RBAC. -- **Offline:** export/import verification, DSSE bundle validation. - -## Definition of done -- Connectors, normalization, consensus, APIs, and integrations deployed with telemetry, runbooks, and Offline Kit parity. -- Documentation (overview, architecture, algorithm, issuer registry, API/CLI, runbooks) updated with imposed rule compliance. -- ./TASKS.md and ../../TASKS.md reflect active status and dependencies. +# Implementation plan — Excitor + +## Delivery phases +- **Phase 1 – Connectors & normalization** + Build connectors for OpenVEX, CSAF VEX, CycloneDX VEX, OCI attestations; capture provenance, signatures, and source metadata; normalise into `VexClaim`. +- **Phase 2 – Mapping & trust registry** + Implement product mapping (CPE → purl/version), issuer registry (trust tiers, signatures), scope scoring, and justification taxonomy. +- **Phase 3 – Consensus & projections** + Deliver consensus computation, conflict preservation, projections (`vex_consensus`, history, provider snapshots), and DSSE events. +- **Phase 4 – APIs & integrations** + Expose REST/CLI endpoints for claims, consensus, conflicts, exports; integrate Policy Engine, Vuln Explorer, Advisory AI, Export Center. +- **Phase 5 – Observability & offline** + Ship metrics, logs, traces, dashboards, incident runbooks, Offline Kit bundles, and performance tuning (10M claims/tenant). + +## Work breakdown +- **Connectors** + - Fetchers for vendor feeds, CSAF repositories, OpenVEX docs, OCI referrers. + - Signature verification (PGP, cosign, PKI) per source; schema validation; rate limiting. + - Source configuration (trust tier, fetch cadence, blackout windows) stored in metadata registry. +- **Normalization** + - Canonical `VexClaim` schema with deterministic IDs, provenance, supersedes chains. + - Product tree parsing, mapping to canonical product keys and environments. + - Justification and scope scoring derived from source semantics. +- **Consensus & projections** + - Lattice join with precedence rules, conflict tracking, confidence scores, recency decay. + - Append-only history, conflict queue, DSSE events (`vex.consensus.updated`). + - Export-ready JSONL & DSSE bundles for Offline Kit and Export Center. +- **APIs & UX** + - REST endpoints (`/claims`, `/consensus`, `/conflicts`, `/providers`) with tenant RBAC. + - CLI commands `stella vex claims|consensus|conflicts|export`. + - Console modules (list/detail, conflict diagnostics, provider health, simulation hooks). +- **Integrations** + - Policy Engine trust knobs, Vuln Explorer consensus badges, Advisory AI narrative generation, Notify alerts for conflicts. + - Orchestrator jobs for recompute/backfill triggered by Excitor deltas. +- **Observability & Ops** + - Metrics (ingest latency, signature failure rate, conflict rate, consensus latency). + - Logs/traces with tenant/issuer/provenance context. + - Runbooks for mapping failures, signature errors, recompute storms, quota exhaustion. + +## Acceptance criteria +- Connectors ingest validated VEX statements with signed provenance, deterministic mapping, and tenant isolation. +- Consensus outputs reproducible, include conflicts, and integrate with Policy Engine/Vuln Explorer/Export Center. +- CLI/Console provide evidence inspection, conflict analysis, and exports; Offline Kit bundles replay verification offline. +- Observability dashboards/alerts capture ingest health, trust anomalies, conflict spikes, and performance budgets. +- Recompute pipeline handles policy changes and new evidence without dropping deterministic outcomes. + +## Risks & mitigations +- **Mapping ambiguity:** maintain scope scores, manual overrides, highlight warnings. +- **Signature trust gaps:** issuer registry with auditing, fallback trust policies, tenant overrides. +- **Evidence surges:** orchestrator backpressure, prioritised queues, shardable workers. +- **Performance regressions:** indexing, caching, load tests, budget enforcement. +- **Tenant leakage:** strict RBAC/filters, fuzz tests, compliance reviews. + +## Test strategy +- **Unit:** connector parsers, normalization, mapping conversions, lattice operations. +- **Property:** randomised evidence ensuring commutative consensus and deterministic digests. +- **Integration:** end-to-end pipeline from Excitor to consensus export, policy simulation, conflict handling. +- **Performance:** large feed ingestion, recompute stress, CLI export throughput. +- **Security:** signature tampering, issuer revocation, RBAC. +- **Offline:** export/import verification, DSSE bundle validation. + +## Definition of done +- Connectors, normalization, consensus, APIs, and integrations deployed with telemetry, runbooks, and Offline Kit parity. +- Documentation (overview, architecture, algorithm, issuer registry, API/CLI, runbooks) updated with imposed rule compliance. +- ./TASKS.md and ../../TASKS.md reflect active status and dependencies. diff --git a/docs/modules/vexer/scoring.md b/docs/modules/excitor/scoring.md similarity index 70% rename from docs/modules/vexer/scoring.md rename to docs/modules/excitor/scoring.md index c4db806c..54ddbcf4 100644 --- a/docs/modules/vexer/scoring.md +++ b/docs/modules/excitor/scoring.md @@ -1,83 +1,83 @@ -## Status - -This document tracks the future-looking risk scoring model for Vexer. The calculation below is not active yet; Sprint 7 work will add the required schema fields, policy controls, and services. Until that ships, Vexer emits consensus statuses without numeric scores. - -## Scoring model (target state) - -**S = Gate(VEX_status) × W_trust(source) × [Severity_base × (1 + α·KEV + β·EPSS)]** - -* **Gate(VEX_status)**: `affected`/`under_investigation` → 1, `not_affected`/`fixed` → 0. A trusted “not affected” or “fixed” still zeroes the score. -* **W_trust(source)**: normalized policy weight (baseline 0‒1). Policies may opt into >1 boosts for signed vendor feeds once Phase 1 closes. -* **Severity_base**: canonical numeric severity from Feedser (CVSS or org-defined scale). -* **KEV flag**: 0/1 boost when CISA Known Exploited Vulnerabilities applies. -* **EPSS**: probability [0,1]; bounded multiplier. -* **α, β**: configurable coefficients (default α=0.25, β=0.5) stored in policy. - -Safeguards: freeze boosts when product identity is unknown, clamp outputs ≥0, and log every factor in the audit trail. - -## Implementation roadmap - -| Phase | Scope | Artifacts | -| --- | --- | --- | -| **Phase 1 – Schema foundations** | Extend Vexer consensus/claims and Feedser canonical advisories with severity, KEV, EPSS, and expose α/β + weight ceilings in policy. | Sprint 7 tasks `VEXER-CORE-02-001`, `VEXER-POLICY-02-001`, `VEXER-STORAGE-02-001`, `FEEDCORE-ENGINE-07-001`. | -| **Phase 2 – Deterministic score engine** | Implement a scoring component that executes alongside consensus and persists score envelopes with hashes. | Planned task `VEXER-CORE-02-002` (backlog). | -| **Phase 3 – Surfacing & enforcement** | Expose scores via WebService/CLI, integrate with Feedser noise priors, and enforce policy-based suppressions. | To be scheduled after Phase 2. | - -## Data model (after Phase 1) - -```json -{ - "vulnerabilityId": "CVE-2025-12345", - "product": "pkg:name@version", - "consensus": { - "status": "affected", - "policyRevisionId": "rev-12", - "policyDigest": "0D9AEC…" - }, - "signals": { - "severity": {"scheme": "CVSS:3.1", "score": 7.5}, - "kev": true, - "epss": 0.40 - }, - "policy": { - "weight": 1.15, - "alpha": 0.25, - "beta": 0.5 - }, - "score": { - "value": 10.8, - "generatedAt": "2025-11-05T14:12:30Z", - "audit": [ - "gate:affected", - "weight:1.15", - "severity:7.5", - "kev:1", - "epss:0.40" - ] - } -} -``` - -## Operational guidance - -* **Inputs**: Feedser delivers severity/KEV/EPSS via the advisory event log; Vexer connectors load VEX statements. Policy owns trust tiers and coefficients. -* **Processing**: the scoring engine (Phase 2) runs next to consensus, storing results with deterministic hashes so exports and attestations can reference them. -* **Consumption**: WebService/CLI will return consensus plus score; scanners may suppress findings only when policy-authorized VEX gating and signed score envelopes agree. - -## Pseudocode (Phase 2 preview) - -```python -def risk_score(gate, weight, severity, kev, epss, alpha, beta, freeze_boosts=False): - if gate == 0: - return 0 - if freeze_boosts: - kev, epss = 0, 0 - boost = 1 + alpha * kev + beta * epss - return max(0, weight * severity * boost) -``` - -## FAQ - -* **Can operators opt out?** Set α=β=0 or keep weights ≤1.0 via policy. -* **What about missing signals?** Treat them as zero and log the omission. -* **When will this ship?** Phase 1 is planned for Sprint 7; later phases depend on connector coverage and attestation delivery. +## Status + +This document tracks the future-looking risk scoring model for Excitor. The calculation below is not active yet; Sprint 7 work will add the required schema fields, policy controls, and services. Until that ships, Excitor emits consensus statuses without numeric scores. + +## Scoring model (target state) + +**S = Gate(VEX_status) × W_trust(source) × [Severity_base × (1 + α·KEV + β·EPSS)]** + +* **Gate(VEX_status)**: `affected`/`under_investigation` → 1, `not_affected`/`fixed` → 0. A trusted “not affected” or “fixed” still zeroes the score. +* **W_trust(source)**: normalized policy weight (baseline 0‒1). Policies may opt into >1 boosts for signed vendor feeds once Phase 1 closes. +* **Severity_base**: canonical numeric severity from Conselier (CVSS or org-defined scale). +* **KEV flag**: 0/1 boost when CISA Known Exploited Vulnerabilities applies. +* **EPSS**: probability [0,1]; bounded multiplier. +* **α, β**: configurable coefficients (default α=0.25, β=0.5) stored in policy. + +Safeguards: freeze boosts when product identity is unknown, clamp outputs ≥0, and log every factor in the audit trail. + +## Implementation roadmap + +| Phase | Scope | Artifacts | +| --- | --- | --- | +| **Phase 1 – Schema foundations** | Extend Excitor consensus/claims and Conselier canonical advisories with severity, KEV, EPSS, and expose α/β + weight ceilings in policy. | Sprint 7 tasks `EXCITOR-CORE-02-001`, `EXCITOR-POLICY-02-001`, `EXCITOR-STORAGE-02-001`, `FEEDCORE-ENGINE-07-001`. | +| **Phase 2 – Deterministic score engine** | Implement a scoring component that executes alongside consensus and persists score envelopes with hashes. | Planned task `EXCITOR-CORE-02-002` (backlog). | +| **Phase 3 – Surfacing & enforcement** | Expose scores via WebService/CLI, integrate with Conselier noise priors, and enforce policy-based suppressions. | To be scheduled after Phase 2. | + +## Data model (after Phase 1) + +```json +{ + "vulnerabilityId": "CVE-2025-12345", + "product": "pkg:name@version", + "consensus": { + "status": "affected", + "policyRevisionId": "rev-12", + "policyDigest": "0D9AEC…" + }, + "signals": { + "severity": {"scheme": "CVSS:3.1", "score": 7.5}, + "kev": true, + "epss": 0.40 + }, + "policy": { + "weight": 1.15, + "alpha": 0.25, + "beta": 0.5 + }, + "score": { + "value": 10.8, + "generatedAt": "2025-11-05T14:12:30Z", + "audit": [ + "gate:affected", + "weight:1.15", + "severity:7.5", + "kev:1", + "epss:0.40" + ] + } +} +``` + +## Operational guidance + +* **Inputs**: Conselier delivers severity/KEV/EPSS via the advisory event log; Excitor connectors load VEX statements. Policy owns trust tiers and coefficients. +* **Processing**: the scoring engine (Phase 2) runs next to consensus, storing results with deterministic hashes so exports and attestations can reference them. +* **Consumption**: WebService/CLI will return consensus plus score; scanners may suppress findings only when policy-authorized VEX gating and signed score envelopes agree. + +## Pseudocode (Phase 2 preview) + +```python +def risk_score(gate, weight, severity, kev, epss, alpha, beta, freeze_boosts=False): + if gate == 0: + return 0 + if freeze_boosts: + kev, epss = 0, 0 + boost = 1 + alpha * kev + beta * epss + return max(0, weight * severity * boost) +``` + +## FAQ + +* **Can operators opt out?** Set α=β=0 or keep weights ≤1.0 via policy. +* **What about missing signals?** Treat them as zero and log the omission. +* **When will this ship?** Phase 1 is planned for Sprint 7; later phases depend on connector coverage and attestation delivery. diff --git a/docs/modules/export-center/provenance-and-signing.md b/docs/modules/export-center/provenance-and-signing.md index 1988b6d5..49fb317f 100644 --- a/docs/modules/export-center/provenance-and-signing.md +++ b/docs/modules/export-center/provenance-and-signing.md @@ -1,150 +1,150 @@ -# Export Center Provenance & Signing - -> **Imposed rule:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. - -Export Center runs emit deterministic manifests, provenance records, and signatures so operators can prove bundle integrity end-to-end—whether the artefact is downloaded over HTTPS, pulled as an OCI object, or staged through the Offline Kit. This guide captures the canonical artefacts, signing pipeline, verification workflows, and failure handling expectations that backlogs `EXPORT-SVC-35-005` and `EXPORT-SVC-37-002` implement. - ---- - -## 1. Goals & scope - -- **Authenticity.** Every export manifest and provenance document is signed using Authority-managed KMS keys (cosign-compatible) with optional SLSA Level 3 attestation. -- **Traceability.** Provenance links each bundle to the inputs that produced it: tenant, findings ledger queries, policy snapshots, SBOM identifiers, adapter versions, and encryption recipients. -- **Determinism.** Canonical JSON (sorted keys, RFC 3339 UTC timestamps, normalized numbers) guarantees byte-for-byte stability across reruns with identical input. -- **Portability.** Signatures and attestations travel with filesystem bundles, OCI artefacts, and Offline Kit staging trees. Verification does not require online Authority access when the bundle includes the cosign public key. - ---- - -## 2. Artefact inventory - -| File | Location | Description | Notes | -|------|----------|-------------|-------| -| `export.json` | `manifests/export.json` or HTTP `GET /api/export/runs/{id}/manifest` | Canonical manifest describing profile, selectors, counts, SHA-256 digests, compression hints, distribution targets. | Hash of this file is included in provenance `subjects[]`. | -| `provenance.json` | `manifests/provenance.json` or `GET /api/export/runs/{id}/provenance` | In-toto provenance record listing subjects, materials, toolchain metadata, encryption recipients, and KMS key identifiers. | Mirrors SLSA Level 2 schema; optionally upgraded to Level 3 with builder attestations. | -| `export.json.sig` / `export.json.dsse` | `signatures/export.json.sig` | Cosign signature (and optional DSSE envelope) for manifest. | File naming matches cosign defaults; offline verification scripts expect `.sig`. | -| `provenance.json.sig` / `provenance.json.dsse` | `signatures/provenance.json.sig` | Cosign signature (and optional DSSE envelope) for provenance document. | `dsse` present when SLSA Level 3 is enabled. | -| `bundle.attestation` | `signatures/bundle.attestation` (optional) | SLSA Level 2/3 attestation binding bundle tarball/OCI digest to the run. | Only produced when `export.attestation.enabled=true`. | -| `manifest.yaml` | bundle root | Human-readable summary including digests, sizes, encryption metadata, and verification hints. | Unsigned but redundant; signatures cover the JSON manifests. | - -All digests use lowercase hex SHA-256 (`sha256:`). When bundle encryption is enabled, `provenance.json` records wrapped data keys and recipient fingerprints under `encryption.recipients[]`. - ---- - -## 3. Signing pipeline - -1. **Canonicalisation.** Export worker serialises `export.json` and `provenance.json` using `NotifyCanonicalJsonSerializer` (identical canonical JSON helpers shared across services). Keys are sorted lexicographically, arrays ordered deterministically, timestamps normalised to UTC. -2. **Digest creation.** SHA-256 digests are computed and recorded: - - `manifest_hash` and `provenance_hash` stored in the run metadata (Mongo) and exported via `/api/export/runs/{id}`. - - Provenance `subjects[]` contains both manifest hash and bundle/archive hash. -3. **Key retrieval.** Worker obtains a short-lived signing token from Authority’s KMS client using tenant-scoped credentials (`export.sign` scope). Keys live in Authority or tenant-specific HSMs depending on deployment. -4. **Signature emission.** Cosign generates detached signatures (`*.sig`). If DSSE is enabled, cosign wraps payload bytes in a DSSE envelope (`*.dsse`). Attestations follow the SLSA Level 2 provenance template; Level 3 requires builder metadata (`EXPORT-SVC-37-002` optional feature flag). -5. **Storage & distribution.** Signatures and attestations are written alongside manifests in object storage, included in filesystem bundles, and attached as OCI artefact layers/annotations. -6. **Audit trail.** Run metadata captures signer identity (`signing_key_id`), cosign certificate serial, signature timestamps, and verification hints. Console/CLI surface these details for downstream automation. - -> **Key management.** Secrets and key references are configured per tenant via `export.signing`, pointing to Authority clients or external HSM aliases. Offline deployments pre-load cosign public keys into the bundle (`signatures/pubkeys/{tenant}.pem`). - ---- - -## 4. Provenance schema highlights - -`provenance.json` follows the SLSA provenance (`https://slsa.dev/provenance/v1`) structure with StellaOps-specific extensions. Key fields: - -| Path | Description | -|------|-------------| -| `subject[]` | Array of `{name,digest}` pairs. Includes bundle tarball/OCI digest and `export.json` digest. | -| `predicateType` | SLSA v1 (default). | -| `predicate.builder` | `{id:"stellaops/export-center@"}` identifies the worker instance/cluster. | -| `predicate.buildType` | Profile identifier (`mirror:full`, `mirror:delta`, etc.). | -| `predicate.invocation.parameters` | Profile selectors, retention flags, encryption mode, base export references. | -| `predicate.materials[]` | Source artefacts with digests: findings ledger query snapshots, policy snapshot IDs + hashes, SBOM identifiers, adapter release digests. | -| `predicate.metadata.buildFinishedOn` | RFC 3339 timestamp when signing completed. | -| `predicate.metadata.reproducible` | Always `true`—workers guarantee determinism. | -| `predicate.environment.encryption` | Records encryption recipients, wrapped keys, algorithm (`age` or `aes-gcm`). | -| `predicate.environment.kms` | Signing key identifier (`authority://tenant/export-signing-key`) and certificate chain fingerprints. | - -Sample (abridged): - -```json -{ - "subject": [ - { "name": "bundle.tar.zst", "digest": { "sha256": "c1fe..." } }, - { "name": "manifests/export.json", "digest": { "sha256": "ad42..." } } - ], - "predicate": { - "buildType": "mirror:delta", - "invocation": { - "parameters": { - "tenant": "tenant-01", - "baseExportId": "run-20251020-01", - "selectors": { "sources": ["concelier","vexer"], "profiles": ["mirror"] } - } - }, - "materials": [ - { "uri": "ledger://tenant-01/findings?cursor=rev-42", "digest": { "sha256": "0f9a..." } }, - { "uri": "policy://tenant-01/snapshots/rev-17", "digest": { "sha256": "8c3d..." } } - ], - "environment": { - "encryption": { - "mode": "age", - "recipients": [ - { "recipient": "age1qxyz...", "wrappedKey": "BASE64...", "keyId": "tenant-01/notify-age" } - ] - }, - "kms": { - "signingKeyId": "authority://tenant-01/export-signing", - "certificateChainSha256": "1f5e..." - } - } - } -} -``` - ---- - -## 5. Verification workflows - -| Scenario | Steps | -|----------|-------| -| **CLI verification** | 1. `stella export manifest --output manifests/export.json --signature manifests/export.json.sig`
2. `stella export provenance --output manifests/provenance.json --signature manifests/provenance.json.sig`
3. `cosign verify-blob --key pubkeys/tenant.pem --signature manifests/export.json.sig manifests/export.json`
4. `cosign verify-blob --key pubkeys/tenant.pem --signature manifests/provenance.json.sig manifests/provenance.json` | -| **Bundle verification (offline)** | 1. Extract bundle (or mount OCI artefact).
2. Validate manifest/provenance signatures using bundled public key.
3. Recompute SHA-256 for `data/` files and compare with entries in `export.json`.
4. If encrypted, decrypt with Age/AES-GCM recipient key, then re-run digest comparisons on decrypted content. | -| **CI pipeline** | Use `stella export verify --manifest manifests/export.json --provenance manifests/provenance.json --signature manifests/export.json.sig --signature manifests/provenance.json.sig` (task `CLI-EXPORT-37-001`). Failure exits non-zero with reason codes (`ERR_EXPORT_SIG_INVALID`, `ERR_EXPORT_DIGEST_MISMATCH`). | -| **Console download** | Console automatically verifies signatures before exposing the bundle; failure surfaces an actionable error referencing the export run ID and required remediation. | - -Verification guidance (docs/modules/cli/guides/cli-reference.md §export) cross-links here; keep both docs in sync when CLI behaviour changes. - ---- - -## 6. Distribution considerations - -- **HTTP headers.** `X-Export-Digest` includes bundle digest; `X-Export-Provenance` references `provenance.json` URL; `X-Export-Signature` references `.sig`. Clients use these hints to short-circuit re-downloads. -- **OCI annotations.** `org.opencontainers.image.ref.name`, `io.stellaops.export.manifest-digest`, and `io.stellaops.export.provenance-ref` allow registry tooling to locate manifests/signatures quickly. -- **Offline Kit staging.** Offline kit assembler copies `manifests/`, `signatures/`, and `pubkeys/` verbatim. Verification scripts (`offline-kits/bin/verify-export.sh`) wrap the cosign commands described above. - ---- - -## 7. Failure handling & observability - -- Runs surface signature status via `/api/export/runs/{id}` (`signing.status`, `signing.lastError`). Common errors include `ERR_EXPORT_KMS_UNAVAILABLE`, `ERR_EXPORT_ATTESTATION_FAILED`, `ERR_EXPORT_CANONICALIZE`. -- Metrics: `exporter_sign_duration_seconds`, `exporter_sign_failures_total{error_code}`, `exporter_provenance_verify_failures_total`. -- Logs: `phase=sign`, `error_code`, `signing_key_id`, `cosign_certificate_sn`. -- Alerts: DevOps dashboards (task `DEVOPS-EXPORT-37-001`) trigger on consecutive signing failures or verification failures >0. - -When verification fails downstream, operators should: -1. Confirm signatures using the known-good key. -2. Inspect `provenance.json` materials; rerun the source queries to ensure matching digests. -3. Review run audit logs and retry export with `--resume` to regenerate manifests. - ---- - -## 8. Compliance checklist - -- [ ] Manifests and provenance documents generated with canonical JSON, deterministic digests, and signatures. -- [ ] Cosign public keys published per tenant, rotated through Authority, and distributed to Offline Kit consumers. -- [ ] SLSA attestations enabled where supply-chain requirements demand Level 3 evidence. -- [ ] CLI/Console verification paths documented and tested (CI pipelines exercise `stella export verify`). -- [ ] Encryption metadata (recipients, wrapped keys) recorded in provenance and validated during verification. -- [ ] Run audit logs capture signature timestamps, signer identity, and failure reasons. - ---- - -> **Imposed rule reminder:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. +# Export Center Provenance & Signing + +> **Imposed rule:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. + +Export Center runs emit deterministic manifests, provenance records, and signatures so operators can prove bundle integrity end-to-end—whether the artefact is downloaded over HTTPS, pulled as an OCI object, or staged through the Offline Kit. This guide captures the canonical artefacts, signing pipeline, verification workflows, and failure handling expectations that backlogs `EXPORT-SVC-35-005` and `EXPORT-SVC-37-002` implement. + +--- + +## 1. Goals & scope + +- **Authenticity.** Every export manifest and provenance document is signed using Authority-managed KMS keys (cosign-compatible) with optional SLSA Level 3 attestation. +- **Traceability.** Provenance links each bundle to the inputs that produced it: tenant, findings ledger queries, policy snapshots, SBOM identifiers, adapter versions, and encryption recipients. +- **Determinism.** Canonical JSON (sorted keys, RFC 3339 UTC timestamps, normalized numbers) guarantees byte-for-byte stability across reruns with identical input. +- **Portability.** Signatures and attestations travel with filesystem bundles, OCI artefacts, and Offline Kit staging trees. Verification does not require online Authority access when the bundle includes the cosign public key. + +--- + +## 2. Artefact inventory + +| File | Location | Description | Notes | +|------|----------|-------------|-------| +| `export.json` | `manifests/export.json` or HTTP `GET /api/export/runs/{id}/manifest` | Canonical manifest describing profile, selectors, counts, SHA-256 digests, compression hints, distribution targets. | Hash of this file is included in provenance `subjects[]`. | +| `provenance.json` | `manifests/provenance.json` or `GET /api/export/runs/{id}/provenance` | In-toto provenance record listing subjects, materials, toolchain metadata, encryption recipients, and KMS key identifiers. | Mirrors SLSA Level 2 schema; optionally upgraded to Level 3 with builder attestations. | +| `export.json.sig` / `export.json.dsse` | `signatures/export.json.sig` | Cosign signature (and optional DSSE envelope) for manifest. | File naming matches cosign defaults; offline verification scripts expect `.sig`. | +| `provenance.json.sig` / `provenance.json.dsse` | `signatures/provenance.json.sig` | Cosign signature (and optional DSSE envelope) for provenance document. | `dsse` present when SLSA Level 3 is enabled. | +| `bundle.attestation` | `signatures/bundle.attestation` (optional) | SLSA Level 2/3 attestation binding bundle tarball/OCI digest to the run. | Only produced when `export.attestation.enabled=true`. | +| `manifest.yaml` | bundle root | Human-readable summary including digests, sizes, encryption metadata, and verification hints. | Unsigned but redundant; signatures cover the JSON manifests. | + +All digests use lowercase hex SHA-256 (`sha256:`). When bundle encryption is enabled, `provenance.json` records wrapped data keys and recipient fingerprints under `encryption.recipients[]`. + +--- + +## 3. Signing pipeline + +1. **Canonicalisation.** Export worker serialises `export.json` and `provenance.json` using `NotifyCanonicalJsonSerializer` (identical canonical JSON helpers shared across services). Keys are sorted lexicographically, arrays ordered deterministically, timestamps normalised to UTC. +2. **Digest creation.** SHA-256 digests are computed and recorded: + - `manifest_hash` and `provenance_hash` stored in the run metadata (Mongo) and exported via `/api/export/runs/{id}`. + - Provenance `subjects[]` contains both manifest hash and bundle/archive hash. +3. **Key retrieval.** Worker obtains a short-lived signing token from Authority’s KMS client using tenant-scoped credentials (`export.sign` scope). Keys live in Authority or tenant-specific HSMs depending on deployment. +4. **Signature emission.** Cosign generates detached signatures (`*.sig`). If DSSE is enabled, cosign wraps payload bytes in a DSSE envelope (`*.dsse`). Attestations follow the SLSA Level 2 provenance template; Level 3 requires builder metadata (`EXPORT-SVC-37-002` optional feature flag). +5. **Storage & distribution.** Signatures and attestations are written alongside manifests in object storage, included in filesystem bundles, and attached as OCI artefact layers/annotations. +6. **Audit trail.** Run metadata captures signer identity (`signing_key_id`), cosign certificate serial, signature timestamps, and verification hints. Console/CLI surface these details for downstream automation. + +> **Key management.** Secrets and key references are configured per tenant via `export.signing`, pointing to Authority clients or external HSM aliases. Offline deployments pre-load cosign public keys into the bundle (`signatures/pubkeys/{tenant}.pem`). + +--- + +## 4. Provenance schema highlights + +`provenance.json` follows the SLSA provenance (`https://slsa.dev/provenance/v1`) structure with StellaOps-specific extensions. Key fields: + +| Path | Description | +|------|-------------| +| `subject[]` | Array of `{name,digest}` pairs. Includes bundle tarball/OCI digest and `export.json` digest. | +| `predicateType` | SLSA v1 (default). | +| `predicate.builder` | `{id:"stellaops/export-center@"}` identifies the worker instance/cluster. | +| `predicate.buildType` | Profile identifier (`mirror:full`, `mirror:delta`, etc.). | +| `predicate.invocation.parameters` | Profile selectors, retention flags, encryption mode, base export references. | +| `predicate.materials[]` | Source artefacts with digests: findings ledger query snapshots, policy snapshot IDs + hashes, SBOM identifiers, adapter release digests. | +| `predicate.metadata.buildFinishedOn` | RFC 3339 timestamp when signing completed. | +| `predicate.metadata.reproducible` | Always `true`—workers guarantee determinism. | +| `predicate.environment.encryption` | Records encryption recipients, wrapped keys, algorithm (`age` or `aes-gcm`). | +| `predicate.environment.kms` | Signing key identifier (`authority://tenant/export-signing-key`) and certificate chain fingerprints. | + +Sample (abridged): + +```json +{ + "subject": [ + { "name": "bundle.tar.zst", "digest": { "sha256": "c1fe..." } }, + { "name": "manifests/export.json", "digest": { "sha256": "ad42..." } } + ], + "predicate": { + "buildType": "mirror:delta", + "invocation": { + "parameters": { + "tenant": "tenant-01", + "baseExportId": "run-20251020-01", + "selectors": { "sources": ["concelier","excitor"], "profiles": ["mirror"] } + } + }, + "materials": [ + { "uri": "ledger://tenant-01/findings?cursor=rev-42", "digest": { "sha256": "0f9a..." } }, + { "uri": "policy://tenant-01/snapshots/rev-17", "digest": { "sha256": "8c3d..." } } + ], + "environment": { + "encryption": { + "mode": "age", + "recipients": [ + { "recipient": "age1qxyz...", "wrappedKey": "BASE64...", "keyId": "tenant-01/notify-age" } + ] + }, + "kms": { + "signingKeyId": "authority://tenant-01/export-signing", + "certificateChainSha256": "1f5e..." + } + } + } +} +``` + +--- + +## 5. Verification workflows + +| Scenario | Steps | +|----------|-------| +| **CLI verification** | 1. `stella export manifest --output manifests/export.json --signature manifests/export.json.sig`
2. `stella export provenance --output manifests/provenance.json --signature manifests/provenance.json.sig`
3. `cosign verify-blob --key pubkeys/tenant.pem --signature manifests/export.json.sig manifests/export.json`
4. `cosign verify-blob --key pubkeys/tenant.pem --signature manifests/provenance.json.sig manifests/provenance.json` | +| **Bundle verification (offline)** | 1. Extract bundle (or mount OCI artefact).
2. Validate manifest/provenance signatures using bundled public key.
3. Recompute SHA-256 for `data/` files and compare with entries in `export.json`.
4. If encrypted, decrypt with Age/AES-GCM recipient key, then re-run digest comparisons on decrypted content. | +| **CI pipeline** | Use `stella export verify --manifest manifests/export.json --provenance manifests/provenance.json --signature manifests/export.json.sig --signature manifests/provenance.json.sig` (task `CLI-EXPORT-37-001`). Failure exits non-zero with reason codes (`ERR_EXPORT_SIG_INVALID`, `ERR_EXPORT_DIGEST_MISMATCH`). | +| **Console download** | Console automatically verifies signatures before exposing the bundle; failure surfaces an actionable error referencing the export run ID and required remediation. | + +Verification guidance (docs/modules/cli/guides/cli-reference.md §export) cross-links here; keep both docs in sync when CLI behaviour changes. + +--- + +## 6. Distribution considerations + +- **HTTP headers.** `X-Export-Digest` includes bundle digest; `X-Export-Provenance` references `provenance.json` URL; `X-Export-Signature` references `.sig`. Clients use these hints to short-circuit re-downloads. +- **OCI annotations.** `org.opencontainers.image.ref.name`, `io.stellaops.export.manifest-digest`, and `io.stellaops.export.provenance-ref` allow registry tooling to locate manifests/signatures quickly. +- **Offline Kit staging.** Offline kit assembler copies `manifests/`, `signatures/`, and `pubkeys/` verbatim. Verification scripts (`offline-kits/bin/verify-export.sh`) wrap the cosign commands described above. + +--- + +## 7. Failure handling & observability + +- Runs surface signature status via `/api/export/runs/{id}` (`signing.status`, `signing.lastError`). Common errors include `ERR_EXPORT_KMS_UNAVAILABLE`, `ERR_EXPORT_ATTESTATION_FAILED`, `ERR_EXPORT_CANONICALIZE`. +- Metrics: `exporter_sign_duration_seconds`, `exporter_sign_failures_total{error_code}`, `exporter_provenance_verify_failures_total`. +- Logs: `phase=sign`, `error_code`, `signing_key_id`, `cosign_certificate_sn`. +- Alerts: DevOps dashboards (task `DEVOPS-EXPORT-37-001`) trigger on consecutive signing failures or verification failures >0. + +When verification fails downstream, operators should: +1. Confirm signatures using the known-good key. +2. Inspect `provenance.json` materials; rerun the source queries to ensure matching digests. +3. Review run audit logs and retry export with `--resume` to regenerate manifests. + +--- + +## 8. Compliance checklist + +- [ ] Manifests and provenance documents generated with canonical JSON, deterministic digests, and signatures. +- [ ] Cosign public keys published per tenant, rotated through Authority, and distributed to Offline Kit consumers. +- [ ] SLSA attestations enabled where supply-chain requirements demand Level 3 evidence. +- [ ] CLI/Console verification paths documented and tested (CI pipelines exercise `stella export verify`). +- [ ] Encryption metadata (recipients, wrapped keys) recorded in provenance and validated during verification. +- [ ] Run audit logs capture signature timestamps, signer identity, and failure reasons. + +--- + +> **Imposed rule reminder:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. diff --git a/docs/modules/issuer-directory/operations/backup-restore.md b/docs/modules/issuer-directory/operations/backup-restore.md index f8c6ab33..4fc3505d 100644 --- a/docs/modules/issuer-directory/operations/backup-restore.md +++ b/docs/modules/issuer-directory/operations/backup-restore.md @@ -88,6 +88,7 @@ - `curl -fsSL https://localhost:8447/health/live` - Issue an access token and list issuers to confirm results. - Check Mongo counts match expectations (`db.issuers.countDocuments()`, etc.). + - Confirm Prometheus scrapes `issuer_directory_changes_total` and `issuer_directory_key_operations_total` for the tenants you restored. ## Disaster recovery notes - **Retention:** Maintain 30 daily + 12 monthly archives. Store copies in geographically separate, access-controlled vaults. @@ -98,6 +99,6 @@ ## Verification checklist - [ ] `/health/live` returns `200 OK`. - [ ] Mongo collections (`issuers`, `issuer_keys`, `issuer_trust_overrides`) have expected counts. -- [ ] `issuer_directory_changes_total` and `issuer_directory_key_operations_total` metrics resume within 1 minute. +- [ ] `issuer_directory_changes_total`, `issuer_directory_key_operations_total`, and `issuer_directory_key_validation_failures_total` metrics resume within 1 minute. - [ ] Audit entries exist for post-restore CRUD activity. - [ ] Client integrations (VEX Lens, Excititor) resolve issuers successfully. diff --git a/docs/modules/issuer-directory/operations/deployment.md b/docs/modules/issuer-directory/operations/deployment.md index 8fb2da8f..276d1726 100644 --- a/docs/modules/issuer-directory/operations/deployment.md +++ b/docs/modules/issuer-directory/operations/deployment.md @@ -39,6 +39,13 @@ ``` Compose automatically mounts `../../etc/issuer-directory.yaml` into the container at `/etc/issuer-directory.yaml`, seeds CSAF publishers, and exposes the API on `https://localhost:8447`. +### Compose environment variables +| Variable | Purpose | Default | +| --- | --- | --- | +| `ISSUER_DIRECTORY_PORT` | Host port that maps to container port `8080`. | `8447` | +| `ISSUER_DIRECTORY_MONGO_CONNECTION_STRING` | Injected into `ISSUERDIRECTORY__MONGO__CONNECTIONSTRING`; should contain credentials. | `mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017` | +| `ISSUER_DIRECTORY_SEED_CSAF` | Toggles CSAF bootstrap on startup. Set to `false` after the first production import if you manage issuers manually. | `true` | + 4. **Smoke test** ```bash curl -k https://localhost:8447/health/live diff --git a/docs/modules/issuer-directory/operations/offline-kit.md b/docs/modules/issuer-directory/operations/offline-kit.md index b68adece..2d8e2bb7 100644 --- a/docs/modules/issuer-directory/operations/offline-kit.md +++ b/docs/modules/issuer-directory/operations/offline-kit.md @@ -12,6 +12,7 @@ Include the following artefacts in your Offline Update Kit staging tree: | `config/issuer-directory/issuer-directory.yaml` | `etc/issuer-directory.yaml` (customised) | Replace Authority issuer, tenant header, and log level as required. | | `config/issuer-directory/csaf-publishers.json` | `src/IssuerDirectory/StellaOps.IssuerDirectory/data/csaf-publishers.json` or regional override | Operators can edit before import to add private publishers. | | `secrets/issuer-directory/connection.env` | Secure secret store export (`ISSUER_DIRECTORY_MONGO_CONNECTION_STRING=`) | Encrypt at rest; Offline Kit importer places it in the Compose/Helm secret. | +| `env/issuer-directory.env` (optional) | Curated `.env` snippet (for example `ISSUER_DIRECTORY_SEED_CSAF=false`) | Helps operators disable reseeding after their first import without editing the main profile. | | `docs/issuer-directory/deployment.md` | `docs/modules/issuer-directory/operations/deployment.md` | Ship alongside kit documentation for operators. | > **Image digests:** Update `deploy/releases/2025.10-edge.yaml` (or the relevant manifest) with the exact digest before building the kit so `offline-manifest.json` can assert integrity. @@ -69,3 +70,4 @@ Include the following artefacts in your Offline Update Kit staging tree: - [ ] `/issuer-directory/issuers` returns global seed issuers (requires token with `issuer-directory:read` scope). - [ ] Audit collection receives entries when you create/update issuers offline. - [ ] Offline kit manifest (`offline-manifest.json`) lists `images/issuer-directory-web.tar` and `config/issuer-directory/issuer-directory.yaml` with SHA-256 values you recorded during packaging. +- [ ] Prometheus in the offline environment reports `issuer_directory_changes_total` for the tenants imported from the kit. diff --git a/docs/modules/notify/architecture.md b/docs/modules/notify/architecture.md index 40322973..0e22af75 100644 --- a/docs/modules/notify/architecture.md +++ b/docs/modules/notify/architecture.md @@ -1,318 +1,319 @@ -> **Scope.** Implementation‑ready architecture for **Notify** (aligned with Epic 11 – Notifications Studio): a rules‑driven, tenant‑aware notification service that consumes platform events (scan completed, report ready, rescan deltas, attestation logged, admission decisions, etc.), evaluates operator‑defined routing rules, renders **channel‑specific messages** (Slack/Teams/Email/Webhook), and delivers them **reliably** with idempotency, throttling, and digests. It is UI‑managed, auditable, and safe by default (no secrets leakage, no spam storms). - ---- - -## 0) Mission & boundaries - -**Mission.** Convert **facts** from Stella Ops into **actionable, noise‑controlled** signals where teams already live (chat/email/webhooks), with **explainable** reasons and deep links to the UI. - -**Boundaries.** - -* Notify **does not make policy decisions** and **does not rescan**; it **consumes** events from Scanner/Scheduler/Vexer/Feedser/Attestor/Zastava and routes them. -* Attachments are **links** (UI/attestation pages); Notify **does not** attach SBOMs or large blobs to messages. -* Secrets for channels (Slack tokens, SMTP creds) are **referenced**, not stored raw in Mongo. - ---- - -## 1) Runtime shape & projects - -``` -src/ - ├─ StellaOps.Notify.WebService/ # REST: rules/channels CRUD, test send, deliveries browse - ├─ StellaOps.Notify.Worker/ # consumers + evaluators + renderers + delivery workers - ├─ StellaOps.Notify.Connectors.* / # channel plug-ins: Slack, Teams, Email, Webhook (v1) - │ └─ *.Tests/ - ├─ StellaOps.Notify.Engine/ # rules engine, templates, idempotency, digests, throttles - ├─ StellaOps.Notify.Models/ # DTOs (Rule, Channel, Event, Delivery, Template) - ├─ StellaOps.Notify.Storage.Mongo/ # rules, channels, deliveries, digests, locks - ├─ StellaOps.Notify.Queue/ # bus client (Redis Streams/NATS JetStream) - └─ StellaOps.Notify.Tests.* # unit/integration/e2e -``` - -**Deployables**: - -* **Notify.WebService** (stateless API) -* **Notify.Worker** (horizontal scale) - -**Dependencies**: Authority (OpToks; DPoP/mTLS), MongoDB, Redis/NATS (bus), HTTP egress to Slack/Teams/Webhooks, SMTP relay for Email. - -> **Configuration.** Notify.WebService bootstraps from `notify.yaml` (see `etc/notify.yaml.sample`). Use `storage.driver: mongo` with a production connection string; the optional `memory` driver exists only for tests. Authority settings follow the platform defaults—when running locally without Authority, set `authority.enabled: false` and supply `developmentSigningKey` so JWTs can be validated offline. -> -> `api.rateLimits` exposes token-bucket controls for delivery history queries and test-send previews (`deliveryHistory`, `testSend`). Default values allow generous browsing while preventing accidental bursts; operators can relax/tighten the buckets per deployment. - -> **Plug-ins.** All channel connectors are packaged under `/plugins/notify`. The ordered load list must start with Slack/Teams before Email/Webhook so chat-first actions are registered deterministically for Offline Kit bundles: -> -> ```yaml -> plugins: -> baseDirectory: "/var/opt/stellaops" -> directory: "plugins/notify" -> orderedPlugins: -> - StellaOps.Notify.Connectors.Slack -> - StellaOps.Notify.Connectors.Teams -> - StellaOps.Notify.Connectors.Email -> - StellaOps.Notify.Connectors.Webhook -> ``` -> -> The Offline Kit job simply copies the `plugins/notify` tree into the air-gapped bundle; the ordered list keeps connector manifests stable across environments. - -> **Authority clients.** Register two OAuth clients in StellaOps Authority: `notify-web-dev` (audience `notify.dev`) for development and `notify-web` (audience `notify`) for staging/production. Both require `notify.read` and `notify.admin` scopes and use DPoP-bound client credentials (`client_secret` in the samples). Reference entries live in `etc/authority.yaml.sample`, with placeholder secrets under `etc/secrets/notify-web*.secret.example`. - ---- - -## 2) Responsibilities - -1. **Ingest** platform events from internal bus with strong ordering per key (e.g., image digest). -2. **Evaluate rules** (tenant‑scoped) with matchers: severity changes, namespaces, repos, labels, KEV flags, provider provenance (VEX), component keys, admission decisions, etc. -3. **Control noise**: **throttle**, **coalesce** (digest windows), and **dedupe** via idempotency keys. -4. **Render** channel‑specific messages using safe templates; include **evidence** and **links**. -5. **Deliver** with retries/backoff; record outcome; expose delivery history to UI. -6. **Test** paths (send test to channel targets) without touching live rules. -7. **Audit**: log who configured what, when, and why a message was sent. - ---- - -## 3) Event model (inputs) - -Notify subscribes to the **internal event bus** (produced by services, escaped JSON; gzip allowed with caps): - -* `scanner.scan.completed` — new SBOM(s) composed; artifacts ready -* `scanner.report.ready` — analysis verdict (policy+vex) available; carries deltas summary -* `scheduler.rescan.delta` — new findings after Feedser/Vexer deltas (already summarized) -* `attestor.logged` — Rekor UUID returned (sbom/report/vex export) -* `zastava.admission` — admit/deny with reasons, namespace, image digests -* `feedser.export.completed` — new export ready (rarely notified directly; usually drives Scheduler) -* `vexer.export.completed` — new consensus snapshot (ditto) - -**Canonical envelope (bus → Notify.Engine):** - -```json -{ - "eventId": "uuid", - "kind": "scanner.report.ready", - "tenant": "tenant-01", - "ts": "2025-10-18T05:41:22Z", - "actor": "scanner-webservice", - "scope": { "namespace":"payments", "repo":"ghcr.io/acme/api", "digest":"sha256:..." }, - "payload": { /* kind-specific fields, see below */ } -} -``` - -**Examples (payload cores):** - -* `scanner.report.ready`: - - ```json - { - "reportId": "report-3def...", - "verdict": "fail", - "summary": {"total": 12, "blocked": 2, "warned": 3, "ignored": 5, "quieted": 2}, - "delta": {"newCritical": 1, "kev": ["CVE-2025-..."]}, - "links": {"ui": "https://ui/.../reports/report-3def...", "rekor": "https://rekor/..."}, - "dsse": { "...": "..." }, - "report": { "...": "..." } - } - ``` - - Payload embeds both the canonical report document and the DSSE envelope so connectors, Notify, and UI tooling can reuse the signed bytes without re-serialising. - -* `scanner.scan.completed`: - - ```json - { - "reportId": "report-3def...", - "digest": "sha256:...", - "verdict": "fail", - "summary": {"total": 12, "blocked": 2, "warned": 3, "ignored": 5, "quieted": 2}, - "delta": {"newCritical": 1, "kev": ["CVE-2025-..."]}, - "policy": {"revisionId": "rev-42", "digest": "27d2..."}, - "findings": [{"id": "finding-1", "severity": "Critical", "cve": "CVE-2025-...", "reachability": "runtime"}], - "dsse": { "...": "..." } - } - ``` - -* `zastava.admission`: - - ```json - { "decision":"deny|allow", "reasons":["unsigned image","missing SBOM"], - "images":[{"digest":"sha256:...","signed":false,"hasSbom":false}] } - ``` - ---- - -## 4) Rules engine — semantics - -**Rule shape (simplified):** - -```yaml -name: "high-critical-alerts-prod" -enabled: true -match: - eventKinds: ["scanner.report.ready","scheduler.rescan.delta","zastava.admission"] - namespaces: ["prod-*"] - repos: ["ghcr.io/acme/*"] - minSeverity: "high" # min of new findings (delta context) - kev: true # require KEV-tagged or allow any if false - verdict: ["fail","deny"] # filter for report/admission - vex: - includeRejectedJustifications: false # notify only on accepted 'affected' -actions: - - channel: "slack:sec-alerts" # reference to Channel object - template: "concise" - throttle: "5m" - - channel: "email:soc" - digest: "hourly" - template: "detailed" -``` - -**Evaluation order** - -1. **Tenant check** → discard if rule tenant ≠ event tenant. -2. **Kind filter** → discard early. -3. **Scope match** (namespace/repo/labels). -4. **Delta/severity gates** (if event carries `delta`). -5. **VEX gate** (drop if event’s finding is not affected under policy consensus unless rule says otherwise). -6. **Throttling/dedup** (idempotency key) — skip if suppressed. -7. **Actions** → enqueue per‑channel job(s). - -**Idempotency key**: `hash(ruleId | actionId | event.kind | scope.digest | delta.hash | day-bucket)`; ensures “same alert” doesn’t fire more than once within throttle window. - -**Digest windows**: maintain per action a **coalescer**: - -* Window: `5m|15m|1h|1d` (configurable); coalesces events by tenant + namespace/repo or by digest group. -* Digest messages summarize top N items and counts, with safe truncation. - ---- - -## 5) Channels & connectors (plug‑ins) - -Channel config is **two‑part**: a **Channel** record (name, type, options) and a Secret **reference** (Vault/K8s Secret). Connectors are **restart-time plug-ins** discovered on service start (same manifest convention as Concelier/Excititor) and live under `plugins/notify//`. - -**Built‑in v1:** - -* **Slack**: Bot token (xoxb‑…), `chat.postMessage` + `blocks`; rate limit aware (HTTP 429). -* **Microsoft Teams**: Incoming Webhook (or Graph card later); adaptive card payloads. -* **Email (SMTP)**: TLS (STARTTLS or implicit), From/To/CC/BCC; HTML+text alt; DKIM optional. -* **Generic Webhook**: POST JSON with HMAC signature (Ed25519 or SHA‑256) in headers. - -**Connector contract:** (implemented by plug-in assemblies) - -```csharp -public interface INotifyConnector { - string Type { get; } // "slack" | "teams" | "email" | "webhook" | ... - Task SendAsync(DeliveryContext ctx, CancellationToken ct); - Task HealthAsync(ChannelConfig cfg, CancellationToken ct); -} -``` - -**DeliveryContext** includes **rendered content** and **raw event** for audit. - -**Test-send previews.** Plug-ins can optionally implement `INotifyChannelTestProvider` to shape `/channels/{id}/test` responses. Providers receive a sanitised `ChannelTestPreviewContext` (channel, tenant, target, timestamp, trace) and return a `NotifyDeliveryRendered` preview + metadata. When no provider is present, the host falls back to a generic preview so the endpoint always responds. - -**Secrets**: `ChannelConfig.secretRef` points to Authority‑managed secret handle or K8s Secret path; workers load at send-time; plug-in manifests (`notify-plugin.json`) declare capabilities and version. - ---- - -## 6) Templates & rendering - -**Template engine**: strongly typed, safe Handlebars‑style; no arbitrary code. Partial templates per channel. Deterministic outputs (prop order, no locale drift unless requested). - -**Variables** (examples): - -* `event.kind`, `event.ts`, `scope.namespace`, `scope.repo`, `scope.digest` -* `payload.verdict`, `payload.delta.newCritical`, `payload.links.ui`, `payload.links.rekor` -* `topFindings[]` with `purl`, `vulnId`, `severity` -* `policy.name`, `policy.revision` (if available) - -**Helpers**: - -* `severity_icon(sev)`, `link(text,url)`, `pluralize(n, "finding")`, `truncate(text, n)`, `code(text)`. - -**Channel mapping**: - -* Slack: title + blocks, limited to 50 blocks/3000 chars per section; long lists → link to UI. -* Teams: Adaptive Card schema 1.5; fallback text for older channels (surfaced as `teams.fallbackText` metadata alongside webhook hash). -* Email: HTML + text; inline table of top N findings, rest behind UI link. -* Webhook: JSON with `event`, `ruleId`, `actionId`, `summary`, `links`, and raw `payload` subset. - -**i18n**: template set per locale (English default; Bulgarian built‑in). - ---- - -## 7) Data model (Mongo) - -Canonical JSON Schemas for rules/channels/events live in `docs/modules/notify/resources/schemas/`. Sample payloads intended for tests/UI mock responses are captured in `docs/modules/notify/resources/samples/`. - -**Database**: `notify` - -* `rules` - - ``` - { _id, tenantId, name, enabled, match, actions, createdBy, updatedBy, createdAt, updatedAt } - ``` - -* `channels` - - ``` - { _id, tenantId, name:"slack:sec-alerts", type:"slack", - config:{ webhookUrl?:"", channel:"#sec-alerts", workspace?: "...", secretRef:"ref://..." }, - createdAt, updatedAt } - ``` - -* `deliveries` - - ``` - { _id, tenantId, ruleId, actionId, eventId, kind, scope, status:"sent|failed|throttled|digested|dropped", - attempts:[{ts, status, code, reason}], - rendered:{ title, body, target }, // redacted for PII; body hash stored - sentAt, lastError? } - ``` - -* `digests` - - ``` - { _id, tenantId, actionKey, window:"hourly", openedAt, items:[{eventId, scope, delta}], status:"open|flushed" } - ``` - -* `throttles` - - ``` - { key:"idem:", ttlAt } // short-lived, also cached in Redis - ``` - -**Indexes**: rules by `{tenantId, enabled}`, deliveries by `{tenantId, sentAt desc}`, digests by `{tenantId, actionKey}`. - ---- - -## 8) External APIs (WebService) - -Base path: `/api/v1/notify` (Authority OpToks; scopes: `notify.admin` for write, `notify.read` for view). - -*All* REST calls require the tenant header `X-StellaOps-Tenant` (matches the canonical `tenantId` stored in Mongo). Payloads are normalised via `NotifySchemaMigration` before persistence to guarantee schema version pinning. - -Authentication today is stubbed with Bearer tokens (`Authorization: Bearer `). When Authority wiring lands, this will switch to OpTok validation + scope enforcement, but the header contract will remain the same. - -Service configuration exposes `notify:auth:*` keys (issuer, audience, signing key, scope names) so operators can wire the Authority JWKS or (in dev) a symmetric test key. `notify:storage:*` keys cover Mongo URI/database/collection overrides. Both sets are required for the new API surface. - -Internal tooling can hit `/internal/notify//normalize` to upgrade legacy JSON and return canonical output used in the docs fixtures. - -* **Channels** - - * `POST /channels` | `GET /channels` | `GET /channels/{id}` | `PATCH /channels/{id}` | `DELETE /channels/{id}` - * `POST /channels/{id}/test` → send sample message (no rule evaluation); returns `202 Accepted` with rendered preview + metadata (base keys: `channelType`, `target`, `previewProvider`, `traceId` + connector-specific entries); governed by `api.rateLimits:testSend`. -* `GET /channels/{id}/health` → connector self‑check (returns redacted metadata: secret refs hashed, sensitive config keys masked, fallbacks noted via `teams.fallbackText`/`teams.validation.*`) - -* **Rules** - - * `POST /rules` | `GET /rules` | `GET /rules/{id}` | `PATCH /rules/{id}` | `DELETE /rules/{id}` - * `POST /rules/{id}/test` → dry‑run rule against a **sample event** (no delivery unless `--send`) - -* **Deliveries** - - * `POST /deliveries` → ingest worker delivery state (idempotent via `deliveryId`). - * `GET /deliveries?since=...&status=...&limit=...` → list envelope `{ items, count, continuationToken }` (most recent first); base metadata keys match the test-send response (`channelType`, `target`, `previewProvider`, `traceId`); rate-limited via `api.rateLimits.deliveryHistory`. See `docs/modules/notify/resources/samples/notify-delivery-list-response.sample.json`. - * `GET /deliveries/{id}` → detail (redacted body + metadata) - * `POST /deliveries/{id}/retry` → force retry (admin, future sprint) - +> **Scope.** Implementation‑ready architecture for **Notify** (aligned with Epic 11 – Notifications Studio): a rules‑driven, tenant‑aware notification service that consumes platform events (scan completed, report ready, rescan deltas, attestation logged, admission decisions, etc.), evaluates operator‑defined routing rules, renders **channel‑specific messages** (Slack/Teams/Email/Webhook), and delivers them **reliably** with idempotency, throttling, and digests. It is UI‑managed, auditable, and safe by default (no secrets leakage, no spam storms). + +--- + +## 0) Mission & boundaries + +**Mission.** Convert **facts** from Stella Ops into **actionable, noise‑controlled** signals where teams already live (chat/email/webhooks), with **explainable** reasons and deep links to the UI. + +**Boundaries.** + +* Notify **does not make policy decisions** and **does not rescan**; it **consumes** events from Scanner/Scheduler/Excitor/Conselier/Attestor/Zastava and routes them. +* Attachments are **links** (UI/attestation pages); Notify **does not** attach SBOMs or large blobs to messages. +* Secrets for channels (Slack tokens, SMTP creds) are **referenced**, not stored raw in Mongo. +* **2025-11-02 module boundary.** Maintain `src/Notify/` as the reusable notification toolkit (engine, storage, queue, connectors) and `src/Notifier/` as the Notifications Studio host that composes those libraries. Do not merge directories without an approved packaging RFC that covers build impacts, offline kit parity, and cross-module governance. + +--- + +## 1) Runtime shape & projects + +``` +src/ + ├─ StellaOps.Notify.WebService/ # REST: rules/channels CRUD, test send, deliveries browse + ├─ StellaOps.Notify.Worker/ # consumers + evaluators + renderers + delivery workers + ├─ StellaOps.Notify.Connectors.* / # channel plug-ins: Slack, Teams, Email, Webhook (v1) + │ └─ *.Tests/ + ├─ StellaOps.Notify.Engine/ # rules engine, templates, idempotency, digests, throttles + ├─ StellaOps.Notify.Models/ # DTOs (Rule, Channel, Event, Delivery, Template) + ├─ StellaOps.Notify.Storage.Mongo/ # rules, channels, deliveries, digests, locks + ├─ StellaOps.Notify.Queue/ # bus client (Redis Streams/NATS JetStream) + └─ StellaOps.Notify.Tests.* # unit/integration/e2e +``` + +**Deployables**: + +* **Notify.WebService** (stateless API) +* **Notify.Worker** (horizontal scale) + +**Dependencies**: Authority (OpToks; DPoP/mTLS), MongoDB, Redis/NATS (bus), HTTP egress to Slack/Teams/Webhooks, SMTP relay for Email. + +> **Configuration.** Notify.WebService bootstraps from `notify.yaml` (see `etc/notify.yaml.sample`). Use `storage.driver: mongo` with a production connection string; the optional `memory` driver exists only for tests. Authority settings follow the platform defaults—when running locally without Authority, set `authority.enabled: false` and supply `developmentSigningKey` so JWTs can be validated offline. +> +> `api.rateLimits` exposes token-bucket controls for delivery history queries and test-send previews (`deliveryHistory`, `testSend`). Default values allow generous browsing while preventing accidental bursts; operators can relax/tighten the buckets per deployment. + +> **Plug-ins.** All channel connectors are packaged under `/plugins/notify`. The ordered load list must start with Slack/Teams before Email/Webhook so chat-first actions are registered deterministically for Offline Kit bundles: +> +> ```yaml +> plugins: +> baseDirectory: "/var/opt/stellaops" +> directory: "plugins/notify" +> orderedPlugins: +> - StellaOps.Notify.Connectors.Slack +> - StellaOps.Notify.Connectors.Teams +> - StellaOps.Notify.Connectors.Email +> - StellaOps.Notify.Connectors.Webhook +> ``` +> +> The Offline Kit job simply copies the `plugins/notify` tree into the air-gapped bundle; the ordered list keeps connector manifests stable across environments. + +> **Authority clients.** Register two OAuth clients in StellaOps Authority: `notify-web-dev` (audience `notify.dev`) for development and `notify-web` (audience `notify`) for staging/production. Both require `notify.read` and `notify.admin` scopes and use DPoP-bound client credentials (`client_secret` in the samples). Reference entries live in `etc/authority.yaml.sample`, with placeholder secrets under `etc/secrets/notify-web*.secret.example`. + +--- + +## 2) Responsibilities + +1. **Ingest** platform events from internal bus with strong ordering per key (e.g., image digest). +2. **Evaluate rules** (tenant‑scoped) with matchers: severity changes, namespaces, repos, labels, KEV flags, provider provenance (VEX), component keys, admission decisions, etc. +3. **Control noise**: **throttle**, **coalesce** (digest windows), and **dedupe** via idempotency keys. +4. **Render** channel‑specific messages using safe templates; include **evidence** and **links**. +5. **Deliver** with retries/backoff; record outcome; expose delivery history to UI. +6. **Test** paths (send test to channel targets) without touching live rules. +7. **Audit**: log who configured what, when, and why a message was sent. + +--- + +## 3) Event model (inputs) + +Notify subscribes to the **internal event bus** (produced by services, escaped JSON; gzip allowed with caps): + +* `scanner.scan.completed` — new SBOM(s) composed; artifacts ready +* `scanner.report.ready` — analysis verdict (policy+vex) available; carries deltas summary +* `scheduler.rescan.delta` — new findings after Conselier/Excitor deltas (already summarized) +* `attestor.logged` — Rekor UUID returned (sbom/report/vex export) +* `zastava.admission` — admit/deny with reasons, namespace, image digests +* `conselier.export.completed` — new export ready (rarely notified directly; usually drives Scheduler) +* `excitor.export.completed` — new consensus snapshot (ditto) + +**Canonical envelope (bus → Notify.Engine):** + +```json +{ + "eventId": "uuid", + "kind": "scanner.report.ready", + "tenant": "tenant-01", + "ts": "2025-10-18T05:41:22Z", + "actor": "scanner-webservice", + "scope": { "namespace":"payments", "repo":"ghcr.io/acme/api", "digest":"sha256:..." }, + "payload": { /* kind-specific fields, see below */ } +} +``` + +**Examples (payload cores):** + +* `scanner.report.ready`: + + ```json + { + "reportId": "report-3def...", + "verdict": "fail", + "summary": {"total": 12, "blocked": 2, "warned": 3, "ignored": 5, "quieted": 2}, + "delta": {"newCritical": 1, "kev": ["CVE-2025-..."]}, + "links": {"ui": "https://ui/.../reports/report-3def...", "rekor": "https://rekor/..."}, + "dsse": { "...": "..." }, + "report": { "...": "..." } + } + ``` + + Payload embeds both the canonical report document and the DSSE envelope so connectors, Notify, and UI tooling can reuse the signed bytes without re-serialising. + +* `scanner.scan.completed`: + + ```json + { + "reportId": "report-3def...", + "digest": "sha256:...", + "verdict": "fail", + "summary": {"total": 12, "blocked": 2, "warned": 3, "ignored": 5, "quieted": 2}, + "delta": {"newCritical": 1, "kev": ["CVE-2025-..."]}, + "policy": {"revisionId": "rev-42", "digest": "27d2..."}, + "findings": [{"id": "finding-1", "severity": "Critical", "cve": "CVE-2025-...", "reachability": "runtime"}], + "dsse": { "...": "..." } + } + ``` + +* `zastava.admission`: + + ```json + { "decision":"deny|allow", "reasons":["unsigned image","missing SBOM"], + "images":[{"digest":"sha256:...","signed":false,"hasSbom":false}] } + ``` + +--- + +## 4) Rules engine — semantics + +**Rule shape (simplified):** + +```yaml +name: "high-critical-alerts-prod" +enabled: true +match: + eventKinds: ["scanner.report.ready","scheduler.rescan.delta","zastava.admission"] + namespaces: ["prod-*"] + repos: ["ghcr.io/acme/*"] + minSeverity: "high" # min of new findings (delta context) + kev: true # require KEV-tagged or allow any if false + verdict: ["fail","deny"] # filter for report/admission + vex: + includeRejectedJustifications: false # notify only on accepted 'affected' +actions: + - channel: "slack:sec-alerts" # reference to Channel object + template: "concise" + throttle: "5m" + - channel: "email:soc" + digest: "hourly" + template: "detailed" +``` + +**Evaluation order** + +1. **Tenant check** → discard if rule tenant ≠ event tenant. +2. **Kind filter** → discard early. +3. **Scope match** (namespace/repo/labels). +4. **Delta/severity gates** (if event carries `delta`). +5. **VEX gate** (drop if event’s finding is not affected under policy consensus unless rule says otherwise). +6. **Throttling/dedup** (idempotency key) — skip if suppressed. +7. **Actions** → enqueue per‑channel job(s). + +**Idempotency key**: `hash(ruleId | actionId | event.kind | scope.digest | delta.hash | day-bucket)`; ensures “same alert” doesn’t fire more than once within throttle window. + +**Digest windows**: maintain per action a **coalescer**: + +* Window: `5m|15m|1h|1d` (configurable); coalesces events by tenant + namespace/repo or by digest group. +* Digest messages summarize top N items and counts, with safe truncation. + +--- + +## 5) Channels & connectors (plug‑ins) + +Channel config is **two‑part**: a **Channel** record (name, type, options) and a Secret **reference** (Vault/K8s Secret). Connectors are **restart-time plug-ins** discovered on service start (same manifest convention as Concelier/Excititor) and live under `plugins/notify//`. + +**Built‑in v1:** + +* **Slack**: Bot token (xoxb‑…), `chat.postMessage` + `blocks`; rate limit aware (HTTP 429). +* **Microsoft Teams**: Incoming Webhook (or Graph card later); adaptive card payloads. +* **Email (SMTP)**: TLS (STARTTLS or implicit), From/To/CC/BCC; HTML+text alt; DKIM optional. +* **Generic Webhook**: POST JSON with HMAC signature (Ed25519 or SHA‑256) in headers. + +**Connector contract:** (implemented by plug-in assemblies) + +```csharp +public interface INotifyConnector { + string Type { get; } // "slack" | "teams" | "email" | "webhook" | ... + Task SendAsync(DeliveryContext ctx, CancellationToken ct); + Task HealthAsync(ChannelConfig cfg, CancellationToken ct); +} +``` + +**DeliveryContext** includes **rendered content** and **raw event** for audit. + +**Test-send previews.** Plug-ins can optionally implement `INotifyChannelTestProvider` to shape `/channels/{id}/test` responses. Providers receive a sanitised `ChannelTestPreviewContext` (channel, tenant, target, timestamp, trace) and return a `NotifyDeliveryRendered` preview + metadata. When no provider is present, the host falls back to a generic preview so the endpoint always responds. + +**Secrets**: `ChannelConfig.secretRef` points to Authority‑managed secret handle or K8s Secret path; workers load at send-time; plug-in manifests (`notify-plugin.json`) declare capabilities and version. + +--- + +## 6) Templates & rendering + +**Template engine**: strongly typed, safe Handlebars‑style; no arbitrary code. Partial templates per channel. Deterministic outputs (prop order, no locale drift unless requested). + +**Variables** (examples): + +* `event.kind`, `event.ts`, `scope.namespace`, `scope.repo`, `scope.digest` +* `payload.verdict`, `payload.delta.newCritical`, `payload.links.ui`, `payload.links.rekor` +* `topFindings[]` with `purl`, `vulnId`, `severity` +* `policy.name`, `policy.revision` (if available) + +**Helpers**: + +* `severity_icon(sev)`, `link(text,url)`, `pluralize(n, "finding")`, `truncate(text, n)`, `code(text)`. + +**Channel mapping**: + +* Slack: title + blocks, limited to 50 blocks/3000 chars per section; long lists → link to UI. +* Teams: Adaptive Card schema 1.5; fallback text for older channels (surfaced as `teams.fallbackText` metadata alongside webhook hash). +* Email: HTML + text; inline table of top N findings, rest behind UI link. +* Webhook: JSON with `event`, `ruleId`, `actionId`, `summary`, `links`, and raw `payload` subset. + +**i18n**: template set per locale (English default; Bulgarian built‑in). + +--- + +## 7) Data model (Mongo) + +Canonical JSON Schemas for rules/channels/events live in `docs/modules/notify/resources/schemas/`. Sample payloads intended for tests/UI mock responses are captured in `docs/modules/notify/resources/samples/`. + +**Database**: `notify` + +* `rules` + + ``` + { _id, tenantId, name, enabled, match, actions, createdBy, updatedBy, createdAt, updatedAt } + ``` + +* `channels` + + ``` + { _id, tenantId, name:"slack:sec-alerts", type:"slack", + config:{ webhookUrl?:"", channel:"#sec-alerts", workspace?: "...", secretRef:"ref://..." }, + createdAt, updatedAt } + ``` + +* `deliveries` + + ``` + { _id, tenantId, ruleId, actionId, eventId, kind, scope, status:"sent|failed|throttled|digested|dropped", + attempts:[{ts, status, code, reason}], + rendered:{ title, body, target }, // redacted for PII; body hash stored + sentAt, lastError? } + ``` + +* `digests` + + ``` + { _id, tenantId, actionKey, window:"hourly", openedAt, items:[{eventId, scope, delta}], status:"open|flushed" } + ``` + +* `throttles` + + ``` + { key:"idem:", ttlAt } // short-lived, also cached in Redis + ``` + +**Indexes**: rules by `{tenantId, enabled}`, deliveries by `{tenantId, sentAt desc}`, digests by `{tenantId, actionKey}`. + +--- + +## 8) External APIs (WebService) + +Base path: `/api/v1/notify` (Authority OpToks; scopes: `notify.admin` for write, `notify.read` for view). + +*All* REST calls require the tenant header `X-StellaOps-Tenant` (matches the canonical `tenantId` stored in Mongo). Payloads are normalised via `NotifySchemaMigration` before persistence to guarantee schema version pinning. + +Authentication today is stubbed with Bearer tokens (`Authorization: Bearer `). When Authority wiring lands, this will switch to OpTok validation + scope enforcement, but the header contract will remain the same. + +Service configuration exposes `notify:auth:*` keys (issuer, audience, signing key, scope names) so operators can wire the Authority JWKS or (in dev) a symmetric test key. `notify:storage:*` keys cover Mongo URI/database/collection overrides. Both sets are required for the new API surface. + +Internal tooling can hit `/internal/notify//normalize` to upgrade legacy JSON and return canonical output used in the docs fixtures. + +* **Channels** + + * `POST /channels` | `GET /channels` | `GET /channels/{id}` | `PATCH /channels/{id}` | `DELETE /channels/{id}` + * `POST /channels/{id}/test` → send sample message (no rule evaluation); returns `202 Accepted` with rendered preview + metadata (base keys: `channelType`, `target`, `previewProvider`, `traceId` + connector-specific entries); governed by `api.rateLimits:testSend`. +* `GET /channels/{id}/health` → connector self‑check (returns redacted metadata: secret refs hashed, sensitive config keys masked, fallbacks noted via `teams.fallbackText`/`teams.validation.*`) + +* **Rules** + + * `POST /rules` | `GET /rules` | `GET /rules/{id}` | `PATCH /rules/{id}` | `DELETE /rules/{id}` + * `POST /rules/{id}/test` → dry‑run rule against a **sample event** (no delivery unless `--send`) + +* **Deliveries** + + * `POST /deliveries` → ingest worker delivery state (idempotent via `deliveryId`). + * `GET /deliveries?since=...&status=...&limit=...` → list envelope `{ items, count, continuationToken }` (most recent first); base metadata keys match the test-send response (`channelType`, `target`, `previewProvider`, `traceId`); rate-limited via `api.rateLimits.deliveryHistory`. See `docs/modules/notify/resources/samples/notify-delivery-list-response.sample.json`. + * `GET /deliveries/{id}` → detail (redacted body + metadata) + * `POST /deliveries/{id}/retry` → force retry (admin, future sprint) + * **Admin** * `GET /stats` (per tenant counts, last hour/day) @@ -335,192 +336,192 @@ Authority signs ack tokens using keys configured under `notifications.ackTokens` **Ingestion**: workers do **not** expose public ingestion; they **subscribe** to the internal bus. (Optional `/events/test` for integration testing, admin-only.) --- - -## 9) Delivery pipeline (worker) - -``` -[Event bus] → [Ingestor] → [RuleMatcher] → [Throttle/Dedupe] → [DigestCoalescer] → [Renderer] → [Connector] → [Result] - └────────→ [DeliveryStore] -``` - -* **Ingestor**: N consumers with per‑key ordering (key = tenant|digest|namespace). -* **RuleMatcher**: loads active rules snapshot for tenant into memory; vectorized predicate check. -* **Throttle/Dedupe**: consult Redis + Mongo `throttles`; if hit → record `status=throttled`. -* **DigestCoalescer**: append to open digest window or flush when timer expires. -* **Renderer**: select template (channel+locale), inject variables, enforce length limits, compute `bodyHash`. -* **Connector**: send; handle provider‑specific rate limits and backoffs; `maxAttempts` with exponential jitter; overflow → DLQ (dead‑letter topic) + UI surfacing. - -**Idempotency**: per action **idempotency key** stored in Redis (TTL = `throttle window` or `digest window`). Connectors also respect **provider** idempotency where available (e.g., Slack `client_msg_id`). - ---- - -## 10) Reliability & rate controls - -* **Per‑tenant** RPM caps (default 600/min) + **per‑channel** concurrency (Slack 1–4, Teams 1–2, Email 8–32 based on relay). -* **Backoff** map: Slack 429 → respect `Retry‑After`; SMTP 4xx → retry; 5xx → retry with jitter; permanent rejects → drop with status recorded. -* **DLQ**: NATS/Redis stream `notify.dlq` with `{event, rule, action, error}` for operator inspection; UI shows DLQ items. - ---- - -## 11) Security & privacy - -* **AuthZ**: all APIs require **Authority** OpToks; actions scoped by tenant. -* **Secrets**: `secretRef` only; Notify fetches just‑in‑time from Authority Secret proxy or K8s Secret (mounted). No plaintext secrets in Mongo. -* **Egress TLS**: validate SSL; pin domains per channel config; optional CA bundle override for on‑prem SMTP. -* **Webhook signing**: HMAC or Ed25519 signatures in `X-StellaOps-Signature` + replay‑window timestamp; include canonical body hash in header. -* **Redaction**: deliveries store **hashes** of bodies, not full payloads for chat/email to minimize PII retention (configurable). -* **Quiet hours**: per tenant (e.g., 22:00–06:00) route high‑sev only; defer others to digests. -* **Loop prevention**: Webhook target allowlist + event origin tags; do not ingest own webhooks. - ---- - -## 12) Observability (Prometheus + OTEL) - -* `notify.events_consumed_total{kind}` -* `notify.rules_matched_total{ruleId}` -* `notify.throttled_total{reason}` -* `notify.digest_coalesced_total{window}` -* `notify.sent_total{channel}` / `notify.failed_total{channel,code}` -* `notify.delivery_latency_seconds{channel}` (end‑to‑end) -* **Tracing**: spans `ingest`, `match`, `render`, `send`; correlation id = `eventId`. - -**SLO targets** - -* Event→delivery p95 **≤ 30–60 s** under nominal load. -* Failure rate p95 **< 0.5%** per hour (excluding provider outages). -* Duplicate rate **≈ 0** (idempotency working). - ---- - -## 13) Configuration (YAML) - -```yaml -notify: - authority: - issuer: "https://authority.internal" - require: "dpop" # or "mtls" - bus: - kind: "redis" # or "nats" - streams: - - "scanner.events" - - "scheduler.events" - - "attestor.events" - - "zastava.events" - mongo: - uri: "mongodb://mongo/notify" - limits: - perTenantRpm: 600 - perChannel: - slack: { concurrency: 2 } - teams: { concurrency: 1 } - email: { concurrency: 8 } - webhook: { concurrency: 8 } - digests: - defaultWindow: "1h" - maxItems: 100 - quietHours: - enabled: true - window: "22:00-06:00" - minSeverity: "critical" - webhooks: - sign: - method: "ed25519" # or "hmac-sha256" - keyRef: "ref://notify/webhook-sign-key" -``` - ---- - -## 14) UI touch‑points - -* **Notifications → Channels**: add Slack/Teams/Email/Webhook; run **health**; rotate secrets. -* **Notifications → Rules**: create/edit YAML rules with linting; test with sample events; see match rate. -* **Notifications → Deliveries**: timeline with filters (status, channel, rule); inspect last error; retry. -* **Digest preview**: shows current window contents and when it will flush. -* **Quiet hours**: configure per tenant; show overrides. -* **DLQ**: browse dead‑letters; requeue after fix. - ---- - -## 15) Failure modes & responses - -| Condition | Behavior | -| ----------------------------------- | ------------------------------------------------------------------------------------- | -| Slack 429 / Teams 429 | Respect `Retry‑After`, backoff with jitter, reduce concurrency | -| SMTP transient 4xx | Retry up to `maxAttempts`; escalate to DLQ on exhaust | -| Invalid channel secret | Mark channel unhealthy; suppress sends; surface in UI | -| Rule explosion (matches everything) | Safety valve: per‑tenant RPM caps; auto‑pause rule after X drops; UI alert | -| Bus outage | Buffer to local queue (bounded); resume consuming when healthy | -| Mongo slowness | Fall back to Redis throttles; batch write deliveries; shed low‑priority notifications | - ---- - -## 16) Testing matrix - -* **Unit**: matchers, throttle math, digest coalescing, idempotency keys, template rendering edge cases. -* **Connectors**: provider‑level rate limits, payload size truncation, error mapping. -* **Integration**: synthetic event storm (10k/min), ensure p95 latency & duplicate rate. -* **Security**: DPoP/mTLS on APIs; secretRef resolution; webhook signing & replay windows. -* **i18n**: localized templates render deterministically. -* **Chaos**: Slack/Teams API flaps; SMTP greylisting; Redis hiccups; ensure graceful degradation. - ---- - -## 17) Sequences (representative) - -**A) New criticals after Feedser delta (Slack immediate + Email hourly digest)** - -```mermaid -sequenceDiagram - autonumber - participant SCH as Scheduler - participant NO as Notify.Worker - participant SL as Slack - participant SMTP as Email - - SCH->>NO: bus event scheduler.rescan.delta { newCritical:1, digest:sha256:... } - NO->>NO: match rules (Slack immediate; Email hourly digest) - NO->>SL: chat.postMessage (concise) - SL-->>NO: 200 OK - NO->>NO: append to digest window (email:soc) - Note over NO: At window close → render digest email - NO->>SMTP: send email (detailed digest) - SMTP-->>NO: 250 OK -``` - -**B) Admission deny (Teams card + Webhook)** - -```mermaid -sequenceDiagram - autonumber - participant ZA as Zastava - participant NO as Notify.Worker - participant TE as Teams - participant WH as Webhook - - ZA->>NO: bus event zastava.admission { decision: "deny", reasons: [...] } - NO->>TE: POST adaptive card - TE-->>NO: 200 OK - NO->>WH: POST JSON (signed) - WH-->>NO: 2xx -``` - ---- - -## 18) Implementation notes - -* **Language**: .NET 10; minimal API; `System.Text.Json` with canonical writer for body hashing; Channels for pipelines. -* **Bus**: Redis Streams (**XGROUP** consumers) or NATS JetStream for at‑least‑once with ack; per‑tenant consumer groups to localize backpressure. -* **Templates**: compile and cache per rule+channel+locale; version with rule `updatedAt` to invalidate. -* **Rules**: store raw YAML + parsed AST; validate with schema + static checks (e.g., nonsensical combos). -* **Secrets**: pluggable secret resolver (Authority Secret proxy, K8s, Vault). -* **Rate limiting**: `System.Threading.RateLimiting` + per‑connector adapters. - ---- - -## 19) Roadmap (post‑v1) - -* **PagerDuty/Opsgenie** connectors; **Jira** ticket creation. -* **User inbox** (in‑app notifications) + mobile push via webhook relay. -* **Anomaly suppression**: auto‑pause noisy rules with hints (learned thresholds). -* **Graph rules**: “only notify if *not_affected → affected* transition at consensus layer”. -* **Label enrichment**: pluggable taggers (business criticality, data classification) to refine matchers. + +## 9) Delivery pipeline (worker) + +``` +[Event bus] → [Ingestor] → [RuleMatcher] → [Throttle/Dedupe] → [DigestCoalescer] → [Renderer] → [Connector] → [Result] + └────────→ [DeliveryStore] +``` + +* **Ingestor**: N consumers with per‑key ordering (key = tenant|digest|namespace). +* **RuleMatcher**: loads active rules snapshot for tenant into memory; vectorized predicate check. +* **Throttle/Dedupe**: consult Redis + Mongo `throttles`; if hit → record `status=throttled`. +* **DigestCoalescer**: append to open digest window or flush when timer expires. +* **Renderer**: select template (channel+locale), inject variables, enforce length limits, compute `bodyHash`. +* **Connector**: send; handle provider‑specific rate limits and backoffs; `maxAttempts` with exponential jitter; overflow → DLQ (dead‑letter topic) + UI surfacing. + +**Idempotency**: per action **idempotency key** stored in Redis (TTL = `throttle window` or `digest window`). Connectors also respect **provider** idempotency where available (e.g., Slack `client_msg_id`). + +--- + +## 10) Reliability & rate controls + +* **Per‑tenant** RPM caps (default 600/min) + **per‑channel** concurrency (Slack 1–4, Teams 1–2, Email 8–32 based on relay). +* **Backoff** map: Slack 429 → respect `Retry‑After`; SMTP 4xx → retry; 5xx → retry with jitter; permanent rejects → drop with status recorded. +* **DLQ**: NATS/Redis stream `notify.dlq` with `{event, rule, action, error}` for operator inspection; UI shows DLQ items. + +--- + +## 11) Security & privacy + +* **AuthZ**: all APIs require **Authority** OpToks; actions scoped by tenant. +* **Secrets**: `secretRef` only; Notify fetches just‑in‑time from Authority Secret proxy or K8s Secret (mounted). No plaintext secrets in Mongo. +* **Egress TLS**: validate SSL; pin domains per channel config; optional CA bundle override for on‑prem SMTP. +* **Webhook signing**: HMAC or Ed25519 signatures in `X-StellaOps-Signature` + replay‑window timestamp; include canonical body hash in header. +* **Redaction**: deliveries store **hashes** of bodies, not full payloads for chat/email to minimize PII retention (configurable). +* **Quiet hours**: per tenant (e.g., 22:00–06:00) route high‑sev only; defer others to digests. +* **Loop prevention**: Webhook target allowlist + event origin tags; do not ingest own webhooks. + +--- + +## 12) Observability (Prometheus + OTEL) + +* `notify.events_consumed_total{kind}` +* `notify.rules_matched_total{ruleId}` +* `notify.throttled_total{reason}` +* `notify.digest_coalesced_total{window}` +* `notify.sent_total{channel}` / `notify.failed_total{channel,code}` +* `notify.delivery_latency_seconds{channel}` (end‑to‑end) +* **Tracing**: spans `ingest`, `match`, `render`, `send`; correlation id = `eventId`. + +**SLO targets** + +* Event→delivery p95 **≤ 30–60 s** under nominal load. +* Failure rate p95 **< 0.5%** per hour (excluding provider outages). +* Duplicate rate **≈ 0** (idempotency working). + +--- + +## 13) Configuration (YAML) + +```yaml +notify: + authority: + issuer: "https://authority.internal" + require: "dpop" # or "mtls" + bus: + kind: "redis" # or "nats" + streams: + - "scanner.events" + - "scheduler.events" + - "attestor.events" + - "zastava.events" + mongo: + uri: "mongodb://mongo/notify" + limits: + perTenantRpm: 600 + perChannel: + slack: { concurrency: 2 } + teams: { concurrency: 1 } + email: { concurrency: 8 } + webhook: { concurrency: 8 } + digests: + defaultWindow: "1h" + maxItems: 100 + quietHours: + enabled: true + window: "22:00-06:00" + minSeverity: "critical" + webhooks: + sign: + method: "ed25519" # or "hmac-sha256" + keyRef: "ref://notify/webhook-sign-key" +``` + +--- + +## 14) UI touch‑points + +* **Notifications → Channels**: add Slack/Teams/Email/Webhook; run **health**; rotate secrets. +* **Notifications → Rules**: create/edit YAML rules with linting; test with sample events; see match rate. +* **Notifications → Deliveries**: timeline with filters (status, channel, rule); inspect last error; retry. +* **Digest preview**: shows current window contents and when it will flush. +* **Quiet hours**: configure per tenant; show overrides. +* **DLQ**: browse dead‑letters; requeue after fix. + +--- + +## 15) Failure modes & responses + +| Condition | Behavior | +| ----------------------------------- | ------------------------------------------------------------------------------------- | +| Slack 429 / Teams 429 | Respect `Retry‑After`, backoff with jitter, reduce concurrency | +| SMTP transient 4xx | Retry up to `maxAttempts`; escalate to DLQ on exhaust | +| Invalid channel secret | Mark channel unhealthy; suppress sends; surface in UI | +| Rule explosion (matches everything) | Safety valve: per‑tenant RPM caps; auto‑pause rule after X drops; UI alert | +| Bus outage | Buffer to local queue (bounded); resume consuming when healthy | +| Mongo slowness | Fall back to Redis throttles; batch write deliveries; shed low‑priority notifications | + +--- + +## 16) Testing matrix + +* **Unit**: matchers, throttle math, digest coalescing, idempotency keys, template rendering edge cases. +* **Connectors**: provider‑level rate limits, payload size truncation, error mapping. +* **Integration**: synthetic event storm (10k/min), ensure p95 latency & duplicate rate. +* **Security**: DPoP/mTLS on APIs; secretRef resolution; webhook signing & replay windows. +* **i18n**: localized templates render deterministically. +* **Chaos**: Slack/Teams API flaps; SMTP greylisting; Redis hiccups; ensure graceful degradation. + +--- + +## 17) Sequences (representative) + +**A) New criticals after Conselier delta (Slack immediate + Email hourly digest)** + +```mermaid +sequenceDiagram + autonumber + participant SCH as Scheduler + participant NO as Notify.Worker + participant SL as Slack + participant SMTP as Email + + SCH->>NO: bus event scheduler.rescan.delta { newCritical:1, digest:sha256:... } + NO->>NO: match rules (Slack immediate; Email hourly digest) + NO->>SL: chat.postMessage (concise) + SL-->>NO: 200 OK + NO->>NO: append to digest window (email:soc) + Note over NO: At window close → render digest email + NO->>SMTP: send email (detailed digest) + SMTP-->>NO: 250 OK +``` + +**B) Admission deny (Teams card + Webhook)** + +```mermaid +sequenceDiagram + autonumber + participant ZA as Zastava + participant NO as Notify.Worker + participant TE as Teams + participant WH as Webhook + + ZA->>NO: bus event zastava.admission { decision: "deny", reasons: [...] } + NO->>TE: POST adaptive card + TE-->>NO: 200 OK + NO->>WH: POST JSON (signed) + WH-->>NO: 2xx +``` + +--- + +## 18) Implementation notes + +* **Language**: .NET 10; minimal API; `System.Text.Json` with canonical writer for body hashing; Channels for pipelines. +* **Bus**: Redis Streams (**XGROUP** consumers) or NATS JetStream for at‑least‑once with ack; per‑tenant consumer groups to localize backpressure. +* **Templates**: compile and cache per rule+channel+locale; version with rule `updatedAt` to invalidate. +* **Rules**: store raw YAML + parsed AST; validate with schema + static checks (e.g., nonsensical combos). +* **Secrets**: pluggable secret resolver (Authority Secret proxy, K8s, Vault). +* **Rate limiting**: `System.Threading.RateLimiting` + per‑connector adapters. + +--- + +## 19) Roadmap (post‑v1) + +* **PagerDuty/Opsgenie** connectors; **Jira** ticket creation. +* **User inbox** (in‑app notifications) + mobile push via webhook relay. +* **Anomaly suppression**: auto‑pause noisy rules with hints (learned thresholds). +* **Graph rules**: “only notify if *not_affected → affected* transition at consensus layer”. +* **Label enrichment**: pluggable taggers (business criticality, data classification) to refine matchers. diff --git a/docs/modules/notify/resources/schemas/notify-event@1.json b/docs/modules/notify/resources/schemas/notify-event@1.json index 63c95f4e..783b35c1 100644 --- a/docs/modules/notify/resources/schemas/notify-event@1.json +++ b/docs/modules/notify/resources/schemas/notify-event@1.json @@ -1,56 +1,56 @@ -{ - "$id": "https://stella-ops.org/schemas/notify/notify-event@1.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Notify Event Envelope", - "type": "object", - "required": ["eventId", "kind", "tenant", "ts", "payload"], - "properties": { - "eventId": {"type": "string", "format": "uuid"}, - "kind": { - "type": "string", - "description": "Event kind identifier (e.g. scanner.report.ready).", - "enum": [ - "scanner.report.ready", - "scanner.scan.completed", - "scheduler.rescan.delta", - "attestor.logged", - "zastava.admission", - "feedser.export.completed", - "vexer.export.completed" - ] - }, - "version": {"type": "string"}, - "tenant": {"type": "string"}, - "ts": {"type": "string", "format": "date-time"}, - "actor": {"type": "string"}, - "scope": { - "type": "object", - "properties": { - "namespace": {"type": "string"}, - "repo": {"type": "string"}, - "digest": {"type": "string"}, - "component": {"type": "string"}, - "image": {"type": "string"}, - "labels": {"$ref": "#/$defs/stringMap"}, - "attributes": {"$ref": "#/$defs/stringMap"} - }, - "additionalProperties": false - }, - "payload": { - "type": "object", - "description": "Event specific body; see individual schemas for shapes.", - "additionalProperties": true - }, - "attributes": {"$ref": "#/$defs/stringMap"} - }, - "additionalProperties": false, - "$defs": { - "stringMap": { - "type": "object", - "patternProperties": { - ".*": {"type": "string"} - }, - "additionalProperties": false - } - } -} +{ + "$id": "https://stella-ops.org/schemas/notify/notify-event@1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Notify Event Envelope", + "type": "object", + "required": ["eventId", "kind", "tenant", "ts", "payload"], + "properties": { + "eventId": {"type": "string", "format": "uuid"}, + "kind": { + "type": "string", + "description": "Event kind identifier (e.g. scanner.report.ready).", + "enum": [ + "scanner.report.ready", + "scanner.scan.completed", + "scheduler.rescan.delta", + "attestor.logged", + "zastava.admission", + "conselier.export.completed", + "excitor.export.completed" + ] + }, + "version": {"type": "string"}, + "tenant": {"type": "string"}, + "ts": {"type": "string", "format": "date-time"}, + "actor": {"type": "string"}, + "scope": { + "type": "object", + "properties": { + "namespace": {"type": "string"}, + "repo": {"type": "string"}, + "digest": {"type": "string"}, + "component": {"type": "string"}, + "image": {"type": "string"}, + "labels": {"$ref": "#/$defs/stringMap"}, + "attributes": {"$ref": "#/$defs/stringMap"} + }, + "additionalProperties": false + }, + "payload": { + "type": "object", + "description": "Event specific body; see individual schemas for shapes.", + "additionalProperties": true + }, + "attributes": {"$ref": "#/$defs/stringMap"} + }, + "additionalProperties": false, + "$defs": { + "stringMap": { + "type": "object", + "patternProperties": { + ".*": {"type": "string"} + }, + "additionalProperties": false + } + } +} diff --git a/docs/modules/policy/AGENTS.md b/docs/modules/policy/AGENTS.md index 4d05ac36..e996a28d 100644 --- a/docs/modules/policy/AGENTS.md +++ b/docs/modules/policy/AGENTS.md @@ -8,6 +8,8 @@ Policy Engine compiles and evaluates Stella DSL policies deterministically, prod - [Architecture](./architecture.md) - [Implementation plan](./implementation_plan.md) - [Task board](./TASKS.md) +- [Secret leak detection readiness](../policy/secret-leak-detection-readiness.md) +- [Windows package readiness](../policy/windows-package-readiness.md) ## How to get started 1. Open ../../implplan/SPRINTS.md and locate the stories referencing this module. diff --git a/docs/modules/policy/README.md b/docs/modules/policy/README.md index e1e72bb2..03265da5 100644 --- a/docs/modules/policy/README.md +++ b/docs/modules/policy/README.md @@ -1,31 +1,33 @@ -# StellaOps Policy Engine - -Policy Engine compiles and evaluates Stella DSL policies deterministically, producing explainable findings with full provenance. - -## Responsibilities -- Compile `stella-dsl@1` packs into executable graphs. -- Join advisories, VEX evidence, and SBOM inventories to derive effective findings. -- Expose simulation and diff APIs for UI/CLI workflows. -- Emit change-stream driven events for Notify/Scheduler integrations. - -## Key components -- `StellaOps.Policy.Engine` service host. -- Shared libraries under `StellaOps.Policy.*` for evaluation, storage, DSL tooling. - -## Integrations & dependencies -- MongoDB findings collections, RustFS explain bundles. -- Scheduler for incremental re-evaluation triggers. -- CLI/UI for policy authoring and runs. - -## Operational notes -- DSL grammar and lifecycle docs in ../../policy/. -- Observability guidance in ../../observability/policy.md. -- Governance and scope mapping in ../../security/policy-governance.md. - -## Backlog references -- DOCS-POLICY-20-001 … DOCS-POLICY-20-012 (completed baseline). -- DOCS-POLICY-23-007 (upcoming command updates). - -## Epic alignment -- **Epic 2 – Policy Engine & Editor:** deliver deterministic evaluation, DSL infrastructure, explain traces, and incremental runs. -- **Epic 4 – Policy Studio:** integrate registry workflows, simulation at scale, approvals, and promotion semantics. +# StellaOps Policy Engine + +Policy Engine compiles and evaluates Stella DSL policies deterministically, producing explainable findings with full provenance. + +## Responsibilities +- Compile `stella-dsl@1` packs into executable graphs. +- Join advisories, VEX evidence, and SBOM inventories to derive effective findings. +- Expose simulation and diff APIs for UI/CLI workflows. +- Emit change-stream driven events for Notify/Scheduler integrations. + +## Key components +- `StellaOps.Policy.Engine` service host. +- Shared libraries under `StellaOps.Policy.*` for evaluation, storage, DSL tooling. + +## Integrations & dependencies +- MongoDB findings collections, RustFS explain bundles. +- Scheduler for incremental re-evaluation triggers. +- CLI/UI for policy authoring and runs. + +## Operational notes +- DSL grammar and lifecycle docs in ../../policy/. +- Observability guidance in ../../observability/policy.md. +- Governance and scope mapping in ../../security/policy-governance.md. +- Readiness briefs: ../policy/secret-leak-detection-readiness.md, ../policy/windows-package-readiness.md. +- Readiness briefs: ../scanner/design/macos-analyzer.md, ../scanner/design/windows-analyzer.md, ../policy/secret-leak-detection-readiness.md, ../policy/windows-package-readiness.md. + +## Backlog references +- DOCS-POLICY-20-001 … DOCS-POLICY-20-012 (completed baseline). +- DOCS-POLICY-23-007 (upcoming command updates). + +## Epic alignment +- **Epic 2 – Policy Engine & Editor:** deliver deterministic evaluation, DSL infrastructure, explain traces, and incremental runs. +- **Epic 4 – Policy Studio:** integrate registry workflows, simulation at scale, approvals, and promotion semantics. diff --git a/docs/modules/policy/TASKS.md b/docs/modules/policy/TASKS.md index ab8f2fea..7e60d4fb 100644 --- a/docs/modules/policy/TASKS.md +++ b/docs/modules/policy/TASKS.md @@ -1,9 +1,11 @@ -# Task board — Policy Engine - -> Local tasks should link back to ./AGENTS.md and mirror status updates into ../../TASKS.md when applicable. - -| ID | Status | Owner(s) | Description | Notes | -|----|--------|----------|-------------|-------| -| POLICY ENGINE-DOCS-0001 | TODO | Docs Guild | Validate that ./README.md aligns with the latest release notes. | See ./AGENTS.md | -| POLICY ENGINE-OPS-0001 | TODO | Ops Guild | Review runbooks/observability assets after next sprint demo. | Sync outcomes back to ../../TASKS.md | -| POLICY ENGINE-ENG-0001 | TODO | Module Team | Cross-check implementation plan milestones against ../../implplan/SPRINTS.md. | Update status via ./AGENTS.md workflow | +# Task board — Policy Engine + +> Local tasks should link back to ./AGENTS.md and mirror status updates into ../../TASKS.md when applicable. + +| ID | Status | Owner(s) | Description | Notes | +|----|--------|----------|-------------|-------| +| POLICY ENGINE-DOCS-0001 | TODO | Docs Guild | Validate that ./README.md aligns with the latest release notes. | See ./AGENTS.md | +| POLICY ENGINE-OPS-0001 | TODO | Ops Guild | Review runbooks/observability assets after next sprint demo. | Sync outcomes back to ../../TASKS.md | +| POLICY ENGINE-ENG-0001 | TODO | Module Team | Cross-check implementation plan milestones against ../../implplan/SPRINTS.md. | Update status via ./AGENTS.md workflow | +| POLICY-READINESS-0001 | DOING (2025-11-03) | Policy Guild, Security Guild | Resolve open questions in `../policy/secret-leak-detection-readiness.md` ahead of SCANNER-ENG-0007. | Decision workshop 2025-11-10 (Northwind demo); cover masking depth, telemetry retention, bundle defaults, tenant overrides. | +| POLICY-READINESS-0002 | DOING (2025-11-03) | Policy Guild, Security Guild, Offline Kit Guild | Review `../policy/windows-package-readiness.md`, set signature verification locus, feed mirroring scopes, and legacy installer posture. | FinSecure PCI blocker; deliver Authenticode/feed decision by 2025-11-07 before analyzer spike kickoff. | diff --git a/docs/modules/policy/secret-leak-detection-readiness.md b/docs/modules/policy/secret-leak-detection-readiness.md new file mode 100644 index 00000000..96a89f2b --- /dev/null +++ b/docs/modules/policy/secret-leak-detection-readiness.md @@ -0,0 +1,80 @@ +# Secret Leak Detection Readiness — Policy & Security Brief + +> Audience: Policy Guild, Security Guild +> Related backlog: SCANNER-ENG-0007 (deterministic leak detection pipeline), DOCS-SCANNER-BENCH-62-007 (rule bundle documentation), SCANNER-SECRETS-01..03 (Surface.Secrets alignment) + +## 1. Goal & scope +- Provide a shared understanding of how the planned `StellaOps.Scanner.Analyzers.Secrets` plug-in will operate so Policy/Security can prepare governance controls in parallel with engineering delivery. +- Document evidence flow, policy predicates, and offline distribution requirements to minimise lead time once implementation lands. +- Capture open questions requiring Policy/Security sign-off (masking rules, tenancy constraints, waiver workflows). + +## 2. Proposed evidence pipeline +1. **Source resolution** + - Surface.Secrets providers (Kubernetes, file bundle, inline) continue to resolve operational credentials. Handles remain opaque and never enter analyzer outputs. +2. **Deterministic scanning** + - New plug-in executes signed rule bundles (regex + entropy signatures) stored under `scanner/rules/secrets/`. + - Execution context restricted to read-only layer mount; analyzers emit `secret.leak` evidence with: `{rule.id, rule.version, confidence, severity, mask, file, line}`. +3. **Analysis store persistence** + - Findings are written into `ScanAnalysisStore` (`ScanAnalysisKeys.secretFindings`) so Policy Engine can ingest them alongside component fragments. +4. **Policy overlay** + - Policy predicates (see §3) evaluate evidence, lattice scores, and tenant-scoped allow/deny lists. + - CLI/export surfaces show masked snippets and remediation hints. +5. **Offline parity** + - Rule bundles, signature manifests, and validator hash lists ship with Offline Kit; rule updates must be signed and versioned to preserve determinism. + +## 3. Policy Engine considerations +- **New predicates** + - `secret.hasFinding(ruleId?, severity?, confidence?)` + - `secret.bundle.version(requiredVersion)` + - `secret.mask.applied` (bool) — verify masking for high severity hits. + - `secret.path.allowlist` — tenant-configured allow list keyed by digest/path. +- **Lattice weight suggestions** + - High severity & high confidence → escalate to `block` unless waived. + - Low confidence → default to `warn` with optional escalation when multiple matches occur (`secret.match.count >= N`). +- **Waiver workflow** + - Reuse VEX-first lattice approach: require attach of remediation note, ticket reference, and expiration date. + - Ensure waivers attach rule version so upgraded rules re-evaluate automatically. +- **Masking / privacy** + - Minimum masking: first and last 2 characters retained; remainder replaced with `*`. + - Persist masked payload only; full value never leaves scanner context. + +## 4. Security guardrails +- Rule bundle signing: Signer issues DSSE envelope for each ruleset; Policy must verify signature before enabling new bundle. +- Tenant isolation: bundle enablement scoped per tenant; defaults deny unknown bundles. +- Telemetry: emit `scanner.secret.finding_total{tenant, ruleId, severity}` with masking applied after count aggregation. +- Access control: restrict retrieval of raw finding payloads to roles with `scanner.secret.read` scope; audits log query + tenant + rule id. + +## 5. Offline & update flow +1. Engineering publishes new bundle → Signer signs → Offline Kit includes bundle + manifest. +2. Operators import bundle via CLI (`stella secrets bundle install --path `). + - CLI verifies signature, version monotonicity, and rule hash list. +3. Policy update: push config snippet enabling bundle, severity mapping, and waiver templates. +4. Run `stella secrets scan --dry-run` to verify deterministic output against golden fixtures before enabling in production. + +## 6. Open questions / owners +| Topic | Question | Owner | Target decision | +| --- | --- | --- | --- | +| Masking depth | Do we mask file paths or only payloads? | Security Guild | Sprint 132 design review | +| Telemetry retention | Should secret finding metrics be sampled or full fidelity? | Policy + Observability Guild | Sprint 132 | +| Default bundles | Which rule families ship enabled-by-default (cloud creds, SSH keys, JWT)? | Security Guild | Sprint 133 | +| Tenant overrides | Format for per-tenant allow lists (path glob vs digest)? | Policy Guild | Sprint 133 | + +### Decision tracker +| Decision | Owner(s) | Target date | Status | +| --- | --- | --- | --- | +| Masking depth (paths vs payloads) | Security Guild | 2025-11-10 | Pending — workshop aligned with Northwind demo | +| Telemetry retention granularity | Policy + Observability Guild | 2025-11-10 | Pending | +| Default rule bundles (cloud creds/SSH/JWT) | Security Guild | 2025-11-10 | Draft proposals under review | +| Tenant override format | Policy Guild | 2025-11-10 | Pending | + +## 7. Next steps +1. Policy Guild drafts predicate specs + policy templates (map to DOCS-SCANNER-BENCH-62-007 exit criteria). +2. Security Guild reviews signing + masking requirements; align with Surface.Secrets roadmap. +3. Docs Guild (this task) continues maintaining `docs/benchmarks/scanner/deep-dives/secrets.md` with finalized rule taxonomy and references. +4. Engineering provides prototype fixture outputs for review once SCANNER-ENG-0007 spikes begin. + + +## Coordination +- Capture macOS customer requirements via `docs/benchmarks/scanner/windows-macos-demand.md` (Northwind Health Services). +- Update dashboard (`docs/api/scanner/windows-macos-summary.md`) after readiness decisions. +- Share outcomes from 2025-11-10 macOS demo before finalising POLICY-READINESS-0001. diff --git a/docs/modules/policy/windows-package-readiness.md b/docs/modules/policy/windows-package-readiness.md new file mode 100644 index 00000000..4e019ed9 --- /dev/null +++ b/docs/modules/policy/windows-package-readiness.md @@ -0,0 +1,92 @@ +# Windows Package Coverage — Policy & Security Readiness Brief + +> Audience: Policy Guild, Security Guild, Offline Kit Guild +> Related engineering backlog (proposed): SCANNER-ENG-0024..0027 +> Docs linkage: DOCS-SCANNER-BENCH-62-016 + +## 1. Goal +- Prepare policy and security guidance ahead of Windows analyzer implementation (MSI, WinSxS, Chocolatey, registry). +- Define evidence handling, predicates, waiver expectations, and offline prerequisites so engineering can align during spike execution. + +## 2. Evidence pipeline snapshot (from `design/windows-analyzer.md`) +1. **Collection** + - MSI database parsing → component records keyed by ProductCode/ComponentCode. + - WinSxS manifests → assembly identities, catalog signatures. + - Chocolatey packages → nuspec metadata, feed provenance, script hashes. + - Registry exports → uninstall/service entries, legacy installers. + - Driver/service mapper → capability overlays (kernel-mode, auto-start). +2. **Storage** + - Results persisted as `LayerComponentFragment`s plus capability overlays (`ScanAnalysisKeys.capability.windows`). + - Provenance metadata includes signature thumbprint, catalog hash, feed URL, install context. +3. **Downstream** + - Policy Engine consumes component + capability evidence; Export Center bundles MSI manifests, nuspec metadata, catalog hashes. + +## 3. Policy predicate requirements +| Predicate | Description | Initial default | +| --- | --- | --- | +| `windows.package.signed(thumbprint?)` | True when Authenticode signature/cert matches allowlist. | Warn on missing signature, fail on mismatched thumbprint for kernel drivers. | +| `windows.package.sourceAllowed(sourceId)` | Validates Chocolatey/nuget feed against tenant allowlist. | Fail if feed not in tenant policy. | +| `windows.driver.kernelMode()` | Flags kernel-mode drivers for extra scrutiny. | Fail when unsigned; warn otherwise. | +| `windows.driver.signedBy(publisher)` | Checks driver publisher matches allowlist. | Warn on unknown publisher. | +| `windows.service.autoStart(name)` | Identifies auto-start services. | Warn if unsigned binary or service not in allowlist. | +| `windows.package.legacyInstaller()` | Legacy EXE-only installers detected via registry. | Warn by default; escalate if binary unsigned. | + +Additional considerations: +- Map KB references (from WinSxS/MSP metadata) to vulnerability posture once Policy Engine supports patch layering. +- Provide predicates to waive specific ProductCodes or AssemblyIdentities with expiration. + +## 4. Waiver & governance model +- Waiver key: `{productCode, version, signatureThumbprint}` or for drivers `{driverName, serviceName, signatureThumbprint}`. +- Required metadata: remediation ticket, justification, expiry date. +- Automated re-evaluation when version or signature changes. +- Tenants maintain allow lists for Chocolatey feeds and driver publishers via policy configuration. + +## 5. Masking & privacy +- Findings should not include raw script contents; provide SHA256 hash and limited excerpt (first/last 8 chars). +- Registry values (install paths, command lines) must be truncated if they contain secrets; rely on Surface.Secrets to manage environment variables referenced during install scripts. + +## 6. Offline kit guidance +- Bundle: + - MSI parser binary + schema definitions. + - Chocolatey feed snapshot(s) (nupkg files) with hash manifest. + - Microsoft root/intermediate certificate bundles; optional CRL/OCSP cache instructions. +- Operators must export registry hives (`SOFTWARE`, `SYSTEM`) during image extraction; document PowerShell script and required access. +- Provide checksum manifest to verify feed snapshot integrity. + +## 7. Telemetry expectations +- Metrics: + - `scanner.windows.package_total{tenant,signed}` — count packages per signature state. + - `scanner.windows.driver_unsigned_total{tenant}`. + - `scanner.windows.choco_feed_total{tenant,feed}`. +- Logs: + - Include product code, version, signature thumbprint, feed ID (no file paths unless sanitized). +- Traces: + - Annotate collector spans (`collector.windows.msi`, `collector.windows.winsxs`, etc.) with component counts and parsing duration. + +## 8. Open questions +| Topic | Question | Owner | Target decision | +| --- | --- | --- | --- | +| Signature verification locus | Scanner vs Policy: where to verify Authenticode signatures + revocation? | Security Guild | Sprint 133 | +| Feed mirroring scope | Default set of Chocolatey feeds to mirror (official/community). | Product + Security Guild | Sprint 133 | +| Legacy installers | Should we block unsigned EXE installers by default or allow warn-only posture? | Policy Guild | Sprint 134 | +| Driver taxonomy | Define high-risk driver categories (kernel-mode, filter drivers) for policy severity. | Policy Guild | Sprint 134 | + +### Decision tracker +| Decision | Owner(s) | Target date | Status | +| --- | --- | --- | --- | +| Authenticode verification locus (Scanner vs Policy) | Security Guild | 2025-11-07 | Pending — blocker for FinSecure | +| Chocolatey feed mirroring scope | Product + Security Guild | 2025-11-07 | Draft proposal circulating | +| Legacy installer posture (warn vs fail) | Policy Guild | 2025-11-14 | Not started | +| Driver risk taxonomy | Policy Guild | 2025-11-14 | Not started | + +## 9. Next steps +1. Policy Guild drafts predicate specs + policy templates; align with DOCS-SCANNER-BENCH-62-016. +2. Security Guild evaluates signature verification approach and revocation handling (online vs offline CRL cache). +3. Offline Kit Guild scopes snapshot size and update cadence for Chocolatey feeds and certificate bundles. +4. Docs Guild prepares policy/user guidance updates once predicates are finalised. +5. Security Guild to report decision for FinSecure Corp (POLICY-READINESS-0002) by 2025-11-07; feed outcome into dashboards. + +## Coordination +- Sync demand signals via `docs/benchmarks/scanner/windows-macos-demand.md`. +- Log policy readiness status in `docs/api/scanner/windows-coverage.md`. +- Update Windows/macOS metrics dashboard when decisions change (`docs/api/scanner/windows-macos-summary.md`). diff --git a/docs/modules/scanner/AGENTS.md b/docs/modules/scanner/AGENTS.md index 23d06b0a..db610e54 100644 --- a/docs/modules/scanner/AGENTS.md +++ b/docs/modules/scanner/AGENTS.md @@ -8,6 +8,12 @@ Scanner analyses container images layer-by-layer, producing deterministic SBOM f - [Architecture](./architecture.md) - [Implementation plan](./implementation_plan.md) - [Task board](./TASKS.md) +- [macOS analyzer design](./design/macos-analyzer.md) +- [Windows analyzer design](./design/windows-analyzer.md) +- [Field engagement workflow](./operations/field-engagement.md) +- [Design dossiers](./design/README.md) +- [Benchmarks overview](../../benchmarks/scanner/README.md) +- [Benchmarks overview](../../benchmarks/scanner/README.md) ## How to get started 1. Open ../../implplan/SPRINTS.md and locate the stories referencing this module. diff --git a/docs/modules/scanner/README.md b/docs/modules/scanner/README.md index 26a39e0a..df0339b8 100644 --- a/docs/modules/scanner/README.md +++ b/docs/modules/scanner/README.md @@ -1,38 +1,46 @@ -# StellaOps Scanner - -Scanner analyses container images layer-by-layer, producing deterministic SBOM fragments, diffs, and signed reports. - -## Responsibilities -- Expose APIs (WebService) for scan orchestration, diffing, and artifact retrieval. -- Run Worker analyzers for OS, language, and native ecosystems with restart-only plug-ins. -- Store SBOM fragments and artifacts in RustFS/object storage. -- Publish DSSE-ready metadata for Signer/Attestor and downstream policy evaluation. - -## Key components -- `StellaOps.Scanner.WebService` minimal API host. -- `StellaOps.Scanner.Worker` analyzer executor. -- Analyzer libraries under `StellaOps.Scanner.Analyzers.*`. - -## Integrations & dependencies -- Scheduler for job intake and retries. -- Policy Engine for evidence handoff. -- Export Center / Offline Kit for artifact packaging. - -## Operational notes -- CAS caches, bounded retries, DSSE integration. -- Monitoring dashboards (see ./operations/analyzers-grafana-dashboard.json). -- RustFS migration playbook. - -## Related resources -- ./operations/analyzers.md -- ./operations/analyzers-grafana-dashboard.json -- ./operations/rustfs-migration.md -- ./operations/entrypoint.md - -## Backlog references -- DOCS-SCANNER updates tracked in ../../TASKS.md. -- Analyzer parity work in src/Scanner/**/TASKS.md. - -## Epic alignment -- **Epic 6 – Vulnerability Explorer:** provide policy-aware scan outputs, explain traces, and findings ledger hooks for triage workflows. -- **Epic 10 – Export Center:** generate export-ready artefacts, manifests, and DSSE metadata for bundles. +# StellaOps Scanner + +Scanner analyses container images layer-by-layer, producing deterministic SBOM fragments, diffs, and signed reports. + +## Responsibilities +- Expose APIs (WebService) for scan orchestration, diffing, and artifact retrieval. +- Run Worker analyzers for OS, language, and native ecosystems with restart-only plug-ins. +- Store SBOM fragments and artifacts in RustFS/object storage. +- Publish DSSE-ready metadata for Signer/Attestor and downstream policy evaluation. + +## Key components +- `StellaOps.Scanner.WebService` minimal API host. +- `StellaOps.Scanner.Worker` analyzer executor. +- Analyzer libraries under `StellaOps.Scanner.Analyzers.*`. + +## Integrations & dependencies +- Scheduler for job intake and retries. +- Policy Engine for evidence handoff. +- Export Center / Offline Kit for artifact packaging. + +## Operational notes +- CAS caches, bounded retries, DSSE integration. +- Monitoring dashboards (see ./operations/analyzers-grafana-dashboard.json). +- RustFS migration playbook. + +## Related resources +- ./operations/analyzers.md +- ./operations/analyzers-grafana-dashboard.json +- ./operations/rustfs-migration.md +- ./operations/entrypoint.md +- ./design/macos-analyzer.md +- ./design/windows-analyzer.md +- ../benchmarks/scanner/deep-dives/macos.md +- ../benchmarks/scanner/deep-dives/windows.md +- ../benchmarks/scanner/windows-macos-demand.md +- ../benchmarks/scanner/windows-macos-interview-template.md +- ./operations/field-engagement.md +- ./design/README.md + +## Backlog references +- DOCS-SCANNER updates tracked in ../../TASKS.md. +- Analyzer parity work in src/Scanner/**/TASKS.md. + +## Epic alignment +- **Epic 6 – Vulnerability Explorer:** provide policy-aware scan outputs, explain traces, and findings ledger hooks for triage workflows. +- **Epic 10 – Export Center:** generate export-ready artefacts, manifests, and DSSE metadata for bundles. diff --git a/docs/modules/scanner/TASKS.md b/docs/modules/scanner/TASKS.md index 2805084d..021de0a7 100644 --- a/docs/modules/scanner/TASKS.md +++ b/docs/modules/scanner/TASKS.md @@ -28,5 +28,13 @@ | SCANNER-ENG-0005 | TODO | Go Analyzer Guild | Enhance Go stripped-binary fallback inference per `docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md`. | Include inferred module metadata & policy integration | | SCANNER-ENG-0006 | TODO | Rust Analyzer Guild | Expand Rust fingerprint coverage per `docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md`. | Ship enriched fingerprint catalogue + policy controls | | SCANNER-ENG-0007 | TODO | Scanner Guild, Policy Guild | Design deterministic secret leak detection pipeline per `docs/benchmarks/scanner/scanning-gaps-stella-misses-from-competitors.md`. | Include rule packaging, Policy Engine integration, CLI workflow | +| SCANNER-ENG-0020 | TODO | Scanner Guild (macOS Cellar Squad) | Implement Homebrew collector and fragment mapper per `design/macos-analyzer.md` §3.1. | Emit brew component fragments with tap provenance; integrate Surface.Validation/FS limits. | +| SCANNER-ENG-0021 | TODO | Scanner Guild (macOS Receipts Squad) | Implement pkgutil receipt collector per `design/macos-analyzer.md` §3.2. | Parse receipts/BOMs into deterministic component records with install metadata. | +| SCANNER-ENG-0022 | TODO | Scanner Guild, Policy Guild (macOS Bundles Squad) | Implement macOS bundle inspector & capability overlays per `design/macos-analyzer.md` §3.3. | Extract signing/entitlements, emit capability evidence, merge with receipts/Homebrew. | +| SCANNER-ENG-0023 | TODO | Scanner Guild, Offline Kit Guild, Policy Guild | Deliver macOS policy/offline integration per `design/macos-analyzer.md` §5–6. | Define policy predicates, CLI toggles, Offline Kit packaging, and documentation. | +| SCANNER-ENG-0024 | TODO | Scanner Guild (Windows MSI Squad) | Implement Windows MSI collector per `design/windows-analyzer.md` §3.1. | Parse MSI databases, emit component fragments with provenance metadata; blocked until POLICY-READINESS-0002 (decision due 2025-11-07). | +| SCANNER-ENG-0025 | TODO | Scanner Guild (Windows WinSxS Squad) | Implement WinSxS manifest collector per `design/windows-analyzer.md` §3.2. | Correlate assemblies with MSI components and catalog signatures; dependent on POLICY-READINESS-0002 outcome. | +| SCANNER-ENG-0026 | TODO | Scanner Guild (Windows Packages Squad) | Implement Chocolatey & registry collectors per `design/windows-analyzer.md` §3.3–3.4. | Harvest nuspec metadata and registry uninstall/service evidence; merge with filesystem artefacts; align with feed decisions from POLICY-READINESS-0002. | +| SCANNER-ENG-0027 | TODO | Scanner Guild, Policy Guild, Offline Kit Guild | Deliver Windows policy/offline integration per `design/windows-analyzer.md` §5–6. | Define predicates, CLI/Offline docs, and packaging for feeds/certs; start after POLICY-READINESS-0002 sign-off. | | SCANNER-OPS-0001 | TODO | Ops Guild | Review runbooks/observability assets after next sprint demo. | Sync outcomes back to ../../TASKS.md | | SCANNER-ENG-0001 | TODO | Module Team | Cross-check implementation plan milestones against ../../implplan/SPRINTS.md. | Update status via ./AGENTS.md workflow | diff --git a/docs/modules/scanner/design/README.md b/docs/modules/scanner/design/README.md new file mode 100644 index 00000000..0f4d154c --- /dev/null +++ b/docs/modules/scanner/design/README.md @@ -0,0 +1,35 @@ +# Scanner Design Dossiers + +This directory contains deep technical designs for current and upcoming analyzers and surface components. + +## Language analyzers +- `ruby-analyzer.md` — lockfile, runtime graph, capability signals for Ruby. + +## Surface & platform contracts +- `surface-fs.md` +- `surface-env.md` +- `surface-validation.md` +- `surface-secrets.md` + +## OS ecosystem designs +- `macos-analyzer.md` — Homebrew, pkgutil, `.app` bundle plan. +- `windows-analyzer.md` — MSI, WinSxS, Chocolatey, registry collectors. + +## Demand & dashboards +- `../../benchmarks/scanner/windows-macos-demand.md` — demand tracker. +- `../../benchmarks/scanner/windows-macos-interview-template.md` — interview template. +- `../../api/scanner/windows-coverage.md` — coverage summary dashboard. +- `../../api/scanner/windows-macos-summary.md` — metric snapshot. + +## Utility & reference +- `../operations/field-engagement.md` — SE workflow guidance. +- `../operations/analyzers.md` — operational runbook. +- `../operations/rustfs-migration.md` — storage migration notes. + +## Maintenance tips +- Keep demand tracker (`../../benchmarks/scanner/windows-macos-demand.md`) and API dashboards in sync when updating macOS/Windows designs. +- Cross-reference policy readiness briefs for associated predicates and waiver models. + +## Policy readiness +- `../policy/secret-leak-detection-readiness.md` — secret leak pipeline decisions. +- `../policy/windows-package-readiness.md` — Windows analyzer policy decisions. diff --git a/docs/modules/scanner/design/macos-analyzer.md b/docs/modules/scanner/design/macos-analyzer.md new file mode 100644 index 00000000..f79af86a --- /dev/null +++ b/docs/modules/scanner/design/macos-analyzer.md @@ -0,0 +1,123 @@ +# macOS Analyzer Design Brief (Draft) + +> Owners: Scanner Guild, Policy Guild, Offline Kit Guild +> Related backlog (proposed): SCANNER-ENG-0020..0023, DOCS-SCANNER-BENCH-62-002 +> Status: Draft pending demand validation (see `docs/benchmarks/scanner/windows-macos-demand.md`) + +## 1. Scope & objectives +- Deliver deterministic inventory coverage for macOS container and VM images, focusing on Homebrew, Apple/pkgutil receipts, and `.app` bundles. +- Preserve StellaOps principles: per-layer provenance, offline parity, policy explainability, and signed evidence pipelines. +- Provide capability signals (entitlements, hardened runtime, launch agents) to enable Policy Engine gating. + +Out of scope (Phase 1): +- Dynamic runtime tracing of macOS services (deferred to Zastava/EntryTrace). +- iOS/tvOS/visionOS package formats (will reuse bundle inspection where feasible). +- Direct notarization ticket validation (delegated to Policy Engine unless otherwise decided). + +## 2. High-level architecture +``` +Scanner.Worker (macOS profile) + ├─ Surface.Validation (enforce allowlists, bundle size limits) + ├─ Surface.FS (layer/materialized filesystem) + ├─ HomebrewCollector (new) -> LayerComponentFragment (brew) + ├─ PkgutilCollector (new) -> LayerComponentFragment (pkgutil) + ├─ BundleInspector (new) -> Capability records + component fragments + ├─ LaunchAgentMapper (optional) -> Usage hints + └─ MacOsAggregator (new) -> merges fragments, emits ComponentGraph & capability overlays +``` + +- Each collector runs deterministically against the mounted filesystem; results persist in `ScanAnalysisStore` under dedicated keys before being normalized by `MacOsComponentMapper`. +- Layer digests follow existing `LayerComponentFragment` semantics to maintain diff/replay parity. + +## 3. Collectors & data sources +### 3.1 Homebrew collector +- Scan `/usr/local/Cellar/**` and `/opt/homebrew/Cellar/**` for `INSTALL_RECEIPT.json` and `*.rb` formula metadata. +- Output fields: `tap`, `formula`, `version`, `revision`, `poured_from_bottle`, `installed_with`: []. +- Store `bottle.sha256`, `source.url`, and `tapped_from` metadata for provenance. +- Map to PURL: `pkg:brew//@?revision=`. +- Guardrails: limit traversal depth, ignore caches (`/Caskroom` optional), enforce 200MB per formula cap (configurable). + +### 3.2 pkgutil receipt collector +- Parse `/var/db/receipts/*.plist` for pkg identifiers, version, install time, volume, installer domain. +- Use `.bom` files to enumerate installed files; capture path hashes via `osx.BomParser`. +- Emit `ComponentRecord`s keyed by `pkgutil:` with metadata: `bundleIdentifier`, `installTimeUtc`, `volume`. +- Provide dependency mapping between pkg receipts and Homebrew formulae when overlapping file hashes exist (optional Phase 2). + +### 3.3 Bundle inspector +- Traverse `/Applications`, `/System/Applications`, `/Users/*/Applications`, `/Library/Application Support`. +- For each `.app`: + - Read `Info.plist` (bundle id, version, short version, minimum system). + - Extract code signing metadata via `codesign --display --xml` style parsing (Team ID, certificate chain, hardened runtime flag). + - Parse entitlements (`archived-entitlements.xcent`) and map to capability taxonomy (network, camera, automation, etc.). + - Hash `CodeResources` manifest to support provenance comparisons. +- Link `.app` bundles to receipts and Homebrew formulae using bundle id or install path heuristics. +- Emit capability overlays (e.g., `macos.entitlement("com.apple.security.network.server") == true`). + +### 3.4 Launch agent mapper (optional Phase 1 extension) +- Inspect `/Library/Launch{Agents,Daemons}` and user equivalents. +- Parse `plist` for program arguments, run conditions, sockets, environment. +- Feed derived runtime hints into EntryTrace (`UsageHints`) to mark active binaries vs dormant installations. + +## 4. Aggregation & output +- `MacOsComponentMapper` converts collector results into: + - `LayerComponentFragment` arrays keyed by synthetic digests (`sha256:stellaops-os-macbrew`, etc.). + - `ComponentMetadata` entries capturing tap origin, Team ID, entitlements, notarization flag (when available). + - Capability overlays stored under `ScanAnalysisKeys.capability.macOS`. +- Export Center enhancements: + - Include Homebrew metadata manifests and CodeResources hashes in attested bundles. + - Provide optional notarization proof attachments if Policy Engine later requires them. + +## 5. Policy & governance integration +- Predicates to introduce: + - `macos.bundle.signed(teamId?, hardenedRuntime?)` + - `macos.entitlement(name)` + - `macos.pkg.receipt(identifier, version?)` + - `macos.notarized` (pending Security decision). +- Lattice guidance: + - Unsigned/unnotarized third-party apps default to `warn`. + - High-risk entitlements (camera, screen capture) escalate severity unless whitelisted. +- Waiver model similar to existing EntryTrace/Secrets: require bundle hash + Team ID to avoid broad exceptions. + +## 6. Offline kit considerations +- Mirror required Homebrew taps (`homebrew/core`, `homebrew/cask`) and freeze metadata per release. +- Bundle notarization cache instructions (CRL/OCSP) so operators can prefetch without Apple connectivity. +- Package Apple certificate chain updates (WWDR) and provide verification script to ensure validity. +- Document disk space impacts (Homebrew cellar snapshots ~500MB per tap snapshot). + +## 7. Testing strategy +- Unit tests with fixture directories: + - Sample Cellar tree with multiple formulas (bottled vs source builds). + - Synthetic pkg receipts + BOM files. + - `.app` bundles with varying signing states (signed/notarized/unsigned). +- Integration tests: + - Build macOS rootfs tarballs via CI job (runner requiring macOS) that mimic layered container conversion. + - Verify deterministic output by re-running collectors and diffing results (CI gating). +- Offline compliance tests: + - Ensure rule bundles and caches load without network by running integration suite in sealed environment. + +## 8. Dependencies & open items +| Item | Description | Owner | Status | +| --- | --- | --- | --- | +| Demand threshold | Need ≥3 qualified customer asks to justify engineering investment | Product Guild | Active (DOCS-SCANNER-BENCH-62-002) | +| macOS rootfs fixtures | Require automation to produce macOS layer tarballs for tests | Scanner Guild | Pending design | +| Notarization validation | Decide whether scanner performs `spctl --assess` or defers | Security Guild | TBD | +| Entitlement taxonomy | Finalize capability grouping for Policy Engine | Policy Guild | TBD | + +## 9. Proposed backlog entries +| ID (proposed) | Title | Summary | +| --- | --- | --- | +| SCANNER-ENG-0020 | Implement Homebrew collector and fragment mapper | Parse cellar manifests, emit brew component records, integrate with Surface.FS/Validation. | +| SCANNER-ENG-0021 | Implement pkgutil receipt collector | Parse receipts/BOM, output installer component records with provenance. | +| SCANNER-ENG-0022 | Implement macOS bundle inspector & capability overlays | Extract plist/signing/entitlements, produce capability evidence and merge with receipts. | +| SCANNER-ENG-0023 | Policy & Offline integration for macOS | Define predicates, CLI toggles, Offline Kit packaging, and documentation. | + +## 10. References +- `docs/benchmarks/scanner/deep-dives/macos.md` — competitor snapshot and roadmap summary. +- `docs/benchmarks/scanner/windows-macos-demand.md` — demand capture log. +- `docs/modules/scanner/design/surface-secrets.md`, `surface-fs.md`, `surface-validation.md` — surface contracts leveraged by collectors. + +Further reading: `../../api/scanner/windows-coverage.md` (summary) and `../../api/scanner/windows-macos-summary.md` (metrics dashboard). + +Policy readiness alignment: see `../policy/secret-leak-detection-readiness.md` (POLICY-READINESS-0001). + +Upcoming milestone: Northwind Health Services demo on 2025-11-10 to validate notarization/entitlement outputs before final policy sign-off. diff --git a/docs/modules/scanner/design/windows-analyzer.md b/docs/modules/scanner/design/windows-analyzer.md new file mode 100644 index 00000000..989ed381 --- /dev/null +++ b/docs/modules/scanner/design/windows-analyzer.md @@ -0,0 +1,135 @@ +# Windows Analyzer Design Brief (Draft) + +> Owners: Scanner Guild, Policy Guild, Offline Kit Guild, Security Guild +> Related backlog (proposed): SCANNER-ENG-0024..0027, DOCS-SCANNER-BENCH-62-002 +> Status: Draft — contingent on Windows demand threshold (see `docs/benchmarks/scanner/windows-macos-demand.md`) + +## 1. Objectives & boundaries +- Provide deterministic inventory for Windows Server/container images covering MSI/WinSxS assemblies, Chocolatey packages, and registry-derived installers. +- Preserve replayability (layer fragments, provenance metadata) and align outputs with existing SBOM/policy pipelines. +- Respect sovereignty constraints: offline-friendly, signed rule bundles, no reliance on Windows APIs unavailable in containerized scans. + +Out of scope (Phase 1): +- Live registry queries on running Windows hosts (requires runtime agent; defer to Zastava/Runtime roadmap). +- Windows Update patch baseline comparison (tracked separately under Runtime/Posture). +- UWP/MSIX packages (flagged for follow-up once MSI parity is complete). + +## 2. Architecture overview +``` +Scanner.Worker (Windows profile) + ├─ Surface.Validation (enforce layer size, path allowlists) + ├─ Surface.FS (materialized NTFS image via 7z/guestmount) + ├─ MsiCollector -> LayerComponentFragment (windows-msi) + ├─ WinSxSCollector -> LayerComponentFragment (windows-winsxs) + ├─ ChocolateyCollector -> LayerComponentFragment (windows-choco) + ├─ RegistryCollector -> Evidence overlays (uninstall/services) + ├─ DriverCapabilityMapper -> Capability overlays (kernel/user drivers) + └─ WindowsComponentMapper -> ComponentGraph + capability metadata +``` + +- Collectors operate on extracted filesystem snapshots; registry access performed on exported hive files produced during image extraction (document in ops runbooks). +- `WindowsComponentMapper` normalizes component identities (ProductCode, AssemblyIdentity, Chocolatey package ID) and merges overlapping evidence into deterministic fragments. + +## 3. Collectors +### 3.1 MSI collector +- Input: `Windows/Installer/*.msi` database files (Jet OLE DB), registry hive exports for product mapping. +- Implementation approach: + - Use open-source MSI parser (custom or MIT-compatible) to avoid COM dependencies. + - Extract Product, Component, File, Feature, Media tables. + - Compute SHA256 for installed files via Component table, linking to WinSxS manifests. +- Output metadata: `productCode`, `upgradeCode`, `productVersion`, `manufacturer`, `language`, `installContext`, `packageCode`, `sourceList`. +- Evidence: file paths with digests, component IDs, CAB/patch references. + +### 3.2 WinSxS collector +- Input: `Windows/WinSxS/Manifests/*.manifest`, `Windows/WinSxS/` payload directories, catalog (.cat) files. +- Parse XML assembly identities (name, version, processor architecture, public key token, language). +- Map to MSI components when file hashes match. +- Capture catalog signature thumbprint and optional patch KB references for policy gating. + +### 3.3 Chocolatey collector +- Input: `ProgramData/Chocolatey/lib/**`, `ProgramData/Chocolatey/package.backup`, `chocolateyinstall.ps1`, `.nuspec`. +- Extract package ID, version, checksum, source feed, installed files and scripts. +- Note whether install used cache or remote feed; record script hash for determinism. + +### 3.4 Registry collector +- Input: Exported `SOFTWARE` hive covering: + - `Microsoft\Windows\CurrentVersion\Uninstall` + - `Microsoft\Windows\CurrentVersion\Installer\UserData` + - `Microsoft\Windows\CurrentVersion\Run` (startup apps) + - Service/driver configuration from `SYSTEM` hive under `Services`. +- Emit fallback evidence for installers not captured by MSI/Chocolatey (legacy EXE installers). +- Record uninstall strings, install dates, publisher, estimated size, install location. + +### 3.5 Driver & service mapper +- Parse `SYSTEM` hive `Services` entries to detect drivers (type=1 or 2) and critical services (start mode auto/boot). +- Output capability overlays (e.g., `windows.driver.kernelMode(true)`, `windows.service.autoStart("Spooler")`) for Policy Engine. + +## 4. Component mapping & output +- `WindowsComponentMapper`: + - Generate `LayerComponentFragment`s with synthetic layer digests (e.g., `sha256:stellaops-windows-msi`). + - Build `ComponentIdentity` with PURL-like scheme: `pkg:msi/` or `pkg:winsxs/`. + - Include metadata: signature thumbprint, catalog hash, KB references, install context, manufacturer. +- Capability overlays stored under `ScanAnalysisKeys.capability.windows` for policy consumption. +- Export Center bundling: + - Include MSI manifest extracts, WinSxS assembly manifests, Chocolatey nuspec snapshots, and service/driver capability CSV. + +## 5. Policy integration +- Predicates to introduce: + - `windows.package.signed(expectedThumbprint?)` + - `windows.package.unsupportedInstallerType` + - `windows.driver.kernelMode`, `windows.driver.unsigned` + - `windows.service.autoStart(name)` + - `windows.choco.sourceAllowed(feed)` +- Lattice approach: + - Unsigned kernel drivers → default `fail`. + - Unknown installer sources → `warn` with escalation on critical services. + - Chocolatey packages from non-whitelisted feeds → configurable severity. +- Waiver semantics bind to product code + signature thumbprint; waivers expire when package version changes. + +## 6. Offline kit & distribution +- Package: + - MSI schema definitions and parser binaries (signed). + - Chocolatey feed snapshot (nupkg archives + index) for allow-listed feeds. + - Windows catalog certificate chains + optional CRL/OCSP caches. +- Documentation: + - Provide instructions for exporting registry hives during image extraction (PowerShell script included). + - Note disk space expectations (Chocolatey snapshot size, WinSxS manifest volume). + +## 7. Testing strategy +- Fixtures: + - Sample MSI packages (with/without transforms), WinSxS manifests, Chocolatey packages. + - Registry hive exports representing mixed installer types. +- Tests: + - Unit tests for each collector parsing edge cases (language-specific manifests, transforms, script hashing). + - Integration tests using synthetic Windows container image layers (generated via CI on Windows worker). + - Determinism checks ensuring repeated runs produce identical fragments. +- Security review: + - Validate script execution paths (collectors must never execute Chocolatey scripts; inspect only). + +## 8. Dependencies & open questions +| Item | Description | Owner | Status | +| --- | --- | --- | --- | +| MSI parser choice | Select MIT/Apache-compatible parser or build internal reader | Scanner Guild | TBD | +| Registry export tooling | Determine standard script/utility for hive exports in container context | Ops Guild | TBD | +| Authenticodes verification locus | Decide scanner vs policy responsibility for signature verification | Security Guild | TBD | +| Feed mirroring policy | Which Chocolatey feeds to mirror by default | Product + Security Guilds | TBD | + +## 9. Proposed backlog entries +| ID (proposed) | Title | Summary | +| --- | --- | --- | +| SCANNER-ENG-0024 | Implement Windows MSI collector | Parse MSI databases, emit component fragments with provenance metadata. | +| SCANNER-ENG-0025 | Implement WinSxS manifest collector | Correlate assemblies with MSI components and catalog signatures. | +| SCANNER-ENG-0026 | Implement Chocolatey & registry collectors | Harvest nuspec metadata and uninstall/service registry data. | +| SCANNER-ENG-0027 | Policy & Offline integration for Windows | Define predicates, CLI toggles, Offline Kit packaging, documentation. | + +## 10. References +- `docs/benchmarks/scanner/deep-dives/windows.md` +- `docs/benchmarks/scanner/windows-macos-demand.md` +- `docs/modules/scanner/design/macos-analyzer.md` (structure/composition parallels) +- Surface design docs (`surface-fs.md`, `surface-validation.md`, `surface-secrets.md`) for interfacing expectations. + +Further reading: `../../api/scanner/windows-coverage.md` (summary) and `../../api/scanner/windows-macos-summary.md` (metrics dashboard). + +Policy readiness alignment: see `../policy/windows-package-readiness.md` (POLICY-READINESS-0002). + +Upcoming milestone: FinSecure Corp PCI review requires Authenticode/feed decision by 2025-11-07 before Windows analyzer spike kickoff. diff --git a/docs/modules/scanner/operations/field-engagement.md b/docs/modules/scanner/operations/field-engagement.md new file mode 100644 index 00000000..c36da39b --- /dev/null +++ b/docs/modules/scanner/operations/field-engagement.md @@ -0,0 +1,30 @@ +# Field Engagement Playbook — Windows & macOS Coverage + +> Audience: Field SEs, Product Specialists • Status: Draft + +## Purpose +Provide quick-reference guidance when prospects or customers ask about Windows/macOS coverage. + +## Key talking points +- **Current scope**: Scanner supports deterministic Linux coverage; Windows/macOS analyzers are in design. +- **Roadmap**: macOS design (brew/pkgutil/.app) at `../design/macos-analyzer.md`; Windows design (MSI/WinSxS/Chocolatey) at `../design/windows-analyzer.md`. +- **Demand tracking**: All signals captured in `../../benchmarks/scanner/windows-macos-demand.md` using the interview template. +- **Policy readiness**: Secret leak detection briefing (`../../policy/secret-leak-detection-readiness.md`) and Windows package readiness (`../../policy/windows-package-readiness.md`). +- **Backlog IDs**: MacOS (SCANNER-ENG-0020..0023), Windows (SCANNER-ENG-0024..0027), policy follow-ups (POLICY-READINESS-0001/0002). + +## SE workflow +1. Use the interview template to capture customer needs. +2. Append structured summary to `windows-macos-demand.md` and update the API dashboards (`docs/api/scanner/windows-macos-summary.md`, `docs/api/scanner/windows-coverage.md`). +3. Notify Product/Scanner guild during weekly sync; flag blockers in Jira. +4. Add highlight to the “Recent updates” section in `docs/api/scanner/windows-macos-summary.md`. +5. Track upcoming milestones (FinSecure decision 2025-11-07, Northwind demo 2025-11-10) and ensure readiness tasks reflect outcomes. + +## FAQ snippets +- *When will Windows/macOS analyzers be GA?* — Pending demand threshold; design complete, awaiting prioritisation. +- *Can we run scans offline?* — Offline parity is a requirement; Offline Kit packaging detailed in design briefs. +- *Do we cover Authenticode/notarization?* — Planned via Policy Engine predicates as part of readiness tasks. + +## Contacts +- Product lead: TBD (record in demand log when assigned) +- Scanner guild rep: TBD +- Policy guild rep: TBD diff --git a/docs/modules/scheduler/architecture.md b/docs/modules/scheduler/architecture.md index 02842e90..730d5a38 100644 --- a/docs/modules/scheduler/architecture.md +++ b/docs/modules/scheduler/architecture.md @@ -1,426 +1,426 @@ # component_architecture_scheduler.md — **Stella Ops Scheduler** (2025Q4) > Synthesises the scheduling requirements documented across the Policy, Vulnerability Explorer, and Orchestrator module guides and implementation plans. - -> **Scope.** Implementation‑ready architecture for **Scheduler**: a service that (1) **re‑evaluates** already‑cataloged images when intel changes (Feedser/Vexer/policy), (2) orchestrates **nightly** and **ad‑hoc** runs, (3) targets only the **impacted** images using the BOM‑Index, and (4) emits **report‑ready** events that downstream **Notify** fans out. Default mode is **analysis‑only** (no image pull); optional **content‑refresh** can be enabled per schedule. - ---- - -## 0) Mission & boundaries - -**Mission.** Keep scan results **current** without rescanning the world. When new advisories or VEX claims land, **pinpoint** affected images and ask the backend to recompute **verdicts** against the **existing SBOMs**. Surface only **meaningful deltas** to humans and ticket queues. - -**Boundaries.** - -* Scheduler **does not** compute SBOMs and **does not** sign. It calls Scanner/WebService’s **/reports (analysis‑only)** endpoint and lets the backend (Policy + Vexer + Feedser) decide PASS/FAIL. -* Scheduler **may** ask Scanner to **content‑refresh** selected targets (e.g., mutable tags) but the default is **no** image pull. -* Notifications are **not** sent directly; Scheduler emits events consumed by **Notify**. - ---- - -## 1) Runtime shape & projects - -``` -src/ - ├─ StellaOps.Scheduler.WebService/ # REST (schedules CRUD, runs, admin) - ├─ StellaOps.Scheduler.Worker/ # planners + runners (N replicas) - ├─ StellaOps.Scheduler.ImpactIndex/ # purl→images inverted index (roaring bitmaps) - ├─ StellaOps.Scheduler.Models/ # DTOs (Schedule, Run, ImpactSet, Deltas) - ├─ StellaOps.Scheduler.Storage.Mongo/ # schedules, runs, cursors, locks - ├─ StellaOps.Scheduler.Queue/ # Redis Streams / NATS abstraction - ├─ StellaOps.Scheduler.Tests.* # unit/integration/e2e -``` - -**Deployables**: - -* **Scheduler.WebService** (stateless) -* **Scheduler.Worker** (scale‑out; planners + executors) - -**Dependencies**: Authority (OpTok + DPoP/mTLS), Scanner.WebService, Feedser, Vexer, MongoDB, Redis/NATS, (optional) Notify. - ---- - -## 2) Core responsibilities - -1. **Time‑based** runs: cron windows per tenant/timezone (e.g., “02:00 Europe/Sofia”). -2. **Event‑driven** runs: react to **Feedser export** and **Vexer export** deltas (changed product keys / advisories / claims). -3. **Impact targeting**: map changes to **image sets** using a **global inverted index** built from Scanner’s per‑image **BOM‑Index** sidecars. -4. **Run planning**: shard, pace, and rate‑limit jobs to avoid thundering herds. -5. **Execution**: call Scanner **/reports (analysis‑only)** or **/scans (content‑refresh)**; aggregate **delta** results. -6. **Events**: publish `rescan.delta` and `report.ready` summaries for **Notify** & **UI**. -7. **Control plane**: CRUD schedules, **pause/resume**, dry‑run previews, audit. - ---- - -## 3) Data model (Mongo) - -**Database**: `scheduler` - -* `schedules` - - ``` - { _id, tenantId, name, enabled, whenCron, timezone, - mode: "analysis-only" | "content-refresh", - selection: { scope: "all-images" | "by-namespace" | "by-repo" | "by-digest" | "by-labels", - includeTags?: ["prod-*"], digests?: [sha256...], resolvesTags?: bool }, - onlyIf: { lastReportOlderThanDays?: int, policyRevision?: string }, - notify: { onNewFindings: bool, minSeverity: "low|medium|high|critical", includeKEV: bool }, - limits: { maxJobs?: int, ratePerSecond?: int, parallelism?: int }, - createdAt, updatedAt, createdBy, updatedBy } - ``` - -* `runs` - - ``` - { _id, scheduleId?, tenantId, trigger: "cron|feedser|vexer|manual", - reason?: { feedserExportId?, vexerExportId?, cursor? }, - state: "planning|queued|running|completed|error|cancelled", - stats: { candidates: int, deduped: int, queued: int, completed: int, deltas: int, newCriticals: int }, - startedAt, finishedAt, error? } - ``` - -* `impact_cursors` - - ``` - { _id: tenantId, feedserLastExportId, vexerLastExportId, updatedAt } - ``` - -* `locks` (singleton schedulers, run leases) - -* `audit` (CRUD actions, run outcomes) - -**Indexes**: - -* `schedules` on `{tenantId, enabled}`, `{whenCron}`. -* `runs` on `{tenantId, startedAt desc}`, `{state}`. -* TTL optional for completed runs (e.g., 180 days). - ---- - -## 4) ImpactIndex (global inverted index) - -Goal: translate **change keys** → **image sets** in **milliseconds**. - -**Source**: Scanner produces per‑image **BOM‑Index** sidecars (purls, and `usedByEntrypoint` bitmaps). Scheduler ingests/refreshes them to build a **global** index. - -**Representation**: - -* Assign **image IDs** (dense ints) to catalog images. -* Keep **Roaring Bitmaps**: - - * `Contains[purl] → bitmap(imageIds)` - * `UsedBy[purl] → bitmap(imageIds)` (subset of Contains) -* Optionally keep **Owner maps**: `{imageId → {tenantId, namespaces[], repos[]}}` for selection filters. -* Persist in RocksDB/LMDB or Redis‑modules; cache hot shards in memory; snapshot to Mongo for cold start. - -**Update paths**: - -* On new/updated image SBOM: **merge** per‑image set into global maps. -* On image remove/expiry: **clear** id from bitmaps. - -**API (internal)**: - -```csharp -IImpactIndex { - ImpactSet ResolveByPurls(IEnumerable purls, bool usageOnly, Selector sel); - ImpactSet ResolveByVulns(IEnumerable vulnIds, bool usageOnly, Selector sel); // optional (vuln->purl precomputed by Feedser) - ImpactSet ResolveAll(Selector sel); // for nightly -} -``` - -**Selector filters**: tenant, namespaces, repos, labels, digest allowlists, `includeTags` patterns. - ---- - -## 5) External interfaces (REST) - -Base path: `/api/v1/scheduler` (Authority OpToks; scopes: `scheduler.read`, `scheduler.admin`). - -### 5.1 Schedules CRUD - -* `POST /schedules` → create -* `GET /schedules` → list (filter by tenant) -* `GET /schedules/{id}` → details + next run -* `PATCH /schedules/{id}` → pause/resume/update -* `DELETE /schedules/{id}` → delete (soft delete, optional) - -### 5.2 Run control & introspection - -* `POST /run` — ad‑hoc run - - ```json - { "mode": "analysis-only|content-refresh", "selection": {...}, "reason": "manual" } - ``` -* `GET /runs` — list with paging -* `GET /runs/{id}` — status, stats, links to deltas -* `POST /runs/{id}/cancel` — best‑effort cancel - -### 5.3 Previews (dry‑run) - -* `POST /preview/impact` — returns **candidate count** and a small sample of impacted digests for given change keys or selection. - -### 5.4 Event webhooks (optional push from Feedser/Vexer) - -* `POST /events/feedser-export` - - ```json - { "exportId":"...", "changedProductKeys":["pkg:rpm/openssl", ...], "kev": ["CVE-..."], "window": { "from":"...","to":"..." } } - ``` -* `POST /events/vexer-export` - - ```json - { "exportId":"...", "changedClaims":[ { "productKey":"pkg:deb/...", "vulnId":"CVE-...", "status":"not_affected→affected"} ], ... } - ``` - -**Security**: webhook requires **mTLS** or an **HMAC** `X-Scheduler-Signature` (Ed25519 / SHA‑256) plus Authority token. - ---- - -## 6) Planner → Runner pipeline - -### 6.1 Planning algorithm (event‑driven) - -``` -On Export Event (Feedser/Vexer): - keys = Normalize(change payload) # productKeys or vulnIds→productKeys - usageOnly = schedule/policy hint? # default true - sel = Selector for tenant/scope from schedules subscribed to events - - impacted = ImpactIndex.ResolveByPurls(keys, usageOnly, sel) - impacted = ApplyOwnerFilters(impacted, sel) # namespaces/repos/labels - impacted = DeduplicateByDigest(impacted) - impacted = EnforceLimits(impacted, limits.maxJobs) - shards = Shard(impacted, byHashPrefix, n=limits.parallelism) - - For each shard: - Enqueue RunSegment (runId, shard, rate=limits.ratePerSecond) -``` - -**Fairness & pacing** - -* Use **leaky bucket** per tenant and per registry host. -* Prioritize **KEV‑tagged** and **critical** first if oversubscribed. - -### 6.2 Nightly planning - -``` -At cron tick: - sel = resolve selection - candidates = ImpactIndex.ResolveAll(sel) - if lastReportOlderThanDays present → filter by report age (via Scanner catalog) - shard & enqueue as above -``` - -### 6.3 Execution (Runner) - -* Pop **RunSegment** job → for each image digest: - - * **analysis‑only**: `POST scanner/reports { imageDigest, policyRevision? }` - * **content‑refresh**: resolve tag→digest if needed; `POST scanner/scans { imageRef, attest? false }` then `POST /reports` -* Collect **delta**: `newFindings`, `newCriticals`/`highs`, `links` (UI deep link, Rekor if present). -* Persist per‑image outcome in `runs.{id}.stats` (incremental counters). -* Emit `scheduler.rescan.delta` events to **Notify** only when **delta > 0** and matches severity rule. - ---- - -## 7) Event model (outbound) - -**Topic**: `rescan.delta` (internal bus → Notify; UI subscribes via backend). - -```json -{ - "tenant": "tenant-01", - "runId": "324af…", - "imageDigest": "sha256:…", - "newCriticals": 1, - "newHigh": 2, - "kevHits": ["CVE-2025-..."], - "topFindings": [ - { "purl":"pkg:rpm/openssl@3.0.12-...","vulnId":"CVE-2025-...","severity":"critical","link":"https://ui/scans/..." } - ], - "reportUrl": "https://ui/.../scans/sha256:.../report", - "attestation": { "uuid":"rekor-uuid", "verified": true }, - "ts": "2025-10-18T03:12:45Z" -} -``` - -**Also**: `report.ready` for “no‑change” summaries (digest + zero delta), which Notify can ignore by rule. - ---- - -## 8) Security posture - -* **AuthN/Z**: Authority OpToks with `aud=scheduler`; DPoP (preferred) or mTLS. -* **Multi‑tenant**: every schedule, run, and event carries `tenantId`; ImpactIndex filters by tenant‑visible images. -* **Webhook** callers (Feedser/Vexer) present **mTLS** or **HMAC** and Authority token. -* **Input hardening**: size caps on changed key lists; reject >100k keys per event; compress (zstd/gzip) allowed with limits. -* **No secrets** in logs; redact tokens and signatures. - ---- - -## 9) Observability & SLOs - -**Metrics (Prometheus)** - -* `scheduler.events_total{source, result}` -* `scheduler.impact_resolve_seconds{quantile}` -* `scheduler.images_selected_total{mode}` -* `scheduler.jobs_enqueued_total{mode}` -* `scheduler.run_latency_seconds{quantile}` // event → first verdict -* `scheduler.delta_images_total{severity}` -* `scheduler.rate_limited_total{reason}` - -**Targets** - -* Resolve 10k changed keys → impacted set in **<300 ms** (hot cache). -* Event → first rescan verdict in **≤60 s** (p95). -* Nightly coverage 50k images in **≤10 min** with 10 workers (analysis‑only). - -**Tracing** (OTEL): spans `plan`, `resolve`, `enqueue`, `report_call`, `persist`, `emit`. - ---- - -## 10) Configuration (YAML) - -```yaml -scheduler: - authority: - issuer: "https://authority.internal" - require: "dpop" # or "mtls" - queue: - kind: "redis" # or "nats" - url: "redis://redis:6379/4" - mongo: - uri: "mongodb://mongo/scheduler" - impactIndex: - storage: "rocksdb" # "rocksdb" | "redis" | "memory" - warmOnStart: true - usageOnlyDefault: true - limits: - defaultRatePerSecond: 50 - defaultParallelism: 8 - maxJobsPerRun: 50000 - integrates: - scannerUrl: "https://scanner-web.internal" - feedserWebhook: true - vexerWebhook: true - notifications: - emitBus: "internal" # deliver to Notify via internal bus -``` - ---- - -## 11) UI touch‑points - -* **Schedules** page: CRUD, enable/pause, next run, last run stats, mode (analysis/content), selector preview. -* **Runs** page: timeline; heat‑map of deltas; drill‑down to affected images. -* **Dry‑run preview** modal: “This Feedser export touches ~3,214 images; projected deltas: ~420 (34 KEV).” - ---- - -## 12) Failure modes & degradations - -| Condition | Behavior | -| ------------------------------------ | ---------------------------------------------------------------------------------------- | -| ImpactIndex cold / incomplete | Fall back to **All** selection for nightly; for events, cap to KEV+critical until warmed | -| Feedser/Vexer webhook storm | Coalesce by exportId; debounce 30–60 s; keep last | -| Scanner under load (429) | Backoff with jitter; respect per‑tenant/leaky bucket | -| Oversubscription (too many impacted) | Prioritize KEV/critical first; spillover to next window; UI banner shows backlog | -| Notify down | Buffer outbound events in queue (TTL 24h) | -| Mongo slow | Cut batch sizes; sample‑log; alert ops; don’t drop runs unless critical | - ---- - -## 13) Testing matrix - -* **ImpactIndex**: correctness (purl→image sets), performance, persistence after restart, memory pressure with 1M purls. -* **Planner**: dedupe, shard, fairness, limit enforcement, KEV prioritization. -* **Runner**: parallel report calls, error backoff, partial failures, idempotency. -* **End‑to‑end**: Feedser export → deltas visible in UI in ≤60 s. -* **Security**: webhook auth (mTLS/HMAC), DPoP nonce dance, tenant isolation. -* **Chaos**: drop scanner availability; simulate registry throttles (content‑refresh mode). -* **Nightly**: cron tick correctness across timezones and DST. - ---- - -## 14) Implementation notes - -* **Language**: .NET 10 minimal API; Channels‑based pipeline; `System.Threading.RateLimiting`. -* **Bitmaps**: Roaring via `RoaringBitmap` bindings; memory‑map large shards if RocksDB used. -* **Cron**: Quartz‑style parser with timezone support; clock skew tolerated ±60 s. -* **Dry‑run**: use ImpactIndex only; never call scanner. -* **Idempotency**: run segments carry deterministic keys; retries safe. -* **Backpressure**: per‑tenant buckets; per‑host registry budgets respected when content‑refresh enabled. - ---- - -## 15) Sequences (representative) - -**A) Event‑driven rescan (Feedser delta)** - -```mermaid -sequenceDiagram - autonumber - participant FE as Feedser - participant SCH as Scheduler.Worker - participant IDX as ImpactIndex - participant SC as Scanner.WebService - participant NO as Notify - - FE->>SCH: POST /events/feedser-export {exportId, changedProductKeys} - SCH->>IDX: ResolveByPurls(keys, usageOnly=true, sel) - IDX-->>SCH: bitmap(imageIds) → digests list - SCH->>SC: POST /reports {imageDigest} (batch/sequenced) - SC-->>SCH: report deltas (new criticals/highs) - alt delta>0 - SCH->>NO: rescan.delta {digest, newCriticals, links} - end -``` - -**B) Nightly rescan** - -```mermaid -sequenceDiagram - autonumber - participant CRON as Cron - participant SCH as Scheduler.Worker - participant IDX as ImpactIndex - participant SC as Scanner.WebService - - CRON->>SCH: tick (02:00 Europe/Sofia) - SCH->>IDX: ResolveAll(selector) - IDX-->>SCH: candidates - SCH->>SC: POST /reports {digest} (paced) - SC-->>SCH: results - SCH-->>SCH: aggregate, store run stats -``` - -**C) Content‑refresh (tag followers)** - -```mermaid -sequenceDiagram - autonumber - participant SCH as Scheduler - participant SC as Scanner - SCH->>SC: resolve tag→digest (if changed) - alt digest changed - SCH->>SC: POST /scans {imageRef} # new SBOM - SC-->>SCH: scan complete (artifacts) - SCH->>SC: POST /reports {imageDigest} - else unchanged - SCH->>SC: POST /reports {imageDigest} # analysis-only - end -``` - ---- - -## 16) Roadmap - -* **Vuln‑centric impact**: pre‑join vuln→purl→images to rank by **KEV** and **exploited‑in‑the‑wild** signals. -* **Policy diff preview**: when a staged policy changes, show projected breakage set before promotion. -* **Cross‑cluster federation**: one Scheduler instance driving many Scanner clusters (tenant isolation). -* **Windows containers**: integrate Zastava runtime hints for Usage view tightening. - ---- - -**End — component_architecture_scheduler.md** + +> **Scope.** Implementation‑ready architecture for **Scheduler**: a service that (1) **re‑evaluates** already‑cataloged images when intel changes (Conselier/Excitor/policy), (2) orchestrates **nightly** and **ad‑hoc** runs, (3) targets only the **impacted** images using the BOM‑Index, and (4) emits **report‑ready** events that downstream **Notify** fans out. Default mode is **analysis‑only** (no image pull); optional **content‑refresh** can be enabled per schedule. + +--- + +## 0) Mission & boundaries + +**Mission.** Keep scan results **current** without rescanning the world. When new advisories or VEX claims land, **pinpoint** affected images and ask the backend to recompute **verdicts** against the **existing SBOMs**. Surface only **meaningful deltas** to humans and ticket queues. + +**Boundaries.** + +* Scheduler **does not** compute SBOMs and **does not** sign. It calls Scanner/WebService’s **/reports (analysis‑only)** endpoint and lets the backend (Policy + Excitor + Conselier) decide PASS/FAIL. +* Scheduler **may** ask Scanner to **content‑refresh** selected targets (e.g., mutable tags) but the default is **no** image pull. +* Notifications are **not** sent directly; Scheduler emits events consumed by **Notify**. + +--- + +## 1) Runtime shape & projects + +``` +src/ + ├─ StellaOps.Scheduler.WebService/ # REST (schedules CRUD, runs, admin) + ├─ StellaOps.Scheduler.Worker/ # planners + runners (N replicas) + ├─ StellaOps.Scheduler.ImpactIndex/ # purl→images inverted index (roaring bitmaps) + ├─ StellaOps.Scheduler.Models/ # DTOs (Schedule, Run, ImpactSet, Deltas) + ├─ StellaOps.Scheduler.Storage.Mongo/ # schedules, runs, cursors, locks + ├─ StellaOps.Scheduler.Queue/ # Redis Streams / NATS abstraction + ├─ StellaOps.Scheduler.Tests.* # unit/integration/e2e +``` + +**Deployables**: + +* **Scheduler.WebService** (stateless) +* **Scheduler.Worker** (scale‑out; planners + executors) + +**Dependencies**: Authority (OpTok + DPoP/mTLS), Scanner.WebService, Conselier, Excitor, MongoDB, Redis/NATS, (optional) Notify. + +--- + +## 2) Core responsibilities + +1. **Time‑based** runs: cron windows per tenant/timezone (e.g., “02:00 Europe/Sofia”). +2. **Event‑driven** runs: react to **Conselier export** and **Excitor export** deltas (changed product keys / advisories / claims). +3. **Impact targeting**: map changes to **image sets** using a **global inverted index** built from Scanner’s per‑image **BOM‑Index** sidecars. +4. **Run planning**: shard, pace, and rate‑limit jobs to avoid thundering herds. +5. **Execution**: call Scanner **/reports (analysis‑only)** or **/scans (content‑refresh)**; aggregate **delta** results. +6. **Events**: publish `rescan.delta` and `report.ready` summaries for **Notify** & **UI**. +7. **Control plane**: CRUD schedules, **pause/resume**, dry‑run previews, audit. + +--- + +## 3) Data model (Mongo) + +**Database**: `scheduler` + +* `schedules` + + ``` + { _id, tenantId, name, enabled, whenCron, timezone, + mode: "analysis-only" | "content-refresh", + selection: { scope: "all-images" | "by-namespace" | "by-repo" | "by-digest" | "by-labels", + includeTags?: ["prod-*"], digests?: [sha256...], resolvesTags?: bool }, + onlyIf: { lastReportOlderThanDays?: int, policyRevision?: string }, + notify: { onNewFindings: bool, minSeverity: "low|medium|high|critical", includeKEV: bool }, + limits: { maxJobs?: int, ratePerSecond?: int, parallelism?: int }, + createdAt, updatedAt, createdBy, updatedBy } + ``` + +* `runs` + + ``` + { _id, scheduleId?, tenantId, trigger: "cron|conselier|excitor|manual", + reason?: { conselierExportId?, excitorExportId?, cursor? }, + state: "planning|queued|running|completed|error|cancelled", + stats: { candidates: int, deduped: int, queued: int, completed: int, deltas: int, newCriticals: int }, + startedAt, finishedAt, error? } + ``` + +* `impact_cursors` + + ``` + { _id: tenantId, conselierLastExportId, excitorLastExportId, updatedAt } + ``` + +* `locks` (singleton schedulers, run leases) + +* `audit` (CRUD actions, run outcomes) + +**Indexes**: + +* `schedules` on `{tenantId, enabled}`, `{whenCron}`. +* `runs` on `{tenantId, startedAt desc}`, `{state}`. +* TTL optional for completed runs (e.g., 180 days). + +--- + +## 4) ImpactIndex (global inverted index) + +Goal: translate **change keys** → **image sets** in **milliseconds**. + +**Source**: Scanner produces per‑image **BOM‑Index** sidecars (purls, and `usedByEntrypoint` bitmaps). Scheduler ingests/refreshes them to build a **global** index. + +**Representation**: + +* Assign **image IDs** (dense ints) to catalog images. +* Keep **Roaring Bitmaps**: + + * `Contains[purl] → bitmap(imageIds)` + * `UsedBy[purl] → bitmap(imageIds)` (subset of Contains) +* Optionally keep **Owner maps**: `{imageId → {tenantId, namespaces[], repos[]}}` for selection filters. +* Persist in RocksDB/LMDB or Redis‑modules; cache hot shards in memory; snapshot to Mongo for cold start. + +**Update paths**: + +* On new/updated image SBOM: **merge** per‑image set into global maps. +* On image remove/expiry: **clear** id from bitmaps. + +**API (internal)**: + +```csharp +IImpactIndex { + ImpactSet ResolveByPurls(IEnumerable purls, bool usageOnly, Selector sel); + ImpactSet ResolveByVulns(IEnumerable vulnIds, bool usageOnly, Selector sel); // optional (vuln->purl precomputed by Conselier) + ImpactSet ResolveAll(Selector sel); // for nightly +} +``` + +**Selector filters**: tenant, namespaces, repos, labels, digest allowlists, `includeTags` patterns. + +--- + +## 5) External interfaces (REST) + +Base path: `/api/v1/scheduler` (Authority OpToks; scopes: `scheduler.read`, `scheduler.admin`). + +### 5.1 Schedules CRUD + +* `POST /schedules` → create +* `GET /schedules` → list (filter by tenant) +* `GET /schedules/{id}` → details + next run +* `PATCH /schedules/{id}` → pause/resume/update +* `DELETE /schedules/{id}` → delete (soft delete, optional) + +### 5.2 Run control & introspection + +* `POST /run` — ad‑hoc run + + ```json + { "mode": "analysis-only|content-refresh", "selection": {...}, "reason": "manual" } + ``` +* `GET /runs` — list with paging +* `GET /runs/{id}` — status, stats, links to deltas +* `POST /runs/{id}/cancel` — best‑effort cancel + +### 5.3 Previews (dry‑run) + +* `POST /preview/impact` — returns **candidate count** and a small sample of impacted digests for given change keys or selection. + +### 5.4 Event webhooks (optional push from Conselier/Excitor) + +* `POST /events/conselier-export` + + ```json + { "exportId":"...", "changedProductKeys":["pkg:rpm/openssl", ...], "kev": ["CVE-..."], "window": { "from":"...","to":"..." } } + ``` +* `POST /events/excitor-export` + + ```json + { "exportId":"...", "changedClaims":[ { "productKey":"pkg:deb/...", "vulnId":"CVE-...", "status":"not_affected→affected"} ], ... } + ``` + +**Security**: webhook requires **mTLS** or an **HMAC** `X-Scheduler-Signature` (Ed25519 / SHA‑256) plus Authority token. + +--- + +## 6) Planner → Runner pipeline + +### 6.1 Planning algorithm (event‑driven) + +``` +On Export Event (Conselier/Excitor): + keys = Normalize(change payload) # productKeys or vulnIds→productKeys + usageOnly = schedule/policy hint? # default true + sel = Selector for tenant/scope from schedules subscribed to events + + impacted = ImpactIndex.ResolveByPurls(keys, usageOnly, sel) + impacted = ApplyOwnerFilters(impacted, sel) # namespaces/repos/labels + impacted = DeduplicateByDigest(impacted) + impacted = EnforceLimits(impacted, limits.maxJobs) + shards = Shard(impacted, byHashPrefix, n=limits.parallelism) + + For each shard: + Enqueue RunSegment (runId, shard, rate=limits.ratePerSecond) +``` + +**Fairness & pacing** + +* Use **leaky bucket** per tenant and per registry host. +* Prioritize **KEV‑tagged** and **critical** first if oversubscribed. + +### 6.2 Nightly planning + +``` +At cron tick: + sel = resolve selection + candidates = ImpactIndex.ResolveAll(sel) + if lastReportOlderThanDays present → filter by report age (via Scanner catalog) + shard & enqueue as above +``` + +### 6.3 Execution (Runner) + +* Pop **RunSegment** job → for each image digest: + + * **analysis‑only**: `POST scanner/reports { imageDigest, policyRevision? }` + * **content‑refresh**: resolve tag→digest if needed; `POST scanner/scans { imageRef, attest? false }` then `POST /reports` +* Collect **delta**: `newFindings`, `newCriticals`/`highs`, `links` (UI deep link, Rekor if present). +* Persist per‑image outcome in `runs.{id}.stats` (incremental counters). +* Emit `scheduler.rescan.delta` events to **Notify** only when **delta > 0** and matches severity rule. + +--- + +## 7) Event model (outbound) + +**Topic**: `rescan.delta` (internal bus → Notify; UI subscribes via backend). + +```json +{ + "tenant": "tenant-01", + "runId": "324af…", + "imageDigest": "sha256:…", + "newCriticals": 1, + "newHigh": 2, + "kevHits": ["CVE-2025-..."], + "topFindings": [ + { "purl":"pkg:rpm/openssl@3.0.12-...","vulnId":"CVE-2025-...","severity":"critical","link":"https://ui/scans/..." } + ], + "reportUrl": "https://ui/.../scans/sha256:.../report", + "attestation": { "uuid":"rekor-uuid", "verified": true }, + "ts": "2025-10-18T03:12:45Z" +} +``` + +**Also**: `report.ready` for “no‑change” summaries (digest + zero delta), which Notify can ignore by rule. + +--- + +## 8) Security posture + +* **AuthN/Z**: Authority OpToks with `aud=scheduler`; DPoP (preferred) or mTLS. +* **Multi‑tenant**: every schedule, run, and event carries `tenantId`; ImpactIndex filters by tenant‑visible images. +* **Webhook** callers (Conselier/Excitor) present **mTLS** or **HMAC** and Authority token. +* **Input hardening**: size caps on changed key lists; reject >100k keys per event; compress (zstd/gzip) allowed with limits. +* **No secrets** in logs; redact tokens and signatures. + +--- + +## 9) Observability & SLOs + +**Metrics (Prometheus)** + +* `scheduler.events_total{source, result}` +* `scheduler.impact_resolve_seconds{quantile}` +* `scheduler.images_selected_total{mode}` +* `scheduler.jobs_enqueued_total{mode}` +* `scheduler.run_latency_seconds{quantile}` // event → first verdict +* `scheduler.delta_images_total{severity}` +* `scheduler.rate_limited_total{reason}` + +**Targets** + +* Resolve 10k changed keys → impacted set in **<300 ms** (hot cache). +* Event → first rescan verdict in **≤60 s** (p95). +* Nightly coverage 50k images in **≤10 min** with 10 workers (analysis‑only). + +**Tracing** (OTEL): spans `plan`, `resolve`, `enqueue`, `report_call`, `persist`, `emit`. + +--- + +## 10) Configuration (YAML) + +```yaml +scheduler: + authority: + issuer: "https://authority.internal" + require: "dpop" # or "mtls" + queue: + kind: "redis" # or "nats" + url: "redis://redis:6379/4" + mongo: + uri: "mongodb://mongo/scheduler" + impactIndex: + storage: "rocksdb" # "rocksdb" | "redis" | "memory" + warmOnStart: true + usageOnlyDefault: true + limits: + defaultRatePerSecond: 50 + defaultParallelism: 8 + maxJobsPerRun: 50000 + integrates: + scannerUrl: "https://scanner-web.internal" + conselierWebhook: true + excitorWebhook: true + notifications: + emitBus: "internal" # deliver to Notify via internal bus +``` + +--- + +## 11) UI touch‑points + +* **Schedules** page: CRUD, enable/pause, next run, last run stats, mode (analysis/content), selector preview. +* **Runs** page: timeline; heat‑map of deltas; drill‑down to affected images. +* **Dry‑run preview** modal: “This Conselier export touches ~3,214 images; projected deltas: ~420 (34 KEV).” + +--- + +## 12) Failure modes & degradations + +| Condition | Behavior | +| ------------------------------------ | ---------------------------------------------------------------------------------------- | +| ImpactIndex cold / incomplete | Fall back to **All** selection for nightly; for events, cap to KEV+critical until warmed | +| Conselier/Excitor webhook storm | Coalesce by exportId; debounce 30–60 s; keep last | +| Scanner under load (429) | Backoff with jitter; respect per‑tenant/leaky bucket | +| Oversubscription (too many impacted) | Prioritize KEV/critical first; spillover to next window; UI banner shows backlog | +| Notify down | Buffer outbound events in queue (TTL 24h) | +| Mongo slow | Cut batch sizes; sample‑log; alert ops; don’t drop runs unless critical | + +--- + +## 13) Testing matrix + +* **ImpactIndex**: correctness (purl→image sets), performance, persistence after restart, memory pressure with 1M purls. +* **Planner**: dedupe, shard, fairness, limit enforcement, KEV prioritization. +* **Runner**: parallel report calls, error backoff, partial failures, idempotency. +* **End‑to‑end**: Conselier export → deltas visible in UI in ≤60 s. +* **Security**: webhook auth (mTLS/HMAC), DPoP nonce dance, tenant isolation. +* **Chaos**: drop scanner availability; simulate registry throttles (content‑refresh mode). +* **Nightly**: cron tick correctness across timezones and DST. + +--- + +## 14) Implementation notes + +* **Language**: .NET 10 minimal API; Channels‑based pipeline; `System.Threading.RateLimiting`. +* **Bitmaps**: Roaring via `RoaringBitmap` bindings; memory‑map large shards if RocksDB used. +* **Cron**: Quartz‑style parser with timezone support; clock skew tolerated ±60 s. +* **Dry‑run**: use ImpactIndex only; never call scanner. +* **Idempotency**: run segments carry deterministic keys; retries safe. +* **Backpressure**: per‑tenant buckets; per‑host registry budgets respected when content‑refresh enabled. + +--- + +## 15) Sequences (representative) + +**A) Event‑driven rescan (Conselier delta)** + +```mermaid +sequenceDiagram + autonumber + participant FE as Conselier + participant SCH as Scheduler.Worker + participant IDX as ImpactIndex + participant SC as Scanner.WebService + participant NO as Notify + + FE->>SCH: POST /events/conselier-export {exportId, changedProductKeys} + SCH->>IDX: ResolveByPurls(keys, usageOnly=true, sel) + IDX-->>SCH: bitmap(imageIds) → digests list + SCH->>SC: POST /reports {imageDigest} (batch/sequenced) + SC-->>SCH: report deltas (new criticals/highs) + alt delta>0 + SCH->>NO: rescan.delta {digest, newCriticals, links} + end +``` + +**B) Nightly rescan** + +```mermaid +sequenceDiagram + autonumber + participant CRON as Cron + participant SCH as Scheduler.Worker + participant IDX as ImpactIndex + participant SC as Scanner.WebService + + CRON->>SCH: tick (02:00 Europe/Sofia) + SCH->>IDX: ResolveAll(selector) + IDX-->>SCH: candidates + SCH->>SC: POST /reports {digest} (paced) + SC-->>SCH: results + SCH-->>SCH: aggregate, store run stats +``` + +**C) Content‑refresh (tag followers)** + +```mermaid +sequenceDiagram + autonumber + participant SCH as Scheduler + participant SC as Scanner + SCH->>SC: resolve tag→digest (if changed) + alt digest changed + SCH->>SC: POST /scans {imageRef} # new SBOM + SC-->>SCH: scan complete (artifacts) + SCH->>SC: POST /reports {imageDigest} + else unchanged + SCH->>SC: POST /reports {imageDigest} # analysis-only + end +``` + +--- + +## 16) Roadmap + +* **Vuln‑centric impact**: pre‑join vuln→purl→images to rank by **KEV** and **exploited‑in‑the‑wild** signals. +* **Policy diff preview**: when a staged policy changes, show projected breakage set before promotion. +* **Cross‑cluster federation**: one Scheduler instance driving many Scanner clusters (tenant isolation). +* **Windows containers**: integrate Zastava runtime hints for Usage view tightening. + +--- + +**End — component_architecture_scheduler.md** diff --git a/docs/modules/scheduler/operations/worker.md b/docs/modules/scheduler/operations/worker.md index 144a3752..9f7f5a9c 100644 --- a/docs/modules/scheduler/operations/worker.md +++ b/docs/modules/scheduler/operations/worker.md @@ -1,82 +1,82 @@ -# Scheduler Worker – Observability & Runbook - -## Purpose -Monitor planner and runner health for the Scheduler Worker (Sprint 16 telemetry). The new .NET meters surface queue throughput, latency, backlog, and delta severities so operators can detect stalled runs before rescan SLAs slip. - -> **Grafana note:** Import `docs/modules/scheduler/operations/worker-grafana-dashboard.json` into the Prometheus-backed Grafana stack that scrapes the OpenTelemetry Collector. - ---- - -## Key metrics - -| Metric | Use case | Suggested query | -| --- | --- | --- | -| `scheduler_planner_runs_total{status}` | Planner throughput & failure ratio | `sum by (status) (rate(scheduler_planner_runs_total[5m]))` | -| `scheduler_planner_latency_seconds_bucket` | Planning latency (p95 / p99) | `histogram_quantile(0.95, sum by (le) (rate(scheduler_planner_latency_seconds_bucket[5m])))` | -| `scheduler_runner_segments_total{status}` | Runner success vs retries | `sum by (status) (rate(scheduler_runner_segments_total[5m]))` | -| `scheduler_runner_delta_{critical,high,total}` | Newly-detected findings | `sum(rate(scheduler_runner_delta_critical_total[5m]))` | -| `scheduler_runner_backlog{scheduleId}` | Remaining digests awaiting runner | `max by (scheduleId) (scheduler_runner_backlog)` | -| `scheduler_runs_active{mode}` | Active runs in-flight | `sum(scheduler_runs_active)` | - -Reference queries power the bundled Grafana dashboard panels. Use the `mode` template variable to focus on `analysisOnly` versus `contentRefresh` schedules. - ---- - -## Grafana dashboard - -1. Import `docs/modules/scheduler/operations/worker-grafana-dashboard.json` (UID `scheduler-worker-observability`). -2. Point the `datasource` variable to the Prometheus instance scraping the collector. Optional: pin the `mode` variable to a specific schedule mode. -3. Panels included: - - **Planner Runs per Status** – visualises success vs failure ratio. - - **Planner Latency P95** – highlights degradations in ImpactIndex or Mongo lookups. - - **Runner Segments per Status** – shows retry pressure and queue health. - - **New Findings per Severity** – rolls up delta counters (critical/high/total). - - **Runner Backlog by Schedule** – tabulates outstanding digests per schedule. - - **Active Runs** – stat panel showing the current number of in-flight runs. - -Capture screenshots once Grafana provisioning completes and store them under `docs/assets/dashboards/` (pending automation ticket OBS-157). - ---- - -## Prometheus alerts - -Import `docs/modules/scheduler/operations/worker-prometheus-rules.yaml` into your Prometheus rule configuration. The bundle defines: - -- **SchedulerPlannerFailuresHigh** – 5%+ of planner runs failed for 10 minutes. Page SRE. -- **SchedulerPlannerLatencyHigh** – planner p95 latency remains above 45 s for 10 minutes. Investigate ImpactIndex, Mongo, and Feedser/Vexer event queues. -- **SchedulerRunnerBacklogGrowing** – backlog exceeded 500 images for 15 minutes. Inspect runner workers, Scanner availability, and rate limiting. -- **SchedulerRunStuck** – active run count stayed flat for 30 minutes while remaining non-zero. Check stuck segments, expired leases, and scanner retries. - -Hook these alerts into the existing Observability notification pathway (`observability-pager` routing key) and ensure `service=scheduler-worker` is mapped to the on-call rotation. - ---- - -## Runbook snapshot - -1. **Planner failure/latency:** - - Check Planner logs for ImpactIndex or Mongo exceptions. - - Verify Feedser/Vexer webhook health; requeue events if necessary. - - If planner is overwhelmed, temporarily reduce schedule parallelism via `stella scheduler schedule update`. -2. **Runner backlog spike:** - - Confirm Scanner WebService health (`/healthz`). - - Inspect runner queue for stuck segments; consider increasing runner workers or scaling scanner capacity. - - Review rate limits (schedule limits, ImpactIndex throughput) before changing global throttles. -3. **Stuck runs:** - - Use `stella scheduler runs list --state running` to identify affected runs. - - Drill into Grafana panel “Runner Backlog by Schedule” to see offending schedule IDs. - - If a segment will not progress, use `stella scheduler segments release --segment ` to force retry after resolving root cause. -4. **Unexpected critical deltas:** - - Correlate `scheduler_runner_delta_critical_total` spikes with Notify events (`scheduler.rescan.delta`). - - Pivot to Scanner report links for impacted digests and confirm they match upstream advisories/policies. - -Document incidents and mitigation in `ops/runbooks/INCIDENT_LOG.md` (per SRE policy) and attach Grafana screenshots for post-mortems. - ---- - -## Checklist - -- [ ] Grafana dashboard imported and wired to Prometheus datasource. -- [ ] Prometheus alert rules deployed (see above). -- [ ] Runbook linked from on-call rotation portal. -- [ ] Observability Guild sign-off captured for Sprint 16 telemetry (OWNER: @obs-guild). - +# Scheduler Worker – Observability & Runbook + +## Purpose +Monitor planner and runner health for the Scheduler Worker (Sprint 16 telemetry). The new .NET meters surface queue throughput, latency, backlog, and delta severities so operators can detect stalled runs before rescan SLAs slip. + +> **Grafana note:** Import `docs/modules/scheduler/operations/worker-grafana-dashboard.json` into the Prometheus-backed Grafana stack that scrapes the OpenTelemetry Collector. + +--- + +## Key metrics + +| Metric | Use case | Suggested query | +| --- | --- | --- | +| `scheduler_planner_runs_total{status}` | Planner throughput & failure ratio | `sum by (status) (rate(scheduler_planner_runs_total[5m]))` | +| `scheduler_planner_latency_seconds_bucket` | Planning latency (p95 / p99) | `histogram_quantile(0.95, sum by (le) (rate(scheduler_planner_latency_seconds_bucket[5m])))` | +| `scheduler_runner_segments_total{status}` | Runner success vs retries | `sum by (status) (rate(scheduler_runner_segments_total[5m]))` | +| `scheduler_runner_delta_{critical,high,total}` | Newly-detected findings | `sum(rate(scheduler_runner_delta_critical_total[5m]))` | +| `scheduler_runner_backlog{scheduleId}` | Remaining digests awaiting runner | `max by (scheduleId) (scheduler_runner_backlog)` | +| `scheduler_runs_active{mode}` | Active runs in-flight | `sum(scheduler_runs_active)` | + +Reference queries power the bundled Grafana dashboard panels. Use the `mode` template variable to focus on `analysisOnly` versus `contentRefresh` schedules. + +--- + +## Grafana dashboard + +1. Import `docs/modules/scheduler/operations/worker-grafana-dashboard.json` (UID `scheduler-worker-observability`). +2. Point the `datasource` variable to the Prometheus instance scraping the collector. Optional: pin the `mode` variable to a specific schedule mode. +3. Panels included: + - **Planner Runs per Status** – visualises success vs failure ratio. + - **Planner Latency P95** – highlights degradations in ImpactIndex or Mongo lookups. + - **Runner Segments per Status** – shows retry pressure and queue health. + - **New Findings per Severity** – rolls up delta counters (critical/high/total). + - **Runner Backlog by Schedule** – tabulates outstanding digests per schedule. + - **Active Runs** – stat panel showing the current number of in-flight runs. + +Capture screenshots once Grafana provisioning completes and store them under `docs/assets/dashboards/` (pending automation ticket OBS-157). + +--- + +## Prometheus alerts + +Import `docs/modules/scheduler/operations/worker-prometheus-rules.yaml` into your Prometheus rule configuration. The bundle defines: + +- **SchedulerPlannerFailuresHigh** – 5%+ of planner runs failed for 10 minutes. Page SRE. +- **SchedulerPlannerLatencyHigh** – planner p95 latency remains above 45 s for 10 minutes. Investigate ImpactIndex, Mongo, and Conselier/Excitor event queues. +- **SchedulerRunnerBacklogGrowing** – backlog exceeded 500 images for 15 minutes. Inspect runner workers, Scanner availability, and rate limiting. +- **SchedulerRunStuck** – active run count stayed flat for 30 minutes while remaining non-zero. Check stuck segments, expired leases, and scanner retries. + +Hook these alerts into the existing Observability notification pathway (`observability-pager` routing key) and ensure `service=scheduler-worker` is mapped to the on-call rotation. + +--- + +## Runbook snapshot + +1. **Planner failure/latency:** + - Check Planner logs for ImpactIndex or Mongo exceptions. + - Verify Conselier/Excitor webhook health; requeue events if necessary. + - If planner is overwhelmed, temporarily reduce schedule parallelism via `stella scheduler schedule update`. +2. **Runner backlog spike:** + - Confirm Scanner WebService health (`/healthz`). + - Inspect runner queue for stuck segments; consider increasing runner workers or scaling scanner capacity. + - Review rate limits (schedule limits, ImpactIndex throughput) before changing global throttles. +3. **Stuck runs:** + - Use `stella scheduler runs list --state running` to identify affected runs. + - Drill into Grafana panel “Runner Backlog by Schedule” to see offending schedule IDs. + - If a segment will not progress, use `stella scheduler segments release --segment ` to force retry after resolving root cause. +4. **Unexpected critical deltas:** + - Correlate `scheduler_runner_delta_critical_total` spikes with Notify events (`scheduler.rescan.delta`). + - Pivot to Scanner report links for impacted digests and confirm they match upstream advisories/policies. + +Document incidents and mitigation in `ops/runbooks/INCIDENT_LOG.md` (per SRE policy) and attach Grafana screenshots for post-mortems. + +--- + +## Checklist + +- [ ] Grafana dashboard imported and wired to Prometheus datasource. +- [ ] Prometheus alert rules deployed (see above). +- [ ] Runbook linked from on-call rotation portal. +- [ ] Observability Guild sign-off captured for Sprint 16 telemetry (OWNER: @obs-guild). + diff --git a/docs/modules/vex-lens/implementation_plan.md b/docs/modules/vex-lens/implementation_plan.md index b0d8331a..612c5255 100644 --- a/docs/modules/vex-lens/implementation_plan.md +++ b/docs/modules/vex-lens/implementation_plan.md @@ -1,63 +1,63 @@ -# Implementation plan — VEX Consensus Lens - -## Delivery phases -- **Phase 1 – Core lens service** - Build normalisation pipeline (CSAF/OpenVEX/CycloneDX), product mapping library, trust weighting functions, consensus algorithm, and persistence (`vex_consensus`, history, conflicts). -- **Phase 2 – API & integrations** - Expose `/vex/consensus` query/detail/simulate/export endpoints, integrate Policy Engine thresholds, Vuln Explorer UI chips, and VEX Lens change events. -- **Phase 3 – Issuer Directory & signatures** - Deliver issuer registry, key management, signature verification, RBAC, audit logs, and tenant overrides. -- **Phase 4 – Console & CLI experiences** - Ship Console module (lists, evidence table, quorum bar, conflicts, simulation drawer) and CLI commands (`stella vex consensus ...`) with export support. -- **Phase 5 – Recompute & performance** - Implement recompute scheduling (policy activation, Excitator deltas), caching, load tests (10M records/tenant), observability dashboards, and Offline Kit exports. - -## Work breakdown -- **VEX Lens service** - - Normalise VEX payloads, maintain scope scores, compute consensus digest. - - Trust weighting functions (issuer tier, freshness decay, scope quality). - - Idempotent workers for consensus projection and history tracking. - - Conflict handling queue for manual review and notifications. -- **Integrations** - - Excitator: enrich VEX events with issuer hints, signatures, product trees. - - Policy Engine: trust knobs, simulation endpoints, policy-driven recompute. - - Vuln Explorer & Advisory AI: consensus badges, conflict surfacing. -- **Issuer Directory** - - CRUD for issuers/keys, audit logs, import CSAF publishers, tenant overrides. - - Signature verification endpoints consumed by Lens. -- **APIs & UX** - - REST endpoints for query/detail/conflict export, trust weight updates. - - Console module with filters, saved views, evidence table, simulation drawer. - - CLI commands for list/show/simulate/export with JSON/CSV output. -- **Observability & Ops** - - Metrics (consensus latency, conflict rate, signature failures, cache hit rate), logs, traces. - - Dashboards + runbooks for recompute storms, mapping failures, signature errors, quota breaches. - - Offline exports for Export Center/Offline Kit. - -## Acceptance criteria -- Consensus results reproducible across supported VEX formats with deterministic digests and provenance. -- Signature verification influences trust weights; unverifiable evidence is down-weighted without pipeline failure. -- Policy simulations show quorum shifts without persisting state; Vuln Explorer consumes consensus signals. -- Issuer Directory enforces RBAC, audit logs, and key rotation; CLI & Console parity achieved. -- Recompute pipeline handles Excitator deltas and policy activations with backpressure and incident surfacing. -- Observability dashboards/alerts cover ingestion lag, conflict spikes, signature failures, performance budgets (P95 < 500 ms for 100-row pages at 10M records/tenant). - -## Risks & mitigations -- **Product mapping ambiguity:** conservative scope scoring, manual overrides, surfaced warnings, policy review hooks. -- **Issuer compromise:** signature verification, trust weighting, tenant overrides, revocation runbooks. -- **Evidence storms:** batching, worker sharding, orchestrator rate limiting, priority queues. -- **Performance degradation:** caching, indexing, load tests, quota enforcement. -- **Offline gaps:** deterministic exports, manifest hashes, Offline Kit tests. - -## Test strategy -- **Unit:** normalisers, mapping, trust weights, consensus lattice, signature verification. -- **Property:** randomised evidence sets verifying lattice commutativity and determinism. -- **Integration:** Excitator → Lens → Policy/Vuln Explorer flow, issuer overrides, simulation. -- **Performance:** large tenant datasets, cache behaviour, concurrency tests. -- **Security:** RBAC, tenant scoping, signature tampering, issuer revocation. -- **Offline:** export/import verification, CLI parity. - -## Definition of done -- Lens service, issuer directory, API/CLI/Console components deployed with telemetry and runbooks. -- Documentation set (overview, algorithm, issuer directory, API, console, policy trust) updated with imposed rule statements. -- ./TASKS.md and ../../TASKS.md reflect current status; Offline Kit parity confirmed. +# Implementation plan — VEX Consensus Lens + +## Delivery phases +- **Phase 1 – Core lens service** + Build normalisation pipeline (CSAF/OpenVEX/CycloneDX), product mapping library, trust weighting functions, consensus algorithm, and persistence (`vex_consensus`, history, conflicts). +- **Phase 2 – API & integrations** + Expose `/vex/consensus` query/detail/simulate/export endpoints, integrate Policy Engine thresholds, Vuln Explorer UI chips, and VEX Lens change events. +- **Phase 3 – Issuer Directory & signatures** + Deliver issuer registry, key management, signature verification, RBAC, audit logs, and tenant overrides. +- **Phase 4 – Console & CLI experiences** + Ship Console module (lists, evidence table, quorum bar, conflicts, simulation drawer) and CLI commands (`stella vex consensus ...`) with export support. +- **Phase 5 – Recompute & performance** + Implement recompute scheduling (policy activation, Excitor deltas), caching, load tests (10M records/tenant), observability dashboards, and Offline Kit exports. + +## Work breakdown +- **VEX Lens service** + - Normalise VEX payloads, maintain scope scores, compute consensus digest. + - Trust weighting functions (issuer tier, freshness decay, scope quality). + - Idempotent workers for consensus projection and history tracking. + - Conflict handling queue for manual review and notifications. +- **Integrations** + - Excitor: enrich VEX events with issuer hints, signatures, product trees. + - Policy Engine: trust knobs, simulation endpoints, policy-driven recompute. + - Vuln Explorer & Advisory AI: consensus badges, conflict surfacing. +- **Issuer Directory** + - CRUD for issuers/keys, audit logs, import CSAF publishers, tenant overrides. + - Signature verification endpoints consumed by Lens. +- **APIs & UX** + - REST endpoints for query/detail/conflict export, trust weight updates. + - Console module with filters, saved views, evidence table, simulation drawer. + - CLI commands for list/show/simulate/export with JSON/CSV output. +- **Observability & Ops** + - Metrics (consensus latency, conflict rate, signature failures, cache hit rate), logs, traces. + - Dashboards + runbooks for recompute storms, mapping failures, signature errors, quota breaches. + - Offline exports for Export Center/Offline Kit. + +## Acceptance criteria +- Consensus results reproducible across supported VEX formats with deterministic digests and provenance. +- Signature verification influences trust weights; unverifiable evidence is down-weighted without pipeline failure. +- Policy simulations show quorum shifts without persisting state; Vuln Explorer consumes consensus signals. +- Issuer Directory enforces RBAC, audit logs, and key rotation; CLI & Console parity achieved. +- Recompute pipeline handles Excitor deltas and policy activations with backpressure and incident surfacing. +- Observability dashboards/alerts cover ingestion lag, conflict spikes, signature failures, performance budgets (P95 < 500 ms for 100-row pages at 10M records/tenant). + +## Risks & mitigations +- **Product mapping ambiguity:** conservative scope scoring, manual overrides, surfaced warnings, policy review hooks. +- **Issuer compromise:** signature verification, trust weighting, tenant overrides, revocation runbooks. +- **Evidence storms:** batching, worker sharding, orchestrator rate limiting, priority queues. +- **Performance degradation:** caching, indexing, load tests, quota enforcement. +- **Offline gaps:** deterministic exports, manifest hashes, Offline Kit tests. + +## Test strategy +- **Unit:** normalisers, mapping, trust weights, consensus lattice, signature verification. +- **Property:** randomised evidence sets verifying lattice commutativity and determinism. +- **Integration:** Excitor → Lens → Policy/Vuln Explorer flow, issuer overrides, simulation. +- **Performance:** large tenant datasets, cache behaviour, concurrency tests. +- **Security:** RBAC, tenant scoping, signature tampering, issuer revocation. +- **Offline:** export/import verification, CLI parity. + +## Definition of done +- Lens service, issuer directory, API/CLI/Console components deployed with telemetry and runbooks. +- Documentation set (overview, algorithm, issuer directory, API, console, policy trust) updated with imposed rule statements. +- ./TASKS.md and ../../TASKS.md reflect current status; Offline Kit parity confirmed. diff --git a/docs/modules/vexer/TASKS.md b/docs/modules/vexer/TASKS.md deleted file mode 100644 index 54fb56e2..00000000 --- a/docs/modules/vexer/TASKS.md +++ /dev/null @@ -1,9 +0,0 @@ -# Task board — Vexer - -> Local tasks should link back to ./AGENTS.md and mirror status updates into ../../TASKS.md when applicable. - -| ID | Status | Owner(s) | Description | Notes | -|----|--------|----------|-------------|-------| -| VEXER-DOCS-0001 | DOING (2025-10-29) | Docs Guild | Validate that ./README.md aligns with the latest release notes. | See ./AGENTS.md | -| VEXER-OPS-0001 | TODO | Ops Guild | Review runbooks/observability assets after next sprint demo. | Sync outcomes back to ../../TASKS.md | -| VEXER-ENG-0001 | TODO | Module Team | Cross-check implementation plan milestones against ../../implplan/SPRINTS.md. | Update status via ./AGENTS.md workflow | diff --git a/docs/modules/vuln-explorer/architecture.md b/docs/modules/vuln-explorer/architecture.md index f1dbc49f..59d91629 100644 --- a/docs/modules/vuln-explorer/architecture.md +++ b/docs/modules/vuln-explorer/architecture.md @@ -47,18 +47,25 @@ CLI mirrors these endpoints (`stella findings list|view|update|export`). Console - Scheduler integration triggers follow-up scans or policy re-evaluation when remediation plan reaches checkpoint. - Zastava (Differential SBOM) feeds runtime exposure signals to reprioritise findings automatically. -## 5) Observability & compliance - -- Metrics: `findings_open_total{severity,tenant}`, `findings_mttr_seconds`, `triage_actions_total{type}`, `report_generation_seconds`. -- Logs: structured with `findingId`, `artifactId`, `advisory`, `policyVersion`, `actor`, `actionType`. -- Audit exports: `audit_log.jsonl` appended whenever state changes; offline bundles include signed audit log and manifest. -- Compliance: accepted risk requires dual approval and stores justification plus expiry reminders (raised through Notify). - -## 6) Offline bundle requirements - -- Bundle structure: - - `manifest.json` (hashes, counts, policy version, generation timestamp). - - `findings.jsonl` (current open findings). +## 5) Observability & compliance + +- Metrics: `findings_open_total{severity,tenant}`, `findings_mttr_seconds`, `triage_actions_total{type}`, `report_generation_seconds`. +- Logs: structured with `findingId`, `artifactId`, `advisory`, `policyVersion`, `actor`, `actionType`. +- Audit exports: `audit_log.jsonl` appended whenever state changes; offline bundles include signed audit log and manifest. +- Compliance: accepted risk requires dual approval and stores justification plus expiry reminders (raised through Notify). + +## 6) Identity & access integration + +- **Scopes** – `vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit` map to read-only, triage, workflow, and audit experiences respectively. The deprecated `vuln:read` scope is still honoured for legacy tokens but is no longer advertised. +- **Attribute filters (ABAC)** – Authority enforces per-service-account filters via the client-credential parameters `vuln_env`, `vuln_owner`, and `vuln_business_tier`. Service accounts define the allowed values in `authority.yaml` (`attributes` block). Tokens include the resolved filters as claims (`stellaops:vuln_env`, `stellaops:vuln_owner`, `stellaops:vuln_business_tier`), and tokens persisted to Mongo retain the same values for audit and revocation. +- **Audit trail** – Every token issuance emits `authority.vuln_attr.*` audit properties that mirror the resolved filter set, along with `delegation.service_account` and ordered `delegation.actor[n]` entries so Vuln Explorer can correlate access decisions. +- **Permalinks** – Signed permalinks inherit the caller’s ABAC filters; consuming services must enforce the embedded claims in addition to scope checks when resolving permalinks. + +## 7) Offline bundle requirements + +- Bundle structure: + - `manifest.json` (hashes, counts, policy version, generation timestamp). + - `findings.jsonl` (current open findings). - `history.jsonl` (state changes). - `actions.jsonl` (comments, assignments, tickets). - `reports/` (generated PDFs/CSVs). diff --git a/docs/modules/vuln-explorer/implementation_plan.md b/docs/modules/vuln-explorer/implementation_plan.md index 2a6c734e..30dbfa92 100644 --- a/docs/modules/vuln-explorer/implementation_plan.md +++ b/docs/modules/vuln-explorer/implementation_plan.md @@ -1,70 +1,70 @@ -# Implementation plan — Vulnerability Explorer - -## Delivery phases -- **Phase 1 – Findings Ledger & resolver** - Create append-only ledger, projector, ecosystem resolvers (npm/Maven/PyPI/Go/RPM/DEB), canonical advisory keys, and provenance hashing. -- **Phase 2 – API & simulation** - Ship Vuln Explorer API (list/detail/grouping/simulation), batch evaluation with Policy Engine rationales, and export orchestrator. -- **Phase 3 – Console & CLI workflows** - Deliver triage UI (assignments, comments, remediation plans, simulation bar), keyboard accessibility, and CLI commands (`stella vuln ...`) with JSON/CSV output. -- **Phase 4 – Automation & integrations** - Integrate Advisory AI hints, Zastava runtime exposure, Notify rules, Scheduler follow-up scans, and Graph Explorer deep links. -- **Phase 5 – Exports & offline parity** - Generate deterministic bundles (JSON, CSV, PDF, Offline Kit manifests), audit logs, and signed reports. -- **Phase 6 – Observability & hardening** - Complete dashboards (projection lag, MTTR, accepted-risk cadence), alerts, runbooks, performance tuning (5M findings/tenant), and security/RBAC validation. - -## Work breakdown -- **Findings Ledger** - - Define event schema, Merkle root anchoring, append-only storage, history tables. - - Projector to `finding_records` and `finding_history`, idempotent event processing, time travel snapshots. - - Resolver pipelines referencing SBOM inventory deltas, policy outputs, VEX consensus, runtime signals. -- **API & exports** - - REST endpoints (`/v1/findings`, `/v1/findings/{id}`, `/actions`, `/reports`, `/exports`) with ABAC filters. - - Simulation endpoint returning diffs, integration with Policy Engine batch evaluation. - - Export jobs for JSON/CSV/PDF plus Offline Kit bundle assembly and signing. -- **Console** - - Feature module `vuln-explorer` with grid, filters, saved views, deep links, detail tabs (policy, evidence, paths, remediation). - - Simulation drawer, delta chips, accepted-risk approvals, evidence bundle viewer. - - Accessibility (keyboard navigation, ARIA), virtualization for large result sets. -- **CLI** - - Commands `stella vuln list|show|simulate|assign|accept-risk|verify-fix|export`. - - Stable schemas for automation; piping support; tests for exit codes. -- **Integrations** - - Conseiller/Excitator: normalized advisory keys, linksets, evidence retrieval. - - SBOM Service: inventory deltas with scope/runtime flags, safe version hints. - - Notify: events for SLA breaches, accepted-risk expiries, remediation deadlines. - - Scheduler: trigger rescans when remediation plan milestones complete. -- **Observability & ops** - - Metrics (open findings, MTTR, projection lag, export duration, SLA burn), logs/traces with correlation IDs. - - Alerting on projector backlog, API 5xx spikes, export failures, accepted-risk nearing expiry. - - Runbooks covering recompute storms, mapping errors, report issues. - -## Acceptance criteria -- Ledger/event sourcing reproduces historical states byte-for-byte; Merkle hashes verify integrity. -- Resolver respects ecosystem semantics, scope, and runtime context; path evidence presented in UI/CLI. -- Triage workflows (assignment, comments, accepted-risk) enforce justification and approval requirements with audit records. -- Simulation returns policy diffs without mutating state; CLI/UI parity achieved for simulation and exports. -- Exports and Offline Kit bundles reproducible with signed manifests and provenance; reports available in JSON/CSV/PDF. -- Observability dashboards show green SLOs, alerts fire for projection lag or SLA burns, and runbooks documented. -- RBAC/ABAC validated; attachments encrypted; tenant isolation guaranteed. - -## Risks & mitigations -- **Advisory identity collisions:** strict canonicalization, linkset references, raw evidence access. -- **Resolver inaccuracies:** property-based tests, path verification, manual override workflows. -- **Projection lag/backlog:** autoscaling, queue backpressure, alerting, pause controls. -- **Export size/performance:** streaming NDJSON, size estimators, chunked downloads. -- **User confusion on suppression:** rationale tab, explicit badges, explain traces. - -## Test strategy -- **Unit:** resolver algorithms, state machine transitions, policy mapping, export builders. -- **Integration:** ingestion → ledger → projector → API flow, simulation, Notify notifications. -- **E2E:** Console triage scenarios, CLI flows, accessibility tests. -- **Performance:** 5M findings/tenant, projection rebuild, export generation. -- **Security:** RBAC/ABAC matrix, CSRF, attachment encryption, signed URL expiry. -- **Determinism:** time-travel snapshots, export manifest hashing, Offline Kit replay. - -## Definition of done -- Services, UI/CLI, integrations, exports, and observability deployed with runbooks and Offline Kit parity. -- Documentation suite (overview, using-console, API, CLI, findings ledger, policy mapping, VEX/SBOM integration, telemetry, security, runbooks, install) updated with imposed rule statement. -- ./TASKS.md and ../../TASKS.md reflect active progress; compliance checklists appended where required. +# Implementation plan — Vulnerability Explorer + +## Delivery phases +- **Phase 1 – Findings Ledger & resolver** + Create append-only ledger, projector, ecosystem resolvers (npm/Maven/PyPI/Go/RPM/DEB), canonical advisory keys, and provenance hashing. +- **Phase 2 – API & simulation** + Ship Vuln Explorer API (list/detail/grouping/simulation), batch evaluation with Policy Engine rationales, and export orchestrator. +- **Phase 3 – Console & CLI workflows** + Deliver triage UI (assignments, comments, remediation plans, simulation bar), keyboard accessibility, and CLI commands (`stella vuln ...`) with JSON/CSV output. +- **Phase 4 – Automation & integrations** + Integrate Advisory AI hints, Zastava runtime exposure, Notify rules, Scheduler follow-up scans, and Graph Explorer deep links. +- **Phase 5 – Exports & offline parity** + Generate deterministic bundles (JSON, CSV, PDF, Offline Kit manifests), audit logs, and signed reports. +- **Phase 6 – Observability & hardening** + Complete dashboards (projection lag, MTTR, accepted-risk cadence), alerts, runbooks, performance tuning (5M findings/tenant), and security/RBAC validation. + +## Work breakdown +- **Findings Ledger** + - Define event schema, Merkle root anchoring, append-only storage, history tables. + - Projector to `finding_records` and `finding_history`, idempotent event processing, time travel snapshots. + - Resolver pipelines referencing SBOM inventory deltas, policy outputs, VEX consensus, runtime signals. +- **API & exports** + - REST endpoints (`/v1/findings`, `/v1/findings/{id}`, `/actions`, `/reports`, `/exports`) with ABAC filters. + - Simulation endpoint returning diffs, integration with Policy Engine batch evaluation. + - Export jobs for JSON/CSV/PDF plus Offline Kit bundle assembly and signing. +- **Console** + - Feature module `vuln-explorer` with grid, filters, saved views, deep links, detail tabs (policy, evidence, paths, remediation). + - Simulation drawer, delta chips, accepted-risk approvals, evidence bundle viewer. + - Accessibility (keyboard navigation, ARIA), virtualization for large result sets. +- **CLI** + - Commands `stella vuln list|show|simulate|assign|accept-risk|verify-fix|export`. + - Stable schemas for automation; piping support; tests for exit codes. +- **Integrations** + - Conseiller/Excitor: normalized advisory keys, linksets, evidence retrieval. + - SBOM Service: inventory deltas with scope/runtime flags, safe version hints. + - Notify: events for SLA breaches, accepted-risk expiries, remediation deadlines. + - Scheduler: trigger rescans when remediation plan milestones complete. +- **Observability & ops** + - Metrics (open findings, MTTR, projection lag, export duration, SLA burn), logs/traces with correlation IDs. + - Alerting on projector backlog, API 5xx spikes, export failures, accepted-risk nearing expiry. + - Runbooks covering recompute storms, mapping errors, report issues. + +## Acceptance criteria +- Ledger/event sourcing reproduces historical states byte-for-byte; Merkle hashes verify integrity. +- Resolver respects ecosystem semantics, scope, and runtime context; path evidence presented in UI/CLI. +- Triage workflows (assignment, comments, accepted-risk) enforce justification and approval requirements with audit records. +- Simulation returns policy diffs without mutating state; CLI/UI parity achieved for simulation and exports. +- Exports and Offline Kit bundles reproducible with signed manifests and provenance; reports available in JSON/CSV/PDF. +- Observability dashboards show green SLOs, alerts fire for projection lag or SLA burns, and runbooks documented. +- RBAC/ABAC validated; attachments encrypted; tenant isolation guaranteed. + +## Risks & mitigations +- **Advisory identity collisions:** strict canonicalization, linkset references, raw evidence access. +- **Resolver inaccuracies:** property-based tests, path verification, manual override workflows. +- **Projection lag/backlog:** autoscaling, queue backpressure, alerting, pause controls. +- **Export size/performance:** streaming NDJSON, size estimators, chunked downloads. +- **User confusion on suppression:** rationale tab, explicit badges, explain traces. + +## Test strategy +- **Unit:** resolver algorithms, state machine transitions, policy mapping, export builders. +- **Integration:** ingestion → ledger → projector → API flow, simulation, Notify notifications. +- **E2E:** Console triage scenarios, CLI flows, accessibility tests. +- **Performance:** 5M findings/tenant, projection rebuild, export generation. +- **Security:** RBAC/ABAC matrix, CSRF, attachment encryption, signed URL expiry. +- **Determinism:** time-travel snapshots, export manifest hashing, Offline Kit replay. + +## Definition of done +- Services, UI/CLI, integrations, exports, and observability deployed with runbooks and Offline Kit parity. +- Documentation suite (overview, using-console, API, CLI, findings ledger, policy mapping, VEX/SBOM integration, telemetry, security, runbooks, install) updated with imposed rule statement. +- ./TASKS.md and ../../TASKS.md reflect active progress; compliance checklists appended where required. diff --git a/docs/notifications/architecture.md b/docs/notifications/architecture.md index c17d4934..8d5a5316 100644 --- a/docs/notifications/architecture.md +++ b/docs/notifications/architecture.md @@ -31,11 +31,12 @@ Tenant API│ REST + gRPC WIP │ │ rules/channels│ │ Connectors │──────▶│ Slack/Teams/... │ │ (plug-ins) │ │ External targets │ └─────────────┘ └──────────────────┘ -``` - -- **WebService** hosts REST endpoints (`/channels`, `/rules`, `/templates`, `/deliveries`, `/digests`, `/stats`) and handles schema normalisation, validation, and Authority enforcement. -- **Worker** subscribes to the platform event bus, evaluates rules per tenant, applies throttles/digests, renders payloads, writes ledger entries, and invokes connectors. -- **Plug-ins** live under `plugins/notify/` and are loaded deterministically at service start (`orderedPlugins` list). Each implements connector contracts and optional health/test-preview providers. +``` + +- **2025-11-02 decision — module boundaries.** Keep `src/Notify/` as the shared notification toolkit (engine, storage, queue, connectors) that multiple hosts can consume. `src/Notifier/` remains the Notifications Studio runtime (WebService + Worker) composed from those libraries. Do not collapse the directories until a packaging RFC covers build impacts, offline kit parity, and imposed-rule propagation. +- **WebService** hosts REST endpoints (`/channels`, `/rules`, `/templates`, `/deliveries`, `/digests`, `/stats`) and handles schema normalisation, validation, and Authority enforcement. +- **Worker** subscribes to the platform event bus, evaluates rules per tenant, applies throttles/digests, renders payloads, writes ledger entries, and invokes connectors. +- **Plug-ins** live under `plugins/notify/` and are loaded deterministically at service start (`orderedPlugins` list). Each implements connector contracts and optional health/test-preview providers. Both services share options via `notify.yaml` (see `etc/notify.yaml.sample`). For dev/test scenarios, an in-memory repository exists but production requires Mongo + Redis/NATS for durability and coordination. diff --git a/docs/notifications/overview.md b/docs/notifications/overview.md index f0320871..35f6b49e 100644 --- a/docs/notifications/overview.md +++ b/docs/notifications/overview.md @@ -48,6 +48,7 @@ The Notify WebService fronts worker state with REST APIs used by the UI and CLI. | **Scaling** | Workers scale horizontally; per-tenant rule snapshots are cached and refreshed from Mongo change streams. Redis (or equivalent) guards throttles and locks. | | **Offline** | Offline Kits include plug-ins, default templates, and seed rules. Operators can edit YAML/JSON manifests before air-gapped deployment. | | **Security** | Channel secrets use indirection (`secretRef`), Authority-protected OAuth clients secure API access, and delivery payloads are redacted before storage where required. | +| **Module boundaries** | 2025-11-02 decision: keep `src/Notify/` as the shared notification toolkit and `src/Notifier/` as the Notifications Studio runtime host until a packaging RFC covers the implications of merging. | --- diff --git a/docs/risk/risk-profiles.md b/docs/risk/risk-profiles.md index db1ffa4c..37244e5b 100644 --- a/docs/risk/risk-profiles.md +++ b/docs/risk/risk-profiles.md @@ -1,57 +1,57 @@ -# Risk Scoring Profiles - -> Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. - -## Overview - -Risk Scoring Profiles define customizable formulas that convert raw evidence (CVSS, EPSS-like exploit likelihood, KEV exploited lists, VEX status, reachability, runtime evidence, fix availability, asset criticality, provenance trust) into normalized risk scores (0–100) with severity buckets. Profiles are authored in Policy Studio, simulated, versioned, and executed by the scoring engine with full explainability. - -- **Primary components:** Policy Engine, Findings Ledger, Conseiller, Excitator, Console, Policy Studio, CLI, Export Center, Authority & Tenancy, Observability. -- **Surfaces:** policy documents, scoring engine, factor providers, explainability artefacts, APIs, CLI, UI. - -Aggregation-Only Contract remains in force: Conseiller and Excitator never merge or mutate source records. Risk scoring consumes linked evidence and preserves provenance for explainability. - -## Core workflow - -1. **Profile authoring:** Policy Studio exposes declarative DSL to define factors, weights, thresholds, and severity buckets. -2. **Simulation:** operators preview profiles against historical findings/SBOMs, compare with existing policies, and inspect factor breakdowns. -3. **Activation:** Policy Engine evaluates profiles on change streams, producing scores and detailed factor contributions per finding and per asset. -4. **Explainability:** CLI/Console display math traces, provenance IDs, and rationale for each factor. Export Center packages reports for auditors. -5. **Versioning:** profiles carry semantic versions, promotion workflows, and rollback hooks; Authority scopes enforce who can publish or edit. - -## Factor model - -| Factor | Description | Typical signal source | -| --- | --- | --- | -| Exploit likelihood | EPSS/KEV or internal intel | Conseiller enrichment | -| VEX status | not_affected / affected / fixed | Excitator (VEX Lens) | -| Reachability | entrypoint closure, runtime observations | Scanner + Zastava | -| Fix availability | patch released, vendor guidance | Conseiller, Policy Engine | -| Asset criticality | business context, tenant overrides | Policy Studio inputs | -| Provenance trust | signed evidence, attestation status | Attestor, Authority | - -Factors feed into a weighted scoring engine with per-factor contribution reporting. - -## Governance & guardrails - -- Profiles live in Policy Studio with draft/review/approval workflows. -- Policy Engine enforces deterministic evaluation; simulations and production runs share the same scoring code. -- CLI parity enables automated promotion, export/import, and simulation from pipelines. -- Observability records scoring latency, factor distribution, and profile usage. -- Offline support: profiles, factor plugins, and explain bundles ship inside mirror bundles for air-gapped environments. - -## Deliverables - -- Policy language reference and examples. -- Simulation APIs/CLI with diff output. -- Scoring engine implementation with explain traces and determinism checks. -- Console visualizations (severity heatmaps, contribution waterfalls). -- Export Center reports with risk scoring sections. -- Observability dashboards for profile health and scoring throughput. - -## References - -- Policy core: `docs/modules/policy/architecture.md` -- Findings ledger: `docs/modules/vuln-explorer/architecture.md` -- VEX consensus: `docs/modules/vex-lens/architecture.md` -- Offline operations: `docs/airgap/airgap-mode.md` +# Risk Scoring Profiles + +> Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. + +## Overview + +Risk Scoring Profiles define customizable formulas that convert raw evidence (CVSS, EPSS-like exploit likelihood, KEV exploited lists, VEX status, reachability, runtime evidence, fix availability, asset criticality, provenance trust) into normalized risk scores (0–100) with severity buckets. Profiles are authored in Policy Studio, simulated, versioned, and executed by the scoring engine with full explainability. + +- **Primary components:** Policy Engine, Findings Ledger, Conseiller, Excitor, Console, Policy Studio, CLI, Export Center, Authority & Tenancy, Observability. +- **Surfaces:** policy documents, scoring engine, factor providers, explainability artefacts, APIs, CLI, UI. + +Aggregation-Only Contract remains in force: Conseiller and Excitor never merge or mutate source records. Risk scoring consumes linked evidence and preserves provenance for explainability. + +## Core workflow + +1. **Profile authoring:** Policy Studio exposes declarative DSL to define factors, weights, thresholds, and severity buckets. +2. **Simulation:** operators preview profiles against historical findings/SBOMs, compare with existing policies, and inspect factor breakdowns. +3. **Activation:** Policy Engine evaluates profiles on change streams, producing scores and detailed factor contributions per finding and per asset. +4. **Explainability:** CLI/Console display math traces, provenance IDs, and rationale for each factor. Export Center packages reports for auditors. +5. **Versioning:** profiles carry semantic versions, promotion workflows, and rollback hooks; Authority scopes enforce who can publish or edit. + +## Factor model + +| Factor | Description | Typical signal source | +| --- | --- | --- | +| Exploit likelihood | EPSS/KEV or internal intel | Conseiller enrichment | +| VEX status | not_affected / affected / fixed | Excitor (VEX Lens) | +| Reachability | entrypoint closure, runtime observations | Scanner + Zastava | +| Fix availability | patch released, vendor guidance | Conseiller, Policy Engine | +| Asset criticality | business context, tenant overrides | Policy Studio inputs | +| Provenance trust | signed evidence, attestation status | Attestor, Authority | + +Factors feed into a weighted scoring engine with per-factor contribution reporting. + +## Governance & guardrails + +- Profiles live in Policy Studio with draft/review/approval workflows. +- Policy Engine enforces deterministic evaluation; simulations and production runs share the same scoring code. +- CLI parity enables automated promotion, export/import, and simulation from pipelines. +- Observability records scoring latency, factor distribution, and profile usage. +- Offline support: profiles, factor plugins, and explain bundles ship inside mirror bundles for air-gapped environments. + +## Deliverables + +- Policy language reference and examples. +- Simulation APIs/CLI with diff output. +- Scoring engine implementation with explain traces and determinism checks. +- Console visualizations (severity heatmaps, contribution waterfalls). +- Export Center reports with risk scoring sections. +- Observability dashboards for profile health and scoring throughput. + +## References + +- Policy core: `docs/modules/policy/architecture.md` +- Findings ledger: `docs/modules/vuln-explorer/architecture.md` +- VEX consensus: `docs/modules/vex-lens/architecture.md` +- Offline operations: `docs/airgap/airgap-mode.md` diff --git a/docs/security/authority-scopes.md b/docs/security/authority-scopes.md index 5aa81712..58e51897 100644 --- a/docs/security/authority-scopes.md +++ b/docs/security/authority-scopes.md @@ -49,7 +49,11 @@ Authority issues short-lived tokens bound to tenants and scopes. Sprint 19 int | `policy:promote` | Policy Studio / CLI attestation flows | Promote policy attestations between environments (e.g., staging → prod). | Interactive only; tenant required; requires `policy_reason`, `policy_ticket`, digest, and fresh-auth within 5 minutes. | | `policy:audit` | Policy audit exports | Access immutable policy history, comments, and signatures. | Tenant required; read-only access. | | `policy:simulate` | Policy Studio / CLI simulations | Run simulations against tenant inventories. | Tenant required; available to authors, reviewers, operators. | -| `vuln:read` | Vuln Explorer API/UI | Read normalized vulnerability data. | Tenant required. | +| `vuln:view` | Vuln Explorer API/UI | Read normalized vulnerability data, issue permalinks. | Tenant required; ABAC attributes (`env`, `owner`, `business_tier`) further constrain access. | +| `vuln:investigate` | Vuln Explorer triage workflows | Assign findings, add comments, attach remediation notes. | Tenant + ABAC attributes required; typically paired with `vuln:view`. | +| `vuln:operate` | Vuln Explorer state transitions | Change remediation state, accept risk, trigger remediation plans. | Tenant + ABAC attributes required; interactive flows should enforce fresh-auth on prod tenants. | +| `vuln:audit` | Vuln Explorer audit/report exports | Access immutable ledgers, reports, and offline bundles. | Tenant required; ABAC attributes restrict which assets may be exported. | +> **Legacy:** `vuln:read` remains available for backwards compatibility and is still emitted on Vuln Explorer permalinks. New clients should request the granular scopes above. | `export.viewer` | Export Center APIs | List export profiles/runs, fetch manifests and bundles. | Tenant required; read-only access. | | `export.operator` | Export Center APIs | Trigger export runs, manage schedules, request verifications. | Tenant required; pair with `export.admin` for retention/encryption changes. | | `export.admin` | Export Center administrative APIs | Configure retention policies, encryption keys, and scheduling defaults. | Tenant required; token requests must include `export_reason` + `export_ticket`; Authority audits denials. | @@ -80,7 +84,7 @@ Authority issues short-lived tokens bound to tenants and scopes. Sprint 19 int - **`role/policy-engine`** → `effective:write`, `findings:read`. - **`role/cartographer-service`** → `graph:write`, `graph:read`. - **`role/graph-gateway`** → `graph:read`, `graph:export`, `graph:simulate`. -- **`role/console`** → `ui.read`, `advisory:read`, `vex:read`, `exceptions:read`, `aoc:verify`, `findings:read`, `airgap:status:read`, `orch:read`, `vuln:read`. +- **`role/console`** → `ui.read`, `advisory:read`, `vex:read`, `exceptions:read`, `aoc:verify`, `findings:read`, `airgap:status:read`, `orch:read`, `vuln:view`, `vuln:investigate`. - **`role/ui-console-admin`** → `ui.read`, `authority:tenants.read`, `authority:roles.read`, `authority:tokens.read`, `authority:clients.read` (paired with write scopes where required). - **`role/orch-viewer`** *(Authority role: `Orch.Viewer`)* → `orch:read`. - **`role/orch-operator`** *(Authority role: `Orch.Operator`)* → `orch:read`, `orch:operate`. @@ -242,8 +246,14 @@ security: description: Export graph artefacts - name: graph:simulate description: Run graph what-if simulations - - name: vuln:read - description: Read Vuln Explorer data + - name: vuln:view + description: Read Vuln Explorer data, list findings, issue permalinks + - name: vuln:investigate + description: Perform triage actions (assign, comment, remediation notes) + - name: vuln:operate + description: Execute state changes and remediation workflows + - name: vuln:audit + description: Access Vuln Explorer audit ledgers and offline exports claimTransforms: - match: { scope: "effective:write" } require: @@ -262,7 +272,7 @@ Update service clients: - `Policy.Engine` → request `effective:write`, `findings:read`; set `properties.serviceIdentity=policy-engine`. - `Cartographer.Service` → request `graph:write`, `graph:read`; set `properties.serviceIdentity=cartographer`. - `Graph API Gateway` → request `graph:read`, `graph:export`, `graph:simulate`; tenant hint required. -- `Console` → request `advisory:read`, `vex:read`, `aoc:verify`, `findings:read`, `vuln:read` plus existing UI scopes. +- `Console` → request `advisory:read`, `vex:read`, `aoc:verify`, `findings:read`, `vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit` plus existing UI scopes. - `CLI automation` → request `aoc:verify`, `advisory:read`, `vex:read` as needed. Client definition snippet: diff --git a/docs/technical/architecture/README.md b/docs/technical/architecture/README.md index f8241668..2234f760 100644 --- a/docs/technical/architecture/README.md +++ b/docs/technical/architecture/README.md @@ -38,7 +38,7 @@ Each module directory bundles an ownership charter (`AGENTS.md`), current work ( | UI / Console | [architecture.md](../../modules/ui/architecture.md), [console-architecture.md](../../modules/ui/console-architecture.md) | [implementation_plan.md](../../modules/ui/implementation_plan.md) | — | | Vuln Explorer | [architecture.md](../../modules/vuln-explorer/architecture.md) | [implementation_plan.md](../../modules/vuln-explorer/implementation_plan.md) | — | | VEX Lens | [architecture.md](../../modules/vex-lens/architecture.md) | [implementation_plan.md](../../modules/vex-lens/implementation_plan.md) | — | -| Vexer | [architecture.md](../../modules/vexer/architecture.md) | [implementation_plan.md](../../modules/vexer/implementation_plan.md) | [scoring.md](../../modules/vexer/scoring.md) | +| Excitor | [architecture.md](../../modules/excitor/architecture.md) | [implementation_plan.md](../../modules/excitor/implementation_plan.md) | [scoring.md](../../modules/excitor/scoring.md) | | Zastava | [architecture.md](../../modules/zastava/architecture.md) | [implementation_plan.md](../../modules/zastava/implementation_plan.md) | — | > **Tip:** Every module directory also exposes `README.md`, `AGENTS.md`, and `TASKS.md` for roles, current backlog, and ownership responsibilities. diff --git a/docs/technical/development/README.md b/docs/technical/development/README.md index 18fd94cf..412742f2 100644 --- a/docs/technical/development/README.md +++ b/docs/technical/development/README.md @@ -13,7 +13,7 @@ Resources for contributors building features, plug-ins, connectors, and tests. - [../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. - Developer guides under [../dev/](../../dev/): - - Connector playbooks (`30_EXCITITOR_CONNECTOR_GUIDE.md`, `30_VEXER_CONNECTOR_GUIDE.md`, `concelier-connector-research-20251011.md`, `kisa_connector_notes.md`). + - Connector playbooks (`30_EXCITITOR_CONNECTOR_GUIDE.md`, `30_EXCITOR_CONNECTOR_GUIDE.md`, `concelier-connector-research-20251011.md`, `kisa_connector_notes.md`). - Authority and DPoP guidance (`31_AUTHORITY_PLUGIN_DEVELOPER_GUIDE.md`, `authority-dpop-mtls-plan.md`, `authority-plugin-di-coordination.md`, `authority-rate-limit-tuning-outline.md`, `32_AUTH_CLIENT_GUIDE.md`). - Analyzer and cache configuration (`SCANNER_CACHE_CONFIGURATION.md`, `java-analyzer-observation-plan.md`, `EXCITITOR_STATEMENT_BACKFILL.md`). - Normalisation & merge references (`aoc-normalization-removal-notes.md`, `merge_semver_playbook.md`, `normalized-rule-recipes.md`, `normalized_versions_rollout.md`). diff --git a/docs/ui/runs.md b/docs/ui/runs.md index 00d9a594..98cea231 100644 --- a/docs/ui/runs.md +++ b/docs/ui/runs.md @@ -43,7 +43,7 @@ The header integrates the status ticker to show ingestion deltas and planner hea | Column | Description | |--------|-------------| | **Run ID** | Deterministic identifier (`run:::`). Clicking opens detail drawer. | -| **Trigger** | `cron`, `manual`, `feedser`, `vexer`, `policy`, `content-refresh`. Tooltip lists schedule and initiator. | +| **Trigger** | `cron`, `manual`, `conselier`, `excitor`, `policy`, `content-refresh`. Tooltip lists schedule and initiator. | | **State** | Badges: `planning`, `queued`, `running`, `completed`, `cancelled`, `error`. Errors include error code (e.g., `ERR_RUN_005`). | | **Progress** | Percentage + processed/total candidates. SSE updates increment in real time. | | **Duration** | Elapsed time (auto-updating). Completed runs show total duration; running runs show timer. | diff --git a/etc/authority.yaml.sample b/etc/authority.yaml.sample index b490f26f..d1926d41 100644 --- a/etc/authority.yaml.sample +++ b/etc/authority.yaml.sample @@ -80,6 +80,10 @@ delegation: - "findings:read" authorizedClients: - "export-center-worker" + attributes: + env: [ "prod", "stage" ] + owner: [ "secops" ] + business_tier: [ "tier-1" ] # - accountId: "svc-airgap-import" # tenant: "tenant-default" # displayName: "Airgap Import Service Account" @@ -242,7 +246,7 @@ clients: displayName: "StellaOps Console" grantTypes: [ "authorization_code", "refresh_token" ] audiences: [ "console" ] - scopes: [ "openid", "profile", "email", "ui.read", "authority:tenants.read", "advisory:read", "vex:read", "exceptions:read", "exceptions:approve", "aoc:verify", "findings:read", "airgap:status:read", "obs:read", "obs:incident", "timeline:read", "evidence:read", "attest:read", "orch:read", "vuln:read" ] + scopes: [ "openid", "profile", "email", "ui.read", "authority:tenants.read", "advisory:read", "vex:read", "exceptions:read", "exceptions:approve", "aoc:verify", "findings:read", "airgap:status:read", "obs:read", "obs:incident", "timeline:read", "evidence:read", "attest:read", "orch:read", "vuln:view", "vuln:investigate", "vuln:operate", "vuln:audit" ] # exceptions:approve is elevated via fresh-auth and requires an MFA-capable identity provider. tenant: "tenant-default" senderConstraint: "dpop" @@ -391,7 +395,7 @@ clients: displayName: "Vuln Explorer UI" grantTypes: [ "client_credentials" ] audiences: [ "api://vuln-explorer" ] - scopes: [ "vuln:read" ] + scopes: [ "vuln:view", "vuln:investigate", "vuln:operate", "vuln:audit" ] tenant: "tenant-default" senderConstraint: "dpop" auth: @@ -473,6 +477,30 @@ tenants: scopes: [ "advisory-ai:view", "advisory-ai:operate" ] advisory-ai-admin: scopes: [ "advisory-ai:view", "advisory-ai:operate", "advisory-ai:admin" ] + vuln-viewer: + scopes: [ "vuln:view" ] + attributes: + env: [ "*" ] + owner: [ "*" ] + business_tier: [ "*" ] + vuln-investigator: + scopes: [ "vuln:view", "vuln:investigate" ] + attributes: + env: [ "*" ] + owner: [ "*" ] + business_tier: [ "*" ] + vuln-operator: + scopes: [ "vuln:view", "vuln:investigate", "vuln:operate" ] + attributes: + env: [ "*" ] + owner: [ "*" ] + business_tier: [ "*" ] + vuln-auditor: + scopes: [ "vuln:view", "vuln:audit" ] + attributes: + env: [ "*" ] + owner: [ "*" ] + business_tier: [ "*" ] advisoryAi: remoteInference: consentGranted: false diff --git a/etc/issuer-directory.yaml.sample b/etc/issuer-directory.yaml.sample index 958be473..fddcb1e2 100644 --- a/etc/issuer-directory.yaml.sample +++ b/etc/issuer-directory.yaml.sample @@ -1,4 +1,6 @@ IssuerDirectory: + # Override connection secrets via environment variables (ISSUERDIRECTORY__MONGO__*) + # rather than editing this file for production. telemetry: minimumLogLevel: Information authority: diff --git a/ops/devops/TASKS.md b/ops/devops/TASKS.md index 0fc0d89e..d8c9080a 100644 --- a/ops/devops/TASKS.md +++ b/ops/devops/TASKS.md @@ -76,6 +76,8 @@ | DEVOPS-LNM-22-002 | BLOCKED (2025-10-27) | DevOps Guild, Excititor Guild | EXCITITOR-LNM-21-102 | Execute VEX observation/linkset backfill with monitoring; ensure NATS/Redis events integrated; document ops runbook. Blocked until Excititor storage migration lands. | | DEVOPS-LNM-22-003 | TODO | DevOps Guild, Observability Guild | CONCELIER-LNM-21-005, EXCITITOR-LNM-21-005 | Add CI/monitoring coverage for new metrics (`advisory_observations_total`, `linksets_total`, etc.) and alerts on ingest-to-API SLA breaches. | Metrics scraped into Grafana; alert thresholds set; CI job verifies metric emission. | +> 2025-11-03: Link-Not-Merge migration playbook (`docs/migration/no-merge.md`) published—use it to sequence DEVOPS-LNM-22-001 rehearsals and record Phase 0–3 config toggles in runbooks. + ## Graph & Vuln Explorer v1 | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | diff --git a/ops/devops/release/docker/Dockerfile.angular-ui b/ops/devops/release/docker/Dockerfile.angular-ui index 11c1523f..0a2b0abf 100644 --- a/ops/devops/release/docker/Dockerfile.angular-ui +++ b/ops/devops/release/docker/Dockerfile.angular-ui @@ -26,6 +26,6 @@ COPY --from=build /workspace/dist/stellaops-web/ /usr/share/nginx/html/ COPY ops/devops/release/docker/nginx-default.conf /etc/nginx/conf.d/default.conf LABEL org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.revision="${GIT_SHA}" \ - org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/feedser" \ + org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/conselier" \ org.stellaops.release.channel="${CHANNEL}" EXPOSE 8080 diff --git a/ops/devops/release/docker/Dockerfile.dotnet-service b/ops/devops/release/docker/Dockerfile.dotnet-service index 9587fb89..16b6255c 100644 --- a/ops/devops/release/docker/Dockerfile.dotnet-service +++ b/ops/devops/release/docker/Dockerfile.dotnet-service @@ -1,52 +1,52 @@ -# syntax=docker/dockerfile:1.7-labs - -ARG SDK_IMAGE=mcr.microsoft.com/dotnet/nightly/sdk:10.0 -ARG RUNTIME_IMAGE=gcr.io/distroless/dotnet/aspnet:latest - -ARG PROJECT -ARG ENTRYPOINT_DLL -ARG VERSION=0.0.0 -ARG CHANNEL=dev -ARG GIT_SHA=0000000 -ARG SOURCE_DATE_EPOCH=0 - -FROM ${SDK_IMAGE} AS build -ARG PROJECT -ARG GIT_SHA -ARG SOURCE_DATE_EPOCH -WORKDIR /src -ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \ - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 \ - NUGET_XMLDOC_MODE=skip \ - SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} -COPY . . -RUN --mount=type=cache,target=/root/.nuget/packages \ - dotnet restore "${PROJECT}" -RUN --mount=type=cache,target=/root/.nuget/packages \ - dotnet publish "${PROJECT}" \ - -c Release \ - -o /app/publish \ - /p:UseAppHost=false \ - /p:ContinuousIntegrationBuild=true \ - /p:SourceRevisionId=${GIT_SHA} \ - /p:Deterministic=true \ - /p:TreatWarningsAsErrors=true - -FROM ${RUNTIME_IMAGE} AS runtime -WORKDIR /app -ARG ENTRYPOINT_DLL -ARG VERSION -ARG CHANNEL -ARG GIT_SHA -ENV DOTNET_EnableDiagnostics=0 \ - ASPNETCORE_URLS=http://0.0.0.0:8080 -COPY --from=build /app/publish/ ./ -RUN set -eu; \ - printf '#!/usr/bin/env sh\nset -e\nexec dotnet %s "$@"\n' "${ENTRYPOINT_DLL}" > /entrypoint.sh; \ - chmod +x /entrypoint.sh -EXPOSE 8080 -LABEL org.opencontainers.image.version="${VERSION}" \ - org.opencontainers.image.revision="${GIT_SHA}" \ - org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/feedser" \ - org.stellaops.release.channel="${CHANNEL}" -ENTRYPOINT ["/entrypoint.sh"] +# syntax=docker/dockerfile:1.7-labs + +ARG SDK_IMAGE=mcr.microsoft.com/dotnet/nightly/sdk:10.0 +ARG RUNTIME_IMAGE=gcr.io/distroless/dotnet/aspnet:latest + +ARG PROJECT +ARG ENTRYPOINT_DLL +ARG VERSION=0.0.0 +ARG CHANNEL=dev +ARG GIT_SHA=0000000 +ARG SOURCE_DATE_EPOCH=0 + +FROM ${SDK_IMAGE} AS build +ARG PROJECT +ARG GIT_SHA +ARG SOURCE_DATE_EPOCH +WORKDIR /src +ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 \ + NUGET_XMLDOC_MODE=skip \ + SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} +COPY . . +RUN --mount=type=cache,target=/root/.nuget/packages \ + dotnet restore "${PROJECT}" +RUN --mount=type=cache,target=/root/.nuget/packages \ + dotnet publish "${PROJECT}" \ + -c Release \ + -o /app/publish \ + /p:UseAppHost=false \ + /p:ContinuousIntegrationBuild=true \ + /p:SourceRevisionId=${GIT_SHA} \ + /p:Deterministic=true \ + /p:TreatWarningsAsErrors=true + +FROM ${RUNTIME_IMAGE} AS runtime +WORKDIR /app +ARG ENTRYPOINT_DLL +ARG VERSION +ARG CHANNEL +ARG GIT_SHA +ENV DOTNET_EnableDiagnostics=0 \ + ASPNETCORE_URLS=http://0.0.0.0:8080 +COPY --from=build /app/publish/ ./ +RUN set -eu; \ + printf '#!/usr/bin/env sh\nset -e\nexec dotnet %s "$@"\n' "${ENTRYPOINT_DLL}" > /entrypoint.sh; \ + chmod +x /entrypoint.sh +EXPOSE 8080 +LABEL org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${GIT_SHA}" \ + org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/conselier" \ + org.stellaops.release.channel="${CHANNEL}" +ENTRYPOINT ["/entrypoint.sh"] diff --git a/samples/api/scheduler/run-summary.json b/samples/api/scheduler/run-summary.json index b1942ec0..c8b6ffc0 100644 --- a/samples/api/scheduler/run-summary.json +++ b/samples/api/scheduler/run-summary.json @@ -1,101 +1,101 @@ -{ - "tenantId": "tenant-alpha", - "scheduleId": "sch_20251018a", - "updatedAt": "2025-10-18T22:10:10Z", - "lastRun": { - "runId": "run_20251018_0001", - "trigger": "feedser", - "state": "completed", - "createdAt": "2025-10-18T22:03:14Z", - "startedAt": "2025-10-18T22:03:20Z", - "finishedAt": "2025-10-18T22:08:45Z", - "stats": { - "candidates": 1280, - "deduped": 910, - "queued": 0, - "completed": 910, - "deltas": 42, - "newCriticals": 7, - "newHigh": 11, - "newMedium": 18, - "newLow": 6 - }, - "error": null - }, - "recent": [ - { - "runId": "run_20251018_0001", - "trigger": "feedser", - "state": "completed", - "createdAt": "2025-10-18T22:03:14Z", - "startedAt": "2025-10-18T22:03:20Z", - "finishedAt": "2025-10-18T22:08:45Z", - "stats": { - "candidates": 1280, - "deduped": 910, - "queued": 0, - "completed": 910, - "deltas": 42, - "newCriticals": 7, - "newHigh": 11, - "newMedium": 18, - "newLow": 6 - }, - "error": null - }, - { - "runId": "run_20251017_0003", - "trigger": "cron", - "state": "error", - "createdAt": "2025-10-17T22:01:02Z", - "startedAt": "2025-10-17T22:01:08Z", - "finishedAt": "2025-10-17T22:04:11Z", - "stats": { - "candidates": 1040, - "deduped": 812, - "queued": 0, - "completed": 640, - "deltas": 18, - "newCriticals": 2, - "newHigh": 4, - "newMedium": 7, - "newLow": 3 - }, - "error": "scanner timeout" - }, - { - "runId": "run_20251016_0007", - "trigger": "manual", - "state": "cancelled", - "createdAt": "2025-10-16T20:00:00Z", - "startedAt": "2025-10-16T20:00:04Z", - "finishedAt": null, - "stats": { - "candidates": 820, - "deduped": 640, - "queued": 0, - "completed": 0, - "deltas": 0, - "newCriticals": 0, - "newHigh": 0, - "newMedium": 0, - "newLow": 0 - }, - "error": null - } - ], - "counters": { - "total": 3, - "planning": 0, - "queued": 0, - "running": 0, - "completed": 1, - "error": 1, - "cancelled": 1, - "totalDeltas": 60, - "totalNewCriticals": 9, - "totalNewHigh": 15, - "totalNewMedium": 25, - "totalNewLow": 9 - } -} +{ + "tenantId": "tenant-alpha", + "scheduleId": "sch_20251018a", + "updatedAt": "2025-10-18T22:10:10Z", + "lastRun": { + "runId": "run_20251018_0001", + "trigger": "conselier", + "state": "completed", + "createdAt": "2025-10-18T22:03:14Z", + "startedAt": "2025-10-18T22:03:20Z", + "finishedAt": "2025-10-18T22:08:45Z", + "stats": { + "candidates": 1280, + "deduped": 910, + "queued": 0, + "completed": 910, + "deltas": 42, + "newCriticals": 7, + "newHigh": 11, + "newMedium": 18, + "newLow": 6 + }, + "error": null + }, + "recent": [ + { + "runId": "run_20251018_0001", + "trigger": "conselier", + "state": "completed", + "createdAt": "2025-10-18T22:03:14Z", + "startedAt": "2025-10-18T22:03:20Z", + "finishedAt": "2025-10-18T22:08:45Z", + "stats": { + "candidates": 1280, + "deduped": 910, + "queued": 0, + "completed": 910, + "deltas": 42, + "newCriticals": 7, + "newHigh": 11, + "newMedium": 18, + "newLow": 6 + }, + "error": null + }, + { + "runId": "run_20251017_0003", + "trigger": "cron", + "state": "error", + "createdAt": "2025-10-17T22:01:02Z", + "startedAt": "2025-10-17T22:01:08Z", + "finishedAt": "2025-10-17T22:04:11Z", + "stats": { + "candidates": 1040, + "deduped": 812, + "queued": 0, + "completed": 640, + "deltas": 18, + "newCriticals": 2, + "newHigh": 4, + "newMedium": 7, + "newLow": 3 + }, + "error": "scanner timeout" + }, + { + "runId": "run_20251016_0007", + "trigger": "manual", + "state": "cancelled", + "createdAt": "2025-10-16T20:00:00Z", + "startedAt": "2025-10-16T20:00:04Z", + "finishedAt": null, + "stats": { + "candidates": 820, + "deduped": 640, + "queued": 0, + "completed": 0, + "deltas": 0, + "newCriticals": 0, + "newHigh": 0, + "newMedium": 0, + "newLow": 0 + }, + "error": null + } + ], + "counters": { + "total": 3, + "planning": 0, + "queued": 0, + "running": 0, + "completed": 1, + "error": 1, + "cancelled": 1, + "totalDeltas": 60, + "totalNewCriticals": 9, + "totalNewHigh": 15, + "totalNewMedium": 25, + "totalNewLow": 9 + } +} diff --git a/samples/api/scheduler/run.json b/samples/api/scheduler/run.json index 9f4a997e..fcae526c 100644 --- a/samples/api/scheduler/run.json +++ b/samples/api/scheduler/run.json @@ -1,50 +1,50 @@ -{ - "schemaVersion": "scheduler.run@1", - "id": "run_20251018_0001", - "tenantId": "tenant-alpha", - "scheduleId": "sch_20251018a", - "trigger": "feedser", - "state": "running", - "stats": { - "candidates": 1280, - "deduped": 910, - "queued": 624, - "completed": 310, - "deltas": 42, - "newCriticals": 7, - "newHigh": 11, - "newMedium": 18, - "newLow": 6 - }, - "reason": { - "feedserExportId": "exp-20251018-03" - }, - "createdAt": "2025-10-18T22:03:14+00:00", - "startedAt": "2025-10-18T22:03:20+00:00", - "deltas": [ - { - "imageDigest": "sha256:a1b2c3", - "newFindings": 3, - "newCriticals": 1, - "newHigh": 1, - "newMedium": 1, - "newLow": 0, - "kevHits": [ - "CVE-2025-0002" - ], - "topFindings": [ - { - "purl": "pkg:rpm/openssl@3.0.12-5.el9", - "vulnerabilityId": "CVE-2025-0002", - "severity": "critical", - "link": "https://ui.internal/scans/sha256:a1b2c3" - } - ], - "attestation": { - "uuid": "rekor-314", - "verified": true - }, - "detectedAt": "2025-10-18T22:03:21+00:00" - } - ] -} +{ + "schemaVersion": "scheduler.run@1", + "id": "run_20251018_0001", + "tenantId": "tenant-alpha", + "scheduleId": "sch_20251018a", + "trigger": "conselier", + "state": "running", + "stats": { + "candidates": 1280, + "deduped": 910, + "queued": 624, + "completed": 310, + "deltas": 42, + "newCriticals": 7, + "newHigh": 11, + "newMedium": 18, + "newLow": 6 + }, + "reason": { + "conselierExportId": "exp-20251018-03" + }, + "createdAt": "2025-10-18T22:03:14+00:00", + "startedAt": "2025-10-18T22:03:20+00:00", + "deltas": [ + { + "imageDigest": "sha256:a1b2c3", + "newFindings": 3, + "newCriticals": 1, + "newHigh": 1, + "newMedium": 1, + "newLow": 0, + "kevHits": [ + "CVE-2025-0002" + ], + "topFindings": [ + { + "purl": "pkg:rpm/openssl@3.0.12-5.el9", + "vulnerabilityId": "CVE-2025-0002", + "severity": "critical", + "link": "https://ui.internal/scans/sha256:a1b2c3" + } + ], + "attestation": { + "uuid": "rekor-314", + "verified": true + }, + "detectedAt": "2025-10-18T22:03:21+00:00" + } + ] +} diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI/AGENTS.md b/src/AdvisoryAI/StellaOps.AdvisoryAI/AGENTS.md index a5932b88..946b5731 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI/AGENTS.md +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI/AGENTS.md @@ -6,7 +6,7 @@ Deliver the Advisory AI assistant service that synthesizes advisory/VEX evidence ## Scope - Service under `src/AdvisoryAI/StellaOps.AdvisoryAI` (retrievers, deterministics, orchestrator, guardrails, inference adapters, REST APIs). - Batch processing for CLI/automation, caching, observability, and integration with Console, CLI, and downstream systems. -- Coordination across Conseiller, Excitator, VEX Lens, SBOM Service, Policy Engine, Findings Ledger, Web Gateway, Authority, DevOps, and Docs. +- Coordination across Conseiller, Excitor, VEX Lens, SBOM Service, Policy Engine, Findings Ledger, Web Gateway, Authority, DevOps, and Docs. ## Principles 1. **Evidence preservation** – Raw advisory/VEX documents remain untouched; AI outputs reference them with citations. diff --git a/src/AirGap/StellaOps.AirGap.Importer/AGENTS.md b/src/AirGap/StellaOps.AirGap.Importer/AGENTS.md index d3e20a95..3036a895 100644 --- a/src/AirGap/StellaOps.AirGap.Importer/AGENTS.md +++ b/src/AirGap/StellaOps.AirGap.Importer/AGENTS.md @@ -7,7 +7,7 @@ Deliver offline bundle verification and ingestion tooling for sealed environment - TUF metadata verification, DSSE signature checks, Merkle root validation. - Import pipelines writing bundle catalogs, object-store layouts, and audit entries. - CLI + API surfaces for dry-run verification, import, and status queries. -- Integration hooks for Conseiller, Excitator, Policy Engine, and Export Center. +- Integration hooks for Conseiller, Excitor, Policy Engine, and Export Center. - Negative-case handling (tampering, expired signatures, root rotation) with operator guidance. ## Definition of Done diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/HttpClientUsageAnalyzerTests.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/HttpClientUsageAnalyzerTests.cs new file mode 100644 index 00000000..40ac25fe --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/HttpClientUsageAnalyzerTests.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace StellaOps.AirGap.Policy.Analyzers.Tests; + +public sealed class HttpClientUsageAnalyzerTests +{ + [Fact] + public async Task ReportsDiagnostic_ForNewHttpClient() + { + const string source = """ + using System.Net.Http; + + namespace Sample.App; + + public sealed class Demo + { + public void Run() + { + var client = new HttpClient(); + } + } + """; + + var diagnostics = await AnalyzeAsync(source, assemblyName: "Sample.App"); + Assert.Contains(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId); + } + + [Fact] + public async Task DoesNotReportDiagnostic_InsidePolicyAssembly() + { + const string source = """ + using System.Net.Http; + + namespace StellaOps.AirGap.Policy.Internal; + + internal static class Loopback + { + public static HttpClient Create() => new HttpClient(); + } + """; + + var diagnostics = await AnalyzeAsync(source, assemblyName: "StellaOps.AirGap.Policy"); + Assert.DoesNotContain(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId); + } + + [Fact] + public async Task CodeFix_RewritesToFactoryCall() + { + const string source = """ + using System.Net.Http; + + namespace Sample.Service; + + public sealed class Demo + { + public void Run() + { + var client = new HttpClient(); + } + } + """; + + const string expected = """ + using System.Net.Http; + + namespace Sample.Service; + + public sealed class Demo + { + public void Run() + { + var client = global::StellaOps.AirGap.Policy.EgressHttpClientFactory.Create(egressPolicy: /* TODO: provide IEgressPolicy instance */, request: new global::StellaOps.AirGap.Policy.EgressRequest(component: "REPLACE_COMPONENT", destination: new global::System.Uri("https://replace-with-endpoint"), intent: "REPLACE_INTENT")); + } + } + """; + + var updated = await ApplyCodeFixAsync(source, assemblyName: "Sample.Service"); + Assert.Equal(expected.ReplaceLineEndings(), updated.ReplaceLineEndings()); + } + + private static async Task> AnalyzeAsync(string source, string assemblyName) + { + var compilation = CSharpCompilation.Create( + assemblyName, + new[] + { + CSharpSyntaxTree.ParseText(source), + CSharpSyntaxTree.ParseText(PolicyStubSource), + }, + CreateMetadataReferences(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var analyzer = new HttpClientUsageAnalyzer(); + var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); + return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); + } + + private static async Task ApplyCodeFixAsync(string source, string assemblyName) + { + using var workspace = new AdhocWorkspace(); + + var projectId = ProjectId.CreateNewId(); + var documentId = DocumentId.CreateNewId(projectId); + var stubDocumentId = DocumentId.CreateNewId(projectId); + + var solution = workspace.CurrentSolution + .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp) + .WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .WithProjectAssemblyName(projectId, assemblyName) + .AddMetadataReferences(projectId, CreateMetadataReferences()) + .AddDocument(documentId, "Test.cs", SourceText.From(source)) + .AddDocument(stubDocumentId, "PolicyStubs.cs", SourceText.From(PolicyStubSource)); + + var project = solution.GetProject(projectId)!; + var document = solution.GetDocument(documentId)!; + + var compilation = await project.GetCompilationAsync(); + var analyzer = new HttpClientUsageAnalyzer(); + var diagnostics = await compilation!.WithAnalyzers(ImmutableArray.Create(analyzer)) + .GetAnalyzerDiagnosticsAsync(); + + var diagnostic = Assert.Single(diagnostics); + + var codeFixProvider = new HttpClientUsageCodeFixProvider(); + var actions = new List(); + var context = new CodeFixContext( + document, + diagnostic, + (action, _) => actions.Add(action), + CancellationToken.None); + + await codeFixProvider.RegisterCodeFixesAsync(context); + var action = Assert.Single(actions); + var operations = await action.GetOperationsAsync(CancellationToken.None); + + foreach (var operation in operations) + { + operation.Apply(workspace, CancellationToken.None); + } + var updatedDocument = workspace.CurrentSolution.GetDocument(documentId)!; + var updatedText = await updatedDocument.GetTextAsync(); + return updatedText.ToString(); + } + + private static IEnumerable CreateMetadataReferences() + { + yield return MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(Uri).GetTypeInfo().Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(HttpClient).GetTypeInfo().Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location); + } + + private const string PolicyStubSource = """ + namespace StellaOps.AirGap.Policy + { + public interface IEgressPolicy + { + void EnsureAllowed(EgressRequest request); + } + + public readonly record struct EgressRequest(string Component, System.Uri Destination, string Intent); + + public static class EgressHttpClientFactory + { + public static System.Net.Http.HttpClient Create(IEgressPolicy egressPolicy, EgressRequest request) + => throw new System.NotImplementedException(); + } + } + """; +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/StellaOps.AirGap.Policy.Analyzers.Tests.csproj b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/StellaOps.AirGap.Policy.Analyzers.Tests.csproj new file mode 100644 index 00000000..4f7b8b11 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/StellaOps.AirGap.Policy.Analyzers.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/AnalyzerReleases.Unshipped.md b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..c3c3afba --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,5 @@ +### New Rules + +Rule ID | Title | Category | Severity | Notes +--------|-------|----------|----------|------ +AIRGAP001 | Replace raw HttpClient with EgressPolicy-aware client | Usage | Warning | Flags direct HttpClient instantiation outside the EgressPolicy-aware wrappers. diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageAnalyzer.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageAnalyzer.cs new file mode 100644 index 00000000..61d8b2f7 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageAnalyzer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace StellaOps.AirGap.Policy.Analyzers; + +/// +/// Flags direct new HttpClient() usage so services adopt the air-gap aware egress policy wrappers. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class HttpClientUsageAnalyzer : DiagnosticAnalyzer +{ + /// + /// Diagnostic identifier emitted when disallowed HttpClient usage is detected. + /// + public const string DiagnosticId = "AIRGAP001"; + + private const string HttpClientMetadataName = "System.Net.Http.HttpClient"; + private static readonly LocalizableString Title = "Replace raw HttpClient with EgressPolicy-aware client"; + private static readonly LocalizableString MessageFormat = "Instantiate HttpClient via StellaOps.AirGap.Policy wrappers to enforce sealed-mode egress controls"; + private static readonly LocalizableString Description = "Air-gapped environments must route outbound network calls through the EgressPolicy facade so requests are pre-authorised. Replace raw HttpClient usage with the shared factory helpers."; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: Description); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + } + + private static void AnalyzeObjectCreation(OperationAnalysisContext context) + { + if (context.Operation is not IObjectCreationOperation creation) + { + return; + } + + var createdType = creation.Type; + if (createdType is null || !string.Equals(createdType.ToDisplayString(), HttpClientMetadataName, StringComparison.Ordinal)) + { + return; + } + + if (IsWithinAllowedAssembly(context.ContainingSymbol)) + { + return; + } + + var diagnostic = Diagnostic.Create(Rule, creation.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + private static bool IsWithinAllowedAssembly(ISymbol? symbol) + { + var containingAssembly = symbol?.ContainingAssembly; + if (containingAssembly is null) + { + return false; + } + + var assemblyName = containingAssembly.Name; + if (string.IsNullOrEmpty(assemblyName)) + { + return false; + } + + if (string.Equals(assemblyName, "StellaOps.AirGap.Policy", StringComparison.Ordinal)) + { + return true; + } + + if (assemblyName.EndsWith(".Tests", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageCodeFixProvider.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageCodeFixProvider.cs new file mode 100644 index 00000000..9cb38b76 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/HttpClientUsageCodeFixProvider.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StellaOps.AirGap.Policy.Analyzers; + +/// +/// Offers a remediation template that routes HttpClient creation through the shared EgressPolicy factory. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(HttpClientUsageCodeFixProvider))] +[Shared] +public sealed class HttpClientUsageCodeFixProvider : CodeFixProvider +{ + private const string Title = "Use EgressHttpClientFactory.Create(...)"; + + /// + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(HttpClientUsageAnalyzer.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (context.Document is null) + { + return; + } + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var node = root.FindNode(diagnostic.Location.SourceSpan); + if (node is not ObjectCreationExpressionSyntax objectCreation) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + Title, + cancellationToken => ReplaceWithFactoryCallAsync(context.Document, objectCreation, cancellationToken), + equivalenceKey: Title), + diagnostic); + } + + private static async Task ReplaceWithFactoryCallAsync(Document document, ObjectCreationExpressionSyntax creation, CancellationToken cancellationToken) + { + var replacementExpression = SyntaxFactory.ParseExpression( + "global::StellaOps.AirGap.Policy.EgressHttpClientFactory.Create(" + + "egressPolicy: /* TODO: provide IEgressPolicy instance */, " + + "request: new global::StellaOps.AirGap.Policy.EgressRequest(" + + "component: \"REPLACE_COMPONENT\", " + + "destination: new global::System.Uri(\"https://replace-with-endpoint\"), " + + "intent: \"REPLACE_INTENT\"))"); + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + { + return document; + } + + var updatedRoot = root.ReplaceNode(creation, replacementExpression.WithTriviaFrom(creation)); + return document.WithSyntaxRoot(updatedRoot); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/StellaOps.AirGap.Policy.Analyzers.csproj b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/StellaOps.AirGap.Policy.Analyzers.csproj new file mode 100644 index 00000000..a6ac554f --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers/StellaOps.AirGap.Policy.Analyzers.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + enable + enable + true + false + true + latest + + + + + + + + diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.cs new file mode 100644 index 00000000..b60b1dd6 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.AirGap.Policy; +using Xunit; + +namespace StellaOps.AirGap.Policy.Tests; + +public sealed class EgressPolicyTests +{ + [Fact] + public void Evaluate_UnsealedEnvironment_AllowsRequest() + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Unsealed, + }; + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri("https://example.com"), "advisory-sync"); + + var decision = policy.Evaluate(request); + + Assert.True(decision.IsAllowed); + Assert.Null(decision.Reason); + } + + [Fact] + public void EnsureAllowed_SealedEnvironmentWithMatchingRule_Allows() + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + }; + options.AddAllowRule("api.example.com", 443, EgressTransport.Https); + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri("https://api.example.com/v1/status"), "advisory-sync"); + + policy.EnsureAllowed(request); + } + + [Fact] + public void EnsureAllowed_SealedEnvironmentWithoutRule_ThrowsWithGuidance() + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + RemediationDocumentationUrl = "https://docs.stella-ops.org/airgap/egress", + SupportContact = "airgap-oncall@example.org", + }; + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri("https://unauthorized.example.com"), "advisory-sync", operation: "fetch-advisories"); + + var exception = Assert.Throws(() => policy.EnsureAllowed(request)); + + Assert.Contains(AirGapEgressBlockedException.ErrorCode, exception.Message, StringComparison.Ordinal); + Assert.Contains("unauthorized.example.com", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("airgap.egressAllowlist", exception.Remediation, StringComparison.OrdinalIgnoreCase); + Assert.Equal(request, exception.Request); + Assert.Equal(options.RemediationDocumentationUrl, exception.DocumentationUrl); + Assert.Equal(options.SupportContact, exception.SupportContact); + } + + [Fact] + public void EnsureAllowed_SealedEnvironment_AllowsLoopbackWhenConfigured() + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + AllowLoopback = true, + }; + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri("http://127.0.0.1:9000/health"), "local-probe"); + + policy.EnsureAllowed(request); + } + + [Fact] + public void EnsureAllowed_SealedEnvironment_AllowsPrivateNetworkWhenConfigured() + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + AllowPrivateNetworks = true, + }; + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri("https://10.10.0.5:8443/status"), "mirror-sync"); + + policy.EnsureAllowed(request); + } + + [Fact] + public void EnsureAllowed_SealedEnvironment_BlocksPrivateNetworkWhenNotConfigured() + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + AllowPrivateNetworks = false, + }; + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri("https://10.10.0.5:8443/status"), "mirror-sync"); + + var exception = Assert.Throws(() => policy.EnsureAllowed(request)); + Assert.Contains("10.10.0.5", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("https://api.example.com", true)] + [InlineData("https://sub.api.example.com", true)] + [InlineData("https://example.com", false)] + public void Evaluate_SealedEnvironmentWildcardHost_Matches(string url, bool expectedAllowed) + { + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + }; + options.AddAllowRule("*.example.com", transport: EgressTransport.Https); + + var policy = new EgressPolicy(options); + var request = new EgressRequest("PolicyEngine", new Uri(url), "mirror-sync"); + + var decision = policy.Evaluate(request); + Assert.Equal(expectedAllowed, decision.IsAllowed); + } + + [Fact] + public void ServiceCollection_AddAirGapEgressPolicy_RegistersService() + { + var services = new ServiceCollection(); + services.AddAirGapEgressPolicy(options => + { + options.Mode = EgressPolicyMode.Sealed; + options.AddAllowRule("mirror.internal", transport: EgressTransport.Https); + }); + + using var provider = services.BuildServiceProvider(); + var policy = provider.GetRequiredService(); + + Assert.True(policy.IsSealed); + policy.EnsureAllowed(new EgressRequest("PolicyEngine", new Uri("https://mirror.internal"), "mirror-sync")); + } + + [Fact] + public void ServiceCollection_AddAirGapEgressPolicy_BindsFromConfiguration() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AirGap:Egress:Mode"] = "Sealed", + ["AirGap:Egress:AllowLoopback"] = "false", + ["AirGap:Egress:AllowPrivateNetworks"] = "true", + ["AirGap:Egress:RemediationDocumentationUrl"] = "https://docs.example/airgap", + ["AirGap:Egress:SupportContact"] = "airgap@example.org", + ["AirGap:Egress:Allowlist:0:HostPattern"] = "mirror.internal", + ["AirGap:Egress:Allowlist:0:Port"] = "443", + ["AirGap:Egress:Allowlist:0:Transport"] = "https", + ["AirGap:Egress:Allowlist:0:Description"] = "Primary mirror", + }) + .Build(); + + var services = new ServiceCollection(); + services.AddAirGapEgressPolicy(configuration); + + using var provider = services.BuildServiceProvider(); + var policy = provider.GetRequiredService(); + + Assert.True(policy.IsSealed); + var decision = policy.Evaluate(new EgressRequest("ExportCenter", new Uri("https://mirror.internal/feeds"), "mirror-sync")); + Assert.True(decision.IsAllowed); + + var blocked = policy.Evaluate(new EgressRequest("ExportCenter", new Uri("https://external.example"), "mirror-sync")); + Assert.False(blocked.IsAllowed); + Assert.Contains("mirror.internal", blocked.Remediation, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void EgressHttpClientFactory_Create_EnforcesPolicyBeforeReturningClient() + { + var recordingPolicy = new RecordingPolicy(); + var request = new EgressRequest("Component", new Uri("https://allowed.internal"), "mirror-sync"); + + using var client = EgressHttpClientFactory.Create(recordingPolicy, request); + + Assert.True(recordingPolicy.EnsureAllowedCalled); + Assert.NotNull(client); + } + + private sealed class RecordingPolicy : IEgressPolicy + { + public bool EnsureAllowedCalled { get; private set; } + + public bool IsSealed => true; + + public EgressPolicyMode Mode => EgressPolicyMode.Sealed; + + public EgressDecision Evaluate(EgressRequest request) + { + EnsureAllowedCalled = true; + return EgressDecision.Allowed; + } + + public ValueTask EvaluateAsync(EgressRequest request, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(Evaluate(request)); + } + + public void EnsureAllowed(EgressRequest request) + { + EnsureAllowedCalled = true; + } + + public ValueTask EnsureAllowedAsync(EgressRequest request, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + EnsureAllowed(request); + return ValueTask.CompletedTask; + } + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/StellaOps.AirGap.Policy.Tests.csproj b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/StellaOps.AirGap.Policy.Tests.csproj new file mode 100644 index 00000000..7ade5b59 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/StellaOps.AirGap.Policy.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/AirGapEgressBlockedException.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/AirGapEgressBlockedException.cs new file mode 100644 index 00000000..7cf6047e --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/AirGapEgressBlockedException.cs @@ -0,0 +1,100 @@ +using System; +using System.Text; + +namespace StellaOps.AirGap.Policy; + +/// +/// Exception raised when an egress operation is blocked while sealed mode is active. +/// +public sealed class AirGapEgressBlockedException : InvalidOperationException +{ + /// + /// Error code surfaced to callers when egress is blocked. + /// + public const string ErrorCode = "AIRGAP_EGRESS_BLOCKED"; + + /// + /// Initializes a new instance of the class. + /// + /// Request details. + /// Reason returned by the policy. + /// Remediation guidance. + /// Optional documentation URL. + /// Optional support contact. + public AirGapEgressBlockedException( + EgressRequest request, + string reason, + string remediation, + string? documentationUrl, + string? supportContact) + : base(BuildMessage(request, reason, remediation, documentationUrl, supportContact)) + { + Request = request; + Reason = reason; + Remediation = remediation; + DocumentationUrl = documentationUrl; + SupportContact = supportContact; + } + + /// + /// Gets the blocked request. + /// + public EgressRequest Request { get; } + + /// + /// Gets the reason supplied by the policy. + /// + public string Reason { get; } + + /// + /// Gets the remediation guidance. + /// + public string Remediation { get; } + + /// + /// Gets an optional documentation URL. + /// + public string? DocumentationUrl { get; } + + /// + /// Gets an optional support contact (for example, an on-call alias). + /// + public string? SupportContact { get; } + + private static string BuildMessage(EgressRequest request, string reason, string remediation, string? documentationUrl, string? supportContact) + { + var builder = new StringBuilder(); + builder.Append(ErrorCode) + .Append(": component '") + .Append(request.Component) + .Append("' attempted to reach '") + .Append(request.Destination) + .Append("' (intent: ") + .Append(request.Intent); + + if (!string.IsNullOrEmpty(request.Operation)) + { + builder.Append(", operation: ") + .Append(request.Operation); + } + + builder.Append("). Reason: ") + .Append(reason) + .Append(". Remediation: ") + .Append(remediation); + + if (!string.IsNullOrWhiteSpace(documentationUrl)) + { + builder.Append(" Documentation: ") + .Append(documentationUrl); + } + + if (!string.IsNullOrWhiteSpace(supportContact)) + { + builder.Append(" Contact: ") + .Append(supportContact); + } + + return builder.ToString(); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressDecision.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressDecision.cs new file mode 100644 index 00000000..0d6a92e8 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressDecision.cs @@ -0,0 +1,43 @@ +namespace StellaOps.AirGap.Policy; + +/// +/// Represents the outcome of evaluating an egress request. +/// +public sealed record EgressDecision +{ + private EgressDecision(bool isAllowed, string? reason, string? remediation) + { + IsAllowed = isAllowed; + Reason = reason; + Remediation = remediation; + } + + /// + /// Gets a singleton instance representing an allowed decision. + /// + public static EgressDecision Allowed { get; } = new(true, null, null); + + /// + /// Creates a blocked decision for the supplied reason/remediation. + /// + /// Explanation for the denial. + /// Suggested remediation to resolve the denial. + /// An that blocks the request. + public static EgressDecision Blocked(string reason, string remediation) + => new(false, reason, remediation); + + /// + /// Gets a value indicating whether the request is permitted. + /// + public bool IsAllowed { get; } + + /// + /// Gets the reason returned by the policy when the request was blocked. + /// + public string? Reason { get; } + + /// + /// Gets suggested remediation guidance that callers can surface to operators. + /// + public string? Remediation { get; } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs new file mode 100644 index 00000000..de2a2e1e --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs @@ -0,0 +1,45 @@ +using System; +using System.Net.Http; + +namespace StellaOps.AirGap.Policy; + +/// +/// Provides helpers for creating instances that respect the configured . +/// +public static class EgressHttpClientFactory +{ + /// + /// Creates an after validating the supplied egress request against the policy. + /// + /// The policy used to validate outbound requests. + /// Describes the destination and intent for the outbound call. + /// Optional configuration hook applied to the newly created client. + /// An that has been pre-authorised by the policy. + public static HttpClient Create(IEgressPolicy egressPolicy, EgressRequest request, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(egressPolicy); + + egressPolicy.EnsureAllowed(request); + + var client = new HttpClient(); + configure?.Invoke(client); + return client; + } + + /// + /// Creates and configures an after validating the supplied egress request against the policy. + /// + /// The policy used to validate outbound requests. + /// Component initiating the request. + /// Destination that will be contacted. + /// Intent label describing why the request is needed. + /// Optional configuration hook applied to the newly created client. + /// An that has been pre-authorised by the policy. + public static HttpClient Create( + IEgressPolicy egressPolicy, + string component, + Uri destination, + string intent, + Action? configure = null) + => Create(egressPolicy, new EgressRequest(component, destination, intent), configure); +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicy.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicy.cs new file mode 100644 index 00000000..f62066ae --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicy.cs @@ -0,0 +1,191 @@ +using System; +using System.Globalization; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.AirGap.Policy; + +/// +/// Default implementation of . +/// +public sealed class EgressPolicy : IEgressPolicy +{ + private readonly EgressRule[] _rules; + private readonly EgressPolicyOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// Options describing how egress should be enforced. + public EgressPolicy(EgressPolicyOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _rules = options.BuildRuleSet(); + } + + /// + public bool IsSealed => Mode == EgressPolicyMode.Sealed; + + /// + public EgressPolicyMode Mode => _options.Mode; + + /// + public EgressDecision Evaluate(EgressRequest request) + { + if (!IsSealed) + { + return EgressDecision.Allowed; + } + + if (_options.AllowLoopback && IsLoopback(request.Destination)) + { + return EgressDecision.Allowed; + } + + if (_options.AllowPrivateNetworks && IsPrivateNetwork(request.Destination)) + { + return EgressDecision.Allowed; + } + + foreach (var rule in _rules) + { + if (rule.Allows(request)) + { + return EgressDecision.Allowed; + } + } + + var reason = $"Destination '{request.Destination.Host}' is not present in the sealed-mode allow list."; + var remediation = BuildRemediation(request); + return EgressDecision.Blocked(reason, remediation); + } + + /// + public ValueTask EvaluateAsync(EgressRequest request, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return ValueTask.FromResult(Evaluate(request)); + } + + /// + public void EnsureAllowed(EgressRequest request) + { + var decision = Evaluate(request); + if (decision.IsAllowed) + { + return; + } + + throw CreateException(request, decision); + } + + /// + public async ValueTask EnsureAllowedAsync(EgressRequest request, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var decision = await EvaluateAsync(request, cancellationToken).ConfigureAwait(false); + if (!decision.IsAllowed) + { + throw CreateException(request, decision); + } + } + + private AirGapEgressBlockedException CreateException(EgressRequest request, EgressDecision decision) + => new( + request, + decision.Reason ?? "Egress blocked.", + decision.Remediation ?? BuildRemediation(request), + _options.RemediationDocumentationUrl, + _options.SupportContact); + + private string BuildRemediation(EgressRequest request) + { + var host = request.Destination.Host; + var portSegment = request.Destination.IsDefaultPort ? string.Empty : $":{request.Destination.Port.ToString(CultureInfo.InvariantCulture)}"; + var transport = request.Transport.ToString().ToUpperInvariant(); + + var builder = new System.Text.StringBuilder(); + builder.Append("Add '") + .Append(host) + .Append(portSegment) + .Append("' (") + .Append(transport) + .Append(") to the airgap.egressAllowlist configuration."); + + if (_rules.Length == 0) + { + builder.Append(" No allow entries are currently configured; sealed mode blocks every external host."); + } + else + { + builder.Append(" Current allow list sample: "); + var limit = Math.Min(_rules.Length, 3); + for (var i = 0; i < limit; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + builder.Append(_rules[i].HostPattern); + if (_rules[i].Port is int port) + { + builder.Append(':') + .Append(port.ToString(CultureInfo.InvariantCulture)); + } + } + + if (_rules.Length > limit) + { + builder.Append(", ..."); + } + + builder.Append(". Coordinate break-glass with platform operations before expanding access."); + } + + return builder.ToString(); + } + + private static bool IsLoopback(Uri destination) + { + if (string.Equals(destination.Host, "localhost", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (IPAddress.TryParse(destination.Host, out var address)) + { + return IPAddress.IsLoopback(address); + } + + return false; + } + + private static bool IsPrivateNetwork(Uri destination) + { + if (!IPAddress.TryParse(destination.Host, out var address)) + { + return false; + } + + if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + var bytes = address.GetAddressBytes(); + return bytes[0] switch + { + 10 => true, + 172 => bytes[1] >= 16 && bytes[1] <= 31, + 192 => bytes[1] == 168, + _ => false, + }; + } + + if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + return address.IsIPv6LinkLocal || address.IsIPv6SiteLocal; + } + + return false; + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyMode.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyMode.cs new file mode 100644 index 00000000..d0b23e9c --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyMode.cs @@ -0,0 +1,17 @@ +namespace StellaOps.AirGap.Policy; + +/// +/// Enumerates the available egress enforcement modes. +/// +public enum EgressPolicyMode +{ + /// + /// Outbound egress is permitted. Decisions are advisory only. + /// + Unsealed = 0, + + /// + /// Outbound egress is blocked unless an allow rule permits the request. + /// + Sealed = 1, +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyOptions.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyOptions.cs new file mode 100644 index 00000000..16efaa7c --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyOptions.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; + +namespace StellaOps.AirGap.Policy; + +/// +/// Options used to configure the . +/// +public sealed class EgressPolicyOptions +{ + private readonly List _allowRules = new(); + + /// + /// Gets or sets the enforcement mode. + /// + public EgressPolicyMode Mode { get; set; } = EgressPolicyMode.Unsealed; + + /// + /// Gets or sets a value indicating whether localhost/loopback destinations are always allowed. + /// + public bool AllowLoopback { get; set; } = true; + + /// + /// Gets or sets a value indicating whether RFC1918 private network ranges are allowed. + /// + public bool AllowPrivateNetworks { get; set; } = false; + + /// + /// Gets the configured allow rules. Only consulted when is . + /// + public IReadOnlyList AllowRules => _allowRules; + + /// + /// Optional documentation URL that will be included in remediation guidance. + /// + public string? RemediationDocumentationUrl { get; set; } + + /// + /// Optional support contact surfaced to operators when requests are blocked. + /// + public string? SupportContact { get; set; } + + /// + /// Removes all configured allow rules. + /// + public void ClearAllowRules() + { + _allowRules.Clear(); + } + + /// + /// Replaces the configured allow rules with the supplied set. + /// + /// Rules that should be applied. + public void SetAllowRules(IEnumerable rules) + { + if (rules is null) + { + throw new ArgumentNullException(nameof(rules)); + } + + _allowRules.Clear(); + foreach (var rule in rules) + { + AddAllowRule(rule); + } + } + + /// + /// Adds an allow rule to the configuration. + /// + /// Rule that should be added. + public void AddAllowRule(EgressRule rule) + { + if (rule is null) + { + throw new ArgumentNullException(nameof(rule)); + } + + _allowRules.Add(rule); + } + + /// + /// Adds an allow rule to the configuration using the supplied parameters. + /// + /// Host pattern to allow. + /// Optional port restriction. + /// Transport classification. + /// Optional description. + public void AddAllowRule(string hostPattern, int? port = null, EgressTransport transport = EgressTransport.Any, string? description = null) + => AddAllowRule(new EgressRule(hostPattern, port, transport, description)); + + internal EgressRule[] BuildRuleSet() + { + var snapshot = new EgressRule[_allowRules.Count]; + _allowRules.CopyTo(snapshot, 0); + return snapshot; + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyServiceCollectionExtensions.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyServiceCollectionExtensions.cs new file mode 100644 index 00000000..afb0a240 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressPolicyServiceCollectionExtensions.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace StellaOps.AirGap.Policy; + +/// +/// Dependency injection helpers for configuring the air-gap egress policy. +/// +public static class EgressPolicyServiceCollectionExtensions +{ + /// + /// Registers using the provided configuration delegate. + /// + /// Service collection that will be updated. + /// Optional configuration delegate. + /// The original . + public static IServiceCollection AddAirGapEgressPolicy(this IServiceCollection services, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + + if (configure is null) + { + services.AddOptions(); + } + else + { + services.AddOptions().Configure(configure); + } + + services.TryAddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return new EgressPolicy(options); + }); + + return services; + } + + /// + /// Registers using configuration sourced from the provided . + /// + /// Service collection that will be updated. + /// Configuration root used to locate air-gap settings. + /// + /// Optional configuration section name (defaults to "AirGap:Egress"). When the section cannot be resolved, + /// the provided root is used. + /// + /// The original . + public static IServiceCollection AddAirGapEgressPolicy( + this IServiceCollection services, + IConfiguration configuration, + string? sectionName = "AirGap:Egress") + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + var targetSection = ResolveConfigurationSection(configuration, sectionName); + + return services.AddAirGapEgressPolicy(options => + { + ApplyConfiguration(options, targetSection, configuration); + }); + } + + private static IConfiguration ResolveConfigurationSection(IConfiguration configuration, string? sectionName) + { + if (!string.IsNullOrWhiteSpace(sectionName)) + { + var namedSection = configuration.GetSection(sectionName); + if (namedSection.Exists()) + { + return namedSection; + } + } + + return configuration; + } + + private static void ApplyConfiguration(EgressPolicyOptions options, IConfiguration primarySection, IConfiguration root) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(primarySection); + ArgumentNullException.ThrowIfNull(root); + + var effectiveSection = ResolveEffectiveSection(primarySection); + var searchOrder = BuildSearchOrder(effectiveSection, primarySection, root); + + var modeValue = GetStringValue(searchOrder, "Mode"); + if (!string.IsNullOrWhiteSpace(modeValue) && + Enum.TryParse(modeValue, ignoreCase: true, out EgressPolicyMode parsedMode)) + { + options.Mode = parsedMode; + } + + var allowLoopback = GetNullableBool(searchOrder, "AllowLoopback"); + if (allowLoopback.HasValue) + { + options.AllowLoopback = allowLoopback.Value; + } + + var allowPrivateNetworks = GetNullableBool(searchOrder, "AllowPrivateNetworks"); + if (allowPrivateNetworks.HasValue) + { + options.AllowPrivateNetworks = allowPrivateNetworks.Value; + } + + var remediationUrl = GetStringValue(searchOrder, "RemediationDocumentationUrl"); + if (!string.IsNullOrWhiteSpace(remediationUrl)) + { + options.RemediationDocumentationUrl = remediationUrl.Trim(); + } + + var supportContact = GetStringValue(searchOrder, "SupportContact"); + if (!string.IsNullOrWhiteSpace(supportContact)) + { + options.SupportContact = supportContact.Trim(); + } + + var rules = new List(); + foreach (var ruleSection in EnumerateAllowRuleSections(effectiveSection, primarySection, root)) + { + var hostPattern = ruleSection["HostPattern"] + ?? ruleSection["Host"] + ?? ruleSection["Pattern"] + ?? ruleSection.Value; + + if (string.IsNullOrWhiteSpace(hostPattern)) + { + continue; + } + + hostPattern = hostPattern.Trim(); + var port = TryReadPort(ruleSection); + var transport = ParseTransport(ruleSection["Transport"] ?? ruleSection["Protocol"]); + + var description = ruleSection["Description"] ?? ruleSection["Notes"]; + description = string.IsNullOrWhiteSpace(description) ? null : description.Trim(); + + rules.Add(new EgressRule(hostPattern, port, transport, description)); + } + + options.SetAllowRules(rules); + } + + private static IConfiguration ResolveEffectiveSection(IConfiguration configuration) + { + var egressSection = configuration.GetSection("Egress"); + return egressSection.Exists() ? egressSection : configuration; + } + + private static IEnumerable BuildSearchOrder( + IConfiguration effective, + IConfiguration primary, + IConfiguration root) + { + yield return effective; + + if (!ReferenceEquals(primary, effective)) + { + yield return primary; + } + + if (!ReferenceEquals(root, effective) && !ReferenceEquals(root, primary)) + { + yield return root; + } + } + + private static string? GetStringValue(IEnumerable sections, string key) + { + foreach (var section in sections) + { + var value = section[key]; + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + + return null; + } + + private static bool? GetNullableBool(IEnumerable sections, string key) + { + foreach (var section in sections) + { + var value = section[key]; + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + if (bool.TryParse(value, out var parsed)) + { + return parsed; + } + } + + return null; + } + + private static IEnumerable EnumerateAllowRuleSections( + IConfiguration effective, + IConfiguration primary, + IConfiguration root) + { + foreach (var rule in EnumerateAllowRuleSections(effective)) + { + yield return rule; + } + + if (!ReferenceEquals(primary, effective)) + { + foreach (var rule in EnumerateAllowRuleSections(primary)) + { + yield return rule; + } + } + + if (!ReferenceEquals(root, effective) && !ReferenceEquals(root, primary)) + { + foreach (var rule in EnumerateAllowRuleSections(root)) + { + yield return rule; + } + } + } + + private static IEnumerable EnumerateAllowRuleSections(IConfiguration configuration) + { + foreach (var candidate in EnumerateAllowlistContainers(configuration)) + { + if (!candidate.Exists()) + { + continue; + } + + foreach (var child in candidate.GetChildren()) + { + yield return child; + } + } + } + + private static IEnumerable EnumerateAllowlistContainers(IConfiguration configuration) + { + yield return configuration.GetSection("Allowlist"); + yield return configuration.GetSection("AllowList"); + yield return configuration.GetSection("EgressAllowlist"); + yield return configuration.GetSection("Allow"); + } + + private static int? TryReadPort(IConfiguration section) + { + var raw = section["Port"]; + if (string.IsNullOrWhiteSpace(raw)) + { + return null; + } + + return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) + ? parsed + : null; + } + + private static EgressTransport ParseTransport(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return EgressTransport.Any; + } + + return Enum.TryParse(value, ignoreCase: true, out EgressTransport parsed) + ? parsed + : EgressTransport.Any; + } +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRequest.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRequest.cs new file mode 100644 index 00000000..65bb18d3 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRequest.cs @@ -0,0 +1,86 @@ +using System; + +namespace StellaOps.AirGap.Policy; + +/// +/// Describes an outbound operation that must be evaluated by the egress policy. +/// +public readonly record struct EgressRequest +{ + /// + /// Gets the component or subsystem requesting egress (e.g. PolicyEngine, ExportCenter). + /// + public string Component { get; } + + /// + /// Gets the intent of the egress request (e.g. advisories-sync, telemetry-export). + /// + public string Intent { get; } + + /// + /// Gets an optional short description of the operation being performed. + /// + public string? Operation { get; } + + /// + /// Gets the destination URI. + /// + public Uri Destination { get; } + + /// + /// Gets the transport classification of the request. + /// + public EgressTransport Transport { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Component or subsystem initiating the request. + /// Destination URI that will be contacted. + /// Intent label describing why the request is needed. + /// Transport classification. When set to , the transport is inferred from the destination. + /// Optional text describing the concrete operation. + /// Thrown when inputs are invalid. + /// Thrown when is null. + public EgressRequest( + string component, + Uri destination, + string intent, + EgressTransport transport = EgressTransport.Any, + string? operation = null) + { + if (string.IsNullOrWhiteSpace(component)) + { + throw new ArgumentException("Component must be provided.", nameof(component)); + } + + if (destination is null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (!destination.IsAbsoluteUri) + { + throw new ArgumentException("Destination must be an absolute URI.", nameof(destination)); + } + + if (string.IsNullOrWhiteSpace(intent)) + { + throw new ArgumentException("Intent must be provided.", nameof(intent)); + } + + Component = component.Trim(); + Intent = intent.Trim(); + Operation = string.IsNullOrWhiteSpace(operation) ? null : operation.Trim(); + Destination = destination; + Transport = transport == EgressTransport.Any ? InferTransport(destination) : transport; + } + + private static EgressTransport InferTransport(Uri destination) + => destination.Scheme switch + { + "https" => EgressTransport.Https, + "http" => EgressTransport.Http, + _ => EgressTransport.Any, + }; +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRule.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRule.cs new file mode 100644 index 00000000..fbff489b --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressRule.cs @@ -0,0 +1,127 @@ +using System; + +namespace StellaOps.AirGap.Policy; + +/// +/// Represents a single allow entry used when sealed mode is active. +/// +public sealed class EgressRule +{ + private readonly string _hostPattern; + private readonly string? _wildcardSuffix; + private readonly bool _wildcardAnyHost; + + /// + /// Initializes a new instance of the class. + /// + /// Host pattern to allow. Supports exact hosts, *.example.org, or *. + /// Optional port limitation. + /// Transport classification that must match the request. + /// Optional description shown in diagnostics. + public EgressRule(string hostPattern, int? port = null, EgressTransport transport = EgressTransport.Any, string? description = null) + { + if (string.IsNullOrWhiteSpace(hostPattern)) + { + throw new ArgumentException("Host pattern must be provided.", nameof(hostPattern)); + } + + _hostPattern = hostPattern.Trim().ToLowerInvariant(); + Description = string.IsNullOrWhiteSpace(description) ? null : description.Trim(); + Port = port; + Transport = transport; + + if (_hostPattern == "*") + { + _wildcardAnyHost = true; + } + else if (_hostPattern.StartsWith("*.", StringComparison.Ordinal)) + { + _wildcardSuffix = _hostPattern[1..]; // keep leading dot for suffix comparisons + } + } + + /// + /// Gets an optional description that can be surfaced in diagnostics. + /// + public string? Description { get; } + + /// + /// Gets the host pattern that was configured. + /// + public string HostPattern => _hostPattern; + + /// + /// Gets the optional port restriction. + /// + public int? Port { get; } + + /// + /// Gets the transport classification required for the rule. + /// + public EgressTransport Transport { get; } + + /// + /// Determines whether the rule allows the supplied request. + /// + /// The request that will be evaluated. + /// when the request is allowed; otherwise . + public bool Allows(EgressRequest request) + { + if (request.Destination is null) + { + return false; + } + + if (Transport != EgressTransport.Any && Transport != request.Transport) + { + return false; + } + + if (!HostMatches(request.Destination.Host)) + { + return false; + } + + if (Port is null) + { + return true; + } + + var requestPort = request.Destination.Port; + return requestPort == Port.Value; + } + + private bool HostMatches(string host) + { + if (string.IsNullOrEmpty(host)) + { + return false; + } + + var normalized = host.ToLowerInvariant(); + + if (_wildcardAnyHost) + { + return true; + } + + if (_wildcardSuffix is not null) + { + if (!normalized.EndsWith(_wildcardSuffix, StringComparison.Ordinal)) + { + return false; + } + + var remainderLength = normalized.Length - _wildcardSuffix.Length; + return remainderLength > 0; + } + + return string.Equals(normalized, _hostPattern, StringComparison.Ordinal); + } + + /// + public override string ToString() + => Port is null + ? $"{_hostPattern} ({Transport})" + : $"{_hostPattern}:{Port} ({Transport})"; +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressTransport.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressTransport.cs new file mode 100644 index 00000000..1833e446 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressTransport.cs @@ -0,0 +1,27 @@ +namespace StellaOps.AirGap.Policy; + +/// +/// Protocol classification for outbound requests. +/// +public enum EgressTransport +{ + /// + /// Any transport is acceptable. + /// + Any = 0, + + /// + /// HTTP transport. + /// + Http = 1, + + /// + /// HTTPS transport. + /// + Https = 2, + + /// + /// Generic TCP transport. + /// + Tcp = 3, +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/IEgressPolicy.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/IEgressPolicy.cs new file mode 100644 index 00000000..95aeca23 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/IEgressPolicy.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.AirGap.Policy; + +/// +/// Defines the contract used by StellaOps services to validate outbound network requests +/// against the current air-gap configuration. +/// +public interface IEgressPolicy +{ + /// + /// Gets a value indicating whether the environment is operating in sealed mode. + /// + bool IsSealed { get; } + + /// + /// Gets the configured policy mode. + /// + EgressPolicyMode Mode { get; } + + /// + /// Evaluates the supplied request and returns the decision without throwing. + /// + /// The outbound request that should be evaluated. + /// The resulting decision. + EgressDecision Evaluate(EgressRequest request); + + /// + /// Evaluates the supplied request asynchronously and returns the decision without throwing. + /// + /// The outbound request that should be evaluated. + /// Token used to observe cancellation. + /// The resulting decision. + ValueTask EvaluateAsync(EgressRequest request, CancellationToken cancellationToken = default); + + /// + /// Throws when the supplied request is not permitted by the current policy. + /// + /// The outbound request that should be validated. + void EnsureAllowed(EgressRequest request); + + /// + /// Throws when the supplied request is not permitted by the current policy. + /// + /// The outbound request that should be validated. + /// Token used to observe cancellation. + ValueTask EnsureAllowedAsync(EgressRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj new file mode 100644 index 00000000..731c09e0 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + + diff --git a/src/AirGap/StellaOps.AirGap.Policy/TASKS.md b/src/AirGap/StellaOps.AirGap.Policy/TASKS.md index 840b4542..320e8570 100644 --- a/src/AirGap/StellaOps.AirGap.Policy/TASKS.md +++ b/src/AirGap/StellaOps.AirGap.Policy/TASKS.md @@ -1,19 +1,19 @@ -# AirGap Policy Task Board — Epic 16: Air-Gapped Mode - -## Sprint 56 – Facade & Contracts -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| AIRGAP-POL-56-001 | TODO | AirGap Policy Guild | TELEMETRY-OBS-50-001 | Implement `StellaOps.AirGap.Policy` package exposing `EgressPolicy` facade with sealed/unsealed branches and remediation-friendly errors. | Facade package builds/tests; integration tests simulate sealed/unsealed; error contract documented. | -| AIRGAP-POL-56-002 | TODO | AirGap Policy Guild, DevEx Guild | AIRGAP-POL-56-001 | Create Roslyn analyzer/code fix warning on raw `HttpClient` usage outside approved wrappers; add CI integration. | Analyzer packaged; CI fails on intentional violation; docs updated for opt-in. | - -## Sprint 57 – Service Adoption Wave 1 -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| AIRGAP-POL-57-001 | TODO | AirGap Policy Guild, BE-Base Platform Guild | AIRGAP-POL-56-001 | Update core web services (Web, Exporter, Policy, Findings, Authority) to use `EgressPolicy`; ensure configuration wiring for sealed mode. | Services compile with facade; sealed-mode tests run in CI; configuration docs updated. | -| AIRGAP-POL-57-002 | TODO | AirGap Policy Guild, Task Runner Guild | AIRGAP-POL-56-001, TASKRUN-OBS-50-001 | Implement Task Runner job plan validator rejecting network steps unless marked internal allow-list. | Validator blocks forbidden steps; tests cover allow/deny; error surfaces remediation text. | - -## Sprint 58 – Service Adoption Wave 2 -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| AIRGAP-POL-58-001 | TODO | AirGap Policy Guild, Observability Guild | AIRGAP-POL-57-001 | Ensure Observability exporters only target local endpoints in sealed mode; disable remote sinks with warning. | Exporters respect sealed flag; timeline/log message emitted; docs updated. | -| AIRGAP-POL-58-002 | TODO | AirGap Policy Guild, CLI Guild | AIRGAP-POL-56-001, CLI-OBS-50-001 | Add CLI sealed-mode guard that refuses commands needing egress and surfaces remediation. | CLI returns `AIRGAP_EGRESS_BLOCKED`; tests cover sealed/unsealed flows; help text updated. | +# AirGap Policy Task Board — Epic 16: Air-Gapped Mode + +## Sprint 56 – Facade & Contracts +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| AIRGAP-POL-56-001 | DONE | AirGap Policy Guild | TELEMETRY-OBS-50-001 | Implement `StellaOps.AirGap.Policy` package exposing `EgressPolicy` facade with sealed/unsealed branches and remediation-friendly errors. | Facade package builds/tests; integration tests simulate sealed/unsealed; error contract documented. | +| AIRGAP-POL-56-002 | DONE (2025-11-03) | AirGap Policy Guild, DevEx Guild | AIRGAP-POL-56-001 | Create Roslyn analyzer/code fix warning on raw `HttpClient` usage outside approved wrappers; add CI integration.
2025-11-02: Analyzer skeleton drafted (HttpClient walker + diagnostics); CI wiring prototyped locally.
2025-11-03: Analyzer + code fix published (`StellaOps.AirGap.Policy.Analyzers`); unit tests added; NuGet mappings updated; adoption doc appended. | Analyzer packaged; CI fails on intentional violation; docs updated for opt-in. | + +## Sprint 57 – Service Adoption Wave 1 +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| AIRGAP-POL-57-001 | DONE (2025-11-03) | AirGap Policy Guild, BE-Base Platform Guild | AIRGAP-POL-56-001 | Update core web services (Web, Exporter, Policy, Findings, Authority) to use `EgressPolicy`; ensure configuration wiring for sealed mode.
2025-11-03: Authority/Policy/Export Center wired to new configuration binder; auth client enforces egress policy; air-gap configuration tests added. Findings/Web service adoption blocked pending service scaffolds. | Services compile with facade; sealed-mode tests run in CI; configuration docs updated. | +| AIRGAP-POL-57-002 | DONE (2025-11-03) | AirGap Policy Guild, Task Runner Guild | AIRGAP-POL-56-001, TASKRUN-OBS-50-001 | Implement Task Runner job plan validator rejecting network steps unless marked internal allow-list.
2025-11-03: Task Runner worker consumes `IEgressPolicy`; filesystem dispatcher feeds policy-aware planner; sealed-mode dispatcher test added; Http/Logging packages aligned to `10.0.0-rc.2`. | Validator blocks forbidden steps; tests cover allow/deny; error surfaces remediation text. | + +## Sprint 58 – Service Adoption Wave 2 +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| AIRGAP-POL-58-001 | TODO | AirGap Policy Guild, Observability Guild | AIRGAP-POL-57-001 | Ensure Observability exporters only target local endpoints in sealed mode; disable remote sinks with warning. | Exporters respect sealed flag; timeline/log message emitted; docs updated. | +| AIRGAP-POL-58-002 | TODO | AirGap Policy Guild, CLI Guild | AIRGAP-POL-56-001, CLI-OBS-50-001 | Add CLI sealed-mode guard that refuses commands needing egress and surfaces remediation. | CLI returns `AIRGAP_EGRESS_BLOCKED`; tests cover sealed/unsealed flows; help text updated. | diff --git a/src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml b/src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml index 18b05240..aa90a485 100644 --- a/src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml +++ b/src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml @@ -86,9 +86,13 @@ components: signals:write: Publish Signals events or mutate state. stellaops.bypass: Bypass trust boundary protections (restricted identities only). ui.read: Read Console UX resources. - vex:ingest: Submit VEX ingestion payloads. - vex:read: Read VEX ingestion data. - vuln:read: Read vulnerability permalinks and overlays. + vex:ingest: Submit VEX ingestion payloads. + vex:read: Read VEX ingestion data. + vuln:view: Read vulnerability overlays and issue permalinks. + vuln:investigate: Perform vulnerability triage actions (assign, comment, annotate). + vuln:operate: Execute vulnerability workflow transitions and remediation tasks. + vuln:audit: Access vulnerability audit ledgers and exports. + vuln:read: Read vulnerability permalinks and overlays. (legacy compatibility; prefer vuln:view) authorizationCode: authorizationUrl: /authorize tokenUrl: /token @@ -150,9 +154,13 @@ components: signals:write: Publish Signals events or mutate state. stellaops.bypass: Bypass trust boundary protections (restricted identities only). ui.read: Read Console UX resources. - vex:ingest: Submit VEX ingestion payloads. - vex:read: Read VEX ingestion data. - vuln:read: Read vulnerability permalinks and overlays. + vex:ingest: Submit VEX ingestion payloads. + vex:read: Read VEX ingestion data. + vuln:view: Read vulnerability overlays and issue permalinks. + vuln:investigate: Perform vulnerability triage actions (assign, comment, annotate). + vuln:operate: Execute vulnerability workflow transitions and remediation tasks. + vuln:audit: Access vulnerability audit ledgers and exports. + vuln:read: Read vulnerability permalinks and overlays. (legacy compatibility; prefer vuln:view) OAuthClientCredentials: type: oauth2 description: Client credential exchange for machine-to-machine identities. @@ -213,9 +221,13 @@ components: signals:write: Publish Signals events or mutate state. stellaops.bypass: Bypass trust boundary protections (restricted identities only). ui.read: Read Console UX resources. - vex:ingest: Submit VEX ingestion payloads. - vex:read: Read VEX ingestion data. - vuln:read: Read vulnerability permalinks and overlays. + vex:ingest: Submit VEX ingestion payloads. + vex:read: Read VEX ingestion data. + vuln:view: Read vulnerability overlays and issue permalinks. + vuln:investigate: Perform vulnerability triage actions (assign, comment, annotate). + vuln:operate: Execute vulnerability workflow transitions and remediation tasks. + vuln:audit: Access vulnerability audit ledgers and exports. + vuln:read: Read vulnerability permalinks and overlays. (legacy compatibility; prefer vuln:view) schemas: TokenResponse: type: object diff --git a/src/Attestor/StellaOps.Attestor/TASKS.md b/src/Attestor/StellaOps.Attestor/TASKS.md index c346c56c..2939deaa 100644 --- a/src/Attestor/StellaOps.Attestor/TASKS.md +++ b/src/Attestor/StellaOps.Attestor/TASKS.md @@ -29,7 +29,7 @@ ### Sprint 74 – Transparency & Bulk | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| -| ATTESTOR-74-001 | DONE (2025-11-02) | Attestor Service Guild | ATTESTOR-73-002, TRANSP-74-001 | Integrate transparency witness client, inclusion proof verification, and caching. | Witness proofs stored; verification fails on missing/inconsistent proofs; metrics emitted. | +| ATTESTOR-74-001 | DONE (2025-11-02) | Attestor Service Guild | ATTESTOR-73-002, TRANSP-74-001 | Integrate transparency witness client, inclusion proof verification, and caching.
2025-11-02: Witness client wired with repository schema update; verification/reporting paths refreshed and test suite green. | Witness proofs stored; verification fails on missing/inconsistent proofs; metrics emitted. | | ATTESTOR-74-002 | DONE | Attestor Service Guild | ATTESTOR-73-002 | Implement bulk verification worker + API with progress tracking, rate limits, and caching. | Bulk job API functional; worker processes batches; telemetry recorded. | ### Sprint 75 – Air Gap & Hardening diff --git a/src/Authority/StellaOps.Api.OpenApi/authority/openapi.yaml b/src/Authority/StellaOps.Api.OpenApi/authority/openapi.yaml index 615785b9..7b8ab374 100644 --- a/src/Authority/StellaOps.Api.OpenApi/authority/openapi.yaml +++ b/src/Authority/StellaOps.Api.OpenApi/authority/openapi.yaml @@ -75,7 +75,11 @@ components: ui.read: Read Console UX resources. vex:ingest: Submit VEX ingestion payloads. vex:read: Read VEX ingestion data. - vuln:read: Read vulnerability permalinks and overlays. + vuln:view: Read vulnerability overlays and issue permalinks. + vuln:investigate: Perform vulnerability triage actions (assign, comment, annotate). + vuln:operate: Execute vulnerability workflow transitions and remediation tasks. + vuln:audit: Access vulnerability audit ledgers and exports. + vuln:read: Read vulnerability permalinks and overlays. (legacy compatibility; prefer vuln:view) authorizationCode: authorizationUrl: /authorize tokenUrl: /token @@ -126,7 +130,11 @@ components: ui.read: Read Console UX resources. vex:ingest: Submit VEX ingestion payloads. vex:read: Read VEX ingestion data. - vuln:read: Read vulnerability permalinks and overlays. + vuln:view: Read vulnerability overlays and issue permalinks. + vuln:investigate: Perform vulnerability triage actions (assign, comment, annotate). + vuln:operate: Execute vulnerability workflow transitions and remediation tasks. + vuln:audit: Access vulnerability audit ledgers and exports. + vuln:read: Read vulnerability permalinks and overlays. (legacy compatibility; prefer vuln:view) OAuthClientCredentials: type: oauth2 description: Client credential exchange for machine-to-machine identities. @@ -179,7 +187,11 @@ components: ui.read: Read Console UX resources. vex:ingest: Submit VEX ingestion payloads. vex:read: Read VEX ingestion data. - vuln:read: Read vulnerability permalinks and overlays. + vuln:view: Read vulnerability overlays and issue permalinks. + vuln:investigate: Perform vulnerability triage actions (assign, comment, annotate). + vuln:operate: Execute vulnerability workflow transitions and remediation tasks. + vuln:audit: Access vulnerability audit ledgers and exports. + vuln:read: Read vulnerability permalinks and overlays. (legacy compatibility; prefer vuln:view) schemas: TokenResponse: type: object diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOpsScopesTests.cs b/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOpsScopesTests.cs index 8af8ddb1..fb1f63a2 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOpsScopesTests.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOpsScopesTests.cs @@ -1,9 +1,11 @@ using StellaOps.Auth.Abstractions; using Xunit; -namespace StellaOps.Auth.Abstractions.Tests; - -public class StellaOpsScopesTests +namespace StellaOps.Auth.Abstractions.Tests; + +#pragma warning disable CS0618 + +public class StellaOpsScopesTests { [Theory] [InlineData(StellaOpsScopes.AdvisoryRead)] @@ -33,8 +35,12 @@ public class StellaOpsScopesTests [InlineData(StellaOpsScopes.PolicySimulate)] [InlineData(StellaOpsScopes.FindingsRead)] [InlineData(StellaOpsScopes.EffectiveWrite)] - [InlineData(StellaOpsScopes.GraphRead)] - [InlineData(StellaOpsScopes.VulnRead)] + [InlineData(StellaOpsScopes.GraphRead)] + [InlineData(StellaOpsScopes.VulnView)] + [InlineData(StellaOpsScopes.VulnInvestigate)] + [InlineData(StellaOpsScopes.VulnOperate)] + [InlineData(StellaOpsScopes.VulnAudit)] + [InlineData(StellaOpsScopes.VulnRead)] [InlineData(StellaOpsScopes.GraphWrite)] [InlineData(StellaOpsScopes.GraphExport)] [InlineData(StellaOpsScopes.GraphSimulate)] @@ -82,8 +88,13 @@ public class StellaOpsScopesTests [InlineData("Packs.Run", StellaOpsScopes.PacksRun)] [InlineData("Packs.Approve", StellaOpsScopes.PacksApprove)] [InlineData("Notify.Escalate", StellaOpsScopes.NotifyEscalate)] + [InlineData("VULN:VIEW", StellaOpsScopes.VulnView)] + [InlineData("VULN:INVESTIGATE", StellaOpsScopes.VulnInvestigate)] + [InlineData("VULN:OPERATE", StellaOpsScopes.VulnOperate)] + [InlineData("VULN:AUDIT", StellaOpsScopes.VulnAudit)] public void Normalize_NormalizesToLowerCase(string input, string expected) { Assert.Equal(expected, StellaOpsScopes.Normalize(input)); } } +#pragma warning restore CS0618 diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsClaimTypes.cs b/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsClaimTypes.cs index 720259d1..262b3c69 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsClaimTypes.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsClaimTypes.cs @@ -115,6 +115,21 @@ public static class StellaOpsClaimTypes /// public const string IncidentReason = "stellaops:incident_reason"; + /// + /// Attribute-based access control filter for vulnerability environment visibility. + /// + public const string VulnerabilityEnvironment = "stellaops:attr:env"; + + /// + /// Attribute-based access control filter for vulnerability ownership visibility. + /// + public const string VulnerabilityOwner = "stellaops:attr:owner"; + + /// + /// Attribute-based access control filter for vulnerability business tier visibility. + /// + public const string VulnerabilityBusinessTier = "stellaops:attr:business_tier"; + /// /// Session identifier claim (sid). /// diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs b/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs index 7301ff06..19a51649 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs @@ -206,8 +206,29 @@ public static class StellaOpsScopes /// /// Scope granting read-only access to Vuln Explorer resources and permalinks. /// + [Obsolete("Use vuln:view (StellaOpsScopes.VulnView) instead.")] public const string VulnRead = "vuln:read"; + /// + /// Scope granting read-only access to Vuln Explorer findings, reports, and dashboards. + /// + public const string VulnView = "vuln:view"; + + /// + /// Scope permitting triage actions (assign, comment, annotate) within Vuln Explorer. + /// + public const string VulnInvestigate = "vuln:investigate"; + + /// + /// Scope permitting state-changing operations (status transitions, remediation workflows) within Vuln Explorer. + /// + public const string VulnOperate = "vuln:operate"; + + /// + /// Scope permitting access to Vuln Explorer audit exports and immutable ledgers. + /// + public const string VulnAudit = "vuln:audit"; + /// /// Scope granting read-only access to observability dashboards and overlays. /// @@ -399,7 +420,13 @@ public static class StellaOpsScopes FindingsRead, EffectiveWrite, GraphRead, + VulnView, + VulnInvestigate, + VulnOperate, + VulnAudit, +#pragma warning disable CS0618 // track removal once legacy scope dropped VulnRead, +#pragma warning restore CS0618 ObservabilityRead, TimelineRead, TimelineWrite, diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/ServiceCollectionExtensionsTests.cs b/src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/ServiceCollectionExtensionsTests.cs index c94f29bc..a7a93cf9 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/ServiceCollectionExtensionsTests.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/ServiceCollectionExtensionsTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +13,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Tokens; using StellaOps.Auth.Client; +using StellaOps.AirGap.Policy; using Xunit; namespace StellaOps.Auth.Client.Tests; @@ -73,6 +75,39 @@ public class ServiceCollectionExtensionsTests Assert.Contains(recordedHandlers, handler => handler.GetType().Name.Contains("PolicyHttpMessageHandler", StringComparison.Ordinal)); } + [Fact] + public void EnsureEgressAllowed_InvokesPolicyWhenAuthorityProvided() + { + var services = new ServiceCollection(); + var recordingPolicy = new RecordingPolicy(); + services.AddSingleton(recordingPolicy); + + using var provider = services.BuildServiceProvider(); + + var options = new StellaOpsAuthClientOptions + { + Authority = "https://authority.test", + DiscoveryCacheLifetime = TimeSpan.FromMinutes(1), + JwksCacheLifetime = TimeSpan.FromMinutes(1), + AllowOfflineCacheFallback = false, + }; + + options.Validate(); + + var method = typeof(ServiceCollectionExtensions) + .GetMethod("EnsureEgressAllowed", BindingFlags.NonPublic | BindingFlags.Static); + + Assert.NotNull(method); + + method!.Invoke(null, new object?[] { provider, options, "authority-discovery" }); + + Assert.Single(recordingPolicy.Requests); + var request = recordingPolicy.Requests[0]; + Assert.Equal("StellaOpsAuthClient", request.Component); + Assert.Equal(new Uri("https://authority.test"), request.Destination); + Assert.Equal("authority-discovery", request.Intent); + } + private static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string jsonContent) { return new HttpResponseMessage(statusCode) @@ -224,6 +259,37 @@ public class ServiceCollectionExtensionsTests } } + private sealed class RecordingPolicy : IEgressPolicy + { + private readonly List requests = new(); + + public bool IsSealed => true; + + public EgressPolicyMode Mode => EgressPolicyMode.Sealed; + + public IReadOnlyList Requests => requests; + + public EgressDecision Evaluate(EgressRequest request) + { + requests.Add(request); + return EgressDecision.Allowed; + } + + public ValueTask EvaluateAsync(EgressRequest request, CancellationToken cancellationToken = default) + => new(Evaluate(request)); + + public void EnsureAllowed(EgressRequest request) + { + requests.Add(request); + } + + public ValueTask EnsureAllowedAsync(EgressRequest request, CancellationToken cancellationToken = default) + { + EnsureAllowed(request); + return ValueTask.CompletedTask; + } + } + private sealed class ThrowingTokenClient : IStellaOpsTokenClient { public int RequestCount { get; private set; } @@ -278,11 +344,11 @@ public class ServiceCollectionExtensionsTests null, "{}"); - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary? additionalParameters = null, CancellationToken cancellationToken = default) - => throw new NotImplementedException(); + public Task RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary? additionalParameters = null, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); public Task GetJsonWebKeySetAsync(CancellationToken cancellationToken = default) => Task.FromResult(new JsonWebKeySet()); diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/ServiceCollectionExtensions.cs b/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/ServiceCollectionExtensions.cs index a136d1b5..e7ef69b0 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/ServiceCollectionExtensions.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; using Polly.Extensions.Http; +using StellaOps.AirGap.Policy; namespace StellaOps.Auth.Client; @@ -32,18 +33,21 @@ public static class ServiceCollectionExtensions services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; + EnsureEgressAllowed(provider, options, "authority-discovery"); client.Timeout = options.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider)); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; + EnsureEgressAllowed(provider, options, "authority-jwks"); client.Timeout = options.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider)); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; + EnsureEgressAllowed(provider, options, "authority-token"); client.Timeout = options.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider)); @@ -135,4 +139,28 @@ public static class ServiceCollectionExtensions } }); } + + private static void EnsureEgressAllowed( + IServiceProvider provider, + StellaOpsAuthClientOptions options, + string intent) + { + ArgumentNullException.ThrowIfNull(provider); + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(intent); + + if (options.AuthorityUri is null) + { + return; + } + + var policy = provider.GetService(); + if (policy is null) + { + return; + } + + var request = new EgressRequest("StellaOpsAuthClient", options.AuthorityUri, intent); + policy.EnsureAllowed(request); + } } diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj b/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj index c6654381..3ef58d63 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj @@ -28,6 +28,7 @@ + @@ -44,4 +45,4 @@ <_Parameter1>StellaOps.Auth.Client.Tests - \ No newline at end of file + diff --git a/src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOpsScopeAuthorizationHandler.cs b/src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOpsScopeAuthorizationHandler.cs index 34a62c47..46ce465a 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOpsScopeAuthorizationHandler.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOpsScopeAuthorizationHandler.cs @@ -798,6 +798,13 @@ internal sealed class StellaOpsScopeAuthorizationHandler : AuthorizationHandler< } } + #pragma warning disable CS0618 // compatibility with legacy vuln:read scope + if (scopes.Contains(StellaOpsScopes.VulnRead) && !scopes.Contains(StellaOpsScopes.VulnView)) + { + scopes.Add(StellaOpsScopes.VulnView); + } + #pragma warning restore CS0618 + return scopes; } diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityServiceAccountDocument.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityServiceAccountDocument.cs index a0e84e8f..586a9901 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityServiceAccountDocument.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityServiceAccountDocument.cs @@ -38,6 +38,9 @@ public sealed class AuthorityServiceAccountDocument [BsonElement("authorizedClients")] public List AuthorizedClients { get; set; } = new(); + [BsonElement("attributes")] + public Dictionary> Attributes { get; set; } = new(StringComparer.OrdinalIgnoreCase); + [BsonElement("createdAt")] public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityTokenDocument.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityTokenDocument.cs index f65f6c75..715d04e3 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityTokenDocument.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Documents/AuthorityTokenDocument.cs @@ -105,4 +105,16 @@ public sealed class AuthorityTokenDocument [BsonElement("actors")] [BsonIgnoreIfNull] public List? ActorChain { get; set; } + + [BsonElement("vulnEnv")] + [BsonIgnoreIfNull] + public string? VulnerabilityEnvironment { get; set; } + + [BsonElement("vulnOwner")] + [BsonIgnoreIfNull] + public string? VulnerabilityOwner { get; set; } + + [BsonElement("vulnBusinessTier")] + [BsonIgnoreIfNull] + public string? VulnerabilityBusinessTier { get; set; } } diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Stores/AuthorityServiceAccountStore.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Stores/AuthorityServiceAccountStore.cs index 157d6b92..6401cc60 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Stores/AuthorityServiceAccountStore.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Storage.Mongo/Stores/AuthorityServiceAccountStore.cs @@ -142,6 +142,7 @@ internal sealed class AuthorityServiceAccountStore : IAuthorityServiceAccountSto NormalizeList(document.AllowedScopes, static scope => scope.Trim().ToLowerInvariant(), StringComparer.Ordinal); NormalizeList(document.AuthorizedClients, static client => client.Trim().ToLowerInvariant(), StringComparer.OrdinalIgnoreCase); + NormalizeAttributes(document.Attributes); } private static void NormalizeList(IList values, Func normalizer, IEqualityComparer comparer) @@ -181,4 +182,77 @@ internal sealed class AuthorityServiceAccountStore : IAuthorityServiceAccountSto values[index] = normalized; } } + + private static void NormalizeAttributes(IDictionary> attributes) + { + ArgumentNullException.ThrowIfNull(attributes); + + if (attributes.Count == 0) + { + attributes.Clear(); + return; + } + + var normalized = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var (name, values) in attributes) + { + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + var key = name.Trim().ToLowerInvariant(); + if (!AllowedAttributeKeys.Contains(key)) + { + continue; + } + + var normalizedValues = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var wildcard = false; + + if (values is not null) + { + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + var trimmed = value.Trim(); + if (trimmed.Equals("*", StringComparison.Ordinal)) + { + normalizedValues.Clear(); + normalizedValues.Add("*"); + wildcard = true; + break; + } + + var lower = trimmed.ToLowerInvariant(); + if (seen.Add(lower)) + { + normalizedValues.Add(lower); + } + } + } + + if (wildcard) + { + normalized[key] = new List { "*" }; + } + else if (normalizedValues.Count > 0) + { + normalized[key] = normalizedValues; + } + } + + attributes.Clear(); + foreach (var pair in normalized) + { + attributes[pair.Key] = pair.Value; + } + } + + private static readonly HashSet AllowedAttributeKeys = new(new[] { "env", "owner", "business_tier" }, StringComparer.OrdinalIgnoreCase); } diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Bootstrap/ServiceAccountAdminEndpointsTests.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Bootstrap/ServiceAccountAdminEndpointsTests.cs index 2930c61a..6e6f95b7 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Bootstrap/ServiceAccountAdminEndpointsTests.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Bootstrap/ServiceAccountAdminEndpointsTests.cs @@ -7,15 +7,20 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Time.Testing; +using Microsoft.Extensions.Options; using MongoDB.Driver; using StellaOps.Auth.Abstractions; +using Microsoft.AspNetCore.Routing; +using StellaOps.Configuration; using StellaOps.Authority.OpenIddict; using StellaOps.Authority.Storage.Mongo.Documents; using StellaOps.Authority.Storage.Mongo.Stores; +using StellaOps.Authority.Storage.Mongo.Sessions; using StellaOps.Authority.Tests.Infrastructure; using StellaOps.Cryptography.Audit; using Xunit; @@ -101,6 +106,21 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture>(); + Assert.True(options.Value.Bootstrap.Enabled); + var endpoints = scope.ServiceProvider.GetRequiredService().Endpoints; + var serviceAccountsEndpoint = endpoints + .OfType() + .Single(endpoint => + { + var pattern = endpoint.RoutePattern.RawText?.TrimStart('/'); + return string.Equals(pattern, "internal/service-accounts", StringComparison.OrdinalIgnoreCase); + }); + Assert.Equal("internal/service-accounts", serviceAccountsEndpoint.RoutePattern.RawText?.TrimStart('/')); + } + var response = await client.GetAsync($"/internal/service-accounts?tenant={TenantId}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -115,6 +135,13 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture(); + var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None).ConfigureAwait(false); + + Assert.NotNull(document); + initialId = document!.Id; + initialCreatedAt = document.CreatedAt; + } + + using (var secondApp = CreateApplication()) + { + await using var scope = secondApp.Services.CreateAsyncScope(); + var store = scope.ServiceProvider.GetRequiredService(); + var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None).ConfigureAwait(false); + + Assert.NotNull(document); + Assert.Equal(initialId, document!.Id); + Assert.Equal(initialCreatedAt, document.CreatedAt); + Assert.True(document.UpdatedAt >= initialCreatedAt); + } + } + private WebApplicationFactory CreateApplication(Action? configure = null) { + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__BOOTSTRAP__ENABLED", "true"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__BOOTSTRAP__APIKEY", BootstrapKey); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__BOOTSTRAP__DEFAULTIDENTITYPROVIDER", "standard"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__TENANTS__0__ID", TenantId); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__TENANTS__0__DISPLAYNAME", "Default Tenant"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS", "50"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID", ServiceAccountId); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT", TenantId); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME", "Observability Exporter"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION", "Automates evidence exports."); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0", "jobs:read"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__1", "findings:read"); + Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__AUTHORIZEDCLIENTS__0", "export-center-worker"); + return factory.WithWebHostBuilder(host => { host.ConfigureAppConfiguration((_, configuration) => @@ -478,6 +549,47 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture + { + services.PostConfigure(options => + { + options.Bootstrap.Enabled = true; + options.Bootstrap.ApiKey = BootstrapKey; + options.Bootstrap.DefaultIdentityProvider = "standard"; + + if (options.Tenants.Count == 0) + { + options.Tenants.Add(new AuthorityTenantOptions + { + Id = TenantId, + DisplayName = "Default Tenant" + }); + } + + options.Delegation.Quotas.MaxActiveTokens = 50; + + if (options.Delegation.ServiceAccounts.Count == 0) + { + var serviceAccount = new AuthorityServiceAccountSeedOptions + { + AccountId = ServiceAccountId, + Tenant = TenantId, + DisplayName = "Observability Exporter", + Description = "Automates evidence exports." + }; + + serviceAccount.AllowedScopes.Add("jobs:read"); + serviceAccount.AllowedScopes.Add("findings:read"); + serviceAccount.AuthorizedClients.Add("export-center-worker"); + serviceAccount.Attributes["env"] = new List { "prod" }; + serviceAccount.Attributes["owner"] = new List { "vuln-team" }; + serviceAccount.Attributes["business_tier"] = new List { "tier-1" }; + + options.Delegation.ServiceAccounts.Add(serviceAccount); + } + }); + }); + configure?.Invoke(host); }); } @@ -496,7 +608,8 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture AllowedScopes, - IReadOnlyList AuthorizedClients); + IReadOnlyList AuthorizedClients, + IReadOnlyDictionary> Attributes); private sealed record ServiceAccountTokenResponse( string TokenId, diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Infrastructure/AuthorityWebApplicationFactory.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Infrastructure/AuthorityWebApplicationFactory.cs index 719f9f2e..dcbf9767 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Infrastructure/AuthorityWebApplicationFactory.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Infrastructure/AuthorityWebApplicationFactory.cs @@ -20,9 +20,22 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory mongoRunner.ConnectionString; @@ -116,6 +142,19 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory(); services.AddSingleton(sink); services.Replace(ServiceDescriptor.Singleton(timeProvider)); + services.PostConfigure(options => + { + options.Notifications.AckTokens.Enabled = true; + options.Notifications.AckTokens.ActiveKeyId = "ack-key-1"; + options.Notifications.AckTokens.KeyPath = key1Path; + options.Notifications.AckTokens.KeySource = "file"; + options.Notifications.AckTokens.Algorithm = SignatureAlgorithms.Es256; + }); var authBuilder = services.AddAuthentication(options => { options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName; @@ -144,6 +153,14 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture(); services.AddSingleton(sink); services.Replace(ServiceDescriptor.Singleton(timeProvider)); + services.PostConfigure(options => + { + options.Notifications.AckTokens.Enabled = true; + options.Notifications.AckTokens.ActiveKeyId = "ack-key-1"; + options.Notifications.AckTokens.KeyPath = key1Path; + options.Notifications.AckTokens.KeySource = "file"; + options.Notifications.AckTokens.Algorithm = SignatureAlgorithms.Es256; + }); var authBuilder = services.AddAuthentication(options => { options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName; diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/OpenIddict/ClientCredentialsAndTokenHandlersTests.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/OpenIddict/ClientCredentialsAndTokenHandlersTests.cs index 72a11b0b..31948a8e 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/OpenIddict/ClientCredentialsAndTokenHandlersTests.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/OpenIddict/ClientCredentialsAndTokenHandlersTests.cs @@ -306,6 +306,213 @@ public class ClientCredentialsHandlersTests Assert.Equal("Delegation token quota exceeded for service account.", context.ErrorDescription); } + [Fact] + public async Task ValidateClientCredentials_RejectsVulnScopeWhenAttributeAmbiguous() + { + var clientDocument = CreateClient( + secret: "s3cr3t!", + allowedGrantTypes: "client_credentials", + allowedScopes: "vuln:view", + tenant: "tenant-alpha"); + + var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)); + var metadataAccessor = new TestRateLimiterMetadataAccessor(); + var options = TestHelpers.CreateAuthorityOptions(); + + var serviceAccount = new AuthorityServiceAccountDocument + { + AccountId = "svc-vuln", + Tenant = "tenant-alpha", + AllowedScopes = new List { "vuln:view" }, + AuthorizedClients = new List { clientDocument.ClientId } + }; + + serviceAccount.Attributes["env"] = new List { "prod", "stage" }; + serviceAccount.Attributes["owner"] = new List { "security" }; + serviceAccount.Attributes["business_tier"] = new List { "tier-1" }; + + var handler = new ValidateClientCredentialsHandler( + new TestClientStore(clientDocument), + registry, + TestActivitySource, + new TestAuthEventSink(), + metadataAccessor, + new TestServiceAccountStore(serviceAccount), + new TestTokenStore(), + TimeProvider.System, + new NoopCertificateValidator(), + new HttpContextAccessor(), + options, + NullLogger.Instance); + + var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln"); + + var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction); + await handler.HandleAsync(context); + + Assert.True(context.IsRejected); + Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, context.Error); + Assert.Equal("vuln_env must be supplied when multiple values are configured for the service account.", context.ErrorDescription); + } + + [Fact] + public async Task ValidateClientCredentials_AllowsVulnScopeWhenAttributesProvided() + { + var clientDocument = CreateClient( + secret: "s3cr3t!", + allowedGrantTypes: "client_credentials", + allowedScopes: "vuln:view", + tenant: "tenant-alpha"); + + var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)); + var metadataAccessor = new TestRateLimiterMetadataAccessor(); + var options = TestHelpers.CreateAuthorityOptions(); + + var serviceAccount = new AuthorityServiceAccountDocument + { + AccountId = "svc-vuln", + Tenant = "tenant-alpha", + AllowedScopes = new List { "vuln:view" }, + AuthorizedClients = new List { clientDocument.ClientId } + }; + + serviceAccount.Attributes["env"] = new List { "prod", "stage" }; + serviceAccount.Attributes["owner"] = new List { "security" }; + serviceAccount.Attributes["business_tier"] = new List { "tier-1" }; + + var serviceAccountStore = new TestServiceAccountStore(serviceAccount); + + var handler = new ValidateClientCredentialsHandler( + new TestClientStore(clientDocument), + registry, + TestActivitySource, + new TestAuthEventSink(), + metadataAccessor, + serviceAccountStore, + new TestTokenStore(), + TimeProvider.System, + new NoopCertificateValidator(), + new HttpContextAccessor(), + options, + NullLogger.Instance); + + var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "security"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1"); + + var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction); + await handler.HandleAsync(context); + + Assert.False(context.IsRejected); + Assert.Equal("prod", context.Transaction.Properties[AuthorityOpenIddictConstants.VulnEnvironmentProperty]); + Assert.Equal("security", context.Transaction.Properties[AuthorityOpenIddictConstants.VulnOwnerProperty]); + Assert.Equal("tier-1", context.Transaction.Properties[AuthorityOpenIddictConstants.VulnBusinessTierProperty]); + + var metadata = metadataAccessor.GetMetadata(); + Assert.NotNull(metadata); + Assert.True(metadata!.Tags.TryGetValue("authority.vuln_env", out var envTag)); + Assert.Equal("prod", envTag); + } + + [Fact] + public async Task HandleClientCredentials_PersistsVulnAttributes() + { + var clientDocument = CreateClient( + secret: "s3cr3t!", + allowedGrantTypes: "client_credentials", + allowedScopes: "vuln:view", + tenant: "tenant-alpha"); + + var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)); + var metadataAccessor = new TestRateLimiterMetadataAccessor(); + var options = TestHelpers.CreateAuthorityOptions(); + + var serviceAccount = new AuthorityServiceAccountDocument + { + AccountId = "svc-vuln", + Tenant = "tenant-alpha", + AllowedScopes = new List { "vuln:view" }, + AuthorizedClients = new List { clientDocument.ClientId } + }; + + serviceAccount.Attributes["env"] = new List { "prod", "stage" }; + serviceAccount.Attributes["owner"] = new List { "security" }; + serviceAccount.Attributes["business_tier"] = new List { "tier-1" }; + + var tokenStore = new TestTokenStore(); + var serviceAccountStore = new TestServiceAccountStore(serviceAccount); + + var validateHandler = new ValidateClientCredentialsHandler( + new TestClientStore(clientDocument), + registry, + TestActivitySource, + new TestAuthEventSink(), + metadataAccessor, + serviceAccountStore, + tokenStore, + TimeProvider.System, + new NoopCertificateValidator(), + new HttpContextAccessor(), + options, + NullLogger.Instance); + + var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view"); + transaction.Options = new OpenIddictServerOptions + { + AccessTokenLifetime = TimeSpan.FromMinutes(5) + }; + transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "security"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1"); + + var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction); + await validateHandler.HandleAsync(validateContext); + Assert.False(validateContext.IsRejected); + + var sessionAccessor = new NullMongoSessionAccessor(); + var handleHandler = new HandleClientCredentialsHandler( + registry, + tokenStore, + sessionAccessor, + metadataAccessor, + TimeProvider.System, + TestActivitySource, + NullLogger.Instance); + + var handleContext = new OpenIddictServerEvents.HandleTokenRequestContext(transaction); + await handleHandler.HandleAsync(handleContext); + + Assert.True(handleContext.IsRequestHandled); + var principal = Assert.IsType(handleContext.Principal); + Assert.Equal("prod", principal.FindFirstValue(StellaOpsClaimTypes.VulnerabilityEnvironment)); + Assert.Equal("security", principal.FindFirstValue(StellaOpsClaimTypes.VulnerabilityOwner)); + Assert.Equal("tier-1", principal.FindFirstValue(StellaOpsClaimTypes.VulnerabilityBusinessTier)); + + var persistHandler = new PersistTokensHandler( + tokenStore, + sessionAccessor, + TimeProvider.System, + TestActivitySource, + NullLogger.Instance); + + var signInContext = new OpenIddictServerEvents.ProcessSignInContext(transaction) + { + Principal = principal, + AccessTokenPrincipal = principal + }; + + await persistHandler.HandleAsync(signInContext); + + Assert.NotNull(tokenStore.Inserted); + Assert.Equal("prod", tokenStore.Inserted!.VulnerabilityEnvironment); + Assert.Equal("security", tokenStore.Inserted!.VulnerabilityOwner); + Assert.Equal("tier-1", tokenStore.Inserted!.VulnerabilityBusinessTier); + } + [Fact] public async Task ValidateClientCredentials_RejectsAdvisoryReadWithoutAocVerify() { @@ -1967,6 +2174,41 @@ public class ClientCredentialsHandlersTests Assert.Equal("VEX scopes require a tenant assignment.", context.ErrorDescription); } + [Fact] + public async Task ValidateClientCredentials_RejectsVulnViewScope_WhenTenantMissing() + { + var clientDocument = CreateClient( + clientId: "vuln-explorer-ui", + secret: "s3cr3t!", + allowedGrantTypes: "client_credentials", + allowedScopes: "vuln:view vuln:investigate"); + + var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)); + var options = TestHelpers.CreateAuthorityOptions(); + var handler = new ValidateClientCredentialsHandler( + new TestClientStore(clientDocument), + registry, + TestActivitySource, + new TestAuthEventSink(), + new TestRateLimiterMetadataAccessor(), + new TestServiceAccountStore(), + new TestTokenStore(), + TimeProvider.System, + new NoopCertificateValidator(), + new HttpContextAccessor(), + options, + NullLogger.Instance); + + var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view"); + var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction); + + await handler.HandleAsync(context); + + Assert.True(context.IsRejected); + Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, context.Error); + Assert.Equal("vuln_env is required when requesting vulnerability scopes.", context.ErrorDescription); + } + [Fact] public async Task ValidateClientCredentials_AllowsAdvisoryScopes_WithTenant() { @@ -2146,12 +2388,16 @@ public class ClientCredentialsHandlersTests Assert.False(validateContext.IsRejected); var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)); + var tokenStore = new TestTokenStore(); + var serviceAccountStore = new TestServiceAccountStore(); var validateHandler = new ValidateClientCredentialsHandler( clientStore, registry, TestActivitySource, auditSink, rateMetadata, + serviceAccountStore, + tokenStore, TimeProvider.System, new NoopCertificateValidator(), new HttpContextAccessor(), @@ -2161,7 +2407,6 @@ public class ClientCredentialsHandlersTests await validateHandler.HandleAsync(validateContext); Assert.False(validateContext.IsRejected); - var tokenStore = new TestTokenStore(); var sessionAccessor = new NullMongoSessionAccessor(); var handleHandler = new HandleClientCredentialsHandler( registry, @@ -2654,6 +2899,91 @@ public class ClientCredentialsHandlersTests Assert.Equal("tenant-alpha", inserted.Tenant); Assert.Contains("jobs:read", inserted.Scope); } + + [Fact] + public async Task HandleClientCredentials_ProjectsServiceAccountAttributeClaims() + { + var clientDocument = CreateClient( + clientId: "vuln-explorer-worker", + secret: "s3cr3t!", + allowedGrantTypes: "client_credentials", + allowedScopes: "vuln:view vuln:investigate", + tenant: "tenant-alpha"); + + var serviceAccount = new AuthorityServiceAccountDocument + { + AccountId = "svc-vuln", + Tenant = "tenant-alpha", + AllowedScopes = new List { "vuln:view", "vuln:investigate" }, + AuthorizedClients = new List { clientDocument.ClientId }, + Attributes = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["env"] = new List { "Prod", "stage" }, + ["owner"] = new List { "SecOps" }, + ["business_tier"] = new List { "*" }, + ["ignored"] = new List { "value" } + } + }; + + var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)); + var tokenStore = new TestTokenStore(); + var sessionAccessor = new NullMongoSessionAccessor(); + var authSink = new TestAuthEventSink(); + var metadataAccessor = new TestRateLimiterMetadataAccessor(); + var serviceAccountStore = new TestServiceAccountStore(serviceAccount); + var options = TestHelpers.CreateAuthorityOptions(opts => + { + opts.Delegation.Quotas.MaxActiveTokens = 5; + }); + + var validateHandler = new ValidateClientCredentialsHandler( + new TestClientStore(clientDocument), + registry, + TestActivitySource, + authSink, + metadataAccessor, + serviceAccountStore, + tokenStore, + TimeProvider.System, + new NoopCertificateValidator(), + new HttpContextAccessor(), + options, + NullLogger.Instance); + + var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view vuln:investigate"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "secops"); + transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1"); + + var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction); + await validateHandler.HandleAsync(validateContext); + Assert.False(validateContext.IsRejected); + + var handleHandler = new HandleClientCredentialsHandler( + registry, + tokenStore, + sessionAccessor, + metadataAccessor, + TimeProvider.System, + TestActivitySource, + NullLogger.Instance); + + var handleContext = new OpenIddictServerEvents.HandleTokenRequestContext(transaction); + await handleHandler.HandleAsync(handleContext); + + Assert.True(handleContext.IsRequestHandled); + var principal = handleContext.Principal ?? throw new InvalidOperationException("Principal missing"); + + var envClaims = principal.FindAll(StellaOpsClaimTypes.VulnerabilityEnvironment).Select(c => c.Value).ToArray(); + Assert.Equal(new[] { "prod" }, envClaims); + + var ownerClaims = principal.FindAll(StellaOpsClaimTypes.VulnerabilityOwner).Select(c => c.Value).ToArray(); + Assert.Equal(new[] { "secops" }, ownerClaims); + + var tierClaims = principal.FindAll(StellaOpsClaimTypes.VulnerabilityBusinessTier).Select(c => c.Value).ToArray(); + Assert.Equal(new[] { "tier-1" }, tierClaims); + } } public class TokenValidationHandlersTests diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Permalinks/VulnPermalinkServiceTests.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Permalinks/VulnPermalinkServiceTests.cs index 7ecaf14b..c6c7c75b 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Permalinks/VulnPermalinkServiceTests.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Permalinks/VulnPermalinkServiceTests.cs @@ -20,9 +20,11 @@ using StellaOps.Cryptography.DependencyInjection; using StellaOps.Auth.Abstractions; using Xunit; -namespace StellaOps.Authority.Tests.Permalinks; - -public sealed class VulnPermalinkServiceTests +namespace StellaOps.Authority.Tests.Permalinks; + +#pragma warning disable CS0618 // legacy scope coverage + +public sealed class VulnPermalinkServiceTests { [Fact] public async Task CreateAsync_IssuesSignedTokenWithExpectedClaims() @@ -150,4 +152,5 @@ public sealed class VulnPermalinkServiceTests public IFileProvider ContentRootFileProvider { get; set; } } -} +} +#pragma warning restore CS0618 diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Vulnerability/VulnWorkflowTokenEndpointTests.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Vulnerability/VulnWorkflowTokenEndpointTests.cs new file mode 100644 index 00000000..691252c8 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/Vulnerability/VulnWorkflowTokenEndpointTests.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Auth.Abstractions; +using StellaOps.Authority; +using StellaOps.Authority.Tests.Infrastructure; +using StellaOps.Authority.Vulnerability.Attachments; +using StellaOps.Authority.Vulnerability.Workflow; +using StellaOps.Configuration; +using StellaOps.Cryptography; +using StellaOps.Cryptography.Audit; +using Xunit; + +namespace StellaOps.Authority.Tests.Vulnerability; + +public sealed class VulnWorkflowTokenEndpointTests : IClassFixture +{ + private readonly AuthorityWebApplicationFactory factory; + + public VulnWorkflowTokenEndpointTests(AuthorityWebApplicationFactory factory) + { + this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + [Fact] + public async Task IssueAndVerifyWorkflowToken_SucceedsAndAudits() + { + var tempDir = Directory.CreateTempSubdirectory("workflow-token-success"); + var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem"); + + try + { + CreateEcPrivateKey(keyPath); + + var sink = new RecordingAuthEventSink(); + var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:00:00Z")); + + using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath); + using var client = app.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName); + client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate); + client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default"); + client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default"); + + var issuePayload = new + { + tenant = "tenant-default", + actions = new[] { "assign", "comment" }, + context = new Dictionary { ["finding_id"] = "F-123" }, + nonce = "workflow-nonce-123456", + expiresInSeconds = 600 + }; + + var issueResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload); + var issueBody = await issueResponse.Content.ReadAsStringAsync(); + Assert.True(issueResponse.StatusCode == HttpStatusCode.OK, $"Issue anti-forgery failed: {issueResponse.StatusCode} {issueBody}"); + + var issued = System.Text.Json.JsonSerializer.Deserialize( + issueBody, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.NotNull(issued); + Assert.Equal("workflow-nonce-123456", issued!.Nonce); + Assert.Contains("assign", issued.Actions); + Assert.Contains("comment", issued.Actions); + + var verifyPayload = new VulnWorkflowAntiForgeryVerifyRequest + { + Token = issued.Token, + RequiredAction = "assign", + Tenant = "tenant-default", + Nonce = "workflow-nonce-123456" + }; + + var verifyResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/verify", verifyPayload); + var verifyBody = await verifyResponse.Content.ReadAsStringAsync(); + Assert.True(verifyResponse.StatusCode == HttpStatusCode.OK, $"Verify anti-forgery failed: {verifyResponse.StatusCode} {verifyBody}"); + + var verified = System.Text.Json.JsonSerializer.Deserialize( + verifyBody, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.NotNull(verified); + Assert.Equal("tenant-default", verified!.Tenant); + Assert.Equal("workflow-nonce-123456", verified.Nonce); + + var issuedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.issued")); + Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.workflow.actor"); + + var verifiedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.verified")); + Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.workflow.nonce" && property.Value.Value == "workflow-nonce-123456"); + } + finally + { + TryDeleteDirectory(tempDir.FullName); + } + } + + [Fact] + public async Task IssueWorkflowToken_ReturnsBadRequest_WhenActionsMissing() + { + var tempDir = Directory.CreateTempSubdirectory("workflow-token-missing-actions"); + var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem"); + + try + { + CreateEcPrivateKey(keyPath); + + var sink = new RecordingAuthEventSink(); + var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:10:00Z")); + + using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath); + using var client = app.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName); + client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate); + client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default"); + client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default"); + + var issuePayload = new + { + tenant = "tenant-default", + actions = Array.Empty() + }; + + var response = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var error = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(error); + Assert.Equal("invalid_request", error!["error"]); + Assert.Contains("action", error["message"], StringComparison.OrdinalIgnoreCase); + + Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued"); + } + finally + { + TryDeleteDirectory(tempDir.FullName); + } + } + + [Fact] + public async Task VerifyWorkflowToken_ReturnsBadRequest_WhenActionNotPermitted() + { + var tempDir = Directory.CreateTempSubdirectory("workflow-token-invalid-action"); + var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem"); + + try + { + CreateEcPrivateKey(keyPath); + + var sink = new RecordingAuthEventSink(); + var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:20:00Z")); + + using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath); + using var client = app.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName); + client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate); + client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default"); + client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default"); + + var issuePayload = new + { + tenant = "tenant-default", + actions = new[] { "assign" }, + nonce = "workflow-nonce-789012" + }; + + var issueResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload); + Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode); + var issued = await issueResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(issued); + + var verifyPayload = new VulnWorkflowAntiForgeryVerifyRequest + { + Token = issued!.Token, + RequiredAction = "close", + Tenant = "tenant-default", + Nonce = "workflow-nonce-789012" + }; + + var verifyResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/verify", verifyPayload); + Assert.Equal(HttpStatusCode.BadRequest, verifyResponse.StatusCode); + + var error = await verifyResponse.Content.ReadFromJsonAsync>(); + Assert.NotNull(error); + Assert.Equal("invalid_token", error!["error"]); + Assert.Contains("Token does not permit action", error["message"], StringComparison.Ordinal); + + Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.issued")); + Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified"); + } + finally + { + TryDeleteDirectory(tempDir.FullName); + } + } + + [Fact] + public async Task IssueAndVerifyAttachmentToken_SucceedsAndAudits() + { + var tempDir = Directory.CreateTempSubdirectory("attachment-token-success"); + var keyPath = Path.Combine(tempDir.FullName, "attachment-key.pem"); + + try + { + CreateEcPrivateKey(keyPath); + + var sink = new RecordingAuthEventSink(); + var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:00:00Z")); + + using var app = CreateSignedAuthorityApp(sink, timeProvider, "attachment-key", keyPath); + using var client = app.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName); + client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnInvestigate); + client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default"); + client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default"); + + var issuePayload = new VulnAttachmentTokenIssueRequest + { + Tenant = "tenant-default", + LedgerEventHash = "ledger-hash-001", + AttachmentId = "attach-123", + FindingId = "find-456", + ContentHash = "sha256:abc123", + ContentType = "application/pdf", + Metadata = new Dictionary { ["origin"] = "vuln-workflow" } + }; + + var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload); + Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode); + var issued = await issueResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(issued); + Assert.Equal("attach-123", issued!.AttachmentId); + + var verifyPayload = new VulnAttachmentTokenVerifyRequest + { + Token = issued.Token, + Tenant = "tenant-default", + LedgerEventHash = "ledger-hash-001", + AttachmentId = "attach-123" + }; + + var verifyResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/verify", verifyPayload); + Assert.Equal(HttpStatusCode.OK, verifyResponse.StatusCode); + var verified = await verifyResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(verified); + Assert.Equal("ledger-hash-001", verified!.LedgerEventHash); + + var issuedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.issued")); + Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001"); + + var verifiedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.verified")); + Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001"); + } + finally + { + TryDeleteDirectory(tempDir.FullName); + } + } + + [Fact] + public async Task VerifyAttachmentToken_ReturnsBadRequest_WhenLedgerMismatch() + { + var tempDir = Directory.CreateTempSubdirectory("attachment-token-ledger-mismatch"); + var keyPath = Path.Combine(tempDir.FullName, "attachment-key.pem"); + + try + { + CreateEcPrivateKey(keyPath); + + var sink = new RecordingAuthEventSink(); + var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:10:00Z")); + + using var app = CreateSignedAuthorityApp(sink, timeProvider, "attachment-key", keyPath); + using var client = app.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName); + client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnInvestigate); + client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default"); + client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default"); + + var issuePayload = new VulnAttachmentTokenIssueRequest + { + Tenant = "tenant-default", + LedgerEventHash = "ledger-hash-001", + AttachmentId = "attach-123" + }; + + var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload); + Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode); + var issued = await issueResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(issued); + + var verifyPayload = new VulnAttachmentTokenVerifyRequest + { + Token = issued!.Token, + Tenant = "tenant-default", + LedgerEventHash = "ledger-hash-999", + AttachmentId = "attach-123" + }; + + var verifyResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/verify", verifyPayload); + Assert.Equal(HttpStatusCode.BadRequest, verifyResponse.StatusCode); + + var error = await verifyResponse.Content.ReadFromJsonAsync>(); + Assert.NotNull(error); + Assert.Equal("invalid_token", error!["error"]); + Assert.Contains("ledger reference", error["message"], StringComparison.OrdinalIgnoreCase); + + Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.issued")); + Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified"); + } + finally + { + TryDeleteDirectory(tempDir.FullName); + } + } + + private WebApplicationFactory CreateSignedAuthorityApp( + RecordingAuthEventSink sink, + FakeTimeProvider timeProvider, + string signingKeyId, + string signingKeyPath) + { + return factory.WithWebHostBuilder(host => + { + host.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new Dictionary + { + ["Authority:Signing:Enabled"] = "true", + ["Authority:Signing:ActiveKeyId"] = signingKeyId, + ["Authority:Signing:KeyPath"] = signingKeyPath, + ["Authority:Signing:KeySource"] = "file", + ["Authority:Signing:Algorithm"] = SignatureAlgorithms.Es256 + }); + }); + + host.ConfigureServices(services => + { + services.RemoveAll(); + services.AddSingleton(sink); + services.Replace(ServiceDescriptor.Singleton(timeProvider)); + services.PostConfigure(options => + { + options.Signing.Enabled = true; + options.Signing.ActiveKeyId = signingKeyId; + options.Signing.KeyPath = signingKeyPath; + options.Signing.KeySource = "file"; + options.Signing.Algorithm = SignatureAlgorithms.Es256; + options.VulnerabilityExplorer.Workflow.AntiForgery.Enabled = true; + options.VulnerabilityExplorer.Attachments.Enabled = true; + }); + + var authBuilder = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName; + options.DefaultChallengeScheme = TestAuthHandler.SchemeName; + }); + + authBuilder.AddScheme(TestAuthHandler.SchemeName, _ => { }); + authBuilder.AddScheme(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { }); + }); + }); + } + + private static void CreateEcPrivateKey(string path) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + File.WriteAllText(path, ecdsa.ExportECPrivateKeyPem()); + } + + private static void TryDeleteDirectory(string directory) + { + try + { + Directory.Delete(directory, recursive: true); + } + catch + { + // Ignored during cleanup. + } + } + + private sealed class RecordingAuthEventSink : IAuthEventSink + { + private readonly List events = new(); + + public IReadOnlyList Events => events; + + public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken) + { + events.Add(record); + return ValueTask.CompletedTask; + } + } +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/AuthorityOpenIddictConstants.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/AuthorityOpenIddictConstants.cs index b8fa657d..78a71104 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/AuthorityOpenIddictConstants.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/AuthorityOpenIddictConstants.cs @@ -28,6 +28,7 @@ internal static class AuthorityOpenIddictConstants internal const string MtlsCertificateHexProperty = "authority:mtls_thumbprint_hex"; internal const string ClientTenantProperty = "authority:client_tenant"; internal const string ClientProjectProperty = "authority:client_project"; + internal const string ClientAttributesProperty = "authority:client_attributes"; internal const string OperatorReasonProperty = "authority:operator_reason"; internal const string OperatorTicketProperty = "authority:operator_ticket"; internal const string OperatorReasonParameterName = "operator_reason"; @@ -51,4 +52,20 @@ internal static class AuthorityOpenIddictConstants internal const string ServiceAccountProperty = "authority:service_account"; internal const string TokenKindProperty = "authority:token_kind"; internal const string ActorChainProperty = "authority:actor_chain"; + internal const string VulnEnvironmentParameterName = "vuln_env"; + internal const string VulnOwnerParameterName = "vuln_owner"; + internal const string VulnBusinessTierParameterName = "vuln_business_tier"; + internal const string VulnEnvironmentProperty = "authority:vuln_env"; + internal const string VulnOwnerProperty = "authority:vuln_owner"; + internal const string VulnBusinessTierProperty = "authority:vuln_business_tier"; + internal const string PolicyReasonParameterName = "policy_reason"; + internal const string PolicyTicketParameterName = "policy_ticket"; + internal const string PolicyDigestParameterName = "policy_digest"; + internal const string PolicyOperationProperty = "authority:policy_operation"; + internal const string PolicyReasonProperty = "authority:policy_reason"; + internal const string PolicyTicketProperty = "authority:policy_ticket"; + internal const string PolicyDigestProperty = "authority:policy_digest"; + internal const string PolicyAuditPropertiesProperty = "authority:policy_audit_properties"; + internal const string PolicyOperationPublishValue = "publish"; + internal const string PolicyOperationPromoteValue = "promote"; } diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/ClientCredentialsHandlers.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/ClientCredentialsHandlers.cs index 687627af..bc019929 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/ClientCredentialsHandlers.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/ClientCredentialsHandlers.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text.Json; +using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using OpenIddict.Abstractions; @@ -26,6 +27,91 @@ using StellaOps.Cryptography.Audit; namespace StellaOps.Authority.OpenIddict.Handlers; +internal static class VulnerabilityAttributeMetadata +{ + internal const string EnvironmentKey = "env"; + internal const string OwnerKey = "owner"; + internal const string BusinessTierKey = "business_tier"; + + internal static readonly IReadOnlyDictionary ClaimTypes = new Dictionary(StringComparer.Ordinal) + { + [EnvironmentKey] = StellaOpsClaimTypes.VulnerabilityEnvironment, + [OwnerKey] = StellaOpsClaimTypes.VulnerabilityOwner, + [BusinessTierKey] = StellaOpsClaimTypes.VulnerabilityBusinessTier + }; + + internal static IReadOnlyDictionary>? NormalizeFilters( + IReadOnlyDictionary>? attributes) + { + if (attributes is null || attributes.Count == 0) + { + return null; + } + + var normalized = new Dictionary>(StringComparer.Ordinal); + + foreach (var (rawKey, rawValues) in attributes) + { + if (string.IsNullOrWhiteSpace(rawKey)) + { + continue; + } + + var normalizedKey = rawKey.Trim().ToLowerInvariant(); + if (!ClaimTypes.ContainsKey(normalizedKey)) + { + continue; + } + + if (rawValues is null || rawValues.Count == 0) + { + continue; + } + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var values = new List(); + var wildcard = false; + + foreach (var rawValue in rawValues) + { + if (string.IsNullOrWhiteSpace(rawValue)) + { + continue; + } + + var trimmed = rawValue.Trim(); + if (trimmed.Equals("*", StringComparison.OrdinalIgnoreCase)) + { + values.Clear(); + values.Add("*"); + wildcard = true; + break; + } + + var lower = trimmed.ToLowerInvariant(); + if (seen.Add(lower)) + { + values.Add(lower); + } + } + + if (values.Count == 0) + { + continue; + } + + normalized[normalizedKey] = values.ToArray(); + + if (wildcard) + { + continue; + } + } + + return normalized.Count == 0 ? null : normalized; + } +} + internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandler { private readonly IAuthorityClientStore clientStore; @@ -41,6 +127,8 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle private readonly StellaOpsAuthorityOptions authorityOptions; private readonly ILogger logger; + private static readonly Regex AttributeValueRegex = new("^[a-z0-9][a-z0-9:_-]{0,127}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); + public ValidateClientCredentialsHandler( IAuthorityClientStore clientStore, IAuthorityIdentityProviderRegistry registry, @@ -327,14 +415,6 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle : normalizedServiceTenant; var maxDelegationTokens = authorityOptions.Delegation.ResolveMaxActiveTokens(targetTenant); - var currentDelegationTokens = await tokenStore.CountActiveDelegationTokensAsync(targetTenant, null, context.CancellationToken).ConfigureAwait(false); - if (currentDelegationTokens >= maxDelegationTokens) - { - context.Reject(OpenIddictConstants.Errors.InvalidRequest, "Delegation token quota exceeded for tenant."); - logger.LogWarning("Client credentials validation failed for {ClientId}: tenant {Tenant} exceeded delegation token quota (limit {Limit}).", document.ClientId, targetTenant, maxDelegationTokens); - return; - } - var accountDelegationTokens = await tokenStore.CountActiveDelegationTokensAsync(targetTenant, serviceAccount.AccountId, context.CancellationToken).ConfigureAwait(false); if (accountDelegationTokens >= maxDelegationTokens) { @@ -343,6 +423,14 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle return; } + var currentDelegationTokens = await tokenStore.CountActiveDelegationTokensAsync(targetTenant, null, context.CancellationToken).ConfigureAwait(false); + if (currentDelegationTokens >= maxDelegationTokens) + { + context.Reject(OpenIddictConstants.Errors.InvalidRequest, "Delegation token quota exceeded for tenant."); + logger.LogWarning("Client credentials validation failed for {ClientId}: tenant {Tenant} exceeded delegation token quota (limit {Limit}).", document.ClientId, targetTenant, maxDelegationTokens); + return; + } + var actorList = new List { document.ClientId }; var actorOverrideRaw = NormalizeMetadata(context.Request.GetParameter(AuthorityOpenIddictConstants.DelegationActorParameterName)?.Value?.ToString()); if (!string.IsNullOrWhiteSpace(actorOverrideRaw)) @@ -355,12 +443,66 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle context.Transaction.Properties[AuthorityOpenIddictConstants.ServiceAccountProperty] = serviceAccount; context.Transaction.Properties[AuthorityOpenIddictConstants.TokenKindProperty] = AuthorityTokenKinds.ServiceAccount; context.Transaction.Properties[AuthorityOpenIddictConstants.ActorChainProperty] = actorList; + + var attributeFilters = VulnerabilityAttributeMetadata.NormalizeFilters(serviceAccount.Attributes); + if (attributeFilters is not null) + { + if (attributeFilters.TryGetValue(VulnerabilityAttributeMetadata.EnvironmentKey, out var envValues) && envValues.Count > 0) + { + activity?.SetTag("authority.vuln_attr_env", string.Join(",", envValues)); + } + + if (attributeFilters.TryGetValue(VulnerabilityAttributeMetadata.OwnerKey, out var ownerValues) && ownerValues.Count > 0) + { + activity?.SetTag("authority.vuln_attr_owner", string.Join(",", ownerValues)); + } + + if (attributeFilters.TryGetValue(VulnerabilityAttributeMetadata.BusinessTierKey, out var tierValues) && tierValues.Count > 0) + { + activity?.SetTag("authority.vuln_attr_business_tier", string.Join(",", tierValues)); + } + } } else { EnsureTenantAssigned(); } + var includesVulnScopes = grantedScopes.Any(scope => scope.StartsWith("vuln:", StringComparison.Ordinal)); + if (includesVulnScopes) + { + if (!TryResolveVulnAttributes( + context, + serviceAccount, + out var vulnerableEnvironment, + out var vulnerableOwner, + out var vulnerableBusinessTier)) + { + return; + } + + if (!string.IsNullOrWhiteSpace(vulnerableEnvironment)) + { + context.Transaction.Properties[AuthorityOpenIddictConstants.VulnEnvironmentProperty] = vulnerableEnvironment; + metadataAccessor.SetTag("authority.vuln_env", vulnerableEnvironment); + activity?.SetTag("authority.vuln_env", vulnerableEnvironment); + } + + if (!string.IsNullOrWhiteSpace(vulnerableOwner)) + { + context.Transaction.Properties[AuthorityOpenIddictConstants.VulnOwnerProperty] = vulnerableOwner; + metadataAccessor.SetTag("authority.vuln_owner", vulnerableOwner); + activity?.SetTag("authority.vuln_owner", vulnerableOwner); + } + + if (!string.IsNullOrWhiteSpace(vulnerableBusinessTier)) + { + context.Transaction.Properties[AuthorityOpenIddictConstants.VulnBusinessTierProperty] = vulnerableBusinessTier; + metadataAccessor.SetTag("authority.vuln_business_tier", vulnerableBusinessTier); + activity?.SetTag("authority.vuln_business_tier", vulnerableBusinessTier); + } + } + bool EnsureTenantAssigned() { @@ -389,6 +531,118 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle static string? NormalizeMetadata(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + bool TryResolveVulnAttributes( + OpenIddictServerEvents.ValidateTokenRequestContext context, + AuthorityServiceAccountDocument? serviceAccount, + out string? environment, + out string? owner, + out string? businessTier) + { + environment = null; + owner = null; + businessTier = null; + + var envParameter = NormalizeMetadata(context.Request.GetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName)?.Value?.ToString()); + if (!ResolveVulnAttribute("env", AuthorityOpenIddictConstants.VulnEnvironmentParameterName, envParameter, serviceAccount, context, out environment)) + { + return false; + } + + var ownerParameter = NormalizeMetadata(context.Request.GetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName)?.Value?.ToString()); + if (!ResolveVulnAttribute("owner", AuthorityOpenIddictConstants.VulnOwnerParameterName, ownerParameter, serviceAccount, context, out owner)) + { + return false; + } + + var tierParameter = NormalizeMetadata(context.Request.GetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName)?.Value?.ToString()); + if (!ResolveVulnAttribute("business_tier", AuthorityOpenIddictConstants.VulnBusinessTierParameterName, tierParameter, serviceAccount, context, out businessTier)) + { + return false; + } + + return true; + } + + bool ResolveVulnAttribute( + string attributeKey, + string parameterName, + string? requestedValue, + AuthorityServiceAccountDocument? serviceAccount, + OpenIddictServerEvents.ValidateTokenRequestContext context, + out string? resolvedValue) + { + resolvedValue = null; + + var clientIdForLog = context.ClientId ?? context.Request.ClientId ?? ""; + var allowed = serviceAccount?.Attributes is { Count: > 0 } && + serviceAccount.Attributes.TryGetValue(attributeKey, out var attributeValues) + ? attributeValues + : null; + + var wildcardAllowed = allowed is { Count: > 0 } && allowed.Any(value => string.Equals(value, "*", StringComparison.Ordinal)); + + if (!string.IsNullOrWhiteSpace(requestedValue)) + { + var normalizedValue = requestedValue.ToLowerInvariant(); + + if (normalizedValue.Equals("*", StringComparison.Ordinal)) + { + if (!wildcardAllowed) + { + context.Reject(OpenIddictConstants.Errors.InvalidRequest, $"{parameterName} cannot be a wildcard for this client."); + logger.LogWarning("Client credentials validation failed for {ClientId}: wildcard value not permitted for {Parameter}.", clientIdForLog, parameterName); + return false; + } + + resolvedValue = "*"; + return true; + } + + if (!AttributeValueRegex.IsMatch(normalizedValue)) + { + context.Reject(OpenIddictConstants.Errors.InvalidRequest, $"{parameterName} must start with a letter or digit and may contain lowercase letters, digits, :, _, or - (max 128 characters)."); + logger.LogWarning("Client credentials validation failed for {ClientId}: invalid characters in {Parameter}.", clientIdForLog, parameterName); + return false; + } + + if (allowed is { Count: > 0 } && !wildcardAllowed) + { + if (!allowed.Any(value => string.Equals(value, normalizedValue, StringComparison.OrdinalIgnoreCase))) + { + context.Reject(OpenIddictConstants.Errors.InvalidRequest, $"{parameterName} must match one of the configured values: {string.Join(", ", allowed)}."); + logger.LogWarning("Client credentials validation failed for {ClientId}: value {Value} for {Parameter} not allowed for service account {ServiceAccount}.", clientIdForLog, normalizedValue, parameterName, serviceAccount?.AccountId ?? ""); + return false; + } + } + + resolvedValue = normalizedValue; + return true; + } + + if (allowed is not { Count: > 0 }) + { + context.Reject(OpenIddictConstants.Errors.InvalidRequest, $"{parameterName} is required when requesting vulnerability scopes."); + logger.LogWarning("Client credentials validation failed for {ClientId}: missing required parameter {Parameter}.", clientIdForLog, parameterName); + return false; + } + + if (wildcardAllowed) + { + resolvedValue = "*"; + return true; + } + + if (allowed.Count == 1) + { + resolvedValue = allowed[0]; + return true; + } + + context.Reject(OpenIddictConstants.Errors.InvalidRequest, $"{parameterName} must be supplied when multiple values are configured for the service account."); + logger.LogWarning("Client credentials validation failed for {ClientId}: ambiguous {Parameter} configuration for service account {ServiceAccount}.", clientIdForLog, parameterName, serviceAccount?.AccountId ?? ""); + return false; + } + var hasGraphRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.GraphRead) >= 0; var hasGraphWrite = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.GraphWrite) >= 0; var hasGraphExport = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.GraphExport) >= 0; @@ -410,7 +664,14 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle var advisoryAiScopesRequested = hasAdvisoryAiView || hasAdvisoryAiOperate || hasAdvisoryAiAdmin; var hasVexIngest = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VexIngest) >= 0; var hasVexRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VexRead) >= 0; +#pragma warning disable CS0618 // legacy vuln:read support var hasVulnRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VulnRead) >= 0; +#pragma warning restore CS0618 + var hasVulnView = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VulnView) >= 0; + var hasVulnInvestigate = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VulnInvestigate) >= 0; + var hasVulnOperate = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VulnOperate) >= 0; + var hasVulnAudit = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VulnAudit) >= 0; + var vulnScopesRequested = hasVulnRead || hasVulnView || hasVulnInvestigate || hasVulnOperate || hasVulnAudit; var hasObservabilityIncident = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.ObservabilityIncident) >= 0; var hasSignalsRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.SignalsRead) >= 0; var hasSignalsWrite = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.SignalsWrite) >= 0; @@ -735,9 +996,23 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle return; } - if (hasVulnRead && !EnsureTenantAssigned()) + if (vulnScopesRequested && !EnsureTenantAssigned()) { - context.Transaction.Properties[AuthorityOpenIddictConstants.AuditInvalidScopeProperty] = StellaOpsScopes.VulnRead; + var scopeForAudit = hasVulnOperate + ? StellaOpsScopes.VulnOperate + : hasVulnInvestigate + ? StellaOpsScopes.VulnInvestigate + : hasVulnAudit + ? StellaOpsScopes.VulnAudit + : hasVulnView + ? StellaOpsScopes.VulnView +#pragma warning disable CS0618 + : hasVulnRead + ? StellaOpsScopes.VulnRead +#pragma warning restore CS0618 + : StellaOpsScopes.VulnView; + + context.Transaction.Properties[AuthorityOpenIddictConstants.AuditInvalidScopeProperty] = scopeForAudit; context.Reject(OpenIddictConstants.Errors.InvalidClient, "Vuln Explorer scopes require a tenant assignment."); logger.LogWarning( "Client credentials validation failed for {ClientId}: vuln scopes require tenant assignment.", @@ -1001,6 +1276,39 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle }); } + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnEnvironmentProperty, out var auditEnvObj) && + auditEnvObj is string auditEnv && + !string.IsNullOrWhiteSpace(auditEnv)) + { + extraProperties.Add(new AuthEventProperty + { + Name = "vuln.attr.env", + Value = ClassifiedString.Public(auditEnv) + }); + } + + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnOwnerProperty, out var auditOwnerObj) && + auditOwnerObj is string auditOwner && + !string.IsNullOrWhiteSpace(auditOwner)) + { + extraProperties.Add(new AuthEventProperty + { + Name = "vuln.attr.owner", + Value = ClassifiedString.Public(auditOwner) + }); + } + + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnBusinessTierProperty, out var auditTierObj) && + auditTierObj is string auditTier && + !string.IsNullOrWhiteSpace(auditTier)) + { + extraProperties.Add(new AuthEventProperty + { + Name = "vuln.attr.business_tier", + Value = ClassifiedString.Public(auditTier) + }); + } + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ActorChainProperty, out var auditActorObj) && auditActorObj is IReadOnlyCollection auditActorChain && auditActorChain.Count > 0) { @@ -1220,11 +1528,32 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler< identity.AddClaim(new Claim(StellaOpsClaimTypes.ServiceAccount, serviceAccount.AccountId)); } + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnEnvironmentProperty, out var vulnEnvObj) && + vulnEnvObj is string vulnEnvironment && + !string.IsNullOrWhiteSpace(vulnEnvironment)) + { + identity.AddClaim(new Claim(StellaOpsClaimTypes.VulnerabilityEnvironment, vulnEnvironment)); + } + + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnOwnerProperty, out var vulnOwnerObj) && + vulnOwnerObj is string vulnOwner && + !string.IsNullOrWhiteSpace(vulnOwner)) + { + identity.AddClaim(new Claim(StellaOpsClaimTypes.VulnerabilityOwner, vulnOwner)); + } + + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnBusinessTierProperty, out var vulnTierObj) && + vulnTierObj is string vulnBusinessTier && + !string.IsNullOrWhiteSpace(vulnBusinessTier)) + { + identity.AddClaim(new Claim(StellaOpsClaimTypes.VulnerabilityBusinessTier, vulnBusinessTier)); + } + metadataAccessor.SetSubjectId(subjectValue); if (serviceAccount is not null) { var actors = actorChain.Count > 0 ? actorChain : new[] { document.ClientId }; - identity.SetClaim("act", BuildActorClaim(actors)); + identity.SetClaim("act", ClientCredentialHandlerHelpers.BuildActorClaim(actors)); } activity?.SetTag("authority.client_id", document.ClientId); @@ -1289,6 +1618,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler< metadataAccessor.SetProject(project); activity?.SetTag("authority.project", project); + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientAttributesProperty, out var attributeValue) && + attributeValue is IReadOnlyDictionary> attributeFilters && + attributeFilters.Count > 0) + { + ApplyAttributeClaims(identity, attributeFilters); + } + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.OperatorReasonProperty, out var operatorReasonValue) && operatorReasonValue is string operatorReasonValueString && !string.IsNullOrWhiteSpace(operatorReasonValueString)) @@ -1551,6 +1887,27 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler< record.Project = projectValue; } + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnEnvironmentProperty, out var tokenEnvObj) && + tokenEnvObj is string tokenEnvironment && + !string.IsNullOrWhiteSpace(tokenEnvironment)) + { + record.VulnerabilityEnvironment = tokenEnvironment; + } + + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnOwnerProperty, out var tokenOwnerObj) && + tokenOwnerObj is string tokenOwner && + !string.IsNullOrWhiteSpace(tokenOwner)) + { + record.VulnerabilityOwner = tokenOwner; + } + + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.VulnBusinessTierProperty, out var tokenTierObj) && + tokenTierObj is string tokenBusinessTier && + !string.IsNullOrWhiteSpace(tokenBusinessTier)) + { + record.VulnerabilityBusinessTier = tokenBusinessTier; + } + if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.DpopConsumedNonceProperty, out var nonceObj) && nonceObj is string nonce && !string.IsNullOrWhiteSpace(nonce)) @@ -1637,6 +1994,33 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler< } } + private static void ApplyAttributeClaims( + ClaimsIdentity identity, + IReadOnlyDictionary> attributeFilters) + { + foreach (var (key, values) in attributeFilters) + { + if (!VulnerabilityAttributeMetadata.ClaimTypes.TryGetValue(key, out var claimType)) + { + continue; + } + + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + if (identity.HasClaim(claim => claim.Type == claimType && string.Equals(claim.Value, value, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + identity.AddClaim(new Claim(claimType, value)); + } + } + } } internal static class ClientCredentialHandlerHelpers @@ -1727,7 +2111,7 @@ internal static class ClientCredentialHandlerHelpers return null; } - private static string BuildActorClaim(IReadOnlyCollection actors) + internal static string BuildActorClaim(IReadOnlyCollection actors) { if (actors is null || actors.Count == 0) { diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/DiscoveryHandlers.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/DiscoveryHandlers.cs index 1deef479..0587d0f6 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/DiscoveryHandlers.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/DiscoveryHandlers.cs @@ -47,6 +47,14 @@ internal sealed class ConfigureAuthorityDiscoveryHandler : IOpenIddictServerHand StellaOpsScopes.NotifyAdmin }; + context.Metadata["stellaops_vuln_scopes_supported"] = new[] + { + StellaOpsScopes.VulnView, + StellaOpsScopes.VulnInvestigate, + StellaOpsScopes.VulnOperate, + StellaOpsScopes.VulnAudit + }; + context.Metadata["stellaops_observability_scopes_supported"] = new[] { StellaOpsScopes.ObservabilityRead, diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/TokenPersistenceHandlers.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/TokenPersistenceHandlers.cs index 7d458cb5..35b643a9 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/TokenPersistenceHandlers.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/Handlers/TokenPersistenceHandlers.cs @@ -117,6 +117,43 @@ internal sealed class PersistTokensHandler : IOpenIddictServerHandler claim.Value) + .FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(vulnerabilityEnvironment)) + { + document.VulnerabilityEnvironment = vulnerabilityEnvironment.Trim(); + } + + var vulnerabilityOwner = principal.FindAll(StellaOpsClaimTypes.VulnerabilityOwner) + .Select(claim => claim.Value) + .FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(vulnerabilityOwner)) + { + document.VulnerabilityOwner = vulnerabilityOwner.Trim(); + } + + var vulnerabilityBusinessTier = principal.FindAll(StellaOpsClaimTypes.VulnerabilityBusinessTier) + .Select(claim => claim.Value) + .FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(vulnerabilityBusinessTier)) + { + document.VulnerabilityBusinessTier = vulnerabilityBusinessTier.Trim(); + } + + var actorChain = ExtractActorChain(principal); + if (actorChain is not null) + { + document.ActorChain = actorChain; + } + var incidentReason = principal.GetClaim(StellaOpsClaimTypes.IncidentReason); if (!string.IsNullOrWhiteSpace(incidentReason)) { @@ -167,13 +204,55 @@ internal sealed class PersistTokensHandler : IOpenIddictServerHandler ExtractScopes(ClaimsPrincipal principal) - => principal.GetScopes() - .Where(scope => !string.IsNullOrWhiteSpace(scope)) - .Select(scope => scope.Trim()) - .Distinct(StringComparer.Ordinal) - .OrderBy(scope => scope, StringComparer.Ordinal) - .ToList(); + private static List ExtractScopes(ClaimsPrincipal principal) + => principal.GetScopes() + .Where(scope => !string.IsNullOrWhiteSpace(scope)) + .Select(scope => scope.Trim()) + .Distinct(StringComparer.Ordinal) + .OrderBy(scope => scope, StringComparer.Ordinal) + .ToList(); + + private static List? ExtractActorChain(ClaimsPrincipal principal) + { + var claim = principal.GetClaim("act"); + if (string.IsNullOrWhiteSpace(claim)) + { + return null; + } + + try + { + using var document = JsonDocument.Parse(claim); + var element = document.RootElement; + var actors = new List(); + + while (element.ValueKind == JsonValueKind.Object) + { + if (element.TryGetProperty("sub", out var subjectElement)) + { + var subject = subjectElement.GetString(); + if (!string.IsNullOrWhiteSpace(subject)) + { + actors.Add(subject); + } + } + + if (element.TryGetProperty("act", out var nextElement) && nextElement.ValueKind == JsonValueKind.Object) + { + element = nextElement; + continue; + } + + break; + } + + return actors.Count == 0 ? null : actors; + } + catch (JsonException) + { + return null; + } + } private static DateTimeOffset? TryGetExpiration(ClaimsPrincipal principal) { diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/TokenRequestTamperInspector.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/TokenRequestTamperInspector.cs index d1134a5f..53364549 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/TokenRequestTamperInspector.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/OpenIddict/TokenRequestTamperInspector.cs @@ -54,7 +54,10 @@ internal static class TokenRequestTamperInspector AuthorityOpenIddictConstants.ExportAdminReasonParameterName, AuthorityOpenIddictConstants.ExportAdminTicketParameterName, AuthorityOpenIddictConstants.QuotaReasonParameterName, - AuthorityOpenIddictConstants.QuotaTicketParameterName + AuthorityOpenIddictConstants.QuotaTicketParameterName, + AuthorityOpenIddictConstants.VulnEnvironmentParameterName, + AuthorityOpenIddictConstants.VulnOwnerParameterName, + AuthorityOpenIddictConstants.VulnBusinessTierParameterName }; internal static IReadOnlyList GetUnexpectedPasswordGrantParameters(OpenIddictRequest request) diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Permalinks/VulnPermalinkService.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Permalinks/VulnPermalinkService.cs index fd7ec4fd..103c7431 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/Permalinks/VulnPermalinkService.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Permalinks/VulnPermalinkService.cs @@ -123,18 +123,26 @@ internal sealed class VulnPermalinkService signing.Provider); var signer = resolution.Signer; - var payload = new VulnPermalinkPayload( - Subject: "vuln:permalink", - Audience: "stellaops:vuln-explorer", - Type: resourceKind, - Tenant: tenant, - Environment: string.IsNullOrWhiteSpace(request.Environment) ? null : request.Environment.Trim(), - Scopes: new[] { StellaOpsScopes.VulnRead }, - IssuedAt: issuedAt.ToUnixTimeSeconds(), - NotBefore: issuedAt.ToUnixTimeSeconds(), - ExpiresAt: expiresAt.ToUnixTimeSeconds(), - TokenId: Guid.NewGuid().ToString("N"), - Resource: new VulnPermalinkResource(resourceKind, stateElement)); + var scopes = new[] + { + StellaOpsScopes.VulnView, +#pragma warning disable CS0618 // legacy scope retained for backward compatibility + StellaOpsScopes.VulnRead +#pragma warning restore CS0618 + }; + + var payload = new VulnPermalinkPayload( + Subject: "vuln:permalink", + Audience: "stellaops:vuln-explorer", + Type: resourceKind, + Tenant: tenant, + Environment: string.IsNullOrWhiteSpace(request.Environment) ? null : request.Environment.Trim(), + Scopes: scopes, + IssuedAt: issuedAt.ToUnixTimeSeconds(), + NotBefore: issuedAt.ToUnixTimeSeconds(), + ExpiresAt: expiresAt.ToUnixTimeSeconds(), + TokenId: Guid.NewGuid().ToString("N"), + Resource: new VulnPermalinkResource(resourceKind, stateElement)); var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload, PayloadSerializerOptions); var header = new Dictionary @@ -155,12 +163,12 @@ internal sealed class VulnPermalinkService logger.LogDebug("Issued Vuln Explorer permalink for tenant {Tenant} with resource kind {Resource}.", tenant, resourceKind); - return new VulnPermalinkResponse( - Token: token, - IssuedAt: issuedAt, - ExpiresAt: expiresAt, - Scopes: new[] { StellaOpsScopes.VulnRead }); - } + return new VulnPermalinkResponse( + Token: token, + IssuedAt: issuedAt, + ExpiresAt: expiresAt, + Scopes: scopes); + } private sealed record VulnPermalinkPayload( [property: JsonPropertyName("sub")] string Subject, diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs index 8ab67139..31c85996 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs @@ -1,389 +1,398 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Security.Claims; -using System.Globalization; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.AspNetCore.RateLimiting; -using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Net.Http.Headers; -using OpenIddict.Abstractions; -using OpenIddict.Server; -using OpenIddict.Server.AspNetCore; -using MongoDB.Driver; -using Serilog; -using Serilog.Events; -using StellaOps.Authority; -using StellaOps.Authority.Airgap; -using StellaOps.Authority.Audit; -using StellaOps.Authority.AdvisoryAi; -using StellaOps.Authority.Notifications; -using StellaOps.Authority.Notifications.Ack; -using StellaOps.Authority.Plugins.Abstractions; -using StellaOps.Authority.Plugins; -using StellaOps.Authority.Bootstrap; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Claims; +using System.Globalization; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Net.Http.Headers; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using MongoDB.Driver; +using Serilog; +using Serilog.Events; +using StellaOps.Authority; +using StellaOps.Authority.Airgap; +using StellaOps.Authority.Audit; +using StellaOps.Authority.AdvisoryAi; +using StellaOps.Authority.Notifications; +using StellaOps.Authority.Notifications.Ack; +using StellaOps.Authority.Plugins.Abstractions; +using StellaOps.Authority.Plugins; +using StellaOps.Authority.Bootstrap; using StellaOps.Authority.Storage.Mongo.Extensions; using StellaOps.Authority.Storage.Mongo.Initialization; using StellaOps.Authority.Storage.Mongo.Stores; using StellaOps.Authority.Storage.Mongo.Sessions; -using StellaOps.Authority.RateLimiting; -using StellaOps.Configuration; -using StellaOps.Plugin.DependencyInjection; -using StellaOps.Plugin.Hosting; -using StellaOps.Authority.OpenIddict.Handlers; -using StellaOps.Authority.Observability; -using System.Linq; -using StellaOps.Cryptography.Audit; -using StellaOps.Cryptography.DependencyInjection; -using StellaOps.Authority.Permalinks; -using StellaOps.Authority.Revocation; -using System.Security.Cryptography; -using System.Text; -using StellaOps.Authority.Signing; -using StellaOps.Cryptography; -using StellaOps.Cryptography.Kms; +using StellaOps.Authority.RateLimiting; +using StellaOps.Configuration; +using StellaOps.Plugin.DependencyInjection; +using StellaOps.Plugin.Hosting; +using StellaOps.Authority.OpenIddict.Handlers; +using StellaOps.Authority.Observability; +using System.Linq; +using StellaOps.Cryptography.Audit; +using StellaOps.Cryptography.DependencyInjection; +using StellaOps.AirGap.Policy; +using StellaOps.Authority.Permalinks; +using StellaOps.Authority.Revocation; +using System.Security.Cryptography; +using System.Text; +using StellaOps.Authority.Signing; +using StellaOps.Cryptography; +using StellaOps.Cryptography.Kms; using StellaOps.Authority.Storage.Mongo.Documents; -using StellaOps.Authority.Security; -using StellaOps.Authority.OpenApi; -using StellaOps.Auth.Abstractions; -using StellaOps.Auth.ServerIntegration; -#if STELLAOPS_AUTH_SECURITY -using StellaOps.Auth.Security.Dpop; -using StackExchange.Redis; -#endif - -var builder = WebApplication.CreateBuilder(args); - -Activity.DefaultIdFormat = ActivityIdFormat.W3C; -Activity.ForceDefaultIdFormat = true; - -AuthorityTelemetryConfiguration.Configure(builder); - -var authorityConfiguration = StellaOpsAuthorityConfiguration.Build(options => -{ - options.BasePath = builder.Environment.ContentRootPath; - options.EnvironmentPrefix = "STELLAOPS_AUTHORITY_"; - options.ConfigureBuilder = configurationBuilder => - { - var contentRoot = builder.Environment.ContentRootPath; - foreach (var relative in new[] - { - "../etc/authority.yaml", - "../etc/authority.local.yaml", - "authority.yaml", - "authority.local.yaml" - }) - { - var path = Path.Combine(contentRoot, relative); - configurationBuilder.AddYamlFile(path, optional: true); - } - }; -}); - -builder.WebHost.ConfigureKestrel(options => -{ - options.ConfigureHttpsDefaults(https => - { - https.ClientCertificateMode = ClientCertificateMode.AllowCertificate; - https.CheckCertificateRevocation = true; - }); -}); - -builder.Configuration.AddConfiguration(authorityConfiguration.Configuration); - -builder.Host.UseSerilog((context, _, loggerConfiguration) => -{ - loggerConfiguration - .ReadFrom.Configuration(context.Configuration) - .Enrich.FromLogContext() - .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) - .WriteTo.Console(); -}); - -var authorityOptions = authorityConfiguration.Options; -var issuerUri = authorityOptions.Issuer; -if (issuerUri is null) -{ - var issuerValue = builder.Configuration["Authority:Issuer"]; - if (string.IsNullOrWhiteSpace(issuerValue)) - { - throw new InvalidOperationException("Authority issuer configuration is required."); - } - - issuerUri = new Uri(issuerValue, UriKind.Absolute); -} - -authorityOptions.Issuer = issuerUri; -builder.Services.AddSingleton(authorityOptions); -builder.Services.AddSingleton>(Options.Create(authorityOptions)); -builder.Services.AddHttpContextAccessor(); -builder.Services.TryAddSingleton(_ => TimeProvider.System); -builder.Services.AddMemoryCache(); -builder.Services.TryAddSingleton(); -builder.Services.TryAddSingleton(); -builder.Services.AddSingleton(); -builder.Services.TryAddSingleton(); -builder.Services.AddSingleton(); - -#if STELLAOPS_AUTH_SECURITY -var senderConstraints = authorityOptions.Security.SenderConstraints; - -builder.Services.AddOptions() - .Configure(options => - { - options.ProofLifetime = senderConstraints.Dpop.ProofLifetime; - options.AllowedClockSkew = senderConstraints.Dpop.AllowedClockSkew; - options.ReplayWindow = senderConstraints.Dpop.ReplayWindow; - - options.AllowedAlgorithms.Clear(); - foreach (var algorithm in senderConstraints.Dpop.NormalizedAlgorithms) - { - options.AllowedAlgorithms.Add(algorithm); - } - }) - .PostConfigure(static options => options.Validate()); - -builder.Services.TryAddSingleton(provider => new InMemoryDpopReplayCache(provider.GetService())); -builder.Services.TryAddSingleton(); -if (string.Equals(senderConstraints.Dpop.Nonce.Store, "redis", StringComparison.OrdinalIgnoreCase)) -{ - builder.Services.TryAddSingleton(_ => - ConnectionMultiplexer.Connect(senderConstraints.Dpop.Nonce.RedisConnectionString!)); - - builder.Services.TryAddSingleton(provider => - { - var multiplexer = provider.GetRequiredService(); - var timeProvider = provider.GetService(); - return new RedisDpopNonceStore(multiplexer, timeProvider); - }); -} -else -{ - builder.Services.TryAddSingleton(provider => - { - var timeProvider = provider.GetService(); - var nonceLogger = provider.GetService>(); - return new InMemoryDpopNonceStore(timeProvider, nonceLogger); - }); -} - -builder.Services.AddScoped(); -#endif - -builder.Services.AddRateLimiter(rateLimiterOptions => -{ - AuthorityRateLimiter.Configure(rateLimiterOptions, authorityOptions); -}); - -var requiresKms = string.Equals(authorityOptions.Signing.KeySource, "kms", StringComparison.OrdinalIgnoreCase) - || authorityOptions.Signing.AdditionalKeys.Any(k => string.Equals(k.Source, "kms", StringComparison.OrdinalIgnoreCase)); - -if (requiresKms) -{ - if (string.IsNullOrWhiteSpace(authorityOptions.Signing.KeyPassphrase)) - { - throw new InvalidOperationException("Authority signing with source 'kms' requires signing.keyPassphrase to be configured."); - } - - var kmsRoot = Path.Combine(builder.Environment.ContentRootPath, "kms"); - builder.Services.AddFileKms(options => - { - options.RootPath = kmsRoot; - options.Password = authorityOptions.Signing.KeyPassphrase!; - options.Algorithm = authorityOptions.Signing.Algorithm; - }); - - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); -} - -builder.Services.AddStellaOpsCrypto(); -builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -AuthorityPluginContext[] pluginContexts = AuthorityPluginConfigurationLoader - .Load(authorityOptions, builder.Environment.ContentRootPath) - .ToArray(); - -builder.Services.AddSingleton>(pluginContexts); -builder.Services.AddSingleton(_ => new AuthorityPluginRegistry(pluginContexts)); - -var pluginHostOptions = BuildPluginHostOptions(authorityOptions, builder.Environment.ContentRootPath); -builder.Services.AddSingleton(pluginHostOptions); -builder.Services.RegisterPluginRoutines(authorityConfiguration.Configuration, pluginHostOptions); - -builder.Services.AddAuthorityMongoStorage(storageOptions => -{ - storageOptions.ConnectionString = authorityOptions.Storage.ConnectionString; - storageOptions.DatabaseName = authorityOptions.Storage.DatabaseName; - storageOptions.CommandTimeout = authorityOptions.Storage.CommandTimeout; -}); - -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddHostedService(); - -var pluginRegistrationSummary = AuthorityPluginLoader.RegisterPlugins( - builder.Services, - authorityConfiguration.Configuration, - pluginHostOptions, - pluginContexts, - NullLogger.Instance); - -builder.Services.AddSingleton(pluginRegistrationSummary); - -builder.Services.AddRouting(options => options.LowercaseUrls = true); -builder.Services.AddProblemDetails(); -builder.Services.AddAuthentication(); -builder.Services.AddAuthorization(); - -builder.Services.AddStellaOpsScopeHandler(); - -builder.Services.TryAddSingleton(); - -builder.Services.AddOptions() - .Configure(options => - { - options.Authority = issuerUri.ToString(); - options.RequireHttpsMetadata = !issuerUri.IsLoopback; - }) - .PostConfigure(static options => options.Validate()); - -builder.Services.AddOpenIddict() - .AddServer(options => - { - options.SetIssuer(issuerUri); - options.SetTokenEndpointUris("/token"); - options.SetAuthorizationEndpointUris("/authorize"); - options.SetIntrospectionEndpointUris("/introspect"); - options.SetRevocationEndpointUris("/revoke"); - options.SetJsonWebKeySetEndpointUris("/jwks"); - - options.AllowPasswordFlow(); - options.AllowClientCredentialsFlow(); - options.AllowRefreshTokenFlow(); - - options.SetAccessTokenLifetime(authorityOptions.AccessTokenLifetime); - options.SetRefreshTokenLifetime(authorityOptions.RefreshTokenLifetime); - options.SetIdentityTokenLifetime(authorityOptions.IdentityTokenLifetime); - options.SetAuthorizationCodeLifetime(authorityOptions.AuthorizationCodeLifetime); - options.SetDeviceCodeLifetime(authorityOptions.DeviceCodeLifetime); - - options.DisableAccessTokenEncryption(); - options.DisableTokenStorage(); - options.DisableAuthorizationStorage(); - - options.RegisterScopes( - new[] - { - OpenIddictConstants.Scopes.OpenId, - OpenIddictConstants.Scopes.Email, - OpenIddictConstants.Scopes.Profile, - OpenIddictConstants.Scopes.OfflineAccess - } - .Concat(StellaOpsScopes.All) - .Distinct(StringComparer.Ordinal) - .ToArray()); - - options.AddEphemeralEncryptionKey() - .AddEphemeralSigningKey(); - - var aspNetCoreBuilder = options.UseAspNetCore() - .EnableAuthorizationEndpointPassthrough() - .EnableTokenEndpointPassthrough(); - - if (builder.Environment.IsDevelopment()) - { - aspNetCoreBuilder.DisableTransportSecurityRequirement(); - } - -#if STELLAOPS_AUTH_SECURITY - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); -#endif - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - - options.AddEventHandler(descriptor => - { - descriptor.UseScopedHandler(); - }); - }); - -builder.Services.Configure(options => -{ - options.DisableSlidingRefreshTokenExpiration = false; - options.DisableRollingRefreshTokens = false; -}); - -var app = builder.Build(); - +using StellaOps.Authority.Security; +using StellaOps.Authority.OpenApi; +using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration; +using StellaOps.Authority.Vulnerability.Workflow; +using StellaOps.Authority.Vulnerability.Attachments; +#if STELLAOPS_AUTH_SECURITY +using StellaOps.Auth.Security.Dpop; +using StackExchange.Redis; +#endif + +var builder = WebApplication.CreateBuilder(args); + +Activity.DefaultIdFormat = ActivityIdFormat.W3C; +Activity.ForceDefaultIdFormat = true; + +AuthorityTelemetryConfiguration.Configure(builder); + +var authorityConfiguration = StellaOpsAuthorityConfiguration.Build(options => +{ + options.BasePath = builder.Environment.ContentRootPath; + options.EnvironmentPrefix = "STELLAOPS_AUTHORITY_"; + options.ConfigureBuilder = configurationBuilder => + { + var contentRoot = builder.Environment.ContentRootPath; + foreach (var relative in new[] + { + "../etc/authority.yaml", + "../etc/authority.local.yaml", + "authority.yaml", + "authority.local.yaml" + }) + { + var path = Path.Combine(contentRoot, relative); + configurationBuilder.AddYamlFile(path, optional: true); + } + }; +}); + +builder.WebHost.ConfigureKestrel(options => +{ + options.ConfigureHttpsDefaults(https => + { + https.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + https.CheckCertificateRevocation = true; + }); +}); + +builder.Configuration.AddConfiguration(authorityConfiguration.Configuration); + +builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap"); + +builder.Host.UseSerilog((context, _, loggerConfiguration) => +{ + loggerConfiguration + .ReadFrom.Configuration(context.Configuration) + .Enrich.FromLogContext() + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) + .WriteTo.Console(); +}); + +var authorityOptions = authorityConfiguration.Options; +var issuerUri = authorityOptions.Issuer; +if (issuerUri is null) +{ + var issuerValue = builder.Configuration["Authority:Issuer"]; + if (string.IsNullOrWhiteSpace(issuerValue)) + { + throw new InvalidOperationException("Authority issuer configuration is required."); + } + + issuerUri = new Uri(issuerValue, UriKind.Absolute); +} + +authorityOptions.Issuer = issuerUri; +builder.Services.AddSingleton(authorityOptions); +builder.Services.AddSingleton>(Options.Create(authorityOptions)); +builder.Services.AddHttpContextAccessor(); +builder.Services.TryAddSingleton(_ => TimeProvider.System); +builder.Services.AddMemoryCache(); +builder.Services.TryAddSingleton(); +builder.Services.TryAddSingleton(); +builder.Services.AddSingleton(); +builder.Services.TryAddSingleton(); +builder.Services.AddSingleton(); + +#if STELLAOPS_AUTH_SECURITY +var senderConstraints = authorityOptions.Security.SenderConstraints; + +builder.Services.AddOptions() + .Configure(options => + { + options.ProofLifetime = senderConstraints.Dpop.ProofLifetime; + options.AllowedClockSkew = senderConstraints.Dpop.AllowedClockSkew; + options.ReplayWindow = senderConstraints.Dpop.ReplayWindow; + + options.AllowedAlgorithms.Clear(); + foreach (var algorithm in senderConstraints.Dpop.NormalizedAlgorithms) + { + options.AllowedAlgorithms.Add(algorithm); + } + }) + .PostConfigure(static options => options.Validate()); + +builder.Services.TryAddSingleton(provider => new InMemoryDpopReplayCache(provider.GetService())); +builder.Services.TryAddSingleton(); +if (string.Equals(senderConstraints.Dpop.Nonce.Store, "redis", StringComparison.OrdinalIgnoreCase)) +{ + builder.Services.TryAddSingleton(_ => + ConnectionMultiplexer.Connect(senderConstraints.Dpop.Nonce.RedisConnectionString!)); + + builder.Services.TryAddSingleton(provider => + { + var multiplexer = provider.GetRequiredService(); + var timeProvider = provider.GetService(); + return new RedisDpopNonceStore(multiplexer, timeProvider); + }); +} +else +{ + builder.Services.TryAddSingleton(provider => + { + var timeProvider = provider.GetService(); + var nonceLogger = provider.GetService>(); + return new InMemoryDpopNonceStore(timeProvider, nonceLogger); + }); +} + +builder.Services.AddScoped(); +#endif + +builder.Services.AddRateLimiter(rateLimiterOptions => +{ + AuthorityRateLimiter.Configure(rateLimiterOptions, authorityOptions); +}); + +var requiresKms = string.Equals(authorityOptions.Signing.KeySource, "kms", StringComparison.OrdinalIgnoreCase) + || authorityOptions.Signing.AdditionalKeys.Any(k => string.Equals(k.Source, "kms", StringComparison.OrdinalIgnoreCase)); + +if (requiresKms) +{ + if (string.IsNullOrWhiteSpace(authorityOptions.Signing.KeyPassphrase)) + { + throw new InvalidOperationException("Authority signing with source 'kms' requires signing.keyPassphrase to be configured."); + } + + var kmsRoot = Path.Combine(builder.Environment.ContentRootPath, "kms"); + builder.Services.AddFileKms(options => + { + options.RootPath = kmsRoot; + options.Password = authorityOptions.Signing.KeyPassphrase!; + options.Algorithm = authorityOptions.Signing.Algorithm; + }); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); +} + +builder.Services.AddStellaOpsCrypto(); +builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +AuthorityPluginContext[] pluginContexts = AuthorityPluginConfigurationLoader + .Load(authorityOptions, builder.Environment.ContentRootPath) + .ToArray(); + +builder.Services.AddSingleton>(pluginContexts); +builder.Services.AddSingleton(_ => new AuthorityPluginRegistry(pluginContexts)); + +var pluginHostOptions = BuildPluginHostOptions(authorityOptions, builder.Environment.ContentRootPath); +builder.Services.AddSingleton(pluginHostOptions); +builder.Services.RegisterPluginRoutines(authorityConfiguration.Configuration, pluginHostOptions); + +builder.Services.AddAuthorityMongoStorage(storageOptions => +{ + storageOptions.ConnectionString = authorityOptions.Storage.ConnectionString; + storageOptions.DatabaseName = authorityOptions.Storage.DatabaseName; + storageOptions.CommandTimeout = authorityOptions.Storage.CommandTimeout; +}); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); + +var pluginRegistrationSummary = AuthorityPluginLoader.RegisterPlugins( + builder.Services, + authorityConfiguration.Configuration, + pluginHostOptions, + pluginContexts, + NullLogger.Instance); + +builder.Services.AddSingleton(pluginRegistrationSummary); + +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddProblemDetails(); +builder.Services.AddAuthentication(); +builder.Services.AddAuthorization(); + +builder.Services.AddStellaOpsScopeHandler(); + +builder.Services.TryAddSingleton(); + +builder.Services.AddOptions() + .Configure(options => + { + options.Authority = issuerUri.ToString(); + options.RequireHttpsMetadata = !issuerUri.IsLoopback; + }) + .PostConfigure(static options => options.Validate()); + +builder.Services.AddOpenIddict() + .AddServer(options => + { + options.SetIssuer(issuerUri); + options.SetTokenEndpointUris("/token"); + options.SetAuthorizationEndpointUris("/authorize"); + options.SetIntrospectionEndpointUris("/introspect"); + options.SetRevocationEndpointUris("/revoke"); + options.SetJsonWebKeySetEndpointUris("/jwks"); + + options.AllowPasswordFlow(); + options.AllowClientCredentialsFlow(); + options.AllowRefreshTokenFlow(); + + options.SetAccessTokenLifetime(authorityOptions.AccessTokenLifetime); + options.SetRefreshTokenLifetime(authorityOptions.RefreshTokenLifetime); + options.SetIdentityTokenLifetime(authorityOptions.IdentityTokenLifetime); + options.SetAuthorizationCodeLifetime(authorityOptions.AuthorizationCodeLifetime); + options.SetDeviceCodeLifetime(authorityOptions.DeviceCodeLifetime); + + options.DisableAccessTokenEncryption(); + options.DisableTokenStorage(); + options.DisableAuthorizationStorage(); + + options.RegisterScopes( + new[] + { + OpenIddictConstants.Scopes.OpenId, + OpenIddictConstants.Scopes.Email, + OpenIddictConstants.Scopes.Profile, + OpenIddictConstants.Scopes.OfflineAccess + } + .Concat(StellaOpsScopes.All) + .Distinct(StringComparer.Ordinal) + .ToArray()); + + options.AddEphemeralEncryptionKey() + .AddEphemeralSigningKey(); + + var aspNetCoreBuilder = options.UseAspNetCore() + .EnableAuthorizationEndpointPassthrough() + .EnableTokenEndpointPassthrough(); + + if (builder.Environment.IsDevelopment()) + { + aspNetCoreBuilder.DisableTransportSecurityRequirement(); + } + +#if STELLAOPS_AUTH_SECURITY + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); +#endif + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + + options.AddEventHandler(descriptor => + { + descriptor.UseScopedHandler(); + }); + }); + +builder.Services.Configure(options => +{ + options.DisableSlidingRefreshTokenExpiration = false; + options.DisableRollingRefreshTokens = false; +}); + +var app = builder.Build(); + var mongoInitializer = app.Services.GetRequiredService(); var mongoDatabase = app.Services.GetRequiredService(); await mongoInitializer.InitialiseAsync(mongoDatabase, CancellationToken.None); @@ -393,845 +402,863 @@ if (authorityOptions.Delegation.ServiceAccounts.Count > 0) { foreach (var seed in authorityOptions.Delegation.ServiceAccounts) { - var document = new AuthorityServiceAccountDocument + var existing = await serviceAccountStore + .FindByAccountIdAsync(seed.AccountId, CancellationToken.None) + .ConfigureAwait(false); + + var document = existing ?? new AuthorityServiceAccountDocument(); + document.AccountId = seed.AccountId; + document.Tenant = seed.Tenant; + document.DisplayName = string.IsNullOrWhiteSpace(seed.DisplayName) ? seed.AccountId : seed.DisplayName; + document.Description = seed.Description; + document.Enabled = seed.Enabled; + document.AllowedScopes = seed.AllowedScopes.ToList(); + document.AuthorizedClients = seed.AuthorizedClients.ToList(); + + if (seed.Attributes.Count == 0) { - AccountId = seed.AccountId, - Tenant = seed.Tenant, - DisplayName = string.IsNullOrWhiteSpace(seed.DisplayName) ? seed.AccountId : seed.DisplayName, - Description = seed.Description, - Enabled = seed.Enabled, - AllowedScopes = seed.AllowedScopes.ToList(), - AuthorizedClients = seed.AuthorizedClients.ToList() - }; + document.Attributes.Clear(); + } + else + { + document.Attributes = seed.Attributes + .ToDictionary( + pair => pair.Key, + pair => pair.Value is { Count: > 0 } + ? pair.Value.Select(value => value).ToList() + : new List(), + StringComparer.OrdinalIgnoreCase); + } + document.CreatedAt = existing?.CreatedAt ?? document.CreatedAt; await serviceAccountStore.UpsertAsync(document, CancellationToken.None).ConfigureAwait(false); } } - -var registrationSummary = app.Services.GetRequiredService(); -if (registrationSummary.RegisteredPlugins.Count > 0) -{ - app.Logger.LogInformation( - "Authority plugins registered: {Plugins}", - string.Join(", ", registrationSummary.RegisteredPlugins)); -} - -foreach (var failure in registrationSummary.Failures) -{ - app.Logger.LogError( - "Authority plugin '{PluginName}' failed to register: {Reason}", - failure.PluginName, - failure.Reason); -} - -foreach (var missing in registrationSummary.MissingOrderedPlugins) -{ - app.Logger.LogWarning( - "Configured Authority plugin '{PluginName}' was not discovered during startup.", - missing); -} - -var identityProviderRegistry = app.Services.GetRequiredService(); -if (identityProviderRegistry.Providers.Count == 0) -{ - app.Logger.LogWarning("No identity provider plugins were registered."); -} -else -{ - foreach (var provider in identityProviderRegistry.Providers) - { - var caps = provider.Capabilities; - app.Logger.LogInformation( - "Identity provider plugin '{PluginName}' (type {PluginType}) capabilities: password={Password}, mfa={Mfa}, clientProvisioning={ClientProvisioning}.", - provider.Name, - provider.Type, - caps.SupportsPassword, - caps.SupportsMfa, - caps.SupportsClientProvisioning); - } -} - -if (authorityOptions.Bootstrap.Enabled) -{ - var bootstrapGroup = app.MapGroup("/internal"); - bootstrapGroup.AddEndpointFilter(new BootstrapApiKeyFilter(authorityOptions)); - - bootstrapGroup.MapPost("/users", async ( - HttpContext httpContext, - BootstrapUserRequest request, - IAuthorityIdentityProviderRegistry registry, - IAuthorityBootstrapInviteStore inviteStore, - IAuthEventSink auditSink, - TimeProvider timeProvider, - CancellationToken cancellationToken) => - { - if (request is null) - { - await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Request payload is required.", null, null, null, Array.Empty(), null).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); - } - - var now = timeProvider.GetUtcNow(); - var inviteToken = string.IsNullOrWhiteSpace(request.InviteToken) ? null : request.InviteToken.Trim(); - AuthorityBootstrapInviteDocument? invite = null; - var inviteReserved = false; - - async Task ReleaseInviteAsync(string reason) - { - if (inviteToken is null) - { - return; - } - - if (inviteReserved) - { - await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false); - } - - await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, reason, invite, inviteToken).ConfigureAwait(false); - } - - if (inviteToken is not null) - { - var reservation = await inviteStore.TryReserveAsync(inviteToken, BootstrapInviteTypes.User, now, request.Username, cancellationToken).ConfigureAwait(false); - - switch (reservation.Status) - { - case BootstrapInviteReservationStatus.Reserved: - inviteReserved = true; - invite = reservation.Invite; - break; - case BootstrapInviteReservationStatus.Expired: - await WriteInviteAuditAsync("authority.bootstrap.invite.expired", AuthEventOutcome.Failure, "Invite expired before use.", reservation.Invite, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invite_expired", message = "Invite has expired." }); - case BootstrapInviteReservationStatus.AlreadyUsed: - await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token already consumed.", reservation.Invite, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invite_used", message = "Invite token has already been used." }); - default: - await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token not found.", reservation.Invite, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_invite", message = "Invite token is invalid." }); - } - } - - var providerName = string.IsNullOrWhiteSpace(request.Provider) - ? invite?.Provider ?? authorityOptions.Bootstrap.DefaultIdentityProvider - : request.Provider; - - if (invite is not null && !string.IsNullOrWhiteSpace(invite.Provider) && - !string.Equals(invite.Provider, providerName, StringComparison.OrdinalIgnoreCase)) - { - await ReleaseInviteAsync("Invite provider does not match requested provider."); - return Results.BadRequest(new { error = "invite_provider_mismatch", message = "Invite is limited to a different identity provider." }); - } - - if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var providerMetadata)) - { - await ReleaseInviteAsync("Specified identity provider was not found."); - await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Specified identity provider was not found.", null, request.Username, providerName, request.Roles ?? Array.Empty(), inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." }); - } - - if (!providerMetadata.Capabilities.SupportsPassword) - { - await ReleaseInviteAsync("Selected provider does not support password provisioning."); - await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support password provisioning.", null, request.Username, providerMetadata.Name, request.Roles ?? Array.Empty(), inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support password provisioning." }); - } - - if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrEmpty(request.Password)) - { - await ReleaseInviteAsync("Username and password are required."); - await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Username and password are required.", null, request.Username, providerMetadata.Name, request.Roles ?? Array.Empty(), inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = "Username and password are required." }); - } - - if (invite is not null && !string.IsNullOrWhiteSpace(invite.Target) && - !string.Equals(invite.Target, request.Username, StringComparison.OrdinalIgnoreCase)) - { - await ReleaseInviteAsync("Invite target does not match requested username."); - return Results.BadRequest(new { error = "invite_target_mismatch", message = "Invite target does not match username." }); - } - - var roles = request.Roles is null ? Array.Empty() : request.Roles.ToArray(); - var attributes = request.Attributes is null - ? new Dictionary(StringComparer.OrdinalIgnoreCase) - : new Dictionary(request.Attributes, StringComparer.OrdinalIgnoreCase); - - var registration = new AuthorityUserRegistration( - request.Username, - request.Password, - request.DisplayName, - request.Email, - request.RequirePasswordReset, - roles, - attributes); - - await using var providerHandle = await registry.AcquireAsync(providerMetadata.Name, cancellationToken).ConfigureAwait(false); - var provider = providerHandle.Provider; - - try - { - var result = await provider.Credentials.UpsertUserAsync(registration, cancellationToken).ConfigureAwait(false); - - if (!result.Succeeded || result.Value is null) - { - await ReleaseInviteAsync(result.Message ?? "User provisioning failed."); - await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, result.Message ?? "User provisioning failed.", null, request.Username, providerMetadata.Name, roles, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "User provisioning failed." }); - } - - if (inviteReserved && inviteToken is not null) - { - var consumed = await inviteStore.MarkConsumedAsync(inviteToken, result.Value.SubjectId ?? result.Value.Username, now, cancellationToken).ConfigureAwait(false); - if (consumed) - { - await WriteInviteAuditAsync("authority.bootstrap.invite.consumed", AuthEventOutcome.Success, null, invite, inviteToken).ConfigureAwait(false); - } - } - - await WriteBootstrapUserAuditAsync(AuthEventOutcome.Success, null, result.Value.SubjectId, result.Value.Username, providerMetadata.Name, roles, inviteToken).ConfigureAwait(false); - - return Results.Ok(new - { - provider = providerMetadata.Name, - subjectId = result.Value.SubjectId, - username = result.Value.Username - }); - } - catch - { - if (inviteReserved && inviteToken is not null) - { - await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false); - await WriteInviteAuditAsync("authority.bootstrap.invite.released", AuthEventOutcome.Error, "Invite released due to provisioning failure.", invite, inviteToken).ConfigureAwait(false); - } - - throw; - } - - async Task WriteBootstrapUserAuditAsync(AuthEventOutcome outcome, string? reason, string? subjectId, string? usernameValue, string? providerValue, IReadOnlyCollection rolesValue, string? inviteValue) - { - var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - AuthEventNetwork? network = null; - var remoteAddress = httpContext.Connection.RemoteIpAddress?.ToString(); - var userAgent = httpContext.Request.Headers.UserAgent.ToString(); - - if (!string.IsNullOrWhiteSpace(remoteAddress) || !string.IsNullOrWhiteSpace(userAgent)) - { - network = new AuthEventNetwork - { - RemoteAddress = ClassifiedString.Personal(remoteAddress), - UserAgent = ClassifiedString.Personal(string.IsNullOrWhiteSpace(userAgent) ? null : userAgent) - }; - } - - var subject = subjectId is null && string.IsNullOrWhiteSpace(usernameValue) && string.IsNullOrWhiteSpace(providerValue) - ? null - : new AuthEventSubject - { - SubjectId = ClassifiedString.Personal(subjectId), - Username = ClassifiedString.Personal(usernameValue), - Realm = ClassifiedString.Public(providerValue) - }; - - var properties = new List(); - if (!string.IsNullOrWhiteSpace(providerValue)) - { - properties.Add(new AuthEventProperty - { - Name = "bootstrap.provider", - Value = ClassifiedString.Public(providerValue) - }); - } - - if (!string.IsNullOrWhiteSpace(inviteValue)) - { - properties.Add(new AuthEventProperty - { - Name = "bootstrap.invite_token", - Value = ClassifiedString.Public(inviteValue) - }); - } - - var scopes = rolesValue is { Count: > 0 } - ? rolesValue.ToArray() - : Array.Empty(); - - var record = new AuthEventRecord - { - EventType = "authority.bootstrap.user", - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = correlationId, - Outcome = outcome, - Reason = reason, - Subject = subject, - Client = null, - Scopes = scopes, - Network = network, - Properties = properties.Count == 0 ? Array.Empty() : properties - }; - - await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); - } - - async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument? document, string? tokenValue) - { - var record = new AuthEventRecord - { - EventType = eventType, - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - Outcome = outcome, - Reason = reason, - Subject = null, - Client = null, - Scopes = Array.Empty(), - Network = null, - Properties = BuildInviteProperties(document, tokenValue) - }; - - await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); - } - - static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument? document, string? token) - { - var properties = new List(); - if (!string.IsNullOrWhiteSpace(token)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.token", - Value = ClassifiedString.Public(token) - }); - } - - if (document is not null) - { - if (!string.IsNullOrWhiteSpace(document.Type)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.type", - Value = ClassifiedString.Public(document.Type) - }); - } - - if (!string.IsNullOrWhiteSpace(document.Provider)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.provider", - Value = ClassifiedString.Public(document.Provider) - }); - } - - if (!string.IsNullOrWhiteSpace(document.Target)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.target", - Value = ClassifiedString.Public(document.Target) - }); - } - - properties.Add(new AuthEventProperty - { - Name = "invite.expires_at", - Value = ClassifiedString.Public(document.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) - }); - } - - return properties.Count == 0 ? Array.Empty() : properties.ToArray(); - } - }); - - bootstrapGroup.MapPost("/clients", async ( - HttpContext httpContext, - BootstrapClientRequest request, - IAuthorityIdentityProviderRegistry registry, - IAuthorityBootstrapInviteStore inviteStore, - IAuthEventSink auditSink, - TimeProvider timeProvider, - CancellationToken cancellationToken) => - { - if (request is null) - { - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Request payload is required.", null, null, null, Array.Empty(), null, null).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); - } - - var now = timeProvider.GetUtcNow(); - var inviteToken = string.IsNullOrWhiteSpace(request.InviteToken) ? null : request.InviteToken.Trim(); - AuthorityBootstrapInviteDocument? invite = null; - var inviteReserved = false; - - async Task ReleaseInviteAsync(string reason) - { - if (inviteToken is null) - { - return; - } - - if (inviteReserved) - { - await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false); - } - - await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, reason, invite, inviteToken).ConfigureAwait(false); - } - - if (inviteToken is not null) - { - var reservation = await inviteStore.TryReserveAsync(inviteToken, BootstrapInviteTypes.Client, now, request.ClientId, cancellationToken).ConfigureAwait(false); - switch (reservation.Status) - { - case BootstrapInviteReservationStatus.Reserved: - inviteReserved = true; - invite = reservation.Invite; - break; - case BootstrapInviteReservationStatus.Expired: - await WriteInviteAuditAsync("authority.bootstrap.invite.expired", AuthEventOutcome.Failure, "Invite expired before use.", reservation.Invite, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invite_expired", message = "Invite has expired." }); - case BootstrapInviteReservationStatus.AlreadyUsed: - await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token already consumed.", reservation.Invite, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invite_used", message = "Invite token has already been used." }); - default: - await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token is invalid.", reservation.Invite, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_invite", message = "Invite token is invalid." }); - } - } - - var providerName = string.IsNullOrWhiteSpace(request.Provider) - ? invite?.Provider ?? authorityOptions.Bootstrap.DefaultIdentityProvider - : request.Provider; - - if (invite is not null && !string.IsNullOrWhiteSpace(invite.Provider) && - !string.Equals(invite.Provider, providerName, StringComparison.OrdinalIgnoreCase)) - { - await ReleaseInviteAsync("Invite provider does not match requested provider."); - return Results.BadRequest(new { error = "invite_provider_mismatch", message = "Invite is limited to a different identity provider." }); - } - - if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var providerMetadata)) - { - await ReleaseInviteAsync("Specified identity provider was not found."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Specified identity provider was not found.", request.ClientId, null, providerName, request.AllowedScopes ?? Array.Empty(), request?.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." }); - } - - if (!providerMetadata.Capabilities.SupportsClientProvisioning) - { - await ReleaseInviteAsync("Selected provider does not support client provisioning."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support client provisioning.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support client provisioning." }); - } - - await using var providerHandle = await registry.AcquireAsync(providerMetadata.Name, cancellationToken).ConfigureAwait(false); - var provider = providerHandle.Provider; - - if (provider.ClientProvisioning is null) - { - await ReleaseInviteAsync("Selected provider does not support client provisioning."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support client provisioning.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support client provisioning." }); - } - - if (string.IsNullOrWhiteSpace(request.ClientId)) - { - await ReleaseInviteAsync("ClientId is required."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "ClientId is required.", null, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = "ClientId is required." }); - } - - if (invite is not null && !string.IsNullOrWhiteSpace(invite.Target) && - !string.Equals(invite.Target, request.ClientId, StringComparison.OrdinalIgnoreCase)) - { - await ReleaseInviteAsync("Invite target does not match requested client id."); - return Results.BadRequest(new { error = "invite_target_mismatch", message = "Invite target does not match client id." }); - } - - if (request.Confidential && string.IsNullOrWhiteSpace(request.ClientSecret)) - { - await ReleaseInviteAsync("Confidential clients require a client secret."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Confidential clients require a client secret.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = "Confidential clients require a client secret." }); - } - - if (!TryParseUris(request.RedirectUris, out var redirectUris, out var redirectError)) - { - var errorMessage = redirectError ?? "Redirect URI validation failed."; - await ReleaseInviteAsync(errorMessage); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, errorMessage, request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = errorMessage }); - } - - if (!TryParseUris(request.PostLogoutRedirectUris, out var postLogoutUris, out var postLogoutError)) - { - var errorMessage = postLogoutError ?? "Post-logout redirect URI validation failed."; - await ReleaseInviteAsync(errorMessage); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, errorMessage, request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = errorMessage }); - } - - var properties = request.Properties is null - ? new Dictionary(StringComparer.OrdinalIgnoreCase) - : new Dictionary(request.Properties, StringComparer.OrdinalIgnoreCase); - - IReadOnlyCollection? certificateBindings = null; - if (request.CertificateBindings is not null) - { - var bindingRegistrations = new List(request.CertificateBindings.Count); - foreach (var binding in request.CertificateBindings) - { - if (binding is null || string.IsNullOrWhiteSpace(binding.Thumbprint)) - { - await ReleaseInviteAsync("Certificate binding thumbprint is required."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Certificate binding thumbprint is required.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = "invalid_request", message = "Certificate binding thumbprint is required." }); - } - - bindingRegistrations.Add(new AuthorityClientCertificateBindingRegistration( - binding.Thumbprint, - binding.SerialNumber, - binding.Subject, - binding.Issuer, - binding.SubjectAlternativeNames, - binding.NotBefore, - binding.NotAfter, - binding.Label)); - } - - certificateBindings = bindingRegistrations; - } - - var requestedTenant = properties.TryGetValue(AuthorityClientMetadataKeys.Tenant, out var tenantMetadata) - ? ClientCredentialHandlerHelpers.NormalizeTenant(tenantMetadata) - : null; - if (!string.IsNullOrWhiteSpace(requestedTenant)) - { - properties[AuthorityClientMetadataKeys.Tenant] = requestedTenant; - } - - var requestedProject = properties.TryGetValue(AuthorityClientMetadataKeys.Project, out var projectMetadata) - ? ClientCredentialHandlerHelpers.NormalizeProject(projectMetadata) - : null; - requestedProject ??= StellaOpsTenancyDefaults.AnyProject; - properties[AuthorityClientMetadataKeys.Project] = requestedProject; - - var registration = new AuthorityClientRegistration( - request.ClientId, - request.Confidential, - request.DisplayName, - request.ClientSecret, - request.AllowedGrantTypes ?? Array.Empty(), - request.AllowedScopes ?? Array.Empty(), - request.AllowedAudiences ?? Array.Empty(), - redirectUris, - postLogoutUris, - requestedTenant, - requestedProject, - properties, - certificateBindings); - - var result = await provider.ClientProvisioning.CreateOrUpdateAsync(registration, cancellationToken).ConfigureAwait(false); - - if (!result.Succeeded || result.Value is null) - { - await ReleaseInviteAsync(result.Message ?? "Client provisioning failed."); - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, result.Message ?? "Client provisioning failed.", request.ClientId, result.Value?.ClientId, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "Client provisioning failed." }); - } - - if (inviteReserved && inviteToken is not null) - { - var consumed = await inviteStore.MarkConsumedAsync(inviteToken, result.Value.ClientId, now, cancellationToken).ConfigureAwait(false); - if (consumed) - { - await WriteInviteAuditAsync("authority.bootstrap.invite.consumed", AuthEventOutcome.Success, null, invite, inviteToken).ConfigureAwait(false); - } - } - - await WriteBootstrapClientAuditAsync(AuthEventOutcome.Success, null, request.ClientId, result.Value.ClientId, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); - - return Results.Ok(new - { - provider = providerMetadata.Name, - clientId = result.Value.ClientId, - confidential = result.Value.Confidential - }); - - async Task WriteBootstrapClientAuditAsync(AuthEventOutcome outcome, string? reason, string? requestedClientId, string? assignedClientId, string? providerValue, IReadOnlyCollection scopes, bool? confidentialFlag, string? inviteValue) - { - var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - AuthEventNetwork? network = null; - var remoteAddress = httpContext.Connection.RemoteIpAddress?.ToString(); - var userAgent = httpContext.Request.Headers.UserAgent.ToString(); - - if (!string.IsNullOrWhiteSpace(remoteAddress) || !string.IsNullOrWhiteSpace(userAgent)) - { - network = new AuthEventNetwork - { - RemoteAddress = ClassifiedString.Personal(remoteAddress), - UserAgent = ClassifiedString.Personal(string.IsNullOrWhiteSpace(userAgent) ? null : userAgent) - }; - } - - var clientIdValue = assignedClientId ?? requestedClientId; - var client = clientIdValue is null && string.IsNullOrWhiteSpace(providerValue) - ? null - : new AuthEventClient - { - ClientId = ClassifiedString.Personal(clientIdValue), - Name = ClassifiedString.Empty, - Provider = ClassifiedString.Public(providerValue) - }; - - var properties = new List(); - if (!string.IsNullOrWhiteSpace(requestedClientId) && !string.Equals(requestedClientId, assignedClientId, StringComparison.Ordinal)) - { - properties.Add(new AuthEventProperty - { - Name = "bootstrap.requested_client_id", - Value = ClassifiedString.Public(requestedClientId) - }); - } - - if (confidentialFlag == true) - { - properties.Add(new AuthEventProperty - { - Name = "bootstrap.confidential", - Value = ClassifiedString.Public("true") - }); - } - - if (!string.IsNullOrWhiteSpace(inviteValue)) - { - properties.Add(new AuthEventProperty - { - Name = "bootstrap.invite_token", - Value = ClassifiedString.Public(inviteValue) - }); - } - - var record = new AuthEventRecord - { - EventType = "authority.bootstrap.client", - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = correlationId, - Outcome = outcome, - Reason = reason, - Subject = null, - Client = client, - Scopes = scopes is { Count: > 0 } ? scopes.ToArray() : Array.Empty(), - Network = network, - Properties = properties.Count == 0 ? Array.Empty() : properties.ToArray() - }; - - await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); - } - - async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument? document, string? tokenValue) - { - var record = new AuthEventRecord - { - EventType = eventType, - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - Outcome = outcome, - Reason = reason, - Subject = null, - Client = null, - Scopes = Array.Empty(), - Network = null, - Properties = BuildInviteProperties(document, tokenValue) - }; - - await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); - } - - static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument? document, string? token) - { - var properties = new List(); - if (!string.IsNullOrWhiteSpace(token)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.token", - Value = ClassifiedString.Public(token) - }); - } - - if (document is not null) - { - if (!string.IsNullOrWhiteSpace(document.Type)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.type", - Value = ClassifiedString.Public(document.Type) - }); - } - - if (!string.IsNullOrWhiteSpace(document.Provider)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.provider", - Value = ClassifiedString.Public(document.Provider) - }); - } - - if (!string.IsNullOrWhiteSpace(document.Target)) - { - properties.Add(new AuthEventProperty - { - Name = "invite.target", - Value = ClassifiedString.Public(document.Target) - }); - } - - properties.Add(new AuthEventProperty - { - Name = "invite.expires_at", - Value = ClassifiedString.Public(document.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) - }); - } - - return properties.Count == 0 ? Array.Empty() : properties.ToArray(); - } - }); - bootstrapGroup.MapPost("/invites", async ( - HttpContext httpContext, - BootstrapInviteRequest request, - IAuthorityBootstrapInviteStore inviteStore, - IAuthEventSink auditSink, - TimeProvider timeProvider, - CancellationToken cancellationToken) => - { - if (request is null) - { - return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); - } - - if (string.IsNullOrWhiteSpace(request.Type) || - ( !string.Equals(request.Type, BootstrapInviteTypes.User, StringComparison.OrdinalIgnoreCase) && - !string.Equals(request.Type, BootstrapInviteTypes.Client, StringComparison.OrdinalIgnoreCase))) - { - return Results.BadRequest(new { error = "invalid_request", message = "Invite type must be 'user' or 'client'." }); - } - - var now = timeProvider.GetUtcNow(); - var expiresAt = request.ExpiresAt ?? now.AddDays(2); - if (expiresAt <= now) - { - return Results.BadRequest(new { error = "invalid_request", message = "ExpiresAt must be in the future." }); - } - - var token = string.IsNullOrWhiteSpace(request.Token) ? Guid.NewGuid().ToString("N") : request.Token.Trim(); - - var document = new AuthorityBootstrapInviteDocument - { - Token = token, - Type = request.Type.ToLowerInvariant(), - Provider = string.IsNullOrWhiteSpace(request.Provider) ? null : request.Provider.Trim(), - Target = string.IsNullOrWhiteSpace(request.Target) ? null : request.Target.Trim(), - IssuedAt = now, - IssuedBy = string.IsNullOrWhiteSpace(request.IssuedBy) ? httpContext.User?.Identity?.Name : request.IssuedBy, - ExpiresAt = expiresAt, - Metadata = request.Metadata is null ? null : new Dictionary(request.Metadata, StringComparer.OrdinalIgnoreCase) - }; - - await inviteStore.CreateAsync(document, cancellationToken).ConfigureAwait(false); - await WriteInviteAuditAsync("authority.bootstrap.invite.created", AuthEventOutcome.Success, null, document).ConfigureAwait(false); - - return Results.Ok(new - { - document.Token, - document.Type, - document.Provider, - document.Target, - document.ExpiresAt - }); - - async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument invite) - { - var record = new AuthEventRecord - { - EventType = eventType, - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - Outcome = outcome, - Reason = reason, - Subject = null, - Client = null, - Scopes = Array.Empty(), - Network = null, - Properties = BuildInviteProperties(invite) - }; - - await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); - } - - static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument invite) - { - var properties = new List - { - new() { Name = "invite.token", Value = ClassifiedString.Public(invite.Token) }, - new() { Name = "invite.type", Value = ClassifiedString.Public(invite.Type) }, - new() { Name = "invite.expires_at", Value = ClassifiedString.Public(invite.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) } - }; - - if (!string.IsNullOrWhiteSpace(invite.Provider)) - { - properties.Add(new AuthEventProperty { Name = "invite.provider", Value = ClassifiedString.Public(invite.Provider) }); - } - - if (!string.IsNullOrWhiteSpace(invite.Target)) - { - properties.Add(new AuthEventProperty { Name = "invite.target", Value = ClassifiedString.Public(invite.Target) }); - } - - if (!string.IsNullOrWhiteSpace(invite.IssuedBy)) - { - properties.Add(new AuthEventProperty { Name = "invite.issued_by", Value = ClassifiedString.Public(invite.IssuedBy) }); - } - - return properties.ToArray(); - } - }); - + +var registrationSummary = app.Services.GetRequiredService(); +if (registrationSummary.RegisteredPlugins.Count > 0) +{ + app.Logger.LogInformation( + "Authority plugins registered: {Plugins}", + string.Join(", ", registrationSummary.RegisteredPlugins)); +} + +foreach (var failure in registrationSummary.Failures) +{ + app.Logger.LogError( + "Authority plugin '{PluginName}' failed to register: {Reason}", + failure.PluginName, + failure.Reason); +} + +foreach (var missing in registrationSummary.MissingOrderedPlugins) +{ + app.Logger.LogWarning( + "Configured Authority plugin '{PluginName}' was not discovered during startup.", + missing); +} + +var identityProviderRegistry = app.Services.GetRequiredService(); +if (identityProviderRegistry.Providers.Count == 0) +{ + app.Logger.LogWarning("No identity provider plugins were registered."); +} +else +{ + foreach (var provider in identityProviderRegistry.Providers) + { + var caps = provider.Capabilities; + app.Logger.LogInformation( + "Identity provider plugin '{PluginName}' (type {PluginType}) capabilities: password={Password}, mfa={Mfa}, clientProvisioning={ClientProvisioning}.", + provider.Name, + provider.Type, + caps.SupportsPassword, + caps.SupportsMfa, + caps.SupportsClientProvisioning); + } +} + +if (authorityOptions.Bootstrap.Enabled) +{ + var bootstrapGroup = app.MapGroup("/internal"); + bootstrapGroup.AddEndpointFilter(new BootstrapApiKeyFilter(authorityOptions)); + + bootstrapGroup.MapPost("/users", async ( + HttpContext httpContext, + BootstrapUserRequest request, + IAuthorityIdentityProviderRegistry registry, + IAuthorityBootstrapInviteStore inviteStore, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => + { + if (request is null) + { + await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Request payload is required.", null, null, null, Array.Empty(), null).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + var now = timeProvider.GetUtcNow(); + var inviteToken = string.IsNullOrWhiteSpace(request.InviteToken) ? null : request.InviteToken.Trim(); + AuthorityBootstrapInviteDocument? invite = null; + var inviteReserved = false; + + async Task ReleaseInviteAsync(string reason) + { + if (inviteToken is null) + { + return; + } + + if (inviteReserved) + { + await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false); + } + + await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, reason, invite, inviteToken).ConfigureAwait(false); + } + + if (inviteToken is not null) + { + var reservation = await inviteStore.TryReserveAsync(inviteToken, BootstrapInviteTypes.User, now, request.Username, cancellationToken).ConfigureAwait(false); + + switch (reservation.Status) + { + case BootstrapInviteReservationStatus.Reserved: + inviteReserved = true; + invite = reservation.Invite; + break; + case BootstrapInviteReservationStatus.Expired: + await WriteInviteAuditAsync("authority.bootstrap.invite.expired", AuthEventOutcome.Failure, "Invite expired before use.", reservation.Invite, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invite_expired", message = "Invite has expired." }); + case BootstrapInviteReservationStatus.AlreadyUsed: + await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token already consumed.", reservation.Invite, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invite_used", message = "Invite token has already been used." }); + default: + await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token not found.", reservation.Invite, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_invite", message = "Invite token is invalid." }); + } + } + + var providerName = string.IsNullOrWhiteSpace(request.Provider) + ? invite?.Provider ?? authorityOptions.Bootstrap.DefaultIdentityProvider + : request.Provider; + + if (invite is not null && !string.IsNullOrWhiteSpace(invite.Provider) && + !string.Equals(invite.Provider, providerName, StringComparison.OrdinalIgnoreCase)) + { + await ReleaseInviteAsync("Invite provider does not match requested provider."); + return Results.BadRequest(new { error = "invite_provider_mismatch", message = "Invite is limited to a different identity provider." }); + } + + if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var providerMetadata)) + { + await ReleaseInviteAsync("Specified identity provider was not found."); + await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Specified identity provider was not found.", null, request.Username, providerName, request.Roles ?? Array.Empty(), inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." }); + } + + if (!providerMetadata.Capabilities.SupportsPassword) + { + await ReleaseInviteAsync("Selected provider does not support password provisioning."); + await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support password provisioning.", null, request.Username, providerMetadata.Name, request.Roles ?? Array.Empty(), inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support password provisioning." }); + } + + if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrEmpty(request.Password)) + { + await ReleaseInviteAsync("Username and password are required."); + await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Username and password are required.", null, request.Username, providerMetadata.Name, request.Roles ?? Array.Empty(), inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = "Username and password are required." }); + } + + if (invite is not null && !string.IsNullOrWhiteSpace(invite.Target) && + !string.Equals(invite.Target, request.Username, StringComparison.OrdinalIgnoreCase)) + { + await ReleaseInviteAsync("Invite target does not match requested username."); + return Results.BadRequest(new { error = "invite_target_mismatch", message = "Invite target does not match username." }); + } + + var roles = request.Roles is null ? Array.Empty() : request.Roles.ToArray(); + var attributes = request.Attributes is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(request.Attributes, StringComparer.OrdinalIgnoreCase); + + var registration = new AuthorityUserRegistration( + request.Username, + request.Password, + request.DisplayName, + request.Email, + request.RequirePasswordReset, + roles, + attributes); + + await using var providerHandle = await registry.AcquireAsync(providerMetadata.Name, cancellationToken).ConfigureAwait(false); + var provider = providerHandle.Provider; + + try + { + var result = await provider.Credentials.UpsertUserAsync(registration, cancellationToken).ConfigureAwait(false); + + if (!result.Succeeded || result.Value is null) + { + await ReleaseInviteAsync(result.Message ?? "User provisioning failed."); + await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, result.Message ?? "User provisioning failed.", null, request.Username, providerMetadata.Name, roles, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "User provisioning failed." }); + } + + if (inviteReserved && inviteToken is not null) + { + var consumed = await inviteStore.MarkConsumedAsync(inviteToken, result.Value.SubjectId ?? result.Value.Username, now, cancellationToken).ConfigureAwait(false); + if (consumed) + { + await WriteInviteAuditAsync("authority.bootstrap.invite.consumed", AuthEventOutcome.Success, null, invite, inviteToken).ConfigureAwait(false); + } + } + + await WriteBootstrapUserAuditAsync(AuthEventOutcome.Success, null, result.Value.SubjectId, result.Value.Username, providerMetadata.Name, roles, inviteToken).ConfigureAwait(false); + + return Results.Ok(new + { + provider = providerMetadata.Name, + subjectId = result.Value.SubjectId, + username = result.Value.Username + }); + } + catch + { + if (inviteReserved && inviteToken is not null) + { + await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false); + await WriteInviteAuditAsync("authority.bootstrap.invite.released", AuthEventOutcome.Error, "Invite released due to provisioning failure.", invite, inviteToken).ConfigureAwait(false); + } + + throw; + } + + async Task WriteBootstrapUserAuditAsync(AuthEventOutcome outcome, string? reason, string? subjectId, string? usernameValue, string? providerValue, IReadOnlyCollection rolesValue, string? inviteValue) + { + var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + AuthEventNetwork? network = null; + var remoteAddress = httpContext.Connection.RemoteIpAddress?.ToString(); + var userAgent = httpContext.Request.Headers.UserAgent.ToString(); + + if (!string.IsNullOrWhiteSpace(remoteAddress) || !string.IsNullOrWhiteSpace(userAgent)) + { + network = new AuthEventNetwork + { + RemoteAddress = ClassifiedString.Personal(remoteAddress), + UserAgent = ClassifiedString.Personal(string.IsNullOrWhiteSpace(userAgent) ? null : userAgent) + }; + } + + var subject = subjectId is null && string.IsNullOrWhiteSpace(usernameValue) && string.IsNullOrWhiteSpace(providerValue) + ? null + : new AuthEventSubject + { + SubjectId = ClassifiedString.Personal(subjectId), + Username = ClassifiedString.Personal(usernameValue), + Realm = ClassifiedString.Public(providerValue) + }; + + var properties = new List(); + if (!string.IsNullOrWhiteSpace(providerValue)) + { + properties.Add(new AuthEventProperty + { + Name = "bootstrap.provider", + Value = ClassifiedString.Public(providerValue) + }); + } + + if (!string.IsNullOrWhiteSpace(inviteValue)) + { + properties.Add(new AuthEventProperty + { + Name = "bootstrap.invite_token", + Value = ClassifiedString.Public(inviteValue) + }); + } + + var scopes = rolesValue is { Count: > 0 } + ? rolesValue.ToArray() + : Array.Empty(); + + var record = new AuthEventRecord + { + EventType = "authority.bootstrap.user", + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = correlationId, + Outcome = outcome, + Reason = reason, + Subject = subject, + Client = null, + Scopes = scopes, + Network = network, + Properties = properties.Count == 0 ? Array.Empty() : properties + }; + + await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); + } + + async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument? document, string? tokenValue) + { + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Outcome = outcome, + Reason = reason, + Subject = null, + Client = null, + Scopes = Array.Empty(), + Network = null, + Properties = BuildInviteProperties(document, tokenValue) + }; + + await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); + } + + static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument? document, string? token) + { + var properties = new List(); + if (!string.IsNullOrWhiteSpace(token)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.token", + Value = ClassifiedString.Public(token) + }); + } + + if (document is not null) + { + if (!string.IsNullOrWhiteSpace(document.Type)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.type", + Value = ClassifiedString.Public(document.Type) + }); + } + + if (!string.IsNullOrWhiteSpace(document.Provider)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.provider", + Value = ClassifiedString.Public(document.Provider) + }); + } + + if (!string.IsNullOrWhiteSpace(document.Target)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.target", + Value = ClassifiedString.Public(document.Target) + }); + } + + properties.Add(new AuthEventProperty + { + Name = "invite.expires_at", + Value = ClassifiedString.Public(document.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) + }); + } + + return properties.Count == 0 ? Array.Empty() : properties.ToArray(); + } + }); + + bootstrapGroup.MapPost("/clients", async ( + HttpContext httpContext, + BootstrapClientRequest request, + IAuthorityIdentityProviderRegistry registry, + IAuthorityBootstrapInviteStore inviteStore, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => + { + if (request is null) + { + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Request payload is required.", null, null, null, Array.Empty(), null, null).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + var now = timeProvider.GetUtcNow(); + var inviteToken = string.IsNullOrWhiteSpace(request.InviteToken) ? null : request.InviteToken.Trim(); + AuthorityBootstrapInviteDocument? invite = null; + var inviteReserved = false; + + async Task ReleaseInviteAsync(string reason) + { + if (inviteToken is null) + { + return; + } + + if (inviteReserved) + { + await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false); + } + + await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, reason, invite, inviteToken).ConfigureAwait(false); + } + + if (inviteToken is not null) + { + var reservation = await inviteStore.TryReserveAsync(inviteToken, BootstrapInviteTypes.Client, now, request.ClientId, cancellationToken).ConfigureAwait(false); + switch (reservation.Status) + { + case BootstrapInviteReservationStatus.Reserved: + inviteReserved = true; + invite = reservation.Invite; + break; + case BootstrapInviteReservationStatus.Expired: + await WriteInviteAuditAsync("authority.bootstrap.invite.expired", AuthEventOutcome.Failure, "Invite expired before use.", reservation.Invite, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invite_expired", message = "Invite has expired." }); + case BootstrapInviteReservationStatus.AlreadyUsed: + await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token already consumed.", reservation.Invite, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invite_used", message = "Invite token has already been used." }); + default: + await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token is invalid.", reservation.Invite, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_invite", message = "Invite token is invalid." }); + } + } + + var providerName = string.IsNullOrWhiteSpace(request.Provider) + ? invite?.Provider ?? authorityOptions.Bootstrap.DefaultIdentityProvider + : request.Provider; + + if (invite is not null && !string.IsNullOrWhiteSpace(invite.Provider) && + !string.Equals(invite.Provider, providerName, StringComparison.OrdinalIgnoreCase)) + { + await ReleaseInviteAsync("Invite provider does not match requested provider."); + return Results.BadRequest(new { error = "invite_provider_mismatch", message = "Invite is limited to a different identity provider." }); + } + + if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var providerMetadata)) + { + await ReleaseInviteAsync("Specified identity provider was not found."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Specified identity provider was not found.", request.ClientId, null, providerName, request.AllowedScopes ?? Array.Empty(), request?.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." }); + } + + if (!providerMetadata.Capabilities.SupportsClientProvisioning) + { + await ReleaseInviteAsync("Selected provider does not support client provisioning."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support client provisioning.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support client provisioning." }); + } + + await using var providerHandle = await registry.AcquireAsync(providerMetadata.Name, cancellationToken).ConfigureAwait(false); + var provider = providerHandle.Provider; + + if (provider.ClientProvisioning is null) + { + await ReleaseInviteAsync("Selected provider does not support client provisioning."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support client provisioning.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support client provisioning." }); + } + + if (string.IsNullOrWhiteSpace(request.ClientId)) + { + await ReleaseInviteAsync("ClientId is required."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "ClientId is required.", null, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = "ClientId is required." }); + } + + if (invite is not null && !string.IsNullOrWhiteSpace(invite.Target) && + !string.Equals(invite.Target, request.ClientId, StringComparison.OrdinalIgnoreCase)) + { + await ReleaseInviteAsync("Invite target does not match requested client id."); + return Results.BadRequest(new { error = "invite_target_mismatch", message = "Invite target does not match client id." }); + } + + if (request.Confidential && string.IsNullOrWhiteSpace(request.ClientSecret)) + { + await ReleaseInviteAsync("Confidential clients require a client secret."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Confidential clients require a client secret.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = "Confidential clients require a client secret." }); + } + + if (!TryParseUris(request.RedirectUris, out var redirectUris, out var redirectError)) + { + var errorMessage = redirectError ?? "Redirect URI validation failed."; + await ReleaseInviteAsync(errorMessage); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, errorMessage, request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = errorMessage }); + } + + if (!TryParseUris(request.PostLogoutRedirectUris, out var postLogoutUris, out var postLogoutError)) + { + var errorMessage = postLogoutError ?? "Post-logout redirect URI validation failed."; + await ReleaseInviteAsync(errorMessage); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, errorMessage, request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = errorMessage }); + } + + var properties = request.Properties is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(request.Properties, StringComparer.OrdinalIgnoreCase); + + IReadOnlyCollection? certificateBindings = null; + if (request.CertificateBindings is not null) + { + var bindingRegistrations = new List(request.CertificateBindings.Count); + foreach (var binding in request.CertificateBindings) + { + if (binding is null || string.IsNullOrWhiteSpace(binding.Thumbprint)) + { + await ReleaseInviteAsync("Certificate binding thumbprint is required."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Certificate binding thumbprint is required.", request.ClientId, null, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = "invalid_request", message = "Certificate binding thumbprint is required." }); + } + + bindingRegistrations.Add(new AuthorityClientCertificateBindingRegistration( + binding.Thumbprint, + binding.SerialNumber, + binding.Subject, + binding.Issuer, + binding.SubjectAlternativeNames, + binding.NotBefore, + binding.NotAfter, + binding.Label)); + } + + certificateBindings = bindingRegistrations; + } + + var requestedTenant = properties.TryGetValue(AuthorityClientMetadataKeys.Tenant, out var tenantMetadata) + ? ClientCredentialHandlerHelpers.NormalizeTenant(tenantMetadata) + : null; + if (!string.IsNullOrWhiteSpace(requestedTenant)) + { + properties[AuthorityClientMetadataKeys.Tenant] = requestedTenant; + } + + var requestedProject = properties.TryGetValue(AuthorityClientMetadataKeys.Project, out var projectMetadata) + ? ClientCredentialHandlerHelpers.NormalizeProject(projectMetadata) + : null; + requestedProject ??= StellaOpsTenancyDefaults.AnyProject; + properties[AuthorityClientMetadataKeys.Project] = requestedProject; + + var registration = new AuthorityClientRegistration( + request.ClientId, + request.Confidential, + request.DisplayName, + request.ClientSecret, + request.AllowedGrantTypes ?? Array.Empty(), + request.AllowedScopes ?? Array.Empty(), + request.AllowedAudiences ?? Array.Empty(), + redirectUris, + postLogoutUris, + requestedTenant, + requestedProject, + properties, + certificateBindings); + + var result = await provider.ClientProvisioning.CreateOrUpdateAsync(registration, cancellationToken).ConfigureAwait(false); + + if (!result.Succeeded || result.Value is null) + { + await ReleaseInviteAsync(result.Message ?? "Client provisioning failed."); + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, result.Message ?? "Client provisioning failed.", request.ClientId, result.Value?.ClientId, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "Client provisioning failed." }); + } + + if (inviteReserved && inviteToken is not null) + { + var consumed = await inviteStore.MarkConsumedAsync(inviteToken, result.Value.ClientId, now, cancellationToken).ConfigureAwait(false); + if (consumed) + { + await WriteInviteAuditAsync("authority.bootstrap.invite.consumed", AuthEventOutcome.Success, null, invite, inviteToken).ConfigureAwait(false); + } + } + + await WriteBootstrapClientAuditAsync(AuthEventOutcome.Success, null, request.ClientId, result.Value.ClientId, providerMetadata.Name, request.AllowedScopes ?? Array.Empty(), request.Confidential, inviteToken).ConfigureAwait(false); + + return Results.Ok(new + { + provider = providerMetadata.Name, + clientId = result.Value.ClientId, + confidential = result.Value.Confidential + }); + + async Task WriteBootstrapClientAuditAsync(AuthEventOutcome outcome, string? reason, string? requestedClientId, string? assignedClientId, string? providerValue, IReadOnlyCollection scopes, bool? confidentialFlag, string? inviteValue) + { + var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + AuthEventNetwork? network = null; + var remoteAddress = httpContext.Connection.RemoteIpAddress?.ToString(); + var userAgent = httpContext.Request.Headers.UserAgent.ToString(); + + if (!string.IsNullOrWhiteSpace(remoteAddress) || !string.IsNullOrWhiteSpace(userAgent)) + { + network = new AuthEventNetwork + { + RemoteAddress = ClassifiedString.Personal(remoteAddress), + UserAgent = ClassifiedString.Personal(string.IsNullOrWhiteSpace(userAgent) ? null : userAgent) + }; + } + + var clientIdValue = assignedClientId ?? requestedClientId; + var client = clientIdValue is null && string.IsNullOrWhiteSpace(providerValue) + ? null + : new AuthEventClient + { + ClientId = ClassifiedString.Personal(clientIdValue), + Name = ClassifiedString.Empty, + Provider = ClassifiedString.Public(providerValue) + }; + + var properties = new List(); + if (!string.IsNullOrWhiteSpace(requestedClientId) && !string.Equals(requestedClientId, assignedClientId, StringComparison.Ordinal)) + { + properties.Add(new AuthEventProperty + { + Name = "bootstrap.requested_client_id", + Value = ClassifiedString.Public(requestedClientId) + }); + } + + if (confidentialFlag == true) + { + properties.Add(new AuthEventProperty + { + Name = "bootstrap.confidential", + Value = ClassifiedString.Public("true") + }); + } + + if (!string.IsNullOrWhiteSpace(inviteValue)) + { + properties.Add(new AuthEventProperty + { + Name = "bootstrap.invite_token", + Value = ClassifiedString.Public(inviteValue) + }); + } + + var record = new AuthEventRecord + { + EventType = "authority.bootstrap.client", + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = correlationId, + Outcome = outcome, + Reason = reason, + Subject = null, + Client = client, + Scopes = scopes is { Count: > 0 } ? scopes.ToArray() : Array.Empty(), + Network = network, + Properties = properties.Count == 0 ? Array.Empty() : properties.ToArray() + }; + + await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); + } + + async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument? document, string? tokenValue) + { + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Outcome = outcome, + Reason = reason, + Subject = null, + Client = null, + Scopes = Array.Empty(), + Network = null, + Properties = BuildInviteProperties(document, tokenValue) + }; + + await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); + } + + static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument? document, string? token) + { + var properties = new List(); + if (!string.IsNullOrWhiteSpace(token)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.token", + Value = ClassifiedString.Public(token) + }); + } + + if (document is not null) + { + if (!string.IsNullOrWhiteSpace(document.Type)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.type", + Value = ClassifiedString.Public(document.Type) + }); + } + + if (!string.IsNullOrWhiteSpace(document.Provider)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.provider", + Value = ClassifiedString.Public(document.Provider) + }); + } + + if (!string.IsNullOrWhiteSpace(document.Target)) + { + properties.Add(new AuthEventProperty + { + Name = "invite.target", + Value = ClassifiedString.Public(document.Target) + }); + } + + properties.Add(new AuthEventProperty + { + Name = "invite.expires_at", + Value = ClassifiedString.Public(document.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) + }); + } + + return properties.Count == 0 ? Array.Empty() : properties.ToArray(); + } + }); + bootstrapGroup.MapPost("/invites", async ( + HttpContext httpContext, + BootstrapInviteRequest request, + IAuthorityBootstrapInviteStore inviteStore, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => + { + if (request is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + if (string.IsNullOrWhiteSpace(request.Type) || + ( !string.Equals(request.Type, BootstrapInviteTypes.User, StringComparison.OrdinalIgnoreCase) && + !string.Equals(request.Type, BootstrapInviteTypes.Client, StringComparison.OrdinalIgnoreCase))) + { + return Results.BadRequest(new { error = "invalid_request", message = "Invite type must be 'user' or 'client'." }); + } + + var now = timeProvider.GetUtcNow(); + var expiresAt = request.ExpiresAt ?? now.AddDays(2); + if (expiresAt <= now) + { + return Results.BadRequest(new { error = "invalid_request", message = "ExpiresAt must be in the future." }); + } + + var token = string.IsNullOrWhiteSpace(request.Token) ? Guid.NewGuid().ToString("N") : request.Token.Trim(); + + var document = new AuthorityBootstrapInviteDocument + { + Token = token, + Type = request.Type.ToLowerInvariant(), + Provider = string.IsNullOrWhiteSpace(request.Provider) ? null : request.Provider.Trim(), + Target = string.IsNullOrWhiteSpace(request.Target) ? null : request.Target.Trim(), + IssuedAt = now, + IssuedBy = string.IsNullOrWhiteSpace(request.IssuedBy) ? httpContext.User?.Identity?.Name : request.IssuedBy, + ExpiresAt = expiresAt, + Metadata = request.Metadata is null ? null : new Dictionary(request.Metadata, StringComparer.OrdinalIgnoreCase) + }; + + await inviteStore.CreateAsync(document, cancellationToken).ConfigureAwait(false); + await WriteInviteAuditAsync("authority.bootstrap.invite.created", AuthEventOutcome.Success, null, document).ConfigureAwait(false); + + return Results.Ok(new + { + document.Token, + document.Type, + document.Provider, + document.Target, + document.ExpiresAt + }); + + async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument invite) + { + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Outcome = outcome, + Reason = reason, + Subject = null, + Client = null, + Scopes = Array.Empty(), + Network = null, + Properties = BuildInviteProperties(invite) + }; + + await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false); + } + + static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument invite) + { + var properties = new List + { + new() { Name = "invite.token", Value = ClassifiedString.Public(invite.Token) }, + new() { Name = "invite.type", Value = ClassifiedString.Public(invite.Type) }, + new() { Name = "invite.expires_at", Value = ClassifiedString.Public(invite.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) } + }; + + if (!string.IsNullOrWhiteSpace(invite.Provider)) + { + properties.Add(new AuthEventProperty { Name = "invite.provider", Value = ClassifiedString.Public(invite.Provider) }); + } + + if (!string.IsNullOrWhiteSpace(invite.Target)) + { + properties.Add(new AuthEventProperty { Name = "invite.target", Value = ClassifiedString.Public(invite.Target) }); + } + + if (!string.IsNullOrWhiteSpace(invite.IssuedBy)) + { + properties.Add(new AuthEventProperty { Name = "invite.issued_by", Value = ClassifiedString.Public(invite.IssuedBy) }); + } + + return properties.ToArray(); + } + }); + bootstrapGroup.MapGet("/revocations/export", async ( AuthorityRevocationExportService exportService, CancellationToken cancellationToken) => { var package = await exportService.ExportAsync(cancellationToken).ConfigureAwait(false); - var build = package.Bundle; - - var response = new RevocationExportResponse - { - SchemaVersion = build.Bundle.SchemaVersion, - BundleId = build.Bundle.BundleId ?? build.Sha256, - Sequence = build.Sequence, - IssuedAt = build.IssuedAt, - SigningKeyId = package.Signature.KeyId, - Bundle = new RevocationExportPayload - { - Data = Convert.ToBase64String(build.CanonicalJson) - }, - Signature = new RevocationExportSignature - { - Algorithm = package.Signature.Algorithm, - KeyId = package.Signature.KeyId, - Provider = package.Signature.Provider, - Value = package.Signature.Value - }, - Digest = new RevocationExportDigest - { - Value = build.Sha256 - } - }; - + var build = package.Bundle; + + var response = new RevocationExportResponse + { + SchemaVersion = build.Bundle.SchemaVersion, + BundleId = build.Bundle.BundleId ?? build.Sha256, + Sequence = build.Sequence, + IssuedAt = build.IssuedAt, + SigningKeyId = package.Signature.KeyId, + Bundle = new RevocationExportPayload + { + Data = Convert.ToBase64String(build.CanonicalJson) + }, + Signature = new RevocationExportSignature + { + Algorithm = package.Signature.Algorithm, + KeyId = package.Signature.KeyId, + Provider = package.Signature.Provider, + Value = package.Signature.Value + }, + Digest = new RevocationExportDigest + { + Value = build.Sha256 + } + }; + return Results.Ok(response); }); @@ -1455,6 +1482,15 @@ if (authorityOptions.Bootstrap.Enabled) ? document.AuthorizedClients.OrderBy(client => client, StringComparer.Ordinal).ToArray() : Array.Empty(); + var attributes = document.Attributes is { Count: > 0 } + ? document.Attributes.ToDictionary( + pair => pair.Key, + pair => (IReadOnlyList)pair.Value + .OrderBy(value => value, StringComparer.OrdinalIgnoreCase) + .ToArray(), + StringComparer.OrdinalIgnoreCase) + : (IReadOnlyDictionary>)new Dictionary>(StringComparer.OrdinalIgnoreCase); + return new ServiceAccountResponse( document.AccountId, document.Tenant, @@ -1462,7 +1498,8 @@ if (authorityOptions.Bootstrap.Enabled) document.Description, document.Enabled, scopes, - clients); + clients, + attributes); } static ServiceAccountTokenResponse MapDelegatedToken(AuthorityTokenDocument document) @@ -1498,39 +1535,39 @@ if (authorityOptions.Bootstrap.Enabled) AuthoritySigningKeyManager signingManager, ILogger signingLogger) => { - if (request is null) - { - signingLogger.LogWarning("Signing rotation request payload missing."); - return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); - } - - try - { - var result = signingManager.Rotate(request); - signingLogger.LogInformation("Signing key rotation completed. Active key {KeyId}.", result.ActiveKeyId); - - return Results.Ok(new - { - activeKeyId = result.ActiveKeyId, - provider = result.ActiveProvider, - source = result.ActiveSource, - location = result.ActiveLocation, - previousKeyId = result.PreviousKeyId, - retiredKeyIds = result.RetiredKeyIds - }); - } - catch (InvalidOperationException ex) - { - signingLogger.LogWarning(ex, "Signing rotation failed due to invalid input."); - return Results.BadRequest(new { error = "rotation_failed", message = ex.Message }); - } - catch (Exception ex) - { - signingLogger.LogError(ex, "Unexpected failure rotating signing key."); - return Results.Problem("Failed to rotate signing key."); - } - }); - + if (request is null) + { + signingLogger.LogWarning("Signing rotation request payload missing."); + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + try + { + var result = signingManager.Rotate(request); + signingLogger.LogInformation("Signing key rotation completed. Active key {KeyId}.", result.ActiveKeyId); + + return Results.Ok(new + { + activeKeyId = result.ActiveKeyId, + provider = result.ActiveProvider, + source = result.ActiveSource, + location = result.ActiveLocation, + previousKeyId = result.PreviousKeyId, + retiredKeyIds = result.RetiredKeyIds + }); + } + catch (InvalidOperationException ex) + { + signingLogger.LogWarning(ex, "Signing rotation failed due to invalid input."); + return Results.BadRequest(new { error = "rotation_failed", message = ex.Message }); + } + catch (Exception ex) + { + signingLogger.LogError(ex, "Unexpected failure rotating signing key."); + return Results.Problem("Failed to rotate signing key."); + } + }); + bootstrapGroup.MapPost("/notifications/ack-tokens/rotate", ( SigningRotationRequest? request, AuthorityAckTokenKeyManager ackManager, @@ -1593,1128 +1630,1473 @@ if (authorityOptions.Bootstrap.Enabled) return Results.Ok(new { activeKeyId = result.ActiveKeyId, - provider = result.ActiveProvider, - source = result.ActiveSource, - location = result.ActiveLocation, - previousKeyId = result.PreviousKeyId, - retiredKeyIds = result.RetiredKeyIds - }); - } - catch (InvalidOperationException ex) - { - ackLogger.LogWarning(ex, "Ack token rotation failed due to invalid input."); - return Results.BadRequest(new { error = "rotation_failed", message = ex.Message }); - } - catch (Exception ex) - { - ackLogger.LogError(ex, "Unexpected failure rotating ack token key."); - return Results.Problem("Failed to rotate ack token key."); - } - }); -} - -app.UseSerilogRequestLogging(options => -{ - options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => - { - diagnosticContext.Set("TraceId", Activity.Current?.TraceId.ToString()); - diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); - }; -}); - -app.UseExceptionHandler(static errorApp => -{ - errorApp.Run(async context => - { - context.Response.ContentType = "application/problem+json"; - var problem = Results.Problem( - statusCode: StatusCodes.Status500InternalServerError, - title: "Unhandled server error", - detail: "Unexpected failure while processing the request."); - - await problem.ExecuteAsync(context); - }); -}); - -app.UseLegacyAuthDeprecation(); -app.UseRouting(); -app.UseAuthorityRateLimiterContext(); -app.UseRateLimiter(); -app.UseAuthentication(); -app.UseAuthorization(); - -app.MapGet("/health", async (IAuthorityIdentityProviderRegistry registry, CancellationToken cancellationToken) => - { - var pluginHealth = new List(); - foreach (var providerMetadata in registry.Providers) - { - await using var handle = await registry.AcquireAsync(providerMetadata.Name, cancellationToken).ConfigureAwait(false); - var health = await handle.Provider.CheckHealthAsync(cancellationToken).ConfigureAwait(false); - pluginHealth.Add(new - { - provider = providerMetadata.Name, - status = health.Status.ToString().ToLowerInvariant(), - message = health.Message - }); - } - - return Results.Ok(new - { - status = "healthy", - identityProviders = pluginHealth - }); - }) - .WithName("HealthCheck"); - -app.MapGet("/ready", (IAuthorityIdentityProviderRegistry registry) => - Results.Ok(new - { - status = registry.Providers.Count > 0 ? "ready" : "degraded", - identityProviders = registry.Providers.Select(p => p.Name).ToArray() - })) - .WithName("ReadinessCheck"); - -app.MapPost("/permalinks/vuln", async ( - VulnPermalinkRequest request, - VulnPermalinkService service, - CancellationToken cancellationToken) => -{ - try - { - var response = await service.CreateAsync(request, cancellationToken).ConfigureAwait(false); - return Results.Ok(response); - } - catch (ArgumentException ex) - { - return Results.BadRequest(new { error = "invalid_request", message = ex.Message }); - } - catch (InvalidOperationException ex) - { - return Results.Problem(ex.Message); - } -}) - .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.VulnRead)) - .WithName("CreateVulnPermalink"); - -app.MapPost("/notify/ack-tokens/rotate", async ( - HttpContext context, - SigningRotationRequest? request, - AuthorityAckTokenKeyManager ackManager, - IOptions optionsAccessor, - IAuthEventSink auditSink, - TimeProvider timeProvider, - ILogger logger, - CancellationToken cancellationToken) => -{ - var scopes = ExtractScopes(context.User); - - if (request is null) - { - const string message = "Request payload is required."; - logger.LogWarning("Ack token rotation request payload missing."); - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Failure, - null, - null, - null, - null, - null, - message, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.BadRequest(new { error = "invalid_request", message }); - } - - logger.LogDebug( - "Ack token rotation request received. keyId='{KeyId}', location='{Location}', provider='{Provider}', source='{Source}'", - request.KeyId, - request.Location, - request.Provider, - request.Source); - - var notifications = optionsAccessor.Value.Notifications ?? throw new InvalidOperationException("Authority notifications configuration is missing."); - var ackOptions = notifications.AckTokens ?? throw new InvalidOperationException("Ack token configuration is missing."); - - var keyId = request.KeyId?.Trim(); - if (string.IsNullOrWhiteSpace(keyId)) - { - const string message = "Ack token key rotation requires a keyId."; - logger.LogWarning("Ack token rotation rejected: missing keyId."); - - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Failure, - activeKeyId: null, - previousKeyId: null, - retiredKeyIds: null, - provider: null, - source: null, - reason: message, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.BadRequest(new { error = "invalid_request", message }); - } - - var location = request.Location?.Trim(); - if (string.IsNullOrWhiteSpace(location)) - { - const string message = "Ack token key rotation requires a key path/location."; - logger.LogWarning("Ack token rotation rejected: missing key path/location."); - - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Failure, - activeKeyId: keyId, - previousKeyId: null, - retiredKeyIds: null, - provider: null, - source: null, - reason: message, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.BadRequest(new { error = "invalid_request", message }); - } - - var trimmedKeyId = keyId!; - var trimmedLocation = location!; - - if (!ackOptions.Enabled) - { - const string message = "Ack tokens are disabled. Enable notifications.ackTokens before rotating keys."; - logger.LogWarning("Ack token rotation attempted while ack tokens are disabled."); - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Failure, - trimmedKeyId, - null, - null, - null, - null, - message, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.BadRequest(new { error = "ack_tokens_disabled", message }); - } - - try - { - request.KeyId = trimmedKeyId; - request.Location = trimmedLocation; - - var result = ackManager.Rotate(request); - logger.LogInformation("Ack token key rotation completed. Active key {KeyId}.", result.ActiveKeyId); - - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Success, - result.ActiveKeyId, - result.PreviousKeyId, - result.RetiredKeyIds, - result.ActiveProvider, - result.ActiveSource, - reason: null, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.Ok(new - { - activeKeyId = result.ActiveKeyId, - provider = result.ActiveProvider, - source = result.ActiveSource, - location = result.ActiveLocation, - previousKeyId = result.PreviousKeyId, - retiredKeyIds = result.RetiredKeyIds - }); - } - catch (InvalidOperationException ex) - { - logger.LogWarning(ex, "Ack token rotation failed due to invalid input."); - - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Failure, - request.KeyId, - null, - null, - null, - null, - ex.Message, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.BadRequest(new { error = "rotation_failed", message = ex.Message }); - } - catch (Exception ex) - { - logger.LogError(ex, "Unexpected failure rotating ack token key."); - - const string message = "Unexpected failure rotating ack token key."; - await WriteAckRotationAuditAsync( - context, - auditSink, - timeProvider, - AuthEventOutcome.Failure, - request.KeyId, - null, - null, - null, - null, - message, - scopes, - cancellationToken).ConfigureAwait(false); - - return Results.Problem("Failed to rotate ack token key."); - } -}) - .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyAdmin)) - .WithName("RotateNotifyAckTokenKey"); - -app.MapPost("/notify/ack-tokens/issue", async ( - HttpContext httpContext, - AckTokenIssueRequest request, - AuthorityAckTokenIssuer issuer, - IAuthEventSink auditSink, - IOptions optionsAccessor, - TimeProvider timeProvider, - CancellationToken cancellationToken) => -{ - if (request is null) - { - return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); - } - - var escalationOptions = optionsAccessor.Value.Notifications.Escalation; - var escalationScope = escalationOptions.Scope; - var hasEscalateScope = !string.IsNullOrWhiteSpace(escalationScope) && PrincipalHasScope(httpContext.User, escalationScope); - var hasAdminScope = PrincipalHasScope(httpContext.User, StellaOpsScopes.NotifyAdmin); - - if (request.AllowEscalation) - { - if (!hasEscalateScope) - { - return Results.Json(new - { - error = "insufficient_scope", - message = $"Scope '{escalationScope}' is required to issue escalation-enabled ack tokens." - }, statusCode: StatusCodes.Status403Forbidden); - } - - if (escalationOptions.RequireAdminScope && !hasAdminScope) - { - return Results.Json(new - { - error = "insufficient_scope", - message = "Escalation-enabled ack tokens require notify.admin." - }, statusCode: StatusCodes.Status403Forbidden); - } - } - - AckTokenIssueResult result; - try - { - result = await issuer.IssueAsync(request, hasEscalateScope, cancellationToken).ConfigureAwait(false); - } - catch (InvalidOperationException ex) - { - return Results.BadRequest(new { error = "invalid_request", message = ex.Message }); - } - - var signatures = new List(); - foreach (var signature in result.Envelope.Signatures) - { - if (string.IsNullOrWhiteSpace(signature.KeyId) || string.IsNullOrWhiteSpace(signature.Signature)) - { - continue; - } - - signatures.Add(new AckTokenSignatureResponse( - signature.KeyId, - signature.Signature, - signature.Algorithm ?? string.Empty)); - } - - var response = new AckTokenIssueResponse( - result.Envelope.PayloadType ?? string.Empty, - result.Envelope.Payload ?? string.Empty, - signatures, - result.Payload.IssuedAt, - result.Payload.ExpiresAt, - result.Payload.Nonce); - - await WriteAckAuditAsync( - httpContext, - auditSink, - timeProvider, - "notify.ack.issued", - AuthEventOutcome.Success, - result.Payload, - request.AllowEscalation ? "escalate" : "ack", - ExtractScopes(httpContext.User), - cancellationToken).ConfigureAwait(false); - - return Results.Ok(response); -}) - .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyOperator)) - .WithName("IssueNotifyAckToken"); - -app.MapPost("/notify/ack-tokens/verify", async ( - HttpContext httpContext, - AckTokenVerifyRequest request, - AuthorityAckTokenVerifier verifier, - IAuthEventSink auditSink, - IOptions optionsAccessor, - TimeProvider timeProvider, - CancellationToken cancellationToken) => -{ - if (request?.Envelope is null) - { - return Results.BadRequest(new { error = "invalid_request", message = "Envelope is required." }); - } - - var normalizedAction = string.IsNullOrWhiteSpace(request.Action) - ? "ack" - : request.Action!.Trim().ToLowerInvariant(); - - AckTokenVerificationResult verification; - try - { - verification = await verifier.VerifyAsync(request.Envelope, normalizedAction, request.ExpectedTenant, cancellationToken).ConfigureAwait(false); - } - catch (InvalidOperationException ex) - { - return Results.BadRequest(new { error = "invalid_token", message = ex.Message }); - } - - if (normalizedAction == "escalate") - { - var escalationOptions = optionsAccessor.Value.Notifications.Escalation; - if (!verification.Payload.EscalationAllowed) - { - return Results.Json(new { error = "escalation_not_permitted", message = "Ack token does not permit escalation." }, statusCode: StatusCodes.Status403Forbidden); - } - - var escalationScope = escalationOptions.Scope; - if (string.IsNullOrWhiteSpace(escalationScope) || !PrincipalHasScope(httpContext.User, escalationScope)) - { - return Results.Json(new { error = "insufficient_scope", message = $"Scope '{escalationScope}' is required to perform escalation acknowledgement." }, statusCode: StatusCodes.Status403Forbidden); - } - - if (escalationOptions.RequireAdminScope && !PrincipalHasScope(httpContext.User, StellaOpsScopes.NotifyAdmin)) - { - return Results.Json(new { error = "insufficient_scope", message = "Escalation acknowledgement requires notify.admin." }, statusCode: StatusCodes.Status403Forbidden); - } - } - - var response = new AckTokenVerifyResponse( - verification.Payload.Tenant, - verification.Payload.NotificationId, - verification.Payload.DeliveryId, - verification.Payload.Channel, - verification.Payload.Actions, - verification.Payload.EscalationAllowed, - verification.Payload.ExpiresAt, - verification.Payload.Nonce); - - await WriteAckAuditAsync( - httpContext, - auditSink, - timeProvider, - normalizedAction == "escalate" ? "notify.ack.escalated" : "notify.ack.verified", - AuthEventOutcome.Success, - verification.Payload, - normalizedAction, - ExtractScopes(httpContext.User), - cancellationToken).ConfigureAwait(false); - - return Results.Ok(response); -}) - .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyOperator)) - .WithName("VerifyNotifyAckToken"); - -static IReadOnlyList ExtractScopes(ClaimsPrincipal principal) -{ - if (principal?.Identity?.IsAuthenticated != true) - { - return Array.Empty(); - } - - var scopes = new HashSet(StringComparer.Ordinal); - - foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope)) - { - foreach (var segment in claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - var normalized = StellaOpsScopes.Normalize(segment); - if (normalized is not null) - { - scopes.Add(normalized); - } - } - } - - foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem)) - { - var normalized = StellaOpsScopes.Normalize(claim.Value); - if (normalized is not null) - { - scopes.Add(normalized); - } - } - - return scopes.Count == 0 - ? Array.Empty() - : scopes.OrderBy(static value => value, StringComparer.Ordinal).ToArray(); -} - -static bool PrincipalHasScope(ClaimsPrincipal principal, string scope) -{ - if (principal?.Identity?.IsAuthenticated != true) - { - return false; - } - - var normalizedTarget = StellaOpsScopes.Normalize(scope); - if (normalizedTarget is null) - { - return false; - } - - foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope)) - { - foreach (var segment in claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - if (string.Equals(StellaOpsScopes.Normalize(segment), normalizedTarget, StringComparison.Ordinal)) - { - return true; - } - } - } - - foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem)) - { - if (string.Equals(StellaOpsScopes.Normalize(claim.Value), normalizedTarget, StringComparison.Ordinal)) - { - return true; - } - } - - return false; -} - -static AuthEventNetwork BuildNetwork(HttpContext context) -{ - context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwarded); - context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent); - - return new AuthEventNetwork - { - RemoteAddress = ClassifiedString.Public(context.Connection.RemoteIpAddress?.ToString()), - ForwardedFor = ClassifiedString.Public(forwarded.ToString()), - UserAgent = ClassifiedString.Public(userAgent.ToString()) - }; -} - -static AuthEventClient? BuildClientContext(ClaimsPrincipal principal) -{ - if (principal?.Identity?.IsAuthenticated != true) - { - return null; - } - - var clientId = principal.FindFirst(StellaOpsClaimTypes.ClientId)?.Value; - if (string.IsNullOrWhiteSpace(clientId)) - { - return null; - } - - var provider = principal.FindFirst(StellaOpsClaimTypes.IdentityProvider)?.Value; - - return new AuthEventClient - { - ClientId = ClassifiedString.Personal(clientId), - Provider = string.IsNullOrWhiteSpace(provider) - ? ClassifiedString.Empty - : ClassifiedString.Public(provider), - Name = ClassifiedString.Empty - }; -} - -static async Task WriteAckRotationAuditAsync( - HttpContext context, - IAuthEventSink auditSink, - TimeProvider timeProvider, - AuthEventOutcome outcome, - string? activeKeyId, - string? previousKeyId, - IReadOnlyCollection? retiredKeyIds, - string? provider, - string? source, - string? reason, - IReadOnlyList scopes, - CancellationToken cancellationToken) -{ - var eventType = outcome == AuthEventOutcome.Success - ? "notify.ack.key_rotated" - : "notify.ack.key_rotation_failed"; - - var properties = new List(); - - if (!string.IsNullOrWhiteSpace(activeKeyId)) - { - properties.Add(new AuthEventProperty - { - Name = "notify.ack.key_id", - Value = ClassifiedString.Public(activeKeyId) - }); - } - - if (!string.IsNullOrWhiteSpace(previousKeyId)) - { - properties.Add(new AuthEventProperty - { - Name = "notify.ack.previous_key_id", - Value = ClassifiedString.Public(previousKeyId) - }); - } - - if (!string.IsNullOrWhiteSpace(provider)) - { - properties.Add(new AuthEventProperty - { - Name = "notify.ack.provider", - Value = ClassifiedString.Public(provider) - }); - } - - if (!string.IsNullOrWhiteSpace(source)) - { - properties.Add(new AuthEventProperty - { - Name = "notify.ack.source", - Value = ClassifiedString.Public(source) - }); - } - - if (retiredKeyIds is { Count: > 0 }) - { - properties.Add(new AuthEventProperty - { - Name = "notify.ack.retired_key_ids", - Value = ClassifiedString.Public(string.Join(",", retiredKeyIds)) - }); - } - - var record = new AuthEventRecord - { - EventType = eventType, - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier, - Outcome = outcome, - Reason = reason, - Client = BuildClientContext(context.User), - Tenant = ClassifiedString.Empty, - Scopes = scopes, - Network = BuildNetwork(context), - Properties = properties - }; - - await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); -} - -static async Task WriteAckAuditAsync( - HttpContext context, - IAuthEventSink auditSink, - TimeProvider timeProvider, - string eventType, - AuthEventOutcome outcome, - AckTokenPayload payload, - string action, - IReadOnlyList scopes, - CancellationToken cancellationToken) -{ - var properties = new List - { - new() { Name = "notify.notification_id", Value = ClassifiedString.Public(payload.NotificationId) }, - new() { Name = "notify.delivery_id", Value = ClassifiedString.Public(payload.DeliveryId) }, - new() { Name = "notify.channel", Value = ClassifiedString.Public(payload.Channel) }, - new() { Name = "notify.webhook", Value = ClassifiedString.Public(payload.Webhook) }, - new() { Name = "notify.actions", Value = ClassifiedString.Public(string.Join(",", payload.Actions)) }, - new() { Name = "notify.action", Value = ClassifiedString.Public(action) }, - new() { Name = "notify.nonce", Value = ClassifiedString.Public(payload.Nonce) } - }; - - if (payload.EscalationAllowed) - { - properties.Add(new AuthEventProperty - { - Name = "notify.escalation_allowed", - Value = ClassifiedString.Public("true") - }); - } - - var record = new AuthEventRecord - { - EventType = eventType, - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier, - Outcome = outcome, - Client = BuildClientContext(context.User), - Tenant = ClassifiedString.Public(payload.Tenant), - Scopes = scopes, - Network = BuildNetwork(context), - Properties = properties - }; - - await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); -} - -var advisoryAiGroup = app.MapGroup("/advisory-ai"); - -advisoryAiGroup.MapPost("/remote-inference/logs", async ( - HttpContext httpContext, - AdvisoryAiRemoteInferenceLogRequest request, - IAuthorityAdvisoryAiConsentEvaluator consentEvaluator, - IAuthEventSink auditSink, - TimeProvider timeProvider, - IAuthorityRateLimiterMetadataAccessor metadataAccessor, - ILoggerFactory loggerFactory, - CancellationToken cancellationToken) => -{ - const int MaxPromptLength = 16_384; - const int MaxMetadataEntries = 16; - const int MaxMetadataValueLength = 256; - - if (request is null) - { - return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); - } - - var snapshot = consentEvaluator.GetSnapshot(); - if (!snapshot.Enabled) - { - return Results.Json( - new { error = "remote_inference_disabled", message = "Remote inference is disabled by configuration." }, - statusCode: StatusCodes.Status403Forbidden); - } - - var principal = httpContext.User; - if (principal?.Identity?.IsAuthenticated != true) - { - return Results.Json( - new { error = "unauthorized", message = "Authentication is required." }, - statusCode: StatusCodes.Status401Unauthorized); - } - - var tenantClaim = principal.FindFirst(StellaOpsClaimTypes.Tenant)?.Value; - var clientId = principal.FindFirst(StellaOpsClaimTypes.ClientId)?.Value; - var projectClaim = principal.FindFirst(StellaOpsClaimTypes.Project)?.Value; - - var consentResult = consentEvaluator.EvaluateTenant(tenantClaim); - if (!consentResult.Allowed) - { - return Results.Json( - new { error = consentResult.ErrorCode, message = consentResult.ErrorMessage }, - statusCode: StatusCodes.Status403Forbidden); - } - - if (!consentEvaluator.TryNormalizeProfile(request.Profile, out var profile)) - { - return Results.Json( - new { error = "profile_not_allowed", message = "Requested remote inference profile is not allowed." }, - statusCode: StatusCodes.Status400BadRequest); - } - - var taskType = NormalizeTaskType(request.TaskType); - if (taskType is null) - { - return Results.Json( - new { error = "task_type_invalid", message = "Task type must contain only lowercase letters, digits, dot, underscore, or hyphen (1-64 characters)." }, - statusCode: StatusCodes.Status400BadRequest); - } - - var prompt = NormalizePrompt(request.Prompt); - if (prompt is null) - { - return Results.Json( - new { error = "prompt_required", message = "Prompt content is required." }, - statusCode: StatusCodes.Status400BadRequest); - } - - if (prompt.Length > MaxPromptLength) - { - return Results.Json( - new { error = "prompt_too_long", message = $"Prompt exceeds maximum length of {MaxPromptLength} characters." }, - statusCode: StatusCodes.Status400BadRequest); - } - - var promptHash = ComputeSha256(prompt); - var metadata = metadataAccessor.GetMetadata(); - metadataAccessor.SetClientId(clientId); - metadataAccessor.SetTenant(tenantClaim); - metadataAccessor.SetProject(projectClaim); - metadataAccessor.SetTag("authority.advisory_ai.profile", profile); - metadataAccessor.SetTag("authority.advisory_ai.task_type", taskType); - - var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier; - var network = BuildNetwork(metadata); - var scopes = ExtractScopes(principal); - - var auditProperties = BuildProperties( - request, - taskType, - profile, - promptHash, - prompt.Length, - consentResult); - - var tenantNormalized = NormalizeTenant(tenantClaim); - var projectNormalized = NormalizeProject(projectClaim); - - var record = new AuthEventRecord - { - EventType = "authority.advisory_ai.remote_inference", - OccurredAt = timeProvider.GetUtcNow(), - CorrelationId = correlationId, - Outcome = AuthEventOutcome.Success, - Client = BuildClient(clientId), - Tenant = ClassifiedString.Public(tenantNormalized), - Project = ClassifiedString.Public(projectNormalized), - Scopes = scopes, - Network = network, - Properties = auditProperties - }; - - await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); - - var logger = loggerFactory.CreateLogger("StellaOps.Authority.AdvisoryAi"); - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation( - "Recorded advisory AI remote inference task {TaskType} for tenant {Tenant} with profile {Profile} (hash={PromptHash}).", - taskType, - tenantNormalized, - profile, - promptHash); - } - - return Results.Json(new - { - status = "logged", - prompt_hash = promptHash, - correlation_id = correlationId, - consent_version = consentResult.ConsentVersion - }); - - static string? NormalizeTaskType(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } - - var trimmed = value.Trim().ToLowerInvariant(); - if (trimmed.Length is < 1 or > 64) - { - return null; - } - - foreach (var ch in trimmed) - { - var allowed = char.IsLetterOrDigit(ch) || ch is '-' or '_' or '.'; - if (!allowed) - { - return null; - } - } - - return trimmed; - } - - static string? NormalizePrompt(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } - - var normalized = value.Trim(); - return normalized.Replace("\r\n", "\n", StringComparison.Ordinal); - } - - static string NormalizeTenant(string? value) - { - return string.IsNullOrWhiteSpace(value) - ? string.Empty - : value.Trim().ToLowerInvariant(); - } - - static string NormalizeProject(string? value) - { - return string.IsNullOrWhiteSpace(value) - ? StellaOpsTenancyDefaults.AnyProject - : value.Trim().ToLowerInvariant(); - } - - static AuthEventClient? BuildClient(string? clientId) - { - if (string.IsNullOrWhiteSpace(clientId)) - { - return null; - } - - return new AuthEventClient - { - ClientId = ClassifiedString.Personal(clientId.Trim()), - Name = ClassifiedString.Empty, - Provider = ClassifiedString.Empty - }; - } - - static AuthEventNetwork? BuildNetwork(AuthorityRateLimiterMetadata? metadata) - { - if (metadata is null) - { - return null; - } - - var remote = string.IsNullOrWhiteSpace(metadata.RemoteIp) ? null : metadata.RemoteIp; - var forwarded = string.IsNullOrWhiteSpace(metadata.ForwardedFor) ? null : metadata.ForwardedFor; - var userAgent = string.IsNullOrWhiteSpace(metadata.UserAgent) ? null : metadata.UserAgent; - - if (remote is null && forwarded is null && userAgent is null) - { - return null; - } - - return new AuthEventNetwork - { - RemoteAddress = ClassifiedString.Personal(remote), - ForwardedFor = ClassifiedString.Personal(forwarded), - UserAgent = ClassifiedString.Personal(userAgent) - }; - } - - static IReadOnlyList ExtractScopes(ClaimsPrincipal principal) - { - var set = new HashSet(StringComparer.Ordinal); - - foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem)) - { - if (string.IsNullOrWhiteSpace(claim.Value)) - { - continue; - } - - set.Add(claim.Value.Trim()); - } - - foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope)) - { - if (string.IsNullOrWhiteSpace(claim.Value)) - { - continue; - } - - var parts = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var part in parts) - { - var normalized = StellaOpsScopes.Normalize(part); - if (!string.IsNullOrWhiteSpace(normalized)) - { - set.Add(normalized); - } - } - } - - return set.Count == 0 - ? Array.Empty() - : set.OrderBy(static scope => scope, StringComparer.Ordinal).ToArray(); - } - - static IReadOnlyList BuildProperties( - AdvisoryAiRemoteInferenceLogRequest request, - string taskType, - string profile, - string promptHash, - int promptLength, - AuthorityTenantRemoteInferenceConsentResult consentResult) - { - var properties = new List - { - CreateProperty("advisory_ai.task_type", taskType), - CreateProperty("advisory_ai.profile", profile), - CreateProperty("advisory_ai.prompt.hash", promptHash), - CreateProperty("advisory_ai.prompt.length", promptLength.ToString(CultureInfo.InvariantCulture)), - CreateProperty("advisory_ai.prompt.algorithm", "sha256") - }; - - if (!string.IsNullOrWhiteSpace(request.ModelId)) - { - properties.Add(CreateProperty("advisory_ai.model_id", request.ModelId.Trim())); - } - - if (!string.IsNullOrWhiteSpace(request.ContextDigest)) - { - properties.Add(CreateProperty("advisory_ai.context.digest", request.ContextDigest.Trim())); - } - - if (!string.IsNullOrWhiteSpace(request.OutputHash)) - { - properties.Add(CreateProperty("advisory_ai.output.hash", request.OutputHash.Trim())); - } - - if (!string.IsNullOrWhiteSpace(request.TaskId)) - { - properties.Add(CreateProperty("advisory_ai.task_id", request.TaskId.Trim())); - } - - if (!string.IsNullOrWhiteSpace(consentResult.ConsentVersion)) - { - properties.Add(CreateProperty("advisory_ai.consent.version", consentResult.ConsentVersion.Trim())); - } - - if (consentResult.ConsentedAt.HasValue) - { - properties.Add(CreateProperty( - "advisory_ai.consent.timestamp", - consentResult.ConsentedAt.Value.ToString("O", CultureInfo.InvariantCulture))); - } - - if (!string.IsNullOrWhiteSpace(consentResult.ConsentedBy)) - { - properties.Add(CreateProperty("advisory_ai.consent.actor", consentResult.ConsentedBy.Trim())); - } - - if (request.Metadata is { Count: > 0 }) - { - var appended = 0; - foreach (var (key, value) in request.Metadata) - { - if (appended >= MaxMetadataEntries) - { - break; - } - - var normalizedKey = NormalizeMetadataKey(key); - if (normalizedKey is null) - { - continue; - } - - var normalizedValue = NormalizeMetadataValue(value); - properties.Add(CreateProperty($"advisory_ai.metadata.{normalizedKey}", normalizedValue)); - appended++; - } - } - - return properties; - } - - static AuthEventProperty CreateProperty(string name, string? value) - { - return new AuthEventProperty - { - Name = name, - Value = ClassifiedString.Public(string.IsNullOrWhiteSpace(value) ? null : value.Trim()) - }; - } - - static string NormalizeMetadataValue(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return string.Empty; - } - - var trimmed = value.Trim(); - return trimmed.Length <= MaxMetadataValueLength - ? trimmed - : trimmed[..MaxMetadataValueLength]; - } - - static string? NormalizeMetadataKey(string? key) - { - if (string.IsNullOrWhiteSpace(key)) - { - return null; - } - - var trimmed = key.Trim().ToLowerInvariant(); - if (trimmed.Length is < 1 or > 32) - { - return null; - } - - foreach (var ch in trimmed) - { - var allowed = char.IsLetterOrDigit(ch) || ch is '-' or '_' or '.'; - if (!allowed) - { - return null; - } - } - - return trimmed; - } - - static string ComputeSha256(string value) - { - var bytes = Encoding.UTF8.GetBytes(value); - var hash = SHA256.HashData(bytes); - return Convert.ToHexString(hash).ToLowerInvariant(); - } - -}) - .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AdvisoryAiOperate)) - .WithName("LogAdvisoryAiRemoteInference"); - - -app.MapAirgapAuditEndpoints(); -app.MapIncidentAuditEndpoints(); -app.MapAuthorityOpenApiDiscovery(); - - - - -app.MapGet("/jwks", (AuthorityJwksService jwksService, HttpContext context) => -{ - var result = jwksService.Get(); - - if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etagValues) && - etagValues.Contains(result.ETag, StringComparer.Ordinal)) - { - context.Response.Headers.CacheControl = result.CacheControl; - context.Response.Headers.ETag = result.ETag; - context.Response.Headers.Expires = result.ExpiresAt.ToString("R", CultureInfo.InvariantCulture); - return Results.StatusCode(StatusCodes.Status304NotModified); - } - - context.Response.Headers.CacheControl = result.CacheControl; - context.Response.Headers.ETag = result.ETag; - context.Response.Headers.Expires = result.ExpiresAt.ToString("R", CultureInfo.InvariantCulture); - - return Results.Json(result.Response); -}) - .WithName("JsonWebKeySet"); - -// Ensure signing key manager initialises key material on startup. + provider = result.ActiveProvider, + source = result.ActiveSource, + location = result.ActiveLocation, + previousKeyId = result.PreviousKeyId, + retiredKeyIds = result.RetiredKeyIds + }); + } + catch (InvalidOperationException ex) + { + ackLogger.LogWarning(ex, "Ack token rotation failed due to invalid input."); + return Results.BadRequest(new { error = "rotation_failed", message = ex.Message }); + } + catch (Exception ex) + { + ackLogger.LogError(ex, "Unexpected failure rotating ack token key."); + return Results.Problem("Failed to rotate ack token key."); + } + }); +} + +app.UseSerilogRequestLogging(options => +{ + options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => + { + diagnosticContext.Set("TraceId", Activity.Current?.TraceId.ToString()); + diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); + }; +}); + +app.UseExceptionHandler(static errorApp => +{ + errorApp.Run(async context => + { + context.Response.ContentType = "application/problem+json"; + var problem = Results.Problem( + statusCode: StatusCodes.Status500InternalServerError, + title: "Unhandled server error", + detail: "Unexpected failure while processing the request."); + + await problem.ExecuteAsync(context); + }); +}); + +app.UseLegacyAuthDeprecation(); +app.UseRouting(); +app.UseAuthorityRateLimiterContext(); +app.UseRateLimiter(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/health", async (IAuthorityIdentityProviderRegistry registry, CancellationToken cancellationToken) => + { + var pluginHealth = new List(); + foreach (var providerMetadata in registry.Providers) + { + await using var handle = await registry.AcquireAsync(providerMetadata.Name, cancellationToken).ConfigureAwait(false); + var health = await handle.Provider.CheckHealthAsync(cancellationToken).ConfigureAwait(false); + pluginHealth.Add(new + { + provider = providerMetadata.Name, + status = health.Status.ToString().ToLowerInvariant(), + message = health.Message + }); + } + + return Results.Ok(new + { + status = "healthy", + identityProviders = pluginHealth + }); + }) + .WithName("HealthCheck"); + +app.MapGet("/ready", (IAuthorityIdentityProviderRegistry registry) => + Results.Ok(new + { + status = registry.Providers.Count > 0 ? "ready" : "degraded", + identityProviders = registry.Providers.Select(p => p.Name).ToArray() + })) + .WithName("ReadinessCheck"); + +app.MapPost("/permalinks/vuln", async ( + VulnPermalinkRequest request, + VulnPermalinkService service, + CancellationToken cancellationToken) => +{ + try + { + var response = await service.CreateAsync(request, cancellationToken).ConfigureAwait(false); + return Results.Ok(response); + } + catch (ArgumentException ex) + { + return Results.BadRequest(new { error = "invalid_request", message = ex.Message }); + } + catch (InvalidOperationException ex) + { + return Results.Problem(ex.Message); + } +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.VulnView)) + .WithName("CreateVulnPermalink"); + +app.MapPost("/notify/ack-tokens/rotate", async ( + HttpContext context, + SigningRotationRequest? request, + AuthorityAckTokenKeyManager ackManager, + IOptions optionsAccessor, + IAuthEventSink auditSink, + TimeProvider timeProvider, + ILogger logger, + CancellationToken cancellationToken) => +{ + var scopes = ExtractScopes(context.User); + + if (request is null) + { + const string message = "Request payload is required."; + logger.LogWarning("Ack token rotation request payload missing."); + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Failure, + null, + null, + null, + null, + null, + message, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.BadRequest(new { error = "invalid_request", message }); + } + + logger.LogDebug( + "Ack token rotation request received. keyId='{KeyId}', location='{Location}', provider='{Provider}', source='{Source}'", + request.KeyId, + request.Location, + request.Provider, + request.Source); + + var notifications = optionsAccessor.Value.Notifications ?? throw new InvalidOperationException("Authority notifications configuration is missing."); + var ackOptions = notifications.AckTokens ?? throw new InvalidOperationException("Ack token configuration is missing."); + + var keyId = request.KeyId?.Trim(); + if (string.IsNullOrWhiteSpace(keyId)) + { + const string message = "Ack token key rotation requires a keyId."; + logger.LogWarning("Ack token rotation rejected: missing keyId."); + + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Failure, + activeKeyId: null, + previousKeyId: null, + retiredKeyIds: null, + provider: null, + source: null, + reason: message, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.BadRequest(new { error = "invalid_request", message }); + } + + var location = request.Location?.Trim(); + if (string.IsNullOrWhiteSpace(location)) + { + const string message = "Ack token key rotation requires a key path/location."; + logger.LogWarning("Ack token rotation rejected: missing key path/location."); + + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Failure, + activeKeyId: keyId, + previousKeyId: null, + retiredKeyIds: null, + provider: null, + source: null, + reason: message, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.BadRequest(new { error = "invalid_request", message }); + } + + var trimmedKeyId = keyId!; + var trimmedLocation = location!; + + if (!ackOptions.Enabled) + { + const string message = "Ack tokens are disabled. Enable notifications.ackTokens before rotating keys."; + logger.LogWarning("Ack token rotation attempted while ack tokens are disabled."); + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Failure, + trimmedKeyId, + null, + null, + null, + null, + message, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.BadRequest(new { error = "ack_tokens_disabled", message }); + } + + try + { + request.KeyId = trimmedKeyId; + request.Location = trimmedLocation; + + var result = ackManager.Rotate(request); + logger.LogInformation("Ack token key rotation completed. Active key {KeyId}.", result.ActiveKeyId); + + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Success, + result.ActiveKeyId, + result.PreviousKeyId, + result.RetiredKeyIds, + result.ActiveProvider, + result.ActiveSource, + reason: null, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.Ok(new + { + activeKeyId = result.ActiveKeyId, + provider = result.ActiveProvider, + source = result.ActiveSource, + location = result.ActiveLocation, + previousKeyId = result.PreviousKeyId, + retiredKeyIds = result.RetiredKeyIds + }); + } + catch (InvalidOperationException ex) + { + logger.LogWarning(ex, "Ack token rotation failed due to invalid input."); + + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Failure, + request.KeyId, + null, + null, + null, + null, + ex.Message, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.BadRequest(new { error = "rotation_failed", message = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected failure rotating ack token key."); + + const string message = "Unexpected failure rotating ack token key."; + await WriteAckRotationAuditAsync( + context, + auditSink, + timeProvider, + AuthEventOutcome.Failure, + request.KeyId, + null, + null, + null, + null, + message, + scopes, + cancellationToken).ConfigureAwait(false); + + return Results.Problem("Failed to rotate ack token key."); + } +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyAdmin)) + .WithName("RotateNotifyAckTokenKey"); + +app.MapPost("/notify/ack-tokens/issue", async ( + HttpContext httpContext, + AckTokenIssueRequest request, + AuthorityAckTokenIssuer issuer, + IAuthEventSink auditSink, + IOptions optionsAccessor, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + var escalationOptions = optionsAccessor.Value.Notifications.Escalation; + var escalationScope = escalationOptions.Scope; + var hasEscalateScope = !string.IsNullOrWhiteSpace(escalationScope) && PrincipalHasScope(httpContext.User, escalationScope); + var hasAdminScope = PrincipalHasScope(httpContext.User, StellaOpsScopes.NotifyAdmin); + + if (request.AllowEscalation) + { + if (!hasEscalateScope) + { + return Results.Json(new + { + error = "insufficient_scope", + message = $"Scope '{escalationScope}' is required to issue escalation-enabled ack tokens." + }, statusCode: StatusCodes.Status403Forbidden); + } + + if (escalationOptions.RequireAdminScope && !hasAdminScope) + { + return Results.Json(new + { + error = "insufficient_scope", + message = "Escalation-enabled ack tokens require notify.admin." + }, statusCode: StatusCodes.Status403Forbidden); + } + } + + AckTokenIssueResult result; + try + { + result = await issuer.IssueAsync(request, hasEscalateScope, cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = "invalid_request", message = ex.Message }); + } + + var signatures = new List(); + foreach (var signature in result.Envelope.Signatures) + { + if (string.IsNullOrWhiteSpace(signature.KeyId) || string.IsNullOrWhiteSpace(signature.Signature)) + { + continue; + } + + signatures.Add(new AckTokenSignatureResponse( + signature.KeyId, + signature.Signature, + signature.Algorithm ?? string.Empty)); + } + + var response = new AckTokenIssueResponse( + result.Envelope.PayloadType ?? string.Empty, + result.Envelope.Payload ?? string.Empty, + signatures, + result.Payload.IssuedAt, + result.Payload.ExpiresAt, + result.Payload.Nonce); + + await WriteAckAuditAsync( + httpContext, + auditSink, + timeProvider, + "notify.ack.issued", + AuthEventOutcome.Success, + result.Payload, + request.AllowEscalation ? "escalate" : "ack", + ExtractScopes(httpContext.User), + cancellationToken).ConfigureAwait(false); + + return Results.Ok(response); +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyOperator)) + .WithName("IssueNotifyAckToken"); + +app.MapPost("/notify/ack-tokens/verify", async ( + HttpContext httpContext, + AckTokenVerifyRequest request, + AuthorityAckTokenVerifier verifier, + IAuthEventSink auditSink, + IOptions optionsAccessor, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request?.Envelope is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Envelope is required." }); + } + + var normalizedAction = string.IsNullOrWhiteSpace(request.Action) + ? "ack" + : request.Action!.Trim().ToLowerInvariant(); + + AckTokenVerificationResult verification; + try + { + verification = await verifier.VerifyAsync(request.Envelope, normalizedAction, request.ExpectedTenant, cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = "invalid_token", message = ex.Message }); + } + + if (normalizedAction == "escalate") + { + var escalationOptions = optionsAccessor.Value.Notifications.Escalation; + if (!verification.Payload.EscalationAllowed) + { + return Results.Json(new { error = "escalation_not_permitted", message = "Ack token does not permit escalation." }, statusCode: StatusCodes.Status403Forbidden); + } + + var escalationScope = escalationOptions.Scope; + if (string.IsNullOrWhiteSpace(escalationScope) || !PrincipalHasScope(httpContext.User, escalationScope)) + { + return Results.Json(new { error = "insufficient_scope", message = $"Scope '{escalationScope}' is required to perform escalation acknowledgement." }, statusCode: StatusCodes.Status403Forbidden); + } + + if (escalationOptions.RequireAdminScope && !PrincipalHasScope(httpContext.User, StellaOpsScopes.NotifyAdmin)) + { + return Results.Json(new { error = "insufficient_scope", message = "Escalation acknowledgement requires notify.admin." }, statusCode: StatusCodes.Status403Forbidden); + } + } + + var response = new AckTokenVerifyResponse( + verification.Payload.Tenant, + verification.Payload.NotificationId, + verification.Payload.DeliveryId, + verification.Payload.Channel, + verification.Payload.Actions, + verification.Payload.EscalationAllowed, + verification.Payload.ExpiresAt, + verification.Payload.Nonce); + + await WriteAckAuditAsync( + httpContext, + auditSink, + timeProvider, + normalizedAction == "escalate" ? "notify.ack.escalated" : "notify.ack.verified", + AuthEventOutcome.Success, + verification.Payload, + normalizedAction, + ExtractScopes(httpContext.User), + cancellationToken).ConfigureAwait(false); + + return Results.Ok(response); +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyOperator)) + .WithName("VerifyNotifyAckToken"); + +static IReadOnlyList ExtractScopes(ClaimsPrincipal principal) +{ + if (principal?.Identity?.IsAuthenticated != true) + { + return Array.Empty(); + } + + var scopes = new HashSet(StringComparer.Ordinal); + + foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope)) + { + foreach (var segment in claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var normalized = StellaOpsScopes.Normalize(segment); + if (normalized is not null) + { + scopes.Add(normalized); + } + } + } + + foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem)) + { + var normalized = StellaOpsScopes.Normalize(claim.Value); + if (normalized is not null) + { + scopes.Add(normalized); + } + } + + return scopes.Count == 0 + ? Array.Empty() + : scopes.OrderBy(static value => value, StringComparer.Ordinal).ToArray(); +} + +static bool PrincipalHasScope(ClaimsPrincipal principal, string scope) +{ + if (principal?.Identity?.IsAuthenticated != true) + { + return false; + } + + var normalizedTarget = StellaOpsScopes.Normalize(scope); + if (normalizedTarget is null) + { + return false; + } + + foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope)) + { + foreach (var segment in claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (string.Equals(StellaOpsScopes.Normalize(segment), normalizedTarget, StringComparison.Ordinal)) + { + return true; + } + } + } + + foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem)) + { + if (string.Equals(StellaOpsScopes.Normalize(claim.Value), normalizedTarget, StringComparison.Ordinal)) + { + return true; + } + } + + return false; +} + +static AuthEventNetwork BuildNetwork(HttpContext context) +{ + context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwarded); + context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent); + + return new AuthEventNetwork + { + RemoteAddress = ClassifiedString.Public(context.Connection.RemoteIpAddress?.ToString()), + ForwardedFor = ClassifiedString.Public(forwarded.ToString()), + UserAgent = ClassifiedString.Public(userAgent.ToString()) + }; +} + +static AuthEventClient? BuildClientContext(ClaimsPrincipal principal) +{ + if (principal?.Identity?.IsAuthenticated != true) + { + return null; + } + + var clientId = principal.FindFirst(StellaOpsClaimTypes.ClientId)?.Value; + if (string.IsNullOrWhiteSpace(clientId)) + { + return null; + } + + var provider = principal.FindFirst(StellaOpsClaimTypes.IdentityProvider)?.Value; + + return new AuthEventClient + { + ClientId = ClassifiedString.Personal(clientId), + Provider = string.IsNullOrWhiteSpace(provider) + ? ClassifiedString.Empty + : ClassifiedString.Public(provider), + Name = ClassifiedString.Empty + }; +} + +static async Task WriteAckRotationAuditAsync( + HttpContext context, + IAuthEventSink auditSink, + TimeProvider timeProvider, + AuthEventOutcome outcome, + string? activeKeyId, + string? previousKeyId, + IReadOnlyCollection? retiredKeyIds, + string? provider, + string? source, + string? reason, + IReadOnlyList scopes, + CancellationToken cancellationToken) +{ + var eventType = outcome == AuthEventOutcome.Success + ? "notify.ack.key_rotated" + : "notify.ack.key_rotation_failed"; + + var properties = new List(); + + if (!string.IsNullOrWhiteSpace(activeKeyId)) + { + properties.Add(new AuthEventProperty + { + Name = "notify.ack.key_id", + Value = ClassifiedString.Public(activeKeyId) + }); + } + + if (!string.IsNullOrWhiteSpace(previousKeyId)) + { + properties.Add(new AuthEventProperty + { + Name = "notify.ack.previous_key_id", + Value = ClassifiedString.Public(previousKeyId) + }); + } + + if (!string.IsNullOrWhiteSpace(provider)) + { + properties.Add(new AuthEventProperty + { + Name = "notify.ack.provider", + Value = ClassifiedString.Public(provider) + }); + } + + if (!string.IsNullOrWhiteSpace(source)) + { + properties.Add(new AuthEventProperty + { + Name = "notify.ack.source", + Value = ClassifiedString.Public(source) + }); + } + + if (retiredKeyIds is { Count: > 0 }) + { + properties.Add(new AuthEventProperty + { + Name = "notify.ack.retired_key_ids", + Value = ClassifiedString.Public(string.Join(",", retiredKeyIds)) + }); + } + + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier, + Outcome = outcome, + Reason = reason, + Client = BuildClientContext(context.User), + Tenant = ClassifiedString.Empty, + Scopes = scopes, + Network = BuildNetwork(context), + Properties = properties + }; + + await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); +} + +static async Task WriteAckAuditAsync( + HttpContext context, + IAuthEventSink auditSink, + TimeProvider timeProvider, + string eventType, + AuthEventOutcome outcome, + AckTokenPayload payload, + string action, + IReadOnlyList scopes, + CancellationToken cancellationToken) +{ + var properties = new List + { + new() { Name = "notify.notification_id", Value = ClassifiedString.Public(payload.NotificationId) }, + new() { Name = "notify.delivery_id", Value = ClassifiedString.Public(payload.DeliveryId) }, + new() { Name = "notify.channel", Value = ClassifiedString.Public(payload.Channel) }, + new() { Name = "notify.webhook", Value = ClassifiedString.Public(payload.Webhook) }, + new() { Name = "notify.actions", Value = ClassifiedString.Public(string.Join(",", payload.Actions)) }, + new() { Name = "notify.action", Value = ClassifiedString.Public(action) }, + new() { Name = "notify.nonce", Value = ClassifiedString.Public(payload.Nonce) } + }; + + if (payload.EscalationAllowed) + { + properties.Add(new AuthEventProperty + { + Name = "notify.escalation_allowed", + Value = ClassifiedString.Public("true") + }); + } + + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier, + Outcome = outcome, + Client = BuildClientContext(context.User), + Tenant = ClassifiedString.Public(payload.Tenant), + Scopes = scopes, + Network = BuildNetwork(context), + Properties = properties + }; + + await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); +} + +static async Task WriteWorkflowAntiForgeryAuditAsync( + HttpContext context, + IAuthEventSink auditSink, + TimeProvider timeProvider, + string eventType, + AuthEventOutcome outcome, + VulnWorkflowAntiForgeryPayload payload, + IReadOnlyList scopes, + CancellationToken cancellationToken) +{ + var properties = new List + { + new() { Name = "vuln.workflow.actor", Value = ClassifiedString.Public(payload.Subject) }, + new() { Name = "vuln.workflow.nonce", Value = ClassifiedString.Public(payload.Nonce) }, + new() { Name = "vuln.workflow.actions", Value = ClassifiedString.Public(string.Join(",", payload.Actions)) } + }; + + if (payload.Environments is { Count: > 0 }) + { + properties.Add(new AuthEventProperty + { + Name = "vuln.workflow.env", + Value = ClassifiedString.Public(string.Join(",", payload.Environments)) + }); + } + + if (payload.Owners is { Count: > 0 }) + { + properties.Add(new AuthEventProperty + { + Name = "vuln.workflow.owner", + Value = ClassifiedString.Public(string.Join(",", payload.Owners)) + }); + } + + if (payload.BusinessTiers is { Count: > 0 }) + { + properties.Add(new AuthEventProperty + { + Name = "vuln.workflow.business_tier", + Value = ClassifiedString.Public(string.Join(",", payload.BusinessTiers)) + }); + } + + if (payload.Context is { Count: > 0 }) + { + foreach (var (key, value) in payload.Context) + { + properties.Add(new AuthEventProperty + { + Name = $"vuln.workflow.context.{key}", + Value = ClassifiedString.Public(value) + }); + } + } + + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier, + Outcome = outcome, + Client = BuildClientContext(context.User), + Tenant = ClassifiedString.Public(payload.Tenant), + Scopes = scopes, + Network = BuildNetwork(context), + Properties = properties + }; + + await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); +} + +static async Task WriteAttachmentAuditAsync( + HttpContext context, + IAuthEventSink auditSink, + TimeProvider timeProvider, + string eventType, + AuthEventOutcome outcome, + VulnAttachmentTokenPayload payload, + IReadOnlyList scopes, + CancellationToken cancellationToken) +{ + var properties = new List + { + new() { Name = "vuln.attachment.ledger_hash", Value = ClassifiedString.Public(payload.LedgerEventHash) }, + new() { Name = "vuln.attachment.id", Value = ClassifiedString.Public(payload.AttachmentId) }, + new() { Name = "vuln.attachment.actor", Value = ClassifiedString.Public(payload.Actor) } + }; + + if (!string.IsNullOrWhiteSpace(payload.FindingId)) + { + properties.Add(new AuthEventProperty + { + Name = "vuln.attachment.finding", + Value = ClassifiedString.Public(payload.FindingId) + }); + } + + if (!string.IsNullOrWhiteSpace(payload.ContentHash)) + { + properties.Add(new AuthEventProperty + { + Name = "vuln.attachment.content_hash", + Value = ClassifiedString.Public(payload.ContentHash) + }); + } + + if (!string.IsNullOrWhiteSpace(payload.ContentType)) + { + properties.Add(new AuthEventProperty + { + Name = "vuln.attachment.content_type", + Value = ClassifiedString.Public(payload.ContentType) + }); + } + + if (payload.Metadata is { Count: > 0 }) + { + foreach (var (key, value) in payload.Metadata) + { + properties.Add(new AuthEventProperty + { + Name = $"vuln.attachment.metadata.{key}", + Value = ClassifiedString.Public(value) + }); + } + } + + var record = new AuthEventRecord + { + EventType = eventType, + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier, + Outcome = outcome, + Client = BuildClientContext(context.User), + Tenant = ClassifiedString.Public(payload.Tenant), + Scopes = scopes, + Network = BuildNetwork(context), + Properties = properties + }; + + await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); +} + +var vulnGroup = app.MapGroup("/vuln"); + +vulnGroup.MapPost("/workflow/anti-forgery/issue", async ( + HttpContext httpContext, + VulnWorkflowAntiForgeryIssueRequest request, + VulnWorkflowAntiForgeryTokenIssuer issuer, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + VulnWorkflowAntiForgeryIssueResult result; + try + { + result = await issuer.IssueAsync(httpContext.User, request, cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = "invalid_request", message = ex.Message }); + } + + await WriteWorkflowAntiForgeryAuditAsync( + httpContext, + auditSink, + timeProvider, + "vuln.workflow.csrf.issued", + AuthEventOutcome.Success, + result.Payload, + ExtractScopes(httpContext.User), + cancellationToken).ConfigureAwait(false); + + var response = new VulnWorkflowAntiForgeryIssueResponse( + result.Token, + DateTimeOffset.FromUnixTimeSeconds(result.Payload.IssuedAt), + DateTimeOffset.FromUnixTimeSeconds(result.Payload.ExpiresAt), + result.Payload.Actions, + result.Payload.Nonce); + + return Results.Ok(response); +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.VulnOperate)) + .WithName("IssueVulnWorkflowAntiForgeryToken"); + +vulnGroup.MapPost("/workflow/anti-forgery/verify", async ( + HttpContext httpContext, + VulnWorkflowAntiForgeryVerifyRequest request, + VulnWorkflowAntiForgeryTokenVerifier verifier, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request?.Token is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Token is required." }); + } + + VulnWorkflowAntiForgeryVerificationResult verification; + try + { + verification = await verifier.VerifyAsync( + request.Token, + request.RequiredAction, + request.Tenant, + request.Nonce, + cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = "invalid_token", message = ex.Message }); + } + + await WriteWorkflowAntiForgeryAuditAsync( + httpContext, + auditSink, + timeProvider, + "vuln.workflow.csrf.verified", + AuthEventOutcome.Success, + verification.Payload, + ExtractScopes(httpContext.User), + cancellationToken).ConfigureAwait(false); + + var response = new VulnWorkflowAntiForgeryVerifyResponse( + verification.Payload.Tenant, + verification.Payload.Subject, + verification.Payload.Actions, + DateTimeOffset.FromUnixTimeSeconds(verification.Payload.ExpiresAt), + verification.Payload.Nonce, + verification.Payload.SessionId, + verification.Payload.Environments, + verification.Payload.Owners, + verification.Payload.BusinessTiers, + verification.Payload.Context); + + return Results.Ok(response); +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.VulnOperate)) + .WithName("VerifyVulnWorkflowAntiForgeryToken"); + +vulnGroup.MapPost("/attachments/tokens/issue", async ( + HttpContext httpContext, + VulnAttachmentTokenIssueRequest request, + VulnAttachmentTokenIssuer issuer, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + VulnAttachmentTokenIssueResult result; + try + { + result = await issuer.IssueAsync(httpContext.User, request, cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = "invalid_request", message = ex.Message }); + } + + await WriteAttachmentAuditAsync( + httpContext, + auditSink, + timeProvider, + "vuln.attachment.token.issued", + AuthEventOutcome.Success, + result.Payload, + ExtractScopes(httpContext.User), + cancellationToken).ConfigureAwait(false); + + var response = new VulnAttachmentTokenIssueResponse( + result.Token, + DateTimeOffset.FromUnixTimeSeconds(result.Payload.IssuedAt), + DateTimeOffset.FromUnixTimeSeconds(result.Payload.ExpiresAt), + result.Payload.LedgerEventHash, + result.Payload.AttachmentId); + + return Results.Ok(response); +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.VulnInvestigate)) + .WithName("IssueVulnAttachmentToken"); + +vulnGroup.MapPost("/attachments/tokens/verify", async ( + HttpContext httpContext, + VulnAttachmentTokenVerifyRequest request, + VulnAttachmentTokenVerifier verifier, + IAuthEventSink auditSink, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request?.Token is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Token is required." }); + } + + VulnAttachmentTokenVerificationResult verification; + try + { + verification = await verifier.VerifyAsync( + request.Token, + request.Tenant, + request.LedgerEventHash, + request.AttachmentId, + cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = "invalid_token", message = ex.Message }); + } + + await WriteAttachmentAuditAsync( + httpContext, + auditSink, + timeProvider, + "vuln.attachment.token.verified", + AuthEventOutcome.Success, + verification.Payload, + ExtractScopes(httpContext.User), + cancellationToken).ConfigureAwait(false); + + var response = new VulnAttachmentTokenVerifyResponse( + verification.Payload.Tenant, + verification.Payload.Actor, + verification.Payload.LedgerEventHash, + verification.Payload.AttachmentId, + verification.Payload.FindingId, + verification.Payload.ContentHash, + verification.Payload.ContentType, + verification.Payload.Metadata, + DateTimeOffset.FromUnixTimeSeconds(verification.Payload.ExpiresAt)); + + return Results.Ok(response); +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.VulnInvestigate)) + .WithName("VerifyVulnAttachmentToken"); + +var advisoryAiGroup = app.MapGroup("/advisory-ai"); + +advisoryAiGroup.MapPost("/remote-inference/logs", async ( + HttpContext httpContext, + AdvisoryAiRemoteInferenceLogRequest request, + IAuthorityAdvisoryAiConsentEvaluator consentEvaluator, + IAuthEventSink auditSink, + TimeProvider timeProvider, + IAuthorityRateLimiterMetadataAccessor metadataAccessor, + ILoggerFactory loggerFactory, + CancellationToken cancellationToken) => +{ + const int MaxPromptLength = 16_384; + const int MaxMetadataEntries = 16; + const int MaxMetadataValueLength = 256; + + if (request is null) + { + return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." }); + } + + var snapshot = consentEvaluator.GetSnapshot(); + if (!snapshot.Enabled) + { + return Results.Json( + new { error = "remote_inference_disabled", message = "Remote inference is disabled by configuration." }, + statusCode: StatusCodes.Status403Forbidden); + } + + var principal = httpContext.User; + if (principal?.Identity?.IsAuthenticated != true) + { + return Results.Json( + new { error = "unauthorized", message = "Authentication is required." }, + statusCode: StatusCodes.Status401Unauthorized); + } + + var tenantClaim = principal.FindFirst(StellaOpsClaimTypes.Tenant)?.Value; + var clientId = principal.FindFirst(StellaOpsClaimTypes.ClientId)?.Value; + var projectClaim = principal.FindFirst(StellaOpsClaimTypes.Project)?.Value; + + var consentResult = consentEvaluator.EvaluateTenant(tenantClaim); + if (!consentResult.Allowed) + { + return Results.Json( + new { error = consentResult.ErrorCode, message = consentResult.ErrorMessage }, + statusCode: StatusCodes.Status403Forbidden); + } + + if (!consentEvaluator.TryNormalizeProfile(request.Profile, out var profile)) + { + return Results.Json( + new { error = "profile_not_allowed", message = "Requested remote inference profile is not allowed." }, + statusCode: StatusCodes.Status400BadRequest); + } + + var taskType = NormalizeTaskType(request.TaskType); + if (taskType is null) + { + return Results.Json( + new { error = "task_type_invalid", message = "Task type must contain only lowercase letters, digits, dot, underscore, or hyphen (1-64 characters)." }, + statusCode: StatusCodes.Status400BadRequest); + } + + var prompt = NormalizePrompt(request.Prompt); + if (prompt is null) + { + return Results.Json( + new { error = "prompt_required", message = "Prompt content is required." }, + statusCode: StatusCodes.Status400BadRequest); + } + + if (prompt.Length > MaxPromptLength) + { + return Results.Json( + new { error = "prompt_too_long", message = $"Prompt exceeds maximum length of {MaxPromptLength} characters." }, + statusCode: StatusCodes.Status400BadRequest); + } + + var promptHash = ComputeSha256(prompt); + var metadata = metadataAccessor.GetMetadata(); + metadataAccessor.SetClientId(clientId); + metadataAccessor.SetTenant(tenantClaim); + metadataAccessor.SetProject(projectClaim); + metadataAccessor.SetTag("authority.advisory_ai.profile", profile); + metadataAccessor.SetTag("authority.advisory_ai.task_type", taskType); + + var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier; + var network = BuildNetwork(metadata); + var scopes = ExtractScopes(principal); + + var auditProperties = BuildProperties( + request, + taskType, + profile, + promptHash, + prompt.Length, + consentResult); + + var tenantNormalized = NormalizeTenant(tenantClaim); + var projectNormalized = NormalizeProject(projectClaim); + + var record = new AuthEventRecord + { + EventType = "authority.advisory_ai.remote_inference", + OccurredAt = timeProvider.GetUtcNow(), + CorrelationId = correlationId, + Outcome = AuthEventOutcome.Success, + Client = BuildClient(clientId), + Tenant = ClassifiedString.Public(tenantNormalized), + Project = ClassifiedString.Public(projectNormalized), + Scopes = scopes, + Network = network, + Properties = auditProperties + }; + + await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false); + + var logger = loggerFactory.CreateLogger("StellaOps.Authority.AdvisoryAi"); + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation( + "Recorded advisory AI remote inference task {TaskType} for tenant {Tenant} with profile {Profile} (hash={PromptHash}).", + taskType, + tenantNormalized, + profile, + promptHash); + } + + return Results.Json(new + { + status = "logged", + prompt_hash = promptHash, + correlation_id = correlationId, + consent_version = consentResult.ConsentVersion + }); + + static string? NormalizeTaskType(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var trimmed = value.Trim().ToLowerInvariant(); + if (trimmed.Length is < 1 or > 64) + { + return null; + } + + foreach (var ch in trimmed) + { + var allowed = char.IsLetterOrDigit(ch) || ch is '-' or '_' or '.'; + if (!allowed) + { + return null; + } + } + + return trimmed; + } + + static string? NormalizePrompt(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var normalized = value.Trim(); + return normalized.Replace("\r\n", "\n", StringComparison.Ordinal); + } + + static string NormalizeTenant(string? value) + { + return string.IsNullOrWhiteSpace(value) + ? string.Empty + : value.Trim().ToLowerInvariant(); + } + + static string NormalizeProject(string? value) + { + return string.IsNullOrWhiteSpace(value) + ? StellaOpsTenancyDefaults.AnyProject + : value.Trim().ToLowerInvariant(); + } + + static AuthEventClient? BuildClient(string? clientId) + { + if (string.IsNullOrWhiteSpace(clientId)) + { + return null; + } + + return new AuthEventClient + { + ClientId = ClassifiedString.Personal(clientId.Trim()), + Name = ClassifiedString.Empty, + Provider = ClassifiedString.Empty + }; + } + + static AuthEventNetwork? BuildNetwork(AuthorityRateLimiterMetadata? metadata) + { + if (metadata is null) + { + return null; + } + + var remote = string.IsNullOrWhiteSpace(metadata.RemoteIp) ? null : metadata.RemoteIp; + var forwarded = string.IsNullOrWhiteSpace(metadata.ForwardedFor) ? null : metadata.ForwardedFor; + var userAgent = string.IsNullOrWhiteSpace(metadata.UserAgent) ? null : metadata.UserAgent; + + if (remote is null && forwarded is null && userAgent is null) + { + return null; + } + + return new AuthEventNetwork + { + RemoteAddress = ClassifiedString.Personal(remote), + ForwardedFor = ClassifiedString.Personal(forwarded), + UserAgent = ClassifiedString.Personal(userAgent) + }; + } + + static IReadOnlyList ExtractScopes(ClaimsPrincipal principal) + { + var set = new HashSet(StringComparer.Ordinal); + + foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem)) + { + if (string.IsNullOrWhiteSpace(claim.Value)) + { + continue; + } + + set.Add(claim.Value.Trim()); + } + + foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope)) + { + if (string.IsNullOrWhiteSpace(claim.Value)) + { + continue; + } + + var parts = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + foreach (var part in parts) + { + var normalized = StellaOpsScopes.Normalize(part); + if (!string.IsNullOrWhiteSpace(normalized)) + { + set.Add(normalized); + } + } + } + + return set.Count == 0 + ? Array.Empty() + : set.OrderBy(static scope => scope, StringComparer.Ordinal).ToArray(); + } + + static IReadOnlyList BuildProperties( + AdvisoryAiRemoteInferenceLogRequest request, + string taskType, + string profile, + string promptHash, + int promptLength, + AuthorityTenantRemoteInferenceConsentResult consentResult) + { + var properties = new List + { + CreateProperty("advisory_ai.task_type", taskType), + CreateProperty("advisory_ai.profile", profile), + CreateProperty("advisory_ai.prompt.hash", promptHash), + CreateProperty("advisory_ai.prompt.length", promptLength.ToString(CultureInfo.InvariantCulture)), + CreateProperty("advisory_ai.prompt.algorithm", "sha256") + }; + + if (!string.IsNullOrWhiteSpace(request.ModelId)) + { + properties.Add(CreateProperty("advisory_ai.model_id", request.ModelId.Trim())); + } + + if (!string.IsNullOrWhiteSpace(request.ContextDigest)) + { + properties.Add(CreateProperty("advisory_ai.context.digest", request.ContextDigest.Trim())); + } + + if (!string.IsNullOrWhiteSpace(request.OutputHash)) + { + properties.Add(CreateProperty("advisory_ai.output.hash", request.OutputHash.Trim())); + } + + if (!string.IsNullOrWhiteSpace(request.TaskId)) + { + properties.Add(CreateProperty("advisory_ai.task_id", request.TaskId.Trim())); + } + + if (!string.IsNullOrWhiteSpace(consentResult.ConsentVersion)) + { + properties.Add(CreateProperty("advisory_ai.consent.version", consentResult.ConsentVersion.Trim())); + } + + if (consentResult.ConsentedAt.HasValue) + { + properties.Add(CreateProperty( + "advisory_ai.consent.timestamp", + consentResult.ConsentedAt.Value.ToString("O", CultureInfo.InvariantCulture))); + } + + if (!string.IsNullOrWhiteSpace(consentResult.ConsentedBy)) + { + properties.Add(CreateProperty("advisory_ai.consent.actor", consentResult.ConsentedBy.Trim())); + } + + if (request.Metadata is { Count: > 0 }) + { + var appended = 0; + foreach (var (key, value) in request.Metadata) + { + if (appended >= MaxMetadataEntries) + { + break; + } + + var normalizedKey = NormalizeMetadataKey(key); + if (normalizedKey is null) + { + continue; + } + + var normalizedValue = NormalizeMetadataValue(value); + properties.Add(CreateProperty($"advisory_ai.metadata.{normalizedKey}", normalizedValue)); + appended++; + } + } + + return properties; + } + + static AuthEventProperty CreateProperty(string name, string? value) + { + return new AuthEventProperty + { + Name = name, + Value = ClassifiedString.Public(string.IsNullOrWhiteSpace(value) ? null : value.Trim()) + }; + } + + static string NormalizeMetadataValue(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var trimmed = value.Trim(); + return trimmed.Length <= MaxMetadataValueLength + ? trimmed + : trimmed[..MaxMetadataValueLength]; + } + + static string? NormalizeMetadataKey(string? key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return null; + } + + var trimmed = key.Trim().ToLowerInvariant(); + if (trimmed.Length is < 1 or > 32) + { + return null; + } + + foreach (var ch in trimmed) + { + var allowed = char.IsLetterOrDigit(ch) || ch is '-' or '_' or '.'; + if (!allowed) + { + return null; + } + } + + return trimmed; + } + + static string ComputeSha256(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + var hash = SHA256.HashData(bytes); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + +}) + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AdvisoryAiOperate)) + .WithName("LogAdvisoryAiRemoteInference"); + + +app.MapAirgapAuditEndpoints(); +app.MapIncidentAuditEndpoints(); +app.MapAuthorityOpenApiDiscovery(); + + + + +app.MapGet("/jwks", (AuthorityJwksService jwksService, HttpContext context) => +{ + var result = jwksService.Get(); + + if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etagValues) && + etagValues.Contains(result.ETag, StringComparer.Ordinal)) + { + context.Response.Headers.CacheControl = result.CacheControl; + context.Response.Headers.ETag = result.ETag; + context.Response.Headers.Expires = result.ExpiresAt.ToString("R", CultureInfo.InvariantCulture); + return Results.StatusCode(StatusCodes.Status304NotModified); + } + + context.Response.Headers.CacheControl = result.CacheControl; + context.Response.Headers.ETag = result.ETag; + context.Response.Headers.Expires = result.ExpiresAt.ToString("R", CultureInfo.InvariantCulture); + + return Results.Json(result.Response); +}) + .WithName("JsonWebKeySet"); + +// Ensure signing key manager initialises key material on startup. app.Services.GetRequiredService(); app.Services.GetRequiredService(); @@ -2725,53 +3107,53 @@ static PluginHostOptions BuildPluginHostOptions(StellaOpsAuthorityOptions option var pluginDirectory = options.PluginDirectories.FirstOrDefault(); var hostOptions = new PluginHostOptions { - BaseDirectory = basePath, - PluginsDirectory = string.IsNullOrWhiteSpace(pluginDirectory) - ? "StellaOps.Authority.PluginBinaries" - : pluginDirectory, - PrimaryPrefix = "StellaOps.Authority" - }; - - if (!hostOptions.SearchPatterns.Any(pattern => string.Equals(pattern, "StellaOps.Authority.Plugin.*.dll", StringComparison.OrdinalIgnoreCase))) - { - hostOptions.SearchPatterns.Add("StellaOps.Authority.Plugin.*.dll"); - } - - foreach (var pair in options.Plugins.Descriptors.OrderBy(static p => p.Key, StringComparer.OrdinalIgnoreCase)) - { - var descriptor = pair.Value; - if (descriptor.Enabled && !string.IsNullOrWhiteSpace(descriptor.AssemblyName)) - { - hostOptions.PluginOrder.Add(descriptor.AssemblyName!); - } - } - - return hostOptions; -} - -static bool TryParseUris(IReadOnlyCollection? values, out IReadOnlyCollection uris, out string? error) -{ - error = null; - - if (values is null || values.Count == 0) - { - uris = Array.Empty(); - return true; - } - - var parsed = new List(values.Count); - foreach (var entry in values) - { - if (string.IsNullOrWhiteSpace(entry) || !Uri.TryCreate(entry, UriKind.Absolute, out var uri)) - { - uris = Array.Empty(); - error = $"Invalid URI value '{entry}'."; - return false; - } - - parsed.Add(uri); - } - - uris = parsed; - return true; -} + BaseDirectory = basePath, + PluginsDirectory = string.IsNullOrWhiteSpace(pluginDirectory) + ? "StellaOps.Authority.PluginBinaries" + : pluginDirectory, + PrimaryPrefix = "StellaOps.Authority" + }; + + if (!hostOptions.SearchPatterns.Any(pattern => string.Equals(pattern, "StellaOps.Authority.Plugin.*.dll", StringComparison.OrdinalIgnoreCase))) + { + hostOptions.SearchPatterns.Add("StellaOps.Authority.Plugin.*.dll"); + } + + foreach (var pair in options.Plugins.Descriptors.OrderBy(static p => p.Key, StringComparer.OrdinalIgnoreCase)) + { + var descriptor = pair.Value; + if (descriptor.Enabled && !string.IsNullOrWhiteSpace(descriptor.AssemblyName)) + { + hostOptions.PluginOrder.Add(descriptor.AssemblyName!); + } + } + + return hostOptions; +} + +static bool TryParseUris(IReadOnlyCollection? values, out IReadOnlyCollection uris, out string? error) +{ + error = null; + + if (values is null || values.Count == 0) + { + uris = Array.Empty(); + return true; + } + + var parsed = new List(values.Count); + foreach (var entry in values) + { + if (string.IsNullOrWhiteSpace(entry) || !Uri.TryCreate(entry, UriKind.Absolute, out var uri)) + { + uris = Array.Empty(); + error = $"Invalid URI value '{entry}'."; + return false; + } + + parsed.Add(uri); + } + + uris = parsed; + return true; +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/ServiceAccountAdminContracts.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/ServiceAccountAdminContracts.cs index 8dafd78d..47470701 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/ServiceAccountAdminContracts.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/ServiceAccountAdminContracts.cs @@ -10,7 +10,8 @@ internal sealed record ServiceAccountResponse( string? Description, bool Enabled, IReadOnlyList AllowedScopes, - IReadOnlyList AuthorizedClients); + IReadOnlyList AuthorizedClients, + IReadOnlyDictionary> Attributes); internal sealed record ServiceAccountTokenResponse( string TokenId, diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj b/src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj index c14a2ee0..dbcbd6f1 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenIssuer.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenIssuer.cs new file mode 100644 index 00000000..b36710d9 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenIssuer.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Auth.Abstractions; +using StellaOps.Configuration; +using StellaOps.Cryptography; +using StellaOps.Authority.Vulnerability; + +namespace StellaOps.Authority.Vulnerability.Attachments; + +internal sealed class VulnAttachmentTokenIssuer +{ + private readonly ICryptoProviderRegistry cryptoRegistry; + private readonly IOptions authorityOptionsAccessor; + private readonly TimeProvider timeProvider; + private readonly ILogger logger; + + public VulnAttachmentTokenIssuer( + ICryptoProviderRegistry cryptoRegistry, + IOptions authorityOptionsAccessor, + TimeProvider timeProvider, + ILogger logger) + { + this.cryptoRegistry = cryptoRegistry ?? throw new ArgumentNullException(nameof(cryptoRegistry)); + this.authorityOptionsAccessor = authorityOptionsAccessor ?? throw new ArgumentNullException(nameof(authorityOptionsAccessor)); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task IssueAsync( + ClaimsPrincipal principal, + VulnAttachmentTokenIssueRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(principal); + ArgumentNullException.ThrowIfNull(request); + + var options = authorityOptionsAccessor.Value ?? throw new InvalidOperationException("Authority configuration is not available."); + var attachmentOptions = options.VulnerabilityExplorer.Attachments; + + if (!attachmentOptions.Enabled) + { + throw new InvalidOperationException("Attachment token issuance is disabled. Enable vulnerabilityExplorer.attachments before issuing tokens."); + } + + var signing = options.Signing ?? throw new InvalidOperationException("Authority signing configuration is required to issue attachment tokens."); + if (!signing.Enabled) + { + throw new InvalidOperationException("Authority signing is disabled. Enable signing before issuing attachment tokens."); + } + + var issuer = options.Issuer ?? throw new InvalidOperationException("Authority issuer configuration is required."); + + var tenant = VulnTokenUtilities.ResolveTenant(principal, request.Tenant); + var actor = VulnTokenUtilities.ResolveSubject(principal); + + var ledgerEventHash = NormalizeRequired(request.LedgerEventHash, "ledgerEventHash"); + var attachmentId = NormalizeRequired(request.AttachmentId, "attachmentId"); + var findingId = NormalizeOptional(request.FindingId); + var contentHash = NormalizeOptional(request.ContentHash); + var contentType = NormalizeOptional(request.ContentType); + + var metadata = VulnTokenUtilities.SanitizeDictionary( + request.Metadata, + attachmentOptions.MaxMetadataEntries, + attachmentOptions.MaxMetadataValueLength, + "Attachment metadata"); + + var lifetime = VulnTokenUtilities.ResolveLifetime( + request.ExpiresInSeconds, + attachmentOptions.DefaultLifetime, + attachmentOptions.MaxLifetime, + "expiresInSeconds"); + + var issuedAt = timeProvider.GetUtcNow(); + var expiresAt = issuedAt.Add(lifetime); + var tokenId = Guid.NewGuid().ToString("N"); + + var payload = new VulnAttachmentTokenPayload( + Issuer: issuer.ToString(), + Subject: $"attachment:{attachmentId}", + Audience: "stellaops:vuln-attachments", + Tenant: tenant, + LedgerEventHash: ledgerEventHash, + AttachmentId: attachmentId, + FindingId: findingId, + ContentHash: contentHash, + ContentType: contentType, + Metadata: metadata, + IssuedAt: issuedAt.ToUnixTimeSeconds(), + NotBefore: issuedAt.ToUnixTimeSeconds(), + ExpiresAt: expiresAt.ToUnixTimeSeconds(), + TokenId: tokenId, + Actor: actor); + + var token = await VulnTokenSigner.SignAsync(cryptoRegistry, signing, payload, cancellationToken).ConfigureAwait(false); + + logger.LogDebug( + "Issued Vuln Explorer attachment token for tenant {Tenant} (ledger: {LedgerHash}, attachment: {AttachmentId}, expires: {Expires}).", + tenant, + ledgerEventHash, + attachmentId, + expiresAt); + + return new VulnAttachmentTokenIssueResult(token, payload); + } + + private static string NormalizeRequired(string? value, string propertyName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException($"Property '{propertyName}' is required."); + } + + var normalized = value.Trim(); + if (normalized.Length == 0) + { + throw new InvalidOperationException($"Property '{propertyName}' is required."); + } + + return normalized; + } + + private static string? NormalizeOptional(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var normalized = value.Trim(); + return normalized.Length == 0 ? null : normalized; + } +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenModels.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenModels.cs new file mode 100644 index 00000000..cab9975a --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenModels.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace StellaOps.Authority.Vulnerability.Attachments; + +public sealed class VulnAttachmentTokenIssueRequest +{ + public string? Tenant { get; set; } + + public string? LedgerEventHash { get; set; } + + public string? AttachmentId { get; set; } + + public string? FindingId { get; set; } + + public string? ContentHash { get; set; } + + public string? ContentType { get; set; } + + public Dictionary? Metadata { get; set; } + + public int? ExpiresInSeconds { get; set; } +} + +public sealed record VulnAttachmentTokenIssueResponse( + string Token, + DateTimeOffset IssuedAt, + DateTimeOffset ExpiresAt, + string LedgerEventHash, + string AttachmentId); + +internal sealed record VulnAttachmentTokenIssueResult( + string Token, + VulnAttachmentTokenPayload Payload); + +public sealed class VulnAttachmentTokenVerifyRequest +{ + public string? Token { get; set; } + + public string? Tenant { get; set; } + + public string? LedgerEventHash { get; set; } + + public string? AttachmentId { get; set; } +} + +public sealed record VulnAttachmentTokenVerifyResponse( + string Tenant, + string Actor, + string LedgerEventHash, + string AttachmentId, + string? FindingId, + string? ContentHash, + string? ContentType, + IReadOnlyDictionary? Metadata, + DateTimeOffset ExpiresAt); + +internal sealed record VulnAttachmentTokenVerificationResult( + VulnAttachmentTokenPayload Payload, + string SigningKeyId); diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenPayload.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenPayload.cs new file mode 100644 index 00000000..9c926800 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenPayload.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace StellaOps.Authority.Vulnerability.Attachments; + +internal sealed record VulnAttachmentTokenPayload( + [property: JsonPropertyName("iss")] string Issuer, + [property: JsonPropertyName("sub")] string Subject, + [property: JsonPropertyName("aud")] string Audience, + [property: JsonPropertyName("tid")] string Tenant, + [property: JsonPropertyName("ledger")] string LedgerEventHash, + [property: JsonPropertyName("attachment")] string AttachmentId, + [property: JsonPropertyName("finding")] string? FindingId, + [property: JsonPropertyName("hash")] string? ContentHash, + [property: JsonPropertyName("type")] string? ContentType, + [property: JsonPropertyName("metadata")] IReadOnlyDictionary? Metadata, + [property: JsonPropertyName("iat")] long IssuedAt, + [property: JsonPropertyName("nbf")] long NotBefore, + [property: JsonPropertyName("exp")] long ExpiresAt, + [property: JsonPropertyName("jti")] string TokenId, + [property: JsonPropertyName("actor")] string Actor); diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenVerifier.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenVerifier.cs new file mode 100644 index 00000000..57095241 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Attachments/VulnAttachmentTokenVerifier.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using StellaOps.Configuration; +using StellaOps.Cryptography; +using StellaOps.Authority.Vulnerability; + +namespace StellaOps.Authority.Vulnerability.Attachments; + +internal sealed class VulnAttachmentTokenVerifier +{ + private readonly ICryptoProviderRegistry cryptoRegistry; + private readonly IOptions authorityOptionsAccessor; + private readonly TimeProvider timeProvider; + + public VulnAttachmentTokenVerifier( + ICryptoProviderRegistry cryptoRegistry, + IOptions authorityOptionsAccessor, + TimeProvider timeProvider) + { + this.cryptoRegistry = cryptoRegistry ?? throw new ArgumentNullException(nameof(cryptoRegistry)); + this.authorityOptionsAccessor = authorityOptionsAccessor ?? throw new ArgumentNullException(nameof(authorityOptionsAccessor)); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } + + public async Task VerifyAsync( + string token, + string? expectedTenant, + string? expectedLedgerHash, + string? expectedAttachmentId, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new InvalidOperationException("Token value is required."); + } + + var options = authorityOptionsAccessor.Value ?? throw new InvalidOperationException("Authority configuration is not available."); + var attachmentOptions = options.VulnerabilityExplorer.Attachments; + + if (!attachmentOptions.Enabled) + { + throw new InvalidOperationException("Attachment token verification is disabled. Enable vulnerabilityExplorer.attachments before verifying tokens."); + } + + var signing = options.Signing ?? throw new InvalidOperationException("Authority signing configuration is required to verify attachment tokens."); + if (!signing.Enabled) + { + throw new InvalidOperationException("Authority signing is disabled. Enable signing before verifying attachment tokens."); + } + + var segments = VulnTokenVerificationUtilities.ParseSegments(token); + var signer = VulnTokenVerificationUtilities.ResolveSigner(cryptoRegistry, signing, segments); + var signatureValid = await VulnTokenVerificationUtilities.VerifySignatureAsync(signer, segments, cancellationToken).ConfigureAwait(false); + if (!signatureValid) + { + throw new InvalidOperationException("Attachment token signature is invalid."); + } + + var payload = VulnTokenVerificationUtilities.DeserializePayload(segments); + ValidatePayload(payload, options, expectedTenant, expectedLedgerHash, expectedAttachmentId); + + return new VulnAttachmentTokenVerificationResult(payload, segments.KeyId); + } + + private void ValidatePayload( + VulnAttachmentTokenPayload payload, + StellaOpsAuthorityOptions options, + string? expectedTenant, + string? expectedLedgerHash, + string? expectedAttachmentId) + { + if (!string.Equals(payload.Issuer, options.Issuer?.ToString(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token issuer is not recognized."); + } + + if (!string.Equals(payload.Audience, "stellaops:vuln-attachments", StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token audience is not valid for attachment verification."); + } + + var now = timeProvider.GetUtcNow(); + var notBefore = DateTimeOffset.FromUnixTimeSeconds(payload.NotBefore); + if (now < notBefore) + { + throw new InvalidOperationException("Token is not yet valid."); + } + + var expiresAt = DateTimeOffset.FromUnixTimeSeconds(payload.ExpiresAt); + if (now > expiresAt) + { + throw new InvalidOperationException("Token has expired."); + } + + if (!string.IsNullOrWhiteSpace(expectedTenant) && + !string.Equals(payload.Tenant, expectedTenant.Trim(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token tenant does not match the expected tenant."); + } + + if (!string.IsNullOrWhiteSpace(expectedLedgerHash) && + !string.Equals(payload.LedgerEventHash, expectedLedgerHash.Trim(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token ledger reference does not match the expected hash."); + } + + if (!string.IsNullOrWhiteSpace(expectedAttachmentId) && + !string.Equals(payload.AttachmentId, expectedAttachmentId.Trim(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token attachment id does not match the expected identifier."); + } + } +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenSigner.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenSigner.cs new file mode 100644 index 00000000..2bc95edb --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenSigner.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; +using StellaOps.Configuration; +using StellaOps.Cryptography; + +namespace StellaOps.Authority.Vulnerability; + +internal static class VulnTokenSigner +{ + private static readonly JsonSerializerOptions HeaderSerializerOptions = new(JsonSerializerDefaults.General) + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + private static readonly JsonSerializerOptions PayloadSerializerOptions = new(JsonSerializerDefaults.General) + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + public static async Task SignAsync( + ICryptoProviderRegistry registry, + AuthoritySigningOptions signingOptions, + object payload, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(registry); + ArgumentNullException.ThrowIfNull(signingOptions); + ArgumentNullException.ThrowIfNull(payload); + + if (!signingOptions.Enabled) + { + throw new InvalidOperationException("Authority signing is disabled."); + } + + var algorithm = string.IsNullOrWhiteSpace(signingOptions.Algorithm) + ? SignatureAlgorithms.Es256 + : signingOptions.Algorithm.Trim(); + + var keyId = (signingOptions.ActiveKeyId ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(keyId)) + { + throw new InvalidOperationException("Authority signing requires an active key identifier."); + } + + var keyReference = new CryptoKeyReference(keyId, signingOptions.Provider); + var signer = registry.ResolveSigner( + CryptoCapability.Signing, + algorithm, + keyReference, + signingOptions.Provider).Signer; + + var header = new Dictionary(StringComparer.Ordinal) + { + ["alg"] = algorithm, + ["typ"] = "JWT", + ["kid"] = signer.KeyId + }; + + var headerBytes = JsonSerializer.SerializeToUtf8Bytes(header, HeaderSerializerOptions); + var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload, PayloadSerializerOptions); + + var encodedHeader = Base64UrlEncoder.Encode(headerBytes); + var encodedPayload = Base64UrlEncoder.Encode(payloadBytes); + var signingInput = Encoding.ASCII.GetBytes(string.Concat(encodedHeader, '.', encodedPayload)); + + var signatureBytes = await signer.SignAsync(signingInput, cancellationToken).ConfigureAwait(false); + var encodedSignature = Base64UrlEncoder.Encode(signatureBytes); + + return string.Concat(encodedHeader, '.', encodedPayload, '.', encodedSignature); + } +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenUtilities.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenUtilities.cs new file mode 100644 index 00000000..a5482bb9 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenUtilities.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using StellaOps.Auth.Abstractions; + +namespace StellaOps.Authority.Vulnerability; + +internal static class VulnTokenUtilities +{ + public static string ResolveTenant(ClaimsPrincipal principal, string? requestedTenant) + { + var tenantClaim = principal.FindAll(StellaOpsClaimTypes.Tenant) + .Select(static claim => claim.Value) + .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); + + if (!string.IsNullOrWhiteSpace(requestedTenant) && !string.Equals(requestedTenant.Trim(), tenantClaim, StringComparison.Ordinal)) + { + throw new InvalidOperationException("Requested tenant does not match the authenticated tenant context."); + } + + if (string.IsNullOrWhiteSpace(tenantClaim)) + { + throw new InvalidOperationException("Tenant context is required to issue tokens."); + } + + return tenantClaim.Trim(); + } + + public static string ResolveSubject(ClaimsPrincipal principal) + { + var subject = principal.FindFirstValue(StellaOpsClaimTypes.Subject); + if (!string.IsNullOrWhiteSpace(subject)) + { + return subject.Trim(); + } + + subject = principal.FindFirstValue(StellaOpsClaimTypes.ClientId); + if (!string.IsNullOrWhiteSpace(subject)) + { + return subject.Trim(); + } + + throw new InvalidOperationException("Unable to resolve subject for token issuance."); + } + + public static TimeSpan ResolveLifetime(int? requestedSeconds, TimeSpan defaultLifetime, TimeSpan maxLifetime, string parameterName) + { + if (!requestedSeconds.HasValue) + { + return defaultLifetime; + } + + if (requestedSeconds.Value <= 0) + { + throw new InvalidOperationException($"{parameterName} must be greater than zero."); + } + + var requested = TimeSpan.FromSeconds(requestedSeconds.Value); + return requested <= maxLifetime ? requested : maxLifetime; + } + + public static IReadOnlyDictionary? SanitizeDictionary( + IDictionary? values, + int maxEntries, + int maxValueLength, + string category) + { + if (values is null || values.Count == 0) + { + return null; + } + + var normalized = new Dictionary(StringComparer.Ordinal); + + foreach (var (key, value) in values) + { + if (string.IsNullOrWhiteSpace(key) || value is null) + { + continue; + } + + var trimmedKey = key.Trim(); + if (trimmedKey.Length == 0) + { + continue; + } + + var trimmedValue = value.Trim(); + if (trimmedValue.Length == 0) + { + continue; + } + + if (trimmedValue.Length > maxValueLength) + { + throw new InvalidOperationException($"{category} value for '{trimmedKey}' exceeds the configured maximum length ({maxValueLength})."); + } + + normalized[trimmedKey] = trimmedValue; + + if (normalized.Count > maxEntries) + { + throw new InvalidOperationException($"{category} includes more than {maxEntries} entries."); + } + } + + return normalized.Count > 0 ? normalized : null; + } +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenVerificationUtilities.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenVerificationUtilities.cs new file mode 100644 index 00000000..aafbfd52 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/VulnTokenVerificationUtilities.cs @@ -0,0 +1,115 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; +using StellaOps.Configuration; +using StellaOps.Cryptography; + +namespace StellaOps.Authority.Vulnerability; + +internal static class VulnTokenVerificationUtilities +{ + private static readonly JsonSerializerOptions PayloadSerializerOptions = new() + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + public static VulnTokenSegments ParseSegments(string token) + { + var segments = token.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length != 3) + { + throw new InvalidOperationException("Token format is invalid. Expected three segments."); + } + + var headerSegment = segments[0]; + var payloadSegment = segments[1]; + var signatureSegment = segments[2]; + + if (signatureSegment.Length == 0) + { + throw new InvalidOperationException("Token signature segment is empty."); + } + + var headerBytes = Base64UrlEncoder.DecodeBytes(headerSegment); + using var headerDocument = JsonDocument.Parse(headerBytes); + var header = headerDocument.RootElement; + + if (!header.TryGetProperty("alg", out var algElement) || algElement.ValueKind != JsonValueKind.String) + { + throw new InvalidOperationException("Token header is missing the alg attribute."); + } + + if (!header.TryGetProperty("kid", out var kidElement) || kidElement.ValueKind != JsonValueKind.String) + { + throw new InvalidOperationException("Token header is missing the kid attribute."); + } + + var payloadBytes = Base64UrlEncoder.DecodeBytes(payloadSegment); + return new VulnTokenSegments( + HeaderSegment: headerSegment, + PayloadSegment: payloadSegment, + SignatureSegment: signatureSegment, + PayloadBytes: payloadBytes, + KeyId: kidElement.GetString()!.Trim(), + AlgorithmId: algElement.GetString()!.Trim()); + } + + public static ICryptoSigner ResolveSigner( + ICryptoProviderRegistry registry, + AuthoritySigningOptions signingOptions, + VulnTokenSegments segments) + { + if (string.IsNullOrWhiteSpace(segments.KeyId)) + { + throw new InvalidOperationException("Token header kid value cannot be empty."); + } + + var keyReference = new CryptoKeyReference(segments.KeyId, signingOptions.Provider); + return registry.ResolveSigner( + CryptoCapability.Signing, + segments.AlgorithmId, + keyReference, + signingOptions.Provider).Signer; + } + + public static async Task VerifySignatureAsync( + ICryptoSigner signer, + VulnTokenSegments segments, + CancellationToken cancellationToken) + { + var signingInput = Encoding.ASCII.GetBytes($"{segments.HeaderSegment}.{segments.PayloadSegment}"); + var signatureBytes = Base64UrlEncoder.DecodeBytes(segments.SignatureSegment); + return await signer.VerifyAsync(signingInput, signatureBytes, cancellationToken).ConfigureAwait(false); + } + + public static TPayload DeserializePayload(VulnTokenSegments segments) + { + try + { + var payload = JsonSerializer.Deserialize(segments.PayloadBytes, PayloadSerializerOptions); + if (payload is null) + { + throw new InvalidOperationException("Token payload is invalid."); + } + + return payload; + } + catch (JsonException ex) + { + throw new InvalidOperationException("Token payload could not be parsed.", ex); + } + } +} + +internal readonly record struct VulnTokenSegments( + string HeaderSegment, + string PayloadSegment, + string SignatureSegment, + byte[] PayloadBytes, + string KeyId, + string AlgorithmId); diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryModels.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryModels.cs new file mode 100644 index 00000000..c0f5f6d7 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryModels.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace StellaOps.Authority.Vulnerability.Workflow; + +public sealed class VulnWorkflowAntiForgeryIssueRequest +{ + public string? Tenant { get; set; } + + public List? Actions { get; set; } + + public Dictionary? Context { get; set; } + + public int? ExpiresInSeconds { get; set; } + + public string? Nonce { get; set; } +} + +public sealed record VulnWorkflowAntiForgeryIssueResponse( + string Token, + DateTimeOffset IssuedAt, + DateTimeOffset ExpiresAt, + IReadOnlyList Actions, + string Nonce); + +internal sealed record VulnWorkflowAntiForgeryIssueResult( + string Token, + VulnWorkflowAntiForgeryPayload Payload); + +public sealed class VulnWorkflowAntiForgeryVerifyRequest +{ + public string? Token { get; set; } + + public string? RequiredAction { get; set; } + + public string? Tenant { get; set; } + + public string? Nonce { get; set; } +} + +public sealed record VulnWorkflowAntiForgeryVerifyResponse( + string Tenant, + string Subject, + IReadOnlyList Actions, + DateTimeOffset ExpiresAt, + string Nonce, + string? SessionId, + IReadOnlyList? Environments, + IReadOnlyList? Owners, + IReadOnlyList? BusinessTiers, + IReadOnlyDictionary? Context); + +internal sealed record VulnWorkflowAntiForgeryVerificationResult( + VulnWorkflowAntiForgeryPayload Payload, + string SigningKeyId); diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryPayload.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryPayload.cs new file mode 100644 index 00000000..a4635e78 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryPayload.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace StellaOps.Authority.Vulnerability.Workflow; + +internal sealed record VulnWorkflowAntiForgeryPayload( + [property: JsonPropertyName("iss")] string Issuer, + [property: JsonPropertyName("sub")] string Subject, + [property: JsonPropertyName("aud")] string Audience, + [property: JsonPropertyName("tid")] string Tenant, + [property: JsonPropertyName("iat")] long IssuedAt, + [property: JsonPropertyName("nbf")] long NotBefore, + [property: JsonPropertyName("exp")] long ExpiresAt, + [property: JsonPropertyName("jti")] string TokenId, + [property: JsonPropertyName("nonce")] string Nonce, + [property: JsonPropertyName("sid")] string? SessionId, + [property: JsonPropertyName("actions")] IReadOnlyList Actions, + [property: JsonPropertyName("env")] IReadOnlyList? Environments, + [property: JsonPropertyName("owner")] IReadOnlyList? Owners, + [property: JsonPropertyName("tier")] IReadOnlyList? BusinessTiers, + [property: JsonPropertyName("context")] IReadOnlyDictionary? Context, + [property: JsonPropertyName("cnf")] VulnTokenConfirmation? Confirmation); + +internal sealed record VulnTokenConfirmation( + [property: JsonPropertyName("jkt")] string? JwkThumbprint, + [property: JsonPropertyName("x5t#S256")] string? CertificateThumbprint); diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenIssuer.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenIssuer.cs new file mode 100644 index 00000000..c0be7d8c --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenIssuer.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using StellaOps.Auth.Abstractions; +using StellaOps.Authority.OpenIddict; +using StellaOps.Authority.Vulnerability; +using StellaOps.Configuration; +using StellaOps.Cryptography; + +namespace StellaOps.Authority.Vulnerability.Workflow; + +internal sealed class VulnWorkflowAntiForgeryTokenIssuer +{ + private static readonly JsonSerializerOptions HeaderSerializerOptions = new(JsonSerializerDefaults.General) + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + private static readonly JsonSerializerOptions PayloadSerializerOptions = new(JsonSerializerDefaults.General) + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + private readonly ICryptoProviderRegistry cryptoRegistry; + private readonly IOptions authorityOptionsAccessor; + private readonly TimeProvider timeProvider; + private readonly ILogger logger; + + public VulnWorkflowAntiForgeryTokenIssuer( + ICryptoProviderRegistry cryptoRegistry, + IOptions authorityOptionsAccessor, + TimeProvider timeProvider, + ILogger logger) + { + this.cryptoRegistry = cryptoRegistry ?? throw new ArgumentNullException(nameof(cryptoRegistry)); + this.authorityOptionsAccessor = authorityOptionsAccessor ?? throw new ArgumentNullException(nameof(authorityOptionsAccessor)); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task IssueAsync( + ClaimsPrincipal principal, + VulnWorkflowAntiForgeryIssueRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(principal); + + var options = authorityOptionsAccessor.Value ?? throw new InvalidOperationException("Authority configuration is not available."); + var workflowOptions = options.VulnerabilityExplorer.Workflow.AntiForgery; + + if (!workflowOptions.Enabled) + { + throw new InvalidOperationException("Anti-forgery token issuance is disabled. Enable vulnerabilityExplorer.workflow.antiForgery before issuing tokens."); + } + + var signing = options.Signing ?? throw new InvalidOperationException("Authority signing configuration is required to issue workflow tokens."); + if (!signing.Enabled) + { + throw new InvalidOperationException("Authority signing is disabled. Enable signing before issuing workflow tokens."); + } + + var issuer = options.Issuer ?? throw new InvalidOperationException("Authority issuer configuration is required."); + + var tenant = VulnTokenUtilities.ResolveTenant(principal, request?.Tenant); + var subject = VulnTokenUtilities.ResolveSubject(principal); + var sessionId = principal.FindFirstValue(StellaOpsClaimTypes.SessionId); + var confirmation = ParseConfirmation(principal.FindFirstValue(AuthorityOpenIddictConstants.ConfirmationClaimType)); + var environments = ResolveMultiValueClaim(principal, StellaOpsClaimTypes.VulnerabilityEnvironment); + var owners = ResolveMultiValueClaim(principal, StellaOpsClaimTypes.VulnerabilityOwner); + var tiers = ResolveMultiValueClaim(principal, StellaOpsClaimTypes.VulnerabilityBusinessTier); + + var normalizedActions = NormalizeActions(request?.Actions); + if (normalizedActions.Count == 0) + { + throw new InvalidOperationException("At least one action must be supplied when issuing workflow anti-forgery tokens."); + } + + var sanitizedContext = VulnTokenUtilities.SanitizeDictionary( + request?.Context, + workflowOptions.MaxContextEntries, + workflowOptions.MaxContextValueLength, + "Workflow context"); + var nonce = NormalizeOrGenerateNonce(request?.Nonce); + var lifetime = VulnTokenUtilities.ResolveLifetime( + request?.ExpiresInSeconds, + workflowOptions.DefaultLifetime, + workflowOptions.MaxLifetime, + "expiresInSeconds"); + + var issuedAt = timeProvider.GetUtcNow(); + var expiresAt = issuedAt.Add(lifetime); + var tokenId = Guid.NewGuid().ToString("N"); + + var payload = new VulnWorkflowAntiForgeryPayload( + Issuer: issuer.ToString(), + Subject: subject, + Audience: workflowOptions.Audience, + Tenant: tenant, + IssuedAt: issuedAt.ToUnixTimeSeconds(), + NotBefore: issuedAt.ToUnixTimeSeconds(), + ExpiresAt: expiresAt.ToUnixTimeSeconds(), + TokenId: tokenId, + Nonce: nonce, + SessionId: string.IsNullOrWhiteSpace(sessionId) ? null : sessionId, + Actions: normalizedActions, + Environments: environments, + Owners: owners, + BusinessTiers: tiers, + Context: sanitizedContext, + Confirmation: confirmation); + + var token = await VulnTokenSigner.SignAsync(cryptoRegistry, signing, payload, cancellationToken).ConfigureAwait(false); + + logger.LogDebug( + "Issued Vuln Explorer workflow anti-forgery token for tenant {Tenant} (actions: {Actions}, expires: {Expires}).", + tenant, + string.Join(',', normalizedActions), + expiresAt); + + return new VulnWorkflowAntiForgeryIssueResult(token, payload); + } + + private static VulnTokenConfirmation? ParseConfirmation(string? confirmationJson) + { + if (string.IsNullOrWhiteSpace(confirmationJson)) + { + return null; + } + + try + { + using var document = JsonDocument.Parse(confirmationJson); + var root = document.RootElement; + if (root.ValueKind != JsonValueKind.Object) + { + return null; + } + + var jkt = root.TryGetProperty("jkt", out var jktElement) && jktElement.ValueKind == JsonValueKind.String + ? jktElement.GetString() + : null; + + var x5t = root.TryGetProperty("x5t#S256", out var x5tElement) && x5tElement.ValueKind == JsonValueKind.String + ? x5tElement.GetString() + : null; + + if (string.IsNullOrWhiteSpace(jkt) && string.IsNullOrWhiteSpace(x5t)) + { + return null; + } + + return new VulnTokenConfirmation( + string.IsNullOrWhiteSpace(jkt) ? null : jkt, + string.IsNullOrWhiteSpace(x5t) ? null : x5t); + } + catch (JsonException) + { + return null; + } + } + + private static IReadOnlyList? ResolveMultiValueClaim(ClaimsPrincipal principal, string claimType) + { + var values = principal.FindAll(claimType) + .Select(static claim => claim.Value) + .Where(static value => !string.IsNullOrWhiteSpace(value)) + .Select(static value => value.Trim()) + .Where(static value => value.Length > 0) + .Distinct(StringComparer.Ordinal) + .ToList(); + + return values.Count > 0 ? values : null; + } + + private static List NormalizeActions(IEnumerable? actions) + { + if (actions is null) + { + return new List(); + } + + var set = new HashSet(StringComparer.Ordinal); + foreach (var action in actions) + { + if (string.IsNullOrWhiteSpace(action)) + { + continue; + } + + var normalized = action.Trim().ToLowerInvariant(); + if (normalized.Length == 0) + { + continue; + } + + set.Add(normalized); + } + + return set.OrderBy(static value => value, StringComparer.Ordinal).ToList(); + } + + private static string NormalizeOrGenerateNonce(string? nonce) + { + if (string.IsNullOrWhiteSpace(nonce)) + { + return Guid.NewGuid().ToString("N"); + } + + var normalized = nonce.Trim(); + if (normalized.Length < 8) + { + throw new InvalidOperationException("Nonce must be at least 8 characters when provided explicitly."); + } + + return normalized; + } +} diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenVerifier.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenVerifier.cs new file mode 100644 index 00000000..c9ae2d70 --- /dev/null +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Vulnerability/Workflow/VulnWorkflowAntiForgeryTokenVerifier.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using StellaOps.Configuration; +using StellaOps.Cryptography; +using StellaOps.Authority.Vulnerability; + +namespace StellaOps.Authority.Vulnerability.Workflow; + +internal sealed class VulnWorkflowAntiForgeryTokenVerifier +{ + private readonly ICryptoProviderRegistry cryptoRegistry; + private readonly IOptions authorityOptionsAccessor; + private readonly TimeProvider timeProvider; + + public VulnWorkflowAntiForgeryTokenVerifier( + ICryptoProviderRegistry cryptoRegistry, + IOptions authorityOptionsAccessor, + TimeProvider timeProvider) + { + this.cryptoRegistry = cryptoRegistry ?? throw new ArgumentNullException(nameof(cryptoRegistry)); + this.authorityOptionsAccessor = authorityOptionsAccessor ?? throw new ArgumentNullException(nameof(authorityOptionsAccessor)); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } + + public async Task VerifyAsync( + string token, + string? requiredAction, + string? expectedTenant, + string? expectedNonce, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new InvalidOperationException("Token value is required."); + } + + var options = authorityOptionsAccessor.Value ?? throw new InvalidOperationException("Authority configuration is not available."); + var workflowOptions = options.VulnerabilityExplorer.Workflow.AntiForgery; + + if (!workflowOptions.Enabled) + { + throw new InvalidOperationException("Anti-forgery token verification is disabled. Enable vulnerabilityExplorer.workflow.antiForgery before verifying tokens."); + } + + var signing = options.Signing ?? throw new InvalidOperationException("Authority signing configuration is required to verify workflow tokens."); + if (!signing.Enabled) + { + throw new InvalidOperationException("Authority signing is disabled. Enable signing before verifying workflow tokens."); + } + + var segments = VulnTokenVerificationUtilities.ParseSegments(token); + var signer = VulnTokenVerificationUtilities.ResolveSigner(cryptoRegistry, signing, segments); + var signatureValid = await VulnTokenVerificationUtilities.VerifySignatureAsync(signer, segments, cancellationToken).ConfigureAwait(false); + if (!signatureValid) + { + throw new InvalidOperationException("Workflow anti-forgery token signature is invalid."); + } + + var payload = VulnTokenVerificationUtilities.DeserializePayload(segments); + ValidatePayload(payload, options, workflowOptions, requiredAction, expectedTenant, expectedNonce); + + return new VulnWorkflowAntiForgeryVerificationResult(payload, segments.KeyId); + } + + private void ValidatePayload( + VulnWorkflowAntiForgeryPayload payload, + StellaOpsAuthorityOptions options, + AuthorityVulnAntiForgeryOptions antiForgeryOptions, + string? requiredAction, + string? expectedTenant, + string? expectedNonce) + { + if (!string.Equals(payload.Issuer, options.Issuer?.ToString(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token issuer is not recognized."); + } + + if (!string.Equals(payload.Audience, antiForgeryOptions.Audience, StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token audience is not valid for workflow verification."); + } + + var now = timeProvider.GetUtcNow(); + var notBefore = DateTimeOffset.FromUnixTimeSeconds(payload.NotBefore); + if (now < notBefore) + { + throw new InvalidOperationException("Token is not yet valid."); + } + + var expiresAt = DateTimeOffset.FromUnixTimeSeconds(payload.ExpiresAt); + if (now > expiresAt) + { + throw new InvalidOperationException("Token has expired."); + } + + if (!string.IsNullOrWhiteSpace(requiredAction)) + { + var action = requiredAction.Trim().ToLowerInvariant(); + if (!payload.Actions.Contains(action, StringComparer.Ordinal)) + { + throw new InvalidOperationException($"Token does not permit action '{requiredAction}'."); + } + } + + if (!string.IsNullOrWhiteSpace(expectedTenant) && + !string.Equals(payload.Tenant, expectedTenant.Trim(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token tenant does not match the expected tenant."); + } + + if (!string.IsNullOrWhiteSpace(expectedNonce) && + !string.Equals(payload.Nonce, expectedNonce.Trim(), StringComparison.Ordinal)) + { + throw new InvalidOperationException("Token nonce does not match the expected value."); + } + } +} diff --git a/src/Authority/StellaOps.Authority/TASKS.completed.md b/src/Authority/StellaOps.Authority/TASKS.completed.md index 90aff9b6..e97fad53 100644 --- a/src/Authority/StellaOps.Authority/TASKS.completed.md +++ b/src/Authority/StellaOps.Authority/TASKS.completed.md @@ -11,7 +11,7 @@ | AUTH-GRAPH-21-002 | DONE (2025-10-26) | Authority Core & Security Guild | AUTH-GRAPH-21-001, AUTH-AOC-19-002 | Wire gateway enforcement for new graph scopes, Cartographer service identity, and tenant propagation across graph APIs. | Gateway config updated; unauthorized access blocked in integration tests; audit logs include graph scope usage. | | AUTH-GRAPH-21-003 | DONE (2025-10-26) | Authority Core & Docs Guild | AUTH-GRAPH-21-001 | Update security docs and samples describing graph access roles, least privilege guidance, and service identities. | Docs merged with compliance checklist; examples refreshed; release notes prepared. | | AUTH-POLICY-23-001 | DONE (2025-10-29) | Authority Core & Security Guild | AUTH-POLICY-20-001 | Introduce fine-grained scopes `policy:read`, `policy:edit`, `policy:approve`, `policy:activate`, `policy:simulate`; update issuer templates and metadata. | Scopes exposed; integration tests confirm enforcement; offline kit updated. | -| AUTH-VULN-24-001 | DONE (2025-10-29) | Authority Core & Security Guild | AUTH-GRAPH-21-001 | Extend scopes to include `vuln:read` and signed permalinks with scoped claims for Vuln Explorer; update metadata. | Scopes published; permalinks validated; integration tests cover RBAC. | +| AUTH-VULN-24-001 | DONE (2025-10-29) | Authority Core & Security Guild | AUTH-GRAPH-21-001 | Extend Vuln Explorer scopes (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) and signed permalinks with scoped claims for Vuln Explorer; update metadata. | Scopes published; permalinks validated; integration tests cover RBAC. | | AUTH-ORCH-32-001 | DONE (2025-10-31) | Authority Core & Security Guild | — | Define `orch:read` scope, register `Orch.Viewer` role, update discovery metadata, and seed offline defaults. | Scope/role available in metadata; integration tests confirm read-only enforcement; offline kit updated. | | AUTH-CONSOLE-23-001 | DONE (2025-10-29) | Authority Core & Security Guild | AUTH-POLICY-20-001 | Register StellaOps Console confidential client with OIDC PKCE support, short-lived ID/access tokens, `console:*` audience claims, and SPA-friendly refresh (token exchange endpoint). Publish discovery metadata + offline kit defaults. | Client registration committed; configuration templates updated; integration tests validate PKCE + scope issuance; security review recorded. | | AUTH-POLICY-27-001 | DONE (2025-10-31) | Authority Core & Security Guild | AUTH-POLICY-20-001, AUTH-CONSOLE-23-001 | Define Policy Studio roles (`policy:author`, `policy:review`, `policy:approve`, `policy:operate`, `policy:audit`) with tenant-scoped claims, update issuer metadata, and seed offline kit defaults. | Scopes/roles exposed via discovery docs; tokens issued with correct claims; integration tests cover role combinations; docs updated. | diff --git a/src/Authority/StellaOps.Authority/TASKS.md b/src/Authority/StellaOps.Authority/TASKS.md index 186b3503..a10337db 100644 --- a/src/Authority/StellaOps.Authority/TASKS.md +++ b/src/Authority/StellaOps.Authority/TASKS.md @@ -91,9 +91,10 @@ | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| -| AUTH-VULN-29-001 | TODO | Authority Core & Security Guild | AUTH-POLICY-27-001 | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. | Roles/scopes published; issuer templates updated; integration tests cover ABAC filters; docs refreshed. | -| AUTH-VULN-29-002 | TODO | Authority Core & Security Guild | AUTH-VULN-29-001, LEDGER-29-002 | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. | Workflow calls require valid tokens; audit logs include ledger references; security tests cover token expiry/abuse. | -| AUTH-VULN-29-003 | TODO | Authority Core & Docs Guild | AUTH-VULN-29-001..002 | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. | Docs merged with compliance checklist; configuration examples validated; release notes updated. | +| AUTH-VULN-29-001 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-POLICY-27-001 | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. | Roles/scopes published; issuer templates updated; integration tests cover ABAC filters; docs refreshed. | +| AUTH-VULN-29-002 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-VULN-29-001, LEDGER-29-002 | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. | Workflow calls require valid tokens; audit logs include ledger references; security tests cover token expiry/abuse. | +| AUTH-VULN-29-003 | DOING (2025-11-03) | Authority Core & Docs Guild | AUTH-VULN-29-001..002 | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. | Docs merged with compliance checklist; configuration examples validated; release notes updated. | +> 2025-11-03: Vuln workflow CSRF + attachment token services live with audit enrichment and negative-path tests. Awaiting completion of full Authority suite run after repository-wide build finishes. ## Advisory AI (Sprint 31) @@ -130,7 +131,9 @@ |----|--------|----------|------------|-------------|---------------| > 2025-10-28: Tidied advisory raw idempotency migration to avoid LINQ-on-`BsonValue` (explicit array copy) while continuing duplicate guardrail validation; scoped scanner/policy token call sites updated to honor new metadata parameter. | AUTH-TEN-49-001 | DOING (2025-11-02) | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement service accounts & delegation tokens (`act` chain), per-tenant quotas, audit stream of auth decisions, and revocation APIs. | Service tokens minted with scopes/TTL; delegation logged; quotas configurable; audit stream live; docs updated. | +> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run. > 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence. +> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour. ## Observability & Forensics (Epic 15) diff --git a/src/Cartographer/StellaOps.Cartographer/AGENTS.md b/src/Cartographer/StellaOps.Cartographer/AGENTS.md index 02b02135..c3e718d0 100644 --- a/src/Cartographer/StellaOps.Cartographer/AGENTS.md +++ b/src/Cartographer/StellaOps.Cartographer/AGENTS.md @@ -7,7 +7,7 @@ Build and operate the Cartographer service that materializes immutable SBOM prop - Ingest normalized SBOM projections (CycloneDX/SPDX) and generate versioned graph snapshots with tenant-aware storage. - Maintain overlay workers that merge Policy Engine effective findings and VEX metadata onto graph nodes/edges, including path relevance computation. - Serve graph APIs for viewport tiles, paths, filters, exports, simulation overlays, and diffing. -- Coordinate with Policy Engine, Scheduler, Conseiller, Excitator, and Authority to keep overlays current, respect RBAC, and uphold determinism guarantees. +- Coordinate with Policy Engine, Scheduler, Conseiller, Excitor, and Authority to keep overlays current, respect RBAC, and uphold determinism guarantees. - Deliver observability (metrics/traces/logs) and performance benchmarks for large graphs (≥50k nodes). ## Expectations diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md b/src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md index f6e8a259..69a08814 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md @@ -1,111 +1,111 @@ -# TASKS — Epic 1: Aggregation-Only Contract -> **AOC Reminder:** ingestion aggregates and links only—no precedence, normalization, or severity computation. Derived data lives in Policy/overlay services. -| ID | Status | Owner(s) | Depends on | Notes | -|---|---|---|---|---| -> Docs alignment (2025-10-26): Behaviour/spec captured in `docs/ingestion/aggregation-only-contract.md` and architecture overview §2. -> Implementation (2025-10-29): Added `AdvisoryRawWriteGuard` + DI extensions wrapping `AocWriteGuard`, throwing domain-specific `ConcelierAocGuardException` with `ERR_AOC_00x` mappings. Unit tests cover valid/missing-tenant/signature cases. -> Coordination (2025-10-27): Authority `dotnet test` run is currently blocked because `AdvisoryObservationQueryService.BuildAliasLookup` returns `ImmutableHashSet`; please normalise these lookups to `ImmutableHashSet` (trim nulls) so downstream builds succeed. -> 2025-10-31: Added advisory linkset mapper + DI registration, normalized PURL/CPE canonicalization, persisted `reconciled_from` pointers, and refreshed observation factory/tests for new raw linkset shape. -> Docs alignment (2025-10-26): Linkset expectations detailed in AOC reference §4 and policy-engine architecture §2.1. -> 2025-10-28: Advisory raw ingestion now strips client-supplied supersedes hints, logs ignored pointers, and surfaces repository-supplied supersedes identifiers; service tests cover duplicate handling and append-only semantics. +# TASKS — Epic 1: Aggregation-Only Contract +> **AOC Reminder:** ingestion aggregates and links only—no precedence, normalization, or severity computation. Derived data lives in Policy/overlay services. +| ID | Status | Owner(s) | Depends on | Notes | +|---|---|---|---|---| +> Docs alignment (2025-10-26): Behaviour/spec captured in `docs/ingestion/aggregation-only-contract.md` and architecture overview §2. +> Implementation (2025-10-29): Added `AdvisoryRawWriteGuard` + DI extensions wrapping `AocWriteGuard`, throwing domain-specific `ConcelierAocGuardException` with `ERR_AOC_00x` mappings. Unit tests cover valid/missing-tenant/signature cases. +> Coordination (2025-10-27): Authority `dotnet test` run is currently blocked because `AdvisoryObservationQueryService.BuildAliasLookup` returns `ImmutableHashSet`; please normalise these lookups to `ImmutableHashSet` (trim nulls) so downstream builds succeed. +> 2025-10-31: Added advisory linkset mapper + DI registration, normalized PURL/CPE canonicalization, persisted `reconciled_from` pointers, and refreshed observation factory/tests for new raw linkset shape. +> Docs alignment (2025-10-26): Linkset expectations detailed in AOC reference §4 and policy-engine architecture §2.1. +> 2025-10-28: Advisory raw ingestion now strips client-supplied supersedes hints, logs ignored pointers, and surfaces repository-supplied supersedes identifiers; service tests cover duplicate handling and append-only semantics. > Docs alignment (2025-10-26): Deployment guide + observability guide describe supersedes metrics; ensure implementation emits `aoc_violation_total` on failure. | CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DOING (2025-10-28) | Concelier Core Guild | CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003 | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.
2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).
2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.
2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`. | -> Docs alignment (2025-10-26): Architecture overview emphasises policy-only derivation; coordinate with Policy Engine guild for rollout. -> 2025-10-29: `AdvisoryRawService` now preserves upstream alias/linkset ordering (trim-only) and updated AOC documentation reflects the behaviour; follow-up to ensure policy consumers handle duplicates remains open. -| CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Concelier Core Guild | AUTH-AOC-19-002 | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. | Coordinate deliverable so Authority docs (`AUTH-AOC-19-003`) can close once tests are in place. | - -## Policy Engine v2 - -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-POLICY-20-002 `Linkset enrichment for policy` | TODO | Concelier Core Guild, Policy Guild | CONCELIER-CORE-AOC-19-002, POLICY-ENGINE-20-001 | Strengthen linkset builders with vendor-specific equivalence tables, NEVRA/PURL normalization, and version range parsing to maximize policy join recall; update fixtures + docs. | -> 2025-10-31: Base advisory linkset mapper landed under `CONCELIER-CORE-AOC-19-002`; policy enrichment work can now proceed with mapper outputs and observation schema fixtures. - -## Graph Explorer v1 - -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-GRAPH-21-001 `SBOM projection enrichment` | BLOCKED (2025-10-27) | Concelier Core Guild, Cartographer Guild | CONCELIER-POLICY-20-002, CARTO-GRAPH-21-002 | Extend SBOM normalization to emit full relationship graph (depends_on/contains/provides), scope tags, entrypoint annotations, and component metadata required by Cartographer. | -> 2025-10-27: Waiting on policy-driven linkset enrichment (`CONCELIER-POLICY-20-002`) and Cartographer API contract (`CARTO-GRAPH-21-002`) to define required relationship payloads. Without those schemas the projection changes cannot be implemented deterministically. -> 2025-10-29: Cross-guild handshake captured in `docs/dev/cartographer-graph-handshake.md`; begin drafting enrichment plan once Cartographer ships the inspector schema/query patterns. -| CONCELIER-GRAPH-21-002 `Change events` | BLOCKED (2025-10-27) | Concelier Core Guild, Scheduler Guild | CONCELIER-GRAPH-21-001 | Publish change events (new SBOM version, relationship delta) for Cartographer build queue; ensure events include tenant/context metadata. | -> 2025-10-27: Depends on `CONCELIER-GRAPH-21-001`; event schema hinges on finalized projection output and Cartographer webhook contract, both pending. -> 2025-10-29: Action item from handshake doc — prepare sample `sbom.relationship.changed` payload + replay notes once schema lands; coordinate with Scheduler for queue semantics. - -## Link-Not-Merge v1 - -| ID | Status | Owner(s) | Depends on | Notes | +> Docs alignment (2025-10-26): Architecture overview emphasises policy-only derivation; coordinate with Policy Engine guild for rollout. +> 2025-10-29: `AdvisoryRawService` now preserves upstream alias/linkset ordering (trim-only) and updated AOC documentation reflects the behaviour; follow-up to ensure policy consumers handle duplicates remains open. +| CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Concelier Core Guild | AUTH-AOC-19-002 | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. | Coordinate deliverable so Authority docs (`AUTH-AOC-19-003`) can close once tests are in place. | + +## Policy Engine v2 + +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-POLICY-20-002 `Linkset enrichment for policy` | TODO | Concelier Core Guild, Policy Guild | CONCELIER-CORE-AOC-19-002, POLICY-ENGINE-20-001 | Strengthen linkset builders with vendor-specific equivalence tables, NEVRA/PURL normalization, and version range parsing to maximize policy join recall; update fixtures + docs. | +> 2025-10-31: Base advisory linkset mapper landed under `CONCELIER-CORE-AOC-19-002`; policy enrichment work can now proceed with mapper outputs and observation schema fixtures. + +## Graph Explorer v1 + +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-GRAPH-21-001 `SBOM projection enrichment` | BLOCKED (2025-10-27) | Concelier Core Guild, Cartographer Guild | CONCELIER-POLICY-20-002, CARTO-GRAPH-21-002 | Extend SBOM normalization to emit full relationship graph (depends_on/contains/provides), scope tags, entrypoint annotations, and component metadata required by Cartographer. | +> 2025-10-27: Waiting on policy-driven linkset enrichment (`CONCELIER-POLICY-20-002`) and Cartographer API contract (`CARTO-GRAPH-21-002`) to define required relationship payloads. Without those schemas the projection changes cannot be implemented deterministically. +> 2025-10-29: Cross-guild handshake captured in `docs/dev/cartographer-graph-handshake.md`; begin drafting enrichment plan once Cartographer ships the inspector schema/query patterns. +| CONCELIER-GRAPH-21-002 `Change events` | BLOCKED (2025-10-27) | Concelier Core Guild, Scheduler Guild | CONCELIER-GRAPH-21-001 | Publish change events (new SBOM version, relationship delta) for Cartographer build queue; ensure events include tenant/context metadata. | +> 2025-10-27: Depends on `CONCELIER-GRAPH-21-001`; event schema hinges on finalized projection output and Cartographer webhook contract, both pending. +> 2025-10-29: Action item from handshake doc — prepare sample `sbom.relationship.changed` payload + replay notes once schema lands; coordinate with Scheduler for queue semantics. + +## Link-Not-Merge v1 + +| ID | Status | Owner(s) | Depends on | Notes | |----|--------|----------|------------|-------| | CONCELIER-LNM-21-001 `Advisory observation schema` | TODO | Concelier Core Guild | CONCELIER-CORE-AOC-19-001 | Introduce immutable `advisory_observations` model with AOC metadata, raw payload pointers, structured per-source fields (version ranges, severity, CVSS), and tenancy guardrails; publish schema definition. `DOCS-LNM-22-001` blocked pending this deliverable. | -| CONCELIER-LNM-21-002 `Linkset builder` | TODO | Concelier Core Guild, Data Science Guild | CONCELIER-LNM-21-001 | Implement correlation pipeline (alias graph, PURL overlap, CVSS vector equality, fuzzy title match) that produces `advisory_linksets` with confidence + conflict annotations. Docs note: unblock `DOCS-LNM-22-001` once builder lands. | -| CONCELIER-LNM-21-003 `Conflict annotator` | TODO | Concelier Core Guild | CONCELIER-LNM-21-002 | Detect field disagreements (severity, CVSS, ranges, references) and record structured conflicts on linksets; surface to API/UI. Docs awaiting structured conflict payloads. | -| CONCELIER-LNM-21-004 `Merge code removal` | TODO | Concelier Core Guild | CONCELIER-LNM-21-002 | Excise existing merge/dedup logic, enforce immutability on observations, and add guards/tests to prevent future merges. | -| CONCELIER-LNM-21-005 `Event emission` | TODO | Concelier Core Guild, Platform Events Guild | CONCELIER-LNM-21-002 | Emit `advisory.linkset.updated` events with delta payloads for downstream Policy Engine/Cartographer consumers; ensure idempotent delivery. | - -## Policy Engine + Editor v1 - -| ID | Status | Owner(s) | Depends on | Notes | +| CONCELIER-LNM-21-002 `Linkset builder` | TODO | Concelier Core Guild, Data Science Guild | CONCELIER-LNM-21-001 | Implement correlation pipeline (alias graph, PURL overlap, CVSS vector equality, fuzzy title match) that produces `advisory_linksets` with confidence + conflict annotations. Docs note: unblock `DOCS-LNM-22-001` once builder lands. | +| CONCELIER-LNM-21-003 `Conflict annotator` | TODO | Concelier Core Guild | CONCELIER-LNM-21-002 | Detect field disagreements (severity, CVSS, ranges, references) and record structured conflicts on linksets; surface to API/UI. Docs awaiting structured conflict payloads. | +| CONCELIER-LNM-21-004 `Merge code removal` | TODO | Concelier Core Guild | CONCELIER-LNM-21-002 | Excise existing merge/dedup logic, enforce immutability on observations, and add guards/tests to prevent future merges. | +| CONCELIER-LNM-21-005 `Event emission` | TODO | Concelier Core Guild, Platform Events Guild | CONCELIER-LNM-21-002 | Emit `advisory.linkset.updated` events with delta payloads for downstream Policy Engine/Cartographer consumers; ensure idempotent delivery. | + +## Policy Engine + Editor v1 + +| ID | Status | Owner(s) | Depends on | Notes | |----|--------|----------|------------|-------| | CONCELIER-POLICY-23-001 `Evidence indexes` | TODO | Concelier Core Guild | CONCELIER-LNM-21-002 | Add secondary indexes/materialized views to accelerate policy lookups (alias, provider severity per observation, correlation confidence). Document query contracts for runtime. | -| CONCELIER-POLICY-23-002 `Event guarantees` | TODO | Concelier Core Guild, Platform Events Guild | CONCELIER-LNM-21-005 | Ensure `advisory.linkset.updated` emits at-least-once with idempotent keys and include policy-relevant metadata (confidence, conflict summary). | - -## Graph & Vuln Explorer v1 - -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -> 2025-10-29: Filter-aware lookup path and /concelier/observations coverage landed; overlay services can consume raw advisory feeds deterministically. - -## Reachability v1 - -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-SIG-26-001 `Vulnerable symbol exposure` | TODO | Concelier Core Guild, Signals Guild | SIGNALS-24-002 | Expose advisory metadata (affected symbols/functions) via API to enrich reachability scoring; update fixtures. | - -## Orchestrator Dashboard - -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-ORCH-32-001 `Source registry integration` | TODO | Concelier Core Guild | ORCH-SVC-32-001, AUTH-ORCH-32-001 | Register Concelier data sources with orchestrator (metadata, schedules, rate policies) and wire provenance IDs/security scopes. | -| CONCELIER-ORCH-32-002 `Worker SDK adoption` | TODO | Concelier Core Guild | CONCELIER-ORCH-32-001, WORKER-GO-32-001, WORKER-PY-32-001 | Embed orchestrator worker SDK in ingestion loops, emit heartbeats/progress/artifact hashes, and enforce idempotency keys. | -| CONCELIER-ORCH-33-001 `Control hook compliance` | TODO | Concelier Core Guild | CONCELIER-ORCH-32-002, ORCH-SVC-33-001, ORCH-SVC-33-002 | Honor orchestrator throttle/pause/retry actions, surface structured error classes, and persist safe checkpoints for resume. | -| CONCELIER-ORCH-34-001 `Backfill + ledger linkage` | TODO | Concelier Core Guild | CONCELIER-ORCH-33-001, ORCH-SVC-33-003, ORCH-SVC-34-001 | Execute orchestrator-driven backfills, reuse artifact hashes to avoid duplicates, and link provenance to run ledger exports. | - -## Authority-Backed Scopes & Tenancy (Epic 14) -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-TEN-48-001 `Tenant-aware linking` | TODO | Concelier Core Guild | AUTH-TEN-47-001 | Ensure advisory normalization/linking runs per tenant with RLS enforcing isolation; emit capability endpoint reporting `merge=false`; update events with tenant context. | - -## Observability & Forensics (Epic 15) -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-OBS-50-001 `Telemetry adoption` | TODO | Concelier Core Guild, Observability Guild | TELEMETRY-OBS-50-001, TELEMETRY-OBS-50-002 | Replace ad-hoc logging with telemetry core across ingestion/linking pipelines; ensure spans/logs include tenant, source vendor, upstream id, content hash, and trace IDs. | -| CONCELIER-OBS-51-001 `Metrics & SLOs` | TODO | Concelier Core Guild, DevOps Guild | CONCELIER-OBS-50-001, TELEMETRY-OBS-51-001 | Emit metrics for ingest latency (cold/warm), queue depth, aoc violation rate, and publish SLO burn-rate alerts (ingest P95 <30s cold / <5s warm). Ship dashboards + alert configs. | -| CONCELIER-OBS-52-001 `Timeline events` | TODO | Concelier Core Guild | CONCELIER-OBS-50-001, TIMELINE-OBS-52-002 | Emit `timeline_event` records for advisory ingest/normalization/linkset creation with provenance, trace IDs, conflict summaries, and evidence placeholders. | -| CONCELIER-OBS-53-001 `Evidence snapshots` | TODO | Concelier Core Guild, Evidence Locker Guild | CONCELIER-OBS-52-001, EVID-OBS-53-002 | Produce advisory evaluation bundle payloads (raw doc, linkset, normalization diff) for evidence locker; ensure Merkle manifests seeded with content hashes. | -| CONCELIER-OBS-54-001 `Attestation & verification` | TODO | Concelier Core Guild, Provenance Guild | CONCELIER-OBS-53-001, PROV-OBS-54-001 | Attach DSSE attestations for advisory processing batches, expose verification API to confirm bundle integrity, and link attestation IDs back to timeline + ledger. | -| CONCELIER-OBS-55-001 `Incident mode hooks` | TODO | Concelier Core Guild, DevOps Guild | CONCELIER-OBS-51-001, DEVOPS-OBS-55-001 | Increase sampling, capture raw payload snapshots, and extend retention under incident mode; emit activation events + guardrails against PII leak. | - -## Air-Gapped Mode (Epic 16) -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-AIRGAP-56-001 `Mirror ingestion adapters` | TODO | Concelier Core Guild | AIRGAP-IMP-57-002, MIRROR-CRT-56-001 | Add mirror source adapters reading advisories from imported bundles, preserving source metadata and bundle IDs. Ensure ingestion remains append-only. | -| CONCELIER-AIRGAP-56-002 `Bundle catalog linking` | TODO | Concelier Core Guild, AirGap Importer Guild | CONCELIER-AIRGAP-56-001, AIRGAP-IMP-57-001 | Persist `bundle_id`, `merkle_root`, and time anchor references on observations/linksets for provenance. | -| CONCELIER-AIRGAP-57-001 `Sealed-mode source restrictions` | TODO | Concelier Core Guild, AirGap Policy Guild | CONCELIER-AIRGAP-56-001, AIRGAP-POL-56-001 | Enforce sealed-mode egress rules by disallowing non-mirror connectors and surfacing remediation errors. | -| CONCELIER-AIRGAP-57-002 `Staleness annotations` | TODO | Concelier Core Guild, AirGap Time Guild | CONCELIER-AIRGAP-56-002, AIRGAP-TIME-58-001 | Compute staleness metadata for advisories per bundle and expose via API for Console/CLI badges. | -| CONCELIER-AIRGAP-58-001 `Portable advisory evidence` | TODO | Concelier Core Guild, Evidence Locker Guild | CONCELIER-OBS-53-001, EVID-OBS-54-001 | Package advisory evidence fragments into portable evidence bundles for cross-domain transfer. | - -## SDKs & OpenAPI (Epic 17) -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-OAS-61-001 `Spec coverage` | TODO | Concelier Core Guild, API Contracts Guild | OAS-61-001 | Update Concelier OAS with advisory observation/linkset endpoints, standard pagination, and source provenance fields. | -| CONCELIER-OAS-61-002 `Examples library` | TODO | Concelier Core Guild | CONCELIER-OAS-61-001 | Provide rich examples for advisories, linksets, conflict annotations used by SDK + docs. | -| CONCELIER-OAS-62-001 `SDK smoke tests` | TODO | Concelier Core Guild, SDK Generator Guild | CONCELIER-OAS-61-001, SDKGEN-63-001 | Add SDK tests covering advisory search, pagination, and conflict handling; ensure source metadata surfaced. | -| CONCELIER-OAS-63-001 `Deprecation headers` | TODO | Concelier Core Guild, API Governance Guild | APIGOV-63-001 | Implement deprecation header support and timeline events for retiring endpoints. | - -## Risk Profiles (Epic 18) -| ID | Status | Owner(s) | Depends on | Notes | -|----|--------|----------|------------|-------| -| CONCELIER-RISK-66-001 `CVSS/KEV providers` | TODO | Concelier Core Guild, Risk Engine Guild | RISK-ENGINE-67-001 | Expose CVSS, KEV, fix availability data via provider APIs with source metadata preserved. | +| CONCELIER-POLICY-23-002 `Event guarantees` | TODO | Concelier Core Guild, Platform Events Guild | CONCELIER-LNM-21-005 | Ensure `advisory.linkset.updated` emits at-least-once with idempotent keys and include policy-relevant metadata (confidence, conflict summary). | + +## Graph & Vuln Explorer v1 + +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +> 2025-10-29: Filter-aware lookup path and /concelier/observations coverage landed; overlay services can consume raw advisory feeds deterministically. + +## Reachability v1 + +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-SIG-26-001 `Vulnerable symbol exposure` | TODO | Concelier Core Guild, Signals Guild | SIGNALS-24-002 | Expose advisory metadata (affected symbols/functions) via API to enrich reachability scoring; update fixtures. | + +## Orchestrator Dashboard + +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-ORCH-32-001 `Source registry integration` | TODO | Concelier Core Guild | ORCH-SVC-32-001, AUTH-ORCH-32-001 | Register Concelier data sources with orchestrator (metadata, schedules, rate policies) and wire provenance IDs/security scopes. | +| CONCELIER-ORCH-32-002 `Worker SDK adoption` | TODO | Concelier Core Guild | CONCELIER-ORCH-32-001, WORKER-GO-32-001, WORKER-PY-32-001 | Embed orchestrator worker SDK in ingestion loops, emit heartbeats/progress/artifact hashes, and enforce idempotency keys. | +| CONCELIER-ORCH-33-001 `Control hook compliance` | TODO | Concelier Core Guild | CONCELIER-ORCH-32-002, ORCH-SVC-33-001, ORCH-SVC-33-002 | Honor orchestrator throttle/pause/retry actions, surface structured error classes, and persist safe checkpoints for resume. | +| CONCELIER-ORCH-34-001 `Backfill + ledger linkage` | TODO | Concelier Core Guild | CONCELIER-ORCH-33-001, ORCH-SVC-33-003, ORCH-SVC-34-001 | Execute orchestrator-driven backfills, reuse artifact hashes to avoid duplicates, and link provenance to run ledger exports. | + +## Authority-Backed Scopes & Tenancy (Epic 14) +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-TEN-48-001 `Tenant-aware linking` | TODO | Concelier Core Guild | AUTH-TEN-47-001 | Ensure advisory normalization/linking runs per tenant with RLS enforcing isolation; emit capability endpoint reporting `merge=false`; update events with tenant context. | + +## Observability & Forensics (Epic 15) +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-OBS-50-001 `Telemetry adoption` | TODO | Concelier Core Guild, Observability Guild | TELEMETRY-OBS-50-001, TELEMETRY-OBS-50-002 | Replace ad-hoc logging with telemetry core across ingestion/linking pipelines; ensure spans/logs include tenant, source vendor, upstream id, content hash, and trace IDs. | +| CONCELIER-OBS-51-001 `Metrics & SLOs` | TODO | Concelier Core Guild, DevOps Guild | CONCELIER-OBS-50-001, TELEMETRY-OBS-51-001 | Emit metrics for ingest latency (cold/warm), queue depth, aoc violation rate, and publish SLO burn-rate alerts (ingest P95 <30s cold / <5s warm). Ship dashboards + alert configs. | +| CONCELIER-OBS-52-001 `Timeline events` | TODO | Concelier Core Guild | CONCELIER-OBS-50-001, TIMELINE-OBS-52-002 | Emit `timeline_event` records for advisory ingest/normalization/linkset creation with provenance, trace IDs, conflict summaries, and evidence placeholders. | +| CONCELIER-OBS-53-001 `Evidence snapshots` | TODO | Concelier Core Guild, Evidence Locker Guild | CONCELIER-OBS-52-001, EVID-OBS-53-002 | Produce advisory evaluation bundle payloads (raw doc, linkset, normalization diff) for evidence locker; ensure Merkle manifests seeded with content hashes. | +| CONCELIER-OBS-54-001 `Attestation & verification` | TODO | Concelier Core Guild, Provenance Guild | CONCELIER-OBS-53-001, PROV-OBS-54-001 | Attach DSSE attestations for advisory processing batches, expose verification API to confirm bundle integrity, and link attestation IDs back to timeline + ledger. | +| CONCELIER-OBS-55-001 `Incident mode hooks` | TODO | Concelier Core Guild, DevOps Guild | CONCELIER-OBS-51-001, DEVOPS-OBS-55-001 | Increase sampling, capture raw payload snapshots, and extend retention under incident mode; emit activation events + guardrails against PII leak. | + +## Air-Gapped Mode (Epic 16) +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-AIRGAP-56-001 `Mirror ingestion adapters` | TODO | Concelier Core Guild | AIRGAP-IMP-57-002, MIRROR-CRT-56-001 | Add mirror source adapters reading advisories from imported bundles, preserving source metadata and bundle IDs. Ensure ingestion remains append-only. | +| CONCELIER-AIRGAP-56-002 `Bundle catalog linking` | TODO | Concelier Core Guild, AirGap Importer Guild | CONCELIER-AIRGAP-56-001, AIRGAP-IMP-57-001 | Persist `bundle_id`, `merkle_root`, and time anchor references on observations/linksets for provenance. | +| CONCELIER-AIRGAP-57-001 `Sealed-mode source restrictions` | TODO | Concelier Core Guild, AirGap Policy Guild | CONCELIER-AIRGAP-56-001, AIRGAP-POL-56-001 | Enforce sealed-mode egress rules by disallowing non-mirror connectors and surfacing remediation errors.
2025-11-02: AIRGAP-POL-56-001 delivered the EgressPolicy facade; ready to draft sealed-mode enforcement flows against the new policy API. | +| CONCELIER-AIRGAP-57-002 `Staleness annotations` | TODO | Concelier Core Guild, AirGap Time Guild | CONCELIER-AIRGAP-56-002, AIRGAP-TIME-58-001 | Compute staleness metadata for advisories per bundle and expose via API for Console/CLI badges. | +| CONCELIER-AIRGAP-58-001 `Portable advisory evidence` | TODO | Concelier Core Guild, Evidence Locker Guild | CONCELIER-OBS-53-001, EVID-OBS-54-001 | Package advisory evidence fragments into portable evidence bundles for cross-domain transfer. | + +## SDKs & OpenAPI (Epic 17) +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-OAS-61-001 `Spec coverage` | TODO | Concelier Core Guild, API Contracts Guild | OAS-61-001 | Update Concelier OAS with advisory observation/linkset endpoints, standard pagination, and source provenance fields. | +| CONCELIER-OAS-61-002 `Examples library` | TODO | Concelier Core Guild | CONCELIER-OAS-61-001 | Provide rich examples for advisories, linksets, conflict annotations used by SDK + docs. | +| CONCELIER-OAS-62-001 `SDK smoke tests` | TODO | Concelier Core Guild, SDK Generator Guild | CONCELIER-OAS-61-001, SDKGEN-63-001 | Add SDK tests covering advisory search, pagination, and conflict handling; ensure source metadata surfaced. | +| CONCELIER-OAS-63-001 `Deprecation headers` | TODO | Concelier Core Guild, API Governance Guild | APIGOV-63-001 | Implement deprecation header support and timeline events for retiring endpoints. | + +## Risk Profiles (Epic 18) +| ID | Status | Owner(s) | Depends on | Notes | +|----|--------|----------|------------|-------| +| CONCELIER-RISK-66-001 `CVSS/KEV providers` | TODO | Concelier Core Guild, Risk Engine Guild | RISK-ENGINE-67-001 | Expose CVSS, KEV, fix availability data via provider APIs with source metadata preserved. | | CONCELIER-RISK-66-002 `Fix availability signals` | TODO | Concelier Core Guild | CONCELIER-RISK-66-001 | Provide structured fix availability and release metadata consumable by risk engine; document provenance. | | CONCELIER-RISK-67-001 `Source coverage metrics` | TODO | Concelier Core Guild | CONCELIER-RISK-66-001 | Add per-source coverage metrics for linked advisories (observation counts, conflicting statuses) without computing consensus scores; ensure explainability includes source digests. | | CONCELIER-RISK-68-001 `Policy Studio integration` | TODO | Concelier Core Guild, Policy Studio Guild | POLICY-RISK-68-001 | Surface advisory fields in Policy Studio profile editor (signal pickers, reducers). | diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md index 86cffc8c..ed04d1f9 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md @@ -9,6 +9,7 @@ ## Link-Not-Merge v1 Transition | Task | Owner(s) | Depends on | Notes | |---|---|---|---| -|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|Draft `no-merge` migration playbook, documenting backfill strategy, feature flag rollout, and rollback steps for legacy merge pipeline deprecation.| -|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage.| +|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** – Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.| +|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DOING (2025-11-03)** – Auditing service registrations, DI bindings, and tests consuming `AdvisoryMergeService`; drafting deprecation plan and analyzer scope prior to code removal.| +> 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage. |MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible.| diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/AGENTS.md b/src/EvidenceLocker/StellaOps.EvidenceLocker/AGENTS.md index 75873b96..cd33014c 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/AGENTS.md +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/AGENTS.md @@ -7,7 +7,7 @@ Implement the append-only, tenant-scoped evidence locker detailed in Epic 15. Pr - Define object store layout, metadata DB schemas, and retention policies. - Build bundle assembly pipelines (evaluation, job, export) with Merkle manifests and DSSE signing. - Provide verification, download, and legal hold APIs with audit trails. -- Integrate with Timeline Indexer, Exporter, Orchestrator, Policy Engine, Concelier, and Excitator for provenance linking. +- Integrate with Timeline Indexer, Exporter, Orchestrator, Policy Engine, Concelier, and Excitor for provenance linking. ## Coordination - Work with Provenance Guild for signature tooling. diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs index c19d0562..dda1972c 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.AirGap.Policy; var builder = WebApplication.CreateBuilder(args); @@ -21,6 +22,8 @@ builder.Services.AddAuthorization(options => options.FallbackPolicy = options.DefaultPolicy; }); +builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap"); + builder.Services.AddOpenApi(); var app = builder.Build(); diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj index a53d5c72..84fea52e 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj @@ -16,5 +16,6 @@ + diff --git a/src/ExportCenter/StellaOps.ExportCenter/TASKS.md b/src/ExportCenter/StellaOps.ExportCenter/TASKS.md index a00a428a..e813f728 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/TASKS.md +++ b/src/ExportCenter/StellaOps.ExportCenter/TASKS.md @@ -1,6 +1,8 @@ -# Exporter Service Task Board — Epic 10: Export Center - -## Sprint 35 – Foundations (JSON + Mirror Full, Download Only) +# Exporter Service Task Board — Epic 10: Export Center + +> 2025-11-03: Link-Not-Merge migration playbook docs/migration/no-merge.md is live—coordinate export bundle staging with its rollout/backfill phases when planning advisory evidence updates. + +## Sprint 35 – Foundations (JSON + Mirror Full, Download Only) | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| | EXPORT-SVC-35-001 | BLOCKED (2025-10-29) | Exporter Service Guild | ORCH-SVC-35-101, LEDGER-EXPORT-35-001 | Bootstrap exporter service project, configuration, and Postgres migrations for `export_profiles`, `export_runs`, `export_inputs`, `export_distributions` with tenant scoping + tests. | Service builds/tests; migrations generated with scripts; baseline integration test seeds schema; compliance checklist recorded. | diff --git a/src/Findings/StellaOps.Findings.Ledger/AGENTS.md b/src/Findings/StellaOps.Findings.Ledger/AGENTS.md index d3d98e29..97308d6a 100644 --- a/src/Findings/StellaOps.Findings.Ledger/AGENTS.md +++ b/src/Findings/StellaOps.Findings.Ledger/AGENTS.md @@ -7,13 +7,13 @@ Operate the append-only Findings Ledger and projection pipeline powering the Vul - Service code under `src/Findings/StellaOps.Findings.Ledger` (event API, projector, migrations, crypto hashing). - Ledger storage schemas, Merkle anchoring jobs, retention policies, and replay tooling. - Projection pipeline writing `findings_projection` collections/tables consumed by Vuln Explorer API and Console. -- Collaboration with Conseiller, Excitator, SBOM Service, Policy Engine, Scheduler, Authority, and DevOps for evidence feeds and policy events. +- Collaboration with Conseiller, Excitor, SBOM Service, Policy Engine, Scheduler, Authority, and DevOps for evidence feeds and policy events. ## Principles 1. **Immutability** – Ledger events are append-only, hashed, and chained; projections derive from ledger plus policy inputs. 2. **Determinism** – Replaying the same event stream yields identical projections and bundle outputs; hashing uses canonical JSON. 3. **Tenant isolation** – Separate namespaces per tenant in storage, queue, and Merkle anchoring artefacts. -4. **AOC alignment** – Ledger records workflow only; evidence remains in Conseiller/Excitator/SBOM stores; no mutation of source facts. +4. **AOC alignment** – Ledger records workflow only; evidence remains in Conseiller/Excitor/SBOM stores; no mutation of source facts. 5. **Auditability** – Provide verifiable hashes, Merkle roots, and replay tooling for auditors. ## Collaboration diff --git a/src/Graph/StellaOps.Graph.Api/AGENTS.md b/src/Graph/StellaOps.Graph.Api/AGENTS.md index c0bf322c..03157217 100644 --- a/src/Graph/StellaOps.Graph.Api/AGENTS.md +++ b/src/Graph/StellaOps.Graph.Api/AGENTS.md @@ -1,7 +1,7 @@ # Graph API Guild Charter (Epic 5) ## Mission -Provide tenant-scoped Graph Explorer APIs for search, query, paths, diffs, overlays, and exports. Deliver cost-aware streaming endpoints that integrate with Policy Engine, Conseiller, Excitator, and the Graph Indexer while honoring AOC and RBAC. +Provide tenant-scoped Graph Explorer APIs for search, query, paths, diffs, overlays, and exports. Deliver cost-aware streaming endpoints that integrate with Policy Engine, Conseiller, Excitor, and the Graph Indexer while honoring AOC and RBAC. ## Scope - Service under `src/Graph/StellaOps.Graph.Api` (Minimal API + streaming pipeline + query planner). diff --git a/src/Graph/StellaOps.Graph.Indexer/AGENTS.md b/src/Graph/StellaOps.Graph.Indexer/AGENTS.md index cb35f4f7..3b78fa2f 100644 --- a/src/Graph/StellaOps.Graph.Indexer/AGENTS.md +++ b/src/Graph/StellaOps.Graph.Indexer/AGENTS.md @@ -6,19 +6,19 @@ Project SBOM, advisory, VEX, and policy overlay data into a tenant-scoped proper ## Scope - Service source under `src/Graph/StellaOps.Graph.Indexer` (workers, ingestion pipelines, schema builders). - Mongo collections/object storage for `graph_nodes`, `graph_edges`, `graph_snapshots`, clustering metadata. -- Event consumers: SBOM ingest, Conseiller advisories, Excitator VEX, Policy overlay materials. +- Event consumers: SBOM ingest, Conseiller advisories, Excitor VEX, Policy overlay materials. - Incremental rebuild, diff, and cache warmers for graph overlays. ## Principles 1. **Immutability** – Graph mirrors SBOM snapshots; new data creates new snapshots rather than mutating historical records. 2. **Determinism** – Given identical inputs, node/edge ids, hashes, and aggregates remain stable across runs. 3. **Tenant isolation** – Enforce isolation at ingestion, storage, and job levels; no cross-tenant leakage. -4. **AOC alignment** – Indexer links facts; it never mutates advisories/VEX/policy outcomes. Conseiller/Excitator/Policy Engine remain authoritative. +4. **AOC alignment** – Indexer links facts; it never mutates advisories/VEX/policy outcomes. Conseiller/Excitor/Policy Engine remain authoritative. 5. **Performance & telemetry** – Every job emits metrics (latency, node/edge counts, queue lag) and structured logs. ## Collaboration - Keep `src/Graph/StellaOps.Graph.Indexer/TASKS.md`, `../../docs/implplan/SPRINTS.md` synchronized. -- Coordinate with SBOM Service, Policy Engine, Conseiller, Excitator, Scheduler, Web Gateway, and Console teams. +- Coordinate with SBOM Service, Policy Engine, Conseiller, Excitor, Scheduler, Web Gateway, and Console teams. - Publish schema docs and fixtures for clients; share cost/identity conventions across services. ## Tooling diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/AGENTS.md b/src/IssuerDirectory/StellaOps.IssuerDirectory/AGENTS.md index 2ee4f71e..10d2cc39 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/AGENTS.md +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/AGENTS.md @@ -5,7 +5,7 @@ Manage trusted VEX issuer metadata, keys, and trust overrides used by the VEX Le ## Scope - Service `src/IssuerDirectory/StellaOps.IssuerDirectory` providing REST APIs and admin tooling for issuers, keys, trust weights, audit logs. -- Integration with Excitator/VEX Lens/Policy Engine for signature verification and trust weighting. +- Integration with Excitor/VEX Lens/Policy Engine for signature verification and trust weighting. - Tenant overrides, import of CSAF publisher metadata, and compliance logging. ## Principles @@ -17,7 +17,7 @@ Manage trusted VEX issuer metadata, keys, and trust overrides used by the VEX Le ## Definition of Done - APIs documented, RBAC enforced, audit logs persisted. -- Key verification integrated with VEX Lens and Excitator; rotation tooling delivered. +- Key verification integrated with VEX Lens and Excitor; rotation tooling delivered. - Docs/runbooks updated with compliance checklist. ## Required Reading diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md b/src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md index 4aa47f60..645e9f3d 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/TASKS.md @@ -1,11 +1,11 @@ -# Issuer Directory Task Board — Epic 7 -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| +# Issuer Directory Task Board — Epic 7 +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| | ISSUER-30-001 | DONE (2025-11-01) | Issuer Directory Guild | AUTH-VULN-29-001 | Implement issuer CRUD API with RBAC, audit logging, and tenant scoping; seed CSAF publisher metadata. | APIs deployed; audit logs capture actor/reason; seed data imported; tests cover RBAC. | | ISSUER-30-002 | DONE (2025-11-01) | Issuer Directory Guild, Security Guild | ISSUER-30-001 | Implement key management endpoints (add/rotate/revoke keys), enforce expiry, validate formats (Ed25519, X.509, DSSE). | Keys stored securely; expiry enforced; validation tests cover key types; docs updated. | | ISSUER-30-003 | DOING | Issuer Directory Guild, Policy Guild | ISSUER-30-001 | Provide trust weight APIs and tenant overrides with validation (+/- bounds) and audit trails. | Trust overrides persisted; policy integration confirmed; tests cover overrides. | -| ISSUER-30-004 | DONE (2025-11-01) | Issuer Directory Guild, VEX Lens Guild | ISSUER-30-001..003 | Integrate with VEX Lens and Excitator signature verification (client SDK, caching, retries). | Lens/Excitator resolve issuer metadata via SDK; integration tests cover network failures. | +| ISSUER-30-004 | DONE (2025-11-01) | Issuer Directory Guild, VEX Lens Guild | ISSUER-30-001..003 | Integrate with VEX Lens and Excitor signature verification (client SDK, caching, retries). | Lens/Excitor resolve issuer metadata via SDK; integration tests cover network failures. | | ISSUER-30-005 | DONE (2025-11-01) | Issuer Directory Guild, Observability Guild | ISSUER-30-001..004 | Instrument metrics/logs (issuer changes, key rotation, verification failures) and dashboards/alerts. | Telemetry live; alerts configured; docs updated. | -| ISSUER-30-006 | DOING (2025-11-02) | Issuer Directory Guild, DevOps Guild | ISSUER-30-001..005 | Provide deployment manifests, backup/restore, secure secret storage, and offline kit instructions. | Deployment docs merged; smoke deploy validated; backup tested; offline kit updated. | +| ISSUER-30-006 | DONE (2025-11-02) | Issuer Directory Guild, DevOps Guild | ISSUER-30-001..005 | Provide deployment manifests, backup/restore, secure secret storage, and offline kit instructions. | Deployment docs merged; smoke deploy validated; backup tested; offline kit updated. | > 2025-11-01: Excititor worker now queries Issuer Directory via during attestation verification, caching active key metadata and trust weights for tenant/global scopes. diff --git a/src/Notifier/StellaOps.Notifier/TASKS.md b/src/Notifier/StellaOps.Notifier/TASKS.md index 040313fd..03d44f81 100644 --- a/src/Notifier/StellaOps.Notifier/TASKS.md +++ b/src/Notifier/StellaOps.Notifier/TASKS.md @@ -1,5 +1,10 @@ # Notifier Service Task Board — Epic 11: Notifications Studio +# Sprint 70 – Documentation & Decisions +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| NOTIFY-DOC-70-001 | DONE | Notifications Service Guild | — | Document the decision to keep `src/Notify` as the shared library toolkit and `src/Notifier` as the runtime host; update notifications docs with rationale and cross-links (completed 2025-11-02). | Notes published in notifications architecture docs clarifying module boundaries and citing decision. | + # Sprint 37 – Pack Approval Bridge (Task Runner integration) | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| diff --git a/src/Notify/__Libraries/StellaOps.Notify.Models/NotifyEventKinds.cs b/src/Notify/__Libraries/StellaOps.Notify.Models/NotifyEventKinds.cs index 4e98d100..7986f186 100644 --- a/src/Notify/__Libraries/StellaOps.Notify.Models/NotifyEventKinds.cs +++ b/src/Notify/__Libraries/StellaOps.Notify.Models/NotifyEventKinds.cs @@ -1,15 +1,15 @@ -namespace StellaOps.Notify.Models; - -/// -/// Known platform event kind identifiers consumed by Notify. -/// -public static class NotifyEventKinds -{ - public const string ScannerReportReady = "scanner.report.ready"; - public const string ScannerScanCompleted = "scanner.scan.completed"; - public const string SchedulerRescanDelta = "scheduler.rescan.delta"; - public const string AttestorLogged = "attestor.logged"; - public const string ZastavaAdmission = "zastava.admission"; - public const string FeedserExportCompleted = "feedser.export.completed"; - public const string VexerExportCompleted = "vexer.export.completed"; -} +namespace StellaOps.Notify.Models; + +/// +/// Known platform event kind identifiers consumed by Notify. +/// +public static class NotifyEventKinds +{ + public const string ScannerReportReady = "scanner.report.ready"; + public const string ScannerScanCompleted = "scanner.scan.completed"; + public const string SchedulerRescanDelta = "scheduler.rescan.delta"; + public const string AttestorLogged = "attestor.logged"; + public const string ZastavaAdmission = "zastava.admission"; + public const string ConselierExportCompleted = "conselier.export.completed"; + public const string ExcitorExportCompleted = "excitor.export.completed"; +} diff --git a/src/Policy/StellaOps.Policy.Engine/Program.cs b/src/Policy/StellaOps.Policy.Engine/Program.cs index e1c77b34..52d660fe 100644 --- a/src/Policy/StellaOps.Policy.Engine/Program.cs +++ b/src/Policy/StellaOps.Policy.Engine/Program.cs @@ -10,7 +10,8 @@ using StellaOps.Policy.Engine.Options; using StellaOps.Policy.Engine.Compilation; using StellaOps.Policy.Engine.Endpoints; using StellaOps.Policy.Engine.Services; -using StellaOps.Policy.Engine.Workers; +using StellaOps.Policy.Engine.Workers; +using StellaOps.AirGap.Policy; var builder = WebApplication.CreateBuilder(args); @@ -60,9 +61,11 @@ var bootstrap = StellaOpsConfigurationBootstrapper.Build(op options.PostBind = static (value, _) => value.Validate(); }); -builder.Configuration.AddConfiguration(bootstrap.Configuration); - -builder.Services.AddOptions() +builder.Configuration.AddConfiguration(bootstrap.Configuration); + +builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap"); + +builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(PolicyEngineOptions.SectionName)) .Validate(options => { diff --git a/src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj b/src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj index b6c4e930..a84864a9 100644 --- a/src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj +++ b/src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj @@ -16,5 +16,6 @@ + - \ No newline at end of file + diff --git a/src/Policy/StellaOps.Policy.Gateway/Program.cs b/src/Policy/StellaOps.Policy.Gateway/Program.cs index bcb416be..1d759740 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Program.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Program.cs @@ -15,11 +15,12 @@ using StellaOps.Auth.ServerIntegration; using StellaOps.Configuration; using StellaOps.Policy.Gateway.Clients; using StellaOps.Policy.Gateway.Contracts; -using StellaOps.Policy.Gateway.Infrastructure; -using StellaOps.Policy.Gateway.Options; -using StellaOps.Policy.Gateway.Services; -using Polly; -using Polly.Extensions.Http; +using StellaOps.Policy.Gateway.Infrastructure; +using StellaOps.Policy.Gateway.Options; +using StellaOps.Policy.Gateway.Services; +using Polly; +using Polly.Extensions.Http; +using StellaOps.AirGap.Policy; var builder = WebApplication.CreateBuilder(args); @@ -69,9 +70,11 @@ var bootstrap = StellaOpsConfigurationBootstrapper.Build(o options.PostBind = static (value, _) => value.Validate(); }); -builder.Configuration.AddConfiguration(bootstrap.Configuration); - -builder.Logging.SetMinimumLevel(bootstrap.Options.Telemetry.MinimumLogLevel); +builder.Configuration.AddConfiguration(bootstrap.Configuration); + +builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap"); + +builder.Logging.SetMinimumLevel(bootstrap.Options.Telemetry.MinimumLogLevel); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(PolicyGatewayOptions.SectionName)) @@ -147,12 +150,17 @@ if (bootstrap.Options.PolicyEngine.ClientCredentials.Enabled) .AddHttpMessageHandler(); } -builder.Services.AddHttpClient((serviceProvider, client) => -{ - var gatewayOptions = serviceProvider.GetRequiredService>().Value; - client.BaseAddress = gatewayOptions.PolicyEngine.BaseUri; - client.Timeout = TimeSpan.FromSeconds(gatewayOptions.PolicyEngine.ClientCredentials.BackchannelTimeoutSeconds); -}) +builder.Services.AddHttpClient((serviceProvider, client) => +{ + var gatewayOptions = serviceProvider.GetRequiredService>().Value; + var egressPolicy = serviceProvider.GetService(); + if (egressPolicy is not null) + { + egressPolicy.EnsureAllowed(new EgressRequest("PolicyGateway", gatewayOptions.PolicyEngine.BaseUri, "policy-engine-client")); + } + client.BaseAddress = gatewayOptions.PolicyEngine.BaseUri; + client.Timeout = TimeSpan.FromSeconds(gatewayOptions.PolicyEngine.ClientCredentials.BackchannelTimeoutSeconds); +}) .AddPolicyHandler(static (provider, _) => CreatePolicyEngineRetryPolicy(provider)); var app = builder.Build(); diff --git a/src/Policy/StellaOps.Policy.Gateway/StellaOps.Policy.Gateway.csproj b/src/Policy/StellaOps.Policy.Gateway/StellaOps.Policy.Gateway.csproj index 1bc80017..b627a173 100644 --- a/src/Policy/StellaOps.Policy.Gateway/StellaOps.Policy.Gateway.csproj +++ b/src/Policy/StellaOps.Policy.Gateway/StellaOps.Policy.Gateway.csproj @@ -15,9 +15,10 @@ + - \ No newline at end of file + diff --git a/src/Policy/__Libraries/StellaOps.Policy/AGENTS.md b/src/Policy/__Libraries/StellaOps.Policy/AGENTS.md index 9e4c6ceb..45abe757 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/AGENTS.md +++ b/src/Policy/__Libraries/StellaOps.Policy/AGENTS.md @@ -7,7 +7,7 @@ Deliver the policy engine outlined in `docs/modules/scanner/ARCHITECTURE.md` and - Offer preview APIs to compare policy impacts on existing reports. ## Expectations -- Coordinate with Scanner.WebService, Feedser, Vexer, UI, Notify. +- Coordinate with Scanner.WebService, Conselier, Excitor, UI, Notify. - Maintain deterministic serialization and unit tests for precedence rules. - Update `TASKS.md` and broadcast contract changes. diff --git a/src/RiskEngine/StellaOps.RiskEngine/AGENTS.md b/src/RiskEngine/StellaOps.RiskEngine/AGENTS.md index a15d08b6..0c869719 100644 --- a/src/RiskEngine/StellaOps.RiskEngine/AGENTS.md +++ b/src/RiskEngine/StellaOps.RiskEngine/AGENTS.md @@ -5,7 +5,7 @@ Design, build, and operate the scoring runtime that computes Risk Scoring Profil ## Scope - Scoring workers, job scheduler, provider registry, caching, and explainability artifacts. -- Integration with Findings Ledger, Conseiller, Excitator, and Policy Engine. +- Integration with Findings Ledger, Conseiller, Excitor, and Policy Engine. - Performance, determinism, and observability of scoring jobs. - Air-gapped support through offline factor bundles. diff --git a/src/RiskEngine/StellaOps.RiskEngine/TASKS.md b/src/RiskEngine/StellaOps.RiskEngine/TASKS.md index 146b667b..110a44f2 100644 --- a/src/RiskEngine/StellaOps.RiskEngine/TASKS.md +++ b/src/RiskEngine/StellaOps.RiskEngine/TASKS.md @@ -1,32 +1,32 @@ -# Risk Engine Task Board — Epic 18: Risk Scoring Profiles - -## Sprint 66 – Foundations -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| RISK-ENGINE-66-001 | TODO | Risk Engine Guild | POLICY-RISK-66-001 | Scaffold scoring service (job queue, worker loop, provider registry) with deterministic execution harness. | Service builds/tests; job queue runs sample job; determinism tests pass. | -| RISK-ENGINE-66-002 | TODO | Risk Engine Guild | RISK-ENGINE-66-001 | Implement default transforms (linear, minmax, logistic, piecewise), clamping, gating, and contribution calculator. | Transform/gating unit tests passing; contribution breakdown matches golden fixtures. | - -## Sprint 67 – Provider Integration -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| RISK-ENGINE-67-001 | TODO | Risk Engine Guild, Concelier Guild | RISK-ENGINE-66-002, CONCELIER-RISK-66-001 | Integrate CVSS and KEV providers pulling data from Conseiller; implement reducers (`max`, `any`, `consensus`). | Providers return sample data; reducer tests pass; provenance recorded. | -| RISK-ENGINE-67-002 | TODO | Risk Engine Guild, Excitator Guild | RISK-ENGINE-66-002, EXCITITOR-RISK-66-001 | Integrate VEX gate provider and ensure gating short-circuits scoring as configured. | VEX gate tests pass; explanation indicates gate decision. | -| RISK-ENGINE-67-003 | TODO | Risk Engine Guild, Policy Engine Guild | RISK-ENGINE-66-002 | Add fix availability, asset criticality, and internet exposure providers with caching + TTL enforcement. | Providers deliver normalized values; cache hit metrics exposed. | - -## Sprint 68 – Ledger & API Wiring -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| RISK-ENGINE-68-001 | TODO | Risk Engine Guild, Findings Ledger Guild | RISK-ENGINE-66-002, LEDGER-RISK-66-001 | Persist scoring results + explanation pointers to Findings Ledger; handle incremental updates via input hash. | Results stored with hash; updates skip unchanged findings; tests cover dedupe. | -| RISK-ENGINE-68-002 | TODO | Risk Engine Guild, API Guild | RISK-ENGINE-68-001, POLICY-RISK-67-002 | Expose APIs (`/risk/jobs`, `/risk/results`, `/risk/results/{id}/explanation`); include pagination, filtering, error codes. | OpenAPI documented; contract tests pass; endpoints gated by scopes. | - -## Sprint 69 – Simulation & Performance -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| RISK-ENGINE-69-001 | TODO | Risk Engine Guild, Policy Studio Guild | RISK-ENGINE-68-002 | Implement simulation mode producing distributions and top movers without mutating ledger. | Simulation API returns metrics; golden tests cover scenarios. | -| RISK-ENGINE-69-002 | TODO | Risk Engine Guild, Observability Guild | RISK-ENGINE-66-001 | Add telemetry (spans, metrics, logs) for provider latency, job throughput, cache hits; define SLO dashboards. | Metrics visible in Grafana; alerts configured for P95 latency + error rate. | - -## Sprint 70 – Air-Gap & Advanced Providers -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| RISK-ENGINE-70-001 | TODO | Risk Engine Guild, Export Guild | RISK-ENGINE-67-003, RISK-BUNDLE-69-001 | Support offline provider bundles with manifest verification and missing-data reporting. | Engine loads bundle data; missing providers logged with `AIRGAP_MISSING_DATA`. | -| RISK-ENGINE-70-002 | TODO | Risk Engine Guild, Observability Guild | RISK-ENGINE-68-002 | Integrate runtime evidence provider and reachability provider outputs with caching + TTL. | Providers return runtime/reachability signals; explanation includes sources; tests pass. | +# Risk Engine Task Board — Epic 18: Risk Scoring Profiles + +## Sprint 66 – Foundations +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| RISK-ENGINE-66-001 | TODO | Risk Engine Guild | POLICY-RISK-66-001 | Scaffold scoring service (job queue, worker loop, provider registry) with deterministic execution harness. | Service builds/tests; job queue runs sample job; determinism tests pass. | +| RISK-ENGINE-66-002 | TODO | Risk Engine Guild | RISK-ENGINE-66-001 | Implement default transforms (linear, minmax, logistic, piecewise), clamping, gating, and contribution calculator. | Transform/gating unit tests passing; contribution breakdown matches golden fixtures. | + +## Sprint 67 – Provider Integration +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| RISK-ENGINE-67-001 | TODO | Risk Engine Guild, Concelier Guild | RISK-ENGINE-66-002, CONCELIER-RISK-66-001 | Integrate CVSS and KEV providers pulling data from Conseiller; implement reducers (`max`, `any`, `consensus`). | Providers return sample data; reducer tests pass; provenance recorded. | +| RISK-ENGINE-67-002 | TODO | Risk Engine Guild, Excitor Guild | RISK-ENGINE-66-002, EXCITITOR-RISK-66-001 | Integrate VEX gate provider and ensure gating short-circuits scoring as configured. | VEX gate tests pass; explanation indicates gate decision. | +| RISK-ENGINE-67-003 | TODO | Risk Engine Guild, Policy Engine Guild | RISK-ENGINE-66-002 | Add fix availability, asset criticality, and internet exposure providers with caching + TTL enforcement. | Providers deliver normalized values; cache hit metrics exposed. | + +## Sprint 68 – Ledger & API Wiring +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| RISK-ENGINE-68-001 | TODO | Risk Engine Guild, Findings Ledger Guild | RISK-ENGINE-66-002, LEDGER-RISK-66-001 | Persist scoring results + explanation pointers to Findings Ledger; handle incremental updates via input hash. | Results stored with hash; updates skip unchanged findings; tests cover dedupe. | +| RISK-ENGINE-68-002 | TODO | Risk Engine Guild, API Guild | RISK-ENGINE-68-001, POLICY-RISK-67-002 | Expose APIs (`/risk/jobs`, `/risk/results`, `/risk/results/{id}/explanation`); include pagination, filtering, error codes. | OpenAPI documented; contract tests pass; endpoints gated by scopes. | + +## Sprint 69 – Simulation & Performance +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| RISK-ENGINE-69-001 | TODO | Risk Engine Guild, Policy Studio Guild | RISK-ENGINE-68-002 | Implement simulation mode producing distributions and top movers without mutating ledger. | Simulation API returns metrics; golden tests cover scenarios. | +| RISK-ENGINE-69-002 | TODO | Risk Engine Guild, Observability Guild | RISK-ENGINE-66-001 | Add telemetry (spans, metrics, logs) for provider latency, job throughput, cache hits; define SLO dashboards. | Metrics visible in Grafana; alerts configured for P95 latency + error rate. | + +## Sprint 70 – Air-Gap & Advanced Providers +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| RISK-ENGINE-70-001 | TODO | Risk Engine Guild, Export Guild | RISK-ENGINE-67-003, RISK-BUNDLE-69-001 | Support offline provider bundles with manifest verification and missing-data reporting. | Engine loads bundle data; missing providers logged with `AIRGAP_MISSING_DATA`. | +| RISK-ENGINE-70-002 | TODO | Risk Engine Guild, Observability Guild | RISK-ENGINE-68-002 | Integrate runtime evidence provider and reachability provider outputs with caching + TTL. | Providers return runtime/reachability signals; explanation includes sources; tests pass. | diff --git a/src/Scanner/StellaOps.Scanner.WebService/TASKS.completed.md b/src/Scanner/StellaOps.Scanner.WebService/TASKS.completed.md index 6c314a96..2223b72c 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/TASKS.completed.md +++ b/src/Scanner/StellaOps.Scanner.WebService/TASKS.completed.md @@ -7,12 +7,12 @@ | SCANNER-WEB-09-103 | DONE (2025-10-19) | Scanner WebService Guild | SCANNER-WEB-09-102, SCANNER-CORE-09-502 | Emit scan progress via SSE/JSONL with correlation IDs and deterministic timestamps; document API reference. | Streaming endpoint verified in tests, timestamps formatted ISO-8601 UTC, docs updated in `docs/09_API_CLI_REFERENCE.md`. | | SCANNER-WEB-09-104 | DONE (2025-10-19) | Scanner WebService Guild | SCANNER-STORAGE-09-301, SCANNER-QUEUE-09-401 | Bind configuration for Mongo, MinIO, queue, feature flags; add startup diagnostics and fail-fast policy for missing deps. | Misconfiguration fails fast with actionable errors, configuration bound tests pass, diagnostics logged with correlation IDs. | | SCANNER-POLICY-09-105 | DONE (2025-10-19) | Scanner WebService Guild | POLICY-CORE-09-001 | Integrate policy schema loader + diagnostics + OpenAPI (YAML ignore rules, VEX include/exclude, vendor precedence). | Policy endpoints documented; validation surfaces actionable errors; OpenAPI schema published. | -| SCANNER-POLICY-09-106 | DONE (2025-10-19) | Scanner WebService Guild | POLICY-CORE-09-002, SCANNER-POLICY-09-105 | `/reports` verdict assembly (Feedser/Vexer/Policy merge) + signed response envelope. | Aggregated report includes policy metadata; integration test verifies signed response; docs updated. | +| SCANNER-POLICY-09-106 | DONE (2025-10-19) | Scanner WebService Guild | POLICY-CORE-09-002, SCANNER-POLICY-09-105 | `/reports` verdict assembly (Conselier/Excitor/Policy merge) + signed response envelope. | Aggregated report includes policy metadata; integration test verifies signed response; docs updated. | | SCANNER-POLICY-09-107 | DONE (2025-10-19) | Scanner WebService Guild | POLICY-CORE-09-005, SCANNER-POLICY-09-106 | Surface score inputs, config version, and `quietedBy` provenance in `/reports` response and signed payload; document schema changes. | `/reports` JSON + DSSE contain score, reachability, sourceTrust, confidenceBand, quiet provenance; contract tests updated; docs refreshed. | | SCANNER-WEB-10-201 | DONE (2025-10-19) | Scanner WebService Guild | SCANNER-CACHE-10-101 | Register scanner cache services and maintenance loop within WebService host. | `AddScannerCache` wired for configuration binding; maintenance service skips when disabled; project references updated. | | SCANNER-RUNTIME-12-301 | DONE (2025-10-20) | Scanner WebService Guild | ZASTAVA-CORE-12-201 | Implement `/runtime/events` ingestion endpoint with validation, batching, and storage hooks per Zastava contract. | Observer fixtures POST events, data persisted and acked; invalid payloads rejected with deterministic errors. | | SCANNER-RUNTIME-12-302 | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-301, ZASTAVA-CORE-12-201 | Implement `/policy/runtime` endpoint joining SBOM baseline + policy verdict, returning admission guidance. Coordinate with CLI (`CLI-RUNTIME-13-008`) before GA to lock response field names/metadata. | Webhook integration test passes; responses include verdict, TTL, reasons; metrics/logging added; CLI contract review signed off. | -| SCANNER-RUNTIME-12-303 | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | Replace `/policy/runtime` heuristic with canonical policy evaluation (Feedser/Vexer inputs, PolicyPreviewService) so results align with `/reports`. | Runtime policy endpoint now pipes findings through `PolicyPreviewService`, emits canonical verdicts/confidence/quiet metadata, and updated tests cover pass/warn/fail paths + CLI contract fixtures. | +| SCANNER-RUNTIME-12-303 | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | Replace `/policy/runtime` heuristic with canonical policy evaluation (Conselier/Excitor inputs, PolicyPreviewService) so results align with `/reports`. | Runtime policy endpoint now pipes findings through `PolicyPreviewService`, emits canonical verdicts/confidence/quiet metadata, and updated tests cover pass/warn/fail paths + CLI contract fixtures. | | SCANNER-RUNTIME-12-304 | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | Surface attestation verification status by integrating Authority/Attestor Rekor validation (beyond presence-only). | `/policy/runtime` maps Rekor UUIDs through the runtime attestation verifier so `rekor.verified` reflects attestor outcomes; webhook/CLI coverage added. | | SCANNER-RUNTIME-12-305 | DONE (2025-10-24) | Scanner WebService Guild | SCANNER-RUNTIME-12-301, SCANNER-RUNTIME-12-302 | Promote shared fixtures with Zastava/CLI and add end-to-end automation for `/runtime/events` + `/policy/runtime`. | Runtime policy integration test + CLI-aligned fixture assert confidence, metadata JSON, and Rekor verification; docs note shared contract. | | SCANNER-EVENTS-15-201 | DONE (2025-10-20) | Scanner WebService Guild | NOTIFY-QUEUE-15-401 | Emit `scanner.report.ready` and `scanner.scan.completed` events (bus adapters + tests). | Event envelopes published to queue with schemas; fixtures committed; Notify consumption test passes. | diff --git a/src/Scanner/StellaOps.Scanner.WebService/TASKS.md b/src/Scanner/StellaOps.Scanner.WebService/TASKS.md index fe42236a..aa552898 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/TASKS.md +++ b/src/Scanner/StellaOps.Scanner.WebService/TASKS.md @@ -2,9 +2,9 @@ | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| -| SCANNER-SURFACE-02 | TODO | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. | -| SCANNER-ENV-02 | TODO | Scanner WebService Guild, Ops Guild | SURFACE-ENV-02 | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration. | Service uses helper; env table documented; helm/compose templates updated. | -| SCANNER-SECRETS-02 | TODO | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens). | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. | +| SCANNER-SURFACE-02 | DOING (2025-11-02) | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata.
2025-11-02: Scan/report API responses now include preview CAS URIs; attestation metadata draft published. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. | +| SCANNER-ENV-02 | DOING (2025-11-02) | Scanner WebService Guild, Ops Guild | SURFACE-ENV-02 | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration.
2025-11-02: Cache root resolution switched to helper; feature flag bindings updated; Helm/Compose updates pending review. | Service uses helper; env table documented; helm/compose templates updated. | +| SCANNER-SECRETS-02 | DOING (2025-11-02) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).
2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. | | SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Scanner WebService Guild | ORCH-SVC-38-101, NOTIFY-SVC-38-001 | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Tests assert envelope schema + orchestrator publish; Notifier consumer harness passes; docs updated with new event contract. Blocked by .NET 10 preview OpenAPI/Auth dependency drift preventing `dotnet test` completion. | | SCANNER-EVENTS-16-302 | DOING (2025-10-26) | Scanner WebService Guild | SCANNER-EVENTS-16-301 | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console. | Links section covers UI/API targets; downstream consumers validated; docs/samples updated. | diff --git a/src/Scanner/StellaOps.Scanner.Worker/TASKS.md b/src/Scanner/StellaOps.Scanner.Worker/TASKS.md index efa2a8bd..ba5bfc68 100644 --- a/src/Scanner/StellaOps.Scanner.Worker/TASKS.md +++ b/src/Scanner/StellaOps.Scanner.Worker/TASKS.md @@ -2,6 +2,6 @@ | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| -| SCANNER-SURFACE-01 | TODO | Scanner Worker Guild | SURFACE-FS-02 | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments. | Integration tests prove cache entries exist; telemetry counters exported. | -| SCANNER-ENV-01 | TODO | Scanner Worker Guild | SURFACE-ENV-02 | Replace ad-hoc environment reads with `StellaOps.Scanner.Surface.Env` helpers for cache roots and CAS endpoints. | Worker boots with helper; misconfiguration warnings documented; smoke tests updated. | -| SCANNER-SECRETS-01 | TODO | Scanner Worker Guild, Security Guild | SURFACE-SECRETS-02 | Adopt `StellaOps.Scanner.Surface.Secrets` for registry/CAS credentials during scan execution. | Secrets fetched via shared provider; legacy secret code removed; integration tests cover rotation. | +| SCANNER-SURFACE-01 | DOING (2025-11-02) | Scanner Worker Guild | SURFACE-FS-02 | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments.
2025-11-02: Draft Surface.FS manifests emitted for sample scans; telemetry counters under review. | Integration tests prove cache entries exist; telemetry counters exported. | +| SCANNER-ENV-01 | DOING (2025-11-02) | Scanner Worker Guild | SURFACE-ENV-02 | Replace ad-hoc environment reads with `StellaOps.Scanner.Surface.Env` helpers for cache roots and CAS endpoints.
2025-11-02: Worker bootstrap now resolves cache roots via helper; warning path documented; smoke tests running. | Worker boots with helper; misconfiguration warnings documented; smoke tests updated. | +| SCANNER-SECRETS-01 | DOING (2025-11-02) | Scanner Worker Guild, Security Guild | SURFACE-SECRETS-02 | Adopt `StellaOps.Scanner.Surface.Secrets` for registry/CAS credentials during scan execution.
2025-11-02: Surface.Secrets provider wired for CAS token retrieval; integration tests added. | Secrets fetched via shared provider; legacy secret code removed; integration tests cover rotation. | diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs index dfac9585..b2e0ab58 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs @@ -1,173 +1,173 @@ -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using StellaOps.Scheduler.WebService.Options; - -namespace StellaOps.Scheduler.WebService.EventWebhooks; - -public static class EventWebhookEndpointExtensions -{ - public static void MapSchedulerEventWebhookEndpoints(this IEndpointRouteBuilder builder) - { - var group = builder.MapGroup("/events"); - - group.MapPost("/feedser-export", HandleFeedserExportAsync); - group.MapPost("/vexer-export", HandleVexerExportAsync); - } - - private static async Task HandleFeedserExportAsync( - HttpContext httpContext, - [FromServices] IOptionsMonitor options, - [FromServices] IWebhookRequestAuthenticator authenticator, - [FromServices] IWebhookRateLimiter rateLimiter, - [FromServices] IInboundExportEventSink sink, - CancellationToken cancellationToken) - { - var webhookOptions = options.CurrentValue.Webhooks.Feedser; - if (!webhookOptions.Enabled) - { - return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); - } - - var readResult = await ReadPayloadAsync(httpContext, cancellationToken).ConfigureAwait(false); - if (!readResult.Succeeded) - { - return readResult.ErrorResult!; - } - - if (!rateLimiter.TryAcquire("feedser", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter)) - { - var response = Results.StatusCode(StatusCodes.Status429TooManyRequests); - if (retryAfter > TimeSpan.Zero) - { - httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(); - } - - return response; - } - - var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false); - if (!authResult.Succeeded) - { - return authResult.ToResult(); - } - - try - { - await sink.HandleFeedserAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false); - return Results.Accepted(value: new { status = "accepted" }); - } - catch (ValidationException ex) - { - return Results.BadRequest(new { error = ex.Message }); - } - } - - private static async Task HandleVexerExportAsync( - HttpContext httpContext, - [FromServices] IOptionsMonitor options, - [FromServices] IWebhookRequestAuthenticator authenticator, - [FromServices] IWebhookRateLimiter rateLimiter, - [FromServices] IInboundExportEventSink sink, - CancellationToken cancellationToken) - { - var webhookOptions = options.CurrentValue.Webhooks.Vexer; - if (!webhookOptions.Enabled) - { - return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); - } - - var readResult = await ReadPayloadAsync(httpContext, cancellationToken).ConfigureAwait(false); - if (!readResult.Succeeded) - { - return readResult.ErrorResult!; - } - - if (!rateLimiter.TryAcquire("vexer", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter)) - { - var response = Results.StatusCode(StatusCodes.Status429TooManyRequests); - if (retryAfter > TimeSpan.Zero) - { - httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(); - } - - return response; - } - - var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false); - if (!authResult.Succeeded) - { - return authResult.ToResult(); - } - - try - { - await sink.HandleVexerAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false); - return Results.Accepted(value: new { status = "accepted" }); - } - catch (ValidationException ex) - { - return Results.BadRequest(new { error = ex.Message }); - } - } - - private static async Task> ReadPayloadAsync(HttpContext context, CancellationToken cancellationToken) - { - context.Request.EnableBuffering(); - - await using var buffer = new MemoryStream(); - await context.Request.Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false); - var bodyBytes = buffer.ToArray(); - context.Request.Body.Position = 0; - - try - { - var payload = JsonSerializer.Deserialize(bodyBytes, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - if (payload is null) - { - return RequestPayload.Failed(Results.BadRequest(new { error = "Request payload cannot be empty." })); - } - - return RequestPayload.Success(payload, bodyBytes); - } - catch (JsonException ex) - { - return RequestPayload.Failed(Results.BadRequest(new { error = ex.Message })); - } - catch (ValidationException ex) - { - return RequestPayload.Failed(Results.BadRequest(new { error = ex.Message })); - } - } - - private readonly struct RequestPayload - { - private RequestPayload(T? payload, byte[] rawBody, IResult? error, bool succeeded) - { - Payload = payload; - RawBody = rawBody; - ErrorResult = error; - Succeeded = succeeded; - } - - public T? Payload { get; } - - public byte[] RawBody { get; } - - public IResult? ErrorResult { get; } - - public bool Succeeded { get; } - - public static RequestPayload Success(T payload, byte[] rawBody) - => new(payload, rawBody, null, true); - - public static RequestPayload Failed(IResult error) - => new(default, Array.Empty(), error, false); - } -} +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Scheduler.WebService.Options; + +namespace StellaOps.Scheduler.WebService.EventWebhooks; + +public static class EventWebhookEndpointExtensions +{ + public static void MapSchedulerEventWebhookEndpoints(this IEndpointRouteBuilder builder) + { + var group = builder.MapGroup("/events"); + + group.MapPost("/conselier-export", HandleConselierExportAsync); + group.MapPost("/excitor-export", HandleExcitorExportAsync); + } + + private static async Task HandleConselierExportAsync( + HttpContext httpContext, + [FromServices] IOptionsMonitor options, + [FromServices] IWebhookRequestAuthenticator authenticator, + [FromServices] IWebhookRateLimiter rateLimiter, + [FromServices] IInboundExportEventSink sink, + CancellationToken cancellationToken) + { + var webhookOptions = options.CurrentValue.Webhooks.Conselier; + if (!webhookOptions.Enabled) + { + return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); + } + + var readResult = await ReadPayloadAsync(httpContext, cancellationToken).ConfigureAwait(false); + if (!readResult.Succeeded) + { + return readResult.ErrorResult!; + } + + if (!rateLimiter.TryAcquire("conselier", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter)) + { + var response = Results.StatusCode(StatusCodes.Status429TooManyRequests); + if (retryAfter > TimeSpan.Zero) + { + httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(); + } + + return response; + } + + var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false); + if (!authResult.Succeeded) + { + return authResult.ToResult(); + } + + try + { + await sink.HandleConselierAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false); + return Results.Accepted(value: new { status = "accepted" }); + } + catch (ValidationException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + } + + private static async Task HandleExcitorExportAsync( + HttpContext httpContext, + [FromServices] IOptionsMonitor options, + [FromServices] IWebhookRequestAuthenticator authenticator, + [FromServices] IWebhookRateLimiter rateLimiter, + [FromServices] IInboundExportEventSink sink, + CancellationToken cancellationToken) + { + var webhookOptions = options.CurrentValue.Webhooks.Excitor; + if (!webhookOptions.Enabled) + { + return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); + } + + var readResult = await ReadPayloadAsync(httpContext, cancellationToken).ConfigureAwait(false); + if (!readResult.Succeeded) + { + return readResult.ErrorResult!; + } + + if (!rateLimiter.TryAcquire("excitor", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter)) + { + var response = Results.StatusCode(StatusCodes.Status429TooManyRequests); + if (retryAfter > TimeSpan.Zero) + { + httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(); + } + + return response; + } + + var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false); + if (!authResult.Succeeded) + { + return authResult.ToResult(); + } + + try + { + await sink.HandleExcitorAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false); + return Results.Accepted(value: new { status = "accepted" }); + } + catch (ValidationException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + } + + private static async Task> ReadPayloadAsync(HttpContext context, CancellationToken cancellationToken) + { + context.Request.EnableBuffering(); + + await using var buffer = new MemoryStream(); + await context.Request.Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false); + var bodyBytes = buffer.ToArray(); + context.Request.Body.Position = 0; + + try + { + var payload = JsonSerializer.Deserialize(bodyBytes, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + if (payload is null) + { + return RequestPayload.Failed(Results.BadRequest(new { error = "Request payload cannot be empty." })); + } + + return RequestPayload.Success(payload, bodyBytes); + } + catch (JsonException ex) + { + return RequestPayload.Failed(Results.BadRequest(new { error = ex.Message })); + } + catch (ValidationException ex) + { + return RequestPayload.Failed(Results.BadRequest(new { error = ex.Message })); + } + } + + private readonly struct RequestPayload + { + private RequestPayload(T? payload, byte[] rawBody, IResult? error, bool succeeded) + { + Payload = payload; + RawBody = rawBody; + ErrorResult = error; + Succeeded = succeeded; + } + + public T? Payload { get; } + + public byte[] RawBody { get; } + + public IResult? ErrorResult { get; } + + public bool Succeeded { get; } + + public static RequestPayload Success(T payload, byte[] rawBody) + => new(payload, rawBody, null, true); + + public static RequestPayload Failed(IResult error) + => new(default, Array.Empty(), error, false); + } +} diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/IInboundExportEventSink.cs b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/IInboundExportEventSink.cs index d902c7d9..fdbff003 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/IInboundExportEventSink.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/IInboundExportEventSink.cs @@ -1,11 +1,11 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scheduler.WebService.EventWebhooks; - -public interface IInboundExportEventSink -{ - Task HandleFeedserAsync(FeedserExportEventRequest request, CancellationToken cancellationToken); - - Task HandleVexerAsync(VexerExportEventRequest request, CancellationToken cancellationToken); -} +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scheduler.WebService.EventWebhooks; + +public interface IInboundExportEventSink +{ + Task HandleConselierAsync(ConselierExportEventRequest request, CancellationToken cancellationToken); + + Task HandleExcitorAsync(ExcitorExportEventRequest request, CancellationToken cancellationToken); +} diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/LoggingExportEventSink.cs b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/LoggingExportEventSink.cs index 22352504..bbcc7843 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/LoggingExportEventSink.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/LoggingExportEventSink.cs @@ -1,33 +1,33 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace StellaOps.Scheduler.WebService.EventWebhooks; - -internal sealed class LoggingExportEventSink : IInboundExportEventSink -{ - private readonly ILogger _logger; - - public LoggingExportEventSink(ILogger logger) - { - _logger = logger; - } - - public Task HandleFeedserAsync(FeedserExportEventRequest request, CancellationToken cancellationToken) - { - _logger.LogInformation( - "Received Feedser export webhook {ExportId} with {ChangedProducts} product keys.", - request.ExportId, - request.ChangedProductKeys.Count); - return Task.CompletedTask; - } - - public Task HandleVexerAsync(VexerExportEventRequest request, CancellationToken cancellationToken) - { - _logger.LogInformation( - "Received Vexer export webhook {ExportId} with {ChangedClaims} claim changes.", - request.ExportId, - request.ChangedClaims.Count); - return Task.CompletedTask; - } -} +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace StellaOps.Scheduler.WebService.EventWebhooks; + +internal sealed class LoggingExportEventSink : IInboundExportEventSink +{ + private readonly ILogger _logger; + + public LoggingExportEventSink(ILogger logger) + { + _logger = logger; + } + + public Task HandleConselierAsync(ConselierExportEventRequest request, CancellationToken cancellationToken) + { + _logger.LogInformation( + "Received Conselier export webhook {ExportId} with {ChangedProducts} product keys.", + request.ExportId, + request.ChangedProductKeys.Count); + return Task.CompletedTask; + } + + public Task HandleExcitorAsync(ExcitorExportEventRequest request, CancellationToken cancellationToken) + { + _logger.LogInformation( + "Received Excitor export webhook {ExportId} with {ChangedClaims} claim changes.", + request.ExportId, + request.ChangedClaims.Count); + return Task.CompletedTask; + } +} diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/WebhookPayloads.cs b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/WebhookPayloads.cs index cd51b7b5..62b2745b 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/WebhookPayloads.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/WebhookPayloads.cs @@ -1,106 +1,106 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using System.Linq; - -namespace StellaOps.Scheduler.WebService.EventWebhooks; - -public sealed record FeedserExportEventRequest( - string ExportId, - IReadOnlyList ChangedProductKeys, - IReadOnlyList? Kev, - WebhookEventWindow? Window) -{ - public string ExportId { get; } = ExportId?.Trim() ?? throw new ArgumentNullException(nameof(ExportId)); - - public IReadOnlyList ChangedProductKeys { get; } = NormalizeList(ChangedProductKeys, nameof(ChangedProductKeys)); - - public IReadOnlyList Kev { get; } = NormalizeList(Kev, nameof(Kev), allowEmpty: true); - - public WebhookEventWindow? Window { get; } = Window; - - private static IReadOnlyList NormalizeList(IReadOnlyList? source, string propertyName, bool allowEmpty = false) - { - if (source is null) - { - if (allowEmpty) - { - return ImmutableArray.Empty; - } - - throw new ValidationException($"{propertyName} must be specified."); - } - - var cleaned = source - .Where(item => !string.IsNullOrWhiteSpace(item)) - .Select(item => item.Trim()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (!allowEmpty && cleaned.Length == 0) - { - throw new ValidationException($"{propertyName} must contain at least one value."); - } - - return cleaned; - } -} - -public sealed record VexerExportEventRequest( - string ExportId, - IReadOnlyList ChangedClaims, - WebhookEventWindow? Window) -{ - public string ExportId { get; } = ExportId?.Trim() ?? throw new ArgumentNullException(nameof(ExportId)); - - public IReadOnlyList ChangedClaims { get; } = NormalizeClaims(ChangedClaims); - - public WebhookEventWindow? Window { get; } = Window; - - private static IReadOnlyList NormalizeClaims(IReadOnlyList? claims) - { - if (claims is null || claims.Count == 0) - { - throw new ValidationException("changedClaims must contain at least one entry."); - } - - foreach (var claim in claims) - { - claim.Validate(); - } - - return claims; - } -} - -public sealed record VexerClaimChange( - string ProductKey, - string VulnerabilityId, - string Status) -{ - public string ProductKey { get; } = Normalize(ProductKey, nameof(ProductKey)); - - public string VulnerabilityId { get; } = Normalize(VulnerabilityId, nameof(VulnerabilityId)); - - public string Status { get; } = Normalize(Status, nameof(Status)); - - internal void Validate() - { - _ = ProductKey; - _ = VulnerabilityId; - _ = Status; - } - - private static string Normalize(string value, string propertyName) - { - if (string.IsNullOrWhiteSpace(value)) - { - throw new ValidationException($"{propertyName} must be provided."); - } - - return value.Trim(); - } -} - -public sealed record WebhookEventWindow(DateTimeOffset? From, DateTimeOffset? To); +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace StellaOps.Scheduler.WebService.EventWebhooks; + +public sealed record ConselierExportEventRequest( + string ExportId, + IReadOnlyList ChangedProductKeys, + IReadOnlyList? Kev, + WebhookEventWindow? Window) +{ + public string ExportId { get; } = ExportId?.Trim() ?? throw new ArgumentNullException(nameof(ExportId)); + + public IReadOnlyList ChangedProductKeys { get; } = NormalizeList(ChangedProductKeys, nameof(ChangedProductKeys)); + + public IReadOnlyList Kev { get; } = NormalizeList(Kev, nameof(Kev), allowEmpty: true); + + public WebhookEventWindow? Window { get; } = Window; + + private static IReadOnlyList NormalizeList(IReadOnlyList? source, string propertyName, bool allowEmpty = false) + { + if (source is null) + { + if (allowEmpty) + { + return ImmutableArray.Empty; + } + + throw new ValidationException($"{propertyName} must be specified."); + } + + var cleaned = source + .Where(item => !string.IsNullOrWhiteSpace(item)) + .Select(item => item.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (!allowEmpty && cleaned.Length == 0) + { + throw new ValidationException($"{propertyName} must contain at least one value."); + } + + return cleaned; + } +} + +public sealed record ExcitorExportEventRequest( + string ExportId, + IReadOnlyList ChangedClaims, + WebhookEventWindow? Window) +{ + public string ExportId { get; } = ExportId?.Trim() ?? throw new ArgumentNullException(nameof(ExportId)); + + public IReadOnlyList ChangedClaims { get; } = NormalizeClaims(ChangedClaims); + + public WebhookEventWindow? Window { get; } = Window; + + private static IReadOnlyList NormalizeClaims(IReadOnlyList? claims) + { + if (claims is null || claims.Count == 0) + { + throw new ValidationException("changedClaims must contain at least one entry."); + } + + foreach (var claim in claims) + { + claim.Validate(); + } + + return claims; + } +} + +public sealed record ExcitorClaimChange( + string ProductKey, + string VulnerabilityId, + string Status) +{ + public string ProductKey { get; } = Normalize(ProductKey, nameof(ProductKey)); + + public string VulnerabilityId { get; } = Normalize(VulnerabilityId, nameof(VulnerabilityId)); + + public string Status { get; } = Normalize(Status, nameof(Status)); + + internal void Validate() + { + _ = ProductKey; + _ = VulnerabilityId; + _ = Status; + } + + private static string Normalize(string value, string propertyName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ValidationException($"{propertyName} must be provided."); + } + + return value.Trim(); + } +} + +public sealed record WebhookEventWindow(DateTimeOffset? From, DateTimeOffset? To); diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Options/SchedulerEventsOptions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Options/SchedulerEventsOptions.cs index d04b8984..198fd88d 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Options/SchedulerEventsOptions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Options/SchedulerEventsOptions.cs @@ -1,18 +1,18 @@ -namespace StellaOps.Scheduler.WebService.Options; - -/// -/// Scheduler WebService event options (outbound + inbound). -/// +namespace StellaOps.Scheduler.WebService.Options; + +/// +/// Scheduler WebService event options (outbound + inbound). +/// using System; using System.Collections.Generic; - -public sealed class SchedulerEventsOptions -{ - public GraphJobEventsOptions GraphJobs { get; set; } = new(); - - public SchedulerInboundWebhooksOptions Webhooks { get; set; } = new(); -} - + +public sealed class SchedulerEventsOptions +{ + public GraphJobEventsOptions GraphJobs { get; set; } = new(); + + public SchedulerInboundWebhooksOptions Webhooks { get; set; } = new(); +} + public sealed class GraphJobEventsOptions { /// @@ -50,91 +50,91 @@ public sealed class GraphJobEventsOptions /// public IDictionary DriverSettings { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); } - -public sealed class SchedulerInboundWebhooksOptions -{ - public SchedulerWebhookOptions Feedser { get; set; } = SchedulerWebhookOptions.CreateDefault("feedser"); - - public SchedulerWebhookOptions Vexer { get; set; } = SchedulerWebhookOptions.CreateDefault("vexer"); -} - -public sealed class SchedulerWebhookOptions -{ - private const string DefaultSignatureHeader = "X-Scheduler-Signature"; - - public SchedulerWebhookOptions() - { - SignatureHeader = DefaultSignatureHeader; - } - - public bool Enabled { get; set; } = true; - - /// - /// Require a client certificate to be presented (mTLS). Optional when HMAC is configured. - /// - public bool RequireClientCertificate { get; set; } - - /// - /// Shared secret (Base64 or raw text) for HMAC-SHA256 signatures. Required if is false. - /// - public string? HmacSecret { get; set; } - - /// - /// Header name carrying the webhook signature (defaults to X-Scheduler-Signature). - /// - public string SignatureHeader { get; set; } - - /// - /// Maximum number of accepted requests per sliding window. - /// - public int RateLimitRequests { get; set; } = 60; - - /// - /// Sliding window duration in seconds for the rate limiter. - /// - public int RateLimitWindowSeconds { get; set; } = 60; - - /// - /// Optional label used for logging/diagnostics; populated via . - /// - public string Name { get; set; } = string.Empty; - - public static SchedulerWebhookOptions CreateDefault(string name) - => new() - { - Name = name, - SignatureHeader = DefaultSignatureHeader, - RateLimitRequests = 120, - RateLimitWindowSeconds = 60 - }; - - public void Validate() - { - if (!Enabled) - { - return; - } - - if (string.IsNullOrWhiteSpace(SignatureHeader)) - { - throw new InvalidOperationException($"Scheduler webhook '{Name}' must specify a signature header when enabled."); - } - - if (!RequireClientCertificate && string.IsNullOrWhiteSpace(HmacSecret)) - { - throw new InvalidOperationException($"Scheduler webhook '{Name}' must configure either HMAC secret or mTLS enforcement."); - } - - if (RateLimitRequests <= 0) - { - throw new InvalidOperationException($"Scheduler webhook '{Name}' must configure a positive rate limit."); - } - - if (RateLimitWindowSeconds <= 0) - { - throw new InvalidOperationException($"Scheduler webhook '{Name}' must configure a rate limit window greater than zero seconds."); - } - } - - public TimeSpan GetRateLimitWindow() => TimeSpan.FromSeconds(RateLimitWindowSeconds <= 0 ? 60 : RateLimitWindowSeconds); -} + +public sealed class SchedulerInboundWebhooksOptions +{ + public SchedulerWebhookOptions Conselier { get; set; } = SchedulerWebhookOptions.CreateDefault("conselier"); + + public SchedulerWebhookOptions Excitor { get; set; } = SchedulerWebhookOptions.CreateDefault("excitor"); +} + +public sealed class SchedulerWebhookOptions +{ + private const string DefaultSignatureHeader = "X-Scheduler-Signature"; + + public SchedulerWebhookOptions() + { + SignatureHeader = DefaultSignatureHeader; + } + + public bool Enabled { get; set; } = true; + + /// + /// Require a client certificate to be presented (mTLS). Optional when HMAC is configured. + /// + public bool RequireClientCertificate { get; set; } + + /// + /// Shared secret (Base64 or raw text) for HMAC-SHA256 signatures. Required if is false. + /// + public string? HmacSecret { get; set; } + + /// + /// Header name carrying the webhook signature (defaults to X-Scheduler-Signature). + /// + public string SignatureHeader { get; set; } + + /// + /// Maximum number of accepted requests per sliding window. + /// + public int RateLimitRequests { get; set; } = 60; + + /// + /// Sliding window duration in seconds for the rate limiter. + /// + public int RateLimitWindowSeconds { get; set; } = 60; + + /// + /// Optional label used for logging/diagnostics; populated via . + /// + public string Name { get; set; } = string.Empty; + + public static SchedulerWebhookOptions CreateDefault(string name) + => new() + { + Name = name, + SignatureHeader = DefaultSignatureHeader, + RateLimitRequests = 120, + RateLimitWindowSeconds = 60 + }; + + public void Validate() + { + if (!Enabled) + { + return; + } + + if (string.IsNullOrWhiteSpace(SignatureHeader)) + { + throw new InvalidOperationException($"Scheduler webhook '{Name}' must specify a signature header when enabled."); + } + + if (!RequireClientCertificate && string.IsNullOrWhiteSpace(HmacSecret)) + { + throw new InvalidOperationException($"Scheduler webhook '{Name}' must configure either HMAC secret or mTLS enforcement."); + } + + if (RateLimitRequests <= 0) + { + throw new InvalidOperationException($"Scheduler webhook '{Name}' must configure a positive rate limit."); + } + + if (RateLimitWindowSeconds <= 0) + { + throw new InvalidOperationException($"Scheduler webhook '{Name}' must configure a rate limit window greater than zero seconds."); + } + } + + public TimeSpan GetRateLimitWindow() => TimeSpan.FromSeconds(RateLimitWindowSeconds <= 0 ? 60 : RateLimitWindowSeconds); +} diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs index 8103adba..1af60edc 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs @@ -1,203 +1,203 @@ -using System.Linq; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using StellaOps.Auth.Abstractions; -using StellaOps.Auth.ServerIntegration; -using StellaOps.Plugin.DependencyInjection; -using StellaOps.Plugin.Hosting; -using StellaOps.Scheduler.WebService.Hosting; -using StellaOps.Scheduler.ImpactIndex; -using StellaOps.Scheduler.Storage.Mongo; -using StellaOps.Scheduler.Storage.Mongo.Repositories; -using StellaOps.Scheduler.Storage.Mongo.Services; -using StellaOps.Scheduler.WebService; -using StellaOps.Scheduler.WebService.Auth; -using StellaOps.Scheduler.WebService.EventWebhooks; -using StellaOps.Scheduler.WebService.GraphJobs; -using StellaOps.Scheduler.WebService.GraphJobs.Events; -using StellaOps.Scheduler.WebService.Schedules; -using StellaOps.Scheduler.WebService.Options; -using StellaOps.Scheduler.WebService.Runs; -using StellaOps.Scheduler.WebService.PolicyRuns; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddRouting(options => options.LowercaseUrls = true); -builder.Services.AddSingleton(); -builder.Services.TryAddSingleton(TimeProvider.System); - -var authorityOptions = new SchedulerAuthorityOptions(); -builder.Configuration.GetSection("Scheduler:Authority").Bind(authorityOptions); - -if (!authorityOptions.RequiredScopes.Any(scope => string.Equals(scope, StellaOpsScopes.GraphRead, StringComparison.OrdinalIgnoreCase))) -{ - authorityOptions.RequiredScopes.Add(StellaOpsScopes.GraphRead); -} - -if (!authorityOptions.RequiredScopes.Any(scope => string.Equals(scope, StellaOpsScopes.GraphWrite, StringComparison.OrdinalIgnoreCase))) -{ - authorityOptions.RequiredScopes.Add(StellaOpsScopes.GraphWrite); -} - -if (authorityOptions.Audiences.Count == 0) -{ - authorityOptions.Audiences.Add("api://scheduler"); -} - -authorityOptions.Validate(); -builder.Services.AddSingleton(authorityOptions); - +using System.Linq; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration; +using StellaOps.Plugin.DependencyInjection; +using StellaOps.Plugin.Hosting; +using StellaOps.Scheduler.WebService.Hosting; +using StellaOps.Scheduler.ImpactIndex; +using StellaOps.Scheduler.Storage.Mongo; +using StellaOps.Scheduler.Storage.Mongo.Repositories; +using StellaOps.Scheduler.Storage.Mongo.Services; +using StellaOps.Scheduler.WebService; +using StellaOps.Scheduler.WebService.Auth; +using StellaOps.Scheduler.WebService.EventWebhooks; +using StellaOps.Scheduler.WebService.GraphJobs; +using StellaOps.Scheduler.WebService.GraphJobs.Events; +using StellaOps.Scheduler.WebService.Schedules; +using StellaOps.Scheduler.WebService.Options; +using StellaOps.Scheduler.WebService.Runs; +using StellaOps.Scheduler.WebService.PolicyRuns; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddSingleton(); +builder.Services.TryAddSingleton(TimeProvider.System); + +var authorityOptions = new SchedulerAuthorityOptions(); +builder.Configuration.GetSection("Scheduler:Authority").Bind(authorityOptions); + +if (!authorityOptions.RequiredScopes.Any(scope => string.Equals(scope, StellaOpsScopes.GraphRead, StringComparison.OrdinalIgnoreCase))) +{ + authorityOptions.RequiredScopes.Add(StellaOpsScopes.GraphRead); +} + +if (!authorityOptions.RequiredScopes.Any(scope => string.Equals(scope, StellaOpsScopes.GraphWrite, StringComparison.OrdinalIgnoreCase))) +{ + authorityOptions.RequiredScopes.Add(StellaOpsScopes.GraphWrite); +} + +if (authorityOptions.Audiences.Count == 0) +{ + authorityOptions.Audiences.Add("api://scheduler"); +} + +authorityOptions.Validate(); +builder.Services.AddSingleton(authorityOptions); + builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("Scheduler:Events")) - .PostConfigure(options => - { - options.Webhooks ??= new SchedulerInboundWebhooksOptions(); - options.Webhooks.Feedser ??= SchedulerWebhookOptions.CreateDefault("feedser"); - options.Webhooks.Vexer ??= SchedulerWebhookOptions.CreateDefault("vexer"); - - options.Webhooks.Feedser.Name = string.IsNullOrWhiteSpace(options.Webhooks.Feedser.Name) - ? "feedser" - : options.Webhooks.Feedser.Name; - options.Webhooks.Vexer.Name = string.IsNullOrWhiteSpace(options.Webhooks.Vexer.Name) - ? "vexer" - : options.Webhooks.Vexer.Name; - - options.Webhooks.Feedser.Validate(); - options.Webhooks.Vexer.Validate(); - }); - + .PostConfigure(options => + { + options.Webhooks ??= new SchedulerInboundWebhooksOptions(); + options.Webhooks.Conselier ??= SchedulerWebhookOptions.CreateDefault("conselier"); + options.Webhooks.Excitor ??= SchedulerWebhookOptions.CreateDefault("excitor"); + + options.Webhooks.Conselier.Name = string.IsNullOrWhiteSpace(options.Webhooks.Conselier.Name) + ? "conselier" + : options.Webhooks.Conselier.Name; + options.Webhooks.Excitor.Name = string.IsNullOrWhiteSpace(options.Webhooks.Excitor.Name) + ? "excitor" + : options.Webhooks.Excitor.Name; + + options.Webhooks.Conselier.Validate(); + options.Webhooks.Excitor.Validate(); + }); + builder.Services.AddMemoryCache(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - -var cartographerOptions = builder.Configuration.GetSection("Scheduler:Cartographer").Get() ?? new SchedulerCartographerOptions(); -builder.Services.AddSingleton(cartographerOptions); -builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection("Scheduler:Cartographer")); - -var storageSection = builder.Configuration.GetSection("Scheduler:Storage"); -if (storageSection.Exists()) -{ - builder.Services.AddSchedulerMongoStorage(storageSection); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} -else -{ - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} -builder.Services.AddSingleton(); -if (cartographerOptions.Webhook.Enabled) -{ - builder.Services.AddHttpClient((serviceProvider, client) => - { - var options = serviceProvider.GetRequiredService>().CurrentValue; - client.Timeout = TimeSpan.FromSeconds(options.Webhook.TimeoutSeconds <= 0 ? 10 : options.Webhook.TimeoutSeconds); - }); -} -else -{ - builder.Services.AddSingleton(); -} -builder.Services.AddScoped(); -builder.Services.AddImpactIndexStub(); - -var schedulerOptions = builder.Configuration.GetSection("Scheduler").Get() ?? new SchedulerOptions(); -schedulerOptions.Validate(); -builder.Services.AddSingleton(schedulerOptions); -builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection("Scheduler")) - .PostConfigure(options => options.Validate()); - -var pluginHostOptions = SchedulerPluginHostFactory.Build(schedulerOptions.Plugins, builder.Environment.ContentRootPath); -builder.Services.AddSingleton(pluginHostOptions); -builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions); - -if (authorityOptions.Enabled) -{ - builder.Services.AddHttpContextAccessor(); - builder.Services.AddStellaOpsResourceServerAuthentication( - builder.Configuration, - configurationSection: null, - configure: resourceOptions => - { - resourceOptions.Authority = authorityOptions.Issuer; - resourceOptions.RequireHttpsMetadata = authorityOptions.RequireHttpsMetadata; - resourceOptions.MetadataAddress = authorityOptions.MetadataAddress; - resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(authorityOptions.BackchannelTimeoutSeconds); - resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(authorityOptions.TokenClockSkewSeconds); - - foreach (var audience in authorityOptions.Audiences) - { - resourceOptions.Audiences.Add(audience); - } - - foreach (var scope in authorityOptions.RequiredScopes) - { - resourceOptions.RequiredScopes.Add(scope); - } - - foreach (var tenant in authorityOptions.RequiredTenants) - { - resourceOptions.RequiredTenants.Add(tenant); - } - - foreach (var network in authorityOptions.BypassNetworks) - { - resourceOptions.BypassNetworks.Add(network); - } - }); - - builder.Services.AddAuthorization(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); -} -else -{ - builder.Services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = "Anonymous"; - options.DefaultChallengeScheme = "Anonymous"; - }).AddScheme("Anonymous", static _ => { }); - - builder.Services.AddAuthorization(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); -} - -builder.Services.AddEndpointsApiExplorer(); - -var app = builder.Build(); - -app.UseAuthentication(); -app.UseAuthorization(); - -if (!authorityOptions.Enabled) -{ - app.Logger.LogWarning("Scheduler Authority authentication is disabled; relying on header-based development fallback."); -} -else if (authorityOptions.AllowAnonymousFallback) -{ - app.Logger.LogWarning("Scheduler Authority authentication is enabled but anonymous fallback remains allowed. Disable fallback before production rollout."); -} - -app.MapGet("/healthz", () => Results.Json(new { status = "ok" })); -app.MapGet("/readyz", () => Results.Json(new { status = "ready" })); - -app.MapGraphJobEndpoints(); -app.MapScheduleEndpoints(); -app.MapRunEndpoints(); -app.MapPolicyRunEndpoints(); -app.MapSchedulerEventWebhookEndpoints(); - -app.Run(); - -public partial class Program; + +var cartographerOptions = builder.Configuration.GetSection("Scheduler:Cartographer").Get() ?? new SchedulerCartographerOptions(); +builder.Services.AddSingleton(cartographerOptions); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("Scheduler:Cartographer")); + +var storageSection = builder.Configuration.GetSection("Scheduler:Storage"); +if (storageSection.Exists()) +{ + builder.Services.AddSchedulerMongoStorage(storageSection); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +} +else +{ + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +} +builder.Services.AddSingleton(); +if (cartographerOptions.Webhook.Enabled) +{ + builder.Services.AddHttpClient((serviceProvider, client) => + { + var options = serviceProvider.GetRequiredService>().CurrentValue; + client.Timeout = TimeSpan.FromSeconds(options.Webhook.TimeoutSeconds <= 0 ? 10 : options.Webhook.TimeoutSeconds); + }); +} +else +{ + builder.Services.AddSingleton(); +} +builder.Services.AddScoped(); +builder.Services.AddImpactIndexStub(); + +var schedulerOptions = builder.Configuration.GetSection("Scheduler").Get() ?? new SchedulerOptions(); +schedulerOptions.Validate(); +builder.Services.AddSingleton(schedulerOptions); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("Scheduler")) + .PostConfigure(options => options.Validate()); + +var pluginHostOptions = SchedulerPluginHostFactory.Build(schedulerOptions.Plugins, builder.Environment.ContentRootPath); +builder.Services.AddSingleton(pluginHostOptions); +builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions); + +if (authorityOptions.Enabled) +{ + builder.Services.AddHttpContextAccessor(); + builder.Services.AddStellaOpsResourceServerAuthentication( + builder.Configuration, + configurationSection: null, + configure: resourceOptions => + { + resourceOptions.Authority = authorityOptions.Issuer; + resourceOptions.RequireHttpsMetadata = authorityOptions.RequireHttpsMetadata; + resourceOptions.MetadataAddress = authorityOptions.MetadataAddress; + resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(authorityOptions.BackchannelTimeoutSeconds); + resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(authorityOptions.TokenClockSkewSeconds); + + foreach (var audience in authorityOptions.Audiences) + { + resourceOptions.Audiences.Add(audience); + } + + foreach (var scope in authorityOptions.RequiredScopes) + { + resourceOptions.RequiredScopes.Add(scope); + } + + foreach (var tenant in authorityOptions.RequiredTenants) + { + resourceOptions.RequiredTenants.Add(tenant); + } + + foreach (var network in authorityOptions.BypassNetworks) + { + resourceOptions.BypassNetworks.Add(network); + } + }); + + builder.Services.AddAuthorization(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); +} +else +{ + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = "Anonymous"; + options.DefaultChallengeScheme = "Anonymous"; + }).AddScheme("Anonymous", static _ => { }); + + builder.Services.AddAuthorization(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); +} + +builder.Services.AddEndpointsApiExplorer(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +if (!authorityOptions.Enabled) +{ + app.Logger.LogWarning("Scheduler Authority authentication is disabled; relying on header-based development fallback."); +} +else if (authorityOptions.AllowAnonymousFallback) +{ + app.Logger.LogWarning("Scheduler Authority authentication is enabled but anonymous fallback remains allowed. Disable fallback before production rollout."); +} + +app.MapGet("/healthz", () => Results.Json(new { status = "ok" })); +app.MapGet("/readyz", () => Results.Json(new { status = "ready" })); + +app.MapGraphJobEndpoints(); +app.MapScheduleEndpoints(); +app.MapRunEndpoints(); +app.MapPolicyRunEndpoints(); +app.MapSchedulerEventWebhookEndpoints(); + +app.Run(); + +public partial class Program; diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/TASKS.completed.md b/src/Scheduler/StellaOps.Scheduler.WebService/TASKS.completed.md index 8a7ab8a8..6bef8d02 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/TASKS.completed.md +++ b/src/Scheduler/StellaOps.Scheduler.WebService/TASKS.completed.md @@ -5,7 +5,7 @@ | SCHED-WEB-16-101 | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-MODELS-16-101 | Bootstrap Minimal API host with Authority OpTok + DPoP, health endpoints, plug-in discovery per architecture §§1–2. | Service boots with config validation; `/healthz`/`/readyz` pass; restart-only plug-ins enforced. | | SCHED-WEB-16-102 | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-16-101 | Implement schedules CRUD (tenant-scoped) with cron validation, pause/resume, audit logging. | CRUD operations tested; invalid cron inputs rejected; audit entries persisted. | | SCHED-WEB-16-103 | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-16-102 | Runs API (list/detail/cancel), ad-hoc run POST, and impact preview endpoints. | Integration tests cover run lifecycle; preview returns counts/sample; cancellation honoured. | -| SCHED-WEB-16-104 | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-QUEUE-16-401, SCHED-STORAGE-16-201 | Webhook endpoints for Feeder/Vexer exports with mTLS/HMAC validation and rate limiting. | Webhooks validated via tests; invalid signatures rejected; rate limits documented. | +| SCHED-WEB-16-104 | DONE (2025-10-27) | Scheduler WebService Guild | SCHED-QUEUE-16-401, SCHED-STORAGE-16-201 | Webhook endpoints for Feeder/Excitor exports with mTLS/HMAC validation and rate limiting. | Webhooks validated via tests; invalid signatures rejected; rate limits documented. | | SCHED-WEB-20-001 | DONE (2025-10-29) | Scheduler WebService Guild, Policy Guild | SCHED-WEB-16-101, POLICY-ENGINE-20-000 | Expose policy run scheduling APIs (`POST /policy/runs`, `GET /policy/runs`) with tenant scoping and RBAC enforcement for `policy:run`. | Endpoints documented; integration tests cover run creation/status; unauthorized access blocked. | | SCHED-WEB-21-001 | DONE (2025-10-26) | Scheduler WebService Guild, Cartographer Guild | SCHED-WEB-16-101, SCHED-MODELS-21-001 | Expose graph build/overlay job APIs (`POST /graphs/build`, `GET /graphs/jobs`) with `graph:write`/`graph:read` enforcement and tenant scoping. | APIs documented in `docs/SCHED-WEB-21-001-GRAPH-APIS.md`; integration tests cover submission/status; unauthorized requests blocked; scope checks now reference `StellaOpsScopes`. | | SCHED-WEB-21-002 | DONE (2025-10-26) | Scheduler WebService Guild | SCHED-WEB-21-001, CARTO-GRAPH-21-007 | Provide overlay lag metrics endpoint and webhook to notify Cartographer of job completions; include correlation IDs. | `POST /graphs/hooks/completed` + `GET /graphs/overlays/lag` documented in `docs/SCHED-WEB-21-001-GRAPH-APIS.md`; integration tests cover completion + metrics. | diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/docs/SCHED-WEB-16-104-WEBHOOKS.md b/src/Scheduler/StellaOps.Scheduler.WebService/docs/SCHED-WEB-16-104-WEBHOOKS.md index 8dd6507d..db5305c6 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/docs/SCHED-WEB-16-104-WEBHOOKS.md +++ b/src/Scheduler/StellaOps.Scheduler.WebService/docs/SCHED-WEB-16-104-WEBHOOKS.md @@ -1,58 +1,58 @@ -# SCHED-WEB-16-104 · Feedser/Vexer Webhook Endpoints - -## Overview - -Scheduler.WebService exposes inbound webhooks that allow Feedser and Vexer to -notify the planner when new exports are available. Each webhook validates the -payload, enforces signature requirements, and applies a per-endpoint rate -limit before queuing downstream processing. - -| Endpoint | Description | AuthZ | -|----------|-------------|-------| -| `POST /events/feedser-export` | Ingest Feedser export metadata (`exportId`, `changedProductKeys`, optional KEV & window). | HMAC `X-Scheduler-Signature` and/or mTLS client certificate | -| `POST /events/vexer-export` | Ingest Vexer export delta summary (`changedClaims`). | HMAC `X-Scheduler-Signature` and/or mTLS client certificate | - -## Security - -* Webhooks require either: - * mTLS with trusted client certificates; **or** - * an HMAC-SHA256 signature in the `X-Scheduler-Signature` header. The - signature must be computed as `sha256=` over the raw request body. -* Requests without the required signature/certificate return `401`. -* Secrets are configured under `Scheduler:Events:Webhooks:{Feedser|Vexer}:HmacSecret`. - -## Rate limiting - -* Each webhook enforces a sliding-window limit (`RateLimitRequests` over - `RateLimitWindowSeconds`). -* Requests over the limit return `429` and include a `Retry-After` header. -* Defaults: 120 requests / 60 seconds. Adjust via configuration. - -## Configuration - -``` -Scheduler: - Events: - Webhooks: - Feedser: - Enabled: true - HmacSecret: feedser-secret - RequireClientCertificate: false - RateLimitRequests: 120 - RateLimitWindowSeconds: 60 - Vexer: - Enabled: true - HmacSecret: vexer-secret - RequireClientCertificate: false -``` - -## Response envelope - -On success the webhook returns `202 Accepted` and a JSON body: - -``` -{ "status": "accepted" } -``` - -Failures return problem JSON with `error` describing the violation. - +# SCHED-WEB-16-104 · Conselier/Excitor Webhook Endpoints + +## Overview + +Scheduler.WebService exposes inbound webhooks that allow Conselier and Excitor to +notify the planner when new exports are available. Each webhook validates the +payload, enforces signature requirements, and applies a per-endpoint rate +limit before queuing downstream processing. + +| Endpoint | Description | AuthZ | +|----------|-------------|-------| +| `POST /events/conselier-export` | Ingest Conselier export metadata (`exportId`, `changedProductKeys`, optional KEV & window). | HMAC `X-Scheduler-Signature` and/or mTLS client certificate | +| `POST /events/excitor-export` | Ingest Excitor export delta summary (`changedClaims`). | HMAC `X-Scheduler-Signature` and/or mTLS client certificate | + +## Security + +* Webhooks require either: + * mTLS with trusted client certificates; **or** + * an HMAC-SHA256 signature in the `X-Scheduler-Signature` header. The + signature must be computed as `sha256=` over the raw request body. +* Requests without the required signature/certificate return `401`. +* Secrets are configured under `Scheduler:Events:Webhooks:{Conselier|Excitor}:HmacSecret`. + +## Rate limiting + +* Each webhook enforces a sliding-window limit (`RateLimitRequests` over + `RateLimitWindowSeconds`). +* Requests over the limit return `429` and include a `Retry-After` header. +* Defaults: 120 requests / 60 seconds. Adjust via configuration. + +## Configuration + +``` +Scheduler: + Events: + Webhooks: + Conselier: + Enabled: true + HmacSecret: conselier-secret + RequireClientCertificate: false + RateLimitRequests: 120 + RateLimitWindowSeconds: 60 + Excitor: + Enabled: true + HmacSecret: excitor-secret + RequireClientCertificate: false +``` + +## Response envelope + +On success the webhook returns `202 Accepted` and a JSON body: + +``` +{ "status": "accepted" } +``` + +Failures return problem JSON with `error` describing the violation. + diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/CanonicalJsonSerializer.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/CanonicalJsonSerializer.cs index 8b0447e8..5eee627c 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/CanonicalJsonSerializer.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/CanonicalJsonSerializer.cs @@ -1,168 +1,168 @@ -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace StellaOps.Scheduler.Models; - -/// -/// Deterministic serializer for scheduler DTOs. -/// -public static class CanonicalJsonSerializer -{ - private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false); - private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true); - - private static readonly IReadOnlyDictionary PropertyOrder = new Dictionary - { - [typeof(Schedule)] = new[] - { - "schemaVersion", - "id", - "tenantId", - "name", - "enabled", - "cronExpression", - "timezone", - "mode", - "selection", - "onlyIf", - "notify", - "limits", - "subscribers", - "createdAt", - "createdBy", - "updatedAt", - "updatedBy", - }, - [typeof(Selector)] = new[] - { - "scope", - "tenantId", - "namespaces", - "repositories", - "digests", - "includeTags", - "labels", - "resolvesTags", - }, - [typeof(LabelSelector)] = new[] - { - "key", - "values", - }, - [typeof(ScheduleOnlyIf)] = new[] - { - "lastReportOlderThanDays", - "policyRevision", - }, - [typeof(ScheduleNotify)] = new[] - { - "onNewFindings", - "minSeverity", - "includeKev", - "includeQuietFindings", - }, - [typeof(ScheduleLimits)] = new[] - { - "maxJobs", - "ratePerSecond", - "parallelism", - "burst", - }, - [typeof(Run)] = new[] - { - "schemaVersion", - "id", - "tenantId", - "scheduleId", - "trigger", - "state", - "stats", - "reason", - "createdAt", - "startedAt", - "finishedAt", - "error", - "deltas", - }, - [typeof(RunStats)] = new[] - { - "candidates", - "deduped", - "queued", - "completed", - "deltas", - "newCriticals", - "newHigh", - "newMedium", - "newLow", +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace StellaOps.Scheduler.Models; + +/// +/// Deterministic serializer for scheduler DTOs. +/// +public static class CanonicalJsonSerializer +{ + private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false); + private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true); + + private static readonly IReadOnlyDictionary PropertyOrder = new Dictionary + { + [typeof(Schedule)] = new[] + { + "schemaVersion", + "id", + "tenantId", + "name", + "enabled", + "cronExpression", + "timezone", + "mode", + "selection", + "onlyIf", + "notify", + "limits", + "subscribers", + "createdAt", + "createdBy", + "updatedAt", + "updatedBy", + }, + [typeof(Selector)] = new[] + { + "scope", + "tenantId", + "namespaces", + "repositories", + "digests", + "includeTags", + "labels", + "resolvesTags", + }, + [typeof(LabelSelector)] = new[] + { + "key", + "values", + }, + [typeof(ScheduleOnlyIf)] = new[] + { + "lastReportOlderThanDays", + "policyRevision", + }, + [typeof(ScheduleNotify)] = new[] + { + "onNewFindings", + "minSeverity", + "includeKev", + "includeQuietFindings", + }, + [typeof(ScheduleLimits)] = new[] + { + "maxJobs", + "ratePerSecond", + "parallelism", + "burst", + }, + [typeof(Run)] = new[] + { + "schemaVersion", + "id", + "tenantId", + "scheduleId", + "trigger", + "state", + "stats", + "reason", + "createdAt", + "startedAt", + "finishedAt", + "error", + "deltas", + }, + [typeof(RunStats)] = new[] + { + "candidates", + "deduped", + "queued", + "completed", + "deltas", + "newCriticals", + "newHigh", + "newMedium", + "newLow", }, [typeof(RunReason)] = new[] { "manualReason", - "feedserExportId", - "vexerExportId", - "cursor", - "impactWindowFrom", - "impactWindowTo", - }, - [typeof(DeltaSummary)] = new[] - { - "imageDigest", - "newFindings", - "newCriticals", - "newHigh", - "newMedium", - "newLow", - "kevHits", - "topFindings", - "reportUrl", - "attestation", - "detectedAt", - }, - [typeof(DeltaFinding)] = new[] - { - "purl", - "vulnerabilityId", - "severity", - "link", - }, - [typeof(ImpactSet)] = new[] - { - "schemaVersion", - "selector", - "images", - "usageOnly", - "generatedAt", - "total", + "conselierExportId", + "excitorExportId", + "cursor", + "impactWindowFrom", + "impactWindowTo", + }, + [typeof(DeltaSummary)] = new[] + { + "imageDigest", + "newFindings", + "newCriticals", + "newHigh", + "newMedium", + "newLow", + "kevHits", + "topFindings", + "reportUrl", + "attestation", + "detectedAt", + }, + [typeof(DeltaFinding)] = new[] + { + "purl", + "vulnerabilityId", + "severity", + "link", + }, + [typeof(ImpactSet)] = new[] + { + "schemaVersion", + "selector", + "images", + "usageOnly", + "generatedAt", + "total", "snapshotId", }, [typeof(ImpactImage)] = new[] { "imageDigest", - "registry", - "repository", - "namespaces", - "tags", - "usedByEntrypoint", - "labels", - }, - [typeof(AuditRecord)] = new[] - { - "id", - "tenantId", - "category", - "action", - "occurredAt", - "actor", - "entityId", - "scheduleId", - "runId", - "correlationId", - "metadata", - "message", - }, + "registry", + "repository", + "namespaces", + "tags", + "usedByEntrypoint", + "labels", + }, + [typeof(AuditRecord)] = new[] + { + "id", + "tenantId", + "category", + "action", + "occurredAt", + "actor", + "entityId", + "scheduleId", + "runId", + "correlationId", + "metadata", + "message", + }, [typeof(AuditActor)] = new[] { "actorId", @@ -378,32 +378,32 @@ public static class CanonicalJsonSerializer "note", }, }; - - public static string Serialize(T value) - => JsonSerializer.Serialize(value, CompactOptions); - - public static string SerializeIndented(T value) - => JsonSerializer.Serialize(value, PrettyOptions); - - public static T Deserialize(string json) - => JsonSerializer.Deserialize(json, PrettyOptions) - ?? throw new InvalidOperationException($"Unable to deserialize {typeof(T).Name}."); - - private static JsonSerializerOptions CreateOptions(bool writeIndented) - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = writeIndented, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - }; - - var resolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver(); - options.TypeInfoResolver = new DeterministicResolver(resolver); - options.Converters.Add(new ScheduleModeConverter()); - options.Converters.Add(new SelectorScopeConverter()); + + public static string Serialize(T value) + => JsonSerializer.Serialize(value, CompactOptions); + + public static string SerializeIndented(T value) + => JsonSerializer.Serialize(value, PrettyOptions); + + public static T Deserialize(string json) + => JsonSerializer.Deserialize(json, PrettyOptions) + ?? throw new InvalidOperationException($"Unable to deserialize {typeof(T).Name}."); + + private static JsonSerializerOptions CreateOptions(bool writeIndented) + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = writeIndented, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + var resolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver(); + options.TypeInfoResolver = new DeterministicResolver(resolver); + options.Converters.Add(new ScheduleModeConverter()); + options.Converters.Add(new SelectorScopeConverter()); options.Converters.Add(new RunTriggerConverter()); options.Converters.Add(new RunStateConverter()); options.Converters.Add(new SeverityRankConverter()); @@ -418,53 +418,53 @@ public static class CanonicalJsonSerializer options.Converters.Add(new PolicyRunJobStatusConverter()); return options; } - - private sealed class DeterministicResolver : IJsonTypeInfoResolver - { - private readonly IJsonTypeInfoResolver _inner; - - public DeterministicResolver(IJsonTypeInfoResolver inner) - { - _inner = inner ?? throw new ArgumentNullException(nameof(inner)); - } - - public JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) - { - var info = _inner.GetTypeInfo(type, options); - if (info is null) - { - throw new InvalidOperationException($"Unable to resolve JsonTypeInfo for '{type}'."); - } - - if (info.Kind is JsonTypeInfoKind.Object && info.Properties.Count > 1) - { - var ordered = info.Properties - .OrderBy(property => ResolveOrder(type, property.Name)) - .ThenBy(property => property.Name, StringComparer.Ordinal) - .ToArray(); - - info.Properties.Clear(); - foreach (var property in ordered) - { - info.Properties.Add(property); - } - } - - return info; - } - - private static int ResolveOrder(Type type, string propertyName) - { - if (PropertyOrder.TryGetValue(type, out var order)) - { - var index = Array.IndexOf(order, propertyName); - if (index >= 0) - { - return index; - } - } - - return int.MaxValue; - } - } -} + + private sealed class DeterministicResolver : IJsonTypeInfoResolver + { + private readonly IJsonTypeInfoResolver _inner; + + public DeterministicResolver(IJsonTypeInfoResolver inner) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + } + + public JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + var info = _inner.GetTypeInfo(type, options); + if (info is null) + { + throw new InvalidOperationException($"Unable to resolve JsonTypeInfo for '{type}'."); + } + + if (info.Kind is JsonTypeInfoKind.Object && info.Properties.Count > 1) + { + var ordered = info.Properties + .OrderBy(property => ResolveOrder(type, property.Name)) + .ThenBy(property => property.Name, StringComparer.Ordinal) + .ToArray(); + + info.Properties.Clear(); + foreach (var property in ordered) + { + info.Properties.Add(property); + } + } + + return info; + } + + private static int ResolveOrder(Type type, string propertyName) + { + if (PropertyOrder.TryGetValue(type, out var order)) + { + var index = Array.IndexOf(order, propertyName); + if (index >= 0) + { + return index; + } + } + + return int.MaxValue; + } + } +} diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Enums.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Enums.cs index 6398caf9..b66226c1 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Enums.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Enums.cs @@ -1,66 +1,66 @@ -using System.Text.Json.Serialization; - -namespace StellaOps.Scheduler.Models; - -/// -/// Execution mode for a schedule. -/// -[JsonConverter(typeof(ScheduleModeConverter))] -public enum ScheduleMode -{ - AnalysisOnly, - ContentRefresh, -} - -/// -/// Selector scope determining which filters are applied. -/// -[JsonConverter(typeof(SelectorScopeConverter))] -public enum SelectorScope -{ - AllImages, - ByNamespace, - ByRepository, - ByDigest, - ByLabels, -} - -/// -/// Source that triggered a run. -/// -[JsonConverter(typeof(RunTriggerConverter))] -public enum RunTrigger -{ - Cron, - Feedser, - Vexer, - Manual, -} - -/// -/// Lifecycle state of a scheduler run. -/// -[JsonConverter(typeof(RunStateConverter))] -public enum RunState -{ - Planning, - Queued, - Running, - Completed, - Error, - Cancelled, -} - -/// -/// Severity rankings used in scheduler payloads. -/// +using System.Text.Json.Serialization; + +namespace StellaOps.Scheduler.Models; + +/// +/// Execution mode for a schedule. +/// +[JsonConverter(typeof(ScheduleModeConverter))] +public enum ScheduleMode +{ + AnalysisOnly, + ContentRefresh, +} + +/// +/// Selector scope determining which filters are applied. +/// +[JsonConverter(typeof(SelectorScopeConverter))] +public enum SelectorScope +{ + AllImages, + ByNamespace, + ByRepository, + ByDigest, + ByLabels, +} + +/// +/// Source that triggered a run. +/// +[JsonConverter(typeof(RunTriggerConverter))] +public enum RunTrigger +{ + Cron, + Conselier, + Excitor, + Manual, +} + +/// +/// Lifecycle state of a scheduler run. +/// +[JsonConverter(typeof(RunStateConverter))] +public enum RunState +{ + Planning, + Queued, + Running, + Completed, + Error, + Cancelled, +} + +/// +/// Severity rankings used in scheduler payloads. +/// [JsonConverter(typeof(SeverityRankConverter))] public enum SeverityRank { None = 0, Info = 1, - Low = 2, - Medium = 3, + Low = 2, + Medium = 3, High = 4, Critical = 5, Unknown = 6, diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Run.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Run.cs index be8798a0..53b34b11 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Run.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Run.cs @@ -1,378 +1,378 @@ -using System.Collections.Immutable; -using System.Text.Json.Serialization; - -namespace StellaOps.Scheduler.Models; - -/// -/// Execution record for a scheduler run. -/// -public sealed record Run -{ - public Run( - string id, - string tenantId, - RunTrigger trigger, - RunState state, - RunStats stats, - DateTimeOffset createdAt, - RunReason? reason = null, - string? scheduleId = null, - DateTimeOffset? startedAt = null, - DateTimeOffset? finishedAt = null, - string? error = null, - IEnumerable? deltas = null, - string? schemaVersion = null) - : this( - id, - tenantId, - trigger, - state, - stats, - reason ?? RunReason.Empty, - scheduleId, - Validation.NormalizeTimestamp(createdAt), - Validation.NormalizeTimestamp(startedAt), - Validation.NormalizeTimestamp(finishedAt), - Validation.TrimToNull(error), - NormalizeDeltas(deltas), - schemaVersion) - { - } - - [JsonConstructor] - public Run( - string id, - string tenantId, - RunTrigger trigger, - RunState state, - RunStats stats, - RunReason reason, - string? scheduleId, - DateTimeOffset createdAt, - DateTimeOffset? startedAt, - DateTimeOffset? finishedAt, - string? error, - ImmutableArray deltas, - string? schemaVersion = null) - { - Id = Validation.EnsureId(id, nameof(id)); - TenantId = Validation.EnsureTenantId(tenantId, nameof(tenantId)); - Trigger = trigger; - State = state; - Stats = stats ?? throw new ArgumentNullException(nameof(stats)); - Reason = reason ?? RunReason.Empty; - ScheduleId = Validation.TrimToNull(scheduleId); - CreatedAt = Validation.NormalizeTimestamp(createdAt); - StartedAt = Validation.NormalizeTimestamp(startedAt); - FinishedAt = Validation.NormalizeTimestamp(finishedAt); - Error = Validation.TrimToNull(error); - Deltas = deltas.IsDefault - ? ImmutableArray.Empty - : deltas.OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal).ToImmutableArray(); - SchemaVersion = SchedulerSchemaVersions.EnsureRun(schemaVersion); - } - - public string SchemaVersion { get; } - - public string Id { get; } - - public string TenantId { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? ScheduleId { get; } - - public RunTrigger Trigger { get; } - - public RunState State { get; init; } - - public RunStats Stats { get; init; } - - public RunReason Reason { get; } - - public DateTimeOffset CreatedAt { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public DateTimeOffset? StartedAt { get; init; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public DateTimeOffset? FinishedAt { get; init; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Error { get; init; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public ImmutableArray Deltas { get; } = ImmutableArray.Empty; - - private static ImmutableArray NormalizeDeltas(IEnumerable? deltas) - { - if (deltas is null) - { - return ImmutableArray.Empty; - } - - return deltas - .Where(static delta => delta is not null) - .Select(static delta => delta!) - .OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal) - .ToImmutableArray(); - } -} - -/// -/// Context describing why a run executed. -/// -public sealed record RunReason -{ - public static RunReason Empty { get; } = new(); - - public RunReason( - string? manualReason = null, - string? feedserExportId = null, - string? vexerExportId = null, - string? cursor = null) - { - ManualReason = Validation.TrimToNull(manualReason); - FeedserExportId = Validation.TrimToNull(feedserExportId); - VexerExportId = Validation.TrimToNull(vexerExportId); - Cursor = Validation.TrimToNull(cursor); - } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? ManualReason { get; } = null; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? FeedserExportId { get; } = null; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? VexerExportId { get; } = null; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Cursor { get; } = null; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? ImpactWindowFrom { get; init; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? ImpactWindowTo { get; init; } -} - -/// -/// Aggregated counters for a scheduler run. -/// -public sealed record RunStats -{ - public static RunStats Empty { get; } = new(); - - public RunStats( - int candidates = 0, - int deduped = 0, - int queued = 0, - int completed = 0, - int deltas = 0, - int newCriticals = 0, - int newHigh = 0, - int newMedium = 0, - int newLow = 0) - { - Candidates = Validation.EnsureNonNegative(candidates, nameof(candidates)); - Deduped = Validation.EnsureNonNegative(deduped, nameof(deduped)); - Queued = Validation.EnsureNonNegative(queued, nameof(queued)); - Completed = Validation.EnsureNonNegative(completed, nameof(completed)); - Deltas = Validation.EnsureNonNegative(deltas, nameof(deltas)); - NewCriticals = Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)); - NewHigh = Validation.EnsureNonNegative(newHigh, nameof(newHigh)); - NewMedium = Validation.EnsureNonNegative(newMedium, nameof(newMedium)); - NewLow = Validation.EnsureNonNegative(newLow, nameof(newLow)); - } - - public int Candidates { get; } = 0; - - public int Deduped { get; } = 0; - - public int Queued { get; } = 0; - - public int Completed { get; } = 0; - - public int Deltas { get; } = 0; - - public int NewCriticals { get; } = 0; - - public int NewHigh { get; } = 0; - - public int NewMedium { get; } = 0; - - public int NewLow { get; } = 0; -} - -/// -/// Snapshot of delta impact for an image processed in a run. -/// -public sealed record DeltaSummary -{ - public DeltaSummary( - string imageDigest, - int newFindings, - int newCriticals, - int newHigh, - int newMedium, - int newLow, - IEnumerable? kevHits = null, - IEnumerable? topFindings = null, - string? reportUrl = null, - DeltaAttestation? attestation = null, - DateTimeOffset? detectedAt = null) - : this( - imageDigest, - Validation.EnsureNonNegative(newFindings, nameof(newFindings)), - Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)), - Validation.EnsureNonNegative(newHigh, nameof(newHigh)), - Validation.EnsureNonNegative(newMedium, nameof(newMedium)), - Validation.EnsureNonNegative(newLow, nameof(newLow)), - NormalizeKevHits(kevHits), - NormalizeFindings(topFindings), - Validation.TrimToNull(reportUrl), - attestation, - Validation.NormalizeTimestamp(detectedAt)) - { - } - - [JsonConstructor] - public DeltaSummary( - string imageDigest, - int newFindings, - int newCriticals, - int newHigh, - int newMedium, - int newLow, - ImmutableArray kevHits, - ImmutableArray topFindings, - string? reportUrl, - DeltaAttestation? attestation, - DateTimeOffset? detectedAt) - { - ImageDigest = Validation.EnsureDigestFormat(imageDigest, nameof(imageDigest)); - NewFindings = Validation.EnsureNonNegative(newFindings, nameof(newFindings)); - NewCriticals = Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)); - NewHigh = Validation.EnsureNonNegative(newHigh, nameof(newHigh)); - NewMedium = Validation.EnsureNonNegative(newMedium, nameof(newMedium)); - NewLow = Validation.EnsureNonNegative(newLow, nameof(newLow)); - KevHits = kevHits.IsDefault ? ImmutableArray.Empty : kevHits; - TopFindings = topFindings.IsDefault - ? ImmutableArray.Empty - : topFindings - .OrderBy(static finding => finding.Severity, SeverityRankComparer.Instance) - .ThenBy(static finding => finding.VulnerabilityId, StringComparer.Ordinal) - .ToImmutableArray(); - ReportUrl = Validation.TrimToNull(reportUrl); - Attestation = attestation; - DetectedAt = Validation.NormalizeTimestamp(detectedAt); - } - - public string ImageDigest { get; } - - public int NewFindings { get; } - - public int NewCriticals { get; } - - public int NewHigh { get; } - - public int NewMedium { get; } - - public int NewLow { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public ImmutableArray KevHits { get; } = ImmutableArray.Empty; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public ImmutableArray TopFindings { get; } = ImmutableArray.Empty; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? ReportUrl { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public DeltaAttestation? Attestation { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public DateTimeOffset? DetectedAt { get; } - - private static ImmutableArray NormalizeKevHits(IEnumerable? kevHits) - => Validation.NormalizeStringSet(kevHits, nameof(kevHits)); - - private static ImmutableArray NormalizeFindings(IEnumerable? findings) - { - if (findings is null) - { - return ImmutableArray.Empty; - } - - return findings - .Where(static finding => finding is not null) - .Select(static finding => finding!) - .OrderBy(static finding => finding.Severity, SeverityRankComparer.Instance) - .ThenBy(static finding => finding.VulnerabilityId, StringComparer.Ordinal) - .ToImmutableArray(); - } -} - -/// -/// Top finding entry included in delta summaries. -/// -public sealed record DeltaFinding -{ - public DeltaFinding(string purl, string vulnerabilityId, SeverityRank severity, string? link = null) - { - Purl = Validation.EnsureSimpleIdentifier(purl, nameof(purl)); - VulnerabilityId = Validation.EnsureSimpleIdentifier(vulnerabilityId, nameof(vulnerabilityId)); - Severity = severity; - Link = Validation.TrimToNull(link); - } - - public string Purl { get; } - - public string VulnerabilityId { get; } - - public SeverityRank Severity { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Link { get; } -} - -/// -/// Rekor/attestation information surfaced with a delta summary. -/// -public sealed record DeltaAttestation -{ - public DeltaAttestation(string? uuid, bool? verified = null) - { - Uuid = Validation.TrimToNull(uuid); - Verified = verified; - } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Uuid { get; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? Verified { get; } -} - -internal sealed class SeverityRankComparer : IComparer -{ - public static SeverityRankComparer Instance { get; } = new(); - - private static readonly Dictionary Order = new() - { - [SeverityRank.Critical] = 0, - [SeverityRank.High] = 1, - [SeverityRank.Unknown] = 2, - [SeverityRank.Medium] = 3, - [SeverityRank.Low] = 4, - [SeverityRank.Info] = 5, - [SeverityRank.None] = 6, - }; - - public int Compare(SeverityRank x, SeverityRank y) - => GetOrder(x).CompareTo(GetOrder(y)); - - private static int GetOrder(SeverityRank severity) - => Order.TryGetValue(severity, out var value) ? value : int.MaxValue; -} +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +namespace StellaOps.Scheduler.Models; + +/// +/// Execution record for a scheduler run. +/// +public sealed record Run +{ + public Run( + string id, + string tenantId, + RunTrigger trigger, + RunState state, + RunStats stats, + DateTimeOffset createdAt, + RunReason? reason = null, + string? scheduleId = null, + DateTimeOffset? startedAt = null, + DateTimeOffset? finishedAt = null, + string? error = null, + IEnumerable? deltas = null, + string? schemaVersion = null) + : this( + id, + tenantId, + trigger, + state, + stats, + reason ?? RunReason.Empty, + scheduleId, + Validation.NormalizeTimestamp(createdAt), + Validation.NormalizeTimestamp(startedAt), + Validation.NormalizeTimestamp(finishedAt), + Validation.TrimToNull(error), + NormalizeDeltas(deltas), + schemaVersion) + { + } + + [JsonConstructor] + public Run( + string id, + string tenantId, + RunTrigger trigger, + RunState state, + RunStats stats, + RunReason reason, + string? scheduleId, + DateTimeOffset createdAt, + DateTimeOffset? startedAt, + DateTimeOffset? finishedAt, + string? error, + ImmutableArray deltas, + string? schemaVersion = null) + { + Id = Validation.EnsureId(id, nameof(id)); + TenantId = Validation.EnsureTenantId(tenantId, nameof(tenantId)); + Trigger = trigger; + State = state; + Stats = stats ?? throw new ArgumentNullException(nameof(stats)); + Reason = reason ?? RunReason.Empty; + ScheduleId = Validation.TrimToNull(scheduleId); + CreatedAt = Validation.NormalizeTimestamp(createdAt); + StartedAt = Validation.NormalizeTimestamp(startedAt); + FinishedAt = Validation.NormalizeTimestamp(finishedAt); + Error = Validation.TrimToNull(error); + Deltas = deltas.IsDefault + ? ImmutableArray.Empty + : deltas.OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal).ToImmutableArray(); + SchemaVersion = SchedulerSchemaVersions.EnsureRun(schemaVersion); + } + + public string SchemaVersion { get; } + + public string Id { get; } + + public string TenantId { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ScheduleId { get; } + + public RunTrigger Trigger { get; } + + public RunState State { get; init; } + + public RunStats Stats { get; init; } + + public RunReason Reason { get; } + + public DateTimeOffset CreatedAt { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTimeOffset? StartedAt { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTimeOffset? FinishedAt { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ImmutableArray Deltas { get; } = ImmutableArray.Empty; + + private static ImmutableArray NormalizeDeltas(IEnumerable? deltas) + { + if (deltas is null) + { + return ImmutableArray.Empty; + } + + return deltas + .Where(static delta => delta is not null) + .Select(static delta => delta!) + .OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal) + .ToImmutableArray(); + } +} + +/// +/// Context describing why a run executed. +/// +public sealed record RunReason +{ + public static RunReason Empty { get; } = new(); + + public RunReason( + string? manualReason = null, + string? conselierExportId = null, + string? excitorExportId = null, + string? cursor = null) + { + ManualReason = Validation.TrimToNull(manualReason); + ConselierExportId = Validation.TrimToNull(conselierExportId); + ExcitorExportId = Validation.TrimToNull(excitorExportId); + Cursor = Validation.TrimToNull(cursor); + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ManualReason { get; } = null; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ConselierExportId { get; } = null; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ExcitorExportId { get; } = null; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Cursor { get; } = null; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ImpactWindowFrom { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ImpactWindowTo { get; init; } +} + +/// +/// Aggregated counters for a scheduler run. +/// +public sealed record RunStats +{ + public static RunStats Empty { get; } = new(); + + public RunStats( + int candidates = 0, + int deduped = 0, + int queued = 0, + int completed = 0, + int deltas = 0, + int newCriticals = 0, + int newHigh = 0, + int newMedium = 0, + int newLow = 0) + { + Candidates = Validation.EnsureNonNegative(candidates, nameof(candidates)); + Deduped = Validation.EnsureNonNegative(deduped, nameof(deduped)); + Queued = Validation.EnsureNonNegative(queued, nameof(queued)); + Completed = Validation.EnsureNonNegative(completed, nameof(completed)); + Deltas = Validation.EnsureNonNegative(deltas, nameof(deltas)); + NewCriticals = Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)); + NewHigh = Validation.EnsureNonNegative(newHigh, nameof(newHigh)); + NewMedium = Validation.EnsureNonNegative(newMedium, nameof(newMedium)); + NewLow = Validation.EnsureNonNegative(newLow, nameof(newLow)); + } + + public int Candidates { get; } = 0; + + public int Deduped { get; } = 0; + + public int Queued { get; } = 0; + + public int Completed { get; } = 0; + + public int Deltas { get; } = 0; + + public int NewCriticals { get; } = 0; + + public int NewHigh { get; } = 0; + + public int NewMedium { get; } = 0; + + public int NewLow { get; } = 0; +} + +/// +/// Snapshot of delta impact for an image processed in a run. +/// +public sealed record DeltaSummary +{ + public DeltaSummary( + string imageDigest, + int newFindings, + int newCriticals, + int newHigh, + int newMedium, + int newLow, + IEnumerable? kevHits = null, + IEnumerable? topFindings = null, + string? reportUrl = null, + DeltaAttestation? attestation = null, + DateTimeOffset? detectedAt = null) + : this( + imageDigest, + Validation.EnsureNonNegative(newFindings, nameof(newFindings)), + Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)), + Validation.EnsureNonNegative(newHigh, nameof(newHigh)), + Validation.EnsureNonNegative(newMedium, nameof(newMedium)), + Validation.EnsureNonNegative(newLow, nameof(newLow)), + NormalizeKevHits(kevHits), + NormalizeFindings(topFindings), + Validation.TrimToNull(reportUrl), + attestation, + Validation.NormalizeTimestamp(detectedAt)) + { + } + + [JsonConstructor] + public DeltaSummary( + string imageDigest, + int newFindings, + int newCriticals, + int newHigh, + int newMedium, + int newLow, + ImmutableArray kevHits, + ImmutableArray topFindings, + string? reportUrl, + DeltaAttestation? attestation, + DateTimeOffset? detectedAt) + { + ImageDigest = Validation.EnsureDigestFormat(imageDigest, nameof(imageDigest)); + NewFindings = Validation.EnsureNonNegative(newFindings, nameof(newFindings)); + NewCriticals = Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)); + NewHigh = Validation.EnsureNonNegative(newHigh, nameof(newHigh)); + NewMedium = Validation.EnsureNonNegative(newMedium, nameof(newMedium)); + NewLow = Validation.EnsureNonNegative(newLow, nameof(newLow)); + KevHits = kevHits.IsDefault ? ImmutableArray.Empty : kevHits; + TopFindings = topFindings.IsDefault + ? ImmutableArray.Empty + : topFindings + .OrderBy(static finding => finding.Severity, SeverityRankComparer.Instance) + .ThenBy(static finding => finding.VulnerabilityId, StringComparer.Ordinal) + .ToImmutableArray(); + ReportUrl = Validation.TrimToNull(reportUrl); + Attestation = attestation; + DetectedAt = Validation.NormalizeTimestamp(detectedAt); + } + + public string ImageDigest { get; } + + public int NewFindings { get; } + + public int NewCriticals { get; } + + public int NewHigh { get; } + + public int NewMedium { get; } + + public int NewLow { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ImmutableArray KevHits { get; } = ImmutableArray.Empty; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ImmutableArray TopFindings { get; } = ImmutableArray.Empty; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ReportUrl { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DeltaAttestation? Attestation { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTimeOffset? DetectedAt { get; } + + private static ImmutableArray NormalizeKevHits(IEnumerable? kevHits) + => Validation.NormalizeStringSet(kevHits, nameof(kevHits)); + + private static ImmutableArray NormalizeFindings(IEnumerable? findings) + { + if (findings is null) + { + return ImmutableArray.Empty; + } + + return findings + .Where(static finding => finding is not null) + .Select(static finding => finding!) + .OrderBy(static finding => finding.Severity, SeverityRankComparer.Instance) + .ThenBy(static finding => finding.VulnerabilityId, StringComparer.Ordinal) + .ToImmutableArray(); + } +} + +/// +/// Top finding entry included in delta summaries. +/// +public sealed record DeltaFinding +{ + public DeltaFinding(string purl, string vulnerabilityId, SeverityRank severity, string? link = null) + { + Purl = Validation.EnsureSimpleIdentifier(purl, nameof(purl)); + VulnerabilityId = Validation.EnsureSimpleIdentifier(vulnerabilityId, nameof(vulnerabilityId)); + Severity = severity; + Link = Validation.TrimToNull(link); + } + + public string Purl { get; } + + public string VulnerabilityId { get; } + + public SeverityRank Severity { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Link { get; } +} + +/// +/// Rekor/attestation information surfaced with a delta summary. +/// +public sealed record DeltaAttestation +{ + public DeltaAttestation(string? uuid, bool? verified = null) + { + Uuid = Validation.TrimToNull(uuid); + Verified = verified; + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Uuid { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Verified { get; } +} + +internal sealed class SeverityRankComparer : IComparer +{ + public static SeverityRankComparer Instance { get; } = new(); + + private static readonly Dictionary Order = new() + { + [SeverityRank.Critical] = 0, + [SeverityRank.High] = 1, + [SeverityRank.Unknown] = 2, + [SeverityRank.Medium] = 3, + [SeverityRank.Low] = 4, + [SeverityRank.Info] = 5, + [SeverityRank.None] = 6, + }; + + public int Compare(SeverityRank x, SeverityRank y) + => GetOrder(x).CompareTo(GetOrder(y)); + + private static int GetOrder(SeverityRank severity) + => Order.TryGetValue(severity, out var value) ? value : int.MaxValue; +} diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs index 5f201f93..d4eb490a 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs @@ -432,14 +432,14 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher return $"manual:{reason.ManualReason}"; } - if (!string.IsNullOrWhiteSpace(reason.FeedserExportId)) + if (!string.IsNullOrWhiteSpace(reason.ConselierExportId)) { - return $"feedser:{reason.FeedserExportId}"; + return $"conselier:{reason.ConselierExportId}"; } - if (!string.IsNullOrWhiteSpace(reason.VexerExportId)) + if (!string.IsNullOrWhiteSpace(reason.ExcitorExportId)) { - return $"vexer:{reason.VexerExportId}"; + return $"excitor:{reason.ExcitorExportId}"; } return null; diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/docs/SCHED-WORKER-16-204-EVENTS.md b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/docs/SCHED-WORKER-16-204-EVENTS.md index a044ccbd..3fc9e362 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/docs/SCHED-WORKER-16-204-EVENTS.md +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/docs/SCHED-WORKER-16-204-EVENTS.md @@ -10,7 +10,7 @@ rescan activity in near real time. - `scheduler.rescan.delta@1` — published once per runner segment when that segment produced at least one meaningful delta (new critical/high findings or KEV hits). Payload batches all impacted digests for the segment and includes - severity totals. Reason strings (manual trigger, Feedser/Vexer exports) flow + severity totals. Reason strings (manual trigger, Conselier/Excitor exports) flow from the run reason when present. - `scanner.report.ready@1` — published for every image the runner processes. The payload mirrors the Scanner contract (verdict, summary buckets, DSSE diff --git a/src/Scheduler/__Tests/StellaOps.Scheduler.Models.Tests/RunValidationTests.cs b/src/Scheduler/__Tests/StellaOps.Scheduler.Models.Tests/RunValidationTests.cs index 0157d2e7..7cac6163 100644 --- a/src/Scheduler/__Tests/StellaOps.Scheduler.Models.Tests/RunValidationTests.cs +++ b/src/Scheduler/__Tests/StellaOps.Scheduler.Models.Tests/RunValidationTests.cs @@ -1,78 +1,78 @@ -using StellaOps.Scheduler.Models; - -namespace StellaOps.Scheduler.Models.Tests; - -public sealed class RunValidationTests -{ - [Fact] - public void RunStatsRejectsNegativeValues() - { - Assert.Throws(() => new RunStats(candidates: -1)); - Assert.Throws(() => new RunStats(deduped: -1)); - Assert.Throws(() => new RunStats(queued: -1)); - Assert.Throws(() => new RunStats(completed: -1)); - Assert.Throws(() => new RunStats(deltas: -1)); - Assert.Throws(() => new RunStats(newCriticals: -1)); - Assert.Throws(() => new RunStats(newHigh: -1)); - Assert.Throws(() => new RunStats(newMedium: -1)); - Assert.Throws(() => new RunStats(newLow: -1)); - } - - [Fact] - public void DeltaSummarySortsTopFindingsBySeverityThenId() - { - var summary = new DeltaSummary( - imageDigest: "sha256:0011", - newFindings: 3, - newCriticals: 1, - newHigh: 1, - newMedium: 1, - newLow: 0, - kevHits: new[] { "CVE-2025-0002", "CVE-2025-0001" }, - topFindings: new[] - { - new DeltaFinding("pkg:maven/b", "CVE-2025-0002", SeverityRank.High), - new DeltaFinding("pkg:maven/a", "CVE-2024-0001", SeverityRank.Critical), - new DeltaFinding("pkg:maven/c", "CVE-2025-0008", SeverityRank.Medium), - }, - reportUrl: "https://ui.example/reports/sha256:0011", - attestation: new DeltaAttestation(uuid: "rekor-1", verified: true), - detectedAt: DateTimeOffset.Parse("2025-10-18T00:01:02Z")); - - Assert.Equal(new[] { "pkg:maven/a", "pkg:maven/b", "pkg:maven/c" }, summary.TopFindings.Select(f => f.Purl)); - Assert.Equal(new[] { "CVE-2025-0001", "CVE-2025-0002" }, summary.KevHits); - } - - [Fact] - public void RunSerializationIncludesDeterministicOrdering() - { - var stats = new RunStats(candidates: 10, deduped: 8, queued: 8, completed: 5, deltas: 3, newCriticals: 2); - var run = new Run( - id: "run_001", - tenantId: "tenant-alpha", - trigger: RunTrigger.Feedser, - state: RunState.Running, - stats: stats, - reason: new RunReason(feedserExportId: "exp-123"), - scheduleId: "sch_001", - createdAt: DateTimeOffset.Parse("2025-10-18T01:00:00Z"), - startedAt: DateTimeOffset.Parse("2025-10-18T01:00:05Z"), - finishedAt: null, - error: null, - deltas: new[] - { - new DeltaSummary( - imageDigest: "sha256:aaa", - newFindings: 1, - newCriticals: 1, - newHigh: 0, - newMedium: 0, - newLow: 0) - }); - - var json = CanonicalJsonSerializer.Serialize(run); - Assert.Equal(SchedulerSchemaVersions.Run, run.SchemaVersion); - Assert.Contains("\"trigger\":\"feedser\"", json, StringComparison.Ordinal); - Assert.Contains("\"stats\":{\"candidates\":10,\"deduped\":8,\"queued\":8,\"completed\":5,\"deltas\":3,\"newCriticals\":2,\"newHigh\":0,\"newMedium\":0,\"newLow\":0}", json, StringComparison.Ordinal); - } -} +using StellaOps.Scheduler.Models; + +namespace StellaOps.Scheduler.Models.Tests; + +public sealed class RunValidationTests +{ + [Fact] + public void RunStatsRejectsNegativeValues() + { + Assert.Throws(() => new RunStats(candidates: -1)); + Assert.Throws(() => new RunStats(deduped: -1)); + Assert.Throws(() => new RunStats(queued: -1)); + Assert.Throws(() => new RunStats(completed: -1)); + Assert.Throws(() => new RunStats(deltas: -1)); + Assert.Throws(() => new RunStats(newCriticals: -1)); + Assert.Throws(() => new RunStats(newHigh: -1)); + Assert.Throws(() => new RunStats(newMedium: -1)); + Assert.Throws(() => new RunStats(newLow: -1)); + } + + [Fact] + public void DeltaSummarySortsTopFindingsBySeverityThenId() + { + var summary = new DeltaSummary( + imageDigest: "sha256:0011", + newFindings: 3, + newCriticals: 1, + newHigh: 1, + newMedium: 1, + newLow: 0, + kevHits: new[] { "CVE-2025-0002", "CVE-2025-0001" }, + topFindings: new[] + { + new DeltaFinding("pkg:maven/b", "CVE-2025-0002", SeverityRank.High), + new DeltaFinding("pkg:maven/a", "CVE-2024-0001", SeverityRank.Critical), + new DeltaFinding("pkg:maven/c", "CVE-2025-0008", SeverityRank.Medium), + }, + reportUrl: "https://ui.example/reports/sha256:0011", + attestation: new DeltaAttestation(uuid: "rekor-1", verified: true), + detectedAt: DateTimeOffset.Parse("2025-10-18T00:01:02Z")); + + Assert.Equal(new[] { "pkg:maven/a", "pkg:maven/b", "pkg:maven/c" }, summary.TopFindings.Select(f => f.Purl)); + Assert.Equal(new[] { "CVE-2025-0001", "CVE-2025-0002" }, summary.KevHits); + } + + [Fact] + public void RunSerializationIncludesDeterministicOrdering() + { + var stats = new RunStats(candidates: 10, deduped: 8, queued: 8, completed: 5, deltas: 3, newCriticals: 2); + var run = new Run( + id: "run_001", + tenantId: "tenant-alpha", + trigger: RunTrigger.Conselier, + state: RunState.Running, + stats: stats, + reason: new RunReason(conselierExportId: "exp-123"), + scheduleId: "sch_001", + createdAt: DateTimeOffset.Parse("2025-10-18T01:00:00Z"), + startedAt: DateTimeOffset.Parse("2025-10-18T01:00:05Z"), + finishedAt: null, + error: null, + deltas: new[] + { + new DeltaSummary( + imageDigest: "sha256:aaa", + newFindings: 1, + newCriticals: 1, + newHigh: 0, + newMedium: 0, + newLow: 0) + }); + + var json = CanonicalJsonSerializer.Serialize(run); + Assert.Equal(SchedulerSchemaVersions.Run, run.SchemaVersion); + Assert.Contains("\"trigger\":\"conselier\"", json, StringComparison.Ordinal); + Assert.Contains("\"stats\":{\"candidates\":10,\"deduped\":8,\"queued\":8,\"completed\":5,\"deltas\":3,\"newCriticals\":2,\"newHigh\":0,\"newMedium\":0,\"newLow\":0}", json, StringComparison.Ordinal); + } +} diff --git a/src/Scheduler/__Tests/StellaOps.Scheduler.Queue.Tests/PlannerAndRunnerMessageTests.cs b/src/Scheduler/__Tests/StellaOps.Scheduler.Queue.Tests/PlannerAndRunnerMessageTests.cs index ffe8804d..4d1bf762 100644 --- a/src/Scheduler/__Tests/StellaOps.Scheduler.Queue.Tests/PlannerAndRunnerMessageTests.cs +++ b/src/Scheduler/__Tests/StellaOps.Scheduler.Queue.Tests/PlannerAndRunnerMessageTests.cs @@ -1,110 +1,110 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using FluentAssertions; -using StellaOps.Scheduler.Models; -using Xunit; - -namespace StellaOps.Scheduler.Queue.Tests; - -public sealed class PlannerAndRunnerMessageTests -{ - [Fact] - public void PlannerMessage_CanonicalSerialization_RoundTrips() - { - var schedule = new Schedule( - id: "sch-tenant-nightly", - tenantId: "tenant-alpha", - name: "Nightly Deltas", - enabled: true, - cronExpression: "0 2 * * *", - timezone: "UTC", - mode: ScheduleMode.AnalysisOnly, - selection: new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha"), - onlyIf: new ScheduleOnlyIf(lastReportOlderThanDays: 3), - notify: new ScheduleNotify(onNewFindings: true, SeverityRank.High, includeKev: true), - limits: new ScheduleLimits(maxJobs: 10, ratePerSecond: 5, parallelism: 3), - createdAt: DateTimeOffset.Parse("2025-10-01T02:00:00Z"), - createdBy: "system", - updatedAt: DateTimeOffset.Parse("2025-10-02T02:00:00Z"), - updatedBy: "system", - subscribers: ImmutableArray.Empty, - schemaVersion: "1.0.0"); - - var run = new Run( - id: "run-123", - tenantId: "tenant-alpha", - trigger: RunTrigger.Cron, - state: RunState.Planning, - stats: new RunStats(candidates: 5, deduped: 4, queued: 0, completed: 0, deltas: 0), - createdAt: DateTimeOffset.Parse("2025-10-02T02:05:00Z"), - reason: new RunReason(manualReason: null, feedserExportId: null, vexerExportId: null, cursor: null) - with { ImpactWindowFrom = "2025-10-01T00:00:00Z", ImpactWindowTo = "2025-10-02T00:00:00Z" }, - scheduleId: "sch-tenant-nightly"); - - var impactSet = new ImpactSet( - selector: new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha"), - images: new[] - { - new ImpactImage( - imageDigest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - registry: "registry", - repository: "repo", - namespaces: new[] { "prod" }, - tags: new[] { "latest" }, - usedByEntrypoint: true, - labels: new[] { KeyValuePair.Create("team", "appsec") }) - }, - usageOnly: true, - generatedAt: DateTimeOffset.Parse("2025-10-02T02:06:00Z"), - total: 1, - snapshotId: "snap-001"); - - var message = new PlannerQueueMessage(run, impactSet, schedule, correlationId: "corr-1"); - - var json = CanonicalJsonSerializer.Serialize(message); - var roundTrip = CanonicalJsonSerializer.Deserialize(json); - - roundTrip.Should().BeEquivalentTo(message, options => options.WithStrictOrdering()); - } - - [Fact] - public void RunnerSegmentMessage_RequiresAtLeastOneDigest() - { - var act = () => new RunnerSegmentQueueMessage( - segmentId: "segment-empty", - runId: "run-123", - tenantId: "tenant-alpha", - imageDigests: Array.Empty()); - - act.Should().Throw(); - } - - [Fact] - public void RunnerSegmentMessage_CanonicalSerialization_RoundTrips() - { - var message = new RunnerSegmentQueueMessage( - segmentId: "segment-01", - runId: "run-123", - tenantId: "tenant-alpha", - imageDigests: new[] - { - "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - }, - scheduleId: "sch-tenant-nightly", - ratePerSecond: 25, - usageOnly: true, - attributes: new Dictionary - { - ["plannerShard"] = "0", - ["priority"] = "kev" - }, - correlationId: "corr-2"); - - var json = CanonicalJsonSerializer.Serialize(message); - var roundTrip = CanonicalJsonSerializer.Deserialize(json); - - roundTrip.Should().BeEquivalentTo(message, options => options.WithStrictOrdering()); - } -} +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using FluentAssertions; +using StellaOps.Scheduler.Models; +using Xunit; + +namespace StellaOps.Scheduler.Queue.Tests; + +public sealed class PlannerAndRunnerMessageTests +{ + [Fact] + public void PlannerMessage_CanonicalSerialization_RoundTrips() + { + var schedule = new Schedule( + id: "sch-tenant-nightly", + tenantId: "tenant-alpha", + name: "Nightly Deltas", + enabled: true, + cronExpression: "0 2 * * *", + timezone: "UTC", + mode: ScheduleMode.AnalysisOnly, + selection: new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha"), + onlyIf: new ScheduleOnlyIf(lastReportOlderThanDays: 3), + notify: new ScheduleNotify(onNewFindings: true, SeverityRank.High, includeKev: true), + limits: new ScheduleLimits(maxJobs: 10, ratePerSecond: 5, parallelism: 3), + createdAt: DateTimeOffset.Parse("2025-10-01T02:00:00Z"), + createdBy: "system", + updatedAt: DateTimeOffset.Parse("2025-10-02T02:00:00Z"), + updatedBy: "system", + subscribers: ImmutableArray.Empty, + schemaVersion: "1.0.0"); + + var run = new Run( + id: "run-123", + tenantId: "tenant-alpha", + trigger: RunTrigger.Cron, + state: RunState.Planning, + stats: new RunStats(candidates: 5, deduped: 4, queued: 0, completed: 0, deltas: 0), + createdAt: DateTimeOffset.Parse("2025-10-02T02:05:00Z"), + reason: new RunReason(manualReason: null, conselierExportId: null, excitorExportId: null, cursor: null) + with { ImpactWindowFrom = "2025-10-01T00:00:00Z", ImpactWindowTo = "2025-10-02T00:00:00Z" }, + scheduleId: "sch-tenant-nightly"); + + var impactSet = new ImpactSet( + selector: new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha"), + images: new[] + { + new ImpactImage( + imageDigest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + registry: "registry", + repository: "repo", + namespaces: new[] { "prod" }, + tags: new[] { "latest" }, + usedByEntrypoint: true, + labels: new[] { KeyValuePair.Create("team", "appsec") }) + }, + usageOnly: true, + generatedAt: DateTimeOffset.Parse("2025-10-02T02:06:00Z"), + total: 1, + snapshotId: "snap-001"); + + var message = new PlannerQueueMessage(run, impactSet, schedule, correlationId: "corr-1"); + + var json = CanonicalJsonSerializer.Serialize(message); + var roundTrip = CanonicalJsonSerializer.Deserialize(json); + + roundTrip.Should().BeEquivalentTo(message, options => options.WithStrictOrdering()); + } + + [Fact] + public void RunnerSegmentMessage_RequiresAtLeastOneDigest() + { + var act = () => new RunnerSegmentQueueMessage( + segmentId: "segment-empty", + runId: "run-123", + tenantId: "tenant-alpha", + imageDigests: Array.Empty()); + + act.Should().Throw(); + } + + [Fact] + public void RunnerSegmentMessage_CanonicalSerialization_RoundTrips() + { + var message = new RunnerSegmentQueueMessage( + segmentId: "segment-01", + runId: "run-123", + tenantId: "tenant-alpha", + imageDigests: new[] + { + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + scheduleId: "sch-tenant-nightly", + ratePerSecond: 25, + usageOnly: true, + attributes: new Dictionary + { + ["plannerShard"] = "0", + ["priority"] = "kev" + }, + correlationId: "corr-2"); + + var json = CanonicalJsonSerializer.Serialize(message); + var roundTrip = CanonicalJsonSerializer.Deserialize(json); + + roundTrip.Should().BeEquivalentTo(message, options => options.WithStrictOrdering()); + } +} diff --git a/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/EventWebhookEndpointTests.cs b/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/EventWebhookEndpointTests.cs index dae86c41..cb345ed6 100644 --- a/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/EventWebhookEndpointTests.cs +++ b/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/EventWebhookEndpointTests.cs @@ -1,128 +1,128 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.Configuration; - -namespace StellaOps.Scheduler.WebService.Tests; - -public sealed class EventWebhookEndpointTests : IClassFixture> -{ - static EventWebhookEndpointTests() - { - Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Feedser__HmacSecret", FeedserSecret); - Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Feedser__Enabled", "true"); - Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Vexer__HmacSecret", VexerSecret); - Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Vexer__Enabled", "true"); - } - - private const string FeedserSecret = "feedser-secret"; - private const string VexerSecret = "vexer-secret"; - - private readonly WebApplicationFactory _factory; - - public EventWebhookEndpointTests(WebApplicationFactory factory) - { - _factory = factory; - } - - [Fact] - public async Task FeedserWebhook_AcceptsValidSignature() - { - using var client = _factory.CreateClient(); - var payload = new - { - exportId = "feedser-exp-1", - changedProductKeys = new[] { "pkg:rpm/openssl", "pkg:deb/nginx" }, - kev = new[] { "CVE-2024-0001" }, - window = new { from = DateTimeOffset.UtcNow.AddHours(-1), to = DateTimeOffset.UtcNow } - }; - - var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - using var request = new HttpRequestMessage(HttpMethod.Post, "/events/feedser-export") - { - Content = new StringContent(json, Encoding.UTF8, "application/json") - }; - request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(FeedserSecret, json)); - - var response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); - } - - [Fact] - public async Task FeedserWebhook_RejectsInvalidSignature() - { - using var client = _factory.CreateClient(); - var payload = new - { - exportId = "feedser-exp-2", - changedProductKeys = new[] { "pkg:nuget/log4net" } - }; - - var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - using var request = new HttpRequestMessage(HttpMethod.Post, "/events/feedser-export") - { - Content = new StringContent(json, Encoding.UTF8, "application/json") - }; - request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", "sha256=invalid"); - - var response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task VexerWebhook_HonoursRateLimit() - { - using var restrictedFactory = _factory.WithWebHostBuilder(builder => - { - builder.ConfigureAppConfiguration((_, configuration) => - { - configuration.AddInMemoryCollection(new Dictionary - { - ["Scheduler:Events:Webhooks:Vexer:RateLimitRequests"] = "1", - ["Scheduler:Events:Webhooks:Vexer:RateLimitWindowSeconds"] = "60" - }); - }); - }); - - using var client = restrictedFactory.CreateClient(); - var payload = new - { - exportId = "vexer-exp-1", - changedClaims = new[] - { - new { productKey = "pkg:deb/openssl", vulnerabilityId = "CVE-2024-1234", status = "affected" } - } - }; - - var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - - using var first = new HttpRequestMessage(HttpMethod.Post, "/events/vexer-export") - { - Content = new StringContent(json, Encoding.UTF8, "application/json") - }; - first.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(VexerSecret, json)); - var firstResponse = await client.SendAsync(first); - Assert.Equal(HttpStatusCode.Accepted, firstResponse.StatusCode); - - using var second = new HttpRequestMessage(HttpMethod.Post, "/events/vexer-export") - { - Content = new StringContent(json, Encoding.UTF8, "application/json") - }; - second.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(VexerSecret, json)); - var secondResponse = await client.SendAsync(second); - Assert.Equal((HttpStatusCode)429, secondResponse.StatusCode); - Assert.True(secondResponse.Headers.Contains("Retry-After")); - } - - private static string ComputeSignature(string secret, string payload) - { - using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); - var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); - return "sha256=" + Convert.ToHexString(hash).ToLowerInvariant(); - } -} +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; + +namespace StellaOps.Scheduler.WebService.Tests; + +public sealed class EventWebhookEndpointTests : IClassFixture> +{ + static EventWebhookEndpointTests() + { + Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Conselier__HmacSecret", ConselierSecret); + Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Conselier__Enabled", "true"); + Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Excitor__HmacSecret", ExcitorSecret); + Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Excitor__Enabled", "true"); + } + + private const string ConselierSecret = "conselier-secret"; + private const string ExcitorSecret = "excitor-secret"; + + private readonly WebApplicationFactory _factory; + + public EventWebhookEndpointTests(WebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task ConselierWebhook_AcceptsValidSignature() + { + using var client = _factory.CreateClient(); + var payload = new + { + exportId = "conselier-exp-1", + changedProductKeys = new[] { "pkg:rpm/openssl", "pkg:deb/nginx" }, + kev = new[] { "CVE-2024-0001" }, + window = new { from = DateTimeOffset.UtcNow.AddHours(-1), to = DateTimeOffset.UtcNow } + }; + + var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + using var request = new HttpRequestMessage(HttpMethod.Post, "/events/conselier-export") + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(ConselierSecret, json)); + + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + } + + [Fact] + public async Task ConselierWebhook_RejectsInvalidSignature() + { + using var client = _factory.CreateClient(); + var payload = new + { + exportId = "conselier-exp-2", + changedProductKeys = new[] { "pkg:nuget/log4net" } + }; + + var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + using var request = new HttpRequestMessage(HttpMethod.Post, "/events/conselier-export") + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", "sha256=invalid"); + + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task ExcitorWebhook_HonoursRateLimit() + { + using var restrictedFactory = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new Dictionary + { + ["Scheduler:Events:Webhooks:Excitor:RateLimitRequests"] = "1", + ["Scheduler:Events:Webhooks:Excitor:RateLimitWindowSeconds"] = "60" + }); + }); + }); + + using var client = restrictedFactory.CreateClient(); + var payload = new + { + exportId = "excitor-exp-1", + changedClaims = new[] + { + new { productKey = "pkg:deb/openssl", vulnerabilityId = "CVE-2024-1234", status = "affected" } + } + }; + + var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + using var first = new HttpRequestMessage(HttpMethod.Post, "/events/excitor-export") + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + first.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(ExcitorSecret, json)); + var firstResponse = await client.SendAsync(first); + Assert.Equal(HttpStatusCode.Accepted, firstResponse.StatusCode); + + using var second = new HttpRequestMessage(HttpMethod.Post, "/events/excitor-export") + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + second.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(ExcitorSecret, json)); + var secondResponse = await client.SendAsync(second); + Assert.Equal((HttpStatusCode)429, secondResponse.StatusCode); + Assert.True(secondResponse.Headers.Contains("Retry-After")); + } + + private static string ComputeSignature(string secret, string payload) + { + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); + return "sha256=" + Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/SchedulerWebApplicationFactory.cs b/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/SchedulerWebApplicationFactory.cs index aaed847f..df066fe4 100644 --- a/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/SchedulerWebApplicationFactory.cs +++ b/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/SchedulerWebApplicationFactory.cs @@ -1,46 +1,46 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using StellaOps.Scheduler.WebService.Options; - -namespace StellaOps.Scheduler.WebService.Tests; - -public sealed class SchedulerWebApplicationFactory : WebApplicationFactory -{ - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureAppConfiguration((_, configuration) => - { - configuration.AddInMemoryCollection(new[] - { - new KeyValuePair("Scheduler:Authority:Enabled", "false"), - new KeyValuePair("Scheduler:Cartographer:Webhook:Enabled", "false"), - new KeyValuePair("Scheduler:Events:GraphJobs:Enabled", "false"), - new KeyValuePair("Scheduler:Events:Webhooks:Feedser:Enabled", "true"), - new KeyValuePair("Scheduler:Events:Webhooks:Feedser:HmacSecret", "feedser-secret"), - new KeyValuePair("Scheduler:Events:Webhooks:Feedser:RateLimitRequests", "20"), - new KeyValuePair("Scheduler:Events:Webhooks:Feedser:RateLimitWindowSeconds", "60"), - new KeyValuePair("Scheduler:Events:Webhooks:Vexer:Enabled", "true"), - new KeyValuePair("Scheduler:Events:Webhooks:Vexer:HmacSecret", "vexer-secret"), - new KeyValuePair("Scheduler:Events:Webhooks:Vexer:RateLimitRequests", "20"), - new KeyValuePair("Scheduler:Events:Webhooks:Vexer:RateLimitWindowSeconds", "60") - }); - }); - - builder.ConfigureServices(services => - { - services.Configure(options => - { - options.Webhooks ??= new SchedulerInboundWebhooksOptions(); - options.Webhooks.Feedser ??= SchedulerWebhookOptions.CreateDefault("feedser"); - options.Webhooks.Vexer ??= SchedulerWebhookOptions.CreateDefault("vexer"); - options.Webhooks.Feedser.HmacSecret = "feedser-secret"; - options.Webhooks.Feedser.Enabled = true; - options.Webhooks.Vexer.HmacSecret = "vexer-secret"; - options.Webhooks.Vexer.Enabled = true; - }); - }); - } -} +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Scheduler.WebService.Options; + +namespace StellaOps.Scheduler.WebService.Tests; + +public sealed class SchedulerWebApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new[] + { + new KeyValuePair("Scheduler:Authority:Enabled", "false"), + new KeyValuePair("Scheduler:Cartographer:Webhook:Enabled", "false"), + new KeyValuePair("Scheduler:Events:GraphJobs:Enabled", "false"), + new KeyValuePair("Scheduler:Events:Webhooks:Conselier:Enabled", "true"), + new KeyValuePair("Scheduler:Events:Webhooks:Conselier:HmacSecret", "conselier-secret"), + new KeyValuePair("Scheduler:Events:Webhooks:Conselier:RateLimitRequests", "20"), + new KeyValuePair("Scheduler:Events:Webhooks:Conselier:RateLimitWindowSeconds", "60"), + new KeyValuePair("Scheduler:Events:Webhooks:Excitor:Enabled", "true"), + new KeyValuePair("Scheduler:Events:Webhooks:Excitor:HmacSecret", "excitor-secret"), + new KeyValuePair("Scheduler:Events:Webhooks:Excitor:RateLimitRequests", "20"), + new KeyValuePair("Scheduler:Events:Webhooks:Excitor:RateLimitWindowSeconds", "60") + }); + }); + + builder.ConfigureServices(services => + { + services.Configure(options => + { + options.Webhooks ??= new SchedulerInboundWebhooksOptions(); + options.Webhooks.Conselier ??= SchedulerWebhookOptions.CreateDefault("conselier"); + options.Webhooks.Excitor ??= SchedulerWebhookOptions.CreateDefault("excitor"); + options.Webhooks.Conselier.HmacSecret = "conselier-secret"; + options.Webhooks.Conselier.Enabled = true; + options.Webhooks.Excitor.HmacSecret = "excitor-secret"; + options.Webhooks.Excitor.Enabled = true; + }); + }); + } +} diff --git a/src/Scheduler/__Tests/StellaOps.Scheduler.Worker.Tests/PlannerBackgroundServiceTests.cs b/src/Scheduler/__Tests/StellaOps.Scheduler.Worker.Tests/PlannerBackgroundServiceTests.cs index beb0bf6a..4e7b0c0e 100644 --- a/src/Scheduler/__Tests/StellaOps.Scheduler.Worker.Tests/PlannerBackgroundServiceTests.cs +++ b/src/Scheduler/__Tests/StellaOps.Scheduler.Worker.Tests/PlannerBackgroundServiceTests.cs @@ -1,411 +1,411 @@ -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using MongoDB.Driver; -using StellaOps.Scheduler.Queue; -using StellaOps.Scheduler.Storage.Mongo.Projections; -using StellaOps.Scheduler.Storage.Mongo.Repositories; -using StellaOps.Scheduler.Storage.Mongo.Services; -using StellaOps.Scheduler.Worker.Options; -using StellaOps.Scheduler.Worker.Observability; -using StellaOps.Scheduler.Worker.Planning; - -namespace StellaOps.Scheduler.Worker.Tests; - -public sealed class PlannerBackgroundServiceTests -{ - [Fact] - public async Task ExecuteAsync_RespectsTenantFairnessCap() - { - var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T12:00:00Z")); - - var runs = new[] - { - CreateRun("run-a1", "tenant-a", RunTrigger.Manual, timeProvider.GetUtcNow().AddMinutes(1), "schedule-a"), - CreateRun("run-a2", "tenant-a", RunTrigger.Cron, timeProvider.GetUtcNow().AddMinutes(2), "schedule-a"), - CreateRun("run-b1", "tenant-b", RunTrigger.Feedser, timeProvider.GetUtcNow().AddMinutes(3), "schedule-b"), - CreateRun("run-c1", "tenant-c", RunTrigger.Cron, timeProvider.GetUtcNow().AddMinutes(4), "schedule-c"), - }; - - var repository = new TestRunRepository(runs, Array.Empty()); - var options = CreateOptions(maxConcurrentTenants: 2); - var scheduleRepository = new TestScheduleRepository(runs.Select(run => CreateSchedule(run.ScheduleId!, run.TenantId, timeProvider.GetUtcNow()))); - var snapshotRepository = new StubImpactSnapshotRepository(); - var runSummaryService = new StubRunSummaryService(timeProvider); - var plannerQueue = new RecordingPlannerQueue(); - var targetingService = new StubImpactTargetingService(timeProvider); - - using var metrics = new SchedulerWorkerMetrics(); - var executionService = new PlannerExecutionService( - scheduleRepository, - repository, - snapshotRepository, - runSummaryService, - targetingService, - plannerQueue, - options, - timeProvider, - metrics, - NullLogger.Instance); - - var service = new PlannerBackgroundService( - repository, - executionService, - options, - timeProvider, - NullLogger.Instance); - - await service.StartAsync(CancellationToken.None); - try - { - await WaitForConditionAsync(() => repository.UpdateCount >= 2); - } - finally - { - await service.StopAsync(CancellationToken.None); - } - - var processedIds = repository.UpdatedRuns.Select(run => run.Id).ToArray(); - Assert.Equal(new[] { "run-a1", "run-b1" }, processedIds); - } - - [Fact] - public async Task ExecuteAsync_PrioritizesManualAndEventTriggers() - { - var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T18:00:00Z")); - - var runs = new[] - { - CreateRun("run-cron", "tenant-alpha", RunTrigger.Cron, timeProvider.GetUtcNow().AddMinutes(1), "schedule-cron"), - CreateRun("run-feedser", "tenant-bravo", RunTrigger.Feedser, timeProvider.GetUtcNow().AddMinutes(2), "schedule-feedser"), - CreateRun("run-manual", "tenant-charlie", RunTrigger.Manual, timeProvider.GetUtcNow().AddMinutes(3), "schedule-manual"), - CreateRun("run-vexer", "tenant-delta", RunTrigger.Vexer, timeProvider.GetUtcNow().AddMinutes(4), "schedule-vexer"), - }; - - var repository = new TestRunRepository(runs, Array.Empty()); - var options = CreateOptions(maxConcurrentTenants: 4); - var scheduleRepository = new TestScheduleRepository(runs.Select(run => CreateSchedule(run.ScheduleId!, run.TenantId, timeProvider.GetUtcNow()))); - var snapshotRepository = new StubImpactSnapshotRepository(); - var runSummaryService = new StubRunSummaryService(timeProvider); - var plannerQueue = new RecordingPlannerQueue(); - var targetingService = new StubImpactTargetingService(timeProvider); - - using var metrics = new SchedulerWorkerMetrics(); - var executionService = new PlannerExecutionService( - scheduleRepository, - repository, - snapshotRepository, - runSummaryService, - targetingService, - plannerQueue, - options, - timeProvider, - metrics, - NullLogger.Instance); - - var service = new PlannerBackgroundService( - repository, - executionService, - options, - timeProvider, - NullLogger.Instance); - - await service.StartAsync(CancellationToken.None); - try - { - await WaitForConditionAsync(() => repository.UpdateCount >= runs.Length); - } - finally - { - await service.StopAsync(CancellationToken.None); - } - - var processedIds = repository.UpdatedRuns.Select(run => run.Id).ToArray(); - Assert.Equal(new[] { "run-manual", "run-feedser", "run-vexer", "run-cron" }, processedIds); - } - - private static SchedulerWorkerOptions CreateOptions(int maxConcurrentTenants) - { - return new SchedulerWorkerOptions - { - Planner = - { - BatchSize = 20, - PollInterval = TimeSpan.FromMilliseconds(1), - IdleDelay = TimeSpan.FromMilliseconds(1), - MaxConcurrentTenants = maxConcurrentTenants, - MaxRunsPerMinute = int.MaxValue, - QueueLeaseDuration = TimeSpan.FromMinutes(5) - } - }; - } - - private static Run CreateRun( - string id, - string tenantId, - RunTrigger trigger, - DateTimeOffset createdAt, - string scheduleId) - => new( - id: id, - tenantId: tenantId, - trigger: trigger, - state: RunState.Planning, - stats: RunStats.Empty, - createdAt: createdAt, - reason: RunReason.Empty, - scheduleId: scheduleId); - - private static Schedule CreateSchedule(string scheduleId, string tenantId, DateTimeOffset now) - => new( - id: scheduleId, - tenantId: tenantId, - name: $"Schedule-{scheduleId}", - enabled: true, - cronExpression: "0 2 * * *", - timezone: "UTC", - mode: ScheduleMode.AnalysisOnly, - selection: new Selector(SelectorScope.AllImages, tenantId), - onlyIf: ScheduleOnlyIf.Default, - notify: ScheduleNotify.Default, - limits: ScheduleLimits.Default, - createdAt: now, - createdBy: "system", - updatedAt: now, - updatedBy: "system", - subscribers: ImmutableArray.Empty); - - private static async Task WaitForConditionAsync(Func predicate, TimeSpan? timeout = null) - { - var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(1)); - while (!predicate()) - { - if (DateTime.UtcNow > deadline) - { - throw new TimeoutException("Planner background service did not reach expected state within the allotted time."); - } - - await Task.Delay(10); - } - } - - private sealed class TestRunRepository : IRunRepository - { - private readonly Queue> _responses; - private readonly ConcurrentQueue _updates = new(); - private int _updateCount; - - public TestRunRepository(params IReadOnlyList[] responses) - { - if (responses is null) - { - throw new ArgumentNullException(nameof(responses)); - } - - _responses = new Queue>(responses.Select(static runs => (IReadOnlyList)runs.ToArray())); - } - - public int UpdateCount => Volatile.Read(ref _updateCount); - - public IReadOnlyList UpdatedRuns => _updates.ToArray(); - - public Task InsertAsync(Run run, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - => throw new NotSupportedException(); - - public Task UpdateAsync(Run run, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - _updates.Enqueue(run); - Interlocked.Increment(ref _updateCount); - return Task.FromResult(true); - } - - public Task GetAsync(string tenantId, string runId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - => Task.FromResult(null); - - public Task> ListAsync(string tenantId, RunQueryOptions? options = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - => Task.FromResult>(Array.Empty()); - - public Task> ListByStateAsync(RunState state, int limit = 50, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - if (state != RunState.Planning) - { - return Task.FromResult>(Array.Empty()); - } - - var next = _responses.Count > 0 ? _responses.Dequeue() : Array.Empty(); - - if (next.Count > limit) - { - next = next.Take(limit).ToArray(); - } - - return Task.FromResult(next); - } - } - - private sealed class TestScheduleRepository : IScheduleRepository - { - public TestScheduleRepository(IEnumerable schedules) - { - ArgumentNullException.ThrowIfNull(schedules); - - _schedules = new Dictionary<(string TenantId, string ScheduleId), Schedule>(); - foreach (var schedule in schedules) - { - if (schedule is null) - { - continue; - } - - _schedules[(schedule.TenantId, schedule.Id)] = schedule; - } - } - - private readonly Dictionary<(string TenantId, string ScheduleId), Schedule> _schedules; - - public Task UpsertAsync(Schedule schedule, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - _schedules[(schedule.TenantId, schedule.Id)] = schedule; - return Task.CompletedTask; - } - - public Task GetAsync(string tenantId, string scheduleId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - _schedules.TryGetValue((tenantId, scheduleId), out var schedule); - return Task.FromResult(schedule); - } - - public Task> ListAsync(string tenantId, ScheduleQueryOptions? options = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - var results = _schedules.Values.Where(schedule => schedule.TenantId == tenantId).ToArray(); - return Task.FromResult>(results); - } - - public Task SoftDeleteAsync(string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - var removed = _schedules.Remove((tenantId, scheduleId)); - return Task.FromResult(removed); - } - } - - private sealed class StubImpactSnapshotRepository : IImpactSnapshotRepository - { - public ImpactSet? LastSnapshot { get; private set; } - - public Task UpsertAsync(ImpactSet snapshot, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - { - LastSnapshot = snapshot; - return Task.CompletedTask; - } - - public Task GetBySnapshotIdAsync(string snapshotId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - => Task.FromResult(null); - - public Task GetLatestBySelectorAsync(Selector selector, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) - => Task.FromResult(null); - } - - private sealed class StubRunSummaryService : IRunSummaryService - { - private readonly TimeProvider _timeProvider; - - public StubRunSummaryService(TimeProvider timeProvider) - { - _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); - } - - public Task ProjectAsync(Run run, CancellationToken cancellationToken = default) - { - var projection = new RunSummaryProjection( - run.TenantId, - run.ScheduleId ?? string.Empty, - _timeProvider.GetUtcNow(), - null, - ImmutableArray.Empty, - new RunSummaryCounters(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); - - return Task.FromResult(projection); - } - - public Task GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default) - => Task.FromResult(null); - - public Task> ListAsync(string tenantId, CancellationToken cancellationToken = default) - => Task.FromResult>(Array.Empty()); - } - - private sealed class StubImpactTargetingService : IImpactTargetingService - { - private static readonly string DefaultDigest = "sha256:" + new string('a', 64); - private readonly TimeProvider _timeProvider; - - public StubImpactTargetingService(TimeProvider timeProvider) - { - _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); - } - - public ValueTask ResolveByPurlsAsync(IEnumerable productKeys, bool usageOnly, Selector selector, CancellationToken cancellationToken = default) - => throw new NotSupportedException(); - - public ValueTask ResolveByVulnerabilitiesAsync(IEnumerable vulnerabilityIds, bool usageOnly, Selector selector, CancellationToken cancellationToken = default) - => throw new NotSupportedException(); - - public ValueTask ResolveAllAsync(Selector selector, bool usageOnly, CancellationToken cancellationToken = default) - { - var image = new ImpactImage( - DefaultDigest, - registry: "registry.test", - repository: "repo/sample", - namespaces: new[] { selector.TenantId ?? "unknown" }, - tags: new[] { "latest" }, - usedByEntrypoint: true); - - var impactSet = new ImpactSet( - selector, - ImmutableArray.Create(image), - usageOnly, - _timeProvider.GetUtcNow(), - total: 1, - snapshotId: null, - schemaVersion: SchedulerSchemaVersions.ImpactSet); - - return ValueTask.FromResult(impactSet); - } - } - - private sealed class RecordingPlannerQueue : ISchedulerPlannerQueue - { - private readonly ConcurrentQueue _messages = new(); - - public IReadOnlyList Messages => _messages.ToArray(); - - public ValueTask EnqueueAsync(PlannerQueueMessage message, CancellationToken cancellationToken = default) - { - _messages.Enqueue(message); - return ValueTask.FromResult(new SchedulerQueueEnqueueResult(message.Run.Id, Deduplicated: false)); - } - - public ValueTask>> LeaseAsync(SchedulerQueueLeaseRequest request, CancellationToken cancellationToken = default) - => ValueTask.FromResult>>(Array.Empty>()); - - public ValueTask>> ClaimExpiredAsync(SchedulerQueueClaimOptions options, CancellationToken cancellationToken = default) - => ValueTask.FromResult>>(Array.Empty>()); - } - - private sealed class TestTimeProvider : TimeProvider - { - private DateTimeOffset _now; - - public TestTimeProvider(DateTimeOffset initial) - { - _now = initial; - } - - public override DateTimeOffset GetUtcNow() => _now; - - public void Advance(TimeSpan delta) => _now = _now.Add(delta); - } -} +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using MongoDB.Driver; +using StellaOps.Scheduler.Queue; +using StellaOps.Scheduler.Storage.Mongo.Projections; +using StellaOps.Scheduler.Storage.Mongo.Repositories; +using StellaOps.Scheduler.Storage.Mongo.Services; +using StellaOps.Scheduler.Worker.Options; +using StellaOps.Scheduler.Worker.Observability; +using StellaOps.Scheduler.Worker.Planning; + +namespace StellaOps.Scheduler.Worker.Tests; + +public sealed class PlannerBackgroundServiceTests +{ + [Fact] + public async Task ExecuteAsync_RespectsTenantFairnessCap() + { + var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T12:00:00Z")); + + var runs = new[] + { + CreateRun("run-a1", "tenant-a", RunTrigger.Manual, timeProvider.GetUtcNow().AddMinutes(1), "schedule-a"), + CreateRun("run-a2", "tenant-a", RunTrigger.Cron, timeProvider.GetUtcNow().AddMinutes(2), "schedule-a"), + CreateRun("run-b1", "tenant-b", RunTrigger.Conselier, timeProvider.GetUtcNow().AddMinutes(3), "schedule-b"), + CreateRun("run-c1", "tenant-c", RunTrigger.Cron, timeProvider.GetUtcNow().AddMinutes(4), "schedule-c"), + }; + + var repository = new TestRunRepository(runs, Array.Empty()); + var options = CreateOptions(maxConcurrentTenants: 2); + var scheduleRepository = new TestScheduleRepository(runs.Select(run => CreateSchedule(run.ScheduleId!, run.TenantId, timeProvider.GetUtcNow()))); + var snapshotRepository = new StubImpactSnapshotRepository(); + var runSummaryService = new StubRunSummaryService(timeProvider); + var plannerQueue = new RecordingPlannerQueue(); + var targetingService = new StubImpactTargetingService(timeProvider); + + using var metrics = new SchedulerWorkerMetrics(); + var executionService = new PlannerExecutionService( + scheduleRepository, + repository, + snapshotRepository, + runSummaryService, + targetingService, + plannerQueue, + options, + timeProvider, + metrics, + NullLogger.Instance); + + var service = new PlannerBackgroundService( + repository, + executionService, + options, + timeProvider, + NullLogger.Instance); + + await service.StartAsync(CancellationToken.None); + try + { + await WaitForConditionAsync(() => repository.UpdateCount >= 2); + } + finally + { + await service.StopAsync(CancellationToken.None); + } + + var processedIds = repository.UpdatedRuns.Select(run => run.Id).ToArray(); + Assert.Equal(new[] { "run-a1", "run-b1" }, processedIds); + } + + [Fact] + public async Task ExecuteAsync_PrioritizesManualAndEventTriggers() + { + var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T18:00:00Z")); + + var runs = new[] + { + CreateRun("run-cron", "tenant-alpha", RunTrigger.Cron, timeProvider.GetUtcNow().AddMinutes(1), "schedule-cron"), + CreateRun("run-conselier", "tenant-bravo", RunTrigger.Conselier, timeProvider.GetUtcNow().AddMinutes(2), "schedule-conselier"), + CreateRun("run-manual", "tenant-charlie", RunTrigger.Manual, timeProvider.GetUtcNow().AddMinutes(3), "schedule-manual"), + CreateRun("run-excitor", "tenant-delta", RunTrigger.Excitor, timeProvider.GetUtcNow().AddMinutes(4), "schedule-excitor"), + }; + + var repository = new TestRunRepository(runs, Array.Empty()); + var options = CreateOptions(maxConcurrentTenants: 4); + var scheduleRepository = new TestScheduleRepository(runs.Select(run => CreateSchedule(run.ScheduleId!, run.TenantId, timeProvider.GetUtcNow()))); + var snapshotRepository = new StubImpactSnapshotRepository(); + var runSummaryService = new StubRunSummaryService(timeProvider); + var plannerQueue = new RecordingPlannerQueue(); + var targetingService = new StubImpactTargetingService(timeProvider); + + using var metrics = new SchedulerWorkerMetrics(); + var executionService = new PlannerExecutionService( + scheduleRepository, + repository, + snapshotRepository, + runSummaryService, + targetingService, + plannerQueue, + options, + timeProvider, + metrics, + NullLogger.Instance); + + var service = new PlannerBackgroundService( + repository, + executionService, + options, + timeProvider, + NullLogger.Instance); + + await service.StartAsync(CancellationToken.None); + try + { + await WaitForConditionAsync(() => repository.UpdateCount >= runs.Length); + } + finally + { + await service.StopAsync(CancellationToken.None); + } + + var processedIds = repository.UpdatedRuns.Select(run => run.Id).ToArray(); + Assert.Equal(new[] { "run-manual", "run-conselier", "run-excitor", "run-cron" }, processedIds); + } + + private static SchedulerWorkerOptions CreateOptions(int maxConcurrentTenants) + { + return new SchedulerWorkerOptions + { + Planner = + { + BatchSize = 20, + PollInterval = TimeSpan.FromMilliseconds(1), + IdleDelay = TimeSpan.FromMilliseconds(1), + MaxConcurrentTenants = maxConcurrentTenants, + MaxRunsPerMinute = int.MaxValue, + QueueLeaseDuration = TimeSpan.FromMinutes(5) + } + }; + } + + private static Run CreateRun( + string id, + string tenantId, + RunTrigger trigger, + DateTimeOffset createdAt, + string scheduleId) + => new( + id: id, + tenantId: tenantId, + trigger: trigger, + state: RunState.Planning, + stats: RunStats.Empty, + createdAt: createdAt, + reason: RunReason.Empty, + scheduleId: scheduleId); + + private static Schedule CreateSchedule(string scheduleId, string tenantId, DateTimeOffset now) + => new( + id: scheduleId, + tenantId: tenantId, + name: $"Schedule-{scheduleId}", + enabled: true, + cronExpression: "0 2 * * *", + timezone: "UTC", + mode: ScheduleMode.AnalysisOnly, + selection: new Selector(SelectorScope.AllImages, tenantId), + onlyIf: ScheduleOnlyIf.Default, + notify: ScheduleNotify.Default, + limits: ScheduleLimits.Default, + createdAt: now, + createdBy: "system", + updatedAt: now, + updatedBy: "system", + subscribers: ImmutableArray.Empty); + + private static async Task WaitForConditionAsync(Func predicate, TimeSpan? timeout = null) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(1)); + while (!predicate()) + { + if (DateTime.UtcNow > deadline) + { + throw new TimeoutException("Planner background service did not reach expected state within the allotted time."); + } + + await Task.Delay(10); + } + } + + private sealed class TestRunRepository : IRunRepository + { + private readonly Queue> _responses; + private readonly ConcurrentQueue _updates = new(); + private int _updateCount; + + public TestRunRepository(params IReadOnlyList[] responses) + { + if (responses is null) + { + throw new ArgumentNullException(nameof(responses)); + } + + _responses = new Queue>(responses.Select(static runs => (IReadOnlyList)runs.ToArray())); + } + + public int UpdateCount => Volatile.Read(ref _updateCount); + + public IReadOnlyList UpdatedRuns => _updates.ToArray(); + + public Task InsertAsync(Run run, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task UpdateAsync(Run run, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + _updates.Enqueue(run); + Interlocked.Increment(ref _updateCount); + return Task.FromResult(true); + } + + public Task GetAsync(string tenantId, string runId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + => Task.FromResult(null); + + public Task> ListAsync(string tenantId, RunQueryOptions? options = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + => Task.FromResult>(Array.Empty()); + + public Task> ListByStateAsync(RunState state, int limit = 50, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + if (state != RunState.Planning) + { + return Task.FromResult>(Array.Empty()); + } + + var next = _responses.Count > 0 ? _responses.Dequeue() : Array.Empty(); + + if (next.Count > limit) + { + next = next.Take(limit).ToArray(); + } + + return Task.FromResult(next); + } + } + + private sealed class TestScheduleRepository : IScheduleRepository + { + public TestScheduleRepository(IEnumerable schedules) + { + ArgumentNullException.ThrowIfNull(schedules); + + _schedules = new Dictionary<(string TenantId, string ScheduleId), Schedule>(); + foreach (var schedule in schedules) + { + if (schedule is null) + { + continue; + } + + _schedules[(schedule.TenantId, schedule.Id)] = schedule; + } + } + + private readonly Dictionary<(string TenantId, string ScheduleId), Schedule> _schedules; + + public Task UpsertAsync(Schedule schedule, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + _schedules[(schedule.TenantId, schedule.Id)] = schedule; + return Task.CompletedTask; + } + + public Task GetAsync(string tenantId, string scheduleId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + _schedules.TryGetValue((tenantId, scheduleId), out var schedule); + return Task.FromResult(schedule); + } + + public Task> ListAsync(string tenantId, ScheduleQueryOptions? options = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + var results = _schedules.Values.Where(schedule => schedule.TenantId == tenantId).ToArray(); + return Task.FromResult>(results); + } + + public Task SoftDeleteAsync(string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + var removed = _schedules.Remove((tenantId, scheduleId)); + return Task.FromResult(removed); + } + } + + private sealed class StubImpactSnapshotRepository : IImpactSnapshotRepository + { + public ImpactSet? LastSnapshot { get; private set; } + + public Task UpsertAsync(ImpactSet snapshot, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + { + LastSnapshot = snapshot; + return Task.CompletedTask; + } + + public Task GetBySnapshotIdAsync(string snapshotId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + => Task.FromResult(null); + + public Task GetLatestBySelectorAsync(Selector selector, IClientSessionHandle? session = null, CancellationToken cancellationToken = default) + => Task.FromResult(null); + } + + private sealed class StubRunSummaryService : IRunSummaryService + { + private readonly TimeProvider _timeProvider; + + public StubRunSummaryService(TimeProvider timeProvider) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } + + public Task ProjectAsync(Run run, CancellationToken cancellationToken = default) + { + var projection = new RunSummaryProjection( + run.TenantId, + run.ScheduleId ?? string.Empty, + _timeProvider.GetUtcNow(), + null, + ImmutableArray.Empty, + new RunSummaryCounters(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + + return Task.FromResult(projection); + } + + public Task GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default) + => Task.FromResult(null); + + public Task> ListAsync(string tenantId, CancellationToken cancellationToken = default) + => Task.FromResult>(Array.Empty()); + } + + private sealed class StubImpactTargetingService : IImpactTargetingService + { + private static readonly string DefaultDigest = "sha256:" + new string('a', 64); + private readonly TimeProvider _timeProvider; + + public StubImpactTargetingService(TimeProvider timeProvider) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } + + public ValueTask ResolveByPurlsAsync(IEnumerable productKeys, bool usageOnly, Selector selector, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public ValueTask ResolveByVulnerabilitiesAsync(IEnumerable vulnerabilityIds, bool usageOnly, Selector selector, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public ValueTask ResolveAllAsync(Selector selector, bool usageOnly, CancellationToken cancellationToken = default) + { + var image = new ImpactImage( + DefaultDigest, + registry: "registry.test", + repository: "repo/sample", + namespaces: new[] { selector.TenantId ?? "unknown" }, + tags: new[] { "latest" }, + usedByEntrypoint: true); + + var impactSet = new ImpactSet( + selector, + ImmutableArray.Create(image), + usageOnly, + _timeProvider.GetUtcNow(), + total: 1, + snapshotId: null, + schemaVersion: SchedulerSchemaVersions.ImpactSet); + + return ValueTask.FromResult(impactSet); + } + } + + private sealed class RecordingPlannerQueue : ISchedulerPlannerQueue + { + private readonly ConcurrentQueue _messages = new(); + + public IReadOnlyList Messages => _messages.ToArray(); + + public ValueTask EnqueueAsync(PlannerQueueMessage message, CancellationToken cancellationToken = default) + { + _messages.Enqueue(message); + return ValueTask.FromResult(new SchedulerQueueEnqueueResult(message.Run.Id, Deduplicated: false)); + } + + public ValueTask>> LeaseAsync(SchedulerQueueLeaseRequest request, CancellationToken cancellationToken = default) + => ValueTask.FromResult>>(Array.Empty>()); + + public ValueTask>> ClaimExpiredAsync(SchedulerQueueClaimOptions options, CancellationToken cancellationToken = default) + => ValueTask.FromResult>>(Array.Empty>()); + } + + private sealed class TestTimeProvider : TimeProvider + { + private DateTimeOffset _now; + + public TestTimeProvider(DateTimeOffset initial) + { + _now = initial; + } + + public override DateTimeOffset GetUtcNow() => _now; + + public void Advance(TimeSpan delta) => _now = _now.Add(delta); + } +} diff --git a/src/StellaOps.sln b/src/StellaOps.sln index 9d6a713d..3c7356e7 100644 --- a/src/StellaOps.sln +++ b/src/StellaOps.sln @@ -1,2706 +1,2931 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{361838C4-72E2-1C48-5D76-CA6D1A861242}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client", "StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj", "{5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "StellaOps.Configuration\StellaOps.Configuration.csproj", "{8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{46D35B4F-6A04-47FF-958B-5E6A73FCC059}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{44A1241B-8ECF-4AFA-9972-452C39AD43D6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority", "StellaOps.Authority\StellaOps.Authority\StellaOps.Authority.csproj", "{85AB3BB7-C493-4387-B39A-EB299AC37312}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugin.Standard", "StellaOps.Authority\StellaOps.Authority.Plugin.Standard\StellaOps.Authority.Plugin.Standard.csproj", "{93DB06DC-B254-48A9-8F2C-6130A5658F27}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "StellaOps.Plugin\StellaOps.Plugin.csproj", "{03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin.Tests", "StellaOps.Plugin.Tests\StellaOps.Plugin.Tests.csproj", "{C6DC3C29-C2AD-4015-8872-42E95A0FE63F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cli", "StellaOps.Cli\StellaOps.Cli.csproj", "{40094279-250C-42AE-992A-856718FEFBAC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cli.Tests", "StellaOps.Cli.Tests\StellaOps.Cli.Tests.csproj", "{B2967228-F8F7-4931-B257-1C63CB58CE1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{37F203A3-624E-4794-9C99-16CAC22C17DF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo", "StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj", "{3FF93987-A30A-4D50-8815-7CF3BB7CAE05}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{AACE8717-0760-42F2-A225-8FCCE876FB65}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core.Tests", "StellaOps.Concelier.Core.Tests\StellaOps.Concelier.Core.Tests.csproj", "{FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.Json", "StellaOps.Concelier.Exporter.Json\StellaOps.Concelier.Exporter.Json.csproj", "{D0FB54BA-4D14-4A32-B09F-7EC94F369460}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.Json.Tests", "StellaOps.Concelier.Exporter.Json.Tests\StellaOps.Concelier.Exporter.Json.Tests.csproj", "{69C9E010-CBDD-4B89-84CF-7AB56D6A078A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.TrivyDb", "StellaOps.Concelier.Exporter.TrivyDb\StellaOps.Concelier.Exporter.TrivyDb.csproj", "{E471176A-E1F3-4DE5-8D30-0865903A217A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.TrivyDb.Tests", "StellaOps.Concelier.Exporter.TrivyDb.Tests\StellaOps.Concelier.Exporter.TrivyDb.Tests.csproj", "{FA013511-DF20-45F7-8077-EBA2D6224D64}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge", "StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj", "{B9F84697-54FE-4648-B173-EE3D904FFA4D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge.Tests", "StellaOps.Concelier.Merge.Tests\StellaOps.Concelier.Merge.Tests.csproj", "{6751A76C-8ED8-40F4-AE2B-069DB31395FE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models.Tests", "StellaOps.Concelier.Models.Tests\StellaOps.Concelier.Models.Tests.csproj", "{DDBFA2EF-9CAE-473F-A438-369CAC25C66A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization.Tests", "StellaOps.Concelier.Normalization.Tests\StellaOps.Concelier.Normalization.Tests.csproj", "{063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Acsc", "StellaOps.Concelier.Connector.Acsc\StellaOps.Concelier.Connector.Acsc.csproj", "{35350FAB-FC51-4FE8-81FB-011003134C37}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Cccs", "StellaOps.Concelier.Connector.Cccs\StellaOps.Concelier.Connector.Cccs.csproj", "{1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertBund", "StellaOps.Concelier.Connector.CertBund\StellaOps.Concelier.Connector.CertBund.csproj", "{C4A65377-22F7-4D15-92A3-4F05847D167E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertCc", "StellaOps.Concelier.Connector.CertCc\StellaOps.Concelier.Connector.CertCc.csproj", "{BDDE59E1-C643-4C87-8608-0F9A7A54DE09}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertFr", "StellaOps.Concelier.Connector.CertFr\StellaOps.Concelier.Connector.CertFr.csproj", "{0CC116C8-A7E5-4B94-9688-32920177FF97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertFr.Tests", "StellaOps.Concelier.Connector.CertFr.Tests\StellaOps.Concelier.Connector.CertFr.Tests.csproj", "{E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertIn", "StellaOps.Concelier.Connector.CertIn\StellaOps.Concelier.Connector.CertIn.csproj", "{84DEDF05-A5BD-4644-86B9-6B7918FE3F31}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertIn.Tests", "StellaOps.Concelier.Connector.CertIn.Tests\StellaOps.Concelier.Connector.CertIn.Tests.csproj", "{9DEB1F54-94B5-40C4-AC44-220E680B016D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common.Tests", "StellaOps.Concelier.Connector.Common.Tests\StellaOps.Concelier.Connector.Common.Tests.csproj", "{7C3E87F2-93D8-4968-95E3-52C46947D46C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Cve", "StellaOps.Concelier.Connector.Cve\StellaOps.Concelier.Connector.Cve.csproj", "{C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Debian", "StellaOps.Concelier.Connector.Distro.Debian\StellaOps.Concelier.Connector.Distro.Debian.csproj", "{31B05493-104F-437F-9FA7-CA5286CE697C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Debian.Tests", "StellaOps.Concelier.Connector.Distro.Debian.Tests\StellaOps.Concelier.Connector.Distro.Debian.Tests.csproj", "{937AF12E-D770-4534-8FF8-C59042609C2A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.RedHat", "StellaOps.Concelier.Connector.Distro.RedHat\StellaOps.Concelier.Connector.Distro.RedHat.csproj", "{5A028B04-9D76-470B-B5B3-766CE4CE860C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.RedHat.Tests", "StellaOps.Concelier.Connector.Distro.RedHat.Tests\StellaOps.Concelier.Connector.Distro.RedHat.Tests.csproj", "{749DE4C8-F733-43F8-B2A8-6649E71C7570}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Suse", "StellaOps.Concelier.Connector.Distro.Suse\StellaOps.Concelier.Connector.Distro.Suse.csproj", "{56D2C79E-2737-4FF9-9D19-150065F568D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Suse.Tests", "StellaOps.Concelier.Connector.Distro.Suse.Tests\StellaOps.Concelier.Connector.Distro.Suse.Tests.csproj", "{E41F6DC4-68B5-4EE3-97AE-801D725A2C13}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Ubuntu", "StellaOps.Concelier.Connector.Distro.Ubuntu\StellaOps.Concelier.Connector.Distro.Ubuntu.csproj", "{285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Ubuntu.Tests", "StellaOps.Concelier.Connector.Distro.Ubuntu.Tests\StellaOps.Concelier.Connector.Distro.Ubuntu.Tests.csproj", "{26055403-C7F5-4709-8813-0F7387102791}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ghsa", "StellaOps.Concelier.Connector.Ghsa\StellaOps.Concelier.Connector.Ghsa.csproj", "{0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ics.Cisa", "StellaOps.Concelier.Connector.Ics.Cisa\StellaOps.Concelier.Connector.Ics.Cisa.csproj", "{258327E9-431E-475C-933B-50893676E452}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ics.Kaspersky", "StellaOps.Concelier.Connector.Ics.Kaspersky\StellaOps.Concelier.Connector.Ics.Kaspersky.csproj", "{42AF60C8-A5E1-40E0-86F8-98256364AF6F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ics.Kaspersky.Tests", "StellaOps.Concelier.Connector.Ics.Kaspersky.Tests\StellaOps.Concelier.Connector.Ics.Kaspersky.Tests.csproj", "{88C6A9C3-B433-4C36-8767-429C8C2396F8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Jvn", "StellaOps.Concelier.Connector.Jvn\StellaOps.Concelier.Connector.Jvn.csproj", "{6B7099AB-01BF-4EC4-87D0-5C9C032266DE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Jvn.Tests", "StellaOps.Concelier.Connector.Jvn.Tests\StellaOps.Concelier.Connector.Jvn.Tests.csproj", "{14C918EA-693E-41FE-ACAE-2E82DF077BEA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Kev", "StellaOps.Concelier.Connector.Kev\StellaOps.Concelier.Connector.Kev.csproj", "{81111B26-74F6-4912-9084-7115FD119945}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Kisa", "StellaOps.Concelier.Connector.Kisa\StellaOps.Concelier.Connector.Kisa.csproj", "{80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Nvd", "StellaOps.Concelier.Connector.Nvd\StellaOps.Concelier.Connector.Nvd.csproj", "{8D0F501D-01B1-4E24-958B-FAF35B267705}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Nvd.Tests", "StellaOps.Concelier.Connector.Nvd.Tests\StellaOps.Concelier.Connector.Nvd.Tests.csproj", "{5BA91095-7F10-4717-B296-49DFBFC1C9C2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Osv", "StellaOps.Concelier.Connector.Osv\StellaOps.Concelier.Connector.Osv.csproj", "{99616566-4EF1-4DC7-B655-825FE43D203D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Osv.Tests", "StellaOps.Concelier.Connector.Osv.Tests\StellaOps.Concelier.Connector.Osv.Tests.csproj", "{EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ru.Bdu", "StellaOps.Concelier.Connector.Ru.Bdu\StellaOps.Concelier.Connector.Ru.Bdu.csproj", "{A3B19095-2D95-4B09-B07E-2C082C72394B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ru.Nkcki", "StellaOps.Concelier.Connector.Ru.Nkcki\StellaOps.Concelier.Connector.Ru.Nkcki.csproj", "{807837AF-B392-4589-ADF1-3FDB34D6C5BF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Adobe", "StellaOps.Concelier.Connector.Vndr.Adobe\StellaOps.Concelier.Connector.Vndr.Adobe.csproj", "{64EAFDCF-8283-4D5C-AC78-7969D5FE926A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Adobe.Tests", "StellaOps.Concelier.Connector.Vndr.Adobe.Tests\StellaOps.Concelier.Connector.Vndr.Adobe.Tests.csproj", "{68F4D8A1-E32F-487A-B460-325F36989BE3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Apple", "StellaOps.Concelier.Connector.Vndr.Apple\StellaOps.Concelier.Connector.Vndr.Apple.csproj", "{4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Chromium", "StellaOps.Concelier.Connector.Vndr.Chromium\StellaOps.Concelier.Connector.Vndr.Chromium.csproj", "{606C751B-7CF1-47CF-A25C-9248A55C814F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Chromium.Tests", "StellaOps.Concelier.Connector.Vndr.Chromium.Tests\StellaOps.Concelier.Connector.Vndr.Chromium.Tests.csproj", "{0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Cisco", "StellaOps.Concelier.Connector.Vndr.Cisco\StellaOps.Concelier.Connector.Vndr.Cisco.csproj", "{CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Cisco.Tests", "StellaOps.Concelier.Connector.Vndr.Cisco.Tests\StellaOps.Concelier.Connector.Vndr.Cisco.Tests.csproj", "{99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Msrc", "StellaOps.Concelier.Connector.Vndr.Msrc\StellaOps.Concelier.Connector.Vndr.Msrc.csproj", "{5CCE0DB7-C115-4B21-A7AE-C8488C22A853}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Oracle", "StellaOps.Concelier.Connector.Vndr.Oracle\StellaOps.Concelier.Connector.Vndr.Oracle.csproj", "{A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Oracle.Tests", "StellaOps.Concelier.Connector.Vndr.Oracle.Tests\StellaOps.Concelier.Connector.Vndr.Oracle.Tests.csproj", "{06DC817F-A936-4F83-8929-E00622B32245}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Vmware", "StellaOps.Concelier.Connector.Vndr.Vmware\StellaOps.Concelier.Connector.Vndr.Vmware.csproj", "{2C999476-0291-4161-B3E9-1AA99A3B1139}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Vmware.Tests", "StellaOps.Concelier.Connector.Vndr.Vmware.Tests\StellaOps.Concelier.Connector.Vndr.Vmware.Tests.csproj", "{476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo.Tests", "StellaOps.Concelier.Storage.Mongo.Tests\StellaOps.Concelier.Storage.Mongo.Tests.csproj", "{0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.WebService", "StellaOps.Concelier.WebService\StellaOps.Concelier.WebService.csproj", "{0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.WebService.Tests", "StellaOps.Concelier.WebService.Tests\StellaOps.Concelier.WebService.Tests.csproj", "{8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration.Tests", "StellaOps.Configuration.Tests\StellaOps.Configuration.Tests.csproj", "{C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions.Tests", "StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions.Tests\StellaOps.Authority.Plugins.Abstractions.Tests.csproj", "{50140A32-6D3C-47DB-983A-7166CBA51845}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Tests", "StellaOps.Authority\StellaOps.Authority.Tests\StellaOps.Authority.Tests.csproj", "{031979F2-6ABA-444F-A6A4-80115DC487CE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugin.Standard.Tests", "StellaOps.Authority\StellaOps.Authority.Plugin.Standard.Tests\StellaOps.Authority.Plugin.Standard.Tests.csproj", "{D71B0DA5-80A3-419E-898D-40E77A9A7F19}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Storage.Mongo", "StellaOps.Authority\StellaOps.Authority.Storage.Mongo\StellaOps.Authority.Storage.Mongo.csproj", "{B2C877D9-B521-4901-8817-76B5DAA62FCE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions.Tests", "StellaOps.Authority\StellaOps.Auth.Abstractions.Tests\StellaOps.Auth.Abstractions.Tests.csproj", "{08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration.Tests", "StellaOps.Authority\StellaOps.Auth.ServerIntegration.Tests\StellaOps.Auth.ServerIntegration.Tests.csproj", "{7116DD6B-2491-49E1-AB27-5210E949F753}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client.Tests", "StellaOps.Authority\StellaOps.Auth.Client.Tests\StellaOps.Auth.Client.Tests.csproj", "{7DBE31A6-D2FD-499E-B675-4092723175AD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Kev.Tests", "StellaOps.Concelier.Connector.Kev.Tests\StellaOps.Concelier.Connector.Kev.Tests.csproj", "{D99E6EAE-D278-4480-AA67-85F025383E47}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Cve.Tests", "StellaOps.Concelier.Connector.Cve.Tests\StellaOps.Concelier.Connector.Cve.Tests.csproj", "{D3825714-3DDA-44B7-A99C-5F3E65716691}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ghsa.Tests", "StellaOps.Concelier.Connector.Ghsa.Tests\StellaOps.Concelier.Connector.Ghsa.Tests.csproj", "{FAB78D21-7372-48FE-B2C3-DE1807F1157D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{EADFA337-B0FA-4712-A24A-7C08235BDF98}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Tests", "StellaOps.Cryptography.Tests\StellaOps.Cryptography.Tests.csproj", "{110F7EC2-3149-4D1B-A972-E69E79F1EBF5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.DependencyInjection", "StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj", "{B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core", "StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj", "{3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core.Tests", "StellaOps.Excititor.Core.Tests\StellaOps.Excititor.Core.Tests.csproj", "{680CA103-DCE8-4D02-8979-72DEA5BE8C00}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Policy", "StellaOps.Excititor.Policy\StellaOps.Excititor.Policy.csproj", "{7F4B19D4-569A-4CCF-B481-EBE04860451A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Policy.Tests", "StellaOps.Excititor.Policy.Tests\StellaOps.Excititor.Policy.Tests.csproj", "{DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Storage.Mongo", "StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj", "{E380F242-031E-483E-8570-0EF7EA525C4F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Export", "StellaOps.Excititor.Export\StellaOps.Excititor.Export.csproj", "{42582C16-F5A9-417F-9D33-BC489925324F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Export.Tests", "StellaOps.Excititor.Export.Tests\StellaOps.Excititor.Export.Tests.csproj", "{06F40DA8-FEFA-4C2B-907B-155BD92BB859}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.RedHat.CSAF", "StellaOps.Excititor.Connectors.RedHat.CSAF\StellaOps.Excititor.Connectors.RedHat.CSAF.csproj", "{A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.RedHat.CSAF.Tests", "StellaOps.Excititor.Connectors.RedHat.CSAF.Tests\StellaOps.Excititor.Connectors.RedHat.CSAF.Tests.csproj", "{3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Abstractions", "StellaOps.Excititor.Connectors.Abstractions\StellaOps.Excititor.Connectors.Abstractions.csproj", "{F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Worker", "StellaOps.Excititor.Worker\StellaOps.Excititor.Worker.csproj", "{781EC793-1DB0-4E31-95BC-12A2B373045F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Worker.Tests", "StellaOps.Excititor.Worker.Tests\StellaOps.Excititor.Worker.Tests.csproj", "{BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CSAF", "StellaOps.Excititor.Formats.CSAF\StellaOps.Excititor.Formats.CSAF.csproj", "{14E9D043-F0EF-4F68-AE83-D6F579119D9A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CSAF.Tests", "StellaOps.Excititor.Formats.CSAF.Tests\StellaOps.Excititor.Formats.CSAF.Tests.csproj", "{27E94B6E-DEF8-4B89-97CB-424703790ECE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CycloneDX", "StellaOps.Excititor.Formats.CycloneDX\StellaOps.Excititor.Formats.CycloneDX.csproj", "{361E3E23-B215-423D-9906-A84171E20AD3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CycloneDX.Tests", "StellaOps.Excititor.Formats.CycloneDX.Tests\StellaOps.Excititor.Formats.CycloneDX.Tests.csproj", "{7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.OpenVEX", "StellaOps.Excititor.Formats.OpenVEX\StellaOps.Excititor.Formats.OpenVEX.csproj", "{C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.OpenVEX.Tests", "StellaOps.Excititor.Formats.OpenVEX.Tests\StellaOps.Excititor.Formats.OpenVEX.Tests.csproj", "{E86CF4A6-2463-4589-A9D8-9DF557C48367}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Cisco.CSAF", "StellaOps.Excititor.Connectors.Cisco.CSAF\StellaOps.Excititor.Connectors.Cisco.CSAF.csproj", "{B308B94C-E01F-4449-A5A6-CD7A48E52D15}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Cisco.CSAF.Tests", "StellaOps.Excititor.Connectors.Cisco.CSAF.Tests\StellaOps.Excititor.Connectors.Cisco.CSAF.Tests.csproj", "{9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub", "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub\StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.csproj", "{E076DC9C-B436-44BF-B02E-FA565086F805}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests", "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests\StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests.csproj", "{55500025-FE82-4F97-A261-9BAEA4B10845}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.MSRC.CSAF", "StellaOps.Excititor.Connectors.MSRC.CSAF\StellaOps.Excititor.Connectors.MSRC.CSAF.csproj", "{CD12875F-9367-41BD-810C-7FBE76314F17}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.MSRC.CSAF.Tests", "StellaOps.Excititor.Connectors.MSRC.CSAF.Tests\StellaOps.Excititor.Connectors.MSRC.CSAF.Tests.csproj", "{063D3280-9918-465A-AF2D-3650A2A50D03}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Oracle.CSAF", "StellaOps.Excititor.Connectors.Oracle.CSAF\StellaOps.Excititor.Connectors.Oracle.CSAF.csproj", "{A3EEE400-3655-4B34-915A-598E60CD55FB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Oracle.CSAF.Tests", "StellaOps.Excititor.Connectors.Oracle.CSAF.Tests\StellaOps.Excititor.Connectors.Oracle.CSAF.Tests.csproj", "{577025AD-2FDD-42DF-BFA2-3FC095B50539}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Ubuntu.CSAF", "StellaOps.Excititor.Connectors.Ubuntu.CSAF\StellaOps.Excititor.Connectors.Ubuntu.CSAF.csproj", "{DD3B2076-E5E0-4533-8D27-7724225D7758}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests", "StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests\StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests.csproj", "{CADA1364-8EB1-479E-AB6F-4105C26335C8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core", "StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj", "{8CC4441E-9D1A-4E00-831B-34828A3F9446}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core.Tests", "StellaOps.Scanner.Core.Tests\StellaOps.Scanner.Core.Tests.csproj", "{01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{BDB24B64-FE4E-C4BD-9F80-9428F98EDF6F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy", "StellaOps.Policy\StellaOps.Policy.csproj", "{37BB9502-CCD1-425A-BF45-D56968B0C2F9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Tests", "StellaOps.Policy.Tests\StellaOps.Policy.Tests.csproj", "{015A7A95-2C07-4C7F-8048-DB591AAC5FE5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService", "StellaOps.Scanner.WebService\StellaOps.Scanner.WebService.csproj", "{EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService.Tests", "StellaOps.Scanner.WebService.Tests\StellaOps.Scanner.WebService.Tests.csproj", "{27D951AD-696D-4330-B4F5-F8F81344C191}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage", "StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj", "{31277AFF-9BFF-4C17-8593-B562A385058E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Tests", "StellaOps.Scanner.Storage.Tests\StellaOps.Scanner.Storage.Tests.csproj", "{3A8F090F-678D-46E2-8899-67402129749C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker", "StellaOps.Scanner.Worker\StellaOps.Scanner.Worker.csproj", "{19FACEC7-D6D4-40F5-84AD-14E2983F18F7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker.Tests", "StellaOps.Scanner.Worker.Tests\StellaOps.Scanner.Worker.Tests.csproj", "{8342286A-BE36-4ACA-87FF-EBEB4E268498}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace", "StellaOps.Scanner.EntryTrace\StellaOps.Scanner.EntryTrace.csproj", "{05D844B6-51C1-4926-919C-D99E24FB3BC9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace.Tests", "StellaOps.Scanner.EntryTrace.Tests\StellaOps.Scanner.EntryTrace.Tests.csproj", "{03E15545-D6A0-4287-A88C-6EDE77C0DCBE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang", "StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj", "{A072C46F-BA45-419E-B1B6-416919F78440}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Tests", "StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj", "{6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff", "StellaOps.Scanner.Diff\StellaOps.Scanner.Diff.csproj", "{10088067-7B8F-4D2E-A8E1-ED546DC17369}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff.Tests", "StellaOps.Scanner.Diff.Tests\StellaOps.Scanner.Diff.Tests.csproj", "{E014565C-2456-4BD0-9481-557F939C1E36}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit", "StellaOps.Scanner.Emit\StellaOps.Scanner.Emit.csproj", "{44825FDA-68D2-4675-8B1D-6D5303DC38CF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit.Tests", "StellaOps.Scanner.Emit.Tests\StellaOps.Scanner.Emit.Tests.csproj", "{6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache", "StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj", "{5E5EB0A7-7A19-4144-81FE-13C31DB678B2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache.Tests", "StellaOps.Scanner.Cache.Tests\StellaOps.Scanner.Cache.Tests.csproj", "{7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java", "StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj", "{B86C287A-734E-4527-A03E-6B970F22E27E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS", "StellaOps.Scanner.Analyzers.OS\StellaOps.Scanner.Analyzers.OS.csproj", "{E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Apk", "StellaOps.Scanner.Analyzers.OS.Apk\StellaOps.Scanner.Analyzers.OS.Apk.csproj", "{50D014B5-99A6-46FC-B745-26687595B293}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Dpkg", "StellaOps.Scanner.Analyzers.OS.Dpkg\StellaOps.Scanner.Analyzers.OS.Dpkg.csproj", "{D99C1F78-67EA-40E7-BD4C-985592F5265A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Rpm", "StellaOps.Scanner.Analyzers.OS.Rpm\StellaOps.Scanner.Analyzers.OS.Rpm.csproj", "{1CBC0B9C-A96B-4143-B70F-37C69229FFF2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Tests", "StellaOps.Scanner.Analyzers.OS.Tests\StellaOps.Scanner.Analyzers.OS.Tests.csproj", "{760E2855-31B3-4CCB-BACB-34B7196A59B8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node", "StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj", "{3F688F21-7E31-4781-8995-9DD34276773F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python", "StellaOps.Scanner.Analyzers.Lang.Python\StellaOps.Scanner.Analyzers.Lang.Python.csproj", "{80AD7C4D-E4C6-4700-87AD-77B5698B338F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go", "StellaOps.Scanner.Analyzers.Lang.Go\StellaOps.Scanner.Analyzers.Lang.Go.csproj", "{60ABAB54-2EE9-4A16-A109-67F7B6F29184}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.DotNet", "StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj", "{D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust", "StellaOps.Scanner.Analyzers.Lang.Rust\StellaOps.Scanner.Analyzers.Lang.Rust.csproj", "{5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Security", "StellaOps.Auth.Security\StellaOps.Auth.Security.csproj", "{05475C0A-C225-4F07-A3C7-9E17E660042E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor", "StellaOps.Attestor", "{78C966F5-2242-D8EC-ADCA-A1A9C7F723A6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Core", "StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj", "{BA47D456-4657-4C86-A665-21293E3AC47F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Infrastructure", "StellaOps.Attestor\StellaOps.Attestor.Infrastructure\StellaOps.Attestor.Infrastructure.csproj", "{49EF86AC-1CC2-4A24-8637-C5151E23DF9D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.WebService", "StellaOps.Attestor\StellaOps.Attestor.WebService\StellaOps.Attestor.WebService.csproj", "{C22333B3-D132-4960-A490-6BEF1EB1C917}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Tests", "StellaOps.Attestor\StellaOps.Attestor.Tests\StellaOps.Attestor.Tests.csproj", "{B8B15A8D-F647-41AE-A55F-A283A47E97C4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Verify", "StellaOps.Attestor\StellaOps.Attestor.Verify\StellaOps.Attestor.Verify.csproj", "{99EC90D8-0D5E-41E4-A895-585A7680916C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Zastava", "StellaOps.Zastava", "{F1F029E6-2E4B-4A42-8D8F-AB325EE3B608}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Core", "StellaOps.Zastava.Core\StellaOps.Zastava.Core.csproj", "{CBE6E3D8-230C-4513-B98F-99D82B83B9F7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Core.Tests", "StellaOps.Zastava.Core.Tests\StellaOps.Zastava.Core.Tests.csproj", "{821C7F88-B775-4D3C-8D89-850B6C34E818}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Webhook", "StellaOps.Zastava.Webhook\StellaOps.Zastava.Webhook.csproj", "{3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Webhook.Tests", "StellaOps.Zastava.Webhook.Tests\StellaOps.Zastava.Webhook.Tests.csproj", "{3C500ECB-5422-4FFB-BD3D-48A850763D31}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cli.Plugins.NonCore", "StellaOps.Cli.Plugins.NonCore\StellaOps.Cli.Plugins.NonCore.csproj", "{D851E54A-5A44-4F74-9FDF-A2C32CACF651}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "StellaOps.Scanner.Analyzers.Lang.Java.Tests\StellaOps.Scanner.Analyzers.Lang.Java.Tests.csproj", "{866807B8-8E68-417C-8148-6450DEA68012}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "StellaOps.Scanner.Analyzers.Lang.Node.Tests\StellaOps.Scanner.Analyzers.Lang.Node.Tests.csproj", "{20BE41BD-9C32-45B5-882A-C01491979633}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "StellaOps.Scanner.Analyzers.Lang.Python.Tests\StellaOps.Scanner.Analyzers.Lang.Python.Tests.csproj", "{9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "StellaOps.Scanner.Analyzers.Lang.Go.Tests\StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj", "{7C3A6012-6FC8-46A9-9966-1AC373614C41}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Observer", "StellaOps.Zastava.Observer\StellaOps.Zastava.Observer.csproj", "{BC38594B-0B84-4657-9F7B-F2A0FC810F04}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Observer.Tests", "StellaOps.Zastava.Observer.Tests\StellaOps.Zastava.Observer.Tests.csproj", "{20E0774F-86D5-4CD0-B636-E5212074FDE8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Engine", "StellaOps.Policy.Engine\StellaOps.Policy.Engine.csproj", "{FE668D8D-AB46-41F4-A82F-8A3330C4D152}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cartographer", "StellaOps.Cartographer\StellaOps.Cartographer.csproj", "{548C296A-476B-433D-9552-923648BDFA97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.SbomService", "StellaOps.SbomService\StellaOps.SbomService.csproj", "{3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scheduler.WebService", "StellaOps.Scheduler.WebService\StellaOps.Scheduler.WebService.csproj", "{C733F161-FCED-4D21-BC83-5CC079E93547}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scheduler.WebService.Tests", "StellaOps.Scheduler.WebService.Tests\StellaOps.Scheduler.WebService.Tests.csproj", "{76E1E74F-41C1-4E24-85EA-ED13F28B80B1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Registry.TokenService", "StellaOps.Registry.TokenService\StellaOps.Registry.TokenService.csproj", "{EC73D558-0472-49E2-B46E-D26F9686AA9C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Registry.TokenService.Tests", "StellaOps.Registry.TokenService.Tests\StellaOps.Registry.TokenService.Tests.csproj", "{1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Bench", "StellaOps.Bench", "{1553F566-661E-A2F5-811B-F74BF45C44CC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PolicyEngine", "PolicyEngine", "{CBDF819E-923F-A07F-78D9-D599DD28197E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Bench.PolicyEngine", "StellaOps.Bench\PolicyEngine\StellaOps.Bench.PolicyEngine\StellaOps.Bench.PolicyEngine.csproj", "{D8B22C17-28E9-4059-97C5-4AC4600A2BD5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "StellaOps.Aoc\StellaOps.Aoc.csproj", "{6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc.AspNetCore", "StellaOps.Aoc.AspNetCore\StellaOps.Aoc.AspNetCore.csproj", "{D3D47993-27D3-4C90-9C8E-14652807DAF5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc.Tests", "StellaOps.Aoc.Tests\StellaOps.Aoc.Tests.csproj", "{4D167781-1AC0-46CF-A32E-1B6E048940B2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc.AspNetCore.Tests", "StellaOps.Aoc.AspNetCore.Tests\StellaOps.Aoc.AspNetCore.Tests.csproj", "{5F9B7682-71E2-4989-9BC9-014A2C26AF50}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{C3AEAEE7-038E-45FF-892B-DB18EE29F790}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels.Tests", "StellaOps.Concelier.RawModels.Tests\StellaOps.Concelier.RawModels.Tests.csproj", "{7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals", "StellaOps.Signals\StellaOps.Signals.csproj", "{1561D597-922F-486E-ACF4-98250DDC5CDA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Tests", "StellaOps.Signals.Tests\StellaOps.Signals.Tests.csproj", "{D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Gateway", "StellaOps.Policy.Gateway\StellaOps.Policy.Gateway.csproj", "{9369FA32-E98A-4180-9251-914925188086}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Gateway.Tests", "StellaOps.Policy.Gateway.Tests\StellaOps.Policy.Gateway.Tests.csproj", "{67650687-2E32-40BB-9849-C4ABBA65A7CF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{41F15E67-7190-CF23-3BC4-77E87134CADD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.IssuerDirectory.Client", "__Libraries\StellaOps.IssuerDirectory.Client\StellaOps.IssuerDirectory.Client.csproj", "{CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x64.ActiveCfg = Debug|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x64.Build.0 = Debug|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x86.ActiveCfg = Debug|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x86.Build.0 = Debug|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|Any CPU.Build.0 = Release|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x64.ActiveCfg = Release|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x64.Build.0 = Release|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x86.ActiveCfg = Release|Any CPU - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x86.Build.0 = Release|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x64.ActiveCfg = Debug|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x64.Build.0 = Debug|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x86.ActiveCfg = Debug|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x86.Build.0 = Debug|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|Any CPU.Build.0 = Release|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x64.ActiveCfg = Release|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x64.Build.0 = Release|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x86.ActiveCfg = Release|Any CPU - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x86.Build.0 = Release|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x64.ActiveCfg = Debug|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x64.Build.0 = Debug|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x86.ActiveCfg = Debug|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x86.Build.0 = Debug|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|Any CPU.Build.0 = Release|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x64.ActiveCfg = Release|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x64.Build.0 = Release|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x86.ActiveCfg = Release|Any CPU - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x86.Build.0 = Release|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|Any CPU.Build.0 = Debug|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x64.ActiveCfg = Debug|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x64.Build.0 = Debug|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x86.ActiveCfg = Debug|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x86.Build.0 = Debug|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|Any CPU.ActiveCfg = Release|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|Any CPU.Build.0 = Release|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x64.ActiveCfg = Release|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x64.Build.0 = Release|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x86.ActiveCfg = Release|Any CPU - {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x86.Build.0 = Release|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x64.ActiveCfg = Debug|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x64.Build.0 = Debug|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x86.ActiveCfg = Debug|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x86.Build.0 = Debug|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|Any CPU.Build.0 = Release|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x64.ActiveCfg = Release|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x64.Build.0 = Release|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x86.ActiveCfg = Release|Any CPU - {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x86.Build.0 = Release|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|Any CPU.Build.0 = Debug|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x64.ActiveCfg = Debug|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x64.Build.0 = Debug|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x86.ActiveCfg = Debug|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x86.Build.0 = Debug|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|Any CPU.Build.0 = Release|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x64.ActiveCfg = Release|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x64.Build.0 = Release|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x86.ActiveCfg = Release|Any CPU - {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x86.Build.0 = Release|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x64.ActiveCfg = Debug|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x64.Build.0 = Debug|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x86.ActiveCfg = Debug|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x86.Build.0 = Debug|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|Any CPU.Build.0 = Release|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x64.ActiveCfg = Release|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x64.Build.0 = Release|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x86.ActiveCfg = Release|Any CPU - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x86.Build.0 = Release|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x64.ActiveCfg = Debug|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x64.Build.0 = Debug|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x86.ActiveCfg = Debug|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x86.Build.0 = Debug|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|Any CPU.Build.0 = Release|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x64.ActiveCfg = Release|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x64.Build.0 = Release|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x86.ActiveCfg = Release|Any CPU - {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x86.Build.0 = Release|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x64.ActiveCfg = Debug|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x64.Build.0 = Debug|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x86.ActiveCfg = Debug|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x86.Build.0 = Debug|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|Any CPU.Build.0 = Release|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x64.ActiveCfg = Release|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x64.Build.0 = Release|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x86.ActiveCfg = Release|Any CPU - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x86.Build.0 = Release|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x64.ActiveCfg = Debug|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x64.Build.0 = Debug|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x86.ActiveCfg = Debug|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x86.Build.0 = Debug|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|Any CPU.Build.0 = Release|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x64.ActiveCfg = Release|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x64.Build.0 = Release|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x86.ActiveCfg = Release|Any CPU - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x86.Build.0 = Release|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x64.ActiveCfg = Debug|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x64.Build.0 = Debug|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x86.ActiveCfg = Debug|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x86.Build.0 = Debug|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Release|Any CPU.Build.0 = Release|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Release|x64.ActiveCfg = Release|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Release|x64.Build.0 = Release|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Release|x86.ActiveCfg = Release|Any CPU - {40094279-250C-42AE-992A-856718FEFBAC}.Release|x86.Build.0 = Release|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x64.Build.0 = Debug|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x86.Build.0 = Debug|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|Any CPU.Build.0 = Release|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x64.ActiveCfg = Release|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x64.Build.0 = Release|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x86.ActiveCfg = Release|Any CPU - {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x86.Build.0 = Release|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x64.ActiveCfg = Debug|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x64.Build.0 = Debug|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x86.ActiveCfg = Debug|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x86.Build.0 = Debug|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|Any CPU.Build.0 = Release|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x64.ActiveCfg = Release|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x64.Build.0 = Release|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x86.ActiveCfg = Release|Any CPU - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x86.Build.0 = Release|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x64.ActiveCfg = Debug|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x64.Build.0 = Debug|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x86.ActiveCfg = Debug|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x86.Build.0 = Debug|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|Any CPU.Build.0 = Release|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x64.ActiveCfg = Release|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x64.Build.0 = Release|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x86.ActiveCfg = Release|Any CPU - {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x86.Build.0 = Release|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x64.ActiveCfg = Debug|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x64.Build.0 = Debug|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x86.ActiveCfg = Debug|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x86.Build.0 = Debug|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|Any CPU.Build.0 = Release|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x64.ActiveCfg = Release|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x64.Build.0 = Release|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x86.ActiveCfg = Release|Any CPU - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x86.Build.0 = Release|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x64.ActiveCfg = Debug|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x64.Build.0 = Debug|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x86.ActiveCfg = Debug|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x86.Build.0 = Debug|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|Any CPU.Build.0 = Release|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x64.ActiveCfg = Release|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x64.Build.0 = Release|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x86.ActiveCfg = Release|Any CPU - {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x86.Build.0 = Release|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x64.ActiveCfg = Debug|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x64.Build.0 = Debug|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x86.ActiveCfg = Debug|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x86.Build.0 = Debug|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|Any CPU.Build.0 = Release|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x64.ActiveCfg = Release|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x64.Build.0 = Release|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x86.ActiveCfg = Release|Any CPU - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x86.Build.0 = Release|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x64.ActiveCfg = Debug|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x64.Build.0 = Debug|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x86.ActiveCfg = Debug|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x86.Build.0 = Debug|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|Any CPU.Build.0 = Release|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x64.ActiveCfg = Release|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x64.Build.0 = Release|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x86.ActiveCfg = Release|Any CPU - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x86.Build.0 = Release|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x64.ActiveCfg = Debug|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x64.Build.0 = Debug|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x86.ActiveCfg = Debug|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x86.Build.0 = Debug|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|Any CPU.Build.0 = Release|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x64.ActiveCfg = Release|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x64.Build.0 = Release|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x86.ActiveCfg = Release|Any CPU - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x86.Build.0 = Release|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x64.ActiveCfg = Debug|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x64.Build.0 = Debug|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x86.ActiveCfg = Debug|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x86.Build.0 = Debug|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|Any CPU.Build.0 = Release|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x64.ActiveCfg = Release|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x64.Build.0 = Release|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x86.ActiveCfg = Release|Any CPU - {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x86.Build.0 = Release|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x64.ActiveCfg = Debug|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x64.Build.0 = Debug|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x86.ActiveCfg = Debug|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x86.Build.0 = Debug|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|Any CPU.Build.0 = Release|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x64.ActiveCfg = Release|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x64.Build.0 = Release|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x86.ActiveCfg = Release|Any CPU - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x86.Build.0 = Release|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x64.ActiveCfg = Debug|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x64.Build.0 = Debug|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x86.ActiveCfg = Debug|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x86.Build.0 = Debug|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|Any CPU.Build.0 = Release|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x64.ActiveCfg = Release|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x64.Build.0 = Release|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x86.ActiveCfg = Release|Any CPU - {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x86.Build.0 = Release|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x64.ActiveCfg = Debug|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x64.Build.0 = Debug|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x86.ActiveCfg = Debug|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x86.Build.0 = Debug|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|Any CPU.Build.0 = Release|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x64.ActiveCfg = Release|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x64.Build.0 = Release|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x86.ActiveCfg = Release|Any CPU - {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x86.Build.0 = Release|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x64.ActiveCfg = Debug|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x64.Build.0 = Debug|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x86.ActiveCfg = Debug|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x86.Build.0 = Debug|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|Any CPU.Build.0 = Release|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x64.ActiveCfg = Release|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x64.Build.0 = Release|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x86.ActiveCfg = Release|Any CPU - {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x86.Build.0 = Release|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x64.ActiveCfg = Debug|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x64.Build.0 = Debug|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x86.ActiveCfg = Debug|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x86.Build.0 = Debug|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|Any CPU.Build.0 = Release|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x64.ActiveCfg = Release|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x64.Build.0 = Release|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x86.ActiveCfg = Release|Any CPU - {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x86.Build.0 = Release|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x64.ActiveCfg = Debug|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x64.Build.0 = Debug|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x86.ActiveCfg = Debug|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x86.Build.0 = Debug|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|Any CPU.Build.0 = Release|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x64.ActiveCfg = Release|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x64.Build.0 = Release|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x86.ActiveCfg = Release|Any CPU - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x86.Build.0 = Release|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x64.ActiveCfg = Debug|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x64.Build.0 = Debug|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x86.ActiveCfg = Debug|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x86.Build.0 = Debug|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|Any CPU.Build.0 = Release|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x64.ActiveCfg = Release|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x64.Build.0 = Release|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x86.ActiveCfg = Release|Any CPU - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x86.Build.0 = Release|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x64.ActiveCfg = Debug|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x64.Build.0 = Debug|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x86.ActiveCfg = Debug|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x86.Build.0 = Debug|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|Any CPU.Build.0 = Release|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x64.ActiveCfg = Release|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x64.Build.0 = Release|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x86.ActiveCfg = Release|Any CPU - {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x86.Build.0 = Release|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x64.ActiveCfg = Debug|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x64.Build.0 = Debug|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x86.ActiveCfg = Debug|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x86.Build.0 = Debug|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|Any CPU.Build.0 = Release|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x64.ActiveCfg = Release|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x64.Build.0 = Release|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x86.ActiveCfg = Release|Any CPU - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x86.Build.0 = Release|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x64.ActiveCfg = Debug|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x64.Build.0 = Debug|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x86.ActiveCfg = Debug|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x86.Build.0 = Debug|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|Any CPU.Build.0 = Release|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x64.ActiveCfg = Release|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x64.Build.0 = Release|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x86.ActiveCfg = Release|Any CPU - {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x86.Build.0 = Release|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x64.ActiveCfg = Debug|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x64.Build.0 = Debug|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x86.ActiveCfg = Debug|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x86.Build.0 = Debug|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|Any CPU.Build.0 = Release|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x64.ActiveCfg = Release|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x64.Build.0 = Release|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x86.ActiveCfg = Release|Any CPU - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x86.Build.0 = Release|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x64.ActiveCfg = Debug|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x64.Build.0 = Debug|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x86.ActiveCfg = Debug|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x86.Build.0 = Debug|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|Any CPU.Build.0 = Release|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x64.ActiveCfg = Release|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x64.Build.0 = Release|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x86.ActiveCfg = Release|Any CPU - {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x86.Build.0 = Release|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x64.ActiveCfg = Debug|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x64.Build.0 = Debug|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x86.ActiveCfg = Debug|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x86.Build.0 = Debug|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|Any CPU.Build.0 = Release|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x64.ActiveCfg = Release|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x64.Build.0 = Release|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x86.ActiveCfg = Release|Any CPU - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x86.Build.0 = Release|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x64.ActiveCfg = Debug|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x64.Build.0 = Debug|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x86.ActiveCfg = Debug|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x86.Build.0 = Debug|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|Any CPU.Build.0 = Release|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x64.ActiveCfg = Release|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x64.Build.0 = Release|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x86.ActiveCfg = Release|Any CPU - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x86.Build.0 = Release|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x64.ActiveCfg = Debug|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x64.Build.0 = Debug|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x86.ActiveCfg = Debug|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x86.Build.0 = Debug|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|Any CPU.Build.0 = Release|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x64.ActiveCfg = Release|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x64.Build.0 = Release|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x86.ActiveCfg = Release|Any CPU - {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x86.Build.0 = Release|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x64.ActiveCfg = Debug|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x64.Build.0 = Debug|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x86.ActiveCfg = Debug|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x86.Build.0 = Debug|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|Any CPU.Build.0 = Release|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x64.ActiveCfg = Release|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x64.Build.0 = Release|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x86.ActiveCfg = Release|Any CPU - {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x86.Build.0 = Release|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x64.ActiveCfg = Debug|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x64.Build.0 = Debug|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x86.ActiveCfg = Debug|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x86.Build.0 = Debug|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|Any CPU.Build.0 = Release|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x64.ActiveCfg = Release|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x64.Build.0 = Release|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x86.ActiveCfg = Release|Any CPU - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x86.Build.0 = Release|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x64.ActiveCfg = Debug|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x64.Build.0 = Debug|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x86.ActiveCfg = Debug|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x86.Build.0 = Debug|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|Any CPU.Build.0 = Release|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x64.ActiveCfg = Release|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x64.Build.0 = Release|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x86.ActiveCfg = Release|Any CPU - {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x86.Build.0 = Release|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x64.ActiveCfg = Debug|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x64.Build.0 = Debug|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x86.ActiveCfg = Debug|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x86.Build.0 = Debug|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|Any CPU.Build.0 = Release|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x64.ActiveCfg = Release|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x64.Build.0 = Release|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x86.ActiveCfg = Release|Any CPU - {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x86.Build.0 = Release|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x64.ActiveCfg = Debug|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x64.Build.0 = Debug|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x86.ActiveCfg = Debug|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x86.Build.0 = Debug|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|Any CPU.Build.0 = Release|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x64.ActiveCfg = Release|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x64.Build.0 = Release|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x86.ActiveCfg = Release|Any CPU - {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x86.Build.0 = Release|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|Any CPU.Build.0 = Debug|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x64.ActiveCfg = Debug|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x64.Build.0 = Debug|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x86.ActiveCfg = Debug|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x86.Build.0 = Debug|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|Any CPU.ActiveCfg = Release|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|Any CPU.Build.0 = Release|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x64.ActiveCfg = Release|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x64.Build.0 = Release|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x86.ActiveCfg = Release|Any CPU - {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x86.Build.0 = Release|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x64.ActiveCfg = Debug|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x64.Build.0 = Debug|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x86.ActiveCfg = Debug|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x86.Build.0 = Debug|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|Any CPU.Build.0 = Release|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x64.ActiveCfg = Release|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x64.Build.0 = Release|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x86.ActiveCfg = Release|Any CPU - {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x86.Build.0 = Release|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x64.ActiveCfg = Debug|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x64.Build.0 = Debug|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x86.ActiveCfg = Debug|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x86.Build.0 = Debug|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|Any CPU.Build.0 = Release|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x64.ActiveCfg = Release|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x64.Build.0 = Release|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x86.ActiveCfg = Release|Any CPU - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x86.Build.0 = Release|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|Any CPU.Build.0 = Debug|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x64.ActiveCfg = Debug|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x64.Build.0 = Debug|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x86.ActiveCfg = Debug|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x86.Build.0 = Debug|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|Any CPU.ActiveCfg = Release|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|Any CPU.Build.0 = Release|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x64.ActiveCfg = Release|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x64.Build.0 = Release|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x86.ActiveCfg = Release|Any CPU - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x86.Build.0 = Release|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Debug|x64.ActiveCfg = Debug|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Debug|x64.Build.0 = Debug|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Debug|x86.ActiveCfg = Debug|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Debug|x86.Build.0 = Debug|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Release|Any CPU.Build.0 = Release|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Release|x64.ActiveCfg = Release|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Release|x64.Build.0 = Release|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Release|x86.ActiveCfg = Release|Any CPU - {26055403-C7F5-4709-8813-0F7387102791}.Release|x86.Build.0 = Release|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x64.ActiveCfg = Debug|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x64.Build.0 = Debug|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x86.ActiveCfg = Debug|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x86.Build.0 = Debug|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|Any CPU.Build.0 = Release|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x64.ActiveCfg = Release|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x64.Build.0 = Release|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x86.ActiveCfg = Release|Any CPU - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x86.Build.0 = Release|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Debug|Any CPU.Build.0 = Debug|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Debug|x64.ActiveCfg = Debug|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Debug|x64.Build.0 = Debug|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Debug|x86.ActiveCfg = Debug|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Debug|x86.Build.0 = Debug|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Release|Any CPU.ActiveCfg = Release|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Release|Any CPU.Build.0 = Release|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Release|x64.ActiveCfg = Release|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Release|x64.Build.0 = Release|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Release|x86.ActiveCfg = Release|Any CPU - {258327E9-431E-475C-933B-50893676E452}.Release|x86.Build.0 = Release|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x64.ActiveCfg = Debug|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x64.Build.0 = Debug|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x86.ActiveCfg = Debug|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x86.Build.0 = Debug|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|Any CPU.Build.0 = Release|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x64.ActiveCfg = Release|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x64.Build.0 = Release|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x86.ActiveCfg = Release|Any CPU - {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x86.Build.0 = Release|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x64.ActiveCfg = Debug|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x64.Build.0 = Debug|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x86.ActiveCfg = Debug|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x86.Build.0 = Debug|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|Any CPU.Build.0 = Release|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x64.ActiveCfg = Release|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x64.Build.0 = Release|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x86.ActiveCfg = Release|Any CPU - {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x86.Build.0 = Release|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x64.ActiveCfg = Debug|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x64.Build.0 = Debug|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x86.ActiveCfg = Debug|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x86.Build.0 = Debug|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|Any CPU.Build.0 = Release|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x64.ActiveCfg = Release|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x64.Build.0 = Release|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x86.ActiveCfg = Release|Any CPU - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x86.Build.0 = Release|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x64.ActiveCfg = Debug|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x64.Build.0 = Debug|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x86.ActiveCfg = Debug|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x86.Build.0 = Debug|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|Any CPU.Build.0 = Release|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x64.ActiveCfg = Release|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x64.Build.0 = Release|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x86.ActiveCfg = Release|Any CPU - {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x86.Build.0 = Release|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Debug|Any CPU.Build.0 = Debug|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Debug|x64.ActiveCfg = Debug|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Debug|x64.Build.0 = Debug|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Debug|x86.ActiveCfg = Debug|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Debug|x86.Build.0 = Debug|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Release|Any CPU.ActiveCfg = Release|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Release|Any CPU.Build.0 = Release|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Release|x64.ActiveCfg = Release|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Release|x64.Build.0 = Release|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Release|x86.ActiveCfg = Release|Any CPU - {81111B26-74F6-4912-9084-7115FD119945}.Release|x86.Build.0 = Release|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x64.ActiveCfg = Debug|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x64.Build.0 = Debug|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x86.ActiveCfg = Debug|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x86.Build.0 = Debug|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|Any CPU.Build.0 = Release|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x64.ActiveCfg = Release|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x64.Build.0 = Release|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x86.ActiveCfg = Release|Any CPU - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x86.Build.0 = Release|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x64.ActiveCfg = Debug|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x64.Build.0 = Debug|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x86.ActiveCfg = Debug|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x86.Build.0 = Debug|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|Any CPU.Build.0 = Release|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x64.ActiveCfg = Release|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x64.Build.0 = Release|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x86.ActiveCfg = Release|Any CPU - {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x86.Build.0 = Release|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x64.ActiveCfg = Debug|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x64.Build.0 = Debug|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x86.ActiveCfg = Debug|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x86.Build.0 = Debug|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|Any CPU.Build.0 = Release|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x64.ActiveCfg = Release|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x64.Build.0 = Release|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x86.ActiveCfg = Release|Any CPU - {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x86.Build.0 = Release|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x64.ActiveCfg = Debug|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x64.Build.0 = Debug|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x86.ActiveCfg = Debug|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x86.Build.0 = Debug|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|Any CPU.Build.0 = Release|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x64.ActiveCfg = Release|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x64.Build.0 = Release|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x86.ActiveCfg = Release|Any CPU - {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x86.Build.0 = Release|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x64.ActiveCfg = Debug|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x64.Build.0 = Debug|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x86.ActiveCfg = Debug|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x86.Build.0 = Debug|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|Any CPU.Build.0 = Release|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x64.ActiveCfg = Release|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x64.Build.0 = Release|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x86.ActiveCfg = Release|Any CPU - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x86.Build.0 = Release|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x64.ActiveCfg = Debug|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x64.Build.0 = Debug|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x86.ActiveCfg = Debug|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x86.Build.0 = Debug|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|Any CPU.Build.0 = Release|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x64.ActiveCfg = Release|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x64.Build.0 = Release|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x86.ActiveCfg = Release|Any CPU - {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x86.Build.0 = Release|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x64.ActiveCfg = Debug|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x64.Build.0 = Debug|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x86.ActiveCfg = Debug|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x86.Build.0 = Debug|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|Any CPU.Build.0 = Release|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x64.ActiveCfg = Release|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x64.Build.0 = Release|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x86.ActiveCfg = Release|Any CPU - {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x86.Build.0 = Release|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x64.ActiveCfg = Debug|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x64.Build.0 = Debug|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x86.ActiveCfg = Debug|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x86.Build.0 = Debug|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|Any CPU.Build.0 = Release|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x64.ActiveCfg = Release|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x64.Build.0 = Release|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x86.ActiveCfg = Release|Any CPU - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x86.Build.0 = Release|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x64.ActiveCfg = Debug|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x64.Build.0 = Debug|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x86.ActiveCfg = Debug|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x86.Build.0 = Debug|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|Any CPU.Build.0 = Release|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x64.ActiveCfg = Release|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x64.Build.0 = Release|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x86.ActiveCfg = Release|Any CPU - {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x86.Build.0 = Release|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x64.ActiveCfg = Debug|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x64.Build.0 = Debug|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x86.ActiveCfg = Debug|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x86.Build.0 = Debug|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|Any CPU.Build.0 = Release|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x64.ActiveCfg = Release|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x64.Build.0 = Release|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x86.ActiveCfg = Release|Any CPU - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x86.Build.0 = Release|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x64.ActiveCfg = Debug|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x64.Build.0 = Debug|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x86.ActiveCfg = Debug|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x86.Build.0 = Debug|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|Any CPU.Build.0 = Release|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x64.ActiveCfg = Release|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x64.Build.0 = Release|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x86.ActiveCfg = Release|Any CPU - {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x86.Build.0 = Release|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x64.ActiveCfg = Debug|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x64.Build.0 = Debug|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x86.ActiveCfg = Debug|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x86.Build.0 = Debug|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|Any CPU.Build.0 = Release|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x64.ActiveCfg = Release|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x64.Build.0 = Release|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x86.ActiveCfg = Release|Any CPU - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x86.Build.0 = Release|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x64.ActiveCfg = Debug|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x64.Build.0 = Debug|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x86.ActiveCfg = Debug|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x86.Build.0 = Debug|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|Any CPU.Build.0 = Release|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x64.ActiveCfg = Release|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x64.Build.0 = Release|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x86.ActiveCfg = Release|Any CPU - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x86.Build.0 = Release|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x64.ActiveCfg = Debug|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x64.Build.0 = Debug|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x86.ActiveCfg = Debug|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x86.Build.0 = Debug|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|Any CPU.Build.0 = Release|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x64.ActiveCfg = Release|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x64.Build.0 = Release|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x86.ActiveCfg = Release|Any CPU - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x86.Build.0 = Release|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x64.ActiveCfg = Debug|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x64.Build.0 = Debug|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x86.ActiveCfg = Debug|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x86.Build.0 = Debug|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|Any CPU.Build.0 = Release|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x64.ActiveCfg = Release|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x64.Build.0 = Release|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x86.ActiveCfg = Release|Any CPU - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x86.Build.0 = Release|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x64.ActiveCfg = Debug|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x64.Build.0 = Debug|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x86.ActiveCfg = Debug|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x86.Build.0 = Debug|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|Any CPU.Build.0 = Release|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x64.ActiveCfg = Release|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x64.Build.0 = Release|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x86.ActiveCfg = Release|Any CPU - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x86.Build.0 = Release|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Debug|Any CPU.Build.0 = Debug|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x64.ActiveCfg = Debug|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x64.Build.0 = Debug|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x86.ActiveCfg = Debug|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x86.Build.0 = Debug|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Release|Any CPU.ActiveCfg = Release|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Release|Any CPU.Build.0 = Release|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Release|x64.ActiveCfg = Release|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Release|x64.Build.0 = Release|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Release|x86.ActiveCfg = Release|Any CPU - {06DC817F-A936-4F83-8929-E00622B32245}.Release|x86.Build.0 = Release|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x64.ActiveCfg = Debug|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x64.Build.0 = Debug|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x86.ActiveCfg = Debug|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x86.Build.0 = Debug|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|Any CPU.Build.0 = Release|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x64.ActiveCfg = Release|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x64.Build.0 = Release|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x86.ActiveCfg = Release|Any CPU - {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x86.Build.0 = Release|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x64.ActiveCfg = Debug|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x64.Build.0 = Debug|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x86.ActiveCfg = Debug|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x86.Build.0 = Debug|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|Any CPU.Build.0 = Release|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x64.ActiveCfg = Release|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x64.Build.0 = Release|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x86.ActiveCfg = Release|Any CPU - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x86.Build.0 = Release|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x64.ActiveCfg = Debug|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x64.Build.0 = Debug|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x86.ActiveCfg = Debug|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x86.Build.0 = Debug|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|Any CPU.Build.0 = Release|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x64.ActiveCfg = Release|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x64.Build.0 = Release|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x86.ActiveCfg = Release|Any CPU - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x86.Build.0 = Release|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x64.ActiveCfg = Debug|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x64.Build.0 = Debug|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x86.ActiveCfg = Debug|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x86.Build.0 = Debug|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|Any CPU.Build.0 = Release|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x64.ActiveCfg = Release|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x64.Build.0 = Release|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x86.ActiveCfg = Release|Any CPU - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x86.Build.0 = Release|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x64.ActiveCfg = Debug|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x64.Build.0 = Debug|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x86.ActiveCfg = Debug|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x86.Build.0 = Debug|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|Any CPU.Build.0 = Release|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x64.ActiveCfg = Release|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x64.Build.0 = Release|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x86.ActiveCfg = Release|Any CPU - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x86.Build.0 = Release|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x64.ActiveCfg = Debug|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x64.Build.0 = Debug|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x86.ActiveCfg = Debug|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x86.Build.0 = Debug|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|Any CPU.Build.0 = Release|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x64.ActiveCfg = Release|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x64.Build.0 = Release|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x86.ActiveCfg = Release|Any CPU - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x86.Build.0 = Release|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|Any CPU.Build.0 = Debug|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x64.ActiveCfg = Debug|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x64.Build.0 = Debug|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x86.ActiveCfg = Debug|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x86.Build.0 = Debug|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|Any CPU.ActiveCfg = Release|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|Any CPU.Build.0 = Release|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x64.ActiveCfg = Release|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x64.Build.0 = Release|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x86.ActiveCfg = Release|Any CPU - {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x86.Build.0 = Release|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x64.ActiveCfg = Debug|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x64.Build.0 = Debug|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x86.ActiveCfg = Debug|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x86.Build.0 = Debug|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|Any CPU.Build.0 = Release|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x64.ActiveCfg = Release|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x64.Build.0 = Release|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x86.ActiveCfg = Release|Any CPU - {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x86.Build.0 = Release|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x64.ActiveCfg = Debug|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x64.Build.0 = Debug|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x86.ActiveCfg = Debug|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x86.Build.0 = Debug|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|Any CPU.Build.0 = Release|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x64.ActiveCfg = Release|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x64.Build.0 = Release|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x86.ActiveCfg = Release|Any CPU - {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x86.Build.0 = Release|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x64.Build.0 = Debug|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x86.Build.0 = Debug|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x64.ActiveCfg = Release|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x64.Build.0 = Release|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x86.ActiveCfg = Release|Any CPU - {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x86.Build.0 = Release|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x64.ActiveCfg = Debug|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x64.Build.0 = Debug|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x86.ActiveCfg = Debug|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x86.Build.0 = Debug|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|Any CPU.Build.0 = Release|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x64.ActiveCfg = Release|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x64.Build.0 = Release|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x86.ActiveCfg = Release|Any CPU - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x86.Build.0 = Release|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x64.ActiveCfg = Debug|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x64.Build.0 = Debug|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x86.ActiveCfg = Debug|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x86.Build.0 = Debug|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|Any CPU.Build.0 = Release|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x64.ActiveCfg = Release|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x64.Build.0 = Release|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x86.ActiveCfg = Release|Any CPU - {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x86.Build.0 = Release|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x64.ActiveCfg = Debug|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x64.Build.0 = Debug|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x86.ActiveCfg = Debug|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x86.Build.0 = Debug|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|Any CPU.Build.0 = Release|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x64.ActiveCfg = Release|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x64.Build.0 = Release|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x86.ActiveCfg = Release|Any CPU - {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x86.Build.0 = Release|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x64.ActiveCfg = Debug|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x64.Build.0 = Debug|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x86.ActiveCfg = Debug|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x86.Build.0 = Debug|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|Any CPU.Build.0 = Release|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x64.ActiveCfg = Release|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x64.Build.0 = Release|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x86.ActiveCfg = Release|Any CPU - {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x86.Build.0 = Release|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x64.ActiveCfg = Debug|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x64.Build.0 = Debug|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x86.ActiveCfg = Debug|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x86.Build.0 = Debug|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|Any CPU.Build.0 = Release|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x64.ActiveCfg = Release|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x64.Build.0 = Release|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x86.ActiveCfg = Release|Any CPU - {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x86.Build.0 = Release|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x64.ActiveCfg = Debug|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x64.Build.0 = Debug|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x86.ActiveCfg = Debug|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x86.Build.0 = Debug|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|Any CPU.Build.0 = Release|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x64.ActiveCfg = Release|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x64.Build.0 = Release|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x86.ActiveCfg = Release|Any CPU - {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x86.Build.0 = Release|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x64.ActiveCfg = Debug|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x64.Build.0 = Debug|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x86.ActiveCfg = Debug|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x86.Build.0 = Debug|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|Any CPU.Build.0 = Release|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x64.ActiveCfg = Release|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x64.Build.0 = Release|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x86.ActiveCfg = Release|Any CPU - {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x86.Build.0 = Release|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x64.ActiveCfg = Debug|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x64.Build.0 = Debug|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x86.ActiveCfg = Debug|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x86.Build.0 = Debug|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|Any CPU.Build.0 = Release|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x64.ActiveCfg = Release|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x64.Build.0 = Release|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x86.ActiveCfg = Release|Any CPU - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x86.Build.0 = Release|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x64.ActiveCfg = Debug|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x64.Build.0 = Debug|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x86.ActiveCfg = Debug|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x86.Build.0 = Debug|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|Any CPU.Build.0 = Release|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x64.ActiveCfg = Release|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x64.Build.0 = Release|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x86.ActiveCfg = Release|Any CPU - {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x86.Build.0 = Release|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x64.ActiveCfg = Debug|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x64.Build.0 = Debug|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x86.ActiveCfg = Debug|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x86.Build.0 = Debug|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|Any CPU.Build.0 = Release|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x64.ActiveCfg = Release|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x64.Build.0 = Release|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x86.ActiveCfg = Release|Any CPU - {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x86.Build.0 = Release|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x64.ActiveCfg = Debug|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x64.Build.0 = Debug|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x86.ActiveCfg = Debug|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x86.Build.0 = Debug|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|Any CPU.Build.0 = Release|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x64.ActiveCfg = Release|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x64.Build.0 = Release|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x86.ActiveCfg = Release|Any CPU - {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x86.Build.0 = Release|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x64.ActiveCfg = Debug|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x64.Build.0 = Debug|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x86.ActiveCfg = Debug|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x86.Build.0 = Debug|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|Any CPU.Build.0 = Release|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x64.ActiveCfg = Release|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x64.Build.0 = Release|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x86.ActiveCfg = Release|Any CPU - {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x86.Build.0 = Release|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x64.ActiveCfg = Debug|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x64.Build.0 = Debug|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x86.ActiveCfg = Debug|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x86.Build.0 = Debug|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|Any CPU.Build.0 = Release|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x64.ActiveCfg = Release|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x64.Build.0 = Release|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x86.ActiveCfg = Release|Any CPU - {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x86.Build.0 = Release|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x64.ActiveCfg = Debug|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x64.Build.0 = Debug|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x86.ActiveCfg = Debug|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x86.Build.0 = Debug|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|Any CPU.Build.0 = Release|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x64.ActiveCfg = Release|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x64.Build.0 = Release|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x86.ActiveCfg = Release|Any CPU - {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x86.Build.0 = Release|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x64.ActiveCfg = Debug|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x64.Build.0 = Debug|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x86.ActiveCfg = Debug|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x86.Build.0 = Debug|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Release|Any CPU.Build.0 = Release|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x64.ActiveCfg = Release|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x64.Build.0 = Release|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x86.ActiveCfg = Release|Any CPU - {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x86.Build.0 = Release|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|Any CPU.Build.0 = Debug|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x64.ActiveCfg = Debug|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x64.Build.0 = Debug|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x86.ActiveCfg = Debug|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x86.Build.0 = Debug|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|Any CPU.ActiveCfg = Release|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|Any CPU.Build.0 = Release|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x64.ActiveCfg = Release|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x64.Build.0 = Release|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x86.ActiveCfg = Release|Any CPU - {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x86.Build.0 = Release|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x64.ActiveCfg = Debug|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x64.Build.0 = Debug|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x86.ActiveCfg = Debug|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x86.Build.0 = Debug|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|Any CPU.Build.0 = Release|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x64.ActiveCfg = Release|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x64.Build.0 = Release|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x86.ActiveCfg = Release|Any CPU - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x86.Build.0 = Release|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x64.ActiveCfg = Debug|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x64.Build.0 = Debug|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x86.ActiveCfg = Debug|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x86.Build.0 = Debug|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|Any CPU.Build.0 = Release|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x64.ActiveCfg = Release|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x64.Build.0 = Release|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x86.ActiveCfg = Release|Any CPU - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x86.Build.0 = Release|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x64.ActiveCfg = Debug|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x64.Build.0 = Debug|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x86.ActiveCfg = Debug|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x86.Build.0 = Debug|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|Any CPU.Build.0 = Release|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x64.ActiveCfg = Release|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x64.Build.0 = Release|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x86.ActiveCfg = Release|Any CPU - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x86.Build.0 = Release|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x64.ActiveCfg = Debug|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x64.Build.0 = Debug|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x86.ActiveCfg = Debug|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x86.Build.0 = Debug|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|Any CPU.Build.0 = Release|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x64.ActiveCfg = Release|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x64.Build.0 = Release|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x86.ActiveCfg = Release|Any CPU - {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x86.Build.0 = Release|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x64.ActiveCfg = Debug|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x64.Build.0 = Debug|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x86.ActiveCfg = Debug|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x86.Build.0 = Debug|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|Any CPU.Build.0 = Release|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x64.ActiveCfg = Release|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x64.Build.0 = Release|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x86.ActiveCfg = Release|Any CPU - {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x86.Build.0 = Release|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x64.ActiveCfg = Debug|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x64.Build.0 = Debug|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x86.ActiveCfg = Debug|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x86.Build.0 = Debug|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|Any CPU.Build.0 = Release|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x64.ActiveCfg = Release|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x64.Build.0 = Release|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x86.ActiveCfg = Release|Any CPU - {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x86.Build.0 = Release|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x64.ActiveCfg = Debug|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x64.Build.0 = Debug|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x86.ActiveCfg = Debug|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x86.Build.0 = Debug|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|Any CPU.Build.0 = Release|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x64.ActiveCfg = Release|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x64.Build.0 = Release|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x86.ActiveCfg = Release|Any CPU - {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x86.Build.0 = Release|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x64.ActiveCfg = Debug|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x64.Build.0 = Debug|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x86.ActiveCfg = Debug|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x86.Build.0 = Debug|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Release|Any CPU.Build.0 = Release|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x64.ActiveCfg = Release|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x64.Build.0 = Release|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x86.ActiveCfg = Release|Any CPU - {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x86.Build.0 = Release|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x64.ActiveCfg = Debug|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x64.Build.0 = Debug|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x86.ActiveCfg = Debug|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x86.Build.0 = Debug|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|Any CPU.Build.0 = Release|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x64.ActiveCfg = Release|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x64.Build.0 = Release|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x86.ActiveCfg = Release|Any CPU - {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x86.Build.0 = Release|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x64.ActiveCfg = Debug|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x64.Build.0 = Debug|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x86.ActiveCfg = Debug|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x86.Build.0 = Debug|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|Any CPU.Build.0 = Release|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x64.ActiveCfg = Release|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x64.Build.0 = Release|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x86.ActiveCfg = Release|Any CPU - {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x86.Build.0 = Release|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x64.ActiveCfg = Debug|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x64.Build.0 = Debug|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x86.ActiveCfg = Debug|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x86.Build.0 = Debug|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|Any CPU.Build.0 = Release|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x64.ActiveCfg = Release|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x64.Build.0 = Release|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x86.ActiveCfg = Release|Any CPU - {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x86.Build.0 = Release|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x64.ActiveCfg = Debug|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x64.Build.0 = Debug|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x86.ActiveCfg = Debug|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x86.Build.0 = Debug|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|Any CPU.Build.0 = Release|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x64.ActiveCfg = Release|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x64.Build.0 = Release|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x86.ActiveCfg = Release|Any CPU - {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x86.Build.0 = Release|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x64.ActiveCfg = Debug|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x64.Build.0 = Debug|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x86.ActiveCfg = Debug|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x86.Build.0 = Debug|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|Any CPU.Build.0 = Release|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x64.ActiveCfg = Release|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x64.Build.0 = Release|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x86.ActiveCfg = Release|Any CPU - {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x86.Build.0 = Release|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x64.ActiveCfg = Debug|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x64.Build.0 = Debug|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x86.ActiveCfg = Debug|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x86.Build.0 = Debug|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|Any CPU.Build.0 = Release|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x64.ActiveCfg = Release|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x64.Build.0 = Release|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x86.ActiveCfg = Release|Any CPU - {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x86.Build.0 = Release|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|Any CPU.Build.0 = Debug|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x64.ActiveCfg = Debug|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x64.Build.0 = Debug|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x86.ActiveCfg = Debug|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x86.Build.0 = Debug|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|Any CPU.ActiveCfg = Release|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|Any CPU.Build.0 = Release|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x64.ActiveCfg = Release|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x64.Build.0 = Release|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x86.ActiveCfg = Release|Any CPU - {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x86.Build.0 = Release|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x64.ActiveCfg = Debug|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x64.Build.0 = Debug|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x86.ActiveCfg = Debug|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x86.Build.0 = Debug|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|Any CPU.Build.0 = Release|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x64.ActiveCfg = Release|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x64.Build.0 = Release|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x86.ActiveCfg = Release|Any CPU - {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x86.Build.0 = Release|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|Any CPU.Build.0 = Debug|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x64.ActiveCfg = Debug|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x64.Build.0 = Debug|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x86.ActiveCfg = Debug|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x86.Build.0 = Debug|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|Any CPU.ActiveCfg = Release|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|Any CPU.Build.0 = Release|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x64.ActiveCfg = Release|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x64.Build.0 = Release|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x86.ActiveCfg = Release|Any CPU - {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x86.Build.0 = Release|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x64.Build.0 = Debug|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x86.ActiveCfg = Debug|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x86.Build.0 = Debug|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|Any CPU.Build.0 = Release|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x64.ActiveCfg = Release|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x64.Build.0 = Release|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x86.ActiveCfg = Release|Any CPU - {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x86.Build.0 = Release|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|Any CPU.Build.0 = Debug|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x64.ActiveCfg = Debug|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x64.Build.0 = Debug|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x86.ActiveCfg = Debug|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x86.Build.0 = Debug|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|Any CPU.ActiveCfg = Release|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|Any CPU.Build.0 = Release|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x64.ActiveCfg = Release|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x64.Build.0 = Release|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x86.ActiveCfg = Release|Any CPU - {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x86.Build.0 = Release|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x64.ActiveCfg = Debug|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x64.Build.0 = Debug|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x86.ActiveCfg = Debug|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x86.Build.0 = Debug|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|Any CPU.Build.0 = Release|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x64.ActiveCfg = Release|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x64.Build.0 = Release|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x86.ActiveCfg = Release|Any CPU - {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x86.Build.0 = Release|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x64.ActiveCfg = Debug|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x64.Build.0 = Debug|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x86.ActiveCfg = Debug|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x86.Build.0 = Debug|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|Any CPU.Build.0 = Release|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x64.ActiveCfg = Release|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x64.Build.0 = Release|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x86.ActiveCfg = Release|Any CPU - {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x86.Build.0 = Release|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x64.ActiveCfg = Debug|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x64.Build.0 = Debug|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x86.ActiveCfg = Debug|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x86.Build.0 = Debug|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|Any CPU.Build.0 = Release|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x64.ActiveCfg = Release|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x64.Build.0 = Release|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x86.ActiveCfg = Release|Any CPU - {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x86.Build.0 = Release|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x64.ActiveCfg = Debug|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x64.Build.0 = Debug|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x86.ActiveCfg = Debug|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x86.Build.0 = Debug|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|Any CPU.Build.0 = Release|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x64.ActiveCfg = Release|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x64.Build.0 = Release|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x86.ActiveCfg = Release|Any CPU - {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x86.Build.0 = Release|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x64.ActiveCfg = Debug|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x64.Build.0 = Debug|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x86.Build.0 = Debug|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|Any CPU.Build.0 = Release|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x64.ActiveCfg = Release|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x64.Build.0 = Release|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x86.ActiveCfg = Release|Any CPU - {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x86.Build.0 = Release|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x64.ActiveCfg = Debug|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x64.Build.0 = Debug|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x86.ActiveCfg = Debug|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x86.Build.0 = Debug|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|Any CPU.Build.0 = Release|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x64.ActiveCfg = Release|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x64.Build.0 = Release|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x86.ActiveCfg = Release|Any CPU - {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x86.Build.0 = Release|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x64.ActiveCfg = Debug|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x64.Build.0 = Debug|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x86.ActiveCfg = Debug|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x86.Build.0 = Debug|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|Any CPU.Build.0 = Release|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x64.ActiveCfg = Release|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x64.Build.0 = Release|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x86.ActiveCfg = Release|Any CPU - {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x86.Build.0 = Release|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x64.ActiveCfg = Debug|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x64.Build.0 = Debug|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x86.ActiveCfg = Debug|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x86.Build.0 = Debug|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|Any CPU.Build.0 = Release|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x64.ActiveCfg = Release|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x64.Build.0 = Release|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x86.ActiveCfg = Release|Any CPU - {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x86.Build.0 = Release|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x64.ActiveCfg = Debug|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x64.Build.0 = Debug|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x86.ActiveCfg = Debug|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x86.Build.0 = Debug|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|Any CPU.Build.0 = Release|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x64.ActiveCfg = Release|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x64.Build.0 = Release|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x86.ActiveCfg = Release|Any CPU - {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x86.Build.0 = Release|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x64.ActiveCfg = Debug|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x64.Build.0 = Debug|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x86.ActiveCfg = Debug|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x86.Build.0 = Debug|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Release|Any CPU.Build.0 = Release|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Release|x64.ActiveCfg = Release|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Release|x64.Build.0 = Release|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Release|x86.ActiveCfg = Release|Any CPU - {3A8F090F-678D-46E2-8899-67402129749C}.Release|x86.Build.0 = Release|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x64.ActiveCfg = Debug|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x64.Build.0 = Debug|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x86.ActiveCfg = Debug|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x86.Build.0 = Debug|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|Any CPU.Build.0 = Release|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x64.ActiveCfg = Release|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x64.Build.0 = Release|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x86.ActiveCfg = Release|Any CPU - {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x86.Build.0 = Release|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x64.ActiveCfg = Debug|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x64.Build.0 = Debug|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x86.ActiveCfg = Debug|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x86.Build.0 = Debug|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|Any CPU.Build.0 = Release|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x64.ActiveCfg = Release|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x64.Build.0 = Release|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x86.ActiveCfg = Release|Any CPU - {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x86.Build.0 = Release|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x64.ActiveCfg = Debug|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x64.Build.0 = Debug|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x86.ActiveCfg = Debug|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x86.Build.0 = Debug|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|Any CPU.Build.0 = Release|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x64.ActiveCfg = Release|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x64.Build.0 = Release|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x86.ActiveCfg = Release|Any CPU - {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x86.Build.0 = Release|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x64.ActiveCfg = Debug|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x64.Build.0 = Debug|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x86.ActiveCfg = Debug|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x86.Build.0 = Debug|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|Any CPU.Build.0 = Release|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x64.ActiveCfg = Release|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x64.Build.0 = Release|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x86.ActiveCfg = Release|Any CPU - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x86.Build.0 = Release|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x64.ActiveCfg = Debug|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x64.Build.0 = Debug|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x86.ActiveCfg = Debug|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x86.Build.0 = Debug|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Release|Any CPU.Build.0 = Release|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x64.ActiveCfg = Release|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x64.Build.0 = Release|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x86.ActiveCfg = Release|Any CPU - {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x86.Build.0 = Release|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x64.ActiveCfg = Debug|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x64.Build.0 = Debug|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x86.ActiveCfg = Debug|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x86.Build.0 = Debug|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|Any CPU.Build.0 = Release|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x64.ActiveCfg = Release|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x64.Build.0 = Release|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x86.ActiveCfg = Release|Any CPU - {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x86.Build.0 = Release|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|Any CPU.Build.0 = Debug|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x64.ActiveCfg = Debug|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x64.Build.0 = Debug|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x86.ActiveCfg = Debug|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x86.Build.0 = Debug|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|Any CPU.Build.0 = Release|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x64.ActiveCfg = Release|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x64.Build.0 = Release|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x86.ActiveCfg = Release|Any CPU - {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x86.Build.0 = Release|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x64.ActiveCfg = Debug|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x64.Build.0 = Debug|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x86.ActiveCfg = Debug|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x86.Build.0 = Debug|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Release|Any CPU.Build.0 = Release|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x64.ActiveCfg = Release|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x64.Build.0 = Release|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x86.ActiveCfg = Release|Any CPU - {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x86.Build.0 = Release|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x64.ActiveCfg = Debug|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x64.Build.0 = Debug|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x86.ActiveCfg = Debug|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x86.Build.0 = Debug|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|Any CPU.Build.0 = Release|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x64.ActiveCfg = Release|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x64.Build.0 = Release|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x86.ActiveCfg = Release|Any CPU - {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x86.Build.0 = Release|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x64.ActiveCfg = Debug|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x64.Build.0 = Debug|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x86.ActiveCfg = Debug|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x86.Build.0 = Debug|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|Any CPU.Build.0 = Release|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x64.ActiveCfg = Release|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x64.Build.0 = Release|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x86.ActiveCfg = Release|Any CPU - {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x86.Build.0 = Release|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x64.ActiveCfg = Debug|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x64.Build.0 = Debug|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x86.ActiveCfg = Debug|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x86.Build.0 = Debug|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|Any CPU.Build.0 = Release|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x64.ActiveCfg = Release|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x64.Build.0 = Release|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x86.ActiveCfg = Release|Any CPU - {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x86.Build.0 = Release|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x64.ActiveCfg = Debug|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x64.Build.0 = Debug|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x86.ActiveCfg = Debug|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x86.Build.0 = Debug|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|Any CPU.Build.0 = Release|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x64.ActiveCfg = Release|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x64.Build.0 = Release|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x86.ActiveCfg = Release|Any CPU - {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x86.Build.0 = Release|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x64.ActiveCfg = Debug|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x64.Build.0 = Debug|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x86.ActiveCfg = Debug|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x86.Build.0 = Debug|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|Any CPU.Build.0 = Release|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x64.ActiveCfg = Release|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x64.Build.0 = Release|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x86.ActiveCfg = Release|Any CPU - {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x86.Build.0 = Release|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x64.ActiveCfg = Debug|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x64.Build.0 = Debug|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x86.ActiveCfg = Debug|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x86.Build.0 = Debug|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|Any CPU.Build.0 = Release|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x64.ActiveCfg = Release|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x64.Build.0 = Release|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x86.ActiveCfg = Release|Any CPU - {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x86.Build.0 = Release|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Debug|Any CPU.Build.0 = Debug|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x64.ActiveCfg = Debug|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x64.Build.0 = Debug|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x86.ActiveCfg = Debug|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x86.Build.0 = Debug|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Release|Any CPU.ActiveCfg = Release|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Release|Any CPU.Build.0 = Release|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Release|x64.ActiveCfg = Release|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Release|x64.Build.0 = Release|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Release|x86.ActiveCfg = Release|Any CPU - {50D014B5-99A6-46FC-B745-26687595B293}.Release|x86.Build.0 = Release|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x64.ActiveCfg = Debug|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x64.Build.0 = Debug|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x86.ActiveCfg = Debug|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x86.Build.0 = Debug|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|Any CPU.Build.0 = Release|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x64.ActiveCfg = Release|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x64.Build.0 = Release|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x86.ActiveCfg = Release|Any CPU - {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x86.Build.0 = Release|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x64.ActiveCfg = Debug|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x64.Build.0 = Debug|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x86.ActiveCfg = Debug|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x86.Build.0 = Debug|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|Any CPU.Build.0 = Release|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x64.ActiveCfg = Release|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x64.Build.0 = Release|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x86.ActiveCfg = Release|Any CPU - {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x86.Build.0 = Release|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x64.ActiveCfg = Debug|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x64.Build.0 = Debug|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x86.ActiveCfg = Debug|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x86.Build.0 = Debug|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|Any CPU.Build.0 = Release|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x64.ActiveCfg = Release|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x64.Build.0 = Release|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x86.ActiveCfg = Release|Any CPU - {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x86.Build.0 = Release|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x64.ActiveCfg = Debug|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x64.Build.0 = Debug|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x86.ActiveCfg = Debug|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x86.Build.0 = Debug|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Release|Any CPU.Build.0 = Release|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x64.ActiveCfg = Release|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x64.Build.0 = Release|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x86.ActiveCfg = Release|Any CPU - {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x86.Build.0 = Release|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x64.ActiveCfg = Debug|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x64.Build.0 = Debug|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x86.ActiveCfg = Debug|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x86.Build.0 = Debug|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|Any CPU.Build.0 = Release|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x64.ActiveCfg = Release|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x64.Build.0 = Release|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x86.ActiveCfg = Release|Any CPU - {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x86.Build.0 = Release|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x64.ActiveCfg = Debug|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x64.Build.0 = Debug|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x86.ActiveCfg = Debug|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x86.Build.0 = Debug|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|Any CPU.Build.0 = Release|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x64.ActiveCfg = Release|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x64.Build.0 = Release|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x86.ActiveCfg = Release|Any CPU - {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x86.Build.0 = Release|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x64.ActiveCfg = Debug|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x64.Build.0 = Debug|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x86.ActiveCfg = Debug|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x86.Build.0 = Debug|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|Any CPU.Build.0 = Release|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x64.ActiveCfg = Release|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x64.Build.0 = Release|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x86.ActiveCfg = Release|Any CPU - {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x86.Build.0 = Release|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x64.ActiveCfg = Debug|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x64.Build.0 = Debug|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x86.ActiveCfg = Debug|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x86.Build.0 = Debug|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|Any CPU.Build.0 = Release|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x64.ActiveCfg = Release|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x64.Build.0 = Release|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x86.ActiveCfg = Release|Any CPU - {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x86.Build.0 = Release|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x64.ActiveCfg = Debug|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x64.Build.0 = Debug|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x86.ActiveCfg = Debug|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x86.Build.0 = Debug|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|Any CPU.Build.0 = Release|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x64.ActiveCfg = Release|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x64.Build.0 = Release|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x86.ActiveCfg = Release|Any CPU - {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x86.Build.0 = Release|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x64.ActiveCfg = Debug|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x64.Build.0 = Debug|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x86.ActiveCfg = Debug|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x86.Build.0 = Debug|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|Any CPU.Build.0 = Release|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x64.ActiveCfg = Release|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x64.Build.0 = Release|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x86.ActiveCfg = Release|Any CPU - {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x86.Build.0 = Release|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x64.ActiveCfg = Debug|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x64.Build.0 = Debug|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x86.ActiveCfg = Debug|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x86.Build.0 = Debug|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|Any CPU.Build.0 = Release|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x64.ActiveCfg = Release|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x64.Build.0 = Release|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x86.ActiveCfg = Release|Any CPU - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x86.Build.0 = Release|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x64.ActiveCfg = Debug|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x64.Build.0 = Debug|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x86.ActiveCfg = Debug|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x86.Build.0 = Debug|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|Any CPU.Build.0 = Release|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x64.ActiveCfg = Release|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x64.Build.0 = Release|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x86.ActiveCfg = Release|Any CPU - {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x86.Build.0 = Release|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x64.ActiveCfg = Debug|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x64.Build.0 = Debug|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x86.ActiveCfg = Debug|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x86.Build.0 = Debug|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|Any CPU.Build.0 = Release|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x64.ActiveCfg = Release|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x64.Build.0 = Release|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x86.ActiveCfg = Release|Any CPU - {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x86.Build.0 = Release|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x64.ActiveCfg = Debug|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x64.Build.0 = Debug|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x86.ActiveCfg = Debug|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x86.Build.0 = Debug|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|Any CPU.Build.0 = Release|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x64.ActiveCfg = Release|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x64.Build.0 = Release|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x86.ActiveCfg = Release|Any CPU - {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x86.Build.0 = Release|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x64.ActiveCfg = Debug|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x64.Build.0 = Debug|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x86.ActiveCfg = Debug|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x86.Build.0 = Debug|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|Any CPU.Build.0 = Release|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x64.ActiveCfg = Release|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x64.Build.0 = Release|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x86.ActiveCfg = Release|Any CPU - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x86.Build.0 = Release|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|Any CPU.Build.0 = Debug|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x64.ActiveCfg = Debug|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x64.Build.0 = Debug|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x86.ActiveCfg = Debug|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x86.Build.0 = Debug|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|Any CPU.ActiveCfg = Release|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|Any CPU.Build.0 = Release|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x64.ActiveCfg = Release|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x64.Build.0 = Release|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x86.ActiveCfg = Release|Any CPU - {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x86.Build.0 = Release|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x64.ActiveCfg = Debug|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x64.Build.0 = Debug|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x86.ActiveCfg = Debug|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x86.Build.0 = Debug|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|Any CPU.Build.0 = Release|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x64.ActiveCfg = Release|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x64.Build.0 = Release|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x86.ActiveCfg = Release|Any CPU - {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x86.Build.0 = Release|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x64.ActiveCfg = Debug|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x64.Build.0 = Debug|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x86.ActiveCfg = Debug|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x86.Build.0 = Debug|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|Any CPU.Build.0 = Release|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x64.ActiveCfg = Release|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x64.Build.0 = Release|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x86.ActiveCfg = Release|Any CPU - {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x86.Build.0 = Release|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x64.ActiveCfg = Debug|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x64.Build.0 = Debug|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x86.ActiveCfg = Debug|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x86.Build.0 = Debug|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|Any CPU.Build.0 = Release|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x64.ActiveCfg = Release|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x64.Build.0 = Release|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x86.ActiveCfg = Release|Any CPU - {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x86.Build.0 = Release|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Debug|Any CPU.Build.0 = Debug|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x64.ActiveCfg = Debug|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x64.Build.0 = Debug|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x86.ActiveCfg = Debug|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x86.Build.0 = Debug|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Release|Any CPU.ActiveCfg = Release|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Release|Any CPU.Build.0 = Release|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Release|x64.ActiveCfg = Release|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Release|x64.Build.0 = Release|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Release|x86.ActiveCfg = Release|Any CPU - {866807B8-8E68-417C-8148-6450DEA68012}.Release|x86.Build.0 = Release|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x64.ActiveCfg = Debug|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x64.Build.0 = Debug|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x86.ActiveCfg = Debug|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x86.Build.0 = Debug|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Release|Any CPU.Build.0 = Release|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x64.ActiveCfg = Release|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x64.Build.0 = Release|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x86.ActiveCfg = Release|Any CPU - {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x86.Build.0 = Release|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x64.ActiveCfg = Debug|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x64.Build.0 = Debug|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x86.ActiveCfg = Debug|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x86.Build.0 = Debug|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|Any CPU.Build.0 = Release|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x64.ActiveCfg = Release|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x64.Build.0 = Release|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x86.ActiveCfg = Release|Any CPU - {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x86.Build.0 = Release|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x64.ActiveCfg = Debug|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x64.Build.0 = Debug|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x86.ActiveCfg = Debug|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x86.Build.0 = Debug|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|Any CPU.Build.0 = Release|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x64.ActiveCfg = Release|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x64.Build.0 = Release|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x86.ActiveCfg = Release|Any CPU - {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x86.Build.0 = Release|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x64.ActiveCfg = Debug|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x64.Build.0 = Debug|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x86.ActiveCfg = Debug|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x86.Build.0 = Debug|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|Any CPU.Build.0 = Release|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x64.ActiveCfg = Release|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x64.Build.0 = Release|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x86.ActiveCfg = Release|Any CPU - {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x86.Build.0 = Release|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x64.ActiveCfg = Debug|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x64.Build.0 = Debug|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x86.ActiveCfg = Debug|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x86.Build.0 = Debug|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|Any CPU.Build.0 = Release|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x64.ActiveCfg = Release|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x64.Build.0 = Release|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x86.ActiveCfg = Release|Any CPU - {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x86.Build.0 = Release|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x64.ActiveCfg = Debug|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x64.Build.0 = Debug|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x86.ActiveCfg = Debug|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x86.Build.0 = Debug|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|Any CPU.Build.0 = Release|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x64.ActiveCfg = Release|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x64.Build.0 = Release|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x86.ActiveCfg = Release|Any CPU - {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x86.Build.0 = Release|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Debug|x64.ActiveCfg = Debug|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Debug|x64.Build.0 = Debug|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Debug|x86.ActiveCfg = Debug|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Debug|x86.Build.0 = Debug|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Release|Any CPU.Build.0 = Release|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Release|x64.ActiveCfg = Release|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Release|x64.Build.0 = Release|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Release|x86.ActiveCfg = Release|Any CPU - {548C296A-476B-433D-9552-923648BDFA97}.Release|x86.Build.0 = Release|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x64.ActiveCfg = Debug|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x64.Build.0 = Debug|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x86.ActiveCfg = Debug|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x86.Build.0 = Debug|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|Any CPU.Build.0 = Release|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x64.ActiveCfg = Release|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x64.Build.0 = Release|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x86.ActiveCfg = Release|Any CPU - {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x86.Build.0 = Release|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x64.ActiveCfg = Debug|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x64.Build.0 = Debug|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x86.ActiveCfg = Debug|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x86.Build.0 = Debug|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|Any CPU.Build.0 = Release|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x64.ActiveCfg = Release|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x64.Build.0 = Release|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x86.ActiveCfg = Release|Any CPU - {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x86.Build.0 = Release|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x64.ActiveCfg = Debug|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x64.Build.0 = Debug|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x86.ActiveCfg = Debug|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x86.Build.0 = Debug|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|Any CPU.Build.0 = Release|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x64.ActiveCfg = Release|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x64.Build.0 = Release|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x86.ActiveCfg = Release|Any CPU - {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x86.Build.0 = Release|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x64.ActiveCfg = Debug|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x64.Build.0 = Debug|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x86.ActiveCfg = Debug|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x86.Build.0 = Debug|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|Any CPU.Build.0 = Release|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x64.ActiveCfg = Release|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x64.Build.0 = Release|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x86.ActiveCfg = Release|Any CPU - {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x86.Build.0 = Release|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x64.ActiveCfg = Debug|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x64.Build.0 = Debug|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x86.ActiveCfg = Debug|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x86.Build.0 = Debug|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|Any CPU.Build.0 = Release|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x64.ActiveCfg = Release|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x64.Build.0 = Release|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x86.ActiveCfg = Release|Any CPU - {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x86.Build.0 = Release|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x64.ActiveCfg = Debug|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x64.Build.0 = Debug|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x86.ActiveCfg = Debug|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x86.Build.0 = Debug|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|Any CPU.Build.0 = Release|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x64.ActiveCfg = Release|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x64.Build.0 = Release|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x86.ActiveCfg = Release|Any CPU - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x86.Build.0 = Release|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x64.ActiveCfg = Debug|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x64.Build.0 = Debug|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x86.ActiveCfg = Debug|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x86.Build.0 = Debug|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|Any CPU.Build.0 = Release|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x64.ActiveCfg = Release|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x64.Build.0 = Release|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x86.ActiveCfg = Release|Any CPU - {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x86.Build.0 = Release|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x64.ActiveCfg = Debug|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x64.Build.0 = Debug|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x86.ActiveCfg = Debug|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x86.Build.0 = Debug|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|Any CPU.Build.0 = Release|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x64.ActiveCfg = Release|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x64.Build.0 = Release|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x86.ActiveCfg = Release|Any CPU - {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x86.Build.0 = Release|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x64.ActiveCfg = Debug|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x64.Build.0 = Debug|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x86.ActiveCfg = Debug|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x86.Build.0 = Debug|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|Any CPU.Build.0 = Release|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x64.ActiveCfg = Release|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x64.Build.0 = Release|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x86.ActiveCfg = Release|Any CPU - {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x86.Build.0 = Release|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x64.ActiveCfg = Debug|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x64.Build.0 = Debug|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x86.ActiveCfg = Debug|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x86.Build.0 = Debug|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|Any CPU.Build.0 = Release|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x64.ActiveCfg = Release|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x64.Build.0 = Release|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x86.ActiveCfg = Release|Any CPU - {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x86.Build.0 = Release|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x64.ActiveCfg = Debug|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x64.Build.0 = Debug|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x86.ActiveCfg = Debug|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x86.Build.0 = Debug|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|Any CPU.Build.0 = Release|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x64.ActiveCfg = Release|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x64.Build.0 = Release|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x86.ActiveCfg = Release|Any CPU - {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x86.Build.0 = Release|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x64.ActiveCfg = Debug|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x64.Build.0 = Debug|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x86.ActiveCfg = Debug|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x86.Build.0 = Debug|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|Any CPU.Build.0 = Release|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x64.ActiveCfg = Release|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x64.Build.0 = Release|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x86.ActiveCfg = Release|Any CPU - {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x86.Build.0 = Release|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x64.ActiveCfg = Debug|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x64.Build.0 = Debug|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x86.ActiveCfg = Debug|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x86.Build.0 = Debug|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|Any CPU.Build.0 = Release|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x64.ActiveCfg = Release|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x64.Build.0 = Release|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x86.ActiveCfg = Release|Any CPU - {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x86.Build.0 = Release|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x64.ActiveCfg = Debug|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x64.Build.0 = Debug|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x86.ActiveCfg = Debug|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x86.Build.0 = Debug|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|Any CPU.Build.0 = Release|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x64.ActiveCfg = Release|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x64.Build.0 = Release|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x86.ActiveCfg = Release|Any CPU - {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x86.Build.0 = Release|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Debug|x64.ActiveCfg = Debug|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Debug|x64.Build.0 = Debug|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Debug|x86.ActiveCfg = Debug|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Debug|x86.Build.0 = Debug|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Release|Any CPU.Build.0 = Release|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Release|x64.ActiveCfg = Release|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Release|x64.Build.0 = Release|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Release|x86.ActiveCfg = Release|Any CPU - {9369FA32-E98A-4180-9251-914925188086}.Release|x86.Build.0 = Release|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x64.ActiveCfg = Debug|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x64.Build.0 = Debug|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x86.ActiveCfg = Debug|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x86.Build.0 = Debug|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|Any CPU.Build.0 = Release|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x64.ActiveCfg = Release|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x64.Build.0 = Release|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x86.ActiveCfg = Release|Any CPU - {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x86.Build.0 = Release|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x64.ActiveCfg = Debug|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x64.Build.0 = Debug|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x86.ActiveCfg = Debug|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x86.Build.0 = Debug|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|Any CPU.Build.0 = Release|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x64.ActiveCfg = Release|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x64.Build.0 = Release|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x86.ActiveCfg = Release|Any CPU - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {361838C4-72E2-1C48-5D76-CA6D1A861242} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {46D35B4F-6A04-47FF-958B-5E6A73FCC059} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {44A1241B-8ECF-4AFA-9972-452C39AD43D6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {85AB3BB7-C493-4387-B39A-EB299AC37312} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {93DB06DC-B254-48A9-8F2C-6130A5658F27} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {C6DC3C29-C2AD-4015-8872-42E95A0FE63F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {40094279-250C-42AE-992A-856718FEFBAC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {B2967228-F8F7-4931-B257-1C63CB58CE1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {37F203A3-624E-4794-9C99-16CAC22C17DF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {3FF93987-A30A-4D50-8815-7CF3BB7CAE05} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {AACE8717-0760-42F2-A225-8FCCE876FB65} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D0FB54BA-4D14-4A32-B09F-7EC94F369460} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {69C9E010-CBDD-4B89-84CF-7AB56D6A078A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {E471176A-E1F3-4DE5-8D30-0865903A217A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {FA013511-DF20-45F7-8077-EBA2D6224D64} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {B9F84697-54FE-4648-B173-EE3D904FFA4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {6751A76C-8ED8-40F4-AE2B-069DB31395FE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {DDBFA2EF-9CAE-473F-A438-369CAC25C66A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {35350FAB-FC51-4FE8-81FB-011003134C37} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {C4A65377-22F7-4D15-92A3-4F05847D167E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {BDDE59E1-C643-4C87-8608-0F9A7A54DE09} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {0CC116C8-A7E5-4B94-9688-32920177FF97} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {84DEDF05-A5BD-4644-86B9-6B7918FE3F31} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {9DEB1F54-94B5-40C4-AC44-220E680B016D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {7C3E87F2-93D8-4968-95E3-52C46947D46C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {31B05493-104F-437F-9FA7-CA5286CE697C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {937AF12E-D770-4534-8FF8-C59042609C2A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {5A028B04-9D76-470B-B5B3-766CE4CE860C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {749DE4C8-F733-43F8-B2A8-6649E71C7570} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {56D2C79E-2737-4FF9-9D19-150065F568D5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {E41F6DC4-68B5-4EE3-97AE-801D725A2C13} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {26055403-C7F5-4709-8813-0F7387102791} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {258327E9-431E-475C-933B-50893676E452} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {42AF60C8-A5E1-40E0-86F8-98256364AF6F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {88C6A9C3-B433-4C36-8767-429C8C2396F8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {6B7099AB-01BF-4EC4-87D0-5C9C032266DE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {14C918EA-693E-41FE-ACAE-2E82DF077BEA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {81111B26-74F6-4912-9084-7115FD119945} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {8D0F501D-01B1-4E24-958B-FAF35B267705} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {5BA91095-7F10-4717-B296-49DFBFC1C9C2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {99616566-4EF1-4DC7-B655-825FE43D203D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {A3B19095-2D95-4B09-B07E-2C082C72394B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {807837AF-B392-4589-ADF1-3FDB34D6C5BF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {64EAFDCF-8283-4D5C-AC78-7969D5FE926A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {68F4D8A1-E32F-487A-B460-325F36989BE3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {606C751B-7CF1-47CF-A25C-9248A55C814F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {5CCE0DB7-C115-4B21-A7AE-C8488C22A853} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {06DC817F-A936-4F83-8929-E00622B32245} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {2C999476-0291-4161-B3E9-1AA99A3B1139} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {50140A32-6D3C-47DB-983A-7166CBA51845} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {031979F2-6ABA-444F-A6A4-80115DC487CE} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {D71B0DA5-80A3-419E-898D-40E77A9A7F19} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {B2C877D9-B521-4901-8817-76B5DAA62FCE} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {7116DD6B-2491-49E1-AB27-5210E949F753} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {7DBE31A6-D2FD-499E-B675-4092723175AD} = {361838C4-72E2-1C48-5D76-CA6D1A861242} - {D99E6EAE-D278-4480-AA67-85F025383E47} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D3825714-3DDA-44B7-A99C-5F3E65716691} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {FAB78D21-7372-48FE-B2C3-DE1807F1157D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {EADFA337-B0FA-4712-A24A-7C08235BDF98} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {110F7EC2-3149-4D1B-A972-E69E79F1EBF5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {31277AFF-9BFF-4C17-8593-B562A385058E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {3A8F090F-678D-46E2-8899-67402129749C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {05D844B6-51C1-4926-919C-D99E24FB3BC9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {03E15545-D6A0-4287-A88C-6EDE77C0DCBE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {BA47D456-4657-4C86-A665-21293E3AC47F} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} - {49EF86AC-1CC2-4A24-8637-C5151E23DF9D} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} - {C22333B3-D132-4960-A490-6BEF1EB1C917} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} - {B8B15A8D-F647-41AE-A55F-A283A47E97C4} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} - {99EC90D8-0D5E-41E4-A895-585A7680916C} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} - {F1F029E6-2E4B-4A42-8D8F-AB325EE3B608} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {CBE6E3D8-230C-4513-B98F-99D82B83B9F7} = {F1F029E6-2E4B-4A42-8D8F-AB325EE3B608} - {821C7F88-B775-4D3C-8D89-850B6C34E818} = {F1F029E6-2E4B-4A42-8D8F-AB325EE3B608} - {CBDF819E-923F-A07F-78D9-D599DD28197E} = {1553F566-661E-A2F5-811B-F74BF45C44CC} - {D8B22C17-28E9-4059-97C5-4AC4600A2BD5} = {CBDF819E-923F-A07F-78D9-D599DD28197E} - {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE} = {41F15E67-7190-CF23-3BC4-77E87134CADD} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{361838C4-72E2-1C48-5D76-CA6D1A861242}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client", "StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj", "{5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "StellaOps.Configuration\StellaOps.Configuration.csproj", "{8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{46D35B4F-6A04-47FF-958B-5E6A73FCC059}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{44A1241B-8ECF-4AFA-9972-452C39AD43D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority", "StellaOps.Authority\StellaOps.Authority\StellaOps.Authority.csproj", "{85AB3BB7-C493-4387-B39A-EB299AC37312}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugin.Standard", "StellaOps.Authority\StellaOps.Authority.Plugin.Standard\StellaOps.Authority.Plugin.Standard.csproj", "{93DB06DC-B254-48A9-8F2C-6130A5658F27}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "StellaOps.Plugin\StellaOps.Plugin.csproj", "{03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin.Tests", "StellaOps.Plugin.Tests\StellaOps.Plugin.Tests.csproj", "{C6DC3C29-C2AD-4015-8872-42E95A0FE63F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cli", "StellaOps.Cli\StellaOps.Cli.csproj", "{40094279-250C-42AE-992A-856718FEFBAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cli.Tests", "StellaOps.Cli.Tests\StellaOps.Cli.Tests.csproj", "{B2967228-F8F7-4931-B257-1C63CB58CE1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{37F203A3-624E-4794-9C99-16CAC22C17DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo", "StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj", "{3FF93987-A30A-4D50-8815-7CF3BB7CAE05}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{AACE8717-0760-42F2-A225-8FCCE876FB65}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core.Tests", "StellaOps.Concelier.Core.Tests\StellaOps.Concelier.Core.Tests.csproj", "{FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.Json", "StellaOps.Concelier.Exporter.Json\StellaOps.Concelier.Exporter.Json.csproj", "{D0FB54BA-4D14-4A32-B09F-7EC94F369460}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.Json.Tests", "StellaOps.Concelier.Exporter.Json.Tests\StellaOps.Concelier.Exporter.Json.Tests.csproj", "{69C9E010-CBDD-4B89-84CF-7AB56D6A078A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.TrivyDb", "StellaOps.Concelier.Exporter.TrivyDb\StellaOps.Concelier.Exporter.TrivyDb.csproj", "{E471176A-E1F3-4DE5-8D30-0865903A217A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Exporter.TrivyDb.Tests", "StellaOps.Concelier.Exporter.TrivyDb.Tests\StellaOps.Concelier.Exporter.TrivyDb.Tests.csproj", "{FA013511-DF20-45F7-8077-EBA2D6224D64}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge", "StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj", "{B9F84697-54FE-4648-B173-EE3D904FFA4D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge.Tests", "StellaOps.Concelier.Merge.Tests\StellaOps.Concelier.Merge.Tests.csproj", "{6751A76C-8ED8-40F4-AE2B-069DB31395FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models.Tests", "StellaOps.Concelier.Models.Tests\StellaOps.Concelier.Models.Tests.csproj", "{DDBFA2EF-9CAE-473F-A438-369CAC25C66A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization.Tests", "StellaOps.Concelier.Normalization.Tests\StellaOps.Concelier.Normalization.Tests.csproj", "{063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Acsc", "StellaOps.Concelier.Connector.Acsc\StellaOps.Concelier.Connector.Acsc.csproj", "{35350FAB-FC51-4FE8-81FB-011003134C37}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Cccs", "StellaOps.Concelier.Connector.Cccs\StellaOps.Concelier.Connector.Cccs.csproj", "{1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertBund", "StellaOps.Concelier.Connector.CertBund\StellaOps.Concelier.Connector.CertBund.csproj", "{C4A65377-22F7-4D15-92A3-4F05847D167E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertCc", "StellaOps.Concelier.Connector.CertCc\StellaOps.Concelier.Connector.CertCc.csproj", "{BDDE59E1-C643-4C87-8608-0F9A7A54DE09}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertFr", "StellaOps.Concelier.Connector.CertFr\StellaOps.Concelier.Connector.CertFr.csproj", "{0CC116C8-A7E5-4B94-9688-32920177FF97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertFr.Tests", "StellaOps.Concelier.Connector.CertFr.Tests\StellaOps.Concelier.Connector.CertFr.Tests.csproj", "{E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertIn", "StellaOps.Concelier.Connector.CertIn\StellaOps.Concelier.Connector.CertIn.csproj", "{84DEDF05-A5BD-4644-86B9-6B7918FE3F31}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.CertIn.Tests", "StellaOps.Concelier.Connector.CertIn.Tests\StellaOps.Concelier.Connector.CertIn.Tests.csproj", "{9DEB1F54-94B5-40C4-AC44-220E680B016D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common.Tests", "StellaOps.Concelier.Connector.Common.Tests\StellaOps.Concelier.Connector.Common.Tests.csproj", "{7C3E87F2-93D8-4968-95E3-52C46947D46C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Cve", "StellaOps.Concelier.Connector.Cve\StellaOps.Concelier.Connector.Cve.csproj", "{C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Debian", "StellaOps.Concelier.Connector.Distro.Debian\StellaOps.Concelier.Connector.Distro.Debian.csproj", "{31B05493-104F-437F-9FA7-CA5286CE697C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Debian.Tests", "StellaOps.Concelier.Connector.Distro.Debian.Tests\StellaOps.Concelier.Connector.Distro.Debian.Tests.csproj", "{937AF12E-D770-4534-8FF8-C59042609C2A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.RedHat", "StellaOps.Concelier.Connector.Distro.RedHat\StellaOps.Concelier.Connector.Distro.RedHat.csproj", "{5A028B04-9D76-470B-B5B3-766CE4CE860C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.RedHat.Tests", "StellaOps.Concelier.Connector.Distro.RedHat.Tests\StellaOps.Concelier.Connector.Distro.RedHat.Tests.csproj", "{749DE4C8-F733-43F8-B2A8-6649E71C7570}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Suse", "StellaOps.Concelier.Connector.Distro.Suse\StellaOps.Concelier.Connector.Distro.Suse.csproj", "{56D2C79E-2737-4FF9-9D19-150065F568D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Suse.Tests", "StellaOps.Concelier.Connector.Distro.Suse.Tests\StellaOps.Concelier.Connector.Distro.Suse.Tests.csproj", "{E41F6DC4-68B5-4EE3-97AE-801D725A2C13}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Ubuntu", "StellaOps.Concelier.Connector.Distro.Ubuntu\StellaOps.Concelier.Connector.Distro.Ubuntu.csproj", "{285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Distro.Ubuntu.Tests", "StellaOps.Concelier.Connector.Distro.Ubuntu.Tests\StellaOps.Concelier.Connector.Distro.Ubuntu.Tests.csproj", "{26055403-C7F5-4709-8813-0F7387102791}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ghsa", "StellaOps.Concelier.Connector.Ghsa\StellaOps.Concelier.Connector.Ghsa.csproj", "{0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ics.Cisa", "StellaOps.Concelier.Connector.Ics.Cisa\StellaOps.Concelier.Connector.Ics.Cisa.csproj", "{258327E9-431E-475C-933B-50893676E452}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ics.Kaspersky", "StellaOps.Concelier.Connector.Ics.Kaspersky\StellaOps.Concelier.Connector.Ics.Kaspersky.csproj", "{42AF60C8-A5E1-40E0-86F8-98256364AF6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ics.Kaspersky.Tests", "StellaOps.Concelier.Connector.Ics.Kaspersky.Tests\StellaOps.Concelier.Connector.Ics.Kaspersky.Tests.csproj", "{88C6A9C3-B433-4C36-8767-429C8C2396F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Jvn", "StellaOps.Concelier.Connector.Jvn\StellaOps.Concelier.Connector.Jvn.csproj", "{6B7099AB-01BF-4EC4-87D0-5C9C032266DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Jvn.Tests", "StellaOps.Concelier.Connector.Jvn.Tests\StellaOps.Concelier.Connector.Jvn.Tests.csproj", "{14C918EA-693E-41FE-ACAE-2E82DF077BEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Kev", "StellaOps.Concelier.Connector.Kev\StellaOps.Concelier.Connector.Kev.csproj", "{81111B26-74F6-4912-9084-7115FD119945}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Kisa", "StellaOps.Concelier.Connector.Kisa\StellaOps.Concelier.Connector.Kisa.csproj", "{80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Nvd", "StellaOps.Concelier.Connector.Nvd\StellaOps.Concelier.Connector.Nvd.csproj", "{8D0F501D-01B1-4E24-958B-FAF35B267705}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Nvd.Tests", "StellaOps.Concelier.Connector.Nvd.Tests\StellaOps.Concelier.Connector.Nvd.Tests.csproj", "{5BA91095-7F10-4717-B296-49DFBFC1C9C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Osv", "StellaOps.Concelier.Connector.Osv\StellaOps.Concelier.Connector.Osv.csproj", "{99616566-4EF1-4DC7-B655-825FE43D203D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Osv.Tests", "StellaOps.Concelier.Connector.Osv.Tests\StellaOps.Concelier.Connector.Osv.Tests.csproj", "{EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ru.Bdu", "StellaOps.Concelier.Connector.Ru.Bdu\StellaOps.Concelier.Connector.Ru.Bdu.csproj", "{A3B19095-2D95-4B09-B07E-2C082C72394B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ru.Nkcki", "StellaOps.Concelier.Connector.Ru.Nkcki\StellaOps.Concelier.Connector.Ru.Nkcki.csproj", "{807837AF-B392-4589-ADF1-3FDB34D6C5BF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Adobe", "StellaOps.Concelier.Connector.Vndr.Adobe\StellaOps.Concelier.Connector.Vndr.Adobe.csproj", "{64EAFDCF-8283-4D5C-AC78-7969D5FE926A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Adobe.Tests", "StellaOps.Concelier.Connector.Vndr.Adobe.Tests\StellaOps.Concelier.Connector.Vndr.Adobe.Tests.csproj", "{68F4D8A1-E32F-487A-B460-325F36989BE3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Apple", "StellaOps.Concelier.Connector.Vndr.Apple\StellaOps.Concelier.Connector.Vndr.Apple.csproj", "{4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Chromium", "StellaOps.Concelier.Connector.Vndr.Chromium\StellaOps.Concelier.Connector.Vndr.Chromium.csproj", "{606C751B-7CF1-47CF-A25C-9248A55C814F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Chromium.Tests", "StellaOps.Concelier.Connector.Vndr.Chromium.Tests\StellaOps.Concelier.Connector.Vndr.Chromium.Tests.csproj", "{0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Cisco", "StellaOps.Concelier.Connector.Vndr.Cisco\StellaOps.Concelier.Connector.Vndr.Cisco.csproj", "{CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Cisco.Tests", "StellaOps.Concelier.Connector.Vndr.Cisco.Tests\StellaOps.Concelier.Connector.Vndr.Cisco.Tests.csproj", "{99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Msrc", "StellaOps.Concelier.Connector.Vndr.Msrc\StellaOps.Concelier.Connector.Vndr.Msrc.csproj", "{5CCE0DB7-C115-4B21-A7AE-C8488C22A853}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Oracle", "StellaOps.Concelier.Connector.Vndr.Oracle\StellaOps.Concelier.Connector.Vndr.Oracle.csproj", "{A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Oracle.Tests", "StellaOps.Concelier.Connector.Vndr.Oracle.Tests\StellaOps.Concelier.Connector.Vndr.Oracle.Tests.csproj", "{06DC817F-A936-4F83-8929-E00622B32245}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Vmware", "StellaOps.Concelier.Connector.Vndr.Vmware\StellaOps.Concelier.Connector.Vndr.Vmware.csproj", "{2C999476-0291-4161-B3E9-1AA99A3B1139}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Vndr.Vmware.Tests", "StellaOps.Concelier.Connector.Vndr.Vmware.Tests\StellaOps.Concelier.Connector.Vndr.Vmware.Tests.csproj", "{476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo.Tests", "StellaOps.Concelier.Storage.Mongo.Tests\StellaOps.Concelier.Storage.Mongo.Tests.csproj", "{0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.WebService", "StellaOps.Concelier.WebService\StellaOps.Concelier.WebService.csproj", "{0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.WebService.Tests", "StellaOps.Concelier.WebService.Tests\StellaOps.Concelier.WebService.Tests.csproj", "{8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration.Tests", "StellaOps.Configuration.Tests\StellaOps.Configuration.Tests.csproj", "{C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions.Tests", "StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions.Tests\StellaOps.Authority.Plugins.Abstractions.Tests.csproj", "{50140A32-6D3C-47DB-983A-7166CBA51845}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Tests", "StellaOps.Authority\StellaOps.Authority.Tests\StellaOps.Authority.Tests.csproj", "{031979F2-6ABA-444F-A6A4-80115DC487CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugin.Standard.Tests", "StellaOps.Authority\StellaOps.Authority.Plugin.Standard.Tests\StellaOps.Authority.Plugin.Standard.Tests.csproj", "{D71B0DA5-80A3-419E-898D-40E77A9A7F19}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Storage.Mongo", "StellaOps.Authority\StellaOps.Authority.Storage.Mongo\StellaOps.Authority.Storage.Mongo.csproj", "{B2C877D9-B521-4901-8817-76B5DAA62FCE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions.Tests", "StellaOps.Authority\StellaOps.Auth.Abstractions.Tests\StellaOps.Auth.Abstractions.Tests.csproj", "{08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration.Tests", "StellaOps.Authority\StellaOps.Auth.ServerIntegration.Tests\StellaOps.Auth.ServerIntegration.Tests.csproj", "{7116DD6B-2491-49E1-AB27-5210E949F753}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client.Tests", "StellaOps.Authority\StellaOps.Auth.Client.Tests\StellaOps.Auth.Client.Tests.csproj", "{7DBE31A6-D2FD-499E-B675-4092723175AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Kev.Tests", "StellaOps.Concelier.Connector.Kev.Tests\StellaOps.Concelier.Connector.Kev.Tests.csproj", "{D99E6EAE-D278-4480-AA67-85F025383E47}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Cve.Tests", "StellaOps.Concelier.Connector.Cve.Tests\StellaOps.Concelier.Connector.Cve.Tests.csproj", "{D3825714-3DDA-44B7-A99C-5F3E65716691}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Ghsa.Tests", "StellaOps.Concelier.Connector.Ghsa.Tests\StellaOps.Concelier.Connector.Ghsa.Tests.csproj", "{FAB78D21-7372-48FE-B2C3-DE1807F1157D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{EADFA337-B0FA-4712-A24A-7C08235BDF98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Tests", "StellaOps.Cryptography.Tests\StellaOps.Cryptography.Tests.csproj", "{110F7EC2-3149-4D1B-A972-E69E79F1EBF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.DependencyInjection", "StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj", "{B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core", "StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj", "{3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core.Tests", "StellaOps.Excititor.Core.Tests\StellaOps.Excititor.Core.Tests.csproj", "{680CA103-DCE8-4D02-8979-72DEA5BE8C00}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Policy", "StellaOps.Excititor.Policy\StellaOps.Excititor.Policy.csproj", "{7F4B19D4-569A-4CCF-B481-EBE04860451A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Policy.Tests", "StellaOps.Excititor.Policy.Tests\StellaOps.Excititor.Policy.Tests.csproj", "{DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Storage.Mongo", "StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj", "{E380F242-031E-483E-8570-0EF7EA525C4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Export", "StellaOps.Excititor.Export\StellaOps.Excititor.Export.csproj", "{42582C16-F5A9-417F-9D33-BC489925324F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Export.Tests", "StellaOps.Excititor.Export.Tests\StellaOps.Excititor.Export.Tests.csproj", "{06F40DA8-FEFA-4C2B-907B-155BD92BB859}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.RedHat.CSAF", "StellaOps.Excititor.Connectors.RedHat.CSAF\StellaOps.Excititor.Connectors.RedHat.CSAF.csproj", "{A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.RedHat.CSAF.Tests", "StellaOps.Excititor.Connectors.RedHat.CSAF.Tests\StellaOps.Excititor.Connectors.RedHat.CSAF.Tests.csproj", "{3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Abstractions", "StellaOps.Excititor.Connectors.Abstractions\StellaOps.Excititor.Connectors.Abstractions.csproj", "{F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Worker", "StellaOps.Excititor.Worker\StellaOps.Excititor.Worker.csproj", "{781EC793-1DB0-4E31-95BC-12A2B373045F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Worker.Tests", "StellaOps.Excititor.Worker.Tests\StellaOps.Excititor.Worker.Tests.csproj", "{BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CSAF", "StellaOps.Excititor.Formats.CSAF\StellaOps.Excititor.Formats.CSAF.csproj", "{14E9D043-F0EF-4F68-AE83-D6F579119D9A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CSAF.Tests", "StellaOps.Excititor.Formats.CSAF.Tests\StellaOps.Excititor.Formats.CSAF.Tests.csproj", "{27E94B6E-DEF8-4B89-97CB-424703790ECE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CycloneDX", "StellaOps.Excititor.Formats.CycloneDX\StellaOps.Excititor.Formats.CycloneDX.csproj", "{361E3E23-B215-423D-9906-A84171E20AD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.CycloneDX.Tests", "StellaOps.Excititor.Formats.CycloneDX.Tests\StellaOps.Excititor.Formats.CycloneDX.Tests.csproj", "{7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.OpenVEX", "StellaOps.Excititor.Formats.OpenVEX\StellaOps.Excititor.Formats.OpenVEX.csproj", "{C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Formats.OpenVEX.Tests", "StellaOps.Excititor.Formats.OpenVEX.Tests\StellaOps.Excititor.Formats.OpenVEX.Tests.csproj", "{E86CF4A6-2463-4589-A9D8-9DF557C48367}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Cisco.CSAF", "StellaOps.Excititor.Connectors.Cisco.CSAF\StellaOps.Excititor.Connectors.Cisco.CSAF.csproj", "{B308B94C-E01F-4449-A5A6-CD7A48E52D15}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Cisco.CSAF.Tests", "StellaOps.Excititor.Connectors.Cisco.CSAF.Tests\StellaOps.Excititor.Connectors.Cisco.CSAF.Tests.csproj", "{9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub", "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub\StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.csproj", "{E076DC9C-B436-44BF-B02E-FA565086F805}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests", "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests\StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests.csproj", "{55500025-FE82-4F97-A261-9BAEA4B10845}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.MSRC.CSAF", "StellaOps.Excititor.Connectors.MSRC.CSAF\StellaOps.Excititor.Connectors.MSRC.CSAF.csproj", "{CD12875F-9367-41BD-810C-7FBE76314F17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.MSRC.CSAF.Tests", "StellaOps.Excititor.Connectors.MSRC.CSAF.Tests\StellaOps.Excititor.Connectors.MSRC.CSAF.Tests.csproj", "{063D3280-9918-465A-AF2D-3650A2A50D03}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Oracle.CSAF", "StellaOps.Excititor.Connectors.Oracle.CSAF\StellaOps.Excititor.Connectors.Oracle.CSAF.csproj", "{A3EEE400-3655-4B34-915A-598E60CD55FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Oracle.CSAF.Tests", "StellaOps.Excititor.Connectors.Oracle.CSAF.Tests\StellaOps.Excititor.Connectors.Oracle.CSAF.Tests.csproj", "{577025AD-2FDD-42DF-BFA2-3FC095B50539}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Ubuntu.CSAF", "StellaOps.Excititor.Connectors.Ubuntu.CSAF\StellaOps.Excititor.Connectors.Ubuntu.CSAF.csproj", "{DD3B2076-E5E0-4533-8D27-7724225D7758}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests", "StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests\StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests.csproj", "{CADA1364-8EB1-479E-AB6F-4105C26335C8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core", "StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj", "{8CC4441E-9D1A-4E00-831B-34828A3F9446}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core.Tests", "StellaOps.Scanner.Core.Tests\StellaOps.Scanner.Core.Tests.csproj", "{01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{BDB24B64-FE4E-C4BD-9F80-9428F98EDF6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy", "StellaOps.Policy\StellaOps.Policy.csproj", "{37BB9502-CCD1-425A-BF45-D56968B0C2F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Tests", "StellaOps.Policy.Tests\StellaOps.Policy.Tests.csproj", "{015A7A95-2C07-4C7F-8048-DB591AAC5FE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService", "StellaOps.Scanner.WebService\StellaOps.Scanner.WebService.csproj", "{EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService.Tests", "StellaOps.Scanner.WebService.Tests\StellaOps.Scanner.WebService.Tests.csproj", "{27D951AD-696D-4330-B4F5-F8F81344C191}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage", "StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj", "{31277AFF-9BFF-4C17-8593-B562A385058E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Tests", "StellaOps.Scanner.Storage.Tests\StellaOps.Scanner.Storage.Tests.csproj", "{3A8F090F-678D-46E2-8899-67402129749C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker", "StellaOps.Scanner.Worker\StellaOps.Scanner.Worker.csproj", "{19FACEC7-D6D4-40F5-84AD-14E2983F18F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker.Tests", "StellaOps.Scanner.Worker.Tests\StellaOps.Scanner.Worker.Tests.csproj", "{8342286A-BE36-4ACA-87FF-EBEB4E268498}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace", "StellaOps.Scanner.EntryTrace\StellaOps.Scanner.EntryTrace.csproj", "{05D844B6-51C1-4926-919C-D99E24FB3BC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace.Tests", "StellaOps.Scanner.EntryTrace.Tests\StellaOps.Scanner.EntryTrace.Tests.csproj", "{03E15545-D6A0-4287-A88C-6EDE77C0DCBE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang", "StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj", "{A072C46F-BA45-419E-B1B6-416919F78440}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Tests", "StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj", "{6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff", "StellaOps.Scanner.Diff\StellaOps.Scanner.Diff.csproj", "{10088067-7B8F-4D2E-A8E1-ED546DC17369}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff.Tests", "StellaOps.Scanner.Diff.Tests\StellaOps.Scanner.Diff.Tests.csproj", "{E014565C-2456-4BD0-9481-557F939C1E36}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit", "StellaOps.Scanner.Emit\StellaOps.Scanner.Emit.csproj", "{44825FDA-68D2-4675-8B1D-6D5303DC38CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit.Tests", "StellaOps.Scanner.Emit.Tests\StellaOps.Scanner.Emit.Tests.csproj", "{6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache", "StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj", "{5E5EB0A7-7A19-4144-81FE-13C31DB678B2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache.Tests", "StellaOps.Scanner.Cache.Tests\StellaOps.Scanner.Cache.Tests.csproj", "{7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java", "StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj", "{B86C287A-734E-4527-A03E-6B970F22E27E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS", "StellaOps.Scanner.Analyzers.OS\StellaOps.Scanner.Analyzers.OS.csproj", "{E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Apk", "StellaOps.Scanner.Analyzers.OS.Apk\StellaOps.Scanner.Analyzers.OS.Apk.csproj", "{50D014B5-99A6-46FC-B745-26687595B293}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Dpkg", "StellaOps.Scanner.Analyzers.OS.Dpkg\StellaOps.Scanner.Analyzers.OS.Dpkg.csproj", "{D99C1F78-67EA-40E7-BD4C-985592F5265A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Rpm", "StellaOps.Scanner.Analyzers.OS.Rpm\StellaOps.Scanner.Analyzers.OS.Rpm.csproj", "{1CBC0B9C-A96B-4143-B70F-37C69229FFF2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Tests", "StellaOps.Scanner.Analyzers.OS.Tests\StellaOps.Scanner.Analyzers.OS.Tests.csproj", "{760E2855-31B3-4CCB-BACB-34B7196A59B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node", "StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj", "{3F688F21-7E31-4781-8995-9DD34276773F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python", "StellaOps.Scanner.Analyzers.Lang.Python\StellaOps.Scanner.Analyzers.Lang.Python.csproj", "{80AD7C4D-E4C6-4700-87AD-77B5698B338F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go", "StellaOps.Scanner.Analyzers.Lang.Go\StellaOps.Scanner.Analyzers.Lang.Go.csproj", "{60ABAB54-2EE9-4A16-A109-67F7B6F29184}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.DotNet", "StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj", "{D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust", "StellaOps.Scanner.Analyzers.Lang.Rust\StellaOps.Scanner.Analyzers.Lang.Rust.csproj", "{5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Security", "StellaOps.Auth.Security\StellaOps.Auth.Security.csproj", "{05475C0A-C225-4F07-A3C7-9E17E660042E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor", "StellaOps.Attestor", "{78C966F5-2242-D8EC-ADCA-A1A9C7F723A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Core", "StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj", "{BA47D456-4657-4C86-A665-21293E3AC47F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Infrastructure", "StellaOps.Attestor\StellaOps.Attestor.Infrastructure\StellaOps.Attestor.Infrastructure.csproj", "{49EF86AC-1CC2-4A24-8637-C5151E23DF9D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.WebService", "StellaOps.Attestor\StellaOps.Attestor.WebService\StellaOps.Attestor.WebService.csproj", "{C22333B3-D132-4960-A490-6BEF1EB1C917}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Tests", "StellaOps.Attestor\StellaOps.Attestor.Tests\StellaOps.Attestor.Tests.csproj", "{B8B15A8D-F647-41AE-A55F-A283A47E97C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Verify", "StellaOps.Attestor\StellaOps.Attestor.Verify\StellaOps.Attestor.Verify.csproj", "{99EC90D8-0D5E-41E4-A895-585A7680916C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Zastava", "StellaOps.Zastava", "{F1F029E6-2E4B-4A42-8D8F-AB325EE3B608}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Core", "StellaOps.Zastava.Core\StellaOps.Zastava.Core.csproj", "{CBE6E3D8-230C-4513-B98F-99D82B83B9F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Core.Tests", "StellaOps.Zastava.Core.Tests\StellaOps.Zastava.Core.Tests.csproj", "{821C7F88-B775-4D3C-8D89-850B6C34E818}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Webhook", "StellaOps.Zastava.Webhook\StellaOps.Zastava.Webhook.csproj", "{3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Webhook.Tests", "StellaOps.Zastava.Webhook.Tests\StellaOps.Zastava.Webhook.Tests.csproj", "{3C500ECB-5422-4FFB-BD3D-48A850763D31}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cli.Plugins.NonCore", "StellaOps.Cli.Plugins.NonCore\StellaOps.Cli.Plugins.NonCore.csproj", "{D851E54A-5A44-4F74-9FDF-A2C32CACF651}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "StellaOps.Scanner.Analyzers.Lang.Java.Tests\StellaOps.Scanner.Analyzers.Lang.Java.Tests.csproj", "{866807B8-8E68-417C-8148-6450DEA68012}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "StellaOps.Scanner.Analyzers.Lang.Node.Tests\StellaOps.Scanner.Analyzers.Lang.Node.Tests.csproj", "{20BE41BD-9C32-45B5-882A-C01491979633}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "StellaOps.Scanner.Analyzers.Lang.Python.Tests\StellaOps.Scanner.Analyzers.Lang.Python.Tests.csproj", "{9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "StellaOps.Scanner.Analyzers.Lang.Go.Tests\StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj", "{7C3A6012-6FC8-46A9-9966-1AC373614C41}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Observer", "StellaOps.Zastava.Observer\StellaOps.Zastava.Observer.csproj", "{BC38594B-0B84-4657-9F7B-F2A0FC810F04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Observer.Tests", "StellaOps.Zastava.Observer.Tests\StellaOps.Zastava.Observer.Tests.csproj", "{20E0774F-86D5-4CD0-B636-E5212074FDE8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Engine", "StellaOps.Policy.Engine\StellaOps.Policy.Engine.csproj", "{FE668D8D-AB46-41F4-A82F-8A3330C4D152}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cartographer", "StellaOps.Cartographer\StellaOps.Cartographer.csproj", "{548C296A-476B-433D-9552-923648BDFA97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.SbomService", "StellaOps.SbomService\StellaOps.SbomService.csproj", "{3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scheduler.WebService", "StellaOps.Scheduler.WebService\StellaOps.Scheduler.WebService.csproj", "{C733F161-FCED-4D21-BC83-5CC079E93547}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scheduler.WebService.Tests", "StellaOps.Scheduler.WebService.Tests\StellaOps.Scheduler.WebService.Tests.csproj", "{76E1E74F-41C1-4E24-85EA-ED13F28B80B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Registry.TokenService", "StellaOps.Registry.TokenService\StellaOps.Registry.TokenService.csproj", "{EC73D558-0472-49E2-B46E-D26F9686AA9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Registry.TokenService.Tests", "StellaOps.Registry.TokenService.Tests\StellaOps.Registry.TokenService.Tests.csproj", "{1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Bench", "StellaOps.Bench", "{1553F566-661E-A2F5-811B-F74BF45C44CC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PolicyEngine", "PolicyEngine", "{CBDF819E-923F-A07F-78D9-D599DD28197E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Bench.PolicyEngine", "StellaOps.Bench\PolicyEngine\StellaOps.Bench.PolicyEngine\StellaOps.Bench.PolicyEngine.csproj", "{D8B22C17-28E9-4059-97C5-4AC4600A2BD5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "StellaOps.Aoc\StellaOps.Aoc.csproj", "{6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc.AspNetCore", "StellaOps.Aoc.AspNetCore\StellaOps.Aoc.AspNetCore.csproj", "{D3D47993-27D3-4C90-9C8E-14652807DAF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc.Tests", "StellaOps.Aoc.Tests\StellaOps.Aoc.Tests.csproj", "{4D167781-1AC0-46CF-A32E-1B6E048940B2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc.AspNetCore.Tests", "StellaOps.Aoc.AspNetCore.Tests\StellaOps.Aoc.AspNetCore.Tests.csproj", "{5F9B7682-71E2-4989-9BC9-014A2C26AF50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{C3AEAEE7-038E-45FF-892B-DB18EE29F790}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels.Tests", "StellaOps.Concelier.RawModels.Tests\StellaOps.Concelier.RawModels.Tests.csproj", "{7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals", "StellaOps.Signals\StellaOps.Signals.csproj", "{1561D597-922F-486E-ACF4-98250DDC5CDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Tests", "StellaOps.Signals.Tests\StellaOps.Signals.Tests.csproj", "{D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Gateway", "StellaOps.Policy.Gateway\StellaOps.Policy.Gateway.csproj", "{9369FA32-E98A-4180-9251-914925188086}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.Gateway.Tests", "StellaOps.Policy.Gateway.Tests\StellaOps.Policy.Gateway.Tests.csproj", "{67650687-2E32-40BB-9849-C4ABBA65A7CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{41F15E67-7190-CF23-3BC4-77E87134CADD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.IssuerDirectory.Client", "__Libraries\StellaOps.IssuerDirectory.Client\StellaOps.IssuerDirectory.Client.csproj", "{CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AirGap", "AirGap", "{704A59BF-CC38-09FA-CE4F-73B27EC8F04F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Policy", "StellaOps.AirGap.Policy", "{045CC5F7-9456-2DBC-9E26-760A1C32B2C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Policy", "AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj", "{3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Policy.Tests", "AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.Tests\StellaOps.AirGap.Policy.Tests.csproj", "{C9ADF5BA-B167-48BE-986C-57CC335DCFF8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Concelier", "Concelier", "{00227F43-A2B4-2312-24D4-35D99B2D62BA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{166ECC12-EF41-266B-D99C-4764D5FBD04E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "Concelier\__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{60BA1521-B6FC-43F6-ABEF-4471A06289E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{6F482CF0-1D61-45EF-859C-6242C8BA08F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo", "Concelier\__Libraries\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj", "{3AF4251B-91CF-4024-88B2-B77A9005604A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{9EB54427-92C8-40F1-8425-26DF6ABF7109}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{52B52BCE-54BA-45B7-8EC2-B547B540EE72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{F8130985-63AB-4102-8CCA-3537829250AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{65559EA9-DE79-4DFA-AD14-C077BE5F0422}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{C96771C6-7567-4CD4-89F2-4A397B6E2846}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aoc", "Aoc", "{D8C5582A-D723-AE4A-ECC5-D8DF76468E74}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{85DDD19D-B5BC-B585-C0A3-6A6133E51DB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{49332975-D217-4256-9EA4-892569FD8347}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Policy.Analyzers", "AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.Analyzers\StellaOps.AirGap.Policy.Analyzers.csproj", "{3A1DBF26-7F97-4643-BC50-F888F5F451EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Policy.Analyzers.Tests", "AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.Analyzers.Tests\StellaOps.AirGap.Policy.Analyzers.Tests.csproj", "{1A894DB5-D8A6-4254-A769-F7BE42103CF3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x64.Build.0 = Debug|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Debug|x86.Build.0 = Debug|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|Any CPU.Build.0 = Release|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x64.ActiveCfg = Release|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x64.Build.0 = Release|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x86.ActiveCfg = Release|Any CPU + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB}.Release|x86.Build.0 = Release|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x64.Build.0 = Debug|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Debug|x86.Build.0 = Debug|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|Any CPU.Build.0 = Release|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x64.ActiveCfg = Release|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x64.Build.0 = Release|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x86.ActiveCfg = Release|Any CPU + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2}.Release|x86.Build.0 = Release|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x64.Build.0 = Debug|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Debug|x86.Build.0 = Debug|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|Any CPU.Build.0 = Release|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x64.ActiveCfg = Release|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x64.Build.0 = Release|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x86.ActiveCfg = Release|Any CPU + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6}.Release|x86.Build.0 = Release|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x64.ActiveCfg = Debug|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x64.Build.0 = Debug|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x86.ActiveCfg = Debug|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Debug|x86.Build.0 = Debug|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|Any CPU.Build.0 = Release|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x64.ActiveCfg = Release|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x64.Build.0 = Release|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x86.ActiveCfg = Release|Any CPU + {46D35B4F-6A04-47FF-958B-5E6A73FCC059}.Release|x86.Build.0 = Release|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x64.Build.0 = Debug|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Debug|x86.Build.0 = Debug|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|Any CPU.Build.0 = Release|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x64.ActiveCfg = Release|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x64.Build.0 = Release|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x86.ActiveCfg = Release|Any CPU + {44A1241B-8ECF-4AFA-9972-452C39AD43D6}.Release|x86.Build.0 = Release|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x64.ActiveCfg = Debug|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x64.Build.0 = Debug|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x86.ActiveCfg = Debug|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Debug|x86.Build.0 = Debug|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|Any CPU.Build.0 = Release|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x64.ActiveCfg = Release|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x64.Build.0 = Release|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x86.ActiveCfg = Release|Any CPU + {85AB3BB7-C493-4387-B39A-EB299AC37312}.Release|x86.Build.0 = Release|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x64.Build.0 = Debug|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Debug|x86.Build.0 = Debug|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|Any CPU.Build.0 = Release|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x64.ActiveCfg = Release|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x64.Build.0 = Release|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x86.ActiveCfg = Release|Any CPU + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3}.Release|x86.Build.0 = Release|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x64.ActiveCfg = Debug|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x64.Build.0 = Debug|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x86.ActiveCfg = Debug|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Debug|x86.Build.0 = Debug|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|Any CPU.Build.0 = Release|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x64.ActiveCfg = Release|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x64.Build.0 = Release|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x86.ActiveCfg = Release|Any CPU + {93DB06DC-B254-48A9-8F2C-6130A5658F27}.Release|x86.Build.0 = Release|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x64.ActiveCfg = Debug|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x64.Build.0 = Debug|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x86.ActiveCfg = Debug|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Debug|x86.Build.0 = Debug|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|Any CPU.Build.0 = Release|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x64.ActiveCfg = Release|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x64.Build.0 = Release|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x86.ActiveCfg = Release|Any CPU + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A}.Release|x86.Build.0 = Release|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x64.Build.0 = Debug|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Debug|x86.Build.0 = Debug|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|Any CPU.Build.0 = Release|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x64.ActiveCfg = Release|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x64.Build.0 = Release|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x86.ActiveCfg = Release|Any CPU + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F}.Release|x86.Build.0 = Release|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x64.Build.0 = Debug|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Debug|x86.Build.0 = Debug|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Release|Any CPU.Build.0 = Release|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Release|x64.ActiveCfg = Release|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Release|x64.Build.0 = Release|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Release|x86.ActiveCfg = Release|Any CPU + {40094279-250C-42AE-992A-856718FEFBAC}.Release|x86.Build.0 = Release|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x64.Build.0 = Debug|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Debug|x86.Build.0 = Debug|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|Any CPU.Build.0 = Release|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x64.ActiveCfg = Release|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x64.Build.0 = Release|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x86.ActiveCfg = Release|Any CPU + {B2967228-F8F7-4931-B257-1C63CB58CE1D}.Release|x86.Build.0 = Release|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x64.Build.0 = Debug|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Debug|x86.Build.0 = Debug|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|Any CPU.Build.0 = Release|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x64.ActiveCfg = Release|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x64.Build.0 = Release|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x86.ActiveCfg = Release|Any CPU + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9}.Release|x86.Build.0 = Release|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x64.Build.0 = Debug|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Debug|x86.Build.0 = Debug|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|Any CPU.Build.0 = Release|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x64.ActiveCfg = Release|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x64.Build.0 = Release|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x86.ActiveCfg = Release|Any CPU + {37F203A3-624E-4794-9C99-16CAC22C17DF}.Release|x86.Build.0 = Release|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x64.ActiveCfg = Debug|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x64.Build.0 = Debug|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x86.ActiveCfg = Debug|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Debug|x86.Build.0 = Debug|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|Any CPU.Build.0 = Release|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x64.ActiveCfg = Release|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x64.Build.0 = Release|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x86.ActiveCfg = Release|Any CPU + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05}.Release|x86.Build.0 = Release|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x64.ActiveCfg = Debug|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x64.Build.0 = Debug|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x86.ActiveCfg = Debug|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Debug|x86.Build.0 = Debug|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|Any CPU.Build.0 = Release|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x64.ActiveCfg = Release|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x64.Build.0 = Release|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x86.ActiveCfg = Release|Any CPU + {AACE8717-0760-42F2-A225-8FCCE876FB65}.Release|x86.Build.0 = Release|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x64.ActiveCfg = Debug|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x64.Build.0 = Debug|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Debug|x86.Build.0 = Debug|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|Any CPU.Build.0 = Release|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x64.ActiveCfg = Release|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x64.Build.0 = Release|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x86.ActiveCfg = Release|Any CPU + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D}.Release|x86.Build.0 = Release|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x64.Build.0 = Debug|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Debug|x86.Build.0 = Debug|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|Any CPU.Build.0 = Release|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x64.ActiveCfg = Release|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x64.Build.0 = Release|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x86.ActiveCfg = Release|Any CPU + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3}.Release|x86.Build.0 = Release|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x64.Build.0 = Debug|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Debug|x86.Build.0 = Debug|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|Any CPU.Build.0 = Release|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x64.ActiveCfg = Release|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x64.Build.0 = Release|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x86.ActiveCfg = Release|Any CPU + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0}.Release|x86.Build.0 = Release|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x64.Build.0 = Debug|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Debug|x86.Build.0 = Debug|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|Any CPU.Build.0 = Release|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x64.ActiveCfg = Release|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x64.Build.0 = Release|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x86.ActiveCfg = Release|Any CPU + {D0FB54BA-4D14-4A32-B09F-7EC94F369460}.Release|x86.Build.0 = Release|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x64.ActiveCfg = Debug|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x64.Build.0 = Debug|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x86.ActiveCfg = Debug|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Debug|x86.Build.0 = Debug|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|Any CPU.Build.0 = Release|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x64.ActiveCfg = Release|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x64.Build.0 = Release|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x86.ActiveCfg = Release|Any CPU + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A}.Release|x86.Build.0 = Release|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x64.Build.0 = Debug|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Debug|x86.Build.0 = Debug|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|Any CPU.Build.0 = Release|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x64.ActiveCfg = Release|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x64.Build.0 = Release|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x86.ActiveCfg = Release|Any CPU + {E471176A-E1F3-4DE5-8D30-0865903A217A}.Release|x86.Build.0 = Release|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x64.Build.0 = Debug|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Debug|x86.Build.0 = Debug|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|Any CPU.Build.0 = Release|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x64.ActiveCfg = Release|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x64.Build.0 = Release|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x86.ActiveCfg = Release|Any CPU + {FA013511-DF20-45F7-8077-EBA2D6224D64}.Release|x86.Build.0 = Release|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x64.Build.0 = Debug|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Debug|x86.Build.0 = Debug|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|Any CPU.Build.0 = Release|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x64.ActiveCfg = Release|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x64.Build.0 = Release|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x86.ActiveCfg = Release|Any CPU + {B9F84697-54FE-4648-B173-EE3D904FFA4D}.Release|x86.Build.0 = Release|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x64.Build.0 = Debug|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Debug|x86.Build.0 = Debug|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|Any CPU.Build.0 = Release|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x64.ActiveCfg = Release|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x64.Build.0 = Release|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x86.ActiveCfg = Release|Any CPU + {6751A76C-8ED8-40F4-AE2B-069DB31395FE}.Release|x86.Build.0 = Release|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x64.ActiveCfg = Debug|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x64.Build.0 = Debug|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x86.ActiveCfg = Debug|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Debug|x86.Build.0 = Debug|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|Any CPU.Build.0 = Release|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x64.ActiveCfg = Release|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x64.Build.0 = Release|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x86.ActiveCfg = Release|Any CPU + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A}.Release|x86.Build.0 = Release|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x64.ActiveCfg = Debug|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x64.Build.0 = Debug|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x86.ActiveCfg = Debug|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Debug|x86.Build.0 = Debug|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|Any CPU.Build.0 = Release|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x64.ActiveCfg = Release|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x64.Build.0 = Release|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x86.ActiveCfg = Release|Any CPU + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22}.Release|x86.Build.0 = Release|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x64.ActiveCfg = Debug|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x64.Build.0 = Debug|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x86.ActiveCfg = Debug|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Debug|x86.Build.0 = Debug|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|Any CPU.Build.0 = Release|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x64.ActiveCfg = Release|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x64.Build.0 = Release|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x86.ActiveCfg = Release|Any CPU + {35350FAB-FC51-4FE8-81FB-011003134C37}.Release|x86.Build.0 = Release|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x64.Build.0 = Debug|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Debug|x86.Build.0 = Debug|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|Any CPU.Build.0 = Release|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x64.ActiveCfg = Release|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x64.Build.0 = Release|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x86.ActiveCfg = Release|Any CPU + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519}.Release|x86.Build.0 = Release|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x64.Build.0 = Debug|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Debug|x86.Build.0 = Debug|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|Any CPU.Build.0 = Release|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x64.ActiveCfg = Release|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x64.Build.0 = Release|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x86.ActiveCfg = Release|Any CPU + {C4A65377-22F7-4D15-92A3-4F05847D167E}.Release|x86.Build.0 = Release|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x64.ActiveCfg = Debug|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x64.Build.0 = Debug|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x86.ActiveCfg = Debug|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Debug|x86.Build.0 = Debug|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|Any CPU.Build.0 = Release|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x64.ActiveCfg = Release|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x64.Build.0 = Release|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x86.ActiveCfg = Release|Any CPU + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09}.Release|x86.Build.0 = Release|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x64.Build.0 = Debug|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Debug|x86.Build.0 = Debug|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|Any CPU.Build.0 = Release|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x64.ActiveCfg = Release|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x64.Build.0 = Release|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x86.ActiveCfg = Release|Any CPU + {0CC116C8-A7E5-4B94-9688-32920177FF97}.Release|x86.Build.0 = Release|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x64.Build.0 = Debug|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Debug|x86.Build.0 = Debug|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|Any CPU.Build.0 = Release|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x64.ActiveCfg = Release|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x64.Build.0 = Release|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x86.ActiveCfg = Release|Any CPU + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E}.Release|x86.Build.0 = Release|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x64.ActiveCfg = Debug|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x64.Build.0 = Debug|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x86.ActiveCfg = Debug|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Debug|x86.Build.0 = Debug|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|Any CPU.Build.0 = Release|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x64.ActiveCfg = Release|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x64.Build.0 = Release|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x86.ActiveCfg = Release|Any CPU + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31}.Release|x86.Build.0 = Release|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x64.Build.0 = Debug|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Debug|x86.Build.0 = Debug|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|Any CPU.Build.0 = Release|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x64.ActiveCfg = Release|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x64.Build.0 = Release|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x86.ActiveCfg = Release|Any CPU + {9DEB1F54-94B5-40C4-AC44-220E680B016D}.Release|x86.Build.0 = Release|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x64.Build.0 = Debug|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Debug|x86.Build.0 = Debug|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|Any CPU.Build.0 = Release|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x64.ActiveCfg = Release|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x64.Build.0 = Release|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x86.ActiveCfg = Release|Any CPU + {7C3E87F2-93D8-4968-95E3-52C46947D46C}.Release|x86.Build.0 = Release|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x64.Build.0 = Debug|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Debug|x86.Build.0 = Debug|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|Any CPU.Build.0 = Release|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x64.ActiveCfg = Release|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x64.Build.0 = Release|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x86.ActiveCfg = Release|Any CPU + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2}.Release|x86.Build.0 = Release|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x64.ActiveCfg = Debug|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x64.Build.0 = Debug|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x86.ActiveCfg = Debug|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Debug|x86.Build.0 = Debug|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|Any CPU.Build.0 = Release|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x64.ActiveCfg = Release|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x64.Build.0 = Release|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x86.ActiveCfg = Release|Any CPU + {31B05493-104F-437F-9FA7-CA5286CE697C}.Release|x86.Build.0 = Release|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x64.Build.0 = Debug|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Debug|x86.Build.0 = Debug|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|Any CPU.Build.0 = Release|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x64.ActiveCfg = Release|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x64.Build.0 = Release|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x86.ActiveCfg = Release|Any CPU + {937AF12E-D770-4534-8FF8-C59042609C2A}.Release|x86.Build.0 = Release|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x64.Build.0 = Debug|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Debug|x86.Build.0 = Debug|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|Any CPU.Build.0 = Release|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x64.ActiveCfg = Release|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x64.Build.0 = Release|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x86.ActiveCfg = Release|Any CPU + {5A028B04-9D76-470B-B5B3-766CE4CE860C}.Release|x86.Build.0 = Release|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x64.ActiveCfg = Debug|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x64.Build.0 = Debug|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x86.ActiveCfg = Debug|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Debug|x86.Build.0 = Debug|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|Any CPU.Build.0 = Release|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x64.ActiveCfg = Release|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x64.Build.0 = Release|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x86.ActiveCfg = Release|Any CPU + {749DE4C8-F733-43F8-B2A8-6649E71C7570}.Release|x86.Build.0 = Release|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x64.Build.0 = Debug|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Debug|x86.Build.0 = Debug|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|Any CPU.Build.0 = Release|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x64.ActiveCfg = Release|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x64.Build.0 = Release|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x86.ActiveCfg = Release|Any CPU + {56D2C79E-2737-4FF9-9D19-150065F568D5}.Release|x86.Build.0 = Release|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x64.ActiveCfg = Debug|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x64.Build.0 = Debug|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x86.ActiveCfg = Debug|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Debug|x86.Build.0 = Debug|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|Any CPU.Build.0 = Release|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x64.ActiveCfg = Release|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x64.Build.0 = Release|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x86.ActiveCfg = Release|Any CPU + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13}.Release|x86.Build.0 = Release|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|Any CPU.Build.0 = Debug|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x64.ActiveCfg = Debug|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x64.Build.0 = Debug|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x86.ActiveCfg = Debug|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Debug|x86.Build.0 = Debug|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|Any CPU.ActiveCfg = Release|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|Any CPU.Build.0 = Release|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x64.ActiveCfg = Release|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x64.Build.0 = Release|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x86.ActiveCfg = Release|Any CPU + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798}.Release|x86.Build.0 = Release|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Debug|x64.ActiveCfg = Debug|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Debug|x64.Build.0 = Debug|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Debug|x86.ActiveCfg = Debug|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Debug|x86.Build.0 = Debug|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Release|Any CPU.Build.0 = Release|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Release|x64.ActiveCfg = Release|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Release|x64.Build.0 = Release|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Release|x86.ActiveCfg = Release|Any CPU + {26055403-C7F5-4709-8813-0F7387102791}.Release|x86.Build.0 = Release|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x64.Build.0 = Debug|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Debug|x86.Build.0 = Debug|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|Any CPU.Build.0 = Release|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x64.ActiveCfg = Release|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x64.Build.0 = Release|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x86.ActiveCfg = Release|Any CPU + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF}.Release|x86.Build.0 = Release|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Debug|Any CPU.Build.0 = Debug|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Debug|x64.ActiveCfg = Debug|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Debug|x64.Build.0 = Debug|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Debug|x86.ActiveCfg = Debug|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Debug|x86.Build.0 = Debug|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Release|Any CPU.ActiveCfg = Release|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Release|Any CPU.Build.0 = Release|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Release|x64.ActiveCfg = Release|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Release|x64.Build.0 = Release|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Release|x86.ActiveCfg = Release|Any CPU + {258327E9-431E-475C-933B-50893676E452}.Release|x86.Build.0 = Release|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x64.ActiveCfg = Debug|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x64.Build.0 = Debug|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x86.ActiveCfg = Debug|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Debug|x86.Build.0 = Debug|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|Any CPU.Build.0 = Release|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x64.ActiveCfg = Release|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x64.Build.0 = Release|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x86.ActiveCfg = Release|Any CPU + {42AF60C8-A5E1-40E0-86F8-98256364AF6F}.Release|x86.Build.0 = Release|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x64.Build.0 = Debug|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Debug|x86.Build.0 = Debug|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|Any CPU.Build.0 = Release|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x64.ActiveCfg = Release|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x64.Build.0 = Release|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x86.ActiveCfg = Release|Any CPU + {88C6A9C3-B433-4C36-8767-429C8C2396F8}.Release|x86.Build.0 = Release|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x64.Build.0 = Debug|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Debug|x86.Build.0 = Debug|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|Any CPU.Build.0 = Release|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x64.ActiveCfg = Release|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x64.Build.0 = Release|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x86.ActiveCfg = Release|Any CPU + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE}.Release|x86.Build.0 = Release|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x64.ActiveCfg = Debug|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x64.Build.0 = Debug|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x86.ActiveCfg = Debug|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Debug|x86.Build.0 = Debug|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|Any CPU.Build.0 = Release|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x64.ActiveCfg = Release|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x64.Build.0 = Release|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x86.ActiveCfg = Release|Any CPU + {14C918EA-693E-41FE-ACAE-2E82DF077BEA}.Release|x86.Build.0 = Release|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Debug|x64.ActiveCfg = Debug|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Debug|x64.Build.0 = Debug|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Debug|x86.ActiveCfg = Debug|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Debug|x86.Build.0 = Debug|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Release|Any CPU.Build.0 = Release|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Release|x64.ActiveCfg = Release|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Release|x64.Build.0 = Release|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Release|x86.ActiveCfg = Release|Any CPU + {81111B26-74F6-4912-9084-7115FD119945}.Release|x86.Build.0 = Release|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x64.Build.0 = Debug|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Debug|x86.Build.0 = Debug|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|Any CPU.Build.0 = Release|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x64.ActiveCfg = Release|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x64.Build.0 = Release|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x86.ActiveCfg = Release|Any CPU + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE}.Release|x86.Build.0 = Release|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x64.Build.0 = Debug|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Debug|x86.Build.0 = Debug|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|Any CPU.Build.0 = Release|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x64.ActiveCfg = Release|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x64.Build.0 = Release|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x86.ActiveCfg = Release|Any CPU + {8D0F501D-01B1-4E24-958B-FAF35B267705}.Release|x86.Build.0 = Release|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x64.Build.0 = Debug|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Debug|x86.Build.0 = Debug|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|Any CPU.Build.0 = Release|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x64.ActiveCfg = Release|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x64.Build.0 = Release|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x86.ActiveCfg = Release|Any CPU + {5BA91095-7F10-4717-B296-49DFBFC1C9C2}.Release|x86.Build.0 = Release|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x64.ActiveCfg = Debug|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x64.Build.0 = Debug|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x86.ActiveCfg = Debug|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Debug|x86.Build.0 = Debug|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|Any CPU.Build.0 = Release|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x64.ActiveCfg = Release|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x64.Build.0 = Release|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x86.ActiveCfg = Release|Any CPU + {99616566-4EF1-4DC7-B655-825FE43D203D}.Release|x86.Build.0 = Release|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x64.ActiveCfg = Debug|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x64.Build.0 = Debug|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x86.ActiveCfg = Debug|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Debug|x86.Build.0 = Debug|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|Any CPU.Build.0 = Release|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x64.ActiveCfg = Release|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x64.Build.0 = Release|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x86.ActiveCfg = Release|Any CPU + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0}.Release|x86.Build.0 = Release|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x64.Build.0 = Debug|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Debug|x86.Build.0 = Debug|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|Any CPU.Build.0 = Release|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x64.ActiveCfg = Release|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x64.Build.0 = Release|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x86.ActiveCfg = Release|Any CPU + {A3B19095-2D95-4B09-B07E-2C082C72394B}.Release|x86.Build.0 = Release|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x64.ActiveCfg = Debug|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x64.Build.0 = Debug|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Debug|x86.Build.0 = Debug|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|Any CPU.Build.0 = Release|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x64.ActiveCfg = Release|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x64.Build.0 = Release|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x86.ActiveCfg = Release|Any CPU + {807837AF-B392-4589-ADF1-3FDB34D6C5BF}.Release|x86.Build.0 = Release|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x64.ActiveCfg = Debug|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x64.Build.0 = Debug|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x86.ActiveCfg = Debug|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Debug|x86.Build.0 = Debug|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|Any CPU.Build.0 = Release|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x64.ActiveCfg = Release|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x64.Build.0 = Release|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x86.ActiveCfg = Release|Any CPU + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A}.Release|x86.Build.0 = Release|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x64.ActiveCfg = Debug|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x64.Build.0 = Debug|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x86.ActiveCfg = Debug|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Debug|x86.Build.0 = Debug|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|Any CPU.Build.0 = Release|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x64.ActiveCfg = Release|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x64.Build.0 = Release|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x86.ActiveCfg = Release|Any CPU + {68F4D8A1-E32F-487A-B460-325F36989BE3}.Release|x86.Build.0 = Release|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x64.Build.0 = Debug|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Debug|x86.Build.0 = Debug|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|Any CPU.Build.0 = Release|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x64.ActiveCfg = Release|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x64.Build.0 = Release|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x86.ActiveCfg = Release|Any CPU + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2}.Release|x86.Build.0 = Release|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x64.ActiveCfg = Debug|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x64.Build.0 = Debug|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x86.ActiveCfg = Debug|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Debug|x86.Build.0 = Debug|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|Any CPU.Build.0 = Release|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x64.ActiveCfg = Release|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x64.Build.0 = Release|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x86.ActiveCfg = Release|Any CPU + {606C751B-7CF1-47CF-A25C-9248A55C814F}.Release|x86.Build.0 = Release|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x64.ActiveCfg = Debug|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x64.Build.0 = Debug|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x86.ActiveCfg = Debug|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Debug|x86.Build.0 = Debug|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|Any CPU.Build.0 = Release|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x64.ActiveCfg = Release|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x64.Build.0 = Release|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x86.ActiveCfg = Release|Any CPU + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431}.Release|x86.Build.0 = Release|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x64.Build.0 = Debug|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Debug|x86.Build.0 = Debug|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|Any CPU.Build.0 = Release|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x64.ActiveCfg = Release|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x64.Build.0 = Release|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x86.ActiveCfg = Release|Any CPU + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC}.Release|x86.Build.0 = Release|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x64.ActiveCfg = Debug|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x64.Build.0 = Debug|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x86.ActiveCfg = Debug|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Debug|x86.Build.0 = Debug|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|Any CPU.Build.0 = Release|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x64.ActiveCfg = Release|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x64.Build.0 = Release|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x86.ActiveCfg = Release|Any CPU + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066}.Release|x86.Build.0 = Release|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x64.ActiveCfg = Debug|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x64.Build.0 = Debug|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x86.ActiveCfg = Debug|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Debug|x86.Build.0 = Debug|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|Any CPU.Build.0 = Release|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x64.ActiveCfg = Release|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x64.Build.0 = Release|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x86.ActiveCfg = Release|Any CPU + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853}.Release|x86.Build.0 = Release|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x64.Build.0 = Debug|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Debug|x86.Build.0 = Debug|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|Any CPU.Build.0 = Release|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x64.ActiveCfg = Release|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x64.Build.0 = Release|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x86.ActiveCfg = Release|Any CPU + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E}.Release|x86.Build.0 = Release|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x64.ActiveCfg = Debug|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x64.Build.0 = Debug|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x86.ActiveCfg = Debug|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Debug|x86.Build.0 = Debug|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Release|Any CPU.Build.0 = Release|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Release|x64.ActiveCfg = Release|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Release|x64.Build.0 = Release|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Release|x86.ActiveCfg = Release|Any CPU + {06DC817F-A936-4F83-8929-E00622B32245}.Release|x86.Build.0 = Release|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x64.Build.0 = Debug|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Debug|x86.Build.0 = Debug|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|Any CPU.Build.0 = Release|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x64.ActiveCfg = Release|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x64.Build.0 = Release|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x86.ActiveCfg = Release|Any CPU + {2C999476-0291-4161-B3E9-1AA99A3B1139}.Release|x86.Build.0 = Release|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x64.Build.0 = Debug|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Debug|x86.Build.0 = Debug|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|Any CPU.Build.0 = Release|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x64.ActiveCfg = Release|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x64.Build.0 = Release|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x86.ActiveCfg = Release|Any CPU + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2}.Release|x86.Build.0 = Release|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x64.Build.0 = Debug|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x86.ActiveCfg = Debug|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Debug|x86.Build.0 = Debug|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|Any CPU.Build.0 = Release|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x64.ActiveCfg = Release|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x64.Build.0 = Release|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x86.ActiveCfg = Release|Any CPU + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98}.Release|x86.Build.0 = Release|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x64.Build.0 = Debug|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Debug|x86.Build.0 = Debug|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|Any CPU.Build.0 = Release|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x64.ActiveCfg = Release|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x64.Build.0 = Release|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x86.ActiveCfg = Release|Any CPU + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D}.Release|x86.Build.0 = Release|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x64.Build.0 = Debug|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Debug|x86.Build.0 = Debug|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|Any CPU.Build.0 = Release|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x64.ActiveCfg = Release|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x64.Build.0 = Release|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x86.ActiveCfg = Release|Any CPU + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE}.Release|x86.Build.0 = Release|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x64.Build.0 = Debug|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Debug|x86.Build.0 = Debug|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|Any CPU.Build.0 = Release|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x64.ActiveCfg = Release|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x64.Build.0 = Release|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x86.ActiveCfg = Release|Any CPU + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92}.Release|x86.Build.0 = Release|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x64.ActiveCfg = Debug|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x64.Build.0 = Debug|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x86.ActiveCfg = Debug|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Debug|x86.Build.0 = Debug|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|Any CPU.Build.0 = Release|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x64.ActiveCfg = Release|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x64.Build.0 = Release|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x86.ActiveCfg = Release|Any CPU + {50140A32-6D3C-47DB-983A-7166CBA51845}.Release|x86.Build.0 = Release|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x64.Build.0 = Debug|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Debug|x86.Build.0 = Debug|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|Any CPU.Build.0 = Release|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x64.ActiveCfg = Release|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x64.Build.0 = Release|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x86.ActiveCfg = Release|Any CPU + {031979F2-6ABA-444F-A6A4-80115DC487CE}.Release|x86.Build.0 = Release|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x64.ActiveCfg = Debug|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x64.Build.0 = Debug|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x86.ActiveCfg = Debug|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Debug|x86.Build.0 = Debug|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|Any CPU.Build.0 = Release|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x64.ActiveCfg = Release|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x64.Build.0 = Release|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x86.ActiveCfg = Release|Any CPU + {D71B0DA5-80A3-419E-898D-40E77A9A7F19}.Release|x86.Build.0 = Release|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x64.Build.0 = Debug|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Debug|x86.Build.0 = Debug|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x64.ActiveCfg = Release|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x64.Build.0 = Release|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x86.ActiveCfg = Release|Any CPU + {B2C877D9-B521-4901-8817-76B5DAA62FCE}.Release|x86.Build.0 = Release|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x64.ActiveCfg = Debug|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x64.Build.0 = Debug|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x86.ActiveCfg = Debug|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Debug|x86.Build.0 = Debug|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|Any CPU.Build.0 = Release|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x64.ActiveCfg = Release|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x64.Build.0 = Release|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x86.ActiveCfg = Release|Any CPU + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278}.Release|x86.Build.0 = Release|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x64.ActiveCfg = Debug|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x64.Build.0 = Debug|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x86.ActiveCfg = Debug|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Debug|x86.Build.0 = Debug|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|Any CPU.Build.0 = Release|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x64.ActiveCfg = Release|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x64.Build.0 = Release|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x86.ActiveCfg = Release|Any CPU + {7116DD6B-2491-49E1-AB27-5210E949F753}.Release|x86.Build.0 = Release|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x64.Build.0 = Debug|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Debug|x86.Build.0 = Debug|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|Any CPU.Build.0 = Release|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x64.ActiveCfg = Release|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x64.Build.0 = Release|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x86.ActiveCfg = Release|Any CPU + {7DBE31A6-D2FD-499E-B675-4092723175AD}.Release|x86.Build.0 = Release|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x64.ActiveCfg = Debug|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x64.Build.0 = Debug|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x86.ActiveCfg = Debug|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Debug|x86.Build.0 = Debug|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|Any CPU.Build.0 = Release|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x64.ActiveCfg = Release|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x64.Build.0 = Release|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x86.ActiveCfg = Release|Any CPU + {D99E6EAE-D278-4480-AA67-85F025383E47}.Release|x86.Build.0 = Release|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x64.Build.0 = Debug|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Debug|x86.Build.0 = Debug|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|Any CPU.Build.0 = Release|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x64.ActiveCfg = Release|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x64.Build.0 = Release|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x86.ActiveCfg = Release|Any CPU + {D3825714-3DDA-44B7-A99C-5F3E65716691}.Release|x86.Build.0 = Release|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x64.ActiveCfg = Debug|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x64.Build.0 = Debug|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x86.ActiveCfg = Debug|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Debug|x86.Build.0 = Debug|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|Any CPU.Build.0 = Release|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x64.ActiveCfg = Release|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x64.Build.0 = Release|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x86.ActiveCfg = Release|Any CPU + {FAB78D21-7372-48FE-B2C3-DE1807F1157D}.Release|x86.Build.0 = Release|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x64.ActiveCfg = Debug|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x64.Build.0 = Debug|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x86.ActiveCfg = Debug|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Debug|x86.Build.0 = Debug|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|Any CPU.Build.0 = Release|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x64.ActiveCfg = Release|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x64.Build.0 = Release|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x86.ActiveCfg = Release|Any CPU + {EADFA337-B0FA-4712-A24A-7C08235BDF98}.Release|x86.Build.0 = Release|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x64.ActiveCfg = Debug|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x64.Build.0 = Debug|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x86.ActiveCfg = Debug|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Debug|x86.Build.0 = Debug|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|Any CPU.Build.0 = Release|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x64.ActiveCfg = Release|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x64.Build.0 = Release|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x86.ActiveCfg = Release|Any CPU + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5}.Release|x86.Build.0 = Release|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x64.Build.0 = Debug|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Debug|x86.Build.0 = Debug|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|Any CPU.Build.0 = Release|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x64.ActiveCfg = Release|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x64.Build.0 = Release|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x86.ActiveCfg = Release|Any CPU + {B84FE2DD-A1AD-437C-95CF-89C1DCCFDF6F}.Release|x86.Build.0 = Release|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x64.Build.0 = Debug|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Debug|x86.Build.0 = Debug|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|Any CPU.Build.0 = Release|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x64.ActiveCfg = Release|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x64.Build.0 = Release|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x86.ActiveCfg = Release|Any CPU + {3288F0F8-FF86-4DB3-A1FD-8EB51893E8C2}.Release|x86.Build.0 = Release|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x64.ActiveCfg = Debug|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x64.Build.0 = Debug|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x86.ActiveCfg = Debug|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Debug|x86.Build.0 = Debug|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|Any CPU.Build.0 = Release|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x64.ActiveCfg = Release|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x64.Build.0 = Release|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x86.ActiveCfg = Release|Any CPU + {680CA103-DCE8-4D02-8979-72DEA5BE8C00}.Release|x86.Build.0 = Release|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x64.Build.0 = Debug|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Debug|x86.Build.0 = Debug|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|Any CPU.Build.0 = Release|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x64.ActiveCfg = Release|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x64.Build.0 = Release|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x86.ActiveCfg = Release|Any CPU + {7F4B19D4-569A-4CCF-B481-EBE04860451A}.Release|x86.Build.0 = Release|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x64.Build.0 = Debug|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Debug|x86.Build.0 = Debug|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|Any CPU.Build.0 = Release|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x64.ActiveCfg = Release|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x64.Build.0 = Release|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x86.ActiveCfg = Release|Any CPU + {DE9863B5-E6D6-4C5F-B52A-ED9E964008A3}.Release|x86.Build.0 = Release|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x64.Build.0 = Debug|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Debug|x86.Build.0 = Debug|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|Any CPU.Build.0 = Release|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x64.ActiveCfg = Release|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x64.Build.0 = Release|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x86.ActiveCfg = Release|Any CPU + {E380F242-031E-483E-8570-0EF7EA525C4F}.Release|x86.Build.0 = Release|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x64.ActiveCfg = Debug|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x64.Build.0 = Debug|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x86.ActiveCfg = Debug|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Debug|x86.Build.0 = Debug|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Release|Any CPU.Build.0 = Release|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x64.ActiveCfg = Release|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x64.Build.0 = Release|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x86.ActiveCfg = Release|Any CPU + {42582C16-F5A9-417F-9D33-BC489925324F}.Release|x86.Build.0 = Release|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x64.ActiveCfg = Debug|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x64.Build.0 = Debug|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x86.ActiveCfg = Debug|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Debug|x86.Build.0 = Debug|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|Any CPU.Build.0 = Release|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x64.ActiveCfg = Release|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x64.Build.0 = Release|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x86.ActiveCfg = Release|Any CPU + {06F40DA8-FEFA-4C2B-907B-155BD92BB859}.Release|x86.Build.0 = Release|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x64.Build.0 = Debug|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Debug|x86.Build.0 = Debug|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|Any CPU.Build.0 = Release|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x64.ActiveCfg = Release|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x64.Build.0 = Release|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x86.ActiveCfg = Release|Any CPU + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7}.Release|x86.Build.0 = Release|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x64.Build.0 = Debug|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Debug|x86.Build.0 = Debug|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|Any CPU.Build.0 = Release|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x64.ActiveCfg = Release|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x64.Build.0 = Release|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x86.ActiveCfg = Release|Any CPU + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679}.Release|x86.Build.0 = Release|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x64.Build.0 = Debug|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Debug|x86.Build.0 = Debug|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|Any CPU.Build.0 = Release|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x64.ActiveCfg = Release|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x64.Build.0 = Release|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x86.ActiveCfg = Release|Any CPU + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67}.Release|x86.Build.0 = Release|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x64.ActiveCfg = Debug|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x64.Build.0 = Debug|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x86.ActiveCfg = Debug|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Debug|x86.Build.0 = Debug|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|Any CPU.Build.0 = Release|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x64.ActiveCfg = Release|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x64.Build.0 = Release|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x86.ActiveCfg = Release|Any CPU + {781EC793-1DB0-4E31-95BC-12A2B373045F}.Release|x86.Build.0 = Release|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x64.Build.0 = Debug|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Debug|x86.Build.0 = Debug|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|Any CPU.Build.0 = Release|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x64.ActiveCfg = Release|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x64.Build.0 = Release|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x86.ActiveCfg = Release|Any CPU + {BB863E0C-50FF-41AE-9C13-4E8A1BABC62C}.Release|x86.Build.0 = Release|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x64.ActiveCfg = Debug|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x64.Build.0 = Debug|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Debug|x86.Build.0 = Debug|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|Any CPU.Build.0 = Release|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x64.ActiveCfg = Release|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x64.Build.0 = Release|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x86.ActiveCfg = Release|Any CPU + {14E9D043-F0EF-4F68-AE83-D6F579119D9A}.Release|x86.Build.0 = Release|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x64.ActiveCfg = Debug|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x64.Build.0 = Debug|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x86.ActiveCfg = Debug|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Debug|x86.Build.0 = Debug|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|Any CPU.Build.0 = Release|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x64.ActiveCfg = Release|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x64.Build.0 = Release|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x86.ActiveCfg = Release|Any CPU + {27E94B6E-DEF8-4B89-97CB-424703790ECE}.Release|x86.Build.0 = Release|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x64.ActiveCfg = Debug|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x64.Build.0 = Debug|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Debug|x86.Build.0 = Debug|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Release|Any CPU.Build.0 = Release|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x64.ActiveCfg = Release|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x64.Build.0 = Release|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x86.ActiveCfg = Release|Any CPU + {361E3E23-B215-423D-9906-A84171E20AD3}.Release|x86.Build.0 = Release|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x64.ActiveCfg = Debug|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x64.Build.0 = Debug|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x86.ActiveCfg = Debug|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Debug|x86.Build.0 = Debug|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|Any CPU.Build.0 = Release|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x64.ActiveCfg = Release|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x64.Build.0 = Release|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x86.ActiveCfg = Release|Any CPU + {7A7A3480-C6C3-4A9F-AF46-1889424B9AC2}.Release|x86.Build.0 = Release|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x64.Build.0 = Debug|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Debug|x86.Build.0 = Debug|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|Any CPU.Build.0 = Release|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x64.ActiveCfg = Release|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x64.Build.0 = Release|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x86.ActiveCfg = Release|Any CPU + {C3EAFCB8-0394-4B74-B9A6-3DBA4509201F}.Release|x86.Build.0 = Release|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x64.ActiveCfg = Debug|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x64.Build.0 = Debug|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x86.ActiveCfg = Debug|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Debug|x86.Build.0 = Debug|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|Any CPU.Build.0 = Release|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x64.ActiveCfg = Release|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x64.Build.0 = Release|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x86.ActiveCfg = Release|Any CPU + {E86CF4A6-2463-4589-A9D8-9DF557C48367}.Release|x86.Build.0 = Release|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x64.ActiveCfg = Debug|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x64.Build.0 = Debug|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x86.ActiveCfg = Debug|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Debug|x86.Build.0 = Debug|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|Any CPU.Build.0 = Release|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x64.ActiveCfg = Release|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x64.Build.0 = Release|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x86.ActiveCfg = Release|Any CPU + {B308B94C-E01F-4449-A5A6-CD7A48E52D15}.Release|x86.Build.0 = Release|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x64.Build.0 = Debug|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Debug|x86.Build.0 = Debug|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|Any CPU.Build.0 = Release|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x64.ActiveCfg = Release|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x64.Build.0 = Release|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x86.ActiveCfg = Release|Any CPU + {9FBA3EC4-D794-48BD-82FA-0289E5A2A5FF}.Release|x86.Build.0 = Release|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x64.ActiveCfg = Debug|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x64.Build.0 = Debug|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x86.ActiveCfg = Debug|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Debug|x86.Build.0 = Debug|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|Any CPU.Build.0 = Release|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x64.ActiveCfg = Release|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x64.Build.0 = Release|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x86.ActiveCfg = Release|Any CPU + {E076DC9C-B436-44BF-B02E-FA565086F805}.Release|x86.Build.0 = Release|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x64.ActiveCfg = Debug|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x64.Build.0 = Debug|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x86.ActiveCfg = Debug|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Debug|x86.Build.0 = Debug|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|Any CPU.Build.0 = Release|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x64.ActiveCfg = Release|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x64.Build.0 = Release|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x86.ActiveCfg = Release|Any CPU + {55500025-FE82-4F97-A261-9BAEA4B10845}.Release|x86.Build.0 = Release|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x64.Build.0 = Debug|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Debug|x86.Build.0 = Debug|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|Any CPU.Build.0 = Release|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x64.ActiveCfg = Release|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x64.Build.0 = Release|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x86.ActiveCfg = Release|Any CPU + {CD12875F-9367-41BD-810C-7FBE76314F17}.Release|x86.Build.0 = Release|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x64.ActiveCfg = Debug|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x64.Build.0 = Debug|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x86.ActiveCfg = Debug|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Debug|x86.Build.0 = Debug|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|Any CPU.Build.0 = Release|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x64.ActiveCfg = Release|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x64.Build.0 = Release|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x86.ActiveCfg = Release|Any CPU + {063D3280-9918-465A-AF2D-3650A2A50D03}.Release|x86.Build.0 = Release|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x64.Build.0 = Debug|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Debug|x86.Build.0 = Debug|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|Any CPU.Build.0 = Release|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x64.ActiveCfg = Release|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x64.Build.0 = Release|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x86.ActiveCfg = Release|Any CPU + {A3EEE400-3655-4B34-915A-598E60CD55FB}.Release|x86.Build.0 = Release|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|Any CPU.Build.0 = Debug|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x64.ActiveCfg = Debug|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x64.Build.0 = Debug|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x86.ActiveCfg = Debug|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Debug|x86.Build.0 = Debug|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|Any CPU.ActiveCfg = Release|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|Any CPU.Build.0 = Release|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x64.ActiveCfg = Release|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x64.Build.0 = Release|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x86.ActiveCfg = Release|Any CPU + {577025AD-2FDD-42DF-BFA2-3FC095B50539}.Release|x86.Build.0 = Release|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x64.Build.0 = Debug|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Debug|x86.Build.0 = Debug|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|Any CPU.Build.0 = Release|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x64.ActiveCfg = Release|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x64.Build.0 = Release|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x86.ActiveCfg = Release|Any CPU + {DD3B2076-E5E0-4533-8D27-7724225D7758}.Release|x86.Build.0 = Release|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x64.Build.0 = Debug|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Debug|x86.Build.0 = Debug|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|Any CPU.Build.0 = Release|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x64.ActiveCfg = Release|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x64.Build.0 = Release|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x86.ActiveCfg = Release|Any CPU + {CADA1364-8EB1-479E-AB6F-4105C26335C8}.Release|x86.Build.0 = Release|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x64.Build.0 = Debug|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Debug|x86.Build.0 = Debug|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|Any CPU.Build.0 = Release|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x64.ActiveCfg = Release|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x64.Build.0 = Release|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x86.ActiveCfg = Release|Any CPU + {8CC4441E-9D1A-4E00-831B-34828A3F9446}.Release|x86.Build.0 = Release|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x64.ActiveCfg = Debug|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x64.Build.0 = Debug|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x86.ActiveCfg = Debug|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Debug|x86.Build.0 = Debug|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|Any CPU.Build.0 = Release|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x64.ActiveCfg = Release|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x64.Build.0 = Release|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x86.ActiveCfg = Release|Any CPU + {01B8AC3F-1B97-4F79-93C6-BE1CBA26FE17}.Release|x86.Build.0 = Release|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x64.Build.0 = Debug|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Debug|x86.Build.0 = Debug|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|Any CPU.Build.0 = Release|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x64.ActiveCfg = Release|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x64.Build.0 = Release|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x86.ActiveCfg = Release|Any CPU + {37BB9502-CCD1-425A-BF45-D56968B0C2F9}.Release|x86.Build.0 = Release|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x64.ActiveCfg = Debug|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x64.Build.0 = Debug|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x86.ActiveCfg = Debug|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Debug|x86.Build.0 = Debug|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|Any CPU.Build.0 = Release|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x64.ActiveCfg = Release|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x64.Build.0 = Release|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x86.ActiveCfg = Release|Any CPU + {015A7A95-2C07-4C7F-8048-DB591AAC5FE5}.Release|x86.Build.0 = Release|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x64.Build.0 = Debug|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Debug|x86.Build.0 = Debug|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|Any CPU.Build.0 = Release|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x64.ActiveCfg = Release|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x64.Build.0 = Release|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x86.ActiveCfg = Release|Any CPU + {EF59DAD6-30CE-47CB-862A-DD79F31BFDE4}.Release|x86.Build.0 = Release|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x64.ActiveCfg = Debug|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x64.Build.0 = Debug|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x86.ActiveCfg = Debug|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Debug|x86.Build.0 = Debug|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|Any CPU.Build.0 = Release|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x64.ActiveCfg = Release|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x64.Build.0 = Release|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x86.ActiveCfg = Release|Any CPU + {27D951AD-696D-4330-B4F5-F8F81344C191}.Release|x86.Build.0 = Release|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x64.ActiveCfg = Debug|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x64.Build.0 = Debug|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x86.ActiveCfg = Debug|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Debug|x86.Build.0 = Debug|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|Any CPU.Build.0 = Release|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x64.ActiveCfg = Release|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x64.Build.0 = Release|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x86.ActiveCfg = Release|Any CPU + {31277AFF-9BFF-4C17-8593-B562A385058E}.Release|x86.Build.0 = Release|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x64.Build.0 = Debug|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Debug|x86.Build.0 = Debug|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Release|Any CPU.Build.0 = Release|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Release|x64.ActiveCfg = Release|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Release|x64.Build.0 = Release|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Release|x86.ActiveCfg = Release|Any CPU + {3A8F090F-678D-46E2-8899-67402129749C}.Release|x86.Build.0 = Release|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x64.Build.0 = Debug|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Debug|x86.Build.0 = Debug|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|Any CPU.Build.0 = Release|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x64.ActiveCfg = Release|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x64.Build.0 = Release|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x86.ActiveCfg = Release|Any CPU + {19FACEC7-D6D4-40F5-84AD-14E2983F18F7}.Release|x86.Build.0 = Release|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x64.ActiveCfg = Debug|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x64.Build.0 = Debug|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x86.ActiveCfg = Debug|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Debug|x86.Build.0 = Debug|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|Any CPU.Build.0 = Release|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x64.ActiveCfg = Release|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x64.Build.0 = Release|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x86.ActiveCfg = Release|Any CPU + {8342286A-BE36-4ACA-87FF-EBEB4E268498}.Release|x86.Build.0 = Release|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x64.Build.0 = Debug|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Debug|x86.Build.0 = Debug|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|Any CPU.Build.0 = Release|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x64.ActiveCfg = Release|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x64.Build.0 = Release|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x86.ActiveCfg = Release|Any CPU + {05D844B6-51C1-4926-919C-D99E24FB3BC9}.Release|x86.Build.0 = Release|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x64.ActiveCfg = Debug|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x64.Build.0 = Debug|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x86.ActiveCfg = Debug|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Debug|x86.Build.0 = Debug|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|Any CPU.Build.0 = Release|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x64.ActiveCfg = Release|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x64.Build.0 = Release|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x86.ActiveCfg = Release|Any CPU + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE}.Release|x86.Build.0 = Release|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x64.ActiveCfg = Debug|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x64.Build.0 = Debug|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x86.ActiveCfg = Debug|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Debug|x86.Build.0 = Debug|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Release|Any CPU.Build.0 = Release|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x64.ActiveCfg = Release|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x64.Build.0 = Release|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x86.ActiveCfg = Release|Any CPU + {A072C46F-BA45-419E-B1B6-416919F78440}.Release|x86.Build.0 = Release|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x64.Build.0 = Debug|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Debug|x86.Build.0 = Debug|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|Any CPU.Build.0 = Release|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x64.ActiveCfg = Release|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x64.Build.0 = Release|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x86.ActiveCfg = Release|Any CPU + {6DE0F48D-8CEA-44C1-82FF-0DC891B33FE3}.Release|x86.Build.0 = Release|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x64.ActiveCfg = Debug|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x64.Build.0 = Debug|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x86.ActiveCfg = Debug|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Debug|x86.Build.0 = Debug|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|Any CPU.Build.0 = Release|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x64.ActiveCfg = Release|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x64.Build.0 = Release|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x86.ActiveCfg = Release|Any CPU + {10088067-7B8F-4D2E-A8E1-ED546DC17369}.Release|x86.Build.0 = Release|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x64.ActiveCfg = Debug|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x64.Build.0 = Debug|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x86.ActiveCfg = Debug|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Debug|x86.Build.0 = Debug|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Release|Any CPU.Build.0 = Release|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x64.ActiveCfg = Release|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x64.Build.0 = Release|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x86.ActiveCfg = Release|Any CPU + {E014565C-2456-4BD0-9481-557F939C1E36}.Release|x86.Build.0 = Release|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x64.Build.0 = Debug|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Debug|x86.Build.0 = Debug|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|Any CPU.Build.0 = Release|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x64.ActiveCfg = Release|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x64.Build.0 = Release|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x86.ActiveCfg = Release|Any CPU + {44825FDA-68D2-4675-8B1D-6D5303DC38CF}.Release|x86.Build.0 = Release|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x64.Build.0 = Debug|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Debug|x86.Build.0 = Debug|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|Any CPU.Build.0 = Release|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x64.ActiveCfg = Release|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x64.Build.0 = Release|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x86.ActiveCfg = Release|Any CPU + {6D46DB08-C8D1-4F67-A6D0-D50FE84F19E0}.Release|x86.Build.0 = Release|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x64.Build.0 = Debug|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Debug|x86.Build.0 = Debug|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|Any CPU.Build.0 = Release|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x64.ActiveCfg = Release|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x64.Build.0 = Release|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x86.ActiveCfg = Release|Any CPU + {5E5EB0A7-7A19-4144-81FE-13C31DB678B2}.Release|x86.Build.0 = Release|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x64.Build.0 = Debug|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Debug|x86.Build.0 = Debug|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|Any CPU.Build.0 = Release|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x64.ActiveCfg = Release|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x64.Build.0 = Release|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x86.ActiveCfg = Release|Any CPU + {7F3D4F33-341A-44A1-96EA-A1729BC2E5D8}.Release|x86.Build.0 = Release|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x64.Build.0 = Debug|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Debug|x86.Build.0 = Debug|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|Any CPU.Build.0 = Release|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x64.ActiveCfg = Release|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x64.Build.0 = Release|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x86.ActiveCfg = Release|Any CPU + {B86C287A-734E-4527-A03E-6B970F22E27E}.Release|x86.Build.0 = Release|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x64.Build.0 = Debug|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Debug|x86.Build.0 = Debug|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|Any CPU.Build.0 = Release|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x64.ActiveCfg = Release|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x64.Build.0 = Release|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x86.ActiveCfg = Release|Any CPU + {E23FBF14-EE5B-49D4-8938-E8368CF4A4B5}.Release|x86.Build.0 = Release|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x64.ActiveCfg = Debug|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x64.Build.0 = Debug|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x86.ActiveCfg = Debug|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Debug|x86.Build.0 = Debug|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Release|Any CPU.Build.0 = Release|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Release|x64.ActiveCfg = Release|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Release|x64.Build.0 = Release|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Release|x86.ActiveCfg = Release|Any CPU + {50D014B5-99A6-46FC-B745-26687595B293}.Release|x86.Build.0 = Release|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x64.ActiveCfg = Debug|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x64.Build.0 = Debug|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x86.ActiveCfg = Debug|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Debug|x86.Build.0 = Debug|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|Any CPU.Build.0 = Release|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x64.ActiveCfg = Release|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x64.Build.0 = Release|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x86.ActiveCfg = Release|Any CPU + {D99C1F78-67EA-40E7-BD4C-985592F5265A}.Release|x86.Build.0 = Release|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x64.Build.0 = Debug|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Debug|x86.Build.0 = Debug|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|Any CPU.Build.0 = Release|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x64.ActiveCfg = Release|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x64.Build.0 = Release|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x86.ActiveCfg = Release|Any CPU + {1CBC0B9C-A96B-4143-B70F-37C69229FFF2}.Release|x86.Build.0 = Release|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x64.ActiveCfg = Debug|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x64.Build.0 = Debug|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x86.ActiveCfg = Debug|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Debug|x86.Build.0 = Debug|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|Any CPU.Build.0 = Release|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x64.ActiveCfg = Release|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x64.Build.0 = Release|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x86.ActiveCfg = Release|Any CPU + {760E2855-31B3-4CCB-BACB-34B7196A59B8}.Release|x86.Build.0 = Release|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x64.Build.0 = Debug|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Debug|x86.Build.0 = Debug|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Release|Any CPU.Build.0 = Release|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x64.ActiveCfg = Release|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x64.Build.0 = Release|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x86.ActiveCfg = Release|Any CPU + {3F688F21-7E31-4781-8995-9DD34276773F}.Release|x86.Build.0 = Release|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x64.ActiveCfg = Debug|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x64.Build.0 = Debug|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x86.ActiveCfg = Debug|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Debug|x86.Build.0 = Debug|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|Any CPU.Build.0 = Release|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x64.ActiveCfg = Release|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x64.Build.0 = Release|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x86.ActiveCfg = Release|Any CPU + {80AD7C4D-E4C6-4700-87AD-77B5698B338F}.Release|x86.Build.0 = Release|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x64.ActiveCfg = Debug|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x64.Build.0 = Debug|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x86.ActiveCfg = Debug|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Debug|x86.Build.0 = Debug|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|Any CPU.Build.0 = Release|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x64.ActiveCfg = Release|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x64.Build.0 = Release|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x86.ActiveCfg = Release|Any CPU + {60ABAB54-2EE9-4A16-A109-67F7B6F29184}.Release|x86.Build.0 = Release|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x64.ActiveCfg = Debug|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x64.Build.0 = Debug|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x86.ActiveCfg = Debug|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Debug|x86.Build.0 = Debug|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|Any CPU.Build.0 = Release|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x64.ActiveCfg = Release|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x64.Build.0 = Release|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x86.ActiveCfg = Release|Any CPU + {D32C1D26-C9A1-4F2A-9DBA-DBF0353E3972}.Release|x86.Build.0 = Release|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x64.Build.0 = Debug|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Debug|x86.Build.0 = Debug|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|Any CPU.Build.0 = Release|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x64.ActiveCfg = Release|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x64.Build.0 = Release|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x86.ActiveCfg = Release|Any CPU + {5CA4E28E-6305-4B21-AD2E-0DF24D47A65B}.Release|x86.Build.0 = Release|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x64.ActiveCfg = Debug|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x64.Build.0 = Debug|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x86.ActiveCfg = Debug|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Debug|x86.Build.0 = Debug|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|Any CPU.Build.0 = Release|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x64.ActiveCfg = Release|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x64.Build.0 = Release|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x86.ActiveCfg = Release|Any CPU + {05475C0A-C225-4F07-A3C7-9E17E660042E}.Release|x86.Build.0 = Release|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x64.Build.0 = Debug|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x86.ActiveCfg = Debug|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Debug|x86.Build.0 = Debug|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|Any CPU.Build.0 = Release|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x64.ActiveCfg = Release|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x64.Build.0 = Release|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x86.ActiveCfg = Release|Any CPU + {BA47D456-4657-4C86-A665-21293E3AC47F}.Release|x86.Build.0 = Release|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x64.ActiveCfg = Debug|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x64.Build.0 = Debug|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x86.ActiveCfg = Debug|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Debug|x86.Build.0 = Debug|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|Any CPU.Build.0 = Release|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x64.ActiveCfg = Release|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x64.Build.0 = Release|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x86.ActiveCfg = Release|Any CPU + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D}.Release|x86.Build.0 = Release|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x64.ActiveCfg = Debug|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x64.Build.0 = Debug|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x86.ActiveCfg = Debug|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Debug|x86.Build.0 = Debug|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|Any CPU.Build.0 = Release|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x64.ActiveCfg = Release|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x64.Build.0 = Release|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x86.ActiveCfg = Release|Any CPU + {C22333B3-D132-4960-A490-6BEF1EB1C917}.Release|x86.Build.0 = Release|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x64.Build.0 = Debug|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Debug|x86.Build.0 = Debug|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|Any CPU.Build.0 = Release|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x64.ActiveCfg = Release|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x64.Build.0 = Release|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x86.ActiveCfg = Release|Any CPU + {B8B15A8D-F647-41AE-A55F-A283A47E97C4}.Release|x86.Build.0 = Release|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x64.ActiveCfg = Debug|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x64.Build.0 = Debug|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x86.ActiveCfg = Debug|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Debug|x86.Build.0 = Debug|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|Any CPU.Build.0 = Release|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x64.ActiveCfg = Release|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x64.Build.0 = Release|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x86.ActiveCfg = Release|Any CPU + {99EC90D8-0D5E-41E4-A895-585A7680916C}.Release|x86.Build.0 = Release|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x64.Build.0 = Debug|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Debug|x86.Build.0 = Debug|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|Any CPU.Build.0 = Release|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x64.ActiveCfg = Release|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x64.Build.0 = Release|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x86.ActiveCfg = Release|Any CPU + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7}.Release|x86.Build.0 = Release|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|Any CPU.Build.0 = Debug|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x64.ActiveCfg = Debug|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x64.Build.0 = Debug|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x86.ActiveCfg = Debug|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Debug|x86.Build.0 = Debug|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|Any CPU.ActiveCfg = Release|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|Any CPU.Build.0 = Release|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x64.ActiveCfg = Release|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x64.Build.0 = Release|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x86.ActiveCfg = Release|Any CPU + {821C7F88-B775-4D3C-8D89-850B6C34E818}.Release|x86.Build.0 = Release|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x64.Build.0 = Debug|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Debug|x86.Build.0 = Debug|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|Any CPU.Build.0 = Release|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x64.ActiveCfg = Release|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x64.Build.0 = Release|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x86.ActiveCfg = Release|Any CPU + {3ABEAD26-B056-45CC-8F72-F40C8B8DBCBC}.Release|x86.Build.0 = Release|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x64.Build.0 = Debug|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Debug|x86.Build.0 = Debug|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|Any CPU.Build.0 = Release|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x64.ActiveCfg = Release|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x64.Build.0 = Release|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x86.ActiveCfg = Release|Any CPU + {3C500ECB-5422-4FFB-BD3D-48A850763D31}.Release|x86.Build.0 = Release|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x64.ActiveCfg = Debug|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x64.Build.0 = Debug|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x86.ActiveCfg = Debug|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Debug|x86.Build.0 = Debug|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|Any CPU.Build.0 = Release|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x64.ActiveCfg = Release|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x64.Build.0 = Release|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x86.ActiveCfg = Release|Any CPU + {D851E54A-5A44-4F74-9FDF-A2C32CACF651}.Release|x86.Build.0 = Release|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x64.ActiveCfg = Debug|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x64.Build.0 = Debug|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x86.ActiveCfg = Debug|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Debug|x86.Build.0 = Debug|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Release|Any CPU.Build.0 = Release|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Release|x64.ActiveCfg = Release|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Release|x64.Build.0 = Release|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Release|x86.ActiveCfg = Release|Any CPU + {866807B8-8E68-417C-8148-6450DEA68012}.Release|x86.Build.0 = Release|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x64.ActiveCfg = Debug|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x64.Build.0 = Debug|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x86.ActiveCfg = Debug|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Debug|x86.Build.0 = Debug|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Release|Any CPU.Build.0 = Release|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x64.ActiveCfg = Release|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x64.Build.0 = Release|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x86.ActiveCfg = Release|Any CPU + {20BE41BD-9C32-45B5-882A-C01491979633}.Release|x86.Build.0 = Release|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x64.Build.0 = Debug|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Debug|x86.Build.0 = Debug|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|Any CPU.Build.0 = Release|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x64.ActiveCfg = Release|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x64.Build.0 = Release|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x86.ActiveCfg = Release|Any CPU + {9E19FDB4-121A-4EF4-8A73-DFCDF04B19ED}.Release|x86.Build.0 = Release|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x64.Build.0 = Debug|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Debug|x86.Build.0 = Debug|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|Any CPU.Build.0 = Release|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x64.ActiveCfg = Release|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x64.Build.0 = Release|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x86.ActiveCfg = Release|Any CPU + {7C3A6012-6FC8-46A9-9966-1AC373614C41}.Release|x86.Build.0 = Release|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x64.Build.0 = Debug|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Debug|x86.Build.0 = Debug|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|Any CPU.Build.0 = Release|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x64.ActiveCfg = Release|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x64.Build.0 = Release|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x86.ActiveCfg = Release|Any CPU + {BC38594B-0B84-4657-9F7B-F2A0FC810F04}.Release|x86.Build.0 = Release|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x64.ActiveCfg = Debug|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x64.Build.0 = Debug|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x86.ActiveCfg = Debug|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Debug|x86.Build.0 = Debug|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|Any CPU.Build.0 = Release|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x64.ActiveCfg = Release|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x64.Build.0 = Release|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x86.ActiveCfg = Release|Any CPU + {20E0774F-86D5-4CD0-B636-E5212074FDE8}.Release|x86.Build.0 = Release|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x64.Build.0 = Debug|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Debug|x86.Build.0 = Debug|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|Any CPU.Build.0 = Release|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x64.ActiveCfg = Release|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x64.Build.0 = Release|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x86.ActiveCfg = Release|Any CPU + {FE668D8D-AB46-41F4-A82F-8A3330C4D152}.Release|x86.Build.0 = Release|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Debug|x64.ActiveCfg = Debug|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Debug|x64.Build.0 = Debug|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Debug|x86.ActiveCfg = Debug|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Debug|x86.Build.0 = Debug|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Release|Any CPU.Build.0 = Release|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Release|x64.ActiveCfg = Release|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Release|x64.Build.0 = Release|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Release|x86.ActiveCfg = Release|Any CPU + {548C296A-476B-433D-9552-923648BDFA97}.Release|x86.Build.0 = Release|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x64.Build.0 = Debug|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Debug|x86.Build.0 = Debug|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|Any CPU.Build.0 = Release|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x64.ActiveCfg = Release|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x64.Build.0 = Release|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x86.ActiveCfg = Release|Any CPU + {3510DF3E-E822-4FB1-8C65-ED6DBAD223D4}.Release|x86.Build.0 = Release|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x64.ActiveCfg = Debug|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x64.Build.0 = Debug|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x86.ActiveCfg = Debug|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Debug|x86.Build.0 = Debug|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|Any CPU.Build.0 = Release|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x64.ActiveCfg = Release|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x64.Build.0 = Release|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x86.ActiveCfg = Release|Any CPU + {C733F161-FCED-4D21-BC83-5CC079E93547}.Release|x86.Build.0 = Release|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x64.Build.0 = Debug|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Debug|x86.Build.0 = Debug|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|Any CPU.Build.0 = Release|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x64.ActiveCfg = Release|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x64.Build.0 = Release|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x86.ActiveCfg = Release|Any CPU + {76E1E74F-41C1-4E24-85EA-ED13F28B80B1}.Release|x86.Build.0 = Release|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x64.Build.0 = Debug|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Debug|x86.Build.0 = Debug|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|Any CPU.Build.0 = Release|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x64.ActiveCfg = Release|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x64.Build.0 = Release|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x86.ActiveCfg = Release|Any CPU + {EC73D558-0472-49E2-B46E-D26F9686AA9C}.Release|x86.Build.0 = Release|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x64.Build.0 = Debug|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Debug|x86.Build.0 = Debug|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|Any CPU.Build.0 = Release|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x64.ActiveCfg = Release|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x64.Build.0 = Release|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x86.ActiveCfg = Release|Any CPU + {1E532EAB-8DB7-42DF-A9BD-BBBA08C8148F}.Release|x86.Build.0 = Release|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x64.Build.0 = Debug|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Debug|x86.Build.0 = Debug|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|Any CPU.Build.0 = Release|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x64.ActiveCfg = Release|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x64.Build.0 = Release|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x86.ActiveCfg = Release|Any CPU + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5}.Release|x86.Build.0 = Release|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x64.Build.0 = Debug|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Debug|x86.Build.0 = Debug|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|Any CPU.Build.0 = Release|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x64.ActiveCfg = Release|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x64.Build.0 = Release|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x86.ActiveCfg = Release|Any CPU + {6BE16682-4FB9-49C7-A2B3-ECB4EC5EF8BD}.Release|x86.Build.0 = Release|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x64.Build.0 = Debug|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Debug|x86.Build.0 = Debug|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|Any CPU.Build.0 = Release|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x64.ActiveCfg = Release|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x64.Build.0 = Release|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x86.ActiveCfg = Release|Any CPU + {D3D47993-27D3-4C90-9C8E-14652807DAF5}.Release|x86.Build.0 = Release|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x64.Build.0 = Debug|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Debug|x86.Build.0 = Debug|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|Any CPU.Build.0 = Release|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x64.ActiveCfg = Release|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x64.Build.0 = Release|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x86.ActiveCfg = Release|Any CPU + {4D167781-1AC0-46CF-A32E-1B6E048940B2}.Release|x86.Build.0 = Release|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x64.Build.0 = Debug|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Debug|x86.Build.0 = Debug|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|Any CPU.Build.0 = Release|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x64.ActiveCfg = Release|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x64.Build.0 = Release|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x86.ActiveCfg = Release|Any CPU + {5F9B7682-71E2-4989-9BC9-014A2C26AF50}.Release|x86.Build.0 = Release|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x64.Build.0 = Debug|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Debug|x86.Build.0 = Debug|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|Any CPU.Build.0 = Release|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x64.ActiveCfg = Release|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x64.Build.0 = Release|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x86.ActiveCfg = Release|Any CPU + {C3AEAEE7-038E-45FF-892B-DB18EE29F790}.Release|x86.Build.0 = Release|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x64.Build.0 = Debug|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Debug|x86.Build.0 = Debug|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|Any CPU.Build.0 = Release|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x64.ActiveCfg = Release|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x64.Build.0 = Release|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x86.ActiveCfg = Release|Any CPU + {7FACF6B4-7E12-4543-AAD4-0072FA1ECE0E}.Release|x86.Build.0 = Release|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x64.ActiveCfg = Debug|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x64.Build.0 = Debug|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x86.ActiveCfg = Debug|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Debug|x86.Build.0 = Debug|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|Any CPU.Build.0 = Release|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x64.ActiveCfg = Release|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x64.Build.0 = Release|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x86.ActiveCfg = Release|Any CPU + {1561D597-922F-486E-ACF4-98250DDC5CDA}.Release|x86.Build.0 = Release|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x64.Build.0 = Debug|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Debug|x86.Build.0 = Debug|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|Any CPU.Build.0 = Release|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x64.ActiveCfg = Release|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x64.Build.0 = Release|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x86.ActiveCfg = Release|Any CPU + {D7B25EC1-CDC8-4D2D-8569-826568E1AAD2}.Release|x86.Build.0 = Release|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Debug|x64.ActiveCfg = Debug|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Debug|x64.Build.0 = Debug|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Debug|x86.ActiveCfg = Debug|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Debug|x86.Build.0 = Debug|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Release|Any CPU.Build.0 = Release|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Release|x64.ActiveCfg = Release|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Release|x64.Build.0 = Release|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Release|x86.ActiveCfg = Release|Any CPU + {9369FA32-E98A-4180-9251-914925188086}.Release|x86.Build.0 = Release|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x64.Build.0 = Debug|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Debug|x86.Build.0 = Debug|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|Any CPU.Build.0 = Release|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x64.ActiveCfg = Release|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x64.Build.0 = Release|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x86.ActiveCfg = Release|Any CPU + {67650687-2E32-40BB-9849-C4ABBA65A7CF}.Release|x86.Build.0 = Release|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x64.Build.0 = Debug|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Debug|x86.Build.0 = Debug|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|Any CPU.Build.0 = Release|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x64.ActiveCfg = Release|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x64.Build.0 = Release|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x86.ActiveCfg = Release|Any CPU + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE}.Release|x86.Build.0 = Release|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Debug|x64.Build.0 = Debug|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Debug|x86.Build.0 = Debug|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Release|Any CPU.Build.0 = Release|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Release|x64.ActiveCfg = Release|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Release|x64.Build.0 = Release|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Release|x86.ActiveCfg = Release|Any CPU + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2}.Release|x86.Build.0 = Release|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Debug|x64.Build.0 = Debug|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Debug|x86.Build.0 = Debug|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Release|Any CPU.Build.0 = Release|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Release|x64.ActiveCfg = Release|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Release|x64.Build.0 = Release|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Release|x86.ActiveCfg = Release|Any CPU + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8}.Release|x86.Build.0 = Release|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Debug|x64.Build.0 = Debug|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Debug|x86.Build.0 = Debug|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Release|Any CPU.Build.0 = Release|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Release|x64.ActiveCfg = Release|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Release|x64.Build.0 = Release|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Release|x86.ActiveCfg = Release|Any CPU + {60BA1521-B6FC-43F6-ABEF-4471A06289E1}.Release|x86.Build.0 = Release|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Debug|x64.Build.0 = Debug|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Debug|x86.Build.0 = Debug|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Release|Any CPU.Build.0 = Release|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Release|x64.ActiveCfg = Release|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Release|x64.Build.0 = Release|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Release|x86.ActiveCfg = Release|Any CPU + {6F482CF0-1D61-45EF-859C-6242C8BA08F4}.Release|x86.Build.0 = Release|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Debug|x64.ActiveCfg = Debug|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Debug|x64.Build.0 = Debug|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Debug|x86.ActiveCfg = Debug|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Debug|x86.Build.0 = Debug|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Release|Any CPU.Build.0 = Release|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Release|x64.ActiveCfg = Release|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Release|x64.Build.0 = Release|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Release|x86.ActiveCfg = Release|Any CPU + {3AF4251B-91CF-4024-88B2-B77A9005604A}.Release|x86.Build.0 = Release|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Debug|x64.ActiveCfg = Debug|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Debug|x64.Build.0 = Debug|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Debug|x86.ActiveCfg = Debug|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Debug|x86.Build.0 = Debug|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Release|Any CPU.Build.0 = Release|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Release|x64.ActiveCfg = Release|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Release|x64.Build.0 = Release|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Release|x86.ActiveCfg = Release|Any CPU + {9EB54427-92C8-40F1-8425-26DF6ABF7109}.Release|x86.Build.0 = Release|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Debug|x64.ActiveCfg = Debug|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Debug|x64.Build.0 = Debug|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Debug|x86.ActiveCfg = Debug|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Debug|x86.Build.0 = Debug|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Release|Any CPU.Build.0 = Release|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Release|x64.ActiveCfg = Release|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Release|x64.Build.0 = Release|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Release|x86.ActiveCfg = Release|Any CPU + {52B52BCE-54BA-45B7-8EC2-B547B540EE72}.Release|x86.Build.0 = Release|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Debug|x64.Build.0 = Debug|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Debug|x86.Build.0 = Debug|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Release|Any CPU.Build.0 = Release|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Release|x64.ActiveCfg = Release|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Release|x64.Build.0 = Release|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Release|x86.ActiveCfg = Release|Any CPU + {F8130985-63AB-4102-8CCA-3537829250AD}.Release|x86.Build.0 = Release|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Debug|x64.ActiveCfg = Debug|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Debug|x64.Build.0 = Debug|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Debug|x86.ActiveCfg = Debug|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Debug|x86.Build.0 = Debug|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Release|Any CPU.Build.0 = Release|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Release|x64.ActiveCfg = Release|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Release|x64.Build.0 = Release|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Release|x86.ActiveCfg = Release|Any CPU + {65559EA9-DE79-4DFA-AD14-C077BE5F0422}.Release|x86.Build.0 = Release|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Debug|x64.ActiveCfg = Debug|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Debug|x64.Build.0 = Debug|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Debug|x86.ActiveCfg = Debug|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Debug|x86.Build.0 = Debug|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Release|Any CPU.Build.0 = Release|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Release|x64.ActiveCfg = Release|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Release|x64.Build.0 = Release|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Release|x86.ActiveCfg = Release|Any CPU + {C96771C6-7567-4CD4-89F2-4A397B6E2846}.Release|x86.Build.0 = Release|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Debug|x64.Build.0 = Debug|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Debug|x86.Build.0 = Debug|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Release|Any CPU.Build.0 = Release|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Release|x64.ActiveCfg = Release|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Release|x64.Build.0 = Release|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Release|x86.ActiveCfg = Release|Any CPU + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA}.Release|x86.Build.0 = Release|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Debug|x64.ActiveCfg = Debug|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Debug|x64.Build.0 = Debug|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Debug|x86.ActiveCfg = Debug|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Debug|x86.Build.0 = Debug|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Release|Any CPU.Build.0 = Release|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Release|x64.ActiveCfg = Release|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Release|x64.Build.0 = Release|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Release|x86.ActiveCfg = Release|Any CPU + {49332975-D217-4256-9EA4-892569FD8347}.Release|x86.Build.0 = Release|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Debug|x64.Build.0 = Debug|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Debug|x86.Build.0 = Debug|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Release|Any CPU.Build.0 = Release|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Release|x64.ActiveCfg = Release|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Release|x64.Build.0 = Release|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Release|x86.ActiveCfg = Release|Any CPU + {3A1DBF26-7F97-4643-BC50-F888F5F451EC}.Release|x86.Build.0 = Release|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Debug|x64.Build.0 = Debug|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Debug|x86.Build.0 = Debug|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Release|Any CPU.Build.0 = Release|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Release|x64.ActiveCfg = Release|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Release|x64.Build.0 = Release|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Release|x86.ActiveCfg = Release|Any CPU + {1A894DB5-D8A6-4254-A769-F7BE42103CF3}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {361838C4-72E2-1C48-5D76-CA6D1A861242} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {D9F91EA0-8AF5-452A-86D8-52BACB2E39CB} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {5DBE2E9E-9905-47CE-B8DC-B25409AF1EF2} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {8BCEAAFC-9168-4CC0-AFDB-177E5F7C15C6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {46D35B4F-6A04-47FF-958B-5E6A73FCC059} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {44A1241B-8ECF-4AFA-9972-452C39AD43D6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {85AB3BB7-C493-4387-B39A-EB299AC37312} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {5C5E91CA-3F98-4E9A-922B-F6415EABD1A3} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {93DB06DC-B254-48A9-8F2C-6130A5658F27} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {03CA315C-8AA1-4CEA-A28B-5EB35C586F4A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {C6DC3C29-C2AD-4015-8872-42E95A0FE63F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {40094279-250C-42AE-992A-856718FEFBAC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {B2967228-F8F7-4931-B257-1C63CB58CE1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {6D52EC2B-0A1A-4693-A8EE-5AB32A4A3ED9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {37F203A3-624E-4794-9C99-16CAC22C17DF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {3FF93987-A30A-4D50-8815-7CF3BB7CAE05} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {AACE8717-0760-42F2-A225-8FCCE876FB65} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {4AAD6965-E879-44AD-A8ED-E1D713A3CD6D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {85D82A87-1F4A-4B1B-8422-5B7A7B7704E3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {FE227DF2-875D-4BEA-A4E0-14EA7F3EC1D0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {D0FB54BA-4D14-4A32-B09F-7EC94F369460} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {69C9E010-CBDD-4B89-84CF-7AB56D6A078A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E471176A-E1F3-4DE5-8D30-0865903A217A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {FA013511-DF20-45F7-8077-EBA2D6224D64} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {B9F84697-54FE-4648-B173-EE3D904FFA4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {6751A76C-8ED8-40F4-AE2B-069DB31395FE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {DDBFA2EF-9CAE-473F-A438-369CAC25C66A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {063DE5E1-C8FE-47D0-A12A-22A25CDF2C22} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {35350FAB-FC51-4FE8-81FB-011003134C37} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {1BFC95B4-4C8A-44B2-903A-11FBCAAB9519} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {C4A65377-22F7-4D15-92A3-4F05847D167E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {BDDE59E1-C643-4C87-8608-0F9A7A54DE09} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0CC116C8-A7E5-4B94-9688-32920177FF97} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E8862F6E-85C1-4FDB-AA92-0BB489B7EA1E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {84DEDF05-A5BD-4644-86B9-6B7918FE3F31} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {9DEB1F54-94B5-40C4-AC44-220E680B016D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {7C3E87F2-93D8-4968-95E3-52C46947D46C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {C0504D97-9BCD-4AE4-B0DC-B31C17B150F2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {31B05493-104F-437F-9FA7-CA5286CE697C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {937AF12E-D770-4534-8FF8-C59042609C2A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5A028B04-9D76-470B-B5B3-766CE4CE860C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {749DE4C8-F733-43F8-B2A8-6649E71C7570} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {56D2C79E-2737-4FF9-9D19-150065F568D5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E41F6DC4-68B5-4EE3-97AE-801D725A2C13} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {285F1D0F-501F-4E2E-8FA0-F2CF28AE3798} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {26055403-C7F5-4709-8813-0F7387102791} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0C00D0DA-C4C3-4B23-941F-A3DB2DBF33AF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {258327E9-431E-475C-933B-50893676E452} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {42AF60C8-A5E1-40E0-86F8-98256364AF6F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {88C6A9C3-B433-4C36-8767-429C8C2396F8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {6B7099AB-01BF-4EC4-87D0-5C9C032266DE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {14C918EA-693E-41FE-ACAE-2E82DF077BEA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {81111B26-74F6-4912-9084-7115FD119945} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {80E2D661-FF3E-4A10-A2DF-AFD4F3D433FE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {8D0F501D-01B1-4E24-958B-FAF35B267705} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5BA91095-7F10-4717-B296-49DFBFC1C9C2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {99616566-4EF1-4DC7-B655-825FE43D203D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {EE3C03AD-E604-4C57-9B78-CF7F49FBFCB0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A3B19095-2D95-4B09-B07E-2C082C72394B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {807837AF-B392-4589-ADF1-3FDB34D6C5BF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {64EAFDCF-8283-4D5C-AC78-7969D5FE926A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {68F4D8A1-E32F-487A-B460-325F36989BE3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {4A3DA4AE-7B88-4674-A7E2-F5D42B8256F2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {606C751B-7CF1-47CF-A25C-9248A55C814F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0BE44D0A-CC4B-4E84-8AF3-D8D99551C431} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {CC4CCE5F-55BC-4745-A204-4FA92BC1BADC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {99BAE717-9A2E-41F5-9ECC-5FB97E4A6066} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5CCE0DB7-C115-4B21-A7AE-C8488C22A853} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A09C9E66-5496-47EC-8B23-9EEB7CBDC75E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {06DC817F-A936-4F83-8929-E00622B32245} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {2C999476-0291-4161-B3E9-1AA99A3B1139} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {476EAADA-1B39-4049-ABE4-CCAC21FFE9E2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0EF56124-E6E8-4E89-95DD-5A5D5FF05A98} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0DBB9FC4-2E46-4C3E-BE88-2A8DCB59DB7D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {8A40142F-E8C8-4E86-BE70-7DD4AB1FFDEE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {C9D20F74-EE5F-4C9E-9AB1-C03E90B34F92} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {50140A32-6D3C-47DB-983A-7166CBA51845} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {031979F2-6ABA-444F-A6A4-80115DC487CE} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {D71B0DA5-80A3-419E-898D-40E77A9A7F19} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {B2C877D9-B521-4901-8817-76B5DAA62FCE} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {08D3B6D0-3CE8-4F24-A6F1-BCAB01AD6278} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {7116DD6B-2491-49E1-AB27-5210E949F753} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {7DBE31A6-D2FD-499E-B675-4092723175AD} = {361838C4-72E2-1C48-5D76-CA6D1A861242} + {D99E6EAE-D278-4480-AA67-85F025383E47} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {D3825714-3DDA-44B7-A99C-5F3E65716691} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {FAB78D21-7372-48FE-B2C3-DE1807F1157D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {EADFA337-B0FA-4712-A24A-7C08235BDF98} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {110F7EC2-3149-4D1B-A972-E69E79F1EBF5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A2E3F03A-0CAD-4E2A-8C71-DDEBB1B7E4F7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {3A1AF0AD-4DAE-4D82-9CCF-2DCB83CC3679} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {F1DF0F07-1BCB-4B55-8353-07BF8A4B2A67} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {31277AFF-9BFF-4C17-8593-B562A385058E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {3A8F090F-678D-46E2-8899-67402129749C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {05D844B6-51C1-4926-919C-D99E24FB3BC9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {03E15545-D6A0-4287-A88C-6EDE77C0DCBE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {BA47D456-4657-4C86-A665-21293E3AC47F} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} + {49EF86AC-1CC2-4A24-8637-C5151E23DF9D} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} + {C22333B3-D132-4960-A490-6BEF1EB1C917} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} + {B8B15A8D-F647-41AE-A55F-A283A47E97C4} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} + {99EC90D8-0D5E-41E4-A895-585A7680916C} = {78C966F5-2242-D8EC-ADCA-A1A9C7F723A6} + {F1F029E6-2E4B-4A42-8D8F-AB325EE3B608} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {CBE6E3D8-230C-4513-B98F-99D82B83B9F7} = {F1F029E6-2E4B-4A42-8D8F-AB325EE3B608} + {821C7F88-B775-4D3C-8D89-850B6C34E818} = {F1F029E6-2E4B-4A42-8D8F-AB325EE3B608} + {CBDF819E-923F-A07F-78D9-D599DD28197E} = {1553F566-661E-A2F5-811B-F74BF45C44CC} + {D8B22C17-28E9-4059-97C5-4AC4600A2BD5} = {CBDF819E-923F-A07F-78D9-D599DD28197E} + {CADD452F-3F55-4FD8-BB01-5A5EE5AF99EE} = {41F15E67-7190-CF23-3BC4-77E87134CADD} + {045CC5F7-9456-2DBC-9E26-760A1C32B2C9} = {704A59BF-CC38-09FA-CE4F-73B27EC8F04F} + {3FFCCEE7-53DB-450A-8E90-7ED8A17540A2} = {045CC5F7-9456-2DBC-9E26-760A1C32B2C9} + {C9ADF5BA-B167-48BE-986C-57CC335DCFF8} = {045CC5F7-9456-2DBC-9E26-760A1C32B2C9} + {166ECC12-EF41-266B-D99C-4764D5FBD04E} = {00227F43-A2B4-2312-24D4-35D99B2D62BA} + {60BA1521-B6FC-43F6-ABEF-4471A06289E1} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {6F482CF0-1D61-45EF-859C-6242C8BA08F4} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {3AF4251B-91CF-4024-88B2-B77A9005604A} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {9EB54427-92C8-40F1-8425-26DF6ABF7109} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {52B52BCE-54BA-45B7-8EC2-B547B540EE72} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {F8130985-63AB-4102-8CCA-3537829250AD} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {65559EA9-DE79-4DFA-AD14-C077BE5F0422} = {166ECC12-EF41-266B-D99C-4764D5FBD04E} + {C96771C6-7567-4CD4-89F2-4A397B6E2846} = {41F15E67-7190-CF23-3BC4-77E87134CADD} + {A7CFF7F0-6892-4B58-8BBD-D494D8D858EA} = {41F15E67-7190-CF23-3BC4-77E87134CADD} + {85DDD19D-B5BC-B585-C0A3-6A6133E51DB6} = {D8C5582A-D723-AE4A-ECC5-D8DF76468E74} + {49332975-D217-4256-9EA4-892569FD8347} = {85DDD19D-B5BC-B585-C0A3-6A6133E51DB6} + {3A1DBF26-7F97-4643-BC50-F888F5F451EC} = {045CC5F7-9456-2DBC-9E26-760A1C32B2C9} + {1A894DB5-D8A6-4254-A769-F7BE42103CF3} = {045CC5F7-9456-2DBC-9E26-760A1C32B2C9} + EndGlobalSection +EndGlobal diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/Planning/TaskPackPlanner.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/Planning/TaskPackPlanner.cs index 80a8adb1..a47af7b3 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/Planning/TaskPackPlanner.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/Planning/TaskPackPlanner.cs @@ -1,19 +1,26 @@ -using System.Collections.Immutable; -using System.Linq; -using System.Text.Json.Nodes; -using StellaOps.TaskRunner.Core.Expressions; -using StellaOps.TaskRunner.Core.TaskPacks; +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Text.Json.Nodes; +using StellaOps.AirGap.Policy; +using StellaOps.TaskRunner.Core.Expressions; +using StellaOps.TaskRunner.Core.TaskPacks; namespace StellaOps.TaskRunner.Core.Planning; public sealed class TaskPackPlanner { - private readonly TaskPackManifestValidator validator; - - public TaskPackPlanner() - { - validator = new TaskPackManifestValidator(); - } + private static readonly string[] NetworkParameterHints = { "url", "uri", "endpoint", "host", "registry", "mirror", "address" }; + + private readonly TaskPackManifestValidator validator; + private readonly IEgressPolicy? egressPolicy; + + public TaskPackPlanner(IEgressPolicy? egressPolicy = null) + { + validator = new TaskPackManifestValidator(); + this.egressPolicy = egressPolicy; + } public TaskPackPlanResult Plan(TaskPackManifest manifest, IDictionary? providedInputs = null) { @@ -50,14 +57,17 @@ public sealed class TaskPackPlanner var context = TaskPackExpressionContext.Create(effectiveInputs, stepTracker, secretTracker); - var planSteps = new List(); - var steps = manifest.Spec.Steps; - for (var i = 0; i < steps.Count; i++) - { - var step = steps[i]; - var planStep = BuildStep(step, context, $"spec.steps[{i}]", errors); - planSteps.Add(planStep); - } + var packName = manifest.Metadata.Name; + var packVersion = manifest.Metadata.Version; + + var planSteps = new List(); + var steps = manifest.Spec.Steps; + for (var i = 0; i < steps.Count; i++) + { + var step = steps[i]; + var planStep = BuildStep(packName, packVersion, step, context, $"spec.steps[{i}]", errors); + planSteps.Add(planStep); + } if (errors.Count > 0) { @@ -70,13 +80,13 @@ public sealed class TaskPackPlanner manifest.Metadata.Description, manifest.Metadata.Tags?.ToList() ?? new List()); - var planApprovals = manifest.Spec.Approvals? - .Select(approval => new TaskPackPlanApproval( - approval.Id, - approval.Grants?.ToList() ?? new List(), - approval.ExpiresAfter, - approval.ReasonTemplate)) - .ToList() ?? new List(); + var planApprovals = manifest.Spec.Approvals? + .Select(approval => new TaskPackPlanApproval( + approval.Id, + NormalizeGrants(approval.Grants), + approval.ExpiresAfter, + approval.ReasonTemplate)) + .ToList() ?? new List(); var planSecrets = manifest.Spec.Secrets? .Select(secret => new TaskPackPlanSecret(secret.Name, secret.Scope, secret.Description)) @@ -134,11 +144,13 @@ public sealed class TaskPackPlanner return effective; } - private TaskPackPlanStep BuildStep( - TaskPackStep step, - TaskPackExpressionContext context, - string path, - ImmutableArray.Builder errors) + private TaskPackPlanStep BuildStep( + string packName, + string packVersion, + TaskPackStep step, + TaskPackExpressionContext context, + string path, + ImmutableArray.Builder errors) { if (!TaskPackExpressions.TryEvaluateBoolean(step.When, context, out var enabled, out var whenError)) { @@ -146,23 +158,23 @@ public sealed class TaskPackPlanner enabled = false; } - TaskPackPlanStep planStep; - - if (step.Run is not null) - { - planStep = BuildRunStep(step, step.Run, context, path, enabled, errors); + TaskPackPlanStep planStep; + + if (step.Run is not null) + { + planStep = BuildRunStep(packName, packVersion, step, step.Run, context, path, enabled, errors); + } + else if (step.Gate is not null) + { + planStep = BuildGateStep(step, step.Gate, context, path, enabled, errors); } - else if (step.Gate is not null) - { - planStep = BuildGateStep(step, step.Gate, context, path, enabled, errors); - } - else if (step.Parallel is not null) - { - planStep = BuildParallelStep(step, step.Parallel, context, path, enabled, errors); - } - else if (step.Map is not null) - { - planStep = BuildMapStep(step, step.Map, context, path, enabled, errors); + else if (step.Parallel is not null) + { + planStep = BuildParallelStep(packName, packVersion, step, step.Parallel, context, path, enabled, errors); + } + else if (step.Map is not null) + { + planStep = BuildMapStep(packName, packVersion, step, step.Map, context, path, enabled, errors); } else { @@ -174,28 +186,235 @@ public sealed class TaskPackPlanner return planStep; } - private TaskPackPlanStep BuildRunStep( - TaskPackStep step, - TaskPackRunStep run, - TaskPackExpressionContext context, - string path, - bool enabled, + private TaskPackPlanStep BuildRunStep( + string packName, + string packVersion, + TaskPackStep step, + TaskPackRunStep run, + TaskPackExpressionContext context, + string path, + bool enabled, ImmutableArray.Builder errors) { - var parameters = ResolveParameters(run.With, context, $"{path}.run", errors); - - return new TaskPackPlanStep( - step.Id, - step.Id, - step.Name, - "run", - enabled, - run.Uses, - parameters, - ApprovalId: null, - GateMessage: null, - Children: null); - } + var parameters = ResolveParameters(run.With, context, $"{path}.run", errors); + + if (egressPolicy?.IsSealed == true) + { + ValidateRunStepEgress(packName, packVersion, step, run, parameters, path, errors); + } + + return new TaskPackPlanStep( + step.Id, + step.Id, + step.Name, + "run", + enabled, + run.Uses, + parameters, + ApprovalId: null, + GateMessage: null, + Children: null); + } + + private void ValidateRunStepEgress( + string packName, + string packVersion, + TaskPackStep step, + TaskPackRunStep run, + IReadOnlyDictionary? parameters, + string path, + ImmutableArray.Builder errors) + { + if (egressPolicy is null || !egressPolicy.IsSealed) + { + return; + } + + var destinations = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + void AddDestination(Uri uri) + { + if (seen.Add(uri.ToString())) + { + destinations.Add(uri); + } + } + + if (run.Egress is not null) + { + for (var i = 0; i < run.Egress.Count; i++) + { + var entry = run.Egress[i]; + var entryPath = $"{path}.egress[{i}]"; + if (entry is null) + { + continue; + } + + if (TryParseNetworkUri(entry.Url, out var uri)) + { + AddDestination(uri); + } + else + { + errors.Add(new TaskPackPlanError($"{entryPath}.url", "Egress URL must be an absolute HTTP or HTTPS address.")); + } + } + } + + var requiresRuntimeNetwork = false; + + if (parameters is not null) + { + foreach (var parameter in parameters) + { + var value = parameter.Value; + if (value.Value is JsonValue jsonValue && jsonValue.TryGetValue(out var literal) && TryParseNetworkUri(literal, out var uri)) + { + AddDestination(uri); + } + else if (value.RequiresRuntimeValue && MightBeNetworkParameter(parameter.Key)) + { + requiresRuntimeNetwork = true; + } + } + } + + if (destinations.Count == 0) + { + if (requiresRuntimeNetwork && (run.Egress is null || run.Egress.Count == 0)) + { + errors.Add(new TaskPackPlanError(path, $"Step '{step.Id}' references runtime network parameters while sealed mode is enabled. Declare explicit run.egress URLs or remove external calls.")); + } + + return; + } + + foreach (var destination in destinations) + { + try + { + var request = new EgressRequest( + component: "TaskRunner", + destination: destination, + intent: $"taskpack:{packName}@{packVersion}:{step.Id}", + transport: DetermineTransport(destination), + operation: run.Uses); + + egressPolicy.EnsureAllowed(request); + } + catch (AirGapEgressBlockedException blocked) + { + var remediation = blocked.Remediation; + errors.Add(new TaskPackPlanError( + path, + $"Step '{step.Id}' attempted to reach '{destination}' in sealed mode and was blocked. Reason: {blocked.Reason}. Remediation: {remediation}")); + } + } + } + + private static bool TryParseNetworkUri(string? value, out Uri uri) + { + uri = default!; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (!Uri.TryCreate(value, UriKind.Absolute, out var parsed)) + { + return false; + } + + if (!IsNetworkScheme(parsed)) + { + return false; + } + + uri = parsed; + return true; + } + + private static bool IsNetworkScheme(Uri uri) + => string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase); + + private static bool MightBeNetworkParameter(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + foreach (var hint in NetworkParameterHints) + { + if (name.Contains(hint, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static EgressTransport DetermineTransport(Uri destination) + => string.Equals(destination.Scheme, "https", StringComparison.OrdinalIgnoreCase) + ? EgressTransport.Https + : string.Equals(destination.Scheme, "http", StringComparison.OrdinalIgnoreCase) + ? EgressTransport.Http + : EgressTransport.Any; + + private static IReadOnlyList NormalizeGrants(IReadOnlyList? grants) + { + if (grants is null || grants.Count == 0) + { + return Array.Empty(); + } + + var normalized = new List(grants.Count); + + foreach (var grant in grants) + { + if (string.IsNullOrWhiteSpace(grant)) + { + continue; + } + + var segments = grant + .Split('.', StringSplitOptions.RemoveEmptyEntries) + .Select(segment => + { + var trimmed = segment.Trim(); + if (trimmed.Length == 0) + { + return string.Empty; + } + + if (trimmed.Length == 1) + { + return trimmed.ToUpperInvariant(); + } + + var first = char.ToUpperInvariant(trimmed[0]); + var rest = trimmed[1..].ToLowerInvariant(); + return string.Concat(first, rest); + }) + .Where(segment => segment.Length > 0) + .ToArray(); + + if (segments.Length == 0) + { + continue; + } + + normalized.Add(string.Join('.', segments)); + } + + return normalized.Count == 0 + ? Array.Empty() + : normalized; + } private TaskPackPlanStep BuildGateStep( TaskPackStep step, @@ -238,20 +457,22 @@ public sealed class TaskPackPlanner Children: null); } - private TaskPackPlanStep BuildParallelStep( - TaskPackStep step, - TaskPackParallelStep parallel, - TaskPackExpressionContext context, - string path, - bool enabled, - ImmutableArray.Builder errors) - { - var children = new List(); - for (var i = 0; i < parallel.Steps.Count; i++) - { - var child = BuildStep(parallel.Steps[i], context, $"{path}.parallel.steps[{i}]", errors); - children.Add(child); - } + private TaskPackPlanStep BuildParallelStep( + string packName, + string packVersion, + TaskPackStep step, + TaskPackParallelStep parallel, + TaskPackExpressionContext context, + string path, + bool enabled, + ImmutableArray.Builder errors) + { + var children = new List(); + for (var i = 0; i < parallel.Steps.Count; i++) + { + var child = BuildStep(packName, packVersion, parallel.Steps[i], context, $"{path}.parallel.steps[{i}]", errors); + children.Add(child); + } var parameters = new Dictionary(StringComparer.Ordinal); if (parallel.MaxParallel.HasValue) @@ -274,12 +495,14 @@ public sealed class TaskPackPlanner Children: children); } - private TaskPackPlanStep BuildMapStep( - TaskPackStep step, - TaskPackMapStep map, - TaskPackExpressionContext context, - string path, - bool enabled, + private TaskPackPlanStep BuildMapStep( + string packName, + string packVersion, + TaskPackStep step, + TaskPackMapStep map, + TaskPackExpressionContext context, + string path, + bool enabled, ImmutableArray.Builder errors) { var parameters = new Dictionary(StringComparer.Ordinal); @@ -324,7 +547,7 @@ public sealed class TaskPackPlanner var item = itemsArray[i]; var iterationContext = context.WithItem(item); var iterationPath = $"{path}.map.step[{i}]"; - var templateStep = BuildStep(map.Step, iterationContext, iterationPath, errors); + var templateStep = BuildStep(packName, packVersion, map.Step, iterationContext, iterationPath, errors); var childId = $"{step.Id}[{i}]::{map.Step.Id}"; var iterationParameters = templateStep.Parameters is null diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/StellaOps.TaskRunner.Core.csproj b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/StellaOps.TaskRunner.Core.csproj index 80bf73b0..aae141bd 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/StellaOps.TaskRunner.Core.csproj +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/StellaOps.TaskRunner.Core.csproj @@ -15,8 +15,12 @@ - - - - - + + + + + + + + + diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifest.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifest.cs index a6496749..55bf1239 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifest.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifest.cs @@ -150,18 +150,33 @@ public sealed class TaskPackStep public TaskPackMapStep? Map { get; init; } } -public sealed class TaskPackRunStep -{ - [JsonPropertyName("uses")] - public required string Uses { get; init; } - - [JsonPropertyName("with")] - public IDictionary? With { get; init; } -} - -public sealed class TaskPackGateStep -{ - [JsonPropertyName("approval")] +public sealed class TaskPackRunStep +{ + [JsonPropertyName("uses")] + public required string Uses { get; init; } + + [JsonPropertyName("with")] + public IDictionary? With { get; init; } + + [JsonPropertyName("egress")] + public IReadOnlyList? Egress { get; init; } +} + +public sealed class TaskPackRunEgress +{ + [JsonPropertyName("url")] + public required string Url { get; init; } + + [JsonPropertyName("intent")] + public string? Intent { get; init; } + + [JsonPropertyName("description")] + public string? Description { get; init; } +} + +public sealed class TaskPackGateStep +{ + [JsonPropertyName("approval")] public TaskPackApprovalGate? Approval { get; init; } [JsonPropertyName("policy")] diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifestValidator.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifestValidator.cs index 413149d3..4c41dd56 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifestValidator.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Core/TaskPacks/TaskPackManifestValidator.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Immutable; using System.Text.RegularExpressions; using System.Linq; @@ -134,10 +135,10 @@ public sealed class TaskPackManifestValidator errors.Add(new TaskPackManifestValidationError(path, "Step may define only one of run, gate, parallel, or map.")); } - if (step.Run is not null) - { - ValidateRunStep(step.Run, $"{path}.run", errors); - } + if (step.Run is not null) + { + ValidateRunStep(step.Run, $"{path}.run", errors); + } if (step.Gate is not null) { @@ -156,13 +157,44 @@ public sealed class TaskPackManifestValidator } } - private static void ValidateRunStep(TaskPackRunStep run, string path, ICollection errors) - { - if (string.IsNullOrWhiteSpace(run.Uses)) - { - errors.Add(new TaskPackManifestValidationError($"{path}.uses", "Run step requires 'uses'.")); - } - } + private static void ValidateRunStep(TaskPackRunStep run, string path, ICollection errors) + { + if (string.IsNullOrWhiteSpace(run.Uses)) + { + errors.Add(new TaskPackManifestValidationError($"{path}.uses", "Run step requires 'uses'.")); + } + + if (run.Egress is not null) + { + for (var i = 0; i < run.Egress.Count; i++) + { + var entry = run.Egress[i]; + var entryPath = $"{path}.egress[{i}]"; + + if (entry is null) + { + errors.Add(new TaskPackManifestValidationError(entryPath, "Egress entry must be specified.")); + continue; + } + + if (string.IsNullOrWhiteSpace(entry.Url)) + { + errors.Add(new TaskPackManifestValidationError($"{entryPath}.url", "Egress entry requires an absolute URL.")); + } + else if (!Uri.TryCreate(entry.Url, UriKind.Absolute, out var uri) || + (!string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) && + !string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + { + errors.Add(new TaskPackManifestValidationError($"{entryPath}.url", "Egress URL must be an absolute HTTP or HTTPS address.")); + } + + if (entry.Intent is not null && string.IsNullOrWhiteSpace(entry.Intent)) + { + errors.Add(new TaskPackManifestValidationError($"{entryPath}.intent", "Intent must be omitted or non-empty.")); + } + } + } + } private static void ValidateGateStep(TaskPackGateStep gate, HashSet approvalIds, string path, ICollection errors) { diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/Execution/FilesystemPackRunDispatcher.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/Execution/FilesystemPackRunDispatcher.cs index ec242226..7f77cef3 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/Execution/FilesystemPackRunDispatcher.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/Execution/FilesystemPackRunDispatcher.cs @@ -1,8 +1,9 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using StellaOps.TaskRunner.Core.Execution; -using StellaOps.TaskRunner.Core.Planning; -using StellaOps.TaskRunner.Core.TaskPacks; +using System.Text.Json; +using System.Text.Json.Nodes; +using StellaOps.AirGap.Policy; +using StellaOps.TaskRunner.Core.Execution; +using StellaOps.TaskRunner.Core.Planning; +using StellaOps.TaskRunner.Core.TaskPacks; namespace StellaOps.TaskRunner.Infrastructure.Execution; @@ -10,17 +11,18 @@ public sealed class FilesystemPackRunDispatcher : IPackRunJobDispatcher { private readonly string queuePath; private readonly string archivePath; - private readonly TaskPackManifestLoader manifestLoader = new(); - private readonly TaskPackPlanner planner = new(); + private readonly TaskPackManifestLoader manifestLoader = new(); + private readonly TaskPackPlanner planner; private readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web); - public FilesystemPackRunDispatcher(string queuePath, string archivePath) - { - this.queuePath = queuePath ?? throw new ArgumentNullException(nameof(queuePath)); - this.archivePath = archivePath ?? throw new ArgumentNullException(nameof(archivePath)); - Directory.CreateDirectory(queuePath); - Directory.CreateDirectory(archivePath); - } + public FilesystemPackRunDispatcher(string queuePath, string archivePath, IEgressPolicy? egressPolicy = null) + { + this.queuePath = queuePath ?? throw new ArgumentNullException(nameof(queuePath)); + this.archivePath = archivePath ?? throw new ArgumentNullException(nameof(archivePath)); + planner = new TaskPackPlanner(egressPolicy); + Directory.CreateDirectory(queuePath); + Directory.CreateDirectory(archivePath); + } public async Task TryDequeueAsync(CancellationToken cancellationToken) { @@ -34,8 +36,12 @@ public sealed class FilesystemPackRunDispatcher : IPackRunJobDispatcher try { - var jobJson = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false); - var job = JsonSerializer.Deserialize(jobJson, serializerOptions) ?? continue; + var jobJson = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false); + var job = JsonSerializer.Deserialize(jobJson, serializerOptions); + if (job is null) + { + continue; + } var manifestPath = ResolvePath(queuePath, job.ManifestPath); var inputsPath = job.InputsPath is null ? null : ResolvePath(queuePath, job.InputsPath); diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/StellaOps.TaskRunner.Infrastructure.csproj b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/StellaOps.TaskRunner.Infrastructure.csproj index 756620dd..ed83ff14 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/StellaOps.TaskRunner.Infrastructure.csproj +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/StellaOps.TaskRunner.Infrastructure.csproj @@ -1,25 +1,16 @@ - - - - - - - - - - - - - - - - net10.0 - enable - enable - preview - true - - - - - + + + + + + + + + + net10.0 + enable + enable + preview + true + + diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/FilesystemPackRunDispatcherTests.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/FilesystemPackRunDispatcherTests.cs new file mode 100644 index 00000000..f6f1ee42 --- /dev/null +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/FilesystemPackRunDispatcherTests.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using StellaOps.AirGap.Policy; +using StellaOps.TaskRunner.Infrastructure.Execution; + +namespace StellaOps.TaskRunner.Tests; + +public sealed class FilesystemPackRunDispatcherTests +{ + [Fact] + public async Task TryDequeueAsync_BlocksJob_WhenEgressPolicyDeniesDestination() + { + var root = Path.Combine(Path.GetTempPath(), "StellaOps_TaskRunnerTests", Guid.NewGuid().ToString("n")); + Directory.CreateDirectory(root); + + var cancellationToken = TestContext.Current.CancellationToken; + + var queuePath = Path.Combine(root, "queue"); + var archivePath = Path.Combine(root, "archive"); + Directory.CreateDirectory(queuePath); + Directory.CreateDirectory(archivePath); + + var manifestPath = Path.Combine(queuePath, "manifest.yaml"); + await File.WriteAllTextAsync(manifestPath, TestManifests.EgressBlocked, cancellationToken); + + var jobEnvelope = new + { + RunId = "run-egress-blocked", + ManifestPath = Path.GetFileName(manifestPath), + InputsPath = (string?)null, + RequestedAt = (DateTimeOffset?)null + }; + + var jobPath = Path.Combine(queuePath, "job.json"); + await File.WriteAllTextAsync(jobPath, JsonSerializer.Serialize(jobEnvelope), cancellationToken); + + var policy = new EgressPolicy(new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed, + AllowLoopback = false, + AllowPrivateNetworks = false + }); + + try + { + var dispatcher = new FilesystemPackRunDispatcher(queuePath, archivePath, policy); + var result = await dispatcher.TryDequeueAsync(cancellationToken); + + Assert.Null(result); + Assert.False(File.Exists(jobPath)); + Assert.True(File.Exists(jobPath + ".failed")); + Assert.Empty(Directory.GetFiles(archivePath)); + } + finally + { + try + { + Directory.Delete(root, recursive: true); + } + catch + { + // Best-effort cleanup; ignore failures to avoid masking test results. + } + } + } +} diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/StellaOps.TaskRunner.Tests.csproj b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/StellaOps.TaskRunner.Tests.csproj index 895a3209..043bab5d 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/StellaOps.TaskRunner.Tests.csproj +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/StellaOps.TaskRunner.Tests.csproj @@ -121,7 +121,8 @@ - + + diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskPackPlannerTests.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskPackPlannerTests.cs index d2438e44..e19911ed 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskPackPlannerTests.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TaskPackPlannerTests.cs @@ -1,6 +1,8 @@ -using System.Linq; -using System.Text.Json.Nodes; -using StellaOps.TaskRunner.Core.Planning; +using System; +using System.Linq; +using System.Text.Json.Nodes; +using StellaOps.AirGap.Policy; +using StellaOps.TaskRunner.Core.Planning; namespace StellaOps.TaskRunner.Tests; @@ -165,13 +167,62 @@ public sealed class TaskPackPlannerTests } [Fact] - public void Plan_WhenRequiredInputMissing_ReturnsError() - { - var manifest = TestManifests.Load(TestManifests.RequiredInput); - var planner = new TaskPackPlanner(); - - var result = planner.Plan(manifest); - Assert.False(result.Success); - Assert.Contains(result.Errors, error => error.Path == "inputs.sbomBundle"); - } -} + public void Plan_WhenRequiredInputMissing_ReturnsError() + { + var manifest = TestManifests.Load(TestManifests.RequiredInput); + var planner = new TaskPackPlanner(); + + var result = planner.Plan(manifest); + Assert.False(result.Success); + Assert.Contains(result.Errors, error => error.Path == "inputs.sbomBundle"); + } + + [Fact] + public void Plan_SealedMode_AllowsDeclaredEgress() + { + var manifest = TestManifests.Load(TestManifests.EgressAllowed); + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed + }; + options.AddAllowRule("mirror.internal", 443, EgressTransport.Https); + + var planner = new TaskPackPlanner(new EgressPolicy(options)); + + var result = planner.Plan(manifest); + + Assert.True(result.Success); + } + + [Fact] + public void Plan_SealedMode_BlocksUndeclaredEgress() + { + var manifest = TestManifests.Load(TestManifests.EgressBlocked); + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed + }; + var planner = new TaskPackPlanner(new EgressPolicy(options)); + + var result = planner.Plan(manifest); + + Assert.False(result.Success); + Assert.Contains(result.Errors, error => error.Message.Contains("example.com", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Plan_SealedMode_RuntimeUrlWithoutDeclaration_ReturnsError() + { + var manifest = TestManifests.Load(TestManifests.EgressRuntime); + var options = new EgressPolicyOptions + { + Mode = EgressPolicyMode.Sealed + }; + var planner = new TaskPackPlanner(new EgressPolicy(options)); + + var result = planner.Plan(manifest); + + Assert.False(result.Success); + Assert.Contains(result.Errors, error => error.Path.StartsWith("spec.steps[0]", StringComparison.Ordinal)); + } +} diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TestManifests.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TestManifests.cs index a20f7a56..0af0792a 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TestManifests.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/TestManifests.cs @@ -143,23 +143,74 @@ spec: expression: "{{ steps.generate.outputs.evidence }}" """; - public const string PolicyGate = """ -apiVersion: stellaops.io/pack.v1 -kind: TaskPack -metadata: - name: policy-gate-pack - version: 1.0.0 -spec: - steps: - - id: prepare - run: - uses: builtin:prepare - - id: policy-check - gate: - policy: - policy: security-hold - parameters: - threshold: high - evidenceRef: "{{ steps.prepare.outputs.evidence }}" -"""; -} +public const string PolicyGate = """ +apiVersion: stellaops.io/pack.v1 +kind: TaskPack +metadata: + name: policy-gate-pack + version: 1.0.0 +spec: + steps: + - id: prepare + run: + uses: builtin:prepare + - id: policy-check + gate: + policy: + policy: security-hold + parameters: + threshold: high + evidenceRef: "{{ steps.prepare.outputs.evidence }}" +"""; + + public const string EgressAllowed = """ +apiVersion: stellaops.io/pack.v1 +kind: TaskPack +metadata: + name: egress-allowed + version: 1.0.0 +spec: + steps: + - id: fetch + run: + uses: builtin:http + with: + url: https://mirror.internal/api/status + egress: + - url: https://mirror.internal/api/status +"""; + + public const string EgressBlocked = """ +apiVersion: stellaops.io/pack.v1 +kind: TaskPack +metadata: + name: egress-blocked + version: 1.0.0 +spec: + steps: + - id: fetch + run: + uses: builtin:http + with: + url: https://example.com/api/status +"""; + + public const string EgressRuntime = """ +apiVersion: stellaops.io/pack.v1 +kind: TaskPack +metadata: + name: egress-runtime + version: 1.0.0 +spec: + inputs: + - name: targetUrl + type: string + required: false + steps: + - id: fetch + run: + uses: builtin:http + with: + url: "{{ inputs.targetUrl }}" +"""; +} diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs index 9f987df4..0a0a06a1 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/Program.cs @@ -1,13 +1,15 @@ -using Microsoft.Extensions.Options; -using StellaOps.TaskRunner.Core.Execution; -using StellaOps.TaskRunner.Infrastructure.Execution; -using StellaOps.TaskRunner.Worker.Services; +using Microsoft.Extensions.Options; +using StellaOps.AirGap.Policy; +using StellaOps.TaskRunner.Core.Execution; +using StellaOps.TaskRunner.Infrastructure.Execution; +using StellaOps.TaskRunner.Worker.Services; -var builder = Host.CreateApplicationBuilder(args); - -builder.Services.Configure(builder.Configuration.GetSection("Worker")); -builder.Services.Configure(builder.Configuration.GetSection("Notifications")); -builder.Services.AddHttpClient("taskrunner-notifications"); +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap"); +builder.Services.Configure(builder.Configuration.GetSection("Worker")); +builder.Services.Configure(builder.Configuration.GetSection("Notifications")); +builder.Services.AddHttpClient("taskrunner-notifications"); builder.Services.AddSingleton(sp => { @@ -15,11 +17,12 @@ builder.Services.AddSingleton(sp => return new FilePackRunApprovalStore(options.Value.ApprovalStorePath); }); -builder.Services.AddSingleton(sp => -{ - var options = sp.GetRequiredService>(); - return new FilesystemPackRunDispatcher(options.Value.QueuePath, options.Value.ArchivePath); -}); +builder.Services.AddSingleton(sp => +{ + var options = sp.GetRequiredService>(); + var egressPolicy = sp.GetRequiredService(); + return new FilesystemPackRunDispatcher(options.Value.QueuePath, options.Value.ArchivePath, egressPolicy); +}); builder.Services.AddSingleton(sp => { diff --git a/src/TaskRunner/StellaOps.TaskRunner/TASKS.md b/src/TaskRunner/StellaOps.TaskRunner/TASKS.md index 1dda96c4..660830d1 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/TASKS.md +++ b/src/TaskRunner/StellaOps.TaskRunner/TASKS.md @@ -37,7 +37,7 @@ ## Air-Gapped Mode (Epic 16) | ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------| -| TASKRUN-AIRGAP-56-001 | TODO | Task Runner Guild, AirGap Policy Guild | AIRGAP-POL-56-001, TASKRUN-OBS-50-001 | Enforce plan-time validation rejecting steps with non-allowlisted network calls in sealed mode and surface remediation errors. | Planner blocks disallowed steps; error contains remediation; tests cover sealed/unsealed behavior. | +| TASKRUN-AIRGAP-56-001 | DOING (2025-11-03) | Task Runner Guild, AirGap Policy Guild | AIRGAP-POL-56-001, TASKRUN-OBS-50-001 | Enforce plan-time validation rejecting steps with non-allowlisted network calls in sealed mode and surface remediation errors. | Planner blocks disallowed steps; error contains remediation; tests cover sealed/unsealed behavior. | | TASKRUN-AIRGAP-56-002 | TODO | Task Runner Guild, AirGap Importer Guild | TASKRUN-AIRGAP-56-001, AIRGAP-IMP-57-002 | Add helper steps for bundle ingestion (checksum verification, staging to object store) with deterministic outputs. | Helper steps succeed deterministically; integration tests import sample bundle. | | TASKRUN-AIRGAP-57-001 | TODO | Task Runner Guild, AirGap Controller Guild | TASKRUN-AIRGAP-56-001, AIRGAP-CTL-56-002 | Refuse to execute plans when environment sealed=false but declared sealed install; emit advisory timeline events. | Mismatch detection works; timeline + telemetry record violation; docs updated. | | TASKRUN-AIRGAP-58-001 | TODO | Task Runner Guild, Evidence Locker Guild | TASKRUN-OBS-53-001, EVID-OBS-55-001 | Capture bundle import job transcripts, hashed inputs, and outputs into portable evidence bundles. | Evidence recorded; manifests deterministic; timeline references created. | diff --git a/src/VexLens/StellaOps.VexLens/AGENTS.md b/src/VexLens/StellaOps.VexLens/AGENTS.md index b4809379..3548afd8 100644 --- a/src/VexLens/StellaOps.VexLens/AGENTS.md +++ b/src/VexLens/StellaOps.VexLens/AGENTS.md @@ -5,7 +5,7 @@ Deliver the VEX Consensus Lens service that normalizes VEX evidence, computes de ## Scope - Service code under `src/VexLens/StellaOps.VexLens` (normalizer, mapping, trust weighting, consensus projection, APIs, simulation hooks). -- Batch workers consuming Excitator, Conseiller, SBOM, and policy events; projection storage and caching; telemetry. +- Batch workers consuming Excitor, Conseiller, SBOM, and policy events; projection storage and caching; telemetry. - Coordination with Policy Engine, Vuln Explorer, Findings Ledger, Console, CLI, and Docs. ## Principles diff --git a/src/VulnExplorer/StellaOps.VulnExplorer.Api/AGENTS.md b/src/VulnExplorer/StellaOps.VulnExplorer.Api/AGENTS.md index 3e9546ca..6cbd123c 100644 --- a/src/VulnExplorer/StellaOps.VulnExplorer.Api/AGENTS.md +++ b/src/VulnExplorer/StellaOps.VulnExplorer.Api/AGENTS.md @@ -5,7 +5,7 @@ Expose policy-aware vulnerability listing, detail, simulation, workflow, and exp ## Scope - Service under `src/VulnExplorer/StellaOps.VulnExplorer.Api` (query engine, workflow endpoints, simulation bridge, export orchestrator). -- Integration with Findings Ledger, Policy Engine, Conseiller, Excitator, SBOM Service, Scheduler, and Authority. +- Integration with Findings Ledger, Policy Engine, Conseiller, Excitor, SBOM Service, Scheduler, and Authority. - Evidence bundle assembly and signing hand-off. ## Principles diff --git a/src/Web/StellaOps.Web/src/app/core/auth/dpop/dpop.service.spec.ts b/src/Web/StellaOps.Web/src/app/core/auth/dpop/dpop.service.spec.ts index 9a79350d..1cb54c76 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/dpop/dpop.service.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/dpop/dpop.service.spec.ts @@ -16,7 +16,7 @@ describe('DpopService', () => { authorizeEndpoint: 'https://auth.stellaops.test/connect/authorize', tokenEndpoint: 'https://auth.stellaops.test/connect/token', redirectUri: 'https://ui.stellaops.test/auth/callback', - scope: 'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:read', + scope: 'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit', audience: 'https://scanner.stellaops.test', }, apiBaseUrls: { diff --git a/src/Web/StellaOps.Web/src/config/config.json b/src/Web/StellaOps.Web/src/config/config.json index ee5c9dbd..8f4e5ce7 100644 --- a/src/Web/StellaOps.Web/src/config/config.json +++ b/src/Web/StellaOps.Web/src/config/config.json @@ -7,7 +7,7 @@ "logoutEndpoint": "https://authority.local/connect/logout", "redirectUri": "http://localhost:4400/auth/callback", "postLogoutRedirectUri": "http://localhost:4400/", - "scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:read", + "scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit", "audience": "https://scanner.local", "dpopAlgorithms": ["ES256"], "refreshLeewaySeconds": 60 diff --git a/src/Web/StellaOps.Web/src/config/config.sample.json b/src/Web/StellaOps.Web/src/config/config.sample.json index 4ba9744d..6ad48420 100644 --- a/src/Web/StellaOps.Web/src/config/config.sample.json +++ b/src/Web/StellaOps.Web/src/config/config.sample.json @@ -7,7 +7,7 @@ "logoutEndpoint": "https://authority.example.dev/connect/logout", "redirectUri": "http://localhost:4400/auth/callback", "postLogoutRedirectUri": "http://localhost:4400/", - "scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:read", + "scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit", "audience": "https://scanner.example.dev", "dpopAlgorithms": ["ES256"], "refreshLeewaySeconds": 60 diff --git a/src/Web/StellaOps.Web/tests/e2e/auth.spec.ts b/src/Web/StellaOps.Web/tests/e2e/auth.spec.ts index b1f9f786..523a965e 100644 --- a/src/Web/StellaOps.Web/tests/e2e/auth.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/auth.spec.ts @@ -9,8 +9,8 @@ const mockConfig = { logoutEndpoint: 'https://authority.local/connect/logout', redirectUri: 'http://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'http://127.0.0.1:4400/', - scope: - 'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:read', + scope: + 'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit', audience: 'https://scanner.local', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, diff --git a/src/__Libraries/StellaOps.Configuration/AuthorityVulnerabilityExplorerOptions.cs b/src/__Libraries/StellaOps.Configuration/AuthorityVulnerabilityExplorerOptions.cs new file mode 100644 index 00000000..93f5fa07 --- /dev/null +++ b/src/__Libraries/StellaOps.Configuration/AuthorityVulnerabilityExplorerOptions.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; + +namespace StellaOps.Configuration; + +/// +/// Options controlling vulnerability explorer specific features exposed by Authority. +/// +public sealed class AuthorityVulnerabilityExplorerOptions +{ + /// + /// Workflow-oriented configuration (anti-forgery tokens, CSRF enforcement helpers). + /// + public AuthorityVulnWorkflowOptions Workflow { get; } = new(); + + /// + /// Attachment handling configuration (signed access tokens). + /// + public AuthorityVulnAttachmentOptions Attachments { get; } = new(); + + internal void Validate() + { + Workflow.Validate(); + Attachments.Validate(); + } +} + +/// +/// Workflow specific configuration for Vuln Explorer clients. +/// +public sealed class AuthorityVulnWorkflowOptions +{ + /// + /// Anti-forgery token configuration. + /// + public AuthorityVulnAntiForgeryOptions AntiForgery { get; } = new(); + + internal void Validate() + { + AntiForgery.Validate(); + } +} + +/// +/// Anti-forgery token configuration used to protect workflow submissions. +/// +public sealed class AuthorityVulnAntiForgeryOptions +{ + /// + /// Determines whether anti-forgery token issuance/verification is enabled. + /// + public bool Enabled { get; set; } = true; + + /// + /// Audience claim value embedded in issued tokens. + /// + public string Audience { get; set; } = "stellaops:vuln-workflow"; + + /// + /// Default lifetime applied when callers omit an explicit expiration. + /// + public TimeSpan DefaultLifetime { get; set; } = TimeSpan.FromMinutes(10); + + /// + /// Maximum lifetime permitted for anti-forgery tokens. + /// + public TimeSpan MaxLifetime { get; set; } = TimeSpan.FromMinutes(30); + + /// + /// Optional maximum size for the context dictionary payload. + /// + public int MaxContextEntries { get; set; } = 16; + + /// + /// Maximum length permitted per context value entry. + /// + public int MaxContextValueLength { get; set; } = 256; + + internal void Validate() + { + if (!Enabled) + { + return; + } + + if (string.IsNullOrWhiteSpace(Audience)) + { + throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.audience must be specified when anti-forgery tokens are enabled."); + } + + if (DefaultLifetime <= TimeSpan.Zero) + { + throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.defaultLifetime must be greater than zero."); + } + + if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime) + { + throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxLifetime must be greater than zero and greater than or equal to defaultLifetime."); + } + + if (MaxContextEntries < 0) + { + throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxContextEntries must be non-negative."); + } + + if (MaxContextValueLength <= 0) + { + throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxContextValueLength must be greater than zero."); + } + } +} + +/// +/// Attachment token configuration used to protect ledger attachments. +/// +public sealed class AuthorityVulnAttachmentOptions +{ + /// + /// Determines whether attachment token issuance/verification is enabled. + /// + public bool Enabled { get; set; } = true; + + /// + /// Default lifetime for attachment access tokens. + /// + public TimeSpan DefaultLifetime { get; set; } = TimeSpan.FromMinutes(30); + + /// + /// Maximum lifetime permitted for attachment access tokens. + /// + public TimeSpan MaxLifetime { get; set; } = TimeSpan.FromHours(4); + + /// + /// Payload type identifier emitted in audit and downstream validation. + /// + public string PayloadType { get; set; } = "application/vnd.stellaops.vuln-attachment-token+json"; + + /// + /// Optional limit on attachment metadata entries. + /// + public int MaxMetadataEntries { get; set; } = 16; + + /// + /// Optional maximum length for metadata values. + /// + public int MaxMetadataValueLength { get; set; } = 512; + + internal void Validate() + { + if (!Enabled) + { + return; + } + + if (DefaultLifetime <= TimeSpan.Zero) + { + throw new InvalidOperationException("vulnerabilityExplorer.attachments.defaultLifetime must be greater than zero when attachment tokens are enabled."); + } + + if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime) + { + throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxLifetime must be greater than zero and greater than or equal to defaultLifetime."); + } + + if (string.IsNullOrWhiteSpace(PayloadType)) + { + throw new InvalidOperationException("vulnerabilityExplorer.attachments.payloadType must be specified when attachment tokens are enabled."); + } + + if (MaxMetadataEntries < 0) + { + throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxMetadataEntries must be non-negative."); + } + + if (MaxMetadataValueLength <= 0) + { + throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxMetadataValueLength must be greater than zero."); + } + } +} diff --git a/src/__Libraries/StellaOps.Configuration/StellaOpsAuthorityOptions.cs b/src/__Libraries/StellaOps.Configuration/StellaOpsAuthorityOptions.cs index 6e26167d..d4b8f280 100644 --- a/src/__Libraries/StellaOps.Configuration/StellaOpsAuthorityOptions.cs +++ b/src/__Libraries/StellaOps.Configuration/StellaOpsAuthorityOptions.cs @@ -99,6 +99,11 @@ public sealed class StellaOpsAuthorityOptions /// public AuthorityNotificationsOptions Notifications { get; } = new(); + /// + /// Vulnerability explorer integration configuration (workflow CSRF tokens, attachments). + /// + public AuthorityVulnerabilityExplorerOptions VulnerabilityExplorer { get; } = new(); + /// /// Exception governance configuration (routing templates, MFA requirements). /// @@ -158,6 +163,7 @@ public sealed class StellaOpsAuthorityOptions AdvisoryAi.Normalize(); AdvisoryAi.Validate(); Notifications.Validate(); + VulnerabilityExplorer.Validate(); ApiLifecycle.Validate(); Signing.Validate(); Delegation.NormalizeAndValidate(tenants); @@ -772,6 +778,9 @@ public sealed class AuthorityTenantOptions public IList DefaultRoles { get; } = new List(); public IList Projects { get; } = new List(); + public IDictionary Roles { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + public AuthorityTenantAdvisoryAiOptions AdvisoryAi { get; } = new(); public AuthorityTenantDelegationOptions Delegation { get; } = new(); @@ -820,6 +829,28 @@ public sealed class AuthorityTenantOptions AdvisoryAi.Normalize(advisoryAiOptions); Delegation.Normalize(delegationOptions); + + if (Roles.Count > 0) + { + var normalizedRoles = new Dictionary(StringComparer.Ordinal); + foreach (var (roleName, roleOptions) in Roles) + { + if (string.IsNullOrWhiteSpace(roleName) || roleOptions is null) + { + continue; + } + + var normalizedName = roleName.Trim().ToLowerInvariant(); + roleOptions.Normalize(normalizedName); + normalizedRoles[normalizedName] = roleOptions; + } + + Roles.Clear(); + foreach (var entry in normalizedRoles) + { + Roles.Add(entry.Key, entry.Value); + } + } } internal void Validate(AuthorityAdvisoryAiOptions? advisoryAiOptions, AuthorityDelegationOptions delegationOptions) @@ -852,12 +883,149 @@ public sealed class AuthorityTenantOptions AdvisoryAi.Validate(advisoryAiOptions); Delegation.Validate(delegationOptions, Id); + + if (Roles.Count > 0) + { + foreach (var (roleName, roleOptions) in Roles) + { + if (roleOptions is null) + { + throw new InvalidOperationException($"Tenant '{Id}' defines role '{roleName}' without configuration."); + } + + roleOptions.Validate(Id, roleName); + } + } } private static readonly Regex TenantSlugRegex = new("^[a-z0-9-]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex ProjectSlugRegex = new("^[a-z0-9-]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant); } +public sealed class AuthorityTenantRoleOptions +{ + public IList Scopes { get; } = new List(); + + public IDictionary> Attributes { get; } = + new Dictionary>(StringComparer.OrdinalIgnoreCase); + + internal void Normalize(string roleName) + { + if (Scopes.Count > 0) + { + var seenScopes = new HashSet(StringComparer.Ordinal); + for (var index = Scopes.Count - 1; index >= 0; index--) + { + var current = Scopes[index]; + var normalized = StellaOpsScopes.Normalize(current); + if (string.IsNullOrWhiteSpace(normalized)) + { + Scopes.RemoveAt(index); + continue; + } + + if (!seenScopes.Add(normalized)) + { + Scopes.RemoveAt(index); + continue; + } + + Scopes[index] = normalized; + } + } + + if (Attributes.Count > 0) + { + var normalizedAttributes = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var (attributeName, values) in Attributes) + { + var normalizedName = attributeName?.Trim(); + if (string.IsNullOrWhiteSpace(normalizedName)) + { + continue; + } + + normalizedName = normalizedName.ToLowerInvariant(); + + var normalizedValues = new List(); + var seenValues = new HashSet(StringComparer.OrdinalIgnoreCase); + var wildcard = false; + + if (values is not null) + { + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + var trimmed = value.Trim(); + if (trimmed.Equals("*", StringComparison.Ordinal)) + { + normalizedValues.Clear(); + normalizedValues.Add("*"); + wildcard = true; + break; + } + + var lower = trimmed.ToLowerInvariant(); + if (seenValues.Add(lower)) + { + normalizedValues.Add(lower); + } + } + } + + if (wildcard || normalizedValues.Count > 0) + { + normalizedAttributes[normalizedName] = normalizedValues; + } + } + + Attributes.Clear(); + foreach (var pair in normalizedAttributes) + { + Attributes[pair.Key] = pair.Value; + } + } + } + + internal void Validate(string tenantId, string roleName) + { + if (Scopes.Count == 0) + { + throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' must specify at least one scope."); + } + + foreach (var scope in Scopes) + { + if (!StellaOpsScopes.IsKnown(scope)) + { + throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' references unknown scope '{scope}'."); + } + } + + if (Attributes.Count > 0) + { + foreach (var attributeName in Attributes.Keys) + { + if (!AllowedAttributeKeys.Contains(attributeName)) + { + throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' defines unsupported attribute '{attributeName}'. Allowed attributes: env, owner, business_tier."); + } + } + } + } + + private static readonly HashSet AllowedAttributeKeys = new(new[] + { + "env", + "owner", + "business_tier" + }, StringComparer.OrdinalIgnoreCase); +} + public sealed class AuthorityDelegationOptions { private readonly IList serviceAccounts = new List(); @@ -977,6 +1145,9 @@ public sealed class AuthorityServiceAccountSeedOptions public IList AllowedScopes { get; } = new List(); + public IDictionary> Attributes { get; } = + new Dictionary>(StringComparer.OrdinalIgnoreCase); + internal void Normalize() { AccountId = (AccountId ?? string.Empty).Trim(); @@ -990,6 +1161,8 @@ public sealed class AuthorityServiceAccountSeedOptions var normalized = StellaOpsScopes.Normalize(scope); return normalized ?? scope.Trim().ToLowerInvariant(); }, StringComparer.Ordinal); + + NormalizeAttributes(Attributes); } internal void Validate(ISet tenantIds) @@ -1018,6 +1191,17 @@ public sealed class AuthorityServiceAccountSeedOptions { throw new InvalidOperationException($"Service account '{AccountId}' must specify at least one allowed scope."); } + + if (Attributes.Count > 0) + { + foreach (var attributeName in Attributes.Keys) + { + if (!AllowedAttributeKeys.Contains(attributeName)) + { + throw new InvalidOperationException($"Service account '{AccountId}' defines unsupported attribute '{attributeName}'. Allowed attributes: env, owner, business_tier."); + } + } + } } private static void NormalizeList(IList values, Func normalize, IEqualityComparer comparer) @@ -1057,6 +1241,74 @@ public sealed class AuthorityServiceAccountSeedOptions values[index] = normalized; } } + + private static void NormalizeAttributes(IDictionary> attributes) + { + ArgumentNullException.ThrowIfNull(attributes); + + if (attributes.Count == 0) + { + return; + } + + var normalized = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var (name, values) in attributes) + { + var key = string.IsNullOrWhiteSpace(name) ? null : name.Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + var normalizedValues = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var wildcard = false; + + if (values is not null) + { + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + var trimmed = value.Trim(); + if (trimmed.Equals("*", StringComparison.Ordinal)) + { + normalizedValues.Clear(); + normalizedValues.Add("*"); + wildcard = true; + break; + } + + var lower = trimmed.ToLowerInvariant(); + if (seen.Add(lower)) + { + normalizedValues.Add(lower); + } + } + } + + if (wildcard || normalizedValues.Count > 0) + { + normalized[key] = normalizedValues; + } + } + + attributes.Clear(); + foreach (var pair in normalized) + { + attributes[pair.Key] = pair.Value; + } + } + + private static readonly HashSet AllowedAttributeKeys = new(new[] + { + "env", + "owner", + "business_tier" + }, StringComparer.OrdinalIgnoreCase); }