Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images.
- Added symbols.json detailing function entry and sink points in the WordPress code.
- Included runtime traces for function calls in both reachable and unreachable scenarios.
- Developed OpenVEX files indicating vulnerability status and justification for both cases.
- Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
master
2025-11-08 20:53:45 +02:00
parent 515975edc5
commit 536f6249a6
837 changed files with 37279 additions and 14675 deletions

View File

@@ -21,6 +21,8 @@ on:
- 'docs/**' - 'docs/**'
- 'scripts/**' - 'scripts/**'
- '.gitea/workflows/**' - '.gitea/workflows/**'
schedule:
- cron: '0 5 * * *'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
force_deploy: force_deploy:
@@ -28,6 +30,11 @@ on:
required: false required: false
default: 'false' default: 'false'
type: boolean type: boolean
excititor_batch:
description: 'Run Excititor batch-ingest validation suite'
required: false
default: 'false'
type: boolean
env: env:
DOTNET_VERSION: '10.0.100-rc.1.25451.107' DOTNET_VERSION: '10.0.100-rc.1.25451.107'
@@ -48,6 +55,18 @@ jobs:
tar -xzf /tmp/helm.tgz -C /tmp tar -xzf /tmp/helm.tgz -C /tmp
sudo install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm sudo install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm
- name: Validate Helm chart rendering
run: |
set -euo pipefail
CHART_PATH="deploy/helm/stellaops"
helm lint "$CHART_PATH"
for values in values.yaml values-dev.yaml values-stage.yaml values-prod.yaml values-airgap.yaml values-mirror.yaml; do
release="stellaops-${values%.*}"
echo "::group::Helm template ${release} (${values})"
helm template "$release" "$CHART_PATH" -f "$CHART_PATH/$values" >/dev/null
echo "::endgroup::"
done
- name: Validate deployment profiles - name: Validate deployment profiles
run: ./deploy/tools/validate-profiles.sh run: ./deploy/tools/validate-profiles.sh
@@ -442,6 +461,15 @@ PY
if-no-files-found: error if-no-files-found: error
retention-days: 7 retention-days: 7
- name: Run console endpoint tests
run: |
mkdir -p "$TEST_RESULTS_DIR"
dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/StellaOps.Authority.Tests.csproj \
--configuration $BUILD_CONFIGURATION \
--logger "trx;LogFileName=console-endpoints.trx" \
--results-directory "$TEST_RESULTS_DIR" \
--filter ConsoleEndpointsTests
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -451,6 +479,44 @@ PY
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 7 retention-days: 7
sealed-mode-ci:
runs-on: ubuntu-22.04
needs: build-test
permissions:
contents: read
packages: read
env:
COMPOSE_PROJECT_NAME: sealedmode
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to registry
if: ${{ secrets.REGISTRY_USERNAME != '' && secrets.REGISTRY_PASSWORD != '' }}
uses: docker/login-action@v3
with:
registry: registry.stella-ops.org
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Run sealed-mode CI harness
working-directory: ops/devops/sealed-mode-ci
env:
COMPOSE_PROJECT_NAME: sealedmode
run: |
set -euo pipefail
./run-sealed-ci.sh
- name: Upload sealed-mode CI artifacts
uses: actions/upload-artifact@v4
with:
name: sealed-mode-ci
path: ops/devops/sealed-mode-ci/artifacts/sealed-mode-ci
if-no-files-found: error
retention-days: 14
authority-container: authority-container:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: build-test needs: build-test
@@ -464,6 +530,41 @@ PY
- name: Build Authority container image - name: Build Authority container image
run: docker build -f ops/authority/Dockerfile -t stellaops-authority:ci . run: docker build -f ops/authority/Dockerfile -t stellaops-authority:ci .
excititor-batch-validation:
needs: build-test
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.excititor_batch == 'true')
runs-on: ubuntu-22.04
env:
BATCH_RESULTS_DIR: ${{ github.workspace }}/artifacts/test-results/excititor-batch
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run Excititor batch ingest validation suite
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
run: |
set -euo pipefail
mkdir -p "$BATCH_RESULTS_DIR"
dotnet test src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj \
--configuration $BUILD_CONFIGURATION \
--filter "Category=BatchIngestValidation" \
--logger "trx;LogFileName=excititor-batch.trx" \
--results-directory "$BATCH_RESULTS_DIR"
- name: Upload Excititor batch ingest results
if: always()
uses: actions/upload-artifact@v4
with:
name: excititor-batch-ingest-results
path: ${{ env.BATCH_RESULTS_DIR }}
docs: docs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env: env:

View File

@@ -1,5 +1,12 @@
{{- $root := . -}} {{- $root := . -}}
{{- range $name, $svc := .Values.services }} {{- $configMaps := default (dict) .Values.configMaps -}}
{{- $hasPolicyActivationConfig := hasKey $configMaps "policy-engine-activation" -}}
{{- $policyActivationConfigName := "" -}}
{{- if $hasPolicyActivationConfig -}}
{{- $policyActivationConfigName = include "stellaops.fullname" (dict "root" $root "name" "policy-engine-activation") -}}
{{- end -}}
{{- $policyActivationTargets := dict "policy-engine" true "policy-gateway" true -}}
{{- range $name, $svc := .Values.services }}
{{- $configMounts := (default (list) $svc.configMounts) }} {{- $configMounts := (default (list) $svc.configMounts) }}
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@@ -36,18 +43,31 @@ spec:
- {{ $arg | quote }} - {{ $arg | quote }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if $svc.env }} {{- if $svc.env }}
env: env:
{{- range $envName, $envValue := $svc.env }} {{- range $envName, $envValue := $svc.env }}
- name: {{ $envName }} - name: {{ $envName }}
value: {{ $envValue | quote }} value: {{ $envValue | quote }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if $svc.envFrom }} {{- $needsPolicyActivation := and $hasPolicyActivationConfig (hasKey $policyActivationTargets $name) }}
envFrom: {{- $envFrom := default (list) $svc.envFrom }}
{{ toYaml $svc.envFrom | nindent 12 }} {{- if and $needsPolicyActivation (ne $policyActivationConfigName "") }}
{{- end }} {{- $hasActivationReference := false }}
{{- if $svc.ports }} {{- range $envFromEntry := $envFrom }}
{{- if and (hasKey $envFromEntry "configMapRef") (eq (index (index $envFromEntry "configMapRef") "name") $policyActivationConfigName) }}
{{- $hasActivationReference = true }}
{{- end }}
{{- end }}
{{- if not $hasActivationReference }}
{{- $envFrom = append $envFrom (dict "configMapRef" (dict "name" $policyActivationConfigName)) }}
{{- end }}
{{- end }}
{{- if $envFrom }}
envFrom:
{{ toYaml $envFrom | nindent 12 }}
{{- end }}
{{- if $svc.ports }}
ports: ports:
{{- range $port := $svc.ports }} {{- range $port := $svc.ports }}
- name: {{ default (printf "%s-%v" $name $port.containerPort) $port.name | trunc 63 | trimSuffix "-" }} - name: {{ default (printf "%s-%v" $name $port.containerPort) $port.name | trunc 63 | trimSuffix "-" }}

View File

@@ -51,6 +51,13 @@ configMaps:
telemetry: telemetry:
enableRequestLogging: true enableRequestLogging: true
minimumLogLevel: Warning minimumLogLevel: Warning
policy-engine-activation:
data:
STELLAOPS_POLICY_ENGINE__ACTIVATION__FORCETWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__DEFAULTREQUIRESTWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__EMITAUDITLOGS: "true"
services: services:
authority: authority:
image: registry.stella-ops.org/stellaops/authority@sha256:5551a3269b7008cd5aceecf45df018c67459ed519557ccbe48b093b926a39bcc image: registry.stella-ops.org/stellaops/authority@sha256:5551a3269b7008cd5aceecf45df018c67459ed519557ccbe48b093b926a39bcc

View File

@@ -58,6 +58,11 @@ configMaps:
telemetry: telemetry:
enableRequestLogging: true enableRequestLogging: true
minimumLogLevel: Debug minimumLogLevel: Debug
policy-engine-activation:
data:
STELLAOPS_POLICY_ENGINE__ACTIVATION__FORCETWOPERSONAPPROVAL: "false"
STELLAOPS_POLICY_ENGINE__ACTIVATION__DEFAULTREQUIRESTWOPERSONAPPROVAL: "false"
STELLAOPS_POLICY_ENGINE__ACTIVATION__EMITAUDITLOGS: "true"
services: services:
authority: authority:
image: registry.stella-ops.org/stellaops/authority@sha256:a8e8faec44a579aa5714e58be835f25575710430b1ad2ccd1282a018cd9ffcdd image: registry.stella-ops.org/stellaops/authority@sha256:a8e8faec44a579aa5714e58be835f25575710430b1ad2ccd1282a018cd9ffcdd

View File

@@ -106,11 +106,18 @@ configMaps:
proxy_cache off; proxy_cache off;
} }
location / { location / {
return 404; return 404;
} }
services:
policy-engine-activation:
data:
STELLAOPS_POLICY_ENGINE__ACTIVATION__FORCETWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__DEFAULTREQUIRESTWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__EMITAUDITLOGS: "true"
services:
concelier: concelier:
image: registry.stella-ops.org/stellaops/concelier@sha256:dafef3954eb4b837e2c424dd2d23e1e4d60fa83794840fac9cd3dea1d43bd085 image: registry.stella-ops.org/stellaops/concelier@sha256:dafef3954eb4b837e2c424dd2d23e1e4d60fa83794840fac9cd3dea1d43bd085
service: service:

View File

@@ -52,6 +52,11 @@ configMaps:
telemetry: telemetry:
enableRequestLogging: true enableRequestLogging: true
minimumLogLevel: Information minimumLogLevel: Information
policy-engine-activation:
data:
STELLAOPS_POLICY_ENGINE__ACTIVATION__FORCETWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__DEFAULTREQUIRESTWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__EMITAUDITLOGS: "true"
services: services:
authority: authority:
image: registry.stella-ops.org/stellaops/authority@sha256:b0348bad1d0b401cc3c71cb40ba034c8043b6c8874546f90d4783c9dbfcc0bf5 image: registry.stella-ops.org/stellaops/authority@sha256:b0348bad1d0b401cc3c71cb40ba034c8043b6c8874546f90d4783c9dbfcc0bf5

View File

@@ -58,6 +58,11 @@ configMaps:
telemetry: telemetry:
enableRequestLogging: true enableRequestLogging: true
minimumLogLevel: Information minimumLogLevel: Information
policy-engine-activation:
data:
STELLAOPS_POLICY_ENGINE__ACTIVATION__FORCETWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__DEFAULTREQUIRESTWOPERSONAPPROVAL: "true"
STELLAOPS_POLICY_ENGINE__ACTIVATION__EMITAUDITLOGS: "true"
services: services:
authority: authority:
image: registry.stella-ops.org/stellaops/authority@sha256:b0348bad1d0b401cc3c71cb40ba034c8043b6c8874546f90d4783c9dbfcc0bf5 image: registry.stella-ops.org/stellaops/authority@sha256:b0348bad1d0b401cc3c71cb40ba034c8043b6c8874546f90d4783c9dbfcc0bf5

View File

@@ -61,6 +61,12 @@ configMaps:
issuerTrustCollection: issuer_trust_overrides issuerTrustCollection: issuer_trust_overrides
auditCollection: issuer_audit auditCollection: issuer_audit
policy-engine-activation:
data:
STELLAOPS_POLICY_ENGINE__ACTIVATION__FORCETWOPERSONAPPROVAL: "false"
STELLAOPS_POLICY_ENGINE__ACTIVATION__DEFAULTREQUIRESTWOPERSONAPPROVAL: "false"
STELLAOPS_POLICY_ENGINE__ACTIVATION__EMITAUDITLOGS: "true"
services: services:
issuer-directory: issuer-directory:
image: registry.stella-ops.org/stellaops/issuer-directory-web:2025.10.0-edge image: registry.stella-ops.org/stellaops/issuer-directory-web:2025.10.0-edge

View File

@@ -132,7 +132,7 @@ These registrations are provided as examples in `etc/authority.yaml.sample`. Clo
- **Interactive only.** `policy:publish` and `policy:promote` are restricted to password/device-code flows (Console, CLI) and are rejected when requested via client credentials or app secrets. Tokens inherit the 5-minute fresh-auth window; resource servers reject stale tokens and emit `authority.policy_attestation_validated=false`. - **Interactive only.** `policy:publish` and `policy:promote` are restricted to password/device-code flows (Console, CLI) and are rejected when requested via client credentials or app secrets. Tokens inherit the 5-minute fresh-auth window; resource servers reject stale tokens and emit `authority.policy_attestation_validated=false`.
- **Mandatory parameters.** Requests must include: - **Mandatory parameters.** Requests must include:
- `policy_reason` (≤512 chars) — human-readable justification (e.g., “Promote tenant A baseline to production”). - Authority enforces mTLS bindings on /token, /fresh-auth, and /introspect by comparing the presented TLS client certificate thumbprint against the stored claim. Requests missing a certificate or presenting a different certificate are rejected with , and the counter is incremented for operational alerts.
- `policy_ticket` (≤128 chars) — change request / CAB identifier (e.g., `CR-2025-1102`). - `policy_ticket` (≤128 chars) — change request / CAB identifier (e.g., `CR-2025-1102`).
- `policy_digest` — lowercase hex digest (32128 characters) of the policy bundle being published/promoted. - `policy_digest` — lowercase hex digest (32128 characters) of the policy bundle being published/promoted.
- **Audit surfaces.** On success, the metadata is copied into the access token (`stellaops:policy_reason`, `stellaops:policy_ticket`, `stellaops:policy_digest`, `stellaops:policy_operation`) and recorded in [`authority.password.grant`] audit events as `policy.*` properties. - **Audit surfaces.** On success, the metadata is copied into the access token (`stellaops:policy_reason`, `stellaops:policy_ticket`, `stellaops:policy_digest`, `stellaops:policy_operation`) and recorded in [`authority.password.grant`] audit events as `policy.*` properties.
@@ -142,6 +142,14 @@ These registrations are provided as examples in `etc/authority.yaml.sample`. Clo
Graph Explorer introduces dedicated scopes: `graph:write` for Cartographer build jobs, `graph:read` for query/read operations, `graph:export` for long-running export downloads, and `graph:simulate` for what-if overlays. Assign only the scopes a client actually needs to preserve least privilege—UI-facing clients should typically request read/export access, while background services (Cartographer, Scheduler) require write privileges. Graph Explorer introduces dedicated scopes: `graph:write` for Cartographer build jobs, `graph:read` for query/read operations, `graph:export` for long-running export downloads, and `graph:simulate` for what-if overlays. Assign only the scopes a client actually needs to preserve least privilege—UI-facing clients should typically request read/export access, while background services (Cartographer, Scheduler) require write privileges.
### Policy activation dual-control
- **Config knobs.** `PolicyEngine.activation.forceTwoPersonApproval` forces every activation to collect two distinct `policy:activate` approvals (first response = `202 pending_second_approval`). `PolicyEngine.activation.defaultRequiresTwoPersonApproval` sets the default when callers omit the flag.
- **Operator choice.** When force is disabled, Console/CLI can opt any revision into dual-control by setting `requiresTwoPersonApproval: true`; the service persists the requirement alongside the revision metadata.
- **Audit coverage.** With `PolicyEngine.activation.emitAuditLogs` (default `true`), every activation emits structured `policy.activation.*` logger scopes (pack id, revision, actor(s), tenant, approval count, comment) so SOC pipelines can diff the two-person trail.
- **Status codes.** First approval on a dual-control revision returns `202 pending_second_approval`; duplicates produce `400 duplicate_approval`; the second distinct actor returns the usual `200 activated`.
#### Least-privilege guidance for graph clients #### Least-privilege guidance for graph clients
- **Service identities** The Cartographer worker should request `graph:write` and `graph:read` only; grant `graph:simulate` exclusively to pipeline automation that invokes Policy Engine overlays on demand. Keep `graph:export` scoped to API gateway components responsible for streaming GraphML/JSONL artifacts. Authority enforces this by rejecting `graph:write` tokens that lack `properties.serviceIdentity: cartographer`. - **Service identities** The Cartographer worker should request `graph:write` and `graph:read` only; grant `graph:simulate` exclusively to pipeline automation that invokes Policy Engine overlays on demand. Keep `graph:export` scoped to API gateway components responsible for streaming GraphML/JSONL artifacts. Authority enforces this by rejecting `graph:write` tokens that lack `properties.serviceIdentity: cartographer`.
@@ -356,6 +364,7 @@ exceptions:
| Bootstrap | `bootstrap.apiKey` | Shared secret required for `/internal/*`. | Only required when `bootstrap.enabled` is true. | | Bootstrap | `bootstrap.apiKey` | Shared secret required for `/internal/*`. | Only required when `bootstrap.enabled` is true. |
### 7.1 Sender-constrained clients (DPoP & mTLS) ### 7.1 Sender-constrained clients (DPoP & mTLS)
> Rollout tracker: see [`docs/security/dpop-mtls-rollout.md`](security/dpop-mtls-rollout.md) for phase gates tied to `AUTH-DPOP-11-001` and `AUTH-MTLS-11-002`.
Authority now understands two flavours of sender-constrained OAuth clients: Authority now understands two flavours of sender-constrained OAuth clients:
@@ -386,6 +395,7 @@ Authority now understands two flavours of sender-constrained OAuth clients:
- Declare client `audiences` in bootstrap manifests or plug-in provisioning metadata; Authority now defaults the token `aud` claim and `resource` indicator from this list, which is also used to trigger nonce enforcement for audiences such as `signer` and `attestor`. - Declare client `audiences` in bootstrap manifests or plug-in provisioning metadata; Authority now defaults the token `aud` claim and `resource` indicator from this list, which is also used to trigger nonce enforcement for audiences such as `signer` and `attestor`.
- **Mutual TLS clients** client registrations may declare an mTLS binding (`senderConstraint: mtls`). When enabled via `security.senderConstraints.mtls`, Authority validates the presented client certificate against stored bindings (`certificateBindings[]`), optional chain verification, and timing windows. Successful requests embed `cnf.x5t#S256` into the access token (and introspection output) so resource servers can enforce the certificate thumbprint. - **Mutual TLS clients** client registrations may declare an mTLS binding (`senderConstraint: mtls`). When enabled via `security.senderConstraints.mtls`, Authority validates the presented client certificate against stored bindings (`certificateBindings[]`), optional chain verification, and timing windows. Successful requests embed `cnf.x5t#S256` into the access token (and introspection output) so resource servers can enforce the certificate thumbprint.
- `security.senderConstraints.mtls.enforceForAudiences` forces mTLS whenever the requested `aud`/`resource` (or the client's configured audiences) intersect the configured allow-list (default includes `signer`). Clients configured for different sender constraints are rejected early so operator policy remains consistent. - `security.senderConstraints.mtls.enforceForAudiences` forces mTLS whenever the requested `aud`/`resource` (or the client's configured audiences) intersect the configured allow-list (default includes `signer`). Clients configured for different sender constraints are rejected early so operator policy remains consistent.
- Authority enforces mTLS bindings on `/token`, `/fresh-auth`, and `/introspect` by comparing the presented TLS client certificate thumbprint against the stored `authority_sender_certificate_hex` claim. Requests missing a certificate or presenting a different certificate are rejected with `invalid_token`, and the `authority_mtls_mismatch_total{reason=...}` counter is incremented for visibility.
- Certificate bindings now act as an allow-list: Authority verifies thumbprint, subject, issuer, serial number, and any declared SAN values against the presented certificate, with rotation grace windows applied to `notBefore/notAfter`. Operators can enforce subject regexes, SAN type allow-lists (`dns`, `uri`, `ip`), trusted certificate authorities, and rotation grace via `security.senderConstraints.mtls.*`. - Certificate bindings now act as an allow-list: Authority verifies thumbprint, subject, issuer, serial number, and any declared SAN values against the presented certificate, with rotation grace windows applied to `notBefore/notAfter`. Operators can enforce subject regexes, SAN type allow-lists (`dns`, `uri`, `ip`), trusted certificate authorities, and rotation grace via `security.senderConstraints.mtls.*`.
Both modes persist additional metadata in `authority_tokens`: `senderConstraint` records the enforced policy, while `senderKeyThumbprint` stores the DPoP JWK thumbprint or mTLS certificate hash captured at issuance. Downstream services can rely on these fields (and the corresponding `cnf` claim) when auditing offline copies of the token store. Both modes persist additional metadata in `authority_tokens`: `senderConstraint` records the enforced policy, while `senderKeyThumbprint` stores the DPoP JWK thumbprint or mTLS certificate hash captured at issuance. Downstream services can rely on these fields (and the corresponding `cnf` claim) when auditing offline copies of the token store.

View File

@@ -57,7 +57,62 @@ The script spins up MongoDB/Redis via Testcontainers and requires:
* Docker25 * Docker25
* Node20 (for Jest/Playwright) * Node20 (for Jest/Playwright)
--- #### Mongo2Go / OpenSSL shim
Multiple suites (Concelier connectors, Excititor worker/WebService, Scheduler)
fall back to [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) when a developer
does not have a local `mongod` listening on `127.0.0.1:27017`. Modern distros
ship OpenSSL3 by default, so you **must** expose the legacy OpenSSL1.1
libraries that the embedded `mongod` requires:
1. From the repo root, export the provided binaries before running any tests:
```bash
export LD_LIBRARY_PATH="$(pwd)/tests/native/openssl-1.1/linux-x64:${LD_LIBRARY_PATH:-}"
```
2. (Optional) If you only need the shim for a single command, prefix it:
```bash
LD_LIBRARY_PATH="$(pwd)/tests/native/openssl-1.1/linux-x64" \
dotnet test src/Concelier/StellaOps.Concelier.sln --nologo
```
3. CI runners or dev containers should either copy
`tests/native/openssl-1.1/linux-x64/libcrypto.so.1.1` and `libssl.so.1.1`
into a directory that is already on the default library path, or export the
`LD_LIBRARY_PATH` value shown above before invoking `dotnet test`.
The shim lives under `tests/native/openssl-1.1/README.md` with upstream source
and licensing details. When the system already has OpenSSL1.1 installed you
can skip this step.
#### Local Mongo helper
Some suites (Concelier WebService/Core, Exporter JSON) need a full
`mongod` instance when you want to debug outside of Mongo2Go (for example to
inspect data with `mongosh` or pin a specific server version). A thin wrapper
is available under `tools/mongodb/local-mongo.sh`:
```bash
# download (cached under .cache/mongodb-local) and start a local replica set
tools/mongodb/local-mongo.sh start
# reuse an existing data set
tools/mongodb/local-mongo.sh restart
# stop / clean
tools/mongodb/local-mongo.sh stop
tools/mongodb/local-mongo.sh clean
```
By default the script downloads MongoDB 6.0.16 for Ubuntu 22.04, binds to
`127.0.0.1:27017`, and initialises a single-node replica set called `rs0`. The
current URI is printed on start, e.g.
`mongodb://127.0.0.1:27017/?replicaSet=rs0`, and you can export it before
running `dotnet test` if a suite supports overriding its connection string.
---
### Concelier OSV↔GHSA parity fixtures ### Concelier OSV↔GHSA parity fixtures
@@ -106,4 +161,3 @@ flowchart LR
--- ---
*Last updated {{ "now" | date: "%Y%m%d" }}* *Last updated {{ "now" | date: "%Y%m%d" }}*

View File

@@ -8,6 +8,7 @@
| DOCS-REPLAY-185-004 | TODO | Docs Guild, Platform Guild | REPLAY-CORE-185-001 | Expand `docs/replay/DEVS_GUIDE_REPLAY.md` with integration checklist and cross-links to sections 3 & 11 of `docs/replay/DETERMINISTIC_REPLAY.md`. | Guide updated with checklist; references validated; lint passes. | | DOCS-REPLAY-185-004 | TODO | Docs Guild, Platform Guild | REPLAY-CORE-185-001 | Expand `docs/replay/DEVS_GUIDE_REPLAY.md` with integration checklist and cross-links to sections 3 & 11 of `docs/replay/DETERMINISTIC_REPLAY.md`. | Guide updated with checklist; references validated; lint passes. |
| DOCS-REPLAY-186-004 | TODO | Docs Guild, Scanner Guild | SCAN-REPLAY-186-001 | Publish `docs/replay/TEST_STRATEGY.md` detailing golden replay, feed drift, and tool upgrade verification steps; link from scanner architecture doc. | New doc merged; links verified; CI scenario notes documented. | | DOCS-REPLAY-186-004 | TODO | Docs Guild, Scanner Guild | SCAN-REPLAY-186-001 | Publish `docs/replay/TEST_STRATEGY.md` detailing golden replay, feed drift, and tool upgrade verification steps; link from scanner architecture doc. | New doc merged; links verified; CI scenario notes documented. |
| RUNBOOK-REPLAY-187-004 | TODO | Docs Guild, Ops Guild | EVID-REPLAY-187-001, CLI-REPLAY-187-002 | Create `/docs/runbooks/replay_ops.md` covering retention enforcement, RootPack rotation, offline kit workflows, and verification drills referencing `docs/replay/DETERMINISTIC_REPLAY.md`. | Runbook merged; rehearsal notes captured; cross-links added. | | RUNBOOK-REPLAY-187-004 | TODO | Docs Guild, Ops Guild | EVID-REPLAY-187-001, CLI-REPLAY-187-002 | Create `/docs/runbooks/replay_ops.md` covering retention enforcement, RootPack rotation, offline kit workflows, and verification drills referencing `docs/replay/DETERMINISTIC_REPLAY.md`. | Runbook merged; rehearsal notes captured; cross-links added. |
| DOCS-REACH-201-006 | TODO | Docs Guild | ZASTAVA-REACH-201-001, SCAN-REACH-201-002, SIGNALS-REACH-201-003 | Author reachability doc suite (`docs/signals/reachability.md`, `docs/signals/callgraph-formats.md`, `docs/signals/runtime-facts.md`, CLI/UI appendices) plus embed replay evidence guidance. | Docs merged with imposed rule text; cross-links to Scanner/Zastava/Replay guides validated. |
| DOCS-OBS-50-002 | TODO | Docs Guild, Security Guild | TELEMETRY-OBS-50-002 | Author `/docs/observability/telemetry-standards.md` detailing common fields, scrubbing policy, sampling defaults, and redaction override procedure. | Doc merged; imposed rule banner present; examples validated with telemetry fixtures; security review sign-off captured. | | DOCS-OBS-50-002 | TODO | Docs Guild, Security Guild | TELEMETRY-OBS-50-002 | Author `/docs/observability/telemetry-standards.md` detailing common fields, scrubbing policy, sampling defaults, and redaction override procedure. | Doc merged; imposed rule banner present; examples validated with telemetry fixtures; security review sign-off captured. |
| DOCS-OBS-50-003 | TODO | Docs Guild, Observability Guild | TELEMETRY-OBS-50-001 | Create `/docs/observability/logging.md` covering structured log schema, dos/don'ts, tenant isolation, and copyable examples. | Doc merged with banner; sample logs redacted; lint passes; linked from coding standards. | | DOCS-OBS-50-003 | TODO | Docs Guild, Observability Guild | TELEMETRY-OBS-50-001 | Create `/docs/observability/logging.md` covering structured log schema, dos/don'ts, tenant isolation, and copyable examples. | Doc merged with banner; sample logs redacted; lint passes; linked from coding standards. |
| DOCS-OBS-50-004 | TODO | Docs Guild, Observability Guild | TELEMETRY-OBS-50-002 | Draft `/docs/observability/tracing.md` explaining context propagation, async linking, CLI header usage, and sampling strategies. | Doc merged; imposed rule banner included; diagrams updated; references to CLI/Console features added. | | DOCS-OBS-50-004 | TODO | Docs Guild, Observability Guild | TELEMETRY-OBS-50-002 | Draft `/docs/observability/tracing.md` explaining context propagation, async linking, CLI header usage, and sampling strategies. | Doc merged; imposed rule banner included; diagrams updated; references to CLI/Console features added. |
@@ -304,7 +305,8 @@
> 2025-11-03: DOCS-AIAI-31-002 completed architecture deep dive documents pipeline, deterministic tooling, caching, profiles, and deployment guidance. > 2025-11-03: DOCS-AIAI-31-002 completed architecture deep dive documents pipeline, deterministic tooling, caching, profiles, and deployment guidance.
| DOCS-AIAI-31-003 | DONE (2025-11-03) | Docs Guild, Advisory AI Guild | AIAI-31-006 | Write `/docs/advisory-ai/api.md` describing endpoints, schemas, errors, rate limits. | API doc aligned with OpenAPI; examples validated; checklist appended. | | DOCS-AIAI-31-003 | DONE (2025-11-03) | Docs Guild, Advisory AI Guild | AIAI-31-006 | Write `/docs/advisory-ai/api.md` describing endpoints, schemas, errors, rate limits. | API doc aligned with OpenAPI; examples validated; checklist appended. |
> 2025-11-03: DOCS-AIAI-31-003 completed `docs/advisory-ai/api.md` covers scopes, request/response schema, rate limits, error codes, observability, offline notes. > 2025-11-03: DOCS-AIAI-31-003 completed `docs/advisory-ai/api.md` covers scopes, request/response schema, rate limits, error codes, observability, offline notes.
| DOCS-AIAI-31-004 | BLOCKED (2025-11-03) | Docs Guild, Console Guild | CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001 | Create `/docs/advisory-ai/console.md` with screenshots, a11y notes, copy-as-ticket instructions. | Doc merged; images stored; checklist appended. | | DOCS-AIAI-31-004 | DOING (2025-11-07) | Docs Guild, Console Guild | CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001 | Create `/docs/advisory-ai/console.md` with screenshots, a11y notes, copy-as-ticket instructions. | Doc merged; images stored; checklist appended. |
> 2025-11-07: Draft outline committed; waiting on final console endpoints for screenshots + API captures.
> 2025-11-03: BLOCKED waiting for Console endpoints/widgets (CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001) to land before documenting UI flows. > 2025-11-03: BLOCKED waiting for Console endpoints/widgets (CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001) to land before documenting UI flows.
| DOCS-AIAI-31-005 | BLOCKED (2025-11-03) | Docs Guild, DevEx/CLI Guild | CLI-VULN-29-001, CLI-VEX-30-001, AIAI-31-004C | Publish `/docs/advisory-ai/cli.md` covering commands, exit codes, scripting patterns. | Doc merged; examples tested; checklist appended. | | DOCS-AIAI-31-005 | BLOCKED (2025-11-03) | Docs Guild, DevEx/CLI Guild | CLI-VULN-29-001, CLI-VEX-30-001, AIAI-31-004C | Publish `/docs/advisory-ai/cli.md` covering commands, exit codes, scripting patterns. | Doc merged; examples tested; checklist appended. |
> 2025-11-03: BLOCKED awaiting CLI implementation (`stella advise run`) and golden outputs (CLI-VULN-29-001, CLI-VEX-30-001, AIAI-31-004C). > 2025-11-03: BLOCKED awaiting CLI implementation (`stella advise run`) and golden outputs (CLI-VULN-29-001, CLI-VEX-30-001, AIAI-31-004C).

View File

@@ -0,0 +1,35 @@
# Advisory AI Console Workflows
_Last updated: 2025-11-07_
This guide documents the forthcoming Advisory AI console experience so that console, docs, and QA guilds share a single reference while the new endpoints finish landing.
## 1. Entry points & navigation
- **Dashboard tile**: `Advisory AI` card on the console overview routes to `/console/vuln/advisory-ai` once CONSOLE-VULN-29-001 ships. The tile must include the current model build stamp and data freshness time.
- **Deep links**: Copy-as-ticket payloads link back into the console using `/console/vex/{statementId}` (CONSOLE-VEX-30-001). Provide fallbacks that open the Evidence modal with a toast if the workspace is still loading.
## 2. Evidence surfacing
| Workflow | Required API | Notes |
| --- | --- | --- |
| Findings overview | `GET /console/vuln/findings` | Must include policy verdict badge, VEX justification summary, and last-seen timestamps. |
| Evidence drawer | `GET /console/vex/statements/{id}` | Stream SSE chunk descriptions so long-form provenance renders progressively. |
| Copy as ticket | `POST /console/vuln/tickets` | Returns signed payload + attachment list for JIRA/ServiceNow templates. |
## 3. Accessibility & offline requirements
- Console screens must pass WCAG 2.2 AA contrast and provide focus order that matches the keyboard shortcuts planned for Advisory AI (see `docs/advisory-ai/overview.md`).
- All screenshots captured for this doc must come from sealed-mode bundles (no external fonts/CDNs). Store them under `docs/assets/advisory-ai/console/` with hashed filenames.
- Modal dialogs need `aria-describedby` attributes referencing the explanation text returned by the API; translation strings must live with existing locale packs.
## 4. Copy-as-ticket guidance
1. Operators select one or more VEX-backed findings.
2. Console renders the sanitized payload (JSON) plus context summary for the receiving system.
3. Users can download the payload or send it via webhook; both flows must log `console.ticket.export` events for audit.
## 5. Open items before publication
- [ ] Replace placeholder API responses with captures from the first merged build of CONSOLE-VULN-29-001 / CONSOLE-VEX-30-001.
- [ ] Capture at least two screenshots (list view + evidence drawer) once UI polish is complete.
- [ ] Verify copy-as-ticket instructions with Support to ensure the payload fields align with existing SOC runbooks.
> Tracking: DOCS-AIAI-31-004 (Docs Guild, Console Guild)
**Reference**: API contracts and sample payloads live in `docs/api/console/workspaces.md` (see `/console/vuln/*` and `/console/vex/*` sections) plus the JSON fixtures under `docs/api/console/samples/`.

View File

@@ -0,0 +1,5 @@
{"event":"statement.created","data":{"statementId":"vex:tenant-default:jwt-auth:5d1a","advisoryId":"CVE-2024-12345","product":"registry.local/ops/auth:2025.10.0","state":"under_investigation","justification":"exploit_observed","sequence":4178,"updatedAt":"2025-11-07T23:10:09Z"}}
{"event":"statement.updated","data":{"statementId":"vex:tenant-default:jwt-auth:5d1a","advisoryId":"CVE-2024-12345","product":"registry.local/ops/auth:2025.10.0","state":"fixed","justification":"solution_available","sequence":4182,"updatedAt":"2025-11-08T11:44:32Z"}}
{"event":"statement.conflict","data":{"statementId":"vex:tenant-default:jwt-auth:5d1a","advisoryId":"CVE-2024-12345","product":"registry.local/ops/auth:2025.10.0","conflictSummary":"Excititor statement GHSA-1111 differs on status","sequence":4183,"updatedAt":"2025-11-08T11:44:59Z"}}
{"event":"statement.updated","data":{"statementId":"vex:tenant-default:jwt-auth:5d1a","advisoryId":"CVE-2024-12345","product":"registry.local/ops/auth:2025.10.0","state":"fixed","justification":"solution_available","sequence":4184,"updatedAt":"2025-11-08T11:45:04Z"}}
{"event":"statement.deleted","data":{"statementId":"vex:tenant-default:legacy:1a2b","advisoryId":"CVE-2023-9999","product":"registry.local/ops/legacy:2024.01.0","sequence":4185,"updatedAt":"2025-11-08T12:01:01Z"}}

View File

@@ -0,0 +1,84 @@
{
"items": [
{
"findingId": "tenant-default:advisory-ai:sha256:5d1a",
"coordinates": {
"advisoryId": "CVE-2024-12345",
"package": "pkg:npm/jsonwebtoken@9.0.2",
"component": "jwt-auth-service",
"image": "registry.local/ops/auth:2025.10.0"
},
"summary": "jsonwebtoken <10.0.0 allows algorithm downgrade.",
"severity": "high",
"cvss": 8.1,
"kev": true,
"policyBadge": "fail",
"vex": {
"statementId": "vex:tenant-default:jwt-auth:5d1a",
"state": "under_investigation",
"justification": "Advisory AI flagged reachable path via Scheduler run 42."
},
"reachability": {
"status": "reachable",
"lastObserved": "2025-11-07T23:11:04Z",
"signalsVersion": "signals-2025.310.1"
},
"evidence": {
"sbomDigest": "sha256:6c81f2bbd8bd7336f197f3f68fba2f76d7287dd1a5e2a0f0e9f14f23f3c2f917",
"policyRunId": "policy-run::2025-11-07::ca9f",
"attestationId": "dsse://authority/attest/84a2"
},
"timestamps": {
"firstSeen": "2025-10-31T04:22:18Z",
"lastSeen": "2025-11-07T23:16:51Z"
}
},
{
"findingId": "tenant-default:advisory-ai:sha256:9bf4",
"coordinates": {
"advisoryId": "GHSA-xxxx-yyyy-zzzz",
"package": "pkg:docker/library/nginx@1.25.2",
"component": "ingress-gateway",
"image": "registry.local/ops/ingress:2025.09.1"
},
"summary": "Heap overflow in nginx HTTP/3 parsing.",
"severity": "critical",
"cvss": 9.8,
"kev": false,
"policyBadge": "warn",
"vex": {
"statementId": "vex:tenant-default:ingress:9bf4",
"state": "not_affected",
"justification": "component_not_present"
},
"reachability": {
"status": "unknown",
"signalsVersion": "signals-2025.309.0"
},
"evidence": {
"sbomDigest": "sha256:99f1e2a7aa0f7c970dcb6674244f0bfb5f37148e3ee09fd4f925d3358dea2239",
"policyRunId": "policy-run::2025-11-06::b210",
"attestationId": "dsse://authority/attest/1d34"
},
"timestamps": {
"firstSeen": "2025-10-29T18:03:11Z",
"lastSeen": "2025-11-07T10:45:03Z"
}
}
],
"facets": {
"severity": [
{ "value": "critical", "count": 1 },
{ "value": "high", "count": 1 }
],
"policyBadge": [
{ "value": "fail", "count": 1 },
{ "value": "warn", "count": 1 }
],
"reachability": [
{ "value": "reachable", "count": 1 },
{ "value": "unknown", "count": 1 }
]
},
"nextPageToken": "eyJjdXJzb3IiOiJmZjg0NiJ9"
}

View File

@@ -0,0 +1,311 @@
# Console Workspaces API
_Tracking: CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, DOCS-AIAI-31-004_
## 1. Goals & Scope
The console workspaces provide read-only aggregates for Advisory AI operators:
- `/console/vuln/*` surfaces tenant-scoped findings annotated with policy verdicts, VEX justifications, Scheduler reachability signals, and Advisory AI rationale.
- `/console/vex/*` streams the underlying VEX statements, conflicts, and justification summaries (with SSE support for live updates).
All endpoints MUST:
1. Remain deterministic offline (stable sort keys, ISO-8601 UTC timestamps, hashed assets).
2. Operate with Authority-issued DPoP or mTLS client credentials that include `console:read` and either `vuln:read` or `vex:read`.
3. Respect tenant isolation every request carries `X-StellaOps-Tenant`.
## 2. Shared Request/Response Conventions
| Requirement | Description |
| --- | --- |
| Headers | `Authorization: DPoP <token>`, `DPoP: <proof>`, `X-StellaOps-Tenant: <tenantId>`, `Accept: application/json` (or `text/event-stream` for SSE). |
| Pagination | Cursor-based via `pageToken`; defaults to 50 items, max 200. Cursors are opaque, base64url, signed. |
| Sorting | Findings sorted by `(severity desc, exploitScore desc, findingId asc)`. Statements sorted by `(lastUpdated desc, statementId asc)`. |
| Dates | RFC 3339 / ISO-8601 UTC (e.g., `2025-11-08T12:02:11Z`). |
| Determinism | All arrays must be pre-sorted; no server-generated uuids in responses. |
## 3. Vulnerability Workspace (`/console/vuln/*`)
### 3.1 `GET /console/vuln/findings`
Query parameters:
| Parameter | Type | Notes |
| --- | --- | --- |
| `pageToken` | string | Optional cursor from previous response. |
| `pageSize` | int | 1-200, default 50. |
| `severity` | string[] | Accepts `critical`, `high`, `medium`, `low`, `info`. |
| `product` | string[] | SBOM `purl` or image digest anchors. |
| `policyBadge` | string[] | `pass`, `warn`, `fail`, `waived`. |
| `vexState` | string[] | `not_affected`, `fixed`, `under_investigation`, etc. |
| `reachability` | string[] | `reachable`, `unreachable`, `unknown`. |
| `search` | string | Substring match on CVE/GHSA/KEV ID (case-insensitive). |
Response body:
```jsonc
{
"items": [
{
"findingId": "tenant-default:advisory-ai:sha256:5d1a",
"coordinates": {
"advisoryId": "CVE-2024-12345",
"package": "pkg:npm/jsonwebtoken@9.0.2",
"component": "jwt-auth-service",
"image": "registry.local/ops/auth:2025.10.0"
},
"summary": "jsonwebtoken <10.0.0 allows algorithm downgrade.",
"severity": "high",
"cvss": 8.1,
"kev": true,
"policyBadge": "fail",
"vex": {
"statementId": "vex:tenant-default:jwt-auth:5d1a",
"state": "under_investigation",
"justification": "Advisory AI flagged reachable path via Scheduler run 42."
},
"reachability": {
"status": "reachable",
"lastObserved": "2025-11-07T23:11:04Z",
"signalsVersion": "signals-2025.310.1"
},
"evidence": {
"sbomDigest": "sha256:6c81…",
"policyRunId": "policy-run::2025-11-07::ca9f",
"attestationId": "dsse://authority/attest/84a2"
},
"timestamps": {
"firstSeen": "2025-10-31T04:22:18Z",
"lastSeen": "2025-11-07T23:16:51Z"
}
}
],
"facets": {
"severity": [
{ "value": "critical", "count": 2 },
{ "value": "high", "count": 7 }
],
"policyBadge": [
{ "value": "fail", "count": 6 },
{ "value": "warn", "count": 3 },
{ "value": "waived", "count": 1 }
],
"reachability": [
{ "value": "reachable", "count": 5 },
{ "value": "unreachable", "count": 2 },
{ "value": "unknown", "count": 1 }
]
},
"nextPageToken": "eyJjdXJzb3IiOiJmZjg0NiJ9"
}
```
### 3.2 `GET /console/vuln/facets`
Returns the full facet catalog (counts by severity, product, policy badge, VEX state, reachability, KEV flag). Designed for sidebar filters without paging; identical parameter surface as `/findings`.
### 3.3 `GET /console/vuln/{findingId}`
Returns the full finding document, including evidence timeline, policy overlays, and export-ready metadata:
```jsonc
{
"findingId": "tenant-default:advisory-ai:sha256:5d1a",
"details": {
"description": "jsonwebtoken <10.0.0 allows algorithm downgrade.",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2024-12345",
"https://github.com/auth0/node-jsonwebtoken/security/advisories/GHSA-45mw-4jw3-g2wg"
],
"exploitAvailability": "known_exploit"
},
"policyBadges": [
{
"policyId": "policy://tenant-default/runtime-hardening",
"verdict": "fail",
"explainUrl": "https://console.local/policy/runs/policy-run::2025-11-07::ca9f"
}
],
"vex": {
"statementId": "vex:tenant-default:jwt-auth:5d1a",
"state": "under_investigation",
"justification": "Runtime telemetry confirmed exploitation path.",
"impactStatement": "Token exchange service remains exposed until patch 2025.11.2.",
"remediations": [
{
"type": "patch",
"description": "Upgrade jwt-auth-service to 2025.11.2.",
"deadline": "2025-11-12T00:00:00Z"
}
]
},
"reachability": {
"status": "reachable",
"callPathSamples": [
"api-gateway -> jwt-auth-service -> jsonwebtoken.verify"
],
"lastUpdated": "2025-11-07T23:11:04Z"
},
"evidence": {
"sbom": {
"digest": "sha256:6c81…",
"componentPath": [
"/src/jwt-auth/package.json",
"/src/jwt-auth/node_modules/jsonwebtoken"
]
},
"attestations": [
{
"type": "scan-report",
"attestationId": "dsse://authority/attest/84a2",
"signer": "attestor@stella-ops.org",
"bundleDigest": "sha256:e2bb…"
}
]
},
"timestamps": {
"firstSeen": "2025-10-31T04:22:18Z",
"lastSeen": "2025-11-07T23:16:51Z",
"vexLastUpdated": "2025-11-07T23:10:09Z"
}
}
```
### 3.4 `POST /console/vuln/tickets`
```jsonc
POST /console/vuln/tickets
{
"tenant": "tenant-default",
"selection": [
"tenant-default:advisory-ai:sha256:5d1a",
"tenant-default:advisory-ai:sha256:9bf4"
],
"targetSystem": "servicenow",
"metadata": {
"assignmentGroup": "runtime-security",
"priority": "P1"
}
}
```
Response:
```jsonc
{
"ticketId": "console-ticket::tenant-default::2025-11-08::00018",
"payload": {
"version": "2025-11-01",
"tenant": "tenant-default",
"findings": [
{ "findingId": "tenant-default:advisory-ai:sha256:5d1a", "severity": "high" },
{ "findingId": "tenant-default:advisory-ai:sha256:9bf4", "severity": "critical" }
],
"policyBadge": "fail",
"vexSummary": "2 reachable findings pending patch.",
"attachments": [
{
"type": "json",
"name": "console-ticket-20251108.json",
"digest": "sha256:1fdd…",
"contentType": "application/json",
"expiresAt": "2025-11-15T00:00:00Z"
}
]
},
"auditEventId": "console.ticket.export::2025-11-08::00018"
}
```
Requests emit `console.ticket.export` audit events (tenant, user, selection counts, target system).
## 4. VEX Workspace (`/console/vex/*`)
### 4.1 `GET /console/vex/statements`
Parameters mirror `/console/vuln/findings` plus:
| Parameter | Type | Notes |
| --- | --- | --- |
| `advisoryId` | string[] | CVE/GHSA/OVAL identifiers. |
| `justification` | string[] | `exploit_observed`, `component_not_present`, etc. |
| `statementType` | string[] | `vex`, `openvex`, `custom`, `advisory_ai`. |
| `prefer` | string | `prefer=stream` enables chunked streaming (NDJSON). |
Response (paged JSON):
```jsonc
{
"items": [
{
"statementId": "vex:tenant-default:jwt-auth:5d1a",
"advisoryId": "CVE-2024-12345",
"product": "registry.local/ops/auth:2025.10.0",
"status": "under_investigation",
"justification": "exploit_observed",
"lastUpdated": "2025-11-07T23:10:09Z",
"source": {
"type": "advisory_ai",
"modelBuild": "aiai-console-2025-10-28",
"confidence": 0.74
},
"links": [
{
"rel": "finding",
"href": "/console/vuln/findings/tenant-default:advisory-ai:sha256:5d1a"
}
]
}
],
"nextPageToken": null
}
```
When `Accept: text/event-stream`, the endpoint emits events (see §4.3) instead of paged JSON.
### 4.2 `GET /console/vex/statements/{statementId}`
Returns the canonical statement plus provenance extracts. SSE clients can call this endpoint when they need full bodies after receiving a summary event.
### 4.3 `GET /console/vex/events` (SSE)
Streams live updates for VEX statements affecting the tenant:
- Event types: `statement.created`, `statement.updated`, `statement.deleted`, `statement.conflict`.
- Fields: `id`, `advisoryId`, `product`, `vexState`, `severityHint`, `policyBadge`, `conflictSummary`, `sequence`.
- Replay: Clients include `Last-Event-ID`; server resumes from sequence.
- Heartbeats every 15 seconds (`event: keepalive`, `data: {}`).
Example event payload:
```jsonc
event: statement.updated
data: {
"statementId": "vex:tenant-default:jwt-auth:5d1a",
"advisoryId": "CVE-2024-12345",
"product": "registry.local/ops/auth:2025.10.0",
"state": "fixed",
"justification": "solution_available",
"sequence": 4182,
"updatedAt": "2025-11-08T11:44:32Z"
}
```
## 5. Signals & Scheduler Integration
- Reachability data is materialized by Scheduler delta jobs (`SCHED-CONSOLE-23-001`). `/console/vuln/findings` should cache the most recent job ID and expose `signalsVersion`.
- VEX justification fields reference Excititor statement IDs; ensure the gateway checks Excititor availability and degrades gracefully (returns `state: unavailable` plus telemetry).
- Scheduler must publish `console.vuln.refresh` events whenever advisory/VEX deltas warrant workspace refresh; console SSE endpoint may piggyback on the same Redis/NATS channel.
## 6. Determinism & Offline Notes
1. All responses are compressible JSON; no CDN fonts/assets referenced.
2. SSE endpoints must tolerate sealed mode by operating on loopback addresses only.
3. `authority-sealed-ci.json` (see DEVOPS-AIRGAP-57-002) is the evidence Authority consumes before enabling these APIs for sealed tenants; console responses echo `sealed: true/false` flags for UI badges.
## 7. Sample Payloads for Docs
- `docs/api/console/samples/vuln-findings-sample.json` exported via `scripts/generate-console-samples.ts` (placeholder script to be added when backend lands).
- `docs/api/console/samples/vex-statement-sse.ndjson` contains 5 chronological SSE events for screenshot reproduction.
> Until backend implementations ship, use the examples above to unblock DOCS-AIAI-31-004; replace them with live captures once the gateway endpoints are available in staging.

View File

@@ -18,14 +18,35 @@
- Build linksets from conflicting advisory observations (e.g., differing severity or status flags). - Build linksets from conflicting advisory observations (e.g., differing severity or status flags).
- Confirm conflict markers propagate to `AdvisoryLinkset` outputs and associated metrics/log records. - Confirm conflict markers propagate to `AdvisoryLinkset` outputs and associated metrics/log records.
- Capture deterministic ordering of conflict explanations for evidence exports. - Capture deterministic ordering of conflict explanations for evidence exports.
- Coverage landed via `AdvisoryObservationFactoryTests.Create_PreservesRawReferencesForConflictAudits` (raw linkset + attribute parity) and `AdvisoryEventLogTests.AppendAsync_SortsConflictStatementIds` (canonical conflict JSON + stable hashes).
3. **Evidence/export parity** 3. **Evidence/export parity**
- Re-run observation/linkset pipelines against identical fixtures and assert resulting evidence manifests hash-identically. - Re-run observation/linkset pipelines against identical fixtures and assert resulting evidence manifests hash-identically.
- Track monotonic `supersedes` chains and ensure canonical link records include `PRIMARY` schemes. - Track monotonic `supersedes` chains and ensure canonical link records include `PRIMARY` schemes.
- `JsonExportSnapshotBuilderTests.WriteAsync_DifferentInputOrderProducesSameDigest` now proves export bundles remain byte-identical regardless of advisory enumeration order; digest sampling extends `ProducesIdenticalBytesAcrossRuns`.
## Mongo2Go/OpenSSL toolchain
Concelier solution tests (and most connector suites) depend on Mongo2Gos embedded `mongod`, which is linked against OpenSSL 1.1. The repo already ships the required libraries in `tests/native/openssl-1.1/linux-x64/{libcrypto.so.1.1,libssl.so.1.1}`; use them instead of installing global packages so offline runners stay deterministic.
1. Add the shim to your shell before executing any Mongo-backed suite:
```bash
export LD_LIBRARY_PATH="$(git rev-parse --show-toplevel)/tests/native/openssl-1.1/linux-x64:${LD_LIBRARY_PATH:-}"
```
2. For single commands you can prefix the invocation (handy for CI copy/paste):
```bash
LD_LIBRARY_PATH="$(pwd)/tests/native/openssl-1.1/linux-x64" \
dotnet test src/Concelier/StellaOps.Concelier.sln --nologo
```
3. The shims provenance and troubleshooting notes live in `tests/native/openssl-1.1/README.md`; reference it when mirroring the toolchain into air-gapped runners.
## Migration Steps ## Migration Steps
- [ ] Retire `StellaOps.Concelier.Merge.Tests` determinism suites once observation/linkset equivalents land. - [x] Retire `StellaOps.Concelier.Merge.Tests` determinism suites once observation/linkset equivalents land.
- [ ] Introduce new regression fixtures under `StellaOps.Concelier.Core.Tests` (shared via `StellaOps.Concelier.Testing`). - [x] Introduce new regression fixtures under `StellaOps.Concelier.Core.Tests` (shared via `StellaOps.Concelier.Testing`).
- [ ] Wire test helpers to Mongo in-memory harness for end-to-end parity runs. - [ ] Wire test helpers to Mongo in-memory harness for end-to-end parity runs.
- [ ] Update documentation (`docs/migration/no-merge.md`) with validation checklist once new tests are green. - [ ] Update documentation (`docs/migration/no-merge.md`) with validation checklist once new tests are green.

View File

@@ -44,6 +44,7 @@ Follow the sprint files below in order. Update task status in both `SPRINTS` and
> 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-001 marked DONE published `docs/migration/no-merge.md` with rollout, backfill, validation, and rollback guidance for the LNM cutover.
> 2025-11-04: GRAPH-INDEX-28-011 marked DONE (Graph Indexer Guild) SBOM ingest DI wiring now emits graph snapshots by default, snapshot root configurable via `STELLAOPS_GRAPH_SNAPSHOT_DIR`, and Graph Indexer tests exercised with Mongo URI guidance. > 2025-11-04: GRAPH-INDEX-28-011 marked DONE (Graph Indexer Guild) SBOM ingest DI wiring now emits graph snapshots by default, snapshot root configurable via `STELLAOPS_GRAPH_SNAPSHOT_DIR`, and Graph Indexer tests exercised with Mongo URI guidance.
> 2025-11-06: MERGE-LNM-21-002 remains DOING (BE-Merge) default-off merge DI + job gating landed, but Concelier WebService ingest/mirror tests are failing; guard and migration fixes pending before completion. > 2025-11-06: MERGE-LNM-21-002 remains DOING (BE-Merge) default-off merge DI + job gating landed, but Concelier WebService ingest/mirror tests are failing; guard and migration fixes pending before completion.
> 2025-11-07: MERGE-LNM-21-002 marked DONE (BE-Merge) Link-Not-Merge telemetry gaps closed by introducing `StellaOps.Ingestion.Telemetry`, guard metrics/tests updated, and Concelier Exporter JSON + solution smoke suites re-run to cover the new filename normalization.
> 2025-11-06: TASKRUN-43-001 marked DONE (Task Runner Guild) approvals resume API now requeues packs, plan snapshots persisted, and filesystem artifact uploader stores manifests/files for offline review. > 2025-11-06: TASKRUN-43-001 marked DONE (Task Runner Guild) approvals resume API now requeues packs, plan snapshots persisted, and filesystem artifact uploader stores manifests/files for offline review.
> 2025-11-06: CLI-POLICY-23-005 marked DONE (DevEx/CLI Guild) policy activate CLI verifies scheduling/approval flow, Spectre console fallbacks emit warnings offline, and full CLI suite passes against local feeds. > 2025-11-06: CLI-POLICY-23-005 marked DONE (DevEx/CLI Guild) policy activate CLI verifies scheduling/approval flow, Spectre console fallbacks emit warnings offline, and full CLI suite passes against local feeds.
> 2025-11-07: DOCS-AIAI-31-007 marked DONE (Docs Guild, Security Guild) published `/docs/security/assistant-guardrails.md` covering redaction rules, blocked phrases, telemetry, and alert wiring. > 2025-11-07: DOCS-AIAI-31-007 marked DONE (Docs Guild, Security Guild) published `/docs/security/assistant-guardrails.md` covering redaction rules, blocked phrases, telemetry, and alert wiring.

View File

@@ -18,7 +18,8 @@ ATTEST-VERIFY-74-001 | DONE | Emit telemetry (spans/metrics) tagged by subject,
ATTEST-VERIFY-74-002 | DONE (2025-11-01) | Document verification report schema and explainability in `/docs/modules/attestor/workflows.md`. Dependencies: ATTEST-VERIFY-73-001. | Verification Guild, Docs Guild (src/Attestor/StellaOps.Attestor.Verify/TASKS.md) ATTEST-VERIFY-74-002 | DONE (2025-11-01) | Document verification report schema and explainability in `/docs/modules/attestor/workflows.md`. Dependencies: ATTEST-VERIFY-73-001. | Verification Guild, Docs Guild (src/Attestor/StellaOps.Attestor.Verify/TASKS.md)
ATTESTOR-72-001 | DONE | Scaffold service (REST API skeleton, storage interfaces, KMS integration stubs) and DSSE validation pipeline. Dependencies: ATTEST-ENVELOPE-72-001. | Attestor Service Guild (src/Attestor/StellaOps.Attestor/TASKS.md) ATTESTOR-72-001 | DONE | Scaffold service (REST API skeleton, storage interfaces, KMS integration stubs) and DSSE validation pipeline. Dependencies: ATTEST-ENVELOPE-72-001. | Attestor Service Guild (src/Attestor/StellaOps.Attestor/TASKS.md)
ATTESTOR-72-002 | DONE | Implement attestation store (DB tables, object storage integration), CRUD, and indexing strategies. Dependencies: ATTESTOR-72-001. | Attestor Service Guild (src/Attestor/StellaOps.Attestor/TASKS.md) ATTESTOR-72-002 | DONE | Implement attestation store (DB tables, object storage integration), CRUD, and indexing strategies. Dependencies: ATTESTOR-72-001. | Attestor Service Guild (src/Attestor/StellaOps.Attestor/TASKS.md)
ATTESTOR-72-003 | BLOCKED | Validate attestation store TTL against production-like Mongo/Redis stack; capture logs and remediation plan. Dependencies: ATTESTOR-72-002. | Attestor Service Guild, QA Guild (src/Attestor/StellaOps.Attestor/TASKS.md) ATTESTOR-72-003 | DONE (2025-11-03) | Validate attestation store TTL against production-like Mongo/Redis stack; capture logs and remediation plan. Dependencies: ATTESTOR-72-002. | Attestor Service Guild, QA Guild (src/Attestor/StellaOps.Attestor/TASKS.md)
> 2025-11-03: TTL soak tests captured in `docs/modules/attestor/ttl-validation.md`; Mongo/Redis evidence archived for replay.
ATTESTOR-73-001 | DONE (2025-11-01) | Implement signing endpoint with Ed25519/ECDSA support, KMS integration, and audit logging. Dependencies: ATTESTOR-72-002, KMS-72-001. | Attestor Service Guild, KMS Guild (src/Attestor/StellaOps.Attestor/TASKS.md) ATTESTOR-73-001 | DONE (2025-11-01) | Implement signing endpoint with Ed25519/ECDSA support, KMS integration, and audit logging. Dependencies: ATTESTOR-72-002, KMS-72-001. | Attestor Service Guild, KMS Guild (src/Attestor/StellaOps.Attestor/TASKS.md)
@@ -48,8 +49,11 @@ AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Provision new scopes (`airgap:seal`, `a
> 2025-11-04: Verified discovery metadata now advertises the airgap scope trio, `etc/authority.yaml.sample` + offline kit docs ship the new roles, and Authority tests enforce tenant gating for `airgap:*` scopes (`dotnet test` executed). > 2025-11-04: Verified discovery metadata now advertises the airgap scope trio, `etc/authority.yaml.sample` + offline kit docs ship the new roles, and Authority tests enforce tenant gating for `airgap:*` scopes (`dotnet test` executed).
AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. Dependencies: AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. Dependencies: AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-04: `/authority/audit/airgap` minimal APIs persist tenant-scoped records with paging, RBAC checks for `airgap:import`/`airgap:status:read` pass, and Authority integration suite (187 tests) exercised the audit flow. > 2025-11-04: `/authority/audit/airgap` minimal APIs persist tenant-scoped records with paging, RBAC checks for `airgap:import`/`airgap:status:read` pass, and Authority integration suite (187 tests) exercised the audit flow.
AUTH-AIRGAP-57-001 | BLOCKED (2025-11-01) | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. Dependencies: AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002. | Authority Core & Security Guild, DevOps Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-AIRGAP-57-001 | DOING (2025-11-08) | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. Dependencies: AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002. | Authority Core & Security Guild, DevOps Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-01: AUTH-AIRGAP-57-001 blocked pending definition of sealed-confirmation evidence and configuration shape before gating (Authority Core & Security Guild, DevOps Guild). > 2025-11-01: AUTH-AIRGAP-57-001 blocked pending definition of sealed-confirmation evidence and configuration shape before gating (Authority Core & Security Guild, DevOps Guild).
> 2025-11-08: Flipped to DOING; partnering with DevOps on artifacts so Authority gating tests can consume sealed confirmations once published (target 2025-11-10).
> 2025-11-07: Still waiting on DEVOPS-AIRGAP-57-002 sealed-mode CI suite (`ops/devops/sealed-mode-ci/*`) to publish artefacts so Authority can wire the gating tests.
> 2025-11-08: DevOps sealed-mode CI now uploads `artifacts/sealed-mode-ci/<commit>/authority-sealed-ci.json`; Authority to hook the gating middleware/tests up to that feed next.
AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-01: AUTH-NOTIFY-38-001 completed—Notify scope catalog, discovery metadata, docs, configuration samples, and service tests updated for new roles. > 2025-11-01: AUTH-NOTIFY-38-001 completed—Notify scope catalog, discovery metadata, docs, configuration samples, and service tests updated for new roles.
AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. Dependencies: AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. Dependencies: AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
@@ -73,6 +77,7 @@ AUTH-PACKS-41-001 | DONE (2025-11-04) | Define CLI SSO profiles and pack scopes
> 2025-11-04: Verified discovery metadata, OpenAPI, `etc/authority.yaml.sample`, and offline kit docs reflect the packs scope set; Authority suite re-run (`dotnet test`) to confirm tenant gating and policy checks. > 2025-11-04: Verified discovery metadata, OpenAPI, `etc/authority.yaml.sample`, and offline kit docs reflect the packs scope set; Authority suite re-run (`dotnet test`) to confirm tenant gating and policy checks.
> 2025-11-02: Shared OpenSSL 1.1 shim now feeds Mongo2Go for Authority & Signals tests, keeping pack scope regressions and other Mongo flows working on OpenSSL 3 hosts. > 2025-11-02: Shared OpenSSL 1.1 shim now feeds Mongo2Go for Authority & Signals tests, keeping pack scope regressions and other Mongo flows working on OpenSSL 3 hosts.
AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. Dependencies: AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. Dependencies: AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-07: AUTH-PACKS-41-001 + TASKRUN-42-001 are DONE; remaining blocker is ORCH-SVC-42-101 (still TODO) for log streaming/approvals APIs. Not deleted—waiting on Orchestrator to publish contracts.
[Identity & Signing] 100.B) Authority.II [Identity & Signing] 100.B) Authority.II
@@ -80,8 +85,17 @@ Depends on: Sprint 100.B - Authority.I
Summary: Identity & Signing focus on Authority (phase II). Summary: Identity & Signing focus on Authority (phase II).
Task ID | State | Task description | Owners (Source) Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
AUTH-POLICY-23-002 | BLOCKED (2025-10-29) | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. Dependencies: AUTH-POLICY-23-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-POLICY-23-001 | DONE (2025-10-27) | Introduce fine-grained policy scopes (`policy:read`, `policy:author`, `policy:review`, `policy:simulate`, `findings:read`) for CLI/service accounts; update discovery metadata, issuer templates, and offline defaults. Dependencies: AUTH-AOC-19-002. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md)
AUTH-POLICY-23-003 | BLOCKED (2025-10-29) | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. Dependencies: AUTH-POLICY-23-001. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-POLICY-23-002 | DONE (2025-11-08) | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. Dependencies: AUTH-POLICY-23-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-08: Added Policy Engine activation options (force/default/audit toggles), enforced pending-second-approval responses, and emitted `policy.activation.*` telemetry across auditor logs.
AUTH-POLICY-23-003 | DONE (2025-11-08) | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. Dependencies: AUTH-POLICY-23-001. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-08: Documented dual-control activation steps, new `PolicyEngine.activation.*` knobs, sample YAML defaults, and console/operator guidance for audit visibility.
> 2025-11-07: Scope migration (AUTH-POLICY-23-001) shipped; activation guardrail and documentation updates now waiting on pairing.
AUTH-DPOP-11-001 | DOING (2025-11-07) | Enforce DPoP sender constraints for `/token` flows (nonce policies, JKT persistence, structured telemetry) so downstream services can trust `cnf` metadata. Dependencies: AUTH-AOC-19-002. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
AUTH-MTLS-11-002 | DOING (2025-11-07) | Deliver mTLS-bound token issuance/validation (cert thumbprint storage, JWKS rotation hooks) required for high-assurance tenants and plugin mitigations. Dependencies: AUTH-DPOP-11-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-07: Authority + DevOps stand-up aligned on a 2025-11-10 delivery target for AUTH-DPOP-11-001 / AUTH-MTLS-11-002 and DEVOPS-AIRGAP-57-002 so plugin security/air-gap gating can flip to DOING immediately after.
> 2025-11-08: Taking ownership to wire certificate thumbprint persistence + audit logging; blocking issues from AUTH-DPOP-11-001 now resolved, so mTLS enforcement can proceed.
> 2025-11-08: `/token`/`/introspect` now enforce TLS certificate matches for mTLS-bound tokens and emit `authority_mtls_mismatch_total` telemetry when rejections occur.
AUTH-POLICY-27-002 | DONE (2025-11-02) | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. Dependencies: AUTH-POLICY-27-001, REGISTRY-API-27-007. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md) AUTH-POLICY-27-002 | DONE (2025-11-02) | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. Dependencies: AUTH-POLICY-27-001, REGISTRY-API-27-007. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
> 2025-11-02: Added interactive-only `policy:publish`/`policy:promote` scopes with metadata requirements (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth validation, audit enrichment, and updated config/docs for operators. > 2025-11-02: Added interactive-only `policy:publish`/`policy:promote` scopes with metadata requirements (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth validation, audit enrichment, and updated config/docs for operators.
AUTH-POLICY-27-003 | DONE (2025-11-04) | 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-POLICY-27-003 | DONE (2025-11-04) | 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)
@@ -100,9 +114,11 @@ AUTH-VULN-29-003 | DONE (2025-11-04) | Update security docs/config samples for V
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) 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) 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) 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)
SEC2.PLG | BLOCKED (2025-10-21) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 to stabilise Authority auth surfaces before final verification + publish. | Security Guild, Storage Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) SEC2.PLG | BLOCKED (2025-10-21) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 to stabilise Authority auth surfaces (PLUGIN-DI-08-001 closed 2025-10-21; re-run once sender constraints land). | Security Guild, Storage Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
SEC3.PLG | BLOCKED (2025-10-21) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 so limiter telemetry contract matches final authority surface. | Security Guild, BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) SEC3.PLG | BLOCKED (2025-10-21) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002; PLUGIN-DI-08-001 already merged, so limiter telemetry just awaits final Authority surface. | Security Guild, BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
SEC5.PLG | BLOCKED (2025-10-21) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation depends on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 outcomes. | Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) SEC5.PLG | BLOCKED (2025-10-21) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation now hinges on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 (PLUGIN-DI-08-001 landed 2025-10-21). | Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
> 2025-11-07: Upstream AUTH-DPOP-11-001 / AUTH-MTLS-11-002 now DOING; revisit plugin backlog once sender-constraint hardening lands.
> 2025-11-08: Dependency audit confirmed — AUTH-DPOP-11-001 / AUTH-MTLS-11-002 staffed with 2025-11-10 delivery; no missing SEC2/SEC3/SEC5 subtasks, so these remain BLOCKED only until sender constraints merge.
PLG7.IMPL-001 | DONE (2025-11-03) | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) PLG7.IMPL-001 | DONE (2025-11-03) | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
> 2025-11-03: Initial `StellaOps.Authority.Plugin.Ldap` project/tests scaffolded with configuration options + registrar; sample manifest (`etc/authority.plugins/ldap.yaml`) updated to new schema (client certificate, trust store, insecure toggle). > 2025-11-03: Initial `StellaOps.Authority.Plugin.Ldap` project/tests scaffolded with configuration options + registrar; sample manifest (`etc/authority.plugins/ldap.yaml`) updated to new schema (client certificate, trust store, insecure toggle).
PLG7.IMPL-002 | DONE (2025-11-04) | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | BE-Auth Plugin, Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md) PLG7.IMPL-002 | DONE (2025-11-04) | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | BE-Auth Plugin, Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
@@ -131,7 +147,14 @@ KMS-73-001 | TODO | Add cloud KMS driver (e.g., AWS KMS, GCP KMS) with signing a
KMS-73-002 | TODO | Implement PKCS#11/HSM driver plus FIDO2 signing support for high assurance workflows. Dependencies: KMS-73-001. | KMS Guild (src/__Libraries/StellaOps.Cryptography.Kms/TASKS.md) KMS-73-002 | TODO | Implement PKCS#11/HSM driver plus FIDO2 signing support for high assurance workflows. Dependencies: KMS-73-001. | KMS Guild (src/__Libraries/StellaOps.Cryptography.Kms/TASKS.md)
[Identity & Signing] 100.E) Deployment
Depends on: Helm base chart scaffolding (HELM-45-001)
Summary: Wire deployment assets so Policy Engine activation guardrails stay deterministic across clusters/offline kits.
Task ID | State | Task description | Owners (Source)
--- | --- | --- | ---
HELM-45-004 | DONE (2025-11-08) | Mount the new `policy-engine-activation` ConfigMap into the Policy Engine (and Policy Gateway) pods, ensure runtime config loads activation overrides from env/file, and refresh Helm/Compose samples for offline parity. | Deployment Guild, Policy Guild (ops/deployment/TASKS.md)
> 2025-11-08: Helm template now injects the activation ConfigMap for policy-engine/gateway pods, Policy Engine host loads `/config/policy-engine/activation.yaml`, Policy Engine/Gateway tests are green, and CI now runs `helm lint`/`helm template` over every `values*.yaml`.
If all tasks are done - read next sprint section - SPRINT_110_ingestion_evidence.md If all tasks are done - read next sprint section - SPRINT_110_ingestion_evidence.md

View File

@@ -34,7 +34,9 @@ AIAI-31-004C | DONE (2025-11-06) | Deliver CLI `stella advise run` command, rend
DOCS-AIAI-31-002 | DONE (2025-11-03) | Author `/docs/advisory-ai/architecture.md` detailing RAG pipeline, deterministic tooling, caching, model profiles. Dependencies: AIAI-31-004. | Docs Guild, Advisory AI Guild (docs/TASKS.md) DOCS-AIAI-31-002 | DONE (2025-11-03) | Author `/docs/advisory-ai/architecture.md` detailing RAG pipeline, deterministic tooling, caching, model profiles. Dependencies: AIAI-31-004. | Docs Guild, Advisory AI Guild (docs/TASKS.md)
DOCS-AIAI-31-001 | DONE (2025-11-03) | Publish `/docs/advisory-ai/overview.md` covering capabilities, guardrails, RBAC personas, and offline posture. | Docs Guild, Advisory AI Guild (docs/TASKS.md) DOCS-AIAI-31-001 | DONE (2025-11-03) | Publish `/docs/advisory-ai/overview.md` covering capabilities, guardrails, RBAC personas, and offline posture. | Docs Guild, Advisory AI Guild (docs/TASKS.md)
DOCS-AIAI-31-003 | DONE (2025-11-03) | Write `/docs/advisory-ai/api.md` covering endpoints, schemas, errors, rate limits, and imposed-rule banner. Dependencies: DOCS-AIAI-31-002. | Docs Guild, Advisory AI Guild (docs/TASKS.md) DOCS-AIAI-31-003 | DONE (2025-11-03) | Write `/docs/advisory-ai/api.md` covering endpoints, schemas, errors, rate limits, and imposed-rule banner. Dependencies: DOCS-AIAI-31-002. | Docs Guild, Advisory AI Guild (docs/TASKS.md)
DOCS-AIAI-31-004 | BLOCKED (2025-11-03) | Create `/docs/advisory-ai/console.md` with screenshots, a11y notes, copy-as-ticket instructions. Dependencies: CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001. | Docs Guild, Console Guild (docs/TASKS.md) DOCS-AIAI-31-004 | DOING (2025-11-07) | Create `/docs/advisory-ai/console.md` with screenshots, a11y notes, copy-as-ticket instructions. Dependencies: CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001. | Docs Guild, Console Guild (docs/TASKS.md)
> 2025-11-07: Draft doc committed (`docs/advisory-ai/console.md`) with workflow outline; screenshots will be added once CONSOLE-VULN-29-001 / CONSOLE-VEX-30-001 ship.
> 2025-11-08: Console endpoints are staffed (CONSOLE-VULN-29-001 / CONSOLE-VEX-30-001 DOING); still waiting on EXCITITOR-CONSOLE-23-001 feeds before capturing screenshots/tests.
DOCS-AIAI-31-005 | BLOCKED (2025-11-03) | Publish `/docs/advisory-ai/cli.md` covering commands, exit codes, scripting patterns. Dependencies: CLI-VULN-29-001, CLI-VEX-30-001, AIAI-31-004C. | Docs Guild, DevEx/CLI Guild (docs/TASKS.md) DOCS-AIAI-31-005 | BLOCKED (2025-11-03) | Publish `/docs/advisory-ai/cli.md` covering commands, exit codes, scripting patterns. Dependencies: CLI-VULN-29-001, CLI-VEX-30-001, AIAI-31-004C. | Docs Guild, DevEx/CLI Guild (docs/TASKS.md)
DOCS-AIAI-31-006 | BLOCKED (2025-11-03) | Update `/docs/policy/assistant-parameters.md` covering temperature, token limits, ranking weights, TTLs. Dependencies: POLICY-ENGINE-31-001. | Docs Guild, Policy Guild (docs/TASKS.md) DOCS-AIAI-31-006 | BLOCKED (2025-11-03) | Update `/docs/policy/assistant-parameters.md` covering temperature, token limits, ranking weights, TTLs. Dependencies: POLICY-ENGINE-31-001. | Docs Guild, Policy Guild (docs/TASKS.md)
DOCS-AIAI-31-007 | DONE (2025-11-07) | Write `/docs/security/assistant-guardrails.md` detailing redaction, injection defense, logging. Dependencies: AIAI-31-005. | Docs Guild, Security Guild (docs/TASKS.md) DOCS-AIAI-31-007 | DONE (2025-11-07) | Write `/docs/security/assistant-guardrails.md` detailing redaction, injection defense, logging. Dependencies: AIAI-31-005. | Docs Guild, Security Guild (docs/TASKS.md)
@@ -127,7 +129,7 @@ CONCELIER-OAS-61-001 `Spec coverage` | TODO | Update Concelier OAS with advisory
CONCELIER-OAS-61-002 `Examples library` | TODO | Provide rich examples for advisories, linksets, conflict annotations used by SDK + docs. Dependencies: CONCELIER-OAS-61-001. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OAS-61-002 `Examples library` | TODO | Provide rich examples for advisories, linksets, conflict annotations used by SDK + docs. Dependencies: CONCELIER-OAS-61-001. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
CONCELIER-OAS-62-001 `SDK smoke tests` | TODO | Add SDK tests covering advisory search, pagination, and conflict handling; ensure source metadata surfaced. Dependencies: CONCELIER-OAS-61-002. | Concelier Core Guild, SDK Generator Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OAS-62-001 `SDK smoke tests` | TODO | Add SDK tests covering advisory search, pagination, and conflict handling; ensure source metadata surfaced. Dependencies: CONCELIER-OAS-61-002. | Concelier Core Guild, SDK Generator Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
CONCELIER-OAS-63-001 `Deprecation headers` | TODO | Implement deprecation header support and timeline events for retiring endpoints. Dependencies: CONCELIER-OAS-62-001. | Concelier Core Guild, API Governance Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OAS-63-001 `Deprecation headers` | TODO | Implement deprecation header support and timeline events for retiring endpoints. Dependencies: CONCELIER-OAS-62-001. | Concelier Core Guild, API Governance Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
CONCELIER-OBS-50-001 `Telemetry adoption` | TODO | 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 Core Guild, Observability Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | 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 Core Guild, Observability Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
CONCELIER-OBS-51-001 `Metrics & SLOs` | TODO | 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. Dependencies: CONCELIER-OBS-50-001. | Concelier Core Guild, DevOps Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OBS-51-001 `Metrics & SLOs` | TODO | 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. Dependencies: CONCELIER-OBS-50-001. | Concelier Core Guild, DevOps Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
CONCELIER-OBS-52-001 `Timeline events` | TODO | Emit `timeline_event` records for advisory ingest/normalization/linkset creation with provenance, trace IDs, conflict summaries, and evidence placeholders. Dependencies: CONCELIER-OBS-51-001. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OBS-52-001 `Timeline events` | TODO | Emit `timeline_event` records for advisory ingest/normalization/linkset creation with provenance, trace IDs, conflict summaries, and evidence placeholders. Dependencies: CONCELIER-OBS-51-001. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
CONCELIER-OBS-53-001 `Evidence snapshots` | TODO | Produce advisory evaluation bundle payloads (raw doc, linkset, normalization diff) for evidence locker; ensure Merkle manifests seeded with content hashes. Dependencies: CONCELIER-OBS-52-001. | Concelier Core Guild, Evidence Locker Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md) CONCELIER-OBS-53-001 `Evidence snapshots` | TODO | Produce advisory evaluation bundle payloads (raw doc, linkset, normalization diff) for evidence locker; ensure Merkle manifests seeded with content hashes. Dependencies: CONCELIER-OBS-52-001. | Concelier Core Guild, Evidence Locker Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
@@ -172,14 +174,17 @@ CONCELIER-WEB-AIRGAP-56-001 `Mirror import APIs` | TODO | Extend ingestion endpo
CONCELIER-WEB-AIRGAP-56-002 `Airgap status surfaces` | TODO | Add staleness metadata and bundle provenance to advisory APIs (`/advisories/observations`, `/advisories/linksets`). Dependencies: CONCELIER-WEB-AIRGAP-56-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-AIRGAP-56-002 `Airgap status surfaces` | TODO | Add staleness metadata and bundle provenance to advisory APIs (`/advisories/observations`, `/advisories/linksets`). Dependencies: CONCELIER-WEB-AIRGAP-56-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AIRGAP-57-001 `Error remediation` | TODO | Map sealed-mode violations to `AIRGAP_EGRESS_BLOCKED` responses with user guidance. Dependencies: CONCELIER-WEB-AIRGAP-56-002. | Concelier WebService Guild, AirGap Policy Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-AIRGAP-57-001 `Error remediation` | TODO | Map sealed-mode violations to `AIRGAP_EGRESS_BLOCKED` responses with user guidance. Dependencies: CONCELIER-WEB-AIRGAP-56-002. | Concelier WebService Guild, AirGap Policy Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AIRGAP-58-001 `Import timeline emission` | TODO | Emit timeline events for bundle ingestion operations with bundle ID, scope, and actor metadata. Dependencies: CONCELIER-WEB-AIRGAP-57-001. | Concelier WebService Guild, AirGap Importer Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-AIRGAP-58-001 `Import timeline emission` | TODO | Emit timeline events for bundle ingestion operations with bundle ID, scope, and actor metadata. Dependencies: CONCELIER-WEB-AIRGAP-57-001. | Concelier WebService Guild, AirGap Importer Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AOC-19-002 `AOC observability` | TODO | Emit `ingestion_write_total`, `aoc_violation_total`, latency histograms, and tracing spans (`ingest.fetch/transform/write`, `aoc.guard`). Wire structured logging to include tenant, source vendor, upstream id, and content hash. | Concelier WebService Guild, Observability Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-AOC-19-002 `AOC observability` | DONE (2025-11-07) | Emit `ingestion_write_total`, `aoc_violation_total`, latency histograms, and tracing spans (`ingest.fetch/transform/write`, `aoc.guard`). Wire structured logging to include tenant, source vendor, upstream id, and content hash. | Concelier WebService Guild, Observability Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AOC-19-003 `Schema/guard unit tests` | TODO | Add unit tests covering schema validation failures, forbidden field rejections (`ERR_AOC_001/002/006/007`), idempotent upserts, and supersedes chains using deterministic fixtures. Dependencies: CONCELIER-WEB-AOC-19-002. | QA Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-AOC-19-003 `Schema/guard unit tests` | TODO | Add unit tests covering schema validation failures, forbidden field rejections (`ERR_AOC_001/002/006/007`), idempotent upserts, and supersedes chains using deterministic fixtures. Dependencies: CONCELIER-WEB-AOC-19-002. | QA Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AOC-19-004 `End-to-end ingest verification` | TODO | Create integration tests ingesting large advisory batches (cold/warm) validating linkset enrichment, metrics emission, and reproducible outputs. Capture load-test scripts + doc notes for Offline Kit dry runs. Dependencies: CONCELIER-WEB-AOC-19-003. | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-AOC-19-004 `End-to-end ingest verification` | TODO | Create integration tests ingesting large advisory batches (cold/warm) validating linkset enrichment, metrics emission, and reproducible outputs. Capture load-test scripts + doc notes for Offline Kit dry runs. Dependencies: CONCELIER-WEB-AOC-19-003. | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AOC-19-005 `Chunk evidence regression` | TODO (2025-11-08) | Fix `/advisories/{key}/chunks` fixture seeding so AdvisoryChunksEndpoint tests stop returning 404/not-found when raw documents are pre-populated; ensure the Mongo migration no longer emits Unable to locate advisory_raw documents during WebService test boot. Dependencies: CONCELIER-WEB-AOC-19-002. | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AOC-19-006 `Allowlist ingest auth parity` | TODO (2025-11-08) | Align WebService auth defaults with the test tokens so the allowlisted tenant can create an advisory before forbidden tenants are rejected in `AdvisoryIngestEndpoint_RejectsTenantOutsideAllowlist`. Dependencies: CONCELIER-WEB-AOC-19-002. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-AOC-19-007 `AOC verify violation codes` | TODO (2025-11-08) | Update AOC verify logic/fixtures so guard failures produce the expected `ERR_AOC_001` payload (current regression returns `ERR_AOC_004`) while keeping mapper/guard parity exercised by the new tests. Dependencies: CONCELIER-WEB-AOC-19-002. | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OAS-61-001 `/.well-known/openapi` | DONE (2025-11-02) | Implement discovery endpoint emitting Concelier spec with version metadata and ETag. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OAS-61-001 `/.well-known/openapi` | DONE (2025-11-02) | Implement discovery endpoint emitting Concelier spec with version metadata and ETag. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OAS-61-002 `Error envelope migration` | TODO | Ensure all API responses use standardized error envelope; update controllers/tests. Dependencies: CONCELIER-WEB-OAS-61-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OAS-61-002 `Error envelope migration` | TODO | Ensure all API responses use standardized error envelope; update controllers/tests. Dependencies: CONCELIER-WEB-OAS-61-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OAS-62-001 `Examples expansion` | TODO | Add curated examples for advisory observations/linksets/conflicts; integrate into dev portal. Dependencies: CONCELIER-WEB-OAS-61-002. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OAS-62-001 `Examples expansion` | TODO | Add curated examples for advisory observations/linksets/conflicts; integrate into dev portal. Dependencies: CONCELIER-WEB-OAS-61-002. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OAS-63-001 `Deprecation headers` | TODO | Add Sunset/Deprecation headers for retiring endpoints and update documentation/notifications. Dependencies: CONCELIER-WEB-OAS-62-001. | Concelier WebService Guild, API Governance Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OAS-63-001 `Deprecation headers` | TODO | Add Sunset/Deprecation headers for retiring endpoints and update documentation/notifications. Dependencies: CONCELIER-WEB-OAS-62-001. | Concelier WebService Guild, API Governance Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OBS-50-001 `Telemetry adoption` | TODO | Adopt telemetry core in web service host, ensure ingest + read endpoints emit trace/log fields (`tenant_id`, `route`, `decision_effect`), and add correlation IDs to responses. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | Adopt telemetry core in web service host, ensure ingest + read endpoints emit trace/log fields (`tenant_id`, `route`, `decision_effect`), and add correlation IDs to responses. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OBS-51-001 `Observability APIs` | TODO | Surface ingest health metrics, queue depth, and SLO status via `/obs/concelier/health` endpoint for Console widgets, with caching and tenant partitioning. Dependencies: CONCELIER-WEB-OBS-50-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OBS-51-001 `Observability APIs` | TODO | Surface ingest health metrics, queue depth, and SLO status via `/obs/concelier/health` endpoint for Console widgets, with caching and tenant partitioning. Dependencies: CONCELIER-WEB-OBS-50-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
CONCELIER-WEB-OBS-52-001 `Timeline streaming` | TODO | Provide SSE stream `/obs/concelier/timeline` bridging to Timeline Indexer with paging tokens, guardrails, and audit logging. Dependencies: CONCELIER-WEB-OBS-51-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OBS-52-001 `Timeline streaming` | TODO | Provide SSE stream `/obs/concelier/timeline` bridging to Timeline Indexer with paging tokens, guardrails, and audit logging. Dependencies: CONCELIER-WEB-OBS-51-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
@@ -194,7 +199,7 @@ CONCELIER-WEB-OBS-54-001 `Attestation exposure` | TODO | Provide `/attestations/
CONCELIER-WEB-OBS-55-001 `Incident mode toggles` | TODO | Implement incident mode toggle endpoints, propagate to orchestrator/locker, and document cooldown/backoff semantics. Dependencies: CONCELIER-WEB-OBS-54-001. | Concelier WebService Guild, DevOps Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md) CONCELIER-WEB-OBS-55-001 `Incident mode toggles` | TODO | Implement incident mode toggle endpoints, propagate to orchestrator/locker, and document cooldown/backoff semantics. Dependencies: CONCELIER-WEB-OBS-54-001. | Concelier WebService Guild, DevOps Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
FEEDCONN-CCCS-02-009 Version range provenance (Oct 2025) | BE-Conn-CCCS | **TODO (due 2025-10-21)** Map CCCS advisories into the new `advisory_observations.affected.versions[]` structure, preserving each upstream range with provenance anchors (`cccs:{serial}:{index}`) and normalized comparison keys. Update mapper tests/fixtures for the Link-Not-Merge schema and verify linkset builders consume the ranges without relying on legacy merge counters.<br>2025-10-29: `docs/dev/normalized-rule-recipes.md` now documents helper snippets for building observation version entries—use them instead of merge-specific builders and refresh fixtures with `UPDATE_CCCS_FIXTURES=1`. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/TASKS.md) FEEDCONN-CCCS-02-009 Version range provenance (Oct 2025) | BE-Conn-CCCS | **TODO (due 2025-10-21)** Map CCCS advisories into the new `advisory_observations.affected.versions[]` structure, preserving each upstream range with provenance anchors (`cccs:{serial}:{index}`) and normalized comparison keys. Update mapper tests/fixtures for the Link-Not-Merge schema and verify linkset builders consume the ranges without relying on legacy merge counters.<br>2025-10-29: `docs/dev/normalized-rule-recipes.md` now documents helper snippets for building observation version entries—use them instead of merge-specific builders and refresh fixtures with `UPDATE_CCCS_FIXTURES=1`. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/TASKS.md)
FEEDCONN-CERTBUND-02-010 Version range provenance | BE-Conn-CERTBUND | **TODO (due 2025-10-22)** Translate `product.Versions` phrases (e.g., `2023.1 bis 2024.2`, `alle`) into comparison helpers for `advisory_observations.affected.versions[]`, capturing provenance (`certbund:{advisoryId}:{vendor}`) and localisation notes. Update mapper/tests for the Link-Not-Merge schema and refresh documentation accordingly. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/TASKS.md) FEEDCONN-CERTBUND-02-010 Version range provenance | BE-Conn-CERTBUND | **TODO (due 2025-10-22)** Translate `product.Versions` phrases (e.g., `2023.1 bis 2024.2`, `alle`) into comparison helpers for `advisory_observations.affected.versions[]`, capturing provenance (`certbund:{advisoryId}:{vendor}`) and localisation notes. Update mapper/tests for the Link-Not-Merge schema and refresh documentation accordingly. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/TASKS.md)
FEEDCONN-CISCO-02-009 SemVer range provenance | BE-Conn-Cisco | **TODO (due 2025-10-21)** Emit Cisco SemVer ranges into `advisory_observations.affected.versions[]` with provenance identifiers (`cisco:{productId}`) and deterministic comparison keys. Update mapper/tests for the Link-Not-Merge schema and replace legacy merge counter checks with observation/linkset validation. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/TASKS.md) FEEDCONN-CISCO-02-009 SemVer range provenance | BE-Conn-Cisco | **DOING (2025-11-08)** Emitting Cisco SemVer ranges into `advisory_observations.affected.versions[]` with provenance identifiers (`cisco:{productId}`) and deterministic comparison keys. Updating mapper/tests for the Link-Not-Merge schema and replacing legacy merge counter checks with observation/linkset validation. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/TASKS.md)
FEEDCONN-ICSCISA-02-012 Version range provenance | BE-Conn-ICS-CISA | **DONE (2025-11-03)** Promote existing firmware/semver data into `advisory_observations.affected.versions[]` entries with deterministic comparison keys and provenance identifiers (`ics-cisa:{advisoryId}:{product}`). Add regression coverage for mixed firmware strings and raise a Models ticket only when observation schema needs a new comparison helper.<br>2025-10-29: Follow `docs/dev/normalized-rule-recipes.md` §2 to build observation version entries and log failures without invoking the retired merge helpers.<br>2025-11-03: Completed connector now normalizes semver ranges with provenance notes, RSS fallback content clears the AOC guard, and end-to-end Fetch/Parse/Map integration tests pass. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/TASKS.md) FEEDCONN-ICSCISA-02-012 Version range provenance | BE-Conn-ICS-CISA | **DONE (2025-11-03)** Promote existing firmware/semver data into `advisory_observations.affected.versions[]` entries with deterministic comparison keys and provenance identifiers (`ics-cisa:{advisoryId}:{product}`). Add regression coverage for mixed firmware strings and raise a Models ticket only when observation schema needs a new comparison helper.<br>2025-10-29: Follow `docs/dev/normalized-rule-recipes.md` §2 to build observation version entries and log failures without invoking the retired merge helpers.<br>2025-11-03: Completed connector now normalizes semver ranges with provenance notes, RSS fallback content clears the AOC guard, and end-to-end Fetch/Parse/Map integration tests pass. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/TASKS.md)
FEEDCONN-KISA-02-008 Firmware range provenance | BE-Conn-KISA, Models | **DONE (2025-11-04)** Define comparison helpers for Hangul-labelled firmware ranges (`XFU 1.0.1.0084 ~ 2.0.1.0034`) and map them into `advisory_observations.affected.versions[]` with provenance tags. Coordinate with Models only if a new comparison scheme is required, then update localisation notes and fixtures for the Link-Not-Merge schema.<br>2025-11-03: Analysis in progress auditing existing mapper output/fixtures ahead of implementing firmware range normalization and provenance wiring.<br>2025-11-03: SemVer normalization helper wired through `KisaMapper` with provenance slugs + vendor extensions; integration tests updated and green, follow-up capture for additional Hangul exclusivity markers queued before completion.<br>2025-11-03: Extended connector tests to cover single-ended (`이상`, `초과`, `이하`, `미만`) and non-numeric phrases, verifying normalized rule types (`gt`, `gte`, `lt`, `lte`) and fallback behaviour; broader corpus review remains before transitioning to DONE.<br>2025-11-03: Captured the top 10 `detailDos.do?IDX=` pages into `seed-data/kisa/html/` via `scripts/kisa_capture_html.py`; JSON endpoint (`rssDetailData.do?IDX=…`) now returns error pages, so connector updates must parse the embedded HTML or secure authenticated API access before closing.<br>2025-11-04: Fetch + parse pipeline now consumes the HTML detail pages end to end (metadata persisted, DOM parser extracts vendor/product ranges); fixtures/tests operate on the HTML snapshots to guard normalized SemVer + vendor extension expectations and severity extraction. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/TASKS.md) FEEDCONN-KISA-02-008 Firmware range provenance | BE-Conn-KISA, Models | **DONE (2025-11-04)** Define comparison helpers for Hangul-labelled firmware ranges (`XFU 1.0.1.0084 ~ 2.0.1.0034`) and map them into `advisory_observations.affected.versions[]` with provenance tags. Coordinate with Models only if a new comparison scheme is required, then update localisation notes and fixtures for the Link-Not-Merge schema.<br>2025-11-03: Analysis in progress auditing existing mapper output/fixtures ahead of implementing firmware range normalization and provenance wiring.<br>2025-11-03: SemVer normalization helper wired through `KisaMapper` with provenance slugs + vendor extensions; integration tests updated and green, follow-up capture for additional Hangul exclusivity markers queued before completion.<br>2025-11-03: Extended connector tests to cover single-ended (`이상`, `초과`, `이하`, `미만`) and non-numeric phrases, verifying normalized rule types (`gt`, `gte`, `lt`, `lte`) and fallback behaviour; broader corpus review remains before transitioning to DONE.<br>2025-11-03: Captured the top 10 `detailDos.do?IDX=` pages into `seed-data/kisa/html/` via `scripts/kisa_capture_html.py`; JSON endpoint (`rssDetailData.do?IDX=…`) now returns error pages, so connector updates must parse the embedded HTML or secure authenticated API access before closing.<br>2025-11-04: Fetch + parse pipeline now consumes the HTML detail pages end to end (metadata persisted, DOM parser extracts vendor/product ranges); fixtures/tests operate on the HTML snapshots to guard normalized SemVer + vendor extension expectations and severity extraction. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/TASKS.md)
FEEDCONN-SHARED-STATE-003 Source state seeding helper | Tools Guild, BE-Conn-MSRC | **DONE (2025-11-04)** Delivered `SourceStateSeeder` CLI + processor APIs, Mongo fixtures, and MSRC runbook updates. Seeds raw docs + cursor state deterministically; tests cover happy/path/idempotent flows (`dotnet test src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/...` note: requires `libcrypto.so.1.1` when running Mongo2Go locally). | Tools (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/TASKS.md) FEEDCONN-SHARED-STATE-003 Source state seeding helper | Tools Guild, BE-Conn-MSRC | **DONE (2025-11-04)** Delivered `SourceStateSeeder` CLI + processor APIs, Mongo fixtures, and MSRC runbook updates. Seeds raw docs + cursor state deterministically; tests cover happy/path/idempotent flows (`dotnet test src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/...` note: requires `libcrypto.so.1.1` when running Mongo2Go locally). | Tools (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/TASKS.md)
@@ -212,7 +217,7 @@ Summary: Ingestion & Evidence focus on Concelier (phase VII).
Task ID | State | Task description | Owners (Source) Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
MERGE-LNM-21-002 | DONE (2025-11-07) | Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage.<br>2025-11-03: Began dependency audit and call-site inventory ahead of deprecation plan; cataloging service registrations/tests referencing merge APIs.<br>2025-11-05 14:42Z: Drafted `concelier:features:noMergeEnabled` gating, merge job allowlist handling, and deprecation/telemetry changes prior to analyzer rollout.<br>2025-11-06 16:10Z: Landed analyzer project (`CONCELIER0002`), wired into Concelier WebService/tests, and updated docs to direct suppressions through explicit migration notes.<br>2025-11-07 03:25Z: Default-on toggle + job gating surfaced ingestion test brittleness; guard/migration diagnostics capture requests missing `upstream.contentHash`.<br>2025-11-07 19:45Z: Set `ConcelierOptions.Features.NoMergeEnabled` default to `true`, added regression coverage (`Features_NoMergeEnabled_DefaultsToTrue`), and rechecked ingest helpers to carry canonical links. Remote .NET 10 CLI run remains queued for validation. | BE-Merge (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) MERGE-LNM-21-002 | DONE (2025-11-07) | Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage.<br>2025-11-03: Began dependency audit and call-site inventory ahead of deprecation plan; cataloging service registrations/tests referencing merge APIs.<br>2025-11-05 14:42Z: Drafted `concelier:features:noMergeEnabled` gating, merge job allowlist handling, and deprecation/telemetry changes prior to analyzer rollout.<br>2025-11-06 16:10Z: Landed analyzer project (`CONCELIER0002`), wired into Concelier WebService/tests, and updated docs to direct suppressions through explicit migration notes.<br>2025-11-07 03:25Z: Default-on toggle + job gating surfaced ingestion test brittleness; guard/migration diagnostics capture requests missing `upstream.contentHash`.<br>2025-11-07 19:45Z: Set `ConcelierOptions.Features.NoMergeEnabled` default to `true`, added regression coverage (`Features_NoMergeEnabled_DefaultsToTrue`), and rechecked ingest helpers to carry canonical links. Remote .NET 10 CLI run remains queued for validation. | BE-Merge (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
MERGE-LNM-21-003 Determinism/test updates | DOING (2025-11-07) | 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.<br>2025-11-07: Drafting test migration plan (`docs/dev/lnm-determinism-tests.md`) to map legacy merge fixtures onto observation/linkset pipelines; identifying coverage gaps (conflict surfacing, raw vs canonical parity, hash stability).<br>2025-11-07 20:05Z: Landed `AdvisoryObservationFactoryTests.Create_IsDeterministicAcrossRuns` to cover canonical JSON stability and pruned the old merge determinism integration test. | MERGE-LNM-21-002 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md) MERGE-LNM-21-003 Determinism/test updates | DONE (2025-11-07) | QA Guild, BE-Merge | Replaced the retired merge determinism harness with observation/linkset/export regressions. `AdvisoryObservationFactoryTests` now assert raw reference parity + conflict notes, `AdvisoryEventLogTests` sort/uniquify conflict statement IDs, and `JsonExportSnapshotBuilderTests` guard digest parity across reordered input. `docs/dev/lnm-determinism-tests.md` checklist updated with the new coverage. | MERGE-LNM-21-002 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
WEB-AOC-19-001 (dependency) | DONE (2025-11-07) | Shared guard primitives now enforce the top-level allowlist (`_id`, tenant, source, upstream, content, identifiers, linkset, supersedes, created/ingested timestamps, attributes) and emit the reusable `AocError` payload consumed by HTTP/CLI tooling. Extend `AocGuardOptions.AllowedTopLevelFields` when staging new schema fields to avoid false-positive `ERR_AOC_007` violations. | BE-Base Platform Guild (docs/aoc/guard-library.md, src/Web/StellaOps.Web/TASKS.md) WEB-AOC-19-001 (dependency) | DONE (2025-11-07) | Shared guard primitives now enforce the top-level allowlist (`_id`, tenant, source, upstream, content, identifiers, linkset, supersedes, created/ingested timestamps, attributes) and emit the reusable `AocError` payload consumed by HTTP/CLI tooling. Extend `AocGuardOptions.AllowedTopLevelFields` when staging new schema fields to avoid false-positive `ERR_AOC_007` violations. | BE-Base Platform Guild (docs/aoc/guard-library.md, src/Web/StellaOps.Web/TASKS.md)
@@ -278,7 +283,7 @@ EXCITITOR-OAS-61-001 `Spec coverage` | TODO | Update VEX OAS to include observat
EXCITITOR-OAS-61-002 `Example catalog` | TODO | Provide examples for VEX justifications, statuses, conflicts; ensure SDK docs reference them. Dependencies: EXCITITOR-OAS-61-001. | Excititor Core Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md) EXCITITOR-OAS-61-002 `Example catalog` | TODO | Provide examples for VEX justifications, statuses, conflicts; ensure SDK docs reference them. Dependencies: EXCITITOR-OAS-61-001. | Excititor Core Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md)
EXCITITOR-OAS-62-001 `SDK smoke tests` | TODO | Add SDK scenarios for VEX observation queries and conflict handling to language smoke suites. Dependencies: EXCITITOR-OAS-61-002. | Excititor Core Guild, SDK Generator Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md) EXCITITOR-OAS-62-001 `SDK smoke tests` | TODO | Add SDK scenarios for VEX observation queries and conflict handling to language smoke suites. Dependencies: EXCITITOR-OAS-61-002. | Excititor Core Guild, SDK Generator Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md)
EXCITITOR-OAS-63-001 `Deprecation headers` | TODO | Add deprecation metadata and notifications for legacy VEX routes. Dependencies: EXCITITOR-OAS-62-001. | Excititor Core Guild, API Governance Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md) EXCITITOR-OAS-63-001 `Deprecation headers` | TODO | Add deprecation metadata and notifications for legacy VEX routes. Dependencies: EXCITITOR-OAS-62-001. | Excititor Core Guild, API Governance Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md)
EXCITITOR-OBS-50-001 `Telemetry adoption` | TODO | Integrate telemetry core across VEX ingestion/linking, ensuring spans/logs capture tenant, product scope, upstream id, justification hash, and trace IDs. | Excititor Core Guild, Observability Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md) EXCITITOR-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | Integrate telemetry core across VEX ingestion/linking, ensuring spans/logs capture tenant, product scope, upstream id, justification hash, and trace IDs. | Excititor Core Guild, Observability Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md)
EXCITITOR-OBS-51-001 `Metrics & SLOs` | TODO | Publish metrics for VEX ingest latency, scope resolution success, conflict rate, signature verification failures. Define SLOs (link latency P95 <30s) and configure burn-rate alerts. Dependencies: EXCITITOR-OBS-50-001. | Excititor Core Guild, DevOps Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md) EXCITITOR-OBS-51-001 `Metrics & SLOs` | TODO | Publish metrics for VEX ingest latency, scope resolution success, conflict rate, signature verification failures. Define SLOs (link latency P95 <30s) and configure burn-rate alerts. Dependencies: EXCITITOR-OBS-50-001. | Excititor Core Guild, DevOps Guild (src/Excititor/__Libraries/StellaOps.Excititor.Core/TASKS.md)
@@ -332,16 +337,16 @@ Summary: Ingestion & Evidence focus on Excititor (phase VI).
Task ID | State | Task description | Owners (Source) Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
EXCITITOR-WEB-AIRGAP-58-001 | TODO | Emit timeline events for VEX bundle imports with bundle ID, scope, and actor metadata. Dependencies: EXCITITOR-WEB-AIRGAP-57-001. | Excititor WebService Guild, AirGap Importer Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-AIRGAP-58-001 | TODO | Emit timeline events for VEX bundle imports with bundle ID, scope, and actor metadata. Dependencies: EXCITITOR-WEB-AIRGAP-57-001. | Excititor WebService Guild, AirGap Importer Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-AOC-19-001 `Raw VEX ingestion APIs` | TODO | Implement `POST /ingest/vex`, `GET /vex/raw*`, and `POST /aoc/verify` endpoints. Enforce Authority scopes, tenant injection, and guard pipeline to ensure only immutable VEX facts are persisted. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-AOC-19-001 `Raw VEX ingestion APIs` | DONE (2025-11-08) | Implement `POST /ingest/vex`, `GET /vex/raw*`, and `POST /aoc/verify` endpoints. Enforce Authority scopes, tenant injection, and guard pipeline to ensure only immutable VEX facts are persisted. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-AOC-19-002 `AOC observability + metrics` | TODO | Export metrics (`ingestion_write_total`, `aoc_violation_total`, signature verification counters) and tracing spans matching Conseiller naming. Ensure structured logging includes tenant, source vendor, upstream id, and content hash. Dependencies: EXCITITOR-WEB-AOC-19-001. | Excititor WebService Guild, Observability Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-AOC-19-002 `AOC observability + metrics` | DONE (2025-11-08) | Export metrics (`ingestion_write_total`, `aoc_violation_total`, signature verification counters) and tracing spans matching Conseiller naming. Ensure structured logging includes tenant, source vendor, upstream id, and content hash. Dependencies: EXCITITOR-WEB-AOC-19-001. | Excititor WebService Guild, Observability Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-AOC-19-003 `Guard + schema test harness` | TODO | Add unit/integration tests for schema validation, forbidden field rejection (`ERR_AOC_001/006/007`), and supersedes behavior using CycloneDX-VEX & CSAF fixtures with deterministic expectations. Dependencies: EXCITITOR-WEB-AOC-19-002. | QA Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-AOC-19-003 `Guard + schema test harness` | DONE (2025-11-08) | Add unit/integration tests for schema validation, forbidden field rejection (`ERR_AOC_001/006/007`), and supersedes behavior using CycloneDX-VEX & CSAF fixtures with deterministic expectations. Dependencies: EXCITITOR-WEB-AOC-19-002. | QA Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-AOC-19-004 `Batch ingest validation` | TODO | Build large fixture ingest covering mixed VEX statuses, verifying raw storage parity, metrics, and CLI `aoc verify` compatibility. Document load test/runbook updates. Dependencies: EXCITITOR-WEB-AOC-19-003. | Excititor WebService Guild, QA Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-AOC-19-004 `Batch ingest validation` | DONE (2025-11-08) | Build large fixture ingest covering mixed VEX statuses, verifying raw storage parity, metrics, and CLI `aoc verify` compatibility. Document load test/runbook updates. Dependencies: EXCITITOR-WEB-AOC-19-003. | Excititor WebService Guild, QA Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OAS-61-001 | TODO | Implement `/.well-known/openapi` discovery endpoint with spec version metadata. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OAS-61-001 | TODO | Implement `/.well-known/openapi` discovery endpoint with spec version metadata. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OAS-61-002 | TODO | Standardize error envelope responses and update controller/unit tests. Dependencies: EXCITITOR-WEB-OAS-61-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OAS-61-002 | TODO | Standardize error envelope responses and update controller/unit tests. Dependencies: EXCITITOR-WEB-OAS-61-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OAS-62-001 | TODO | Add curated examples for VEX observation/linkset endpoints and ensure portal displays them. Dependencies: EXCITITOR-WEB-OAS-61-002. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OAS-62-001 | TODO | Add curated examples for VEX observation/linkset endpoints and ensure portal displays them. Dependencies: EXCITITOR-WEB-OAS-61-002. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OAS-63-001 | TODO | Emit deprecation headers and update docs for retiring VEX APIs. Dependencies: EXCITITOR-WEB-OAS-62-001. | Excititor WebService Guild, API Governance Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OAS-63-001 | TODO | Emit deprecation headers and update docs for retiring VEX APIs. Dependencies: EXCITITOR-WEB-OAS-62-001. | Excititor WebService Guild, API Governance Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OBS-50-001 `Telemetry adoption` | TODO | Adopt telemetry core for VEX APIs, ensure responses include trace IDs & correlation headers, and update structured logging for read endpoints. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | Adopt telemetry core for VEX APIs, ensure responses include trace IDs & correlation headers, and update structured logging for read endpoints. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OBS-51-001 `Observability health endpoints` | TODO | Implement `/obs/excititor/health` summarizing ingest/link SLOs, signature failure counts, and conflict trends for Console dashboards. Dependencies: EXCITITOR-WEB-OBS-50-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OBS-51-001 `Observability health endpoints` | DONE (2025-11-08) | Implement `/obs/excititor/health` summarizing ingest/link SLOs, signature failure counts, and conflict trends for Console dashboards. Dependencies: EXCITITOR-WEB-OBS-50-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OBS-52-001 `Timeline streaming` | TODO | Provide SSE bridge for VEX timeline events with tenant filters, pagination, and guardrails. Dependencies: EXCITITOR-WEB-OBS-51-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OBS-52-001 `Timeline streaming` | TODO | Provide SSE bridge for VEX timeline events with tenant filters, pagination, and guardrails. Dependencies: EXCITITOR-WEB-OBS-51-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OBS-53-001 `Evidence APIs` | TODO | Expose `/evidence/vex/*` endpoints that fetch locker bundles, enforce scopes, and surface verification metadata. Dependencies: EXCITITOR-WEB-OBS-52-001. | Excititor WebService Guild, Evidence Locker Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OBS-53-001 `Evidence APIs` | TODO | Expose `/evidence/vex/*` endpoints that fetch locker bundles, enforce scopes, and surface verification metadata. Dependencies: EXCITITOR-WEB-OBS-52-001. | Excititor WebService Guild, Evidence Locker Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)
EXCITITOR-WEB-OBS-54-001 `Attestation APIs` | TODO | Add `/attestations/vex/*` endpoints returning DSSE verification state, builder identity, and chain-of-custody links. Dependencies: EXCITITOR-WEB-OBS-53-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md) EXCITITOR-WEB-OBS-54-001 `Attestation APIs` | TODO | Add `/attestations/vex/*` endpoints returning DSSE verification state, builder identity, and chain-of-custody links. Dependencies: EXCITITOR-WEB-OBS-53-001. | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService/TASKS.md)

View File

@@ -22,8 +22,8 @@ LEDGER-29-001 | DONE (2025-11-03) | Design ledger & projection schemas (tables/i
LEDGER-29-002 | DONE (2025-11-03) | Implement ledger write API (`POST /vuln/ledger/events`) with validation, idempotency, hash chaining, and Merkle root computation job.<br>2025-11-03: Web service + domain scaffolding landed with canonical hashing helpers, in-memory repository, Merkle scheduler stub, request/response contracts, and unit tests covering hashing & conflict flows. Dependencies: LEDGER-29-001. | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-002 | DONE (2025-11-03) | Implement ledger write API (`POST /vuln/ledger/events`) with validation, idempotency, hash chaining, and Merkle root computation job.<br>2025-11-03: Web service + domain scaffolding landed with canonical hashing helpers, in-memory repository, Merkle scheduler stub, request/response contracts, and unit tests covering hashing & conflict flows. Dependencies: LEDGER-29-001. | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-003 | DONE (2025-11-03) | Build projector worker that derives `findings_projection` rows from ledger events + policy determinations; ensure idempotent replay keyed by `(tenant,finding_id,policy_version)`. <br>2025-11-03: Postgres projection services landed with replay checkpoints, fixtures, and unit coverage (LEDGER-29-003). Dependencies: LEDGER-29-002. | Findings Ledger Guild, Scheduler Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-003 | DONE (2025-11-03) | Build projector worker that derives `findings_projection` rows from ledger events + policy determinations; ensure idempotent replay keyed by `(tenant,finding_id,policy_version)`. <br>2025-11-03: Postgres projection services landed with replay checkpoints, fixtures, and unit coverage (LEDGER-29-003). Dependencies: LEDGER-29-002. | Findings Ledger Guild, Scheduler Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-004 | DONE (2025-11-04) | Integrate Policy Engine batch evaluation (baseline + simulate) with projector; cache rationale references.<br>2025-11-04: Ledger service now calls `/api/policy/eval/batch` with resilient HttpClient, shared cache, and inline fallback; documentation/config samples updated; ledger tests executed (`dotnet test src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj --no-restore`). Dependencies: LEDGER-29-003. | Findings Ledger Guild, Policy Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-004 | DONE (2025-11-04) | Integrate Policy Engine batch evaluation (baseline + simulate) with projector; cache rationale references.<br>2025-11-04: Ledger service now calls `/api/policy/eval/batch` with resilient HttpClient, shared cache, and inline fallback; documentation/config samples updated; ledger tests executed (`dotnet test src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj --no-restore`). Dependencies: LEDGER-29-003. | Findings Ledger Guild, Policy Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-005 | TODO | Implement workflow mutation handlers (assign, comment, accept-risk, target-fix, verify-fix, reopen) producing ledger events with validation and attachments metadata. Dependencies: LEDGER-29-004. | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-005 | DONE | Implement workflow mutation handlers (assign, comment, accept-risk, target-fix, verify-fix, reopen) producing ledger events with validation and attachments metadata. Dependencies: LEDGER-29-004. | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-006 | TODO | Integrate attachment encryption (KMS envelope), signed URL issuance, CSRF protection hooks for Console. Dependencies: LEDGER-29-005. | Findings Ledger Guild, Security Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-006 | DONE | Integrate attachment encryption (KMS envelope), signed URL issuance, CSRF protection hooks for Console. Dependencies: LEDGER-29-005. | Findings Ledger Guild, Security Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-007 | TODO | Instrument metrics (`ledger_write_latency`, `projection_lag_seconds`, `ledger_events_total`), structured logs, and Merkle anchoring alerts; publish dashboards. Dependencies: LEDGER-29-006. | Findings Ledger Guild, Observability Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-007 | TODO | Instrument metrics (`ledger_write_latency`, `projection_lag_seconds`, `ledger_events_total`), structured logs, and Merkle anchoring alerts; publish dashboards. Dependencies: LEDGER-29-006. | Findings Ledger Guild, Observability Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-008 | TODO | Develop unit/property/integration tests, replay/restore tooling, determinism harness, and load tests at 5M findings/tenant. Dependencies: LEDGER-29-007. | Findings Ledger Guild, QA Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-008 | TODO | Develop unit/property/integration tests, replay/restore tooling, determinism harness, and load tests at 5M findings/tenant. Dependencies: LEDGER-29-007. | Findings Ledger Guild, QA Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-009 | TODO | Provide deployment manifests (Helm/Compose), backup/restore guidance, Merkle anchor externalization (optional), and offline kit instructions. Dependencies: LEDGER-29-008. | Findings Ledger Guild, DevOps Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md) LEDGER-29-009 | TODO | Provide deployment manifests (Helm/Compose), backup/restore guidance, Merkle anchor externalization (optional), and offline kit instructions. Dependencies: LEDGER-29-008. | Findings Ledger Guild, DevOps Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
@@ -106,8 +106,8 @@ POLICY-ENGINE-20-008 | TODO | Add unit/property/golden/perf suites covering poli
POLICY-ENGINE-20-009 | TODO | Define Mongo schemas/indexes for `policies`, `policy_runs`, and `effective_finding_*`; implement migrations and tenant enforcement. Dependencies: POLICY-ENGINE-20-008. | Policy Guild, Storage Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-20-009 | TODO | Define Mongo schemas/indexes for `policies`, `policy_runs`, and `effective_finding_*`; implement migrations and tenant enforcement. Dependencies: POLICY-ENGINE-20-008. | Policy Guild, Storage Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)
POLICY-ENGINE-27-001 | TODO | Extend compile outputs to include rule coverage metadata, symbol table, inline documentation, and rule index for editor autocomplete; persist deterministic hashes. Dependencies: POLICY-ENGINE-20-009. | Policy Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-27-001 | TODO | Extend compile outputs to include rule coverage metadata, symbol table, inline documentation, and rule index for editor autocomplete; persist deterministic hashes. Dependencies: POLICY-ENGINE-20-009. | Policy Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)
POLICY-ENGINE-27-002 | TODO | Enhance simulate endpoints to emit rule firing counts, heatmap aggregates, sampled explain traces with deterministic ordering, and delta summaries for quick/batch sims. Dependencies: POLICY-ENGINE-27-001. | Policy Guild, Observability Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-27-002 | TODO | Enhance simulate endpoints to emit rule firing counts, heatmap aggregates, sampled explain traces with deterministic ordering, and delta summaries for quick/batch sims. Dependencies: POLICY-ENGINE-27-001. | Policy Guild, Observability Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)
POLICY-ENGINE-27-003 | TODO | Implement complexity/time limit enforcement with compiler scoring, configurable thresholds, and structured diagnostics (`ERR_POL_COMPLEXITY`). Dependencies: POLICY-ENGINE-27-002. | Policy Guild, Security Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-27-003 | DONE | Implement complexity/time limit enforcement with compiler scoring, configurable thresholds, and structured diagnostics (`ERR_POL_COMPLEXITY`). Dependencies: POLICY-ENGINE-27-002. | Policy Guild, Security Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)
POLICY-ENGINE-27-004 | TODO | Update golden/property tests to cover new coverage metrics, symbol tables, explain traces, and complexity limits; provide fixtures for Registry/Console integration. Dependencies: POLICY-ENGINE-27-003. | Policy Guild, QA Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-27-004 | DONE | Update golden/property tests to cover new coverage metrics, symbol tables, explain traces, and complexity limits; provide fixtures for Registry/Console integration. Dependencies: POLICY-ENGINE-27-003. | Policy Guild, QA Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)
POLICY-ENGINE-29-001 | TODO | Implement batch evaluation endpoint (`POST /policy/eval/batch`) returning determinations + rationale chain for sets of `(artifact,purl,version,advisory)` tuples; support pagination and cost budgets. Dependencies: POLICY-ENGINE-27-004. | Policy Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-29-001 | TODO | Implement batch evaluation endpoint (`POST /policy/eval/batch`) returning determinations + rationale chain for sets of `(artifact,purl,version,advisory)` tuples; support pagination and cost budgets. Dependencies: POLICY-ENGINE-27-004. | Policy Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)
POLICY-ENGINE-29-002 | TODO | Provide streaming simulation API comparing two policy versions, returning per-finding deltas without writes; align determinism with Vuln Explorer simulation. Dependencies: POLICY-ENGINE-29-001. | Policy Guild, Findings Ledger Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md) POLICY-ENGINE-29-002 | TODO | Provide streaming simulation API comparing two policy versions, returning per-finding deltas without writes; align determinism with Vuln Explorer simulation. Dependencies: POLICY-ENGINE-29-001. | Policy Guild, Findings Ledger Guild (src/Policy/StellaOps.Policy.Engine/TASKS.md)

View File

@@ -48,7 +48,12 @@ Notes:
- 2025-10-29: JSON parsers for Java/Node.js/Python/Go implemented; artifacts stored on filesystem with SHA-256 and callgraphs upserted into Mongo. - 2025-10-29: JSON parsers for Java/Node.js/Python/Go implemented; artifacts stored on filesystem with SHA-256 and callgraphs upserted into Mongo.
Task ID | State | Task description | Owners (Source) Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
SIGNALS-24-001 | DOING (2025-11-07) | Stand up Signals API skeleton with RBAC, sealed-mode config, DPoP/mTLS enforcement, and `/facts` scaffolding so downstream ingestion work can begin. Dependencies: AUTH-SIG-26-001. | Signals Guild, Authority Guild (src/Signals/StellaOps.Signals/TASKS.md)
SIGNALS-24-002 | DOING (2025-11-07) | Implement callgraph ingestion/normalization (Java/Node/Python/Go) with CAS persistence and retrieval APIs to feed reachability scoring. Dependencies: SIGNALS-24-001. | Signals Guild (src/Signals/StellaOps.Signals/TASKS.md)
SIGNALS-24-003 | BLOCKED (2025-10-27) | Implement runtime facts ingestion endpoint and normalizer (process, sockets, container metadata) populating `context_facts` with AOC provenance.<br>2025-10-27: Depends on `SIGNALS-24-001` for base API host and authentication plumbing. | Signals Guild, Runtime Guild (src/Signals/StellaOps.Signals/TASKS.md) SIGNALS-24-003 | BLOCKED (2025-10-27) | Implement runtime facts ingestion endpoint and normalizer (process, sockets, container metadata) populating `context_facts` with AOC provenance.<br>2025-10-27: Depends on `SIGNALS-24-001` for base API host and authentication plumbing. | Signals Guild, Runtime Guild (src/Signals/StellaOps.Signals/TASKS.md)
> 2025-11-07: Waiting on SIGNALS-24-001 / SIGNALS-24-002 DOING work to land before flipping this to DOING.
> 2025-11-07: Upstream SIGNALS-24-001 / SIGNALS-24-002 now DOING; this flips to DOING once host + callgraph ingestion merge.
> 2025-11-08: Targeting 2025-11-09 merge for SIGNALS-24-001/002; schema + AOC contract drafted so SIGNALS-24-003 can move to DOING immediately after those PRs land (dependencies confirmed, none missing).
SIGNALS-24-004 | BLOCKED (2025-10-27) | Deliver reachability scoring engine producing states/scores and writing to `reachability_facts`; expose configuration for weights. Dependencies: SIGNALS-24-003.<br>2025-10-27: Upstream ingestion pipelines (`SIGNALS-24-002/003`) blocked; scoring engine cannot proceed. | Signals Guild, Data Science (src/Signals/StellaOps.Signals/TASKS.md) SIGNALS-24-004 | BLOCKED (2025-10-27) | Deliver reachability scoring engine producing states/scores and writing to `reachability_facts`; expose configuration for weights. Dependencies: SIGNALS-24-003.<br>2025-10-27: Upstream ingestion pipelines (`SIGNALS-24-002/003`) blocked; scoring engine cannot proceed. | Signals Guild, Data Science (src/Signals/StellaOps.Signals/TASKS.md)
SIGNALS-24-005 | BLOCKED (2025-10-27) | Implement Redis caches (`reachability_cache:*`), invalidation on new facts, and publish `signals.fact.updated` events. Dependencies: SIGNALS-24-004.<br>2025-10-27: Awaiting scoring engine and ingestion layers before wiring cache/events. | Signals Guild, Platform Events Guild (src/Signals/StellaOps.Signals/TASKS.md) SIGNALS-24-005 | BLOCKED (2025-10-27) | Implement Redis caches (`reachability_cache:*`), invalidation on new facts, and publish `signals.fact.updated` events. Dependencies: SIGNALS-24-004.<br>2025-10-27: Awaiting scoring engine and ingestion layers before wiring cache/events. | Signals Guild, Platform Events Guild (src/Signals/StellaOps.Signals/TASKS.md)

View File

@@ -51,7 +51,8 @@ Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
ORCH-SVC-38-101 | TODO | Standardize event envelope (policy/export/job lifecycle) with idempotency keys, ensure export/job failure events published to notifier bus with provenance metadata. Dependencies: ORCH-SVC-37-101. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md) ORCH-SVC-38-101 | TODO | Standardize event envelope (policy/export/job lifecycle) with idempotency keys, ensure export/job failure events published to notifier bus with provenance metadata. Dependencies: ORCH-SVC-37-101. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md)
ORCH-SVC-41-101 | TODO | Register `pack-run` job type, persist run metadata, integrate logs/artifacts collection, and expose API for Task Runner scheduling. Dependencies: ORCH-SVC-38-101. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md) ORCH-SVC-41-101 | TODO | Register `pack-run` job type, persist run metadata, integrate logs/artifacts collection, and expose API for Task Runner scheduling. Dependencies: ORCH-SVC-38-101. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md)
ORCH-SVC-42-101 | TODO | Stream pack run logs via SSE/WS, add manifest endpoints, enforce quotas, and emit pack run events to Notifications Studio. Dependencies: ORCH-SVC-41-101. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md) ORCH-SVC-42-101 | TODO | Stream pack run logs via SSE/WS, add manifest endpoints, enforce quotas, and emit pack run events to Notifications Studio. Dependencies: ORCH-SVC-41-101. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md)
> 2025-11-07: Still NOT STARTED—Authority pack RBAC (AUTH-PACKS-43-001) remains BLOCKED pending these approvals/log-stream APIs. Not missing; needs staffing.
ORCH-TEN-48-001 | TODO | Include `tenant_id`/`project_id` in job specs, set DB session context before processing, enforce context on all queries, and reject jobs missing tenant metadata. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md) ORCH-TEN-48-001 | TODO | Include `tenant_id`/`project_id` in job specs, set DB session context before processing, enforce context on all queries, and reject jobs missing tenant metadata. | Orchestrator Service Guild (src/Orchestrator/StellaOps.Orchestrator/TASKS.md)
WORKER-GO-32-001 | TODO | Bootstrap Go SDK project with configuration binding, auth headers, job claim/acknowledge client, and smoke sample. | Worker SDK Guild (src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md) WORKER-GO-32-001 | TODO | Bootstrap Go SDK project with configuration binding, auth headers, job claim/acknowledge client, and smoke sample. | Worker SDK Guild (src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md)
WORKER-GO-32-002 | TODO | Add heartbeat/progress helpers, structured logging hooks, Prometheus metrics, and jittered retry defaults. Dependencies: WORKER-GO-32-001. | Worker SDK Guild (src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md) WORKER-GO-32-002 | TODO | Add heartbeat/progress helpers, structured logging hooks, Prometheus metrics, and jittered retry defaults. Dependencies: WORKER-GO-32-001. | Worker SDK Guild (src/Orchestrator/StellaOps.Orchestrator.WorkerSdk.Go/TASKS.md)
@@ -90,7 +91,8 @@ SCHED-IMPACT-16-303 | TODO | Snapshot/compaction + invalidation for removed imag
SCHED-SURFACE-01 | TODO | Evaluate Surface.FS pointers when planning delta scans to avoid redundant work and prioritise drift-triggered assets. | Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md) SCHED-SURFACE-01 | TODO | Evaluate Surface.FS pointers when planning delta scans to avoid redundant work and prioritise drift-triggered assets. | Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md)
SCHED-VULN-29-001 | TODO | Expose resolver job APIs (`POST /vuln/resolver/jobs`, `GET /vuln/resolver/jobs/{id}`) to trigger candidate recomputation per artifact/policy change with RBAC and rate limits. | Scheduler WebService Guild, Findings Ledger Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md) SCHED-VULN-29-001 | TODO | Expose resolver job APIs (`POST /vuln/resolver/jobs`, `GET /vuln/resolver/jobs/{id}`) to trigger candidate recomputation per artifact/policy change with RBAC and rate limits. | Scheduler WebService Guild, Findings Ledger Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md)
SCHED-VULN-29-002 | TODO | Provide projector lag metrics endpoint and webhook notifications for backlog breaches consumed by DevOps dashboards. Dependencies: SCHED-VULN-29-001. | Scheduler WebService Guild, Observability Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md) SCHED-VULN-29-002 | TODO | Provide projector lag metrics endpoint and webhook notifications for backlog breaches consumed by DevOps dashboards. Dependencies: SCHED-VULN-29-001. | Scheduler WebService Guild, Observability Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md)
SCHED-WEB-20-002 | BLOCKED (waiting on SCHED-WORKER-20-301) | Provide simulation trigger endpoint returning diff preview metadata and job state for UI/CLI consumption. | Scheduler WebService Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md) SCHED-WEB-20-002 | BLOCKED (waiting on SCHED-WORKER-20-301) | Provide simulation trigger endpoint returning diff preview metadata and job state for UI/CLI consumption. | Scheduler WebService Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md)
> 2025-11-07: Worker counterpart (SCHED-WORKER-20-301) now DOING; revisit once API scaffolding lands.
SCHED-WEB-21-004 | DONE (2025-11-04) | Persist graph job lifecycle to Mongo storage and publish `scheduler.graph.job.completed@1` events + outbound webhook to Cartographer. Dependencies: SCHED-WEB-20-002. | Scheduler WebService Guild, Scheduler Storage Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md) SCHED-WEB-21-004 | DONE (2025-11-04) | Persist graph job lifecycle to Mongo storage and publish `scheduler.graph.job.completed@1` events + outbound webhook to Cartographer. Dependencies: SCHED-WEB-20-002. | Scheduler WebService Guild, Scheduler Storage Guild (src/Scheduler/StellaOps.Scheduler.WebService/TASKS.md)
> 2025-11-04: Graph job completions now persist to Mongo with optimistic guards, emit Redis/webhook notifications once per transition, and refresh result URI metadata idempotently (tests cover service + Mongo store paths). > 2025-11-04: Graph job completions now persist to Mongo with optimistic guards, emit Redis/webhook notifications once per transition, and refresh result URI metadata idempotently (tests cover service + Mongo store paths).
SCHED-WORKER-21-203 | TODO | Export metrics (`graph_build_seconds`, `graph_jobs_inflight`, `overlay_lag_seconds`) and structured logs with tenant/graph identifiers. | Scheduler Worker Guild, Observability Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md) SCHED-WORKER-21-203 | TODO | Export metrics (`graph_build_seconds`, `graph_jobs_inflight`, `overlay_lag_seconds`) and structured logs with tenant/graph identifiers. | Scheduler Worker Guild, Observability Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/TASKS.md)

View File

@@ -227,6 +227,14 @@ WEB-AOC-19-001 `Shared AOC guard primitives` | DONE (2025-11-07) | Provide `AOCF
WEB-AOC-19-002 `Provenance & signature helpers` | TODO | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AOC-19-002 `Provenance & signature helpers` | TODO | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-AOC-19-003 `Analyzer + test fixtures` | TODO | Author Roslyn analyzer preventing ingestion modules from writing forbidden keys without guard, and provide shared test fixtures for guard validation used by Concelier/Excititor service tests. Dependencies: WEB-AOC-19-002. | QA Guild, BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AOC-19-003 `Analyzer + test fixtures` | TODO | Author Roslyn analyzer preventing ingestion modules from writing forbidden keys without guard, and provide shared test fixtures for guard validation used by Concelier/Excititor service tests. Dependencies: WEB-AOC-19-002. | QA Guild, BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-CONSOLE-23-001 `Global posture endpoints` | TODO | Provide consolidated `/console/dashboard` and `/console/filters` APIs returning tenant-scoped aggregates (findings by severity, VEX override counts, advisory deltas, run health, policy change log). Enforce AOC labelling, deterministic ordering, and cursor-based pagination for drill-down hints. | BE-Base Platform Guild, Product Analytics Guild (src/Web/StellaOps.Web/TASKS.md) WEB-CONSOLE-23-001 `Global posture endpoints` | TODO | Provide consolidated `/console/dashboard` and `/console/filters` APIs returning tenant-scoped aggregates (findings by severity, VEX override counts, advisory deltas, run health, policy change log). Enforce AOC labelling, deterministic ordering, and cursor-based pagination for drill-down hints. | BE-Base Platform Guild, Product Analytics Guild (src/Web/StellaOps.Web/TASKS.md)
CONSOLE-VULN-29-001 `Vulnerability workspace` | DOING (2025-11-08) | Build `/console/vuln/*` APIs and filters surfacing tenant-scoped findings with policy/VEX badges so Docs/UI teams can document workflows. Dependencies: WEB-CONSOLE-23-001, CONCELIER-GRAPH-21-001. | Console Guild, BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
> 2025-11-07: API scaffolding kicked off; `docs/advisory-ai/console.md` consuming placeholder responses until this lands. Scheduler/Signals hooks queued once filters stabilized.
> 2025-11-08: Driving filter + reachability badge wiring plus `/console/vuln/search` DTOs to keep DOCS-AIAI-31-004 on real payloads; aligning Signals/Scheduler dependencies now that upstream tickets exist.
> 2025-11-08: Published HTTP contract + sample payloads in `docs/api/console/workspaces.md` and `docs/api/console/samples/vuln-findings-sample.json` so Docs can stage screenshots while backend wires up.
CONSOLE-VEX-30-001 `VEX evidence workspace` | DOING (2025-11-08) | Provide `/console/vex/*` APIs streaming VEX statements, justification summaries, and advisory links with SSE refresh hooks. Dependencies: WEB-CONSOLE-23-001, EXCITITOR-CONSOLE-23-001. | Console Guild, BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
> 2025-11-07: Endpoint contract draft in progress to unblock DOCS-AIAI-31-004 screenshot capture once responses are wired.
> 2025-11-08: Building SSE controller + `/console/vex/events` payloads and syncing Scheduler Signals tasks so DOCS-AIAI-31-004 can embed live data.
> 2025-11-08: SSE schema + NDJSON sample captured in `docs/api/console/workspaces.md` and `docs/api/console/samples/vex-statement-sse.ndjson`; waiting on Scheduler topic hook-up.
WEB-CONSOLE-23-002 `Live status & SSE proxy` | TODO | Expose `/console/status` polling endpoint and `/console/runs/{id}/stream` SSE/WebSocket proxy with heartbeat/backoff, queue lag metrics, and auth scope enforcement. Surface request IDs + retry headers. Dependencies: WEB-CONSOLE-23-001. | BE-Base Platform Guild, Scheduler Guild (src/Web/StellaOps.Web/TASKS.md) WEB-CONSOLE-23-002 `Live status & SSE proxy` | TODO | Expose `/console/status` polling endpoint and `/console/runs/{id}/stream` SSE/WebSocket proxy with heartbeat/backoff, queue lag metrics, and auth scope enforcement. Surface request IDs + retry headers. Dependencies: WEB-CONSOLE-23-001. | BE-Base Platform Guild, Scheduler Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-CONSOLE-23-003 `Evidence export orchestrator` | TODO | Add `/console/exports` POST/GET routes coordinating evidence bundle creation, streaming CSV/JSON exports, checksum manifest retrieval, and signed attestation references. Ensure requests honor tenant + policy scopes and expose job tracking metadata. Dependencies: WEB-CONSOLE-23-002. | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web/TASKS.md) WEB-CONSOLE-23-003 `Evidence export orchestrator` | TODO | Add `/console/exports` POST/GET routes coordinating evidence bundle creation, streaming CSV/JSON exports, checksum manifest retrieval, and signed attestation references. Ensure requests honor tenant + policy scopes and expose job tracking metadata. Dependencies: WEB-CONSOLE-23-002. | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-CONSOLE-23-004 `Global search router` | TODO | Implement `/console/search` endpoint accepting CVE/GHSA/PURL/SBOM identifiers, performing fan-out queries with caching, ranking, and deterministic tie-breaking. Return typed results for Console navigation; respect result caps and latency SLOs. Dependencies: WEB-CONSOLE-23-003. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-CONSOLE-23-004 `Global search router` | TODO | Implement `/console/search` endpoint accepting CVE/GHSA/PURL/SBOM identifiers, performing fan-out queries with caching, ranking, and deterministic tie-breaking. Return typed results for Console navigation; respect result caps and latency SLOs. Dependencies: WEB-CONSOLE-23-003. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)

View File

@@ -48,7 +48,9 @@ DEVOPS-AIRGAP-56-001 | TODO | Ship deny-all egress policies for Kubernetes (Netw
DEVOPS-AIRGAP-56-002 | TODO | Provide import tooling for bundle staging: checksum validation, offline object-store loader scripts, removable media guidance. Dependencies: DEVOPS-AIRGAP-56-001. | DevOps Guild, AirGap Importer Guild (ops/devops/TASKS.md) DEVOPS-AIRGAP-56-002 | TODO | Provide import tooling for bundle staging: checksum validation, offline object-store loader scripts, removable media guidance. Dependencies: DEVOPS-AIRGAP-56-001. | DevOps Guild, AirGap Importer Guild (ops/devops/TASKS.md)
DEVOPS-AIRGAP-56-003 | TODO | Build Bootstrap Pack pipeline bundling images/charts, generating checksums, and publishing manifest for offline transfer. Dependencies: DEVOPS-AIRGAP-56-002. | DevOps Guild, Container Distribution Guild (ops/devops/TASKS.md) DEVOPS-AIRGAP-56-003 | TODO | Build Bootstrap Pack pipeline bundling images/charts, generating checksums, and publishing manifest for offline transfer. Dependencies: DEVOPS-AIRGAP-56-002. | DevOps Guild, Container Distribution Guild (ops/devops/TASKS.md)
DEVOPS-AIRGAP-57-001 | TODO | Automate Mirror Bundle creation jobs with dual-control approvals, artifact signing, and checksum publication. Dependencies: DEVOPS-AIRGAP-56-003. | DevOps Guild, Mirror Creator Guild (ops/devops/TASKS.md) DEVOPS-AIRGAP-57-001 | TODO | Automate Mirror Bundle creation jobs with dual-control approvals, artifact signing, and checksum publication. Dependencies: DEVOPS-AIRGAP-56-003. | DevOps Guild, Mirror Creator Guild (ops/devops/TASKS.md)
DEVOPS-AIRGAP-57-002 | TODO | Configure sealed-mode CI tests that run services with sealed flag and ensure no egress occurs (iptables + mock DNS). Dependencies: DEVOPS-AIRGAP-57-001. | DevOps Guild, Authority Guild (ops/devops/TASKS.md) DEVOPS-AIRGAP-57-002 | DOING (2025-11-08) | Configure sealed-mode CI tests that run services with sealed flag and ensure no egress occurs (iptables + mock DNS). Dependencies: DEVOPS-AIRGAP-57-001. | DevOps Guild, Authority Guild (ops/devops/TASKS.md)
> 2025-11-07: Harness scaffolded at `ops/devops/sealed-mode-ci/*` (README + runner script); integrate into CI to unblock AUTH-AIRGAP-57-001.
> 2025-11-08: `sealed-mode-compose.yml`, `run-sealed-ci.sh`, and `egress_probe.py` committed plus a `sealed-mode-ci` workflow stage that uploads `artifacts/sealed-mode-ci/<commit>/authority-sealed-ci.json`; Authority can now read the sealed evidence feed.
DEVOPS-AIRGAP-58-001 | TODO | Provide local SMTP/syslog container templates and health checks for sealed environments; integrate into Bootstrap Pack. Dependencies: DEVOPS-AIRGAP-57-002. | DevOps Guild, Notifications Guild (ops/devops/TASKS.md) DEVOPS-AIRGAP-58-001 | TODO | Provide local SMTP/syslog container templates and health checks for sealed environments; integrate into Bootstrap Pack. Dependencies: DEVOPS-AIRGAP-57-002. | DevOps Guild, Notifications Guild (ops/devops/TASKS.md)
DEVOPS-AIRGAP-58-002 | TODO | Ship sealed-mode observability stack (Prometheus/Grafana/Tempo/Loki) pre-configured with offline dashboards and no remote exporters. Dependencies: DEVOPS-AIRGAP-58-001. | DevOps Guild, Observability Guild (ops/devops/TASKS.md) DEVOPS-AIRGAP-58-002 | TODO | Ship sealed-mode observability stack (Prometheus/Grafana/Tempo/Loki) pre-configured with offline dashboards and no remote exporters. Dependencies: DEVOPS-AIRGAP-58-001. | DevOps Guild, Observability Guild (ops/devops/TASKS.md)
DEVOPS-AOC-19-001 | BLOCKED (2025-10-26) | Integrate the AOC Roslyn analyzer and guard tests into CI, failing builds when ingestion projects attempt banned writes. | DevOps Guild, Platform Guild (ops/devops/TASKS.md) DEVOPS-AOC-19-001 | BLOCKED (2025-10-26) | Integrate the AOC Roslyn analyzer and guard tests into CI, failing builds when ingestion projects attempt banned writes. | DevOps Guild, Platform Guild (ops/devops/TASKS.md)
@@ -235,4 +237,24 @@ PROV-OBS-54-001 | TODO | Deliver verification library that validates DSSE signat
PROV-OBS-54-002 | TODO | Generate .NET global tool for local verification + embed command helpers for CLI `stella forensic verify`. Provide deterministic packaging and offline kit instructions. Dependencies: PROV-OBS-54-001. | Provenance Guild, DevEx/CLI Guild (src/Provenance/StellaOps.Provenance.Attestation/TASKS.md) PROV-OBS-54-002 | TODO | Generate .NET global tool for local verification + embed command helpers for CLI `stella forensic verify`. Provide deterministic packaging and offline kit instructions. Dependencies: PROV-OBS-54-001. | Provenance Guild, DevEx/CLI Guild (src/Provenance/StellaOps.Provenance.Attestation/TASKS.md)
[Ops & Offline] 190.K) Sovereign Crypto Enablement
Depends on: Sprint 100.A - Attestor, Sprint 110.A - AdvisoryAI, Sprint 120.A - AirGap, Sprint 130.A - Scanner, Sprint 140.A - Graph, Sprint 150.A - Orchestrator, Sprint 160.A - EvidenceLocker, Sprint 170.A - Notifier, Sprint 180.A - Cli
Summary: Deliver RootPack_RU-ready sovereign crypto providers (CryptoPro + PKCS#11), configuration knobs, deterministic tests, and repo-wide crypto routing audit.
Task ID | State | Task description | Owners (Source)
--- | --- | --- | ---
SEC-CRYPTO-90-001 | DONE (2025-11-07) | Produce RootPack_RU sovereign crypto implementation plan, identify provider strategy (CryptoPro + PKCS#11), and slot work into Sprint 190 with task breakdown. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-002 | DONE (2025-11-07) | Extend signature/catalog constants and configuration schema to recognize `GOST12-256/512`, regional crypto profiles, and provider preference ordering. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-003 | DONE (2025-11-07) | Implement `StellaOps.Cryptography.Plugin.CryptoPro` provider (sign/verify/JWK export) using CryptoPro CSP/GostCryptography with deterministic logging + tests. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-004 | DONE (2025-11-07) | Implement `StellaOps.Cryptography.Plugin.Pkcs11Gost` provider (Rutoken/JaCarta) via Pkcs11Interop, configurable slot/pin/module management, and disposal safeguards. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-005 | DONE (2025-11-08) | Add configuration-driven provider selection (`crypto.regionalProfiles`), CLI/diagnostic verb to list providers/keys, and deterministic telemetry for usage. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-006 | DONE (2025-11-08) | Build deterministic test harness (Streebog + signature vectors), manual runbooks for hardware validation, and capture RootPack audit metadata. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-007 | DONE (2025-11-08) | Package RootPack_RU artifacts (plugin binaries, config templates, trust anchors) and document deployment/install steps + compliance evidence. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
SEC-CRYPTO-90-008 | DONE (2025-11-08) | Audit repository for any cryptography usage bypassing `StellaOps.Cryptography` and file remediation tasks to route through providers. | Security Guild (src/__Libraries/StellaOps.Cryptography/TASKS.md)
AUTH-CRYPTO-90-001 | DOING (2025-11-08) | Migrate Authority signing/key-loading paths (provider registry + crypto hash) so regional bundles can select sovereign providers per docs/security/crypto-routing-audit-2025-11-07.md. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
SCANNER-CRYPTO-90-001 | DONE (2025-11-08) | Route remaining Scanner Worker hashing/digest consumers (Surface pointers, manifest publishers, CAS helpers, Sbomer plugins) through ICryptoHash/provider registry.<br>2025-11-08: EntryTrace execution, Surface manifest writer, Local CAS client, and Sbomer descriptor generator now accept ICryptoHash; tests updated with CryptoHashFactory/TestCryptoHash helpers. | Scanner Worker Guild & Security Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md)
CONCELIER-CRYPTO-90-001 | DOING (2025-11-08) | Route OpenAPI discovery hashing plus Concelier mirror/RU connectors through `ICryptoHash`/provider registry so sovereign bundles can swap CryptoPro/PKCS#11 keys without code changes. | Concelier WebService Guild & Security Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
If all tasks are done - read next sprint section - SPRINT_200_documentation_process.md If all tasks are done - read next sprint section - SPRINT_200_documentation_process.md

View File

@@ -5,15 +5,15 @@ Depends on: Sprint 100.A - Attestor, Sprint 110.A - AdvisoryAI, Sprint 120.A - A
Summary: Documentation & Process focus on Docs Tasks (phase Md.I). Summary: Documentation & Process focus on Docs Tasks (phase Md.I).
Task ID | State | Task description | Owners (Source) Task ID | State | Task description | Owners (Source)
--- | --- | --- | --- --- | --- | --- | ---
DOCS-AIAI-31-001 | TODO | Publish `/docs/advisory-ai/overview.md` covering capabilities, guardrails, RBAC. | Docs Guild, Advisory AI Guild (docs/TASKS.md) DOCS-AIAI-31-001 | DONE (2025-11-03) | Publish `/docs/advisory-ai/overview.md` covering capabilities, guardrails, RBAC. | Docs Guild, Advisory AI Guild (docs/TASKS.md)
DOCS-AIAI-31-002 | TODO | Author `/docs/advisory-ai/architecture.md` detailing RAG pipeline, deterministics, caching, model options. Dependencies: DOCS-AIAI-31-001. | Docs Guild, Advisory AI Guild (docs/TASKS.md) DOCS-AIAI-31-002 | DONE (2025-11-03) | Author `/docs/advisory-ai/architecture.md` detailing RAG pipeline, deterministics, caching, model options. Dependencies: DOCS-AIAI-31-001. | Docs Guild, Advisory AI Guild (docs/TASKS.md)
DOCS-AIAI-31-003 | TODO | Write `/docs/advisory-ai/api.md` describing endpoints, schemas, errors, rate limits. Dependencies: DOCS-AIAI-31-002. | Docs Guild, Advisory AI Guild (docs/TASKS.md) DOCS-AIAI-31-003 | DONE (2025-11-03) | Write `/docs/advisory-ai/api.md` describing endpoints, schemas, errors, rate limits. Dependencies: DOCS-AIAI-31-002. | Docs Guild, Advisory AI Guild (docs/TASKS.md)
DOCS-AIAI-31-004 | TODO | Create `/docs/advisory-ai/console.md` with screenshots, a11y notes, copy-as-ticket instructions. Dependencies: DOCS-AIAI-31-003. | Docs Guild, Console Guild (docs/TASKS.md) DOCS-AIAI-31-004 | DOING (2025-11-07) | Create `/docs/advisory-ai/console.md` with screenshots, a11y notes, copy-as-ticket instructions. Dependencies: DOCS-AIAI-31-003, CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, EXCITITOR-CONSOLE-23-001. | Docs Guild, Console Guild (docs/TASKS.md)
DOCS-AIAI-31-005 | TODO | Publish `/docs/advisory-ai/cli.md` covering commands, exit codes, scripting patterns. Dependencies: DOCS-AIAI-31-004. | Docs Guild, DevEx/CLI Guild (docs/TASKS.md) DOCS-AIAI-31-005 | BLOCKED (2025-11-03) | Publish `/docs/advisory-ai/cli.md` covering commands, exit codes, scripting patterns. Dependencies: DOCS-AIAI-31-004, CLI-VULN-29-001, CLI-VEX-30-001. | Docs Guild, DevEx/CLI Guild (docs/TASKS.md)
DOCS-AIAI-31-006 | TODO | Update `/docs/policy/assistant-parameters.md` covering temperature, token limits, ranking weights, TTLs. Dependencies: DOCS-AIAI-31-005. | Docs Guild, Policy Guild (docs/TASKS.md) DOCS-AIAI-31-006 | BLOCKED (2025-11-03) | Update `/docs/policy/assistant-parameters.md` covering temperature, token limits, ranking weights, TTLs. Dependencies: DOCS-AIAI-31-005, POLICY-ENGINE-31-001. | Docs Guild, Policy Guild (docs/TASKS.md)
DOCS-AIAI-31-007 | TODO | Write `/docs/security/assistant-guardrails.md` detailing redaction, injection defense, logging. Dependencies: DOCS-AIAI-31-006. | Docs Guild, Security Guild (docs/TASKS.md) DOCS-AIAI-31-007 | DONE (2025-11-07) | Write `/docs/security/assistant-guardrails.md` detailing redaction, injection defense, logging. Dependencies: DOCS-AIAI-31-006. | Docs Guild, Security Guild (docs/TASKS.md)
DOCS-AIAI-31-008 | TODO | Publish `/docs/sbom/remediation-heuristics.md` (feasibility scoring, blast radius). Dependencies: DOCS-AIAI-31-007. | Docs Guild, SBOM Service Guild (docs/TASKS.md) DOCS-AIAI-31-008 | BLOCKED (2025-11-03) | Publish `/docs/sbom/remediation-heuristics.md` (feasibility scoring, blast radius). Dependencies: DOCS-AIAI-31-007, SBOM-AIAI-31-001. | Docs Guild, SBOM Service Guild (docs/TASKS.md)
DOCS-AIAI-31-009 | TODO | Create `/docs/runbooks/assistant-ops.md` for warmup, cache priming, model outages, scaling. Dependencies: DOCS-AIAI-31-008. | Docs Guild, DevOps Guild (docs/TASKS.md) DOCS-AIAI-31-009 | BLOCKED (2025-11-03) | Create `/docs/runbooks/assistant-ops.md` for warmup, cache priming, model outages, scaling. Dependencies: DOCS-AIAI-31-008, DEVOPS-AIAI-31-001. | Docs Guild, DevOps Guild (docs/TASKS.md)
DOCS-AIRGAP-56-001 | TODO | Publish `/docs/airgap/overview.md` outlining modes, lifecycle, responsibilities, and imposed rule banner. | Docs Guild, AirGap Controller Guild (docs/TASKS.md) DOCS-AIRGAP-56-001 | TODO | Publish `/docs/airgap/overview.md` outlining modes, lifecycle, responsibilities, and imposed rule banner. | Docs Guild, AirGap Controller Guild (docs/TASKS.md)
DOCS-AIRGAP-56-002 | TODO | Author `/docs/airgap/sealing-and-egress.md` covering network policies, EgressPolicy facade usage, and verification steps. Dependencies: DOCS-AIRGAP-56-001. | Docs Guild, DevOps Guild (docs/TASKS.md) DOCS-AIRGAP-56-002 | TODO | Author `/docs/airgap/sealing-and-egress.md` covering network policies, EgressPolicy facade usage, and verification steps. Dependencies: DOCS-AIRGAP-56-001. | Docs Guild, DevOps Guild (docs/TASKS.md)
DOCS-AIRGAP-56-003 | TODO | Create `/docs/airgap/mirror-bundles.md` describing bundle format, DSSE/TUF/Merkle validation, creation/import workflows. Dependencies: DOCS-AIRGAP-56-002. | Docs Guild, Exporter Guild (docs/TASKS.md) DOCS-AIRGAP-56-003 | TODO | Create `/docs/airgap/mirror-bundles.md` describing bundle format, DSSE/TUF/Merkle validation, creation/import workflows. Dependencies: DOCS-AIRGAP-56-002. | Docs Guild, Exporter Guild (docs/TASKS.md)

View File

@@ -0,0 +1,17 @@
# Sprint 201 - Reachability Explainability & Replay Evidence
[Reachability Delivery] 201.A) Runtime facts + static callgraph union
Depends on: Sprint 140 Runtime Signals, Sprint 185 Replay Core, Sprint 186 Scanner Record Mode, Sprint 187 Evidence & CLI Replay
Summary: Close the explainability gaps by wiring Zastava runtime sampling, Scanner language lifters, Signals scoring, Replay manifests, docs, and test harnesses around the reachbench fixture packs.
Task ID | State | Task description | Owners (Source)
--- | --- | --- | ---
ZASTAVA-REACH-201-001 | TODO | Implement runtime symbol sampling in `StellaOps.Zastava.Observer` (EntryTrace-aware shell AST + build-id capture) and stream ND-JSON batches to Signals `/runtime-facts`, including CAS pointers for traces. Update runbook + config references. | Zastava Observer Guild (`src/Zastava/StellaOps.Zastava.Observer/TASKS.md`)
SCAN-REACH-201-002 | DOING (2025-11-08) | Ship language-aware static lifters (JVM, .NET/Roslyn+IL, Go SSA, Node/Deno TS AST, Rust MIR, Swift SIL, shell/binary analyzers) in Scanner Worker; emit canonical SymbolIDs, CAS-stored graphs, and attach reachability tags to SBOM components. | Scanner Worker Guild (`src/Scanner/StellaOps.Scanner.Worker/TASKS.md`)
SIGNALS-REACH-201-003 | DOING (2025-11-08) | Extend Signals ingestion to accept the new multi-language graphs + runtime facts, normalize into `reachability_graphs` CAS layout, and expose retrieval APIs for Policy/CLI. | Signals Guild (`src/Signals/StellaOps.Signals/TASKS.md`)
SIGNALS-REACH-201-004 | DOING (2025-11-08) | Build the reachability scoring engine (state/score/confidence), wire Redis caches + `signals.fact.updated` events, and integrate reachability weights defined in `docs/11_DATA_SCHEMAS.md`. | Signals Guild · Policy Guild (`src/Signals/StellaOps.Signals/TASKS.md`, `src/Policy/StellaOps.Policy.Engine/TASKS.md`)
REPLAY-REACH-201-005 | DOING (2025-11-08) | Update `StellaOps.Replay.Core` manifest schema + bundle writer so replay packs capture reachability graphs, runtime traces, analyzer versions, and evidence hashes; document new CAS namespace. | BE-Base Platform Guild (`src/__Libraries/StellaOps.Replay.Core/TASKS.md`)
DOCS-REACH-201-006 | TODO | Author the reachability doc set (`docs/signals/reachability.md`, `callgraph-formats.md`, `runtime-facts.md`, CLI/UI appendices) plus update Zastava + Replay guides with the new evidence and operators workflow. | Docs Guild (`docs/TASKS.md`)
QA-REACH-201-007 | TODO | Integrate `reachbench-2025-expanded` fixture pack under `tests/reachability/`, add evaluator harness tests that validate reachable vs unreachable cases, and wire CI guidance for deterministic runs. | QA Guild (`tests/README.md`)
> 2025-11-07: reachbench starter + expanded packs staged under repo root; consuming guilds must relocate fixtures into `tests/reachability/fixtures/` as part of QA-REACH-201-007 before enabling CI.

View File

@@ -75,9 +75,10 @@ At startup, services **selfadvertise** their semver & channel; the UI surface
### 2.4 Gates & tests ### 2.4 Gates & tests
* **Static**: linters, codegen checks, protobuf API freeze (backwardcompat tests). * **Static**: linters, codegen checks, protobuf API freeze (backwardcompat tests).
* **Unit/integration**: percomponent, plus **endtoend** flows (scan→vex→policy→sign→attest). * **Unit/integration**: per-component, plus **end-to-end** flows (scan→vex→policy→sign→attest).
* **Perf SLOs**: hot paths (SBOM compose, diff, export) measured against budgets. * **Perf SLOs**: hot paths (SBOM compose, diff, export) measured against budgets.
* **Security**: dependency audit vs Concelier export; container hardening tests; minimal caps. * **Security**: dependency audit vs Concelier export; container hardening tests; minimal caps.
* **Deployment assets**: `Build Test Deploy` workflows `profile-validation` job installs Helm and runs `helm lint` + `helm template` against `deploy/helm/stellaops` for every `values*.yaml`, catching ConfigMap/templating drift before merges.
* **Analyzer smoke**: restart-time language plug-ins (currently Python) verified via `dotnet run --project src/Tools/LanguageAnalyzerSmoke` to ensure manifest integrity plus cold vs warm determinism (<30s / <5s budgets); the harness logs deviations from repository goldens for follow-up. * **Analyzer smoke**: restart-time language plug-ins (currently Python) verified via `dotnet run --project src/Tools/LanguageAnalyzerSmoke` to ensure manifest integrity plus cold vs warm determinism (<30s / <5s budgets); the harness logs deviations from repository goldens for follow-up.
* **Canary cohort**: internal staging + selected customers; one week on **edge** before **stable** tag. * **Canary cohort**: internal staging + selected customers; one week on **edge** before **stable** tag.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
# Findings Ledger — Workflow Inference Notes
> **Audience:** Findings Ledger Guild, Vuln Explorer API, Console Guild
> **Scope:** How workflow mutations (assign/comment/accept-risk/etc.) derive canonical ledger inputs when the caller omits low-level fields.
## 1. Chain + sequencing
- **Chain derivation.** When a request does not provide `chainId`, we deterministically derive it from `tenantId :: policyVersion` (`Guid` from SHA-256 first 16 bytes). This keeps per-policy ordering stable and avoids callers leaking raw GUID logic.
- **Sequence number.** We fetch the current chain head and expect `head.sequence + 1`. If the chain has no entries, the expected sequence is `1`. The append path still enforces optimistic concurrency and rejects mismatches.
- **Previous hash.** We reuse the heads `event_hash` (defaulting to the all-zero hash for genesis events) so writers dont need to manage hash pointers.
## 2. Event identifiers + timestamps
- **Event IDs.** If the caller omits `eventId`, we mint a V7 GUID to keep chronological ordering while remaining globally unique.
- **Occurred / recorded timestamps.** `occurredAt` defaults to “now” in UTC (based on `TimeProvider`). `recordedAt` always comes from the services `TimeProvider` to avoid caller-provided drift.
## 3. Status & severity fallbacks
- The reducer already maps event types to canonical statuses. Workflow service only writes `status` when the mutation explicitly changes it (e.g., `accept-risk``accepted_risk`, `target-fix``in_progress`, `verify-fix``verified`, `reopen``affected`). Otherwise we leave status null and let the reducer infer it.
- Severity is never inferred inside workflow handlers—they rely on policy evaluation or reducer logic.
## 4. Attachments metadata
- Attachments now include security context: every entry captures an AES-256-GCM envelope (`algorithm`, `ciphertext`, `nonce`, `tag`, `expiresAt`) derived from `attachments.encryptionKey`.
- Signed URLs are generated with HMAC-SHA256 using `attachments.signedUrlSecret` and inherit `attachments.signedUrlLifetime` (default 15 minutes). URLs plus envelopes share the same expiry window.
- Metadata is normalized (trimmed keys, deterministic ordering) before encryption so ledger hashes remain stable; duplicate IDs are deduplicated.
- Actual binary blobs stay in Evidence Locker/S3; the envelope is what downstream services use to decrypt the blob once downloaded.
## 5. Validation surface
- All handlers enforce tenant/policy/finding/artifact/vuln IDs, actor identity, and supported actor types.
- Mutation-specific requirements (e.g., assignment requires assignee, accept-risk needs justification) are validated before any ledger append occurs.
- Attachments are validated for ID/file name/MIME type/positive size and 64-char SHA-256 digests before encryption, preventing malformed payloads from burning hashes or emitting invalid URLs.
These rules let upstream APIs/clients send high-level workflow intents without micromanaging ledger sequencing or hashing, while preserving deterministic ledger entries. LEDGER-29-005 implements the service described here; LEDGER-29-006 builds on it for secure attachment handling.

View File

@@ -150,6 +150,7 @@ sequenceDiagram
2. `inputbundle.tar.zst` (feeds, policies, tools, environment snapshot). 2. `inputbundle.tar.zst` (feeds, policies, tools, environment snapshot).
3. `outputbundle.tar.zst` (SBOM, findings, VEX, logs, Merkle proofs). 3. `outputbundle.tar.zst` (SBOM, findings, VEX, logs, Merkle proofs).
Every artifact is signed with multi-profile keys (FIPS, GOST, SM, etc.) managed by Authority. See `docs/replay/DETERMINISTIC_REPLAY.md` §2§5 for the full schema. Every artifact is signed with multi-profile keys (FIPS, GOST, SM, etc.) managed by Authority. See `docs/replay/DETERMINISTIC_REPLAY.md` §2§5 for the full schema.
- **Reachability subtree:** When reachability recording is enabled, Scanner uploads graphs & runtime traces under `cas://replay/<scan-id>/reachability/graphs/` and `cas://replay/<scan-id>/reachability/traces/`. Manifest references (StellaOps.Replay.Core) bind these URIs along with analyzer hashes so Replay + Signals can rehydrate explainability evidence deterministically.
- **Storage tiers:** Primary storage is Mongo (`replay_runs`, `replay_subjects`) plus the CAS bucket. Evidence Locker mirrors bundles for long-term retention and legal hold workflows (`docs/modules/evidence-locker/architecture.md`). Offline kits package bundles under `offline/replay/<scan-id>` with detached DSSE envelopes for air-gapped verification. - **Storage tiers:** Primary storage is Mongo (`replay_runs`, `replay_subjects`) plus the CAS bucket. Evidence Locker mirrors bundles for long-term retention and legal hold workflows (`docs/modules/evidence-locker/architecture.md`). Offline kits package bundles under `offline/replay/<scan-id>` with detached DSSE envelopes for air-gapped verification.
- **APIs & ownership:** Scanner WebService produces the bundles via `record` mode, Scanner Worker emits Merkle metadata, Signer/Authority provide DSSE signatures, Attestor anchors manifests to Rekor, CLI/Evidence Locker handle retrieval, and Docs Guild maintains runbooks. Responsibilities are tracked in `docs/implplan/SPRINT_185_replay_core.md` through `SPRINT_187_evidence_cli_replay.md`. - **APIs & ownership:** Scanner WebService produces the bundles via `record` mode, Scanner Worker emits Merkle metadata, Signer/Authority provide DSSE signatures, Attestor anchors manifests to Rekor, CLI/Evidence Locker handle retrieval, and Docs Guild maintains runbooks. Responsibilities are tracked in `docs/implplan/SPRINT_185_replay_core.md` through `SPRINT_187_evidence_cli_replay.md`.
- **Operational policies:** Retention defaults to 180days for hot CAS storage and 2years for cold Evidence Locker copies. Rotation and pruning follow the checklist in `docs/runbooks/replay_ops.md`. - **Operational policies:** Retention defaults to 180days for hot CAS storage and 2years for cold Evidence Locker copies. Rotation and pruning follow the checklist in `docs/runbooks/replay_ops.md`.

View File

@@ -25,6 +25,51 @@ This guide captures the canonical signals emitted by Concelier and Excititor onc
- **Stale ingestion:** Alert when `max_over_time(ingestion_latency_seconds_sum / ingestion_latency_seconds_count)[30m]` exceeds 30s or if `ingestion_write_total` has no growth for >60min. - **Stale ingestion:** Alert when `max_over_time(ingestion_latency_seconds_sum / ingestion_latency_seconds_count)[30m]` exceeds 30s or if `ingestion_write_total` has no growth for >60min.
- **Signature drop:** Warn when `rate(ingestion_signature_verified_total{result="fail"}[1h]) > 0`. - **Signature drop:** Warn when `rate(ingestion_signature_verified_total{result="fail"}[1h]) > 0`.
### 1.2 · `/obs/excititor/health`
`GET /obs/excititor/health` (scope `vex.admin`) returns a compact snapshot for Grafana tiles and Console widgets:
- `ingest` — overall status, worst lag (seconds), and the top connectors (status, lagSeconds, failure count, last success).
- `link` — freshness of consensus/linkset processing plus document counts and the number currently carrying conflicts.
- `signature` — recent coverage window (evaluated, with signatures, verified, failures, unsigned, coverage ratio).
- `conflicts` — rolling totals grouped by status plus per-bucket trend data for charts.
```json
{
"generatedAt": "2025-11-08T11:00:00Z",
"ingest": { "status": "healthy", "connectors": [ { "connectorId": "excititor:redhat", "lagSeconds": 45.3 } ] },
"link": { "status": "warning", "lastConsensusAt": "2025-11-08T10:57:03Z" },
"signature": { "status": "critical", "documentsEvaluated": 120, "verified": 30, "failures": 2 },
"conflicts": { "status": "warning", "conflictStatements": 325, "trend": [ { "bucketStart": "2025-11-08T10:00:00Z", "conflicts": 130 } ] }
}
```
| Setting | Default | Purpose |
|---------|---------|---------|
| `Excititor:Observability:IngestWarningThreshold` | `06:00:00` | Connector lag before `ingest.status` becomes `warning`. |
| `Excititor:Observability:IngestCriticalThreshold` | `24:00:00` | Connector lag before `ingest.status` becomes `critical`. |
| `Excititor:Observability:LinkWarningThreshold` | `00:15:00` | Maximum acceptable delay between consensus recalculations. |
| `Excititor:Observability:LinkCriticalThreshold` | `01:00:00` | Delay that marks link status as `critical`. |
| `Excititor:Observability:SignatureWindow` | `12:00:00` | Lookback window for signature coverage. |
| `Excititor:Observability:SignatureHealthyCoverage` | `0.8` | Coverage ratio that still counts as healthy. |
| `Excititor:Observability:SignatureWarningCoverage` | `0.5` | Coverage ratio that flips the status to `warning`. |
| `Excititor:Observability:ConflictTrendWindow` | `24:00:00` | Rolling window used for conflict aggregation. |
| `Excititor:Observability:ConflictTrendBucketMinutes` | `60` | Resolution of conflict `trend` buckets. |
| `Excititor:Observability:ConflictWarningRatio` | `0.15` | Fraction of consensus docs with conflicts that triggers `warning`. |
| `Excititor:Observability:ConflictCriticalRatio` | `0.3` | Ratio that marks `conflicts.status` as `critical`. |
| `Excititor:Observability:MaxConnectorDetails` | `50` | Number of connector entries returned (keeps payloads small). |
### 1.3 · Regression & DI hygiene
1. **Keep storage/integration tests green when telemetry touches persistence.**
- `./tools/mongodb/local-mongo.sh start` downloads MongoDB6.0.16 (if needed), launches `rs0`, and prints `export EXCITITOR_TEST_MONGO_URI=mongodb://.../excititor-tests`. Copy that export into your shell.
- `./tools/mongodb/local-mongo.sh restart` is a shortcut for “stop if running, then start” using the same dataset—use it after tweaking config or when tests need a bounce without wiping fixtures.
- `./tools/mongodb/local-mongo.sh clean` stops the instance (if running) and deletes the managed data/log directories so storage tests begin from a pristine catalog.
- Run `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Storage.Mongo.Tests/StellaOps.Excititor.Storage.Mongo.Tests.csproj -nologo -v minimal` (add `--filter` if you only touched specific suites). These tests exercise the same write paths that feed the dashboards, so regressions show up immediately.
- `./tools/mongodb/local-mongo.sh stop` when finished so CI/dev hosts stay clean; `status|logs|shell` are available for troubleshooting.
2. **Declare optional Minimal API dependencies with `[FromServices] ... = null`.** RequestDelegateFactory treats `[FromServices] IVexSigner? signer = null` (or similar) as optional, so host startup succeeds even when tests have not registered that service. This pattern keeps observability endpoints cancellable while avoiding brittle test overrides.
--- ---
## 2·Traces ## 2·Traces
@@ -45,6 +90,35 @@ This guide captures the canonical signals emitted by Concelier and Excititor onc
- Use `aoc.guard` spans to inspect guard payload snapshots. Sensitive fields are redacted automatically; raw JSON lives in secure logs only. - Use `aoc.guard` spans to inspect guard payload snapshots. Sensitive fields are redacted automatically; raw JSON lives in secure logs only.
- For scheduled verification, filter traces by `initiator="scheduled"` to compare runtimes pre/post change. - For scheduled verification, filter traces by `initiator="scheduled"` to compare runtimes pre/post change.
### 2.3Telemetry configuration (Excititor)
- Configure the web service via `Excititor:Telemetry`:
```jsonc
{
"Excititor": {
"Telemetry": {
"Enabled": true,
"EnableTracing": true,
"EnableMetrics": true,
"ServiceName": "stellaops-excititor-web",
"OtlpEndpoint": "http://otel-collector:4317",
"OtlpHeaders": {
"Authorization": "Bearer ${OTEL_PUSH_TOKEN}"
},
"ResourceAttributes": {
"env": "prod-us",
"service.group": "ingestion"
}
}
}
}
```
- Point the OTLP endpoint at the shared collector profile from §1 so Excititor metrics land in the `ingestion_*` dashboards next to Concelier. Resource attributes drive Grafana filtering (e.g., `env`, `service.group`).
- For offline/air-gap bundles set `Enabled=false` and collect the file exporter artifacts from the Offline Kit; import them into Grafana after transfer to keep time-to-truth dashboards consistent.
- Local development templates: run `tools/mongodb/local-mongo.sh start` to spin up a single-node replica set plus the matching `mongosh` client. The script prints the `export EXCITITOR_TEST_MONGO_URI=...` command that integration tests (e.g., `StellaOps.Excititor.Storage.Mongo.Tests`) will honor. Use `restart` for a quick bounce, `clean` to wipe data between suites, and `stop` when finished.
--- ---
## 3·Logs ## 3·Logs
@@ -61,6 +135,13 @@ Structured logs include the following keys (JSON):
| `violation.code` | Present when guard rejects `ERR_AOC_00x`. | | `violation.code` | Present when guard rejects `ERR_AOC_00x`. |
| `verification.window` | Present on `/aoc/verify` job logs. | | `verification.window` | Present on `/aoc/verify` job logs. |
Excititor APIs mirror these identifiers via response headers:
| Header | Purpose |
| --- | --- |
| `X-Stella-TraceId` | W3C trace/span identifier for deep-linking from Console → Grafana/Loki. |
| `X-Stella-CorrelationId` | Stable correlation identifier (respects inbound header or falls back to the request trace ID). |
Logs are shipped to the central Loki/Elasticsearch cluster. Use the template query: Logs are shipped to the central Loki/Elasticsearch cluster. Use the template query:
```logql ```logql

View File

@@ -1,124 +1,138 @@
# Policy Gateway # Policy Gateway
> **Delivery scope:** `StellaOps.Policy.Gateway` minimal API service fronting Policy Engine pack CRUD + activation endpoints for UI/CLI clients. Sender-constrained with DPoP and tenant headers, suitable for online and Offline Kit deployments. > **Delivery scope:** `StellaOps.Policy.Gateway` minimal API service fronting Policy Engine pack CRUD + activation endpoints for UI/CLI clients. Sender-constrained with DPoP and tenant headers, suitable for online and Offline Kit deployments.
## 1 · Responsibilities ## 1 · Responsibilities
- Proxy policy pack CRUD and activation requests to Policy Engine while enforcing scope policies (`policy:read`, `policy:author`, `policy:review`, `policy:operate`, `policy:activate`). - Proxy policy pack CRUD and activation requests to Policy Engine while enforcing scope policies (`policy:read`, `policy:author`, `policy:review`, `policy:operate`, `policy:activate`).
- Normalise responses (DTO + `ProblemDetails`) so Console, CLI, and automation receive consistent payloads. - Normalise responses (DTO + `ProblemDetails`) so Console, CLI, and automation receive consistent payloads.
- Guard activation actions with structured logging and metrics so approvals are auditable. - Guard activation actions with structured logging and metrics so approvals are auditable.
- Support dual auth modes: - Support dual auth modes:
- Forwarded caller tokens (Console/CLI) with DPoP proofs + `X-Stella-Tenant` header. - Forwarded caller tokens (Console/CLI) with DPoP proofs + `X-Stella-Tenant` header.
- Gateway client credentials (DPoP) for service automation or Offline Kit flows when no caller token is present. - Gateway client credentials (DPoP) for service automation or Offline Kit flows when no caller token is present.
## 2 · Endpoints ## 2 · Endpoints
| Route | Method | Description | Required scope(s) | | Route | Method | Description | Required scope(s) |
|-------|--------|-------------|-------------------| |-------|--------|-------------|-------------------|
| `/api/policy/packs` | `GET` | List policy packs and revisions for the active tenant. | `policy:read` | | `/api/policy/packs` | `GET` | List policy packs and revisions for the active tenant. | `policy:read` |
| `/api/policy/packs` | `POST` | Create a policy pack shell or upsert display metadata. | `policy:author` | | `/api/policy/packs` | `POST` | Create a policy pack shell or upsert display metadata. | `policy:author` |
| `/api/policy/packs/{packId}/revisions` | `POST` | Create or update a policy revision (draft/approved). | `policy:author` | | `/api/policy/packs/{packId}/revisions` | `POST` | Create or update a policy revision (draft/approved). | `policy:author` |
| `/api/policy/packs/{packId}/revisions/{version}:activate` | `POST` | Activate a revision, enforcing single/two-person approvals. | `policy:operate`, `policy:activate` | | `/api/policy/packs/{packId}/revisions/{version}:activate` | `POST` | Activate a revision, enforcing single/two-person approvals. | `policy:operate`, `policy:activate` |
### Response shapes ### Response shapes
- Successful responses return camel-case DTOs matching `PolicyPackDto`, `PolicyRevisionDto`, or `PolicyRevisionActivationDto` as described in the Policy Engine API doc (`/docs/api/policy.md`). - Successful responses return camel-case DTOs matching `PolicyPackDto`, `PolicyRevisionDto`, or `PolicyRevisionActivationDto` as described in the Policy Engine API doc (`/docs/api/policy.md`).
- Errors always return RFC 7807 `ProblemDetails` with deterministic fields (`title`, `detail`, `status`). Missing caller credentials now surface `401` with `"Upstream authorization missing"` detail. - Errors always return RFC 7807 `ProblemDetails` with deterministic fields (`title`, `detail`, `status`). Missing caller credentials now surface `401` with `"Upstream authorization missing"` detail.
## 3 · Authentication & headers ### Dual-control activation
| Header | Source | Notes | - **Config-driven.** Set `PolicyEngine.activation.forceTwoPersonApproval=true` when every activation must collect two distinct `policy:activate` approvals. When false, operators can opt into dual-control per revision (`requiresTwoPersonApproval: true`).
|--------|--------|-------| - **Defaults.** `PolicyEngine.activation.defaultRequiresTwoPersonApproval` feeds the default when callers omit the checkbox/flag.
| `Authorization` | Forwarded caller token *or* gateway client credentials. | Caller tokens must include tenant scope; gateway tokens default to `DPoP` scheme. | - **Statuses.** First approval on a dual-control revision returns `202 pending_second_approval`; duplicate actors get `400 duplicate_approval`; the second distinct approver receives the usual `200 activated`.
| `DPoP` | Caller or gateway. | Required when Authority mandates proof-of-possession (default). Generated per request; gateway keeps ES256/ES384 key material under `etc/policy-gateway-dpop.pem`. | - **Audit trail.** With `PolicyEngine.activation.emitAuditLogs` on, Policy Engine emits structured `policy.activation.*` scopes (pack id, revision, tenant, approver IDs, comments) so the gateway metrics/ELK dashboards can show who approved what.
| `X-Stella-Tenant` | Caller | Tenant isolation header. Forwarded unchanged; gateway automation omits it. |
#### Activation configuration wiring
Gateway client credentials are configured in `policy-gateway.yaml`:
- **Helm ConfigMap.** `deploy/helm/stellaops/values*.yaml` now include a `policy-engine-activation` ConfigMap. The chart automatically injects it via `envFrom` into both the Policy Engine and Policy Gateway pods, so overriding the ConfigMap data updates the services with no manifest edits.
```yaml - **Type safety.** Quote ConfigMap values (e.g., `"true"`, `"false"`) because Kubernetes ConfigMaps carry string data. This mirrors the defaults checked into the repo and keeps `helm template` deterministic.
policyEngine: - **File-based overrides (optional).** The Policy Engine host already probes `/config/policy-engine/activation.yaml`, `../etc/policy-engine.activation.yaml`, and ambient `policy-engine.activation.yaml` files beside the binary. Mounting the ConfigMap as a file at `/config/policy-engine/activation.yaml` works immediately if/when we add a volume.
baseAddress: "https://policy-engine.internal" - **Offline/Compose.** Compose/offline bundles can continue exporting `STELLAOPS_POLICY_ENGINE__ACTIVATION__*` variables directly; the ConfigMap wiring simply mirrors those keys for Kubernetes clusters.
audience: "api://policy-engine"
clientCredentials: ## 3 · Authentication & headers
enabled: true
clientId: "policy-gateway" | Header | Source | Notes |
clientSecret: "<secret>" |--------|--------|-------|
scopes: | `Authorization` | Forwarded caller token *or* gateway client credentials. | Caller tokens must include tenant scope; gateway tokens default to `DPoP` scheme. |
- policy:read | `DPoP` | Caller or gateway. | Required when Authority mandates proof-of-possession (default). Generated per request; gateway keeps ES256/ES384 key material under `etc/policy-gateway-dpop.pem`. |
- policy:author | `X-Stella-Tenant` | Caller | Tenant isolation header. Forwarded unchanged; gateway automation omits it. |
- policy:review
- policy:operate Gateway client credentials are configured in `policy-gateway.yaml`:
- policy:activate
dpop: ```yaml
enabled: true policyEngine:
keyPath: "../etc/policy-gateway-dpop.pem" baseAddress: "https://policy-engine.internal"
algorithm: "ES256" audience: "api://policy-engine"
``` clientCredentials:
enabled: true
> 🔐 **DPoP key** store the private key alongside Offline Kit secrets; rotate it whenever the gateway identity or Authority configuration changes. clientId: "policy-gateway"
clientSecret: "<secret>"
## 4 · Metrics & logging scopes:
- policy:read
All activation calls emit: - policy:author
- policy:review
- `policy_gateway_activation_requests_total{outcome,source}` counter labelled with `outcome` (`activated`, `pending_second_approval`, `already_active`, `bad_request`, `not_found`, `unauthorized`, `forbidden`, `error`) and `source` (`caller`, `service`). - policy:operate
- `policy_gateway_activation_latency_ms{outcome,source}` histogram measuring proxy latency. - policy:activate
dpop:
Structured logs (category `StellaOps.Policy.Gateway.Activation`) include `PackId`, `Version`, `Outcome`, `Source`, and upstream status code for audit trails. enabled: true
keyPath: "../etc/policy-gateway-dpop.pem"
## 5 · Sample `curl` workflows algorithm: "ES256"
```
Assuming you already obtained a DPoP-bound access token (`$TOKEN`) for tenant `acme`:
> 🔐 **DPoP key** store the private key alongside Offline Kit secrets; rotate it whenever the gateway identity or Authority configuration changes.
```bash
# Generate a DPoP proof for GET via the CLI helper ## 4 · Metrics & logging
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs \ All activation calls emit:
--htm GET \
--token "$TOKEN") - `policy_gateway_activation_requests_total{outcome,source}` counter labelled with `outcome` (`activated`, `pending_second_approval`, `already_active`, `bad_request`, `not_found`, `unauthorized`, `forbidden`, `error`) and `source` (`caller`, `service`).
- `policy_gateway_activation_latency_ms{outcome,source}` histogram measuring proxy latency.
curl -sS https://gateway.example.com/api/policy/packs \
-H "Authorization: DPoP $TOKEN" \ Structured logs (category `StellaOps.Policy.Gateway.Activation`) include `PackId`, `Version`, `Outcome`, `Source`, and upstream status code for audit trails.
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" ## 5 · Sample `curl` workflows
# Draft a new revision Assuming you already obtained a DPoP-bound access token (`$TOKEN`) for tenant `acme`:
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs/policy.core/revisions \ ```bash
--htm POST \ # Generate a DPoP proof for GET via the CLI helper
--token "$TOKEN") DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs \
curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions \ --htm GET \
-H "Authorization: DPoP $TOKEN" \ --token "$TOKEN")
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" \ curl -sS https://gateway.example.com/api/policy/packs \
-H "Content-Type: application/json" \ -H "Authorization: DPoP $TOKEN" \
-d '{"version":5,"requiresTwoPersonApproval":true,"initialStatus":"Draft"}' -H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme"
# Activate revision 5 (returns 202 when awaiting the second approver)
DPoP_PROOF=$(stella auth dpop proof \ # Draft a new revision
--htu https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \ DPoP_PROOF=$(stella auth dpop proof \
--htm POST \ --htu https://gateway.example.com/api/policy/packs/policy.core/revisions \
--token "$TOKEN") --htm POST \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
-H "Authorization: DPoP $TOKEN" \ curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions \
-H "DPoP: $DPoP_PROOF" \ -H "Authorization: DPoP $TOKEN" \
-H "X-Stella-Tenant: acme" \ -H "DPoP: $DPoP_PROOF" \
-H "Content-Type: application/json" \ -H "X-Stella-Tenant: acme" \
-d '{"comment":"Rollout baseline"}' -H "Content-Type: application/json" \
``` -d '{"version":5,"requiresTwoPersonApproval":true,"initialStatus":"Draft"}'
For air-gapped environments, bundle `policy-gateway.yaml` and the DPoP key in the Offline Kit (see `/docs/24_OFFLINE_KIT.md` §5.7). # Activate revision 5 (returns 202 when awaiting the second approver)
DPoP_PROOF=$(stella auth dpop proof \
> **DPoP proof helper:** Use `stella auth dpop proof` to mint sender-constrained proofs locally. The command accepts `--htu`, `--htm`, and `--token` arguments and emits a ready-to-use header value. Teams maintaining alternate tooling (for example, `scripts/make-dpop.sh`) can substitute it as long as the inputs and output match the CLI behaviour. --htu https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
--htm POST \
## 6 · Offline Kit guidance --token "$TOKEN")
- Include `policy-gateway.yaml.sample` and the resolved runtime config in the Offline Kits `config/` tree. curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
- Place the DPoP private key under `secrets/policy-gateway-dpop.pem` with restricted permissions; document rotation steps in the manifest. -H "Authorization: DPoP $TOKEN" \
- When building verification scripts, use the gateway endpoints above instead of hitting Policy Engine directly. The Offline Kit validator now expects `policy_gateway_activation_requests_total` metrics in the Prometheus snapshot. -H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" \
## 7 · Change log -H "Content-Type: application/json" \
-d '{"comment":"Rollout baseline"}'
- **2025-10-27 Sprint 18.5**: Initial gateway bootstrap + activation metrics + DPoP client credentials. ```
For air-gapped environments, bundle `policy-gateway.yaml` and the DPoP key in the Offline Kit (see `/docs/24_OFFLINE_KIT.md` §5.7).
> **DPoP proof helper:** Use `stella auth dpop proof` to mint sender-constrained proofs locally. The command accepts `--htu`, `--htm`, and `--token` arguments and emits a ready-to-use header value. Teams maintaining alternate tooling (for example, `scripts/make-dpop.sh`) can substitute it as long as the inputs and output match the CLI behaviour.
## 6 · Offline Kit guidance
- Include `policy-gateway.yaml.sample` and the resolved runtime config in the Offline Kits `config/` tree.
- Place the DPoP private key under `secrets/policy-gateway-dpop.pem` with restricted permissions; document rotation steps in the manifest.
- When building verification scripts, use the gateway endpoints above instead of hitting Policy Engine directly. The Offline Kit validator now expects `policy_gateway_activation_requests_total` metrics in the Prometheus snapshot.
## 7 · Change log
- **2025-10-27 Sprint 18.5**: Initial gateway bootstrap + activation metrics + DPoP client credentials.

View File

@@ -98,23 +98,58 @@ C --> J[Blob Store: Input/Output Bundles]
], ],
"trustProfile": "sha256:..." "trustProfile": "sha256:..."
}, },
"outputs": { "outputs": {
"sbomHash": "sha256:...", "sbomHash": "sha256:...",
"findingsHash": "sha256:...", "findingsHash": "sha256:...",
"vexHash": "sha256:...", "vexHash": "sha256:...",
"logHash": "sha256:..." "logHash": "sha256:..."
}, },
"provenance": { "reachability": {
"signer": "scanner.authority", "graphs": [
"dsseEnvelopeHash": "sha256:...", {
"rekorEntry": "optional" "kind": "static",
} "analyzer": "scanner/java@sha256:...",
} "casUri": "cas://replay/scan-123/reachability/static-graph.tar.zst",
``` "sha256": "abc123"
},
--- {
"kind": "framework",
## 4. Deterministic Execution Rules "analyzer": "scanner/framework@sha256:...",
"casUri": "cas://replay/scan-123/reachability/framework-graph.tar.zst",
"sha256": "def456"
}
],
"runtimeTraces": [
{
"source": "zastava",
"casUri": "cas://replay/scan-123/reachability/runtime-trace.ndjson.zst",
"sha256": "feedface",
"recordedAt": "2025-11-07T11:10:00Z"
}
]
},
"provenance": {
"signer": "scanner.authority",
"dsseEnvelopeHash": "sha256:...",
"rekorEntry": "optional"
}
}
```
### 3.2 Reachability Section
The optional `reachability` block captures the inputs needed to replay explainability decisions:
| Field | Description |
|-------|-------------|
| `reachability.graphs[]` | References to static/framework callgraph bundles. Each entry records the producing analyzer (`analyzer`/`version`), the CAS URI under `cas://replay/<scan-id>/reachability/graphs/`, and the SHA-256 digest of the tarball. |
| `reachability.runtimeTraces[]` | References to runtime observation bundles (e.g., Zastava ND-JSON traces). Each item stores the emitting source, CAS URI (typically `cas://replay/<scan-id>/reachability/traces/`), SHA-256, and capture timestamp. |
Replay engines MUST verify every referenced artifact hash before re-evaluating reachability. Missing graphs downgrade affected signals to `reachability:unknown` and should raise policy warnings.
---
## 4. Deterministic Execution Rules
### 4.1 Environment Normalization ### 4.1 Environment Normalization

View File

@@ -30,10 +30,11 @@ Replay is the foundation for:
| **Subject** | OCI image digest, per-layer Merkle roots | ✅ | | **Subject** | OCI image digest, per-layer Merkle roots | ✅ |
| **Outputs** | SBOM, Findings, VEX, logs (content hashes) | ✅ | | **Outputs** | SBOM, Findings, VEX, logs (content hashes) | ✅ |
| **Toolchain** | Sbomer, Scanner, Vexer binaries + versions + SHA256 | ✅ | | **Toolchain** | Sbomer, Scanner, Vexer binaries + versions + SHA256 | ✅ |
| **Feeds/VEX sources** | Full or pruned snapshot with Merkle proofs | ✅ | | **Feeds/VEX sources** | Full or pruned snapshot with Merkle proofs | ✅ |
| **Policy Bundle** | Lattice rules, mutes, trust profiles, thresholds | ✅ | | **Policy Bundle** | Lattice rules, mutes, trust profiles, thresholds | ✅ |
| **Environment** | OS, arch, locale, TZ, deterministic seed, runtime flags | ✅ | | **Environment** | OS, arch, locale, TZ, deterministic seed, runtime flags | ✅ |
| **Crypto Profile** | Algorithm suites (FIPS, GOST, SM, eIDAS) | ✅ | | **Reachability Evidence** | Callgraphs (`graphs[]`), runtime traces (`runtimeTraces[]`), analyzer/version hashes | ✅ |
| **Crypto Profile** | Algorithm suites (FIPS, GOST, SM, eIDAS) | ✅ |
--- ---
@@ -69,8 +70,9 @@ stella replay manifest.json --what-if --vary=feeds
## Workflow ## Workflow
1. `stella scan image:tag --record out/` 1. `stella scan image:tag --record out/`
- Generates Replay Manifest, InputBundle, OutputBundle, DSSE sigs. - Generates Replay Manifest, InputBundle, OutputBundle, DSSE sigs.
- Captures reachability graphs/traces (if enabled) and references them via `reachability.graphs[]` + `runtimeTraces[]`.
2. `stella verify manifest.json` 2. `stella verify manifest.json`
- Validates hashes, signatures, and completeness. - Validates hashes, signatures, and completeness.
3. `stella replay manifest.json --strict` 3. `stella replay manifest.json --strict`
@@ -82,14 +84,15 @@ stella replay manifest.json --what-if --vary=feeds
--- ---
## Storage ## Storage
- **Mongo collections** - **Mongo collections**
- `replay_runs`: manifest + DSSE envelopes + status - `replay_runs`: manifest + DSSE envelopes + status
- `bundles`: content-addressed (input/output/rootpack) - `bundles`: content-addressed (input/output/rootpack)
- `subjects`: OCI digests, Merkle roots per layer - `subjects`: OCI digests, Merkle roots per layer
- **File store** - `reachability_facts`: graph & runtime trace references tied to scan subjects
- Bundles stored as `<sha256>.tar.zst` - **File store**
- Bundles stored as `<sha256>.tar.zst`
--- ---

View File

@@ -19,6 +19,7 @@ This playbook enumerates the deterministic replay validation suite. It guides th
| T-RETENTION-006 | **Retention Sweep** | Ensure Evidence Locker prunes hot CAS after SLA while preserving cold storage copies. | Evidence Locker, Ops | Replay retention config, audit logs | | T-RETENTION-006 | **Retention Sweep** | Ensure Evidence Locker prunes hot CAS after SLA while preserving cold storage copies. | Evidence Locker, Ops | Replay retention config, audit logs |
| T-OFFLINE-007 | **Offline Kit Replay** | Execute `stella replay` using only Offline Kit artifacts. | CLI, Evidence Locker | Offline kit bundle, local RootPack | | T-OFFLINE-007 | **Offline Kit Replay** | Execute `stella replay` using only Offline Kit artifacts. | CLI, Evidence Locker | Offline kit bundle, local RootPack |
| T-OPA-008 | **Runbook Drill** | Simulate replay-driven incident response per `docs/runbooks/replay_ops.md`. | Ops Guild, Scanner, Authority | Runbook checklist, incident notes | | T-OPA-008 | **Runbook Drill** | Simulate replay-driven incident response per `docs/runbooks/replay_ops.md`. | Ops Guild, Scanner, Authority | Runbook checklist, incident notes |
| T-REACH-009 | **Reachability Replay** | Rehydrate reachability graphs/traces from replay bundles and compare against reachbench fixtures. | Scanner, Signals, Replay | `reachbench-2025-expanded`, reachability CAS references |
--- ---
@@ -50,7 +51,8 @@ This playbook enumerates the deterministic replay validation suite. It guides th
- [ ] Replay verification metrics ingested into Telemetry Stack dashboards. - [ ] Replay verification metrics ingested into Telemetry Stack dashboards.
- [ ] Evidence Locker retention job validated against hot/cold tiers. - [ ] Evidence Locker retention job validated against hot/cold tiers.
- [ ] CLI documentation updated with troubleshooting steps observed during tests. - [ ] CLI documentation updated with troubleshooting steps observed during tests.
- [ ] Runbook drill logged with timestamp and owners in `docs/runbooks/replay_ops.md`. - [ ] Runbook drill logged with timestamp and owners in `docs/runbooks/replay_ops.md`.
- [ ] Reachability replay drill captured (`T-REACH-009`) with fixture references and Signals verification logs.
--- ---

View File

@@ -0,0 +1,113 @@
# Crypto Routing Audit — 07 Nov 2025
**Scope.** Inventory direct uses of `System.Security.Cryptography` (and related primitives) outside the `StellaOps.Cryptography*` stack to identify callers that must be routed through sovereign-aware providers (default, PKCS#11, CryptoPro, future PQC).
**Method.** `rg -l "using System.Security.Cryptography" src | grep -Ev "__Tests|\.Tests/"` (filtered for runtime code). Counts reflect unique files per top-level module.
## Summary (runtime files by module)
| Module/Area | Files bypassing shared crypto |
|-------------|------------------------------|
| Concelier | 34 |
| Scanner | 31 |
| Authority | 20 |
| Excititor | 18 |
| Attestor | 18 |
| EvidenceLocker | 10 |
| Findings / Vuln Explorer | 7 |
| Zastava | 6 |
| ExportCenter| 6 |
| Policy | 4 |
| Scheduler | 3 |
| CLI | 3 |
| Bench | 3 |
| AdvisoryAI | 3 |
| Other (Notify, Registry, Signals, etc.) | 11 combined |
## Configuring `crypto.regionalProfiles`
All hosts can now express provider ordering and profile overrides via configuration:
```yaml
Crypto:
registry:
preferredProviders:
- default
- ru.pkcs11
activeProfile: ru-offline
profiles:
ru-offline:
preferredProviders:
- ru.cryptopro.csp
- ru.pkcs11
pkcs11:
keys:
- keyId: ru-slot-token
libraryPath: /usr/local/lib/librutokenecp.so
slotId: "0x1"
privateKeyLabel: signing-key
certificateThumbprint: "<thumbprint>"
cryptopro:
keys:
- keyId: ru-csp-token
libraryPath: /opt/cprocsp/lib/libcapi20.so
containerLabel: KRYPTO_PRO_KEY
certificateThumbprint: "<thumbprint>"
```
Each deployment picks a profile (`activeProfile`) that resolves to a deterministic provider order, and individual services call into `ICryptoProviderRegistry` rather than new-ing crypto stacks directly.
## Inspecting providers from the CLI
`stellaops crypto providers` now lists the registered providers, signing algorithms, certificate metadata, and the current preferred order. Use `--json` for machine-readable output or `--profile <name>` to preview another profile (e.g., `ru-offline`) before flipping configuration.
## High-priority hotspots
### Concelier (ingestion + mirror connectors)
- `src/Concelier/StellaOps.Concelier.WebService/Services/OpenApiDiscoveryDocumentProvider.cs` builds SHA256 hashes for discovery docs inline.
- `src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/Security/MirrorSignatureVerifier.cs` performs RSA verification directly.
- `src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/RuNkckiConnector.cs` and `.Ru.Bdu` local hash/signature handling for regional advisories.
**Action:** Introduce `ICryptoProviderRegistry` consumption inside connector/lib assemblies (probably through lightweight adapter service). File follow-up tasks in `src/Concelier/StellaOps.Concelier.WebService/TASKS.md` and connector TASK boards to migrate hashing/signing to the new PKCS#11/CryptoPro providers (priority for RU feeds to unblock RootPack_RU).
### Scanner (web service, worker, Sbomer plug-ins)
- `src/Scanner/StellaOps.Scanner.WebService/Utilities/ScanIdGenerator.cs` direct SHA256 for id derivation.
- `src/Scanner/StellaOps.Scanner.WebService/Services/ReportSigner.cs` uses `ECDsa.Create()` directly for DSSE hand-off.
- `src/Scanner/StellaOps.Scanner.Worker/Processing/Surface/SurfaceManifestPublisher.cs` manual digesting before CAS writes.
**Action:** Create shared `IScanCryptoService` backed by `ICryptoProviderRegistry` so both web service and worker reuse sovereign providers. Add tasks under `src/Scanner/StellaOps.Scanner.WebService/TASKS.md` and `src/Scanner/StellaOps.Scanner.Worker/TASKS.md`.
### Authority (plugins + signing host)
- `StellaOps.Authority/Signing/*` classes still load PEM/PKCS#12 directly via `X509Certificate2` and `RSA`.
- `AuthoritySecretHasher` and `AuthorityClientCertificateValidator` maintain custom hashing.
**Action:** Wire Authority signing/loading paths to `ICryptoProviderRegistry` so active keys can point to `ru.cryptopro.csp` or `ru.pkcs11`. Open tasks in `src/Authority/StellaOps.Authority/TASKS.md` covering: signing key loading, JWKS generation, secret hashing migration.
### Excititor / Attestor
- Excititor connectors (e.g., `src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Ghsa/GhsaConnector.cs`) re-hash payloads in place.
- Attestor submission cache uses SHA256 for bundle ids.
**Action:** Introduce shared hashing helper that internally calls `ICryptoProviderRegistry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.GostR3410_2012_256)` for digest+sign combos; log follow-ups in respective TASK boards.
### Evidence Locker / Export Center
- Export packaging code manually builds SHA/V1 digests before signing manifests.
**Action:** Add backlog tasks for both modules to replace `SHA256.Create()` usage with provider-backed hashing (especially for offline bundle sealing).
## Next steps
1. **Open remediation tasks per module** referencing this audit (minimum: Concelier, Scanner, Authority, Excititor, Attestor, Evidence Locker, Export Center). Each task should specify which files to migrate and target provider (default vs sovereign).
2. **Provide shared helpers** (e.g., `ICryptoDigestService`, `ICasSigner`) in `StellaOps.Cryptography` to ease adoption and avoid each module talking to the registry manually.
3. **Follow-up audit** once migrations land; rerun the command and ensure only `StellaOps.Cryptography*` and vetted crypto libraries contain direct `System.Security.Cryptography` usage.
4. **RootPack validation runbook** — see `docs/security/rootpack_ru_validation.md` for deterministic tests, hardware validation, and required audit artifacts before shipping RootPack_RU.
### Remediation tracking snapshot (2025-11-08)
- **Authority:** `AUTH-CRYPTO-90-001` (Authority TASKS board)
- **Scanner:** `SCANNER-CRYPTO-90-001` (WebService TASKS board)
- **Concelier:** `CONCELIER-CRYPTO-90-001` (WebService TASKS board)
- **Excititor:** `EXCITITOR-CRYPTO-90-001` (WebService TASKS board)
- **Attestor:** `ATTESTOR-CRYPTO-90-001` (Attestor TASKS board)
- **Evidence Locker:** `EVID-CRYPTO-90-001` (Evidence Locker TASKS board)
- **Export Center:** `EXPORT-CRYPTO-90-001` (Exporter Service TASKS board)
> Stored query artifacts: `/tmp/crypto_runtime_non_tests.txt` (157 runtime files) and aggregated counts above prepared on 2025-11-07.

View File

@@ -0,0 +1,45 @@
# Authority DPoP + mTLS Rollout Plan (Sprint 100)
_Last updated: 2025-11-07_
## Objectives
1. Enforce DPoP sender constraints (`AUTH-DPOP-11-001`).
2. Bind high-assurance tenants to mTLS tokens (`AUTH-MTLS-11-002`).
3. Provide telemetry + runbooks so plugins (SEC2/SEC3/SEC5) can validate enforcement without regressions.
## Phase 1 · Config & Telemetry (ETA 2025-11-08)
- [ ] Extend `authority.yaml` with `security.senderConstraints.dpop` section (nonce store, allowed algorithms, replay window).
- [ ] Wire structured logs (`authority.dpop.request`) containing tenant, client, cnf thumbprint, nonce status.
- [ ] Add `DPoPNonceStore` abstraction + Redis implementation for multi-node deployments.
- [ ] Update integration tests: `AuthorityTokenTests.DPoPNonceRequired`, `AuthorityTokenTests.DPoPMustMatchCnF`.
## Phase 2 · Enforcement & Fallback (ETA 2025-11-10)
- [ ] Reject `/token` requests lacking DPoP proof when tenant policy requires it.
- [ ] Persist `cnf.jkt` and expose through `/introspect` so downstream services validate sender.
- [ ] Add emergency bypass flag (`security.senderConstraints.dpop.allowTemporaryBypass`) for sealed recap drills; default disabled.
## Phase 3 · mTLS Binding (ETA 2025-11-10)
- [x] Capture client cert thumbprint on `/token` (mutual TLS) and store in `authority_tokens.senderCertificate`.
- [x] Validate cert hash on `/introspect` and `/fresh-auth`.
- [ ] Document bootstrap/rotation in `docs/11_AUTHORITY.md` + `docs/security/dpop-mtls-rollout.md` (this file).
## Verification Matrix
| Scenario | Test/Command | Expected |
| --- | --- | --- |
| DPoP required w/out proof | `dotnet test Authority.Tests --filter DPoPRequiresProofTest` | 400 with `use_dpop_nonce` header. |
| Nonce replay | Replay previous proof within window | 401 + audit log entry. |
| mTLS mismatch | Reuse token with different cert | 401 + `senderCertificateMismatch` metric increment. |
## Telemetry & Alerting
- Metrics: `authority_dpop_nonce_miss_total`, `authority_mtls_mismatch_total` (emitted with `reason` tags for context-missing, missing-certificate, and thumbprint-mismatch cases).
- Logs: `authority.security.senderConstraint` (structured).
- Alerts: Page DevOps when nonce miss > 5% or mTLS mismatches > 0 over 10 min.
## Dependencies
- Authority Core & Security Guild owners.
- DevOps to provide sealed-mode CI coverage (`DEVOPS-AIRGAP-57-002`).
- Plugin Standard Guild to consume new telemetry once rolled out.
## Communication
- Daily async update in `#guild-authority` thread referencing this plan.
- Link this document from `docs/implplan/SPRINT_100_identity_signing.md` notes once Phase 1 merges.

View File

@@ -0,0 +1,68 @@
# RootPack_RU Packaging Guide
This guide describes the reproducible process for assembling the sovereign cryptography bundle that backs RootPack_RU deployments.
## 1. What the bundle contains
| Directory | Purpose |
|-----------|---------|
| `artifacts/` | Published binaries for `StellaOps.Cryptography.Plugin.CryptoPro` and `StellaOps.Cryptography.Plugin.Pkcs11Gost` (targeting `net10.0`). |
| `config/rootpack_ru.crypto.yaml` | Opinionated configuration template that enables the `ru-offline` crypto profile and defines CryptoPro + PKCS#11 keys. |
| `docs/` | Validation runbook, audit report, and this packaging guide. |
| `trust/` | Russian trust-anchor PEM files copied from `certificates/russian_trusted_*`. |
| `README.txt` | High-level summary plus operator checklist. |
## 2. Build the bundle
```bash
# from repository root
scripts/crypto/package-rootpack-ru.sh
# optionally specify destination
scripts/crypto/package-rootpack-ru.sh /tmp/rootpack_ru_$(date -u +%Y%m%dT%H%M%SZ)
```
The script performs the following steps:
1. `dotnet publish` for the CryptoPro + PKCS#11 plug-ins (`Release` configuration).
2. Copies the relevant documentation (`docs/security/rootpack_ru_validation.md`, `docs/security/crypto-routing-audit-2025-11-07.md`, and this guide).
3. Includes the example configuration found at `etc/rootpack/ru/crypto.profile.yaml`.
4. Adds the Russian trust anchors from `certificates/russian_trusted_*`.
5. Emits `README.txt` and optionally creates a `*.tar.gz` archive (set `PACKAGE_TAR=0` to skip the tarball).
## 3. Attach deterministic test evidence
After running `scripts/crypto/package-rootpack-ru.sh`, execute the deterministic harness to capture logs:
```bash
scripts/crypto/run-rootpack-ru-tests.sh
# or specify ROOTPACK_LOG_DIR=/tmp/rootpack_ru_tests scripts/crypto/run-rootpack-ru-tests.sh
```
Copy the resulting `logs/rootpack_ru_<timestamp>/` directory into the bundle before distributing it (or store it alongside the tarball in your evidence store).
Each harness run produces a `README.tests` file plus matching `.log/.trx` pairs for every project. Move the entire directory under an evidence folder inside the bundle (for example `evidence/validation/<timestamp>/`) so operators can quickly locate the README, raw logs, and provider JSON snapshots when assembling compliance paperwork:
```bash
dest="${1:-out/rootpack_ru}"
ts="$(ls logs | grep rootpack_ru_ | sort | tail -n1)"
mkdir -p "${dest}/evidence/validation"
cp -a "logs/${ts}" "${dest}/evidence/validation/${ts}"
```
## 4. Hardware validation + audit metadata
Follow `docs/security/rootpack_ru_validation.md` to:
- Validate CryptoPro CSP and PKCS#11 tokens.
- Capture `stellaops crypto providers --profile ru-offline --json` output.
- Archive JWKS snapshots and `CryptoProviderMetrics` samples.
- Document hardware serials and operator initials in `hardware_notes.md`.
Store these artifacts under `logs/rootpack_ru_<timestamp>/` (same directory as the test harness outputs) and reference them in release paperwork.
## 5. Deployment summary
1. Import the bundled trust anchors into the target installation (Authority + Scanner).
2. Apply `config/rootpack_ru.crypto.yaml`, update certificate thumbprints, slots, and container labels to match the operator tokens.
3. Restart the services so `ICryptoProviderRegistry` reloads the `ru-offline` profile.
4. Re-run the validation runbook to confirm JWKS, telemetry, and RootPack evidence are aligned with the shipping bundle.

View File

@@ -0,0 +1,45 @@
# RootPack_RU Crypto Validation Runbook
## Purpose
This runbook documents the repeatable steps for validating the Russian sovereign crypto profile (CryptoPro + PKCS#11) prior to publishing a RootPack bundle. It supplements the crypto routing audit by covering deterministic tests, hardware validation, and the audit metadata artifacts that must be attached to each release.
## 1. Deterministic Test Harness
1. Run `scripts/crypto/run-rootpack-ru-tests.sh` (optional `ROOTPACK_LOG_DIR=/tmp/rootpack_ru_logs` to override the output path). The script executes:
- `src/__Libraries/__Tests/StellaOps.Cryptography.Tests/StellaOps.Cryptography.Tests.csproj`
- `src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.csproj`
- `src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj`
and emits `.log` + `.trx` pairs plus `README.tests` under `logs/rootpack_ru_<timestamp>/`.
2. For ad-hoc runs (CI or IDE) ensure the same three projects succeed; the cryptography tests validate SHA-256/SHA-512 against BCL implementations and both Streebog variants against BouncyCastle digests.
3. Archive the generated log directory (`logs/rootpack_ru_<timestamp>/`) along with any additional test outputs inside the RootPack evidence bundle.
## 2. Hardware Validation (CryptoPro CSP)
1. Install CryptoPro CSP (v5.0 or later) on the validation host and import the qualified certificate configured for the deployment.
2. Configure `StellaOps:Crypto:CryptoPro:Keys` with the container handle and certificate thumbprint and set `StellaOps:Crypto:Registry:ActiveProfile=ru-offline`.
3. Run the provider diagnostics to confirm the key material is visible:
- `stellaops crypto providers --profile ru-offline --json > logs/ru_cryptopro_providers.json`
4. Issue a JWKS fetch (`curl https://authority.local/.well-known/jwks`) and verify the `kid` and `crv` values match the CryptoPro-backed key.
5. Capture the Authority logs showing `AuthoritySecretHasherInitializer` startup and the `CryptoProviderMetrics` counters for `ru.cryptopro.csp` usage.
## 3. Hardware Validation (PKCS#11 Tokens)
1. Install the vendor PKCS#11 library (e.g., Rutoken `rtPKCS11ECP.dll` or JaCarta) and configure the slot/PIN inside `StellaOps:Crypto:Pkcs11:Keys`.
2. Switch the registry profile to prioritize `ru.pkcs11` and rerun `stellaops crypto providers --profile ru-offline --json > logs/ru_pkcs11_providers.json`.
3. Execute a signing workflow (Authority JWKS refresh or Scanner manifest publish) and confirm the `CryptoProviderMetrics` counters record `ru.pkcs11` activity.
4. Export the token audit logs (if available) and store them with the RootPack evidence bundle.
## 4. RootPack Audit Metadata
Create a metadata bundle per validation run and store it under `logs/rootpack_ru_<timestamp>/` containing:
- `providers_ru_offline.json` output from `stellaops crypto providers --profile ru-offline --json`.
- `crypto_tests.txt` snippets from the unit-test executions listed above.
- `hardware_notes.md` human-readable notes describing token serials, firmware, and operator initials.
- `jwks_snapshot.json` raw JWKS response captured after sovereign providers are active.
- `metrics_snapshot.json` scrape of `CryptoProviderMetrics` Prometheus samples for both providers.
Attach this directory to the RootPack artifact and reference it from the release checklist.
Refer back to `docs/security/crypto-routing-audit-2025-11-07.md` for the full inventory of components that must consume the shared cryptography stack, and `docs/security/rootpack_ru_package.md` for packaging/attachment steps.

View File

@@ -1,33 +1,38 @@
# StellaOps Policy Engine configuration template. # StellaOps Policy Engine configuration template.
# Copy to ../etc/policy-engine.yaml (relative to the Policy Engine content root) # Copy to ../etc/policy-engine.yaml (relative to the Policy Engine content root)
# and adjust values to fit your environment. Environment variables prefixed with # and adjust values to fit your environment. Environment variables prefixed with
# STELLAOPS_POLICY_ENGINE_ override these values at runtime. # STELLAOPS_POLICY_ENGINE_ override these values at runtime.
schemaVersion: 1 schemaVersion: 1
authority: authority:
enabled: true enabled: true
issuer: "https://authority.stella-ops.local" issuer: "https://authority.stella-ops.local"
clientId: "policy-engine" clientId: "policy-engine"
clientSecret: "change-me" clientSecret: "change-me"
scopes: [ "policy:run", "findings:read", "effective:write" ] scopes: [ "policy:run", "findings:read", "effective:write" ]
backchannelTimeoutSeconds: 30 backchannelTimeoutSeconds: 30
storage: storage:
connectionString: "mongodb://localhost:27017/policy-engine" connectionString: "mongodb://localhost:27017/policy-engine"
databaseName: "policy_engine" databaseName: "policy_engine"
commandTimeoutSeconds: 30 commandTimeoutSeconds: 30
workers: workers:
schedulerIntervalSeconds: 15 schedulerIntervalSeconds: 15
maxConcurrentEvaluations: 4 maxConcurrentEvaluations: 4
resourceServer: activation:
authority: "https://authority.stella-ops.local" forceTwoPersonApproval: false
requireHttpsMetadata: true defaultRequiresTwoPersonApproval: false
audiences: [ "api://policy-engine" ] emitAuditLogs: true
requiredScopes: [ "policy:run" ]
requiredTenants: [ ] resourceServer:
bypassNetworks: authority: "https://authority.stella-ops.local"
- "127.0.0.1/32" requireHttpsMetadata: true
- "::1/128" audiences: [ "api://policy-engine" ]
requiredScopes: [ "policy:run" ]
requiredTenants: [ ]
bypassNetworks:
- "127.0.0.1/32"
- "::1/128"

View File

@@ -0,0 +1,30 @@
StellaOps:
Crypto:
Registry:
ActiveProfile: ru-offline
PreferredProviders:
- default
Profiles:
ru-offline:
PreferredProviders:
- ru.cryptopro.csp
- ru.pkcs11
CryptoPro:
Keys:
- KeyId: ru-csp-default
LibraryPath: /opt/cprocsp/lib/amd64/libcapi20.so
ContainerLabel: CN=RootPack Signing
CertificateThumbprint: "<thumbprint>"
Pkcs11:
Keys:
- KeyId: ru-token-default
LibraryPath: /usr/local/lib/librutokenecp.so
SlotId: "0x1"
Pin: "${PKCS11_PIN}"
PrivateKeyLabel: rootpack-signing
CertificateThumbprint: "<thumbprint>"
Diagnostics:
Providers:
Enabled: true
Metrics:
LogLevel: Information

View File

@@ -23,6 +23,7 @@ Signals:
Mongo: Mongo:
ConnectionString: "mongodb://localhost:27017/signals" ConnectionString: "mongodb://localhost:27017/signals"
Database: "signals" Database: "signals"
CallgraphsCollection: "callgraphs" CallgraphsCollection: "callgraphs"
ReachabilityFactsCollection: "reachability_facts"
Storage: Storage:
RootPath: "../data/signals-artifacts" RootPath: "../data/signals-artifacts"

View File

@@ -46,3 +46,5 @@
| HELM-45-001 | TODO | Deployment Guild | COMPOSE-44-001 | Scaffold `deploy/helm/stella` chart with values, component toggles, and pinned image digests for all services; include migration Job templates. | Chart installs in dev cluster; images pinned; lint/tests pass. | | HELM-45-001 | TODO | Deployment Guild | COMPOSE-44-001 | Scaffold `deploy/helm/stella` chart with values, component toggles, and pinned image digests for all services; include migration Job templates. | Chart installs in dev cluster; images pinned; lint/tests pass. |
| HELM-45-002 | TODO | Deployment Guild, Security Guild | HELM-45-001 | Add TLS/Ingress, NetworkPolicy, PodSecurityContexts, Secrets integration (external secrets), and document security posture. | Helm values support secure defaults; policies validated; docs updated. | | HELM-45-002 | TODO | Deployment Guild, Security Guild | HELM-45-001 | Add TLS/Ingress, NetworkPolicy, PodSecurityContexts, Secrets integration (external secrets), and document security posture. | Helm values support secure defaults; policies validated; docs updated. |
| HELM-45-003 | TODO | Deployment Guild, Observability Guild | HELM-45-001 | Implement HPA, PDB, readiness gates, Prometheus scraping annotations, OTel configuration hooks, and upgrade hooks. | Rolling upgrade succeeds in CI; observability wires confirmed; upgrade docs updated. | | HELM-45-003 | TODO | Deployment Guild, Observability Guild | HELM-45-001 | Implement HPA, PDB, readiness gates, Prometheus scraping annotations, OTel configuration hooks, and upgrade hooks. | Rolling upgrade succeeds in CI; observability wires confirmed; upgrade docs updated. |
| HELM-45-004 | DONE (2025-11-08) | Deployment Guild, Policy Guild | HELM-45-001 | Wire Policy Engine / Gateway pods to consume the `policy-engine-activation` ConfigMap (envFrom/volume mounts), ensure host configuration loads activation overrides, and update Helm/Compose samples accordingly. | Pods mount config map deterministically; activation settings honored in Policy Engine; samples/tests updated for air-gap parity. |
> 2025-11-08: Added config builder support for `/config/policy-engine/activation.yaml`, templated envFrom injection for policy-engine/gateway pods, verified Policy Engine/Gateway tests, and CI now runs `helm lint` + `helm template` for every `values*.yaml`.

View File

@@ -45,7 +45,10 @@
| DEVOPS-AIRGAP-56-002 | TODO | DevOps Guild, AirGap Importer Guild | AIRGAP-IMP-57-002 | Provide import tooling for bundle staging: checksum validation, offline object-store loader scripts, removable media guidance. | Scripts documented; smoke tests validate import; runbook updated. | | DEVOPS-AIRGAP-56-002 | TODO | DevOps Guild, AirGap Importer Guild | AIRGAP-IMP-57-002 | Provide import tooling for bundle staging: checksum validation, offline object-store loader scripts, removable media guidance. | Scripts documented; smoke tests validate import; runbook updated. |
| DEVOPS-AIRGAP-56-003 | TODO | DevOps Guild, Container Distribution Guild | EXPORT-AIRGAP-56-002 | Build Bootstrap Pack pipeline bundling images/charts, generating checksums, and publishing manifest for offline transfer. | Pipeline runs in connected env; pack verified in air-gap smoke test; manifest recorded. | | DEVOPS-AIRGAP-56-003 | TODO | DevOps Guild, Container Distribution Guild | EXPORT-AIRGAP-56-002 | Build Bootstrap Pack pipeline bundling images/charts, generating checksums, and publishing manifest for offline transfer. | Pipeline runs in connected env; pack verified in air-gap smoke test; manifest recorded. |
| DEVOPS-AIRGAP-57-001 | TODO | DevOps Guild, Mirror Creator Guild | MIRROR-CRT-56-002 | Automate Mirror Bundle creation jobs with dual-control approvals, artifact signing, and checksum publication. | Approval workflow enforced; CI artifact includes DSSE/TUF metadata; audit logs stored. | | DEVOPS-AIRGAP-57-001 | TODO | DevOps Guild, Mirror Creator Guild | MIRROR-CRT-56-002 | Automate Mirror Bundle creation jobs with dual-control approvals, artifact signing, and checksum publication. | Approval workflow enforced; CI artifact includes DSSE/TUF metadata; audit logs stored. |
| DEVOPS-AIRGAP-57-002 | TODO | DevOps Guild, Authority Guild | AUTH-OBS-50-001 | Configure sealed-mode CI tests that run services with sealed flag and ensure no egress occurs (iptables + mock DNS). | CI suite fails on attempted egress; reports remediation; documentation updated. | | DEVOPS-AIRGAP-57-002 | DOING (2025-11-08) | DevOps Guild, Authority Guild | AUTH-OBS-50-001 | Configure sealed-mode CI tests that run services with sealed flag and ensure no egress occurs (iptables + mock DNS). | CI suite fails on attempted egress; reports remediation; documentation updated. |
> 2025-11-08: Landed `sealed-mode-compose.yml`, `run-sealed-ci.sh`, and `egress_probe.py`, plus the `.gitea/workflows/build-test-deploy.yml` job that uploads `artifacts/sealed-mode-ci/<commit>/authority-sealed-ci.json`; waiting on Authority to consume the artefact before flipping DONE.
> 2025-11-07: Blocking AUTH-AIRGAP-57-001 (Authority gating); prioritize sealed-mode CI artifacts so Authority can flip the enforcement switch.
> 2025-11-07: Target ETA agreed with Authority is 2025-11-10 for first CI run (iptables + mock DNS) plus doc updates.
| DEVOPS-AIRGAP-58-001 | TODO | DevOps Guild, Notifications Guild | NOTIFY-AIRGAP-56-002 | Provide local SMTP/syslog container templates and health checks for sealed environments; integrate into Bootstrap Pack. | Templates deployed successfully; health checks in CI; docs updated. | | DEVOPS-AIRGAP-58-001 | TODO | DevOps Guild, Notifications Guild | NOTIFY-AIRGAP-56-002 | Provide local SMTP/syslog container templates and health checks for sealed environments; integrate into Bootstrap Pack. | Templates deployed successfully; health checks in CI; docs updated. |
| DEVOPS-AIRGAP-58-002 | TODO | DevOps Guild, Observability Guild | DEVOPS-AIRGAP-56-001, DEVOPS-OBS-51-001 | Ship sealed-mode observability stack (Prometheus/Grafana/Tempo/Loki) pre-configured with offline dashboards and no remote exporters. | Stack boots offline; dashboards available; verification script confirms zero egress. | | DEVOPS-AIRGAP-58-002 | TODO | DevOps Guild, Observability Guild | DEVOPS-AIRGAP-56-001, DEVOPS-OBS-51-001 | Ship sealed-mode observability stack (Prometheus/Grafana/Tempo/Loki) pre-configured with offline dashboards and no remote exporters. | Stack boots offline; dashboards available; verification script confirms zero egress. |
| DEVOPS-REL-17-004 | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-REL-17-002 | Ensure release workflow publishes `out/release/debug` (build-id tree + manifest) and fails when symbols are missing. | Release job emits debug artefacts, `mirror_debug_store.py` summary committed, warning cleared from build logs, docs updated. | | DEVOPS-REL-17-004 | BLOCKED (2025-10-26) | DevOps Guild | DEVOPS-REL-17-002 | Ensure release workflow publishes `out/release/debug` (build-id tree + manifest) and fails when symbols are missing. | Release job emits debug artefacts, `mirror_debug_store.py` summary committed, warning cleared from build logs, docs updated. |

View File

@@ -0,0 +1,25 @@
# Sealed-Mode CI Harness
This harness supports `DEVOPS-AIRGAP-57-002` by exercising services with the `sealed` flag, verifying that no outbound network traffic succeeds, and producing artefacts Authority can use for `AUTH-AIRGAP-57-001` gating.
## Workflow
1. Run `./run-sealed-ci.sh` from this directory (the script now boots the stack, applies the iptables guard, and captures artefacts automatically).
2. The harness:
- Launches `sealed-mode-compose.yml` with Authority/Signer/Attestor + Mongo.
- Snapshots iptables, injects a `STELLAOPS_SEALED` chain into `DOCKER-USER`/`OUTPUT`, and whitelists only loopback + RFC1918 ranges so container egress is denied.
- Repeatedly polls `/healthz` on `5088/6088/7088` to verify sealed-mode bindings stay healthy while egress is blocked.
- Executes `egress_probe.py`, which runs curl probes from inside the compose network to confirm off-cluster addresses are unreachable.
- Writes logs, iptables counters, and the summary contract to `artifacts/sealed-mode-ci/<timestamp>`.
3. `.gitea/workflows/build-test-deploy.yml` now includes a `sealed-mode-ci` job that runs this script on every push/PR and uploads the artefacts for `AUTH-AIRGAP-57-001`.
## Outputs
- `authority.health.log`, `signer.health.log`, `attestor.health.log`
- `iptables-docker-user.txt`, `iptables-output.txt`
- `egress-probe.json`
- `compose.log`, `compose.ps`
- `authority-sealed-ci.json` (single file Authority uses to validate the run)
## TODO
- [ ] Wire into offline kit smoke tests (DEVOPS-AIRGAP-58-001).
Refer to `docs/security/dpop-mtls-rollout.md` for cross-guild milestones.

View File

@@ -0,0 +1,8 @@
The command 'docker' could not be found in this WSL 2 distro.
We recommend to activate the WSL integration in Docker Desktop settings.
For details about using Docker Desktop with WSL 2, visit:
https://docs.docker.com/go/wsl2/

View File

@@ -0,0 +1,8 @@
The command 'docker' could not be found in this WSL 2 distro.
We recommend to activate the WSL integration in Docker Desktop settings.
For details about using Docker Desktop with WSL 2, visit:
https://docs.docker.com/go/wsl2/

View File

@@ -0,0 +1,54 @@
schemaVersion: 1
issuer: http://authority.sealed-ci.local
accessTokenLifetime: 00:02:00
refreshTokenLifetime: 01:00:00
identityTokenLifetime: 00:05:00
authorizationCodeLifetime: 00:05:00
deviceCodeLifetime: 00:15:00
pluginDirectories:
- /app
plugins:
configurationDirectory: /app/plugins
descriptors:
standard:
type: standard
assemblyName: StellaOps.Authority.Plugin.Standard
enabled: true
configFile: standard.yaml
storage:
connectionString: mongodb://sealedci:sealedci@mongo:27017/authority?authSource=admin
databaseName: authority
commandTimeout: 00:00:30
signing:
enabled: true
activeKeyId: sealed-ci
keyPath: /certificates/authority-signing-dev.pem
algorithm: ES256
keySource: file
bootstrap:
enabled: false
crypto:
providers: []
security:
senderConstraints:
dpop:
enabled: true
proofLifetime: 00:02:00
replayWindow: 00:05:00
nonce:
enabled: false
mtls:
enabled: false
airGap:
egress:
mode: Sealed
allowLoopback: true
allowPrivateNetworks: true
remediationDocumentationUrl: https://docs.stella-ops.org/airgap/sealed-ci
supportContact: airgap-ops@stella-ops.org
tenants:
- name: sealed-ci
roles:
operators:
scopes:
- policy:read

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Run egress probes from the sealed-mode Docker network."""
from __future__ import annotations
import argparse
import json
import os
import shlex
import subprocess
import sys
import time
from datetime import datetime, timezone
from typing import List
DEFAULT_TARGETS = [
"https://example.com",
"https://www.cloudflare.com",
"https://releases.stella-ops.org/healthz",
]
def run_probe(image: str, network: str, target: str, timeout: int) -> dict:
cmd: List[str] = [
"docker",
"run",
"--rm",
"--network",
network,
image,
"-fsS",
"--max-time",
str(timeout),
target,
]
started = time.monotonic()
proc = subprocess.run(cmd, capture_output=True, text=True)
duration = time.monotonic() - started
status = "blocked" if proc.returncode != 0 else "connected"
return {
"target": target,
"status": status,
"durationSeconds": round(duration, 3),
"exitCode": proc.returncode,
"command": " ".join(shlex.quote(part) for part in cmd),
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--network", required=True, help="Docker network to join (compose project network)")
parser.add_argument("--image", default="curlimages/curl:8.6.0", help="Container image providing curl")
parser.add_argument("--timeout", type=int, default=10, help="Curl max-time for each probe (seconds)")
parser.add_argument("--output", required=True, help="Path to write JSON results")
parser.add_argument("targets", nargs="*", help="Override target URLs")
args = parser.parse_args()
targets = args.targets or DEFAULT_TARGETS
results = [run_probe(args.image, args.network, target, args.timeout) for target in targets]
passed = all(result["status"] == "blocked" for result in results)
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"network": args.network,
"image": args.image,
"targets": results,
"passed": passed,
}
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write("\n")
return 0 if passed else 1
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as exc: # pragma: no cover
print(f"egress probe failed: {exc}", file=sys.stderr)
sys.exit(2)

View File

@@ -0,0 +1,18 @@
bootstrapUser:
username: sealed-admin
password: ChangeMe11!
passwordPolicy:
minimumLength: 8
requireUppercase: false
requireLowercase: true
requireDigit: true
requireSymbol: false
passwordHashing:
algorithm: Argon2id
memorySizeInKib: 8192
iterations: 2
parallelism: 1
lockout:
enabled: false
tokenSigning:
keyDirectory: /certificates

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cd "$SCRIPT_DIR"
COMPOSE_FILE=${COMPOSE_FILE:-"$SCRIPT_DIR/sealed-mode-compose.yml"}
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-sealedmode}
NETWORK_NAME="${PROJECT_NAME}_sealed-ci"
ARTIFACT_ROOT=${ARTIFACT_ROOT:-"$SCRIPT_DIR/artifacts/sealed-mode-ci"}
STAMP=$(date -u +"%Y%m%dT%H%M%SZ")
OUT_DIR="$ARTIFACT_ROOT/$STAMP"
mkdir -p "$OUT_DIR"
log() {
printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"
}
EXIT_CODE=0
IPTABLES_SNAPSHOT=""
cleanup() {
local exit_code=$?
log "Collecting docker compose logs"
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" logs >"$OUT_DIR/compose.log" 2>&1 || true
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" ps -a >"$OUT_DIR/compose.ps" 2>&1 || true
log "Tearing down sealed-mode stack"
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >"$OUT_DIR/docker-down.log" 2>&1 || true
if [[ -n "$IPTABLES_SNAPSHOT" && -f "$IPTABLES_SNAPSHOT" ]]; then
log "Restoring iptables snapshot"
sudo iptables-restore <"$IPTABLES_SNAPSHOT" || true
rm -f "$IPTABLES_SNAPSHOT"
fi
log "Artifacts stored at $OUT_DIR"
exit $exit_code
}
trap cleanup EXIT
log "Pulling compose images (best effort)"
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" pull --ignore-pull-failures || true
log "Starting sealed-mode stack"
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d --remove-orphans
wait_for_port() {
local port=$1
local label=$2
for attempt in $(seq 1 30); do
if curl -fsS --max-time 5 "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1; then
log "$label responded on port $port"
return 0
fi
sleep 2
done
log "$label failed to respond on port $port"
return 1
}
wait_for_port 5088 "Authority" || EXIT_CODE=1
wait_for_port 6088 "Signer" || EXIT_CODE=1
wait_for_port 7088 "Attestor" || EXIT_CODE=1
log "Fetching probe helper image"
docker pull curlimages/curl:8.6.0 >/dev/null 2>&1 || true
log "Snapshotting iptables state"
IPTABLES_SNAPSHOT=$(mktemp)
sudo iptables-save >"$IPTABLES_SNAPSHOT"
log "Applying sealed-mode egress policy"
CHAIN="STELLAOPS_SEALED"
sudo iptables -N "$CHAIN" 2>/dev/null || sudo iptables -F "$CHAIN"
for cidr in 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16; do
sudo iptables -A "$CHAIN" -d "$cidr" -j RETURN
done
sudo iptables -A "$CHAIN" -j LOG --log-prefix "stellaops-sealed-deny " --log-level 4
sudo iptables -A "$CHAIN" -j DROP
sudo iptables -I DOCKER-USER 1 -j "$CHAIN"
sudo iptables -I OUTPUT 1 -j "$CHAIN"
check_health() {
local name=$1
local port=$2
local url="http://127.0.0.1:${port}/healthz"
local log_file="$OUT_DIR/${name}.health.log"
local status="fail"
for attempt in $(seq 1 20); do
if curl -fsS --max-time 5 "$url" >"$log_file" 2>&1; then
status="pass"
break
fi
sleep 2
done
if [[ "$status" == "pass" ]]; then
log "$name health check succeeded"
else
log "$name health check failed"
EXIT_CODE=1
fi
local upper
upper=$(echo "$name" | tr '[:lower:]' '[:upper:]')
eval "${upper}_HEALTH_STATUS=$status"
eval "${upper}_HEALTH_URL=$url"
}
check_health authority 5088
check_health signer 6088
check_health attestor 7088
log "Running egress probe via docker network $NETWORK_NAME"
EGRESS_JSON="$OUT_DIR/egress-probe.json"
if python3 "$SCRIPT_DIR/egress_probe.py" --network "$NETWORK_NAME" --image curlimages/curl:8.6.0 --timeout 8 --output "$EGRESS_JSON"; then
EGRESS_STATUS="pass"
else
EGRESS_STATUS="fail"
EXIT_CODE=1
fi
log "Dumping iptables counters"
sudo iptables -v -x -L DOCKER-USER >"$OUT_DIR/iptables-docker-user.txt"
sudo iptables -v -x -L OUTPUT >"$OUT_DIR/iptables-output.txt"
log "Recording summary JSON"
export PROJECT_NAME NETWORK_NAME EGRESS_STATUS EGRESS_JSON
export AUTHORITY_HEALTH_STATUS SIGNER_HEALTH_STATUS ATTESTOR_HEALTH_STATUS
export AUTHORITY_HEALTH_URL SIGNER_HEALTH_URL ATTESTOR_HEALTH_URL
python3 - <<'PY' >"$OUT_DIR/authority-sealed-ci.json"
import json
import os
import sys
from datetime import datetime, timezone
summary = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"project": os.environ.get("PROJECT_NAME"),
"network": os.environ.get("NETWORK_NAME"),
"health": {
"authority": {
"status": os.environ.get("AUTHORITY_HEALTH_STATUS", "unknown"),
"url": os.environ.get("AUTHORITY_HEALTH_URL"),
"log": "authority.health.log",
},
"signer": {
"status": os.environ.get("SIGNER_HEALTH_STATUS", "unknown"),
"url": os.environ.get("SIGNER_HEALTH_URL"),
"log": "signer.health.log",
},
"attestor": {
"status": os.environ.get("ATTESTOR_HEALTH_STATUS", "unknown"),
"url": os.environ.get("ATTESTOR_HEALTH_URL"),
"log": "attestor.health.log",
},
},
"egressProbe": {
"status": os.environ.get("EGRESS_STATUS", "unknown"),
"report": os.path.basename(os.environ.get("EGRESS_JSON", "egress-probe.json")),
},
}
json.dump(summary, sys.stdout, indent=2)
print()
PY
if [[ $EXIT_CODE -eq 0 ]]; then
log "Sealed-mode CI run completed successfully"
else
log "Sealed-mode CI run completed with failures"
fi
exit $EXIT_CODE

View File

@@ -0,0 +1,83 @@
version: '3.9'
x-release-labels: &release-labels
com.stellaops.profile: 'sealed-ci'
com.stellaops.airgap.mode: 'sealed'
networks:
sealed-ci:
driver: bridge
volumes:
sealed-mongo-data:
services:
mongo:
image: docker.io/library/mongo@sha256:c258b26dbb7774f97f52aff52231ca5f228273a84329c5f5e451c3739457db49
command: ['mongod', '--bind_ip_all']
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: sealedci
MONGO_INITDB_ROOT_PASSWORD: sealedci-secret
volumes:
- sealed-mongo-data:/data/db
networks:
- sealed-ci
labels: *release-labels
authority:
image: registry.stella-ops.org/stellaops/authority@sha256:a8e8faec44a579aa5714e58be835f25575710430b1ad2ccd1282a018cd9ffcdd
depends_on:
- mongo
restart: unless-stopped
environment:
ASPNETCORE_URLS: http://+:5088
STELLAOPS_AUTHORITY__ISSUER: http://authority.sealed-ci.local
STELLAOPS_AUTHORITY__MONGO__CONNECTIONSTRING: mongodb://sealedci:sealedci-secret@mongo:27017/authority?authSource=admin
STELLAOPS_AUTHORITY__PLUGINDIRECTORIES__0: /app/plugins
STELLAOPS_AUTHORITY__PLUGINS__CONFIGURATIONDIRECTORY: /app/plugins
STELLAOPS_AUTHORITY__SECURITY__SENDERCONSTRAINTS__DPOP__ENABLED: 'true'
STELLAOPS_AUTHORITY__SECURITY__SENDERCONSTRAINTS__MTLS__ENABLED: 'true'
STELLAOPS_AUTHORITY__AIRGAP__EGRESS__MODE: Sealed
volumes:
- ./authority.harness.yaml:/etc/authority.yaml:ro
- ./plugins:/app/plugins:ro
- ../../../certificates:/certificates:ro
ports:
- '5088:5088'
networks:
- sealed-ci
labels: *release-labels
signer:
image: registry.stella-ops.org/stellaops/signer@sha256:8bfef9a75783883d49fc18e3566553934e970b00ee090abee9cb110d2d5c3298
depends_on:
- authority
restart: unless-stopped
environment:
ASPNETCORE_URLS: http://+:6088
SIGNER__AUTHORITY__BASEURL: http://authority:5088
SIGNER__POE__INTROSPECTURL: http://authority:5088/device-code
SIGNER__STORAGE__MONGO__CONNECTIONSTRING: mongodb://sealedci:sealedci-secret@mongo:27017/signer?authSource=admin
SIGNER__SEALED__MODE: Enabled
ports:
- '6088:6088'
networks:
- sealed-ci
labels: *release-labels
attestor:
image: registry.stella-ops.org/stellaops/attestor@sha256:5cc417948c029da01dccf36e4645d961a3f6d8de7e62fe98d845f07cd2282114
depends_on:
- signer
restart: unless-stopped
environment:
ASPNETCORE_URLS: http://+:7088
ATTESTOR__SIGNER__BASEURL: http://signer:6088
ATTESTOR__MONGO__CONNECTIONSTRING: mongodb://sealedci:sealedci-secret@mongo:27017/attestor?authSource=admin
ATTESTOR__SEALED__MODE: Enabled
ports:
- '7088:7088'
networks:
- sealed-ci
labels: *release-labels

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
OUTPUT_ROOT="${1:-${ROOT_DIR}/build/rootpack_ru_${TIMESTAMP}}"
ARTIFACT_DIR="${OUTPUT_ROOT}/artifacts"
DOC_DIR="${OUTPUT_ROOT}/docs"
CONFIG_DIR="${OUTPUT_ROOT}/config"
TRUST_DIR="${OUTPUT_ROOT}/trust"
mkdir -p "$ARTIFACT_DIR" "$DOC_DIR" "$CONFIG_DIR" "$TRUST_DIR"
publish_plugin() {
local project="$1"
local name="$2"
local publish_dir="${ARTIFACT_DIR}/${name}"
echo "[rootpack-ru] Publishing ${project} -> ${publish_dir}"
dotnet publish "$project" -c Release -o "$publish_dir" --nologo >/dev/null
}
publish_plugin "src/__Libraries/StellaOps.Cryptography.Plugin.CryptoPro/StellaOps.Cryptography.Plugin.CryptoPro.csproj" "StellaOps.Cryptography.Plugin.CryptoPro"
publish_plugin "src/__Libraries/StellaOps.Cryptography.Plugin.Pkcs11Gost/StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" "StellaOps.Cryptography.Plugin.Pkcs11Gost"
cp docs/security/rootpack_ru_validation.md "$DOC_DIR/"
cp docs/security/crypto-routing-audit-2025-11-07.md "$DOC_DIR/"
cp docs/security/rootpack_ru_package.md "$DOC_DIR/"
cp etc/rootpack/ru/crypto.profile.yaml "$CONFIG_DIR/rootpack_ru.crypto.yaml"
shopt -s nullglob
for pem in "$ROOT_DIR"/certificates/russian_trusted_*; do
cp "$pem" "$TRUST_DIR/"
done
shopt -u nullglob
cat <<README >"${OUTPUT_ROOT}/README.txt"
RootPack_RU bundle (${TIMESTAMP})
--------------------------------
Contents:
- artifacts/ : Sovereign crypto plug-ins published for net10.0 (CryptoPro + PKCS#11)
- config/rootpack_ru.crypto.yaml : example configuration binding registry profiles
- docs/ : validation + audit documentation
- trust/ : Russian trust anchor PEM bundle copied from certificates/
Usage:
1. Review docs/rootpack_ru_package.md for installation steps.
2. Execute scripts/crypto/run-rootpack-ru-tests.sh (or CI equivalent) and attach the logs to this bundle.
3. Record hardware validation outputs per docs/rootpack_ru_validation.md and store alongside this directory.
README
if [[ "${PACKAGE_TAR:-1}" != "0" ]]; then
tarball="${OUTPUT_ROOT}.tar.gz"
echo "[rootpack-ru] Creating ${tarball}"
tar -czf "$tarball" -C "$(dirname "$OUTPUT_ROOT")" "$(basename "$OUTPUT_ROOT")"
fi
echo "[rootpack-ru] Bundle staged under $OUTPUT_ROOT"

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
DEFAULT_LOG_ROOT="${ROOT_DIR}/logs/rootpack_ru_$(date -u +%Y%m%dT%H%M%SZ)"
LOG_ROOT="${ROOTPACK_LOG_DIR:-$DEFAULT_LOG_ROOT}"
mkdir -p "$LOG_ROOT"
PROJECTS=(
"src/__Libraries/__Tests/StellaOps.Cryptography.Tests/StellaOps.Cryptography.Tests.csproj"
"src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.csproj"
"src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj"
)
run_test() {
local project="$1"
local safe_name
safe_name="$(basename "${project%.csproj}")"
local log_file="${LOG_ROOT}/${safe_name}.log"
local trx_name="${safe_name}.trx"
echo "[rootpack-ru] Running tests for ${project}" | tee "$log_file"
dotnet test "$project" \
--nologo \
--verbosity minimal \
--results-directory "$LOG_ROOT" \
--logger "trx;LogFileName=${trx_name}" | tee -a "$log_file"
}
PROJECT_SUMMARY=()
for project in "${PROJECTS[@]}"; do
run_test "$project"
safe_name="$(basename "${project%.csproj}")"
PROJECT_SUMMARY+=("$project|$safe_name")
echo "[rootpack-ru] Wrote logs for ${project} -> ${LOG_ROOT}/${safe_name}.log"
done
{
echo "RootPack_RU deterministic test harness"
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Log Directory: $LOG_ROOT"
echo ""
echo "Projects:"
for entry in "${PROJECT_SUMMARY[@]}"; do
project_path="${entry%%|*}"
safe_name="${entry##*|}"
printf ' - %s (log: %s.log, trx: %s.trx)\n' "$project_path" "$safe_name" "$safe_name"
done
} > "$LOG_ROOT/README.tests"
echo "Logs and TRX files available under $LOG_ROOT"

View File

@@ -24,7 +24,9 @@ public sealed record AocGuardOptions
"createdAt", "createdAt",
"created_at", "created_at",
"ingestedAt", "ingestedAt",
"ingested_at" "ingested_at",
"links",
"advisory_key"
}, StringComparer.OrdinalIgnoreCase) }, StringComparer.OrdinalIgnoreCase)
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);

View File

@@ -27,12 +27,42 @@ public sealed class AocWriteGuardTests
} }
"""); """);
var result = Guard.Validate(document.RootElement); var result = Guard.Validate(document.RootElement);
Assert.True(result.IsValid); Assert.True(result.IsValid);
Assert.Empty(result.Violations); Assert.Empty(result.Violations);
} }
[Fact]
public void Validate_AllowsLinksAndAdvisoryKey_ByDefault()
{
using var document = JsonDocument.Parse("""
{
"tenant": "default",
"source": {"vendor": "osv"},
"upstream": {
"upstream_id": "GHSA-xxxx",
"content_hash": "sha256:abc",
"signature": { "present": false }
},
"content": {
"format": "OSV",
"raw": {"id": "GHSA-xxxx"}
},
"linkset": {},
"links": [
{ "scheme": "cve", "value": "CVE-2025-0001" }
],
"advisory_key": "ghsa-xxxx"
}
""");
var result = Guard.Validate(document.RootElement);
Assert.True(result.IsValid);
Assert.Empty(result.Violations);
}
[Fact] [Fact]
public void Validate_FlagsMissingTenant() public void Validate_FlagsMissingTenant()
{ {

View File

@@ -5,6 +5,7 @@
> Remark (2025-10-19): Wave 0 prerequisites reviewed (none outstanding); ATTESTOR-API-11-201, ATTESTOR-VERIFY-11-202, and ATTESTOR-OBS-11-203 tracked as DOING per Wave 0A kickoff. > Remark (2025-10-19): Wave 0 prerequisites reviewed (none outstanding); ATTESTOR-API-11-201, ATTESTOR-VERIFY-11-202, and ATTESTOR-OBS-11-203 tracked as DOING per Wave 0A kickoff.
> Remark (2025-10-19): Dual-log submissions, signature/proof verification, and observability hardening landed; attestor endpoints now rate-limited per client with correlation-ID logging and updated docs/tests. > Remark (2025-10-19): Dual-log submissions, signature/proof verification, and observability hardening landed; attestor endpoints now rate-limited per client with correlation-ID logging and updated docs/tests.
| ATTESTOR-CRYPTO-90-001 | TODO | Attestor Service Guild, Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Migrate bundle hashing, witness proof caching, and signing submissions to `ICryptoProviderRegistry`/`ICryptoHash` so RootPack_RU deployments use CryptoPro or PKCS#11 per `docs/security/crypto-routing-audit-2025-11-07.md`. | Attestor services resolve registry providers; DSSE signing/verifying honors config profiles; tests cover default + `ru-offline` modes; docs updated. |
--- ---

View File

@@ -2,9 +2,9 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
| SEC2.PLG | BLOCKED (2025-10-21) | Security Guild, Storage Guild | SEC2.A (audit contract) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 to stabilise Authority auth surfaces before final verification + publish. | ✅ Serilog events enriched with subject/client/IP/outcome; ✅ Mongo records written per attempt; ✅ Tests assert success/lockout/failure cases. | | SEC2.PLG | BLOCKED (2025-10-21) | Security Guild, Storage Guild | SEC2.A (audit contract) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 to stabilise Authority auth surfaces (PLUGIN-DI-08-001 landed 2025-10-21). | ✅ Serilog events enriched with subject/client/IP/outcome; ✅ Mongo records written per attempt; ✅ Tests assert success/lockout/failure cases. |
| SEC3.PLG | BLOCKED (2025-10-21) | Security Guild, BE-Auth Plugin | CORE8, SEC3.A (rate limiter) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 so limiter telemetry contract matches final authority surface. | ✅ Audit record includes retry-after; ✅ Tests confirm lockout + limiter interplay. | | SEC3.PLG | BLOCKED (2025-10-21) | Security Guild, BE-Auth Plugin | CORE8, SEC3.A (rate limiter) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002; PLUGIN-DI-08-001 is done, limiter telemetry just awaits the updated Authority surface. | ✅ Audit record includes retry-after; ✅ Tests confirm lockout + limiter interplay. |
| SEC5.PLG | BLOCKED (2025-10-21) | Security Guild | SEC5.A (threat model) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation depends on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 outcomes. | ✅ Threat model lists plugin attack surfaces; ✅ Mitigation items filed. | | SEC5.PLG | BLOCKED (2025-10-21) | Security Guild | SEC5.A (threat model) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation now hinges on AUTH-DPOP-11-001 / AUTH-MTLS-11-002; scoped DI work is complete. | ✅ Threat model lists plugin attack surfaces; ✅ Mitigation items filed. |
| PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1PLG3 | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | ✅ Capability metadata + validation merged; ✅ Plugin guide updated with final copy & diagrams; ✅ Release notes mention new toggles. <br>⛔ Blocked awaiting Authority rate-limiter stream (CORE8/SEC3) to resume so doc updates reflect final limiter behaviour. | | PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1PLG3 | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | ✅ Capability metadata + validation merged; ✅ Plugin guide updated with final copy & diagrams; ✅ Release notes mention new toggles. <br>⛔ Blocked awaiting Authority rate-limiter stream (CORE8/SEC3) to resume so doc updates reflect final limiter behaviour. |
| PLG7.RFC | DONE (2025-11-03) | BE-Auth Plugin, Security Guild | PLG4 | Socialize LDAP plugin RFC (`docs/rfcs/authority-plugin-ldap.md`) and capture guild feedback. | ✅ Guild review sign-off recorded; ✅ Follow-up issues filed in module boards. | | PLG7.RFC | DONE (2025-11-03) | BE-Auth Plugin, Security Guild | PLG4 | Socialize LDAP plugin RFC (`docs/rfcs/authority-plugin-ldap.md`) and capture guild feedback. | ✅ Guild review sign-off recorded; ✅ Follow-up issues filed in module boards. |
| PLG7.IMPL-001 | DONE (2025-11-03) | BE-Auth Plugin | PLG7.RFC | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | ✅ Project + test harness build; ✅ Configuration bound & validated; ✅ Sample config updated. | | PLG7.IMPL-001 | DONE (2025-11-03) | BE-Auth Plugin | PLG7.RFC | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | ✅ Project + test harness build; ✅ Configuration bound & validated; ✅ Sample config updated. |

View File

@@ -1,5 +1,8 @@
using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading;
using StellaOps.Cryptography;
namespace StellaOps.Authority.Plugins.Abstractions; namespace StellaOps.Authority.Plugins.Abstractions;
@@ -8,18 +11,55 @@ namespace StellaOps.Authority.Plugins.Abstractions;
/// </summary> /// </summary>
public static class AuthoritySecretHasher public static class AuthoritySecretHasher
{ {
private static ICryptoHash? configuredHash;
private static string defaultAlgorithm = HashAlgorithms.Sha256;
/// <summary> /// <summary>
/// Computes a stable SHA-256 hash for the provided secret. /// Configures the shared crypto hash service used for secret hashing.
/// </summary> /// </summary>
public static string ComputeHash(string secret) public static void Configure(ICryptoHash hash, string? algorithmId = null)
{
ArgumentNullException.ThrowIfNull(hash);
Volatile.Write(ref configuredHash, hash);
if (!string.IsNullOrWhiteSpace(algorithmId))
{
defaultAlgorithm = NormalizeAlgorithm(algorithmId);
}
}
/// <summary>
/// Computes a stable hash for the provided secret using the configured crypto provider.
/// </summary>
public static string ComputeHash(string secret, string? algorithmId = null)
{ {
if (string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(secret))
{ {
return string.Empty; return string.Empty;
} }
var algorithm = string.IsNullOrWhiteSpace(algorithmId)
? defaultAlgorithm
: NormalizeAlgorithm(algorithmId);
var hasher = Volatile.Read(ref configuredHash);
if (hasher is not null)
{
var digest = hasher.ComputeHash(Encoding.UTF8.GetBytes(secret), algorithm);
return Convert.ToBase64String(digest);
}
if (!string.Equals(algorithm, HashAlgorithms.Sha256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Authority secret hasher is not configured for the requested algorithm.");
}
using var sha256 = SHA256.Create(); using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(secret)); var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(secret));
return Convert.ToBase64String(bytes); return Convert.ToBase64String(bytes);
} }
private static string NormalizeAlgorithm(string algorithmId)
=> string.IsNullOrWhiteSpace(algorithmId)
? HashAlgorithms.Sha256
: algorithmId.Trim().ToUpperInvariant();
} }

View File

@@ -74,6 +74,10 @@ public sealed class AuthorityTokenDocument
[BsonIgnoreIfNull] [BsonIgnoreIfNull]
public string? SenderKeyThumbprint { get; set; } public string? SenderKeyThumbprint { get; set; }
[BsonElement("senderCertificateHex")]
[BsonIgnoreIfNull]
public string? SenderCertificateHex { get; set; }
[BsonElement("senderNonce")] [BsonElement("senderNonce")]
[BsonIgnoreIfNull] [BsonIgnoreIfNull]
public string? SenderNonce { get; set; } public string? SenderNonce { get; set; }

View File

@@ -1,10 +1,12 @@
using System.Net; using System.Collections.Generic;
using System.Net.Http.Headers; using System.Net;
using System.Security.Claims; using System.Net.Http.Headers;
using System.Text.Encodings.Web; using System.Security.Claims;
using System.Text.Json; using System.Text.Encodings.Web;
using System.Linq; using System.Text.Json;
using Microsoft.AspNetCore.Authentication; using System.Linq;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
@@ -155,8 +157,8 @@ public sealed class ConsoleEndpointsTests
} }
[Fact] [Fact]
public async Task TokenIntrospect_FlagsInactive_WhenExpired() public async Task TokenIntrospect_FlagsInactive_WhenExpired()
{ {
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-31T12:00:00Z")); var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-31T12:00:00Z"));
var sink = new RecordingAuthEventSink(); var sink = new RecordingAuthEventSink();
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>())); await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
@@ -189,8 +191,118 @@ public sealed class ConsoleEndpointsTests
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.token.introspect"); var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.token.introspect");
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome); Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
Assert.Equal(2, events.Count); Assert.Equal(2, events.Count);
} }
[Fact]
public async Task VulnerabilityFindings_ReturnsSamplePayload()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
var sink = new RecordingAuthEventSink();
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
accessor.Principal = CreatePrincipal(
tenant: "tenant-default",
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.VexRead },
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
var client = app.CreateTestClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
var response = await client.GetAsync("/console/vuln/findings?severity=high");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var items = json.RootElement.GetProperty("items");
Assert.True(items.GetArrayLength() >= 1);
Assert.Equal("CVE-2024-12345", items[0].GetProperty("coordinates").GetProperty("advisoryId").GetString());
}
[Fact]
public async Task VulnerabilityFindingDetail_ReturnsExpandedDocument()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
var sink = new RecordingAuthEventSink();
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
accessor.Principal = CreatePrincipal(
tenant: "tenant-default",
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.VexRead },
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
var client = app.CreateTestClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
var response = await client.GetAsync("/console/vuln/tenant-default:advisory-ai:sha256:5d1a");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var summary = json.RootElement.GetProperty("summary");
Assert.Equal("tenant-default:advisory-ai:sha256:5d1a", summary.GetProperty("findingId").GetString());
Assert.Equal("reachable", summary.GetProperty("reachability").GetProperty("status").GetString());
var detailReachability = json.RootElement.GetProperty("reachability");
Assert.Equal("reachable", detailReachability.GetProperty("status").GetString());
}
[Fact]
public async Task VulnerabilityTicket_ReturnsDeterministicPayload()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
var sink = new RecordingAuthEventSink();
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
accessor.Principal = CreatePrincipal(
tenant: "tenant-default",
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.VexRead },
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
var client = app.CreateTestClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
var payload = new ConsoleVulnerabilityTicketRequest(
Selection: new[] { "tenant-default:advisory-ai:sha256:5d1a" },
TargetSystem: "servicenow",
Metadata: new Dictionary<string, string> { ["assignmentGroup"] = "runtime-security" });
var response = await client.PostAsJsonAsync("/console/vuln/tickets", payload);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.StartsWith("console-ticket::tenant-default::", json.RootElement.GetProperty("ticketId").GetString());
Assert.Equal("servicenow", payload.TargetSystem);
}
[Fact]
public async Task VexStatements_ReturnsSampleRows()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
var sink = new RecordingAuthEventSink();
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
accessor.Principal = CreatePrincipal(
tenant: "tenant-default",
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.VexRead },
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
var client = app.CreateTestClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
var response = await client.GetAsync("/console/vex/statements?advisoryId=CVE-2024-12345");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var items = json.RootElement.GetProperty("items");
Assert.True(items.GetArrayLength() >= 1);
Assert.Equal("CVE-2024-12345", items[0].GetProperty("advisoryId").GetString());
}
private static ClaimsPrincipal CreatePrincipal( private static ClaimsPrincipal CreatePrincipal(
string tenant, string tenant,
@@ -259,9 +371,10 @@ public sealed class ConsoleEndpointsTests
builder.Services.AddSingleton<TimeProvider>(timeProvider); builder.Services.AddSingleton<TimeProvider>(timeProvider);
builder.Services.AddSingleton<IAuthEventSink>(sink); builder.Services.AddSingleton<IAuthEventSink>(sink);
builder.Services.AddSingleton<IAuthorityTenantCatalog>(new FakeTenantCatalog(tenants)); builder.Services.AddSingleton<IAuthorityTenantCatalog>(new FakeTenantCatalog(tenants));
builder.Services.AddSingleton<TestPrincipalAccessor>(); builder.Services.AddSingleton<TestPrincipalAccessor>();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<StellaOpsBypassEvaluator>(); builder.Services.AddSingleton<StellaOpsBypassEvaluator>();
builder.Services.AddSingleton<IConsoleWorkspaceService, ConsoleWorkspaceSampleService>();
var authBuilder = builder.Services.AddAuthentication(options => var authBuilder = builder.Services.AddAuthentication(options =>
{ {

View File

@@ -177,16 +177,17 @@ public sealed class TokenPersistenceIntegrationTests
var auditSink = new TestAuthEventSink(); var auditSink = new TestAuthEventSink();
await using var scope = provider.CreateAsyncScope(); await using var scope = provider.CreateAsyncScope();
var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>(); var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>();
var handler = new ValidateAccessTokenHandler( var handler = new ValidateAccessTokenHandler(
tokenStore, tokenStore,
sessionAccessor, sessionAccessor,
clientStore, clientStore,
registry, registry,
metadataAccessor, metadataAccessor,
auditSink, auditSink,
clock, clock,
TestActivitySource, TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance); TestInstruments.Meter,
NullLogger<ValidateAccessTokenHandler>.Instance);
var transaction = new OpenIddictServerTransaction var transaction = new OpenIddictServerTransaction
{ {

View File

@@ -30,8 +30,10 @@ public sealed class AuthorityJwksServiceTests
var registry = new TestRegistry(provider); var registry = new TestRegistry(provider);
using var cache = new MemoryCache(new MemoryCacheOptions()); using var cache = new MemoryCache(new MemoryCacheOptions());
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z")); var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z"));
var hash = CryptoHashFactory.CreateDefault();
var service = new AuthorityJwksService( var service = new AuthorityJwksService(
registry, registry,
hash,
NullLogger<AuthorityJwksService>.Instance, NullLogger<AuthorityJwksService>.Instance,
cache, cache,
clock, clock,
@@ -64,8 +66,10 @@ public sealed class AuthorityJwksServiceTests
var registry = new TestRegistry(provider); var registry = new TestRegistry(provider);
using var cache = new MemoryCache(new MemoryCacheOptions()); using var cache = new MemoryCache(new MemoryCacheOptions());
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z")); var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z"));
var hash = CryptoHashFactory.CreateDefault();
var service = new AuthorityJwksService( var service = new AuthorityJwksService(
registry, registry,
hash,
NullLogger<AuthorityJwksService>.Instance, NullLogger<AuthorityJwksService>.Instance,
cache, cache,
clock, clock,

View File

@@ -48,7 +48,7 @@ public sealed class KmsAuthoritySigningKeySourceTests
var signingKey = source.Load(request); var signingKey = source.Load(request);
Assert.Equal(CryptoSigningKeyKind.Raw, signingKey.Kind); Assert.Equal(CryptoSigningKeyKind.Raw, signingKey.Kind);
Assert.Equal(material.KeyId, signingKey.Reference.KeyId); Assert.Equal(request.KeyId, signingKey.Reference.KeyId);
Assert.True(signingKey.PrivateKey.Length > 0); Assert.True(signingKey.PrivateKey.Length > 0);
Assert.True(signingKey.PublicKey.Length > 0); Assert.True(signingKey.PublicKey.Length > 0);
Assert.Equal(material.VersionId, signingKey.Metadata[KmsAuthoritySigningKeySource.KmsMetadataKeys.Version]); Assert.Equal(material.VersionId, signingKey.Metadata[KmsAuthoritySigningKeySource.KmsMetadataKeys.Version]);

View File

@@ -37,10 +37,41 @@ internal static class ConsoleEndpointExtensions
.WithName("ConsoleProfile") .WithName("ConsoleProfile")
.WithSummary("Return the authenticated principal profile metadata."); .WithSummary("Return the authenticated principal profile metadata.");
group.MapPost("/token/introspect", IntrospectToken) group.MapPost("/token/introspect", IntrospectToken)
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.UiRead)) .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.UiRead))
.WithName("ConsoleTokenIntrospect") .WithName("ConsoleTokenIntrospect")
.WithSummary("Introspect the current access token and return expiry, scope, and tenant metadata."); .WithSummary("Introspect the current access token and return expiry, scope, and tenant metadata.");
var vulnGroup = group.MapGroup("/vuln")
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(
StellaOpsScopes.UiRead,
StellaOpsScopes.AdvisoryRead,
StellaOpsScopes.VexRead));
vulnGroup.MapGet("/findings", GetVulnerabilityFindings)
.WithName("ConsoleVulnerabilityFindings")
.WithSummary("List tenant-scoped vulnerability findings with policy/VEX metadata.");
vulnGroup.MapGet("/{findingId}", GetVulnerabilityFindingById)
.WithName("ConsoleVulnerabilityFindingDetail")
.WithSummary("Return the full finding document, including evidence and policy overlays.");
vulnGroup.MapPost("/tickets", CreateVulnerabilityTicket)
.WithName("ConsoleVulnerabilityTickets")
.WithSummary("Generate a signed payload payload for external ticketing workflows.");
var vexGroup = group.MapGroup("/vex")
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(
StellaOpsScopes.UiRead,
StellaOpsScopes.VexRead));
vexGroup.MapGet("/statements", GetVexStatements)
.WithName("ConsoleVexStatements")
.WithSummary("List VEX statements impacting the tenant.");
vexGroup.MapGet("/events", StreamVexEvents)
.WithName("ConsoleVexEvents")
.WithSummary("Server-sent events feed for live VEX updates (placeholder).");
} }
private static async Task<IResult> GetTenants( private static async Task<IResult> GetTenants(
@@ -134,11 +165,11 @@ internal static class ConsoleEndpointExtensions
return Results.Ok(profile); return Results.Ok(profile);
} }
private static async Task<IResult> IntrospectToken( private static async Task<IResult> IntrospectToken(
HttpContext httpContext, HttpContext httpContext,
TimeProvider timeProvider, TimeProvider timeProvider,
IAuthEventSink auditSink, IAuthEventSink auditSink,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(timeProvider);
@@ -152,21 +183,214 @@ internal static class ConsoleEndpointExtensions
var introspection = BuildTokenIntrospection(principal, timeProvider); var introspection = BuildTokenIntrospection(principal, timeProvider);
await WriteAuditAsync( await WriteAuditAsync(
httpContext, httpContext,
auditSink, auditSink,
timeProvider, timeProvider,
"authority.console.token.introspect", "authority.console.token.introspect",
AuthEventOutcome.Success, AuthEventOutcome.Success,
null, null,
BuildProperties( BuildProperties(
("token.active", introspection.Active ? "true" : "false"), ("token.active", introspection.Active ? "true" : "false"),
("token.expires_at", FormatInstant(introspection.ExpiresAt)), ("token.expires_at", FormatInstant(introspection.ExpiresAt)),
("tenant.resolved", introspection.Tenant)), ("tenant.resolved", introspection.Tenant)),
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
return Results.Ok(introspection); return Results.Ok(introspection);
} }
private static async Task<IResult> GetVulnerabilityFindings(
HttpContext httpContext,
IConsoleWorkspaceService workspaceService,
TimeProvider timeProvider,
IAuthEventSink auditSink,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(workspaceService);
var tenant = TenantHeaderFilter.GetTenant(httpContext);
if (string.IsNullOrWhiteSpace(tenant))
{
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.findings",
AuthEventOutcome.Failure,
"tenant_header_missing",
BuildProperties(("tenant.header", null)),
cancellationToken).ConfigureAwait(false);
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
}
var query = BuildVulnerabilityQuery(httpContext.Request);
var response = await workspaceService.SearchFindingsAsync(tenant, query, cancellationToken).ConfigureAwait(false);
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.findings",
AuthEventOutcome.Success,
null,
BuildProperties(("tenant.resolved", tenant), ("pagination.next_token", response.NextPageToken)),
cancellationToken).ConfigureAwait(false);
return Results.Ok(response);
}
private static async Task<IResult> GetVulnerabilityFindingById(
HttpContext httpContext,
string findingId,
IConsoleWorkspaceService workspaceService,
TimeProvider timeProvider,
IAuthEventSink auditSink,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(workspaceService);
var tenant = TenantHeaderFilter.GetTenant(httpContext);
if (string.IsNullOrWhiteSpace(tenant))
{
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.finding",
AuthEventOutcome.Failure,
"tenant_header_missing",
BuildProperties(("tenant.header", null)),
cancellationToken).ConfigureAwait(false);
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
}
var detail = await workspaceService.GetFindingAsync(tenant, findingId, cancellationToken).ConfigureAwait(false);
if (detail is null)
{
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.finding",
AuthEventOutcome.Failure,
"finding_not_found",
BuildProperties(("tenant.resolved", tenant), ("finding.id", findingId)),
cancellationToken).ConfigureAwait(false);
return Results.NotFound(new { error = "finding_not_found", message = $"Finding '{findingId}' not found." });
}
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.finding",
AuthEventOutcome.Success,
null,
BuildProperties(("tenant.resolved", tenant), ("finding.id", findingId)),
cancellationToken).ConfigureAwait(false);
return Results.Ok(detail);
}
private static async Task<IResult> CreateVulnerabilityTicket(
HttpContext httpContext,
ConsoleVulnerabilityTicketRequest request,
IConsoleWorkspaceService workspaceService,
TimeProvider timeProvider,
IAuthEventSink auditSink,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(workspaceService);
if (request is null || request.Selection.Count == 0)
{
return Results.BadRequest(new { error = "invalid_request", message = "At least one finding must be selected." });
}
var tenant = TenantHeaderFilter.GetTenant(httpContext);
if (string.IsNullOrWhiteSpace(tenant))
{
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.ticket",
AuthEventOutcome.Failure,
"tenant_header_missing",
BuildProperties(("tenant.header", null)),
cancellationToken).ConfigureAwait(false);
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
}
var ticket = await workspaceService.CreateTicketAsync(tenant, request, cancellationToken).ConfigureAwait(false);
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vuln.ticket",
AuthEventOutcome.Success,
null,
BuildProperties(
("tenant.resolved", tenant),
("ticket.id", ticket.TicketId),
("ticket.selection.count", request.Selection.Count.ToString(CultureInfo.InvariantCulture))),
cancellationToken).ConfigureAwait(false);
return Results.Ok(ticket);
}
private static async Task<IResult> GetVexStatements(
HttpContext httpContext,
IConsoleWorkspaceService workspaceService,
TimeProvider timeProvider,
IAuthEventSink auditSink,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(workspaceService);
var tenant = TenantHeaderFilter.GetTenant(httpContext);
if (string.IsNullOrWhiteSpace(tenant))
{
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vex.statements",
AuthEventOutcome.Failure,
"tenant_header_missing",
BuildProperties(("tenant.header", null)),
cancellationToken).ConfigureAwait(false);
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
}
var query = BuildVexQuery(httpContext.Request);
var response = await workspaceService.GetVexStatementsAsync(tenant, query, cancellationToken).ConfigureAwait(false);
await WriteAuditAsync(
httpContext,
auditSink,
timeProvider,
"authority.console.vex.statements",
AuthEventOutcome.Success,
null,
BuildProperties(("tenant.resolved", tenant), ("pagination.next_token", response.NextPageToken)),
cancellationToken).ConfigureAwait(false);
return Results.Ok(response);
}
private static IResult StreamVexEvents() =>
Results.StatusCode(StatusCodes.Status501NotImplemented);
private static ConsoleProfileResponse BuildProfile(ClaimsPrincipal principal, TimeProvider timeProvider) private static ConsoleProfileResponse BuildProfile(ClaimsPrincipal principal, TimeProvider timeProvider)
{ {
@@ -231,9 +455,9 @@ internal static class ConsoleEndpointExtensions
FreshAuth: freshAuth); FreshAuth: freshAuth);
} }
private static bool DetermineFreshAuth(ClaimsPrincipal principal, DateTimeOffset now) private static bool DetermineFreshAuth(ClaimsPrincipal principal, DateTimeOffset now)
{ {
var flag = principal.FindFirst("stellaops:fresh_auth") ?? principal.FindFirst("fresh_auth"); var flag = principal.FindFirst("stellaops:fresh_auth") ?? principal.FindFirst("fresh_auth");
if (flag is not null && bool.TryParse(flag.Value, out var freshFlag)) if (flag is not null && bool.TryParse(flag.Value, out var freshFlag))
{ {
if (freshFlag) if (freshFlag)
@@ -254,9 +478,67 @@ internal static class ConsoleEndpointExtensions
return authTime.Value.Add(ttl) > now; return authTime.Value.Add(ttl) > now;
} }
const int defaultFreshAuthWindowSeconds = 300; const int defaultFreshAuthWindowSeconds = 300;
return authTime.Value.AddSeconds(defaultFreshAuthWindowSeconds) > now; return authTime.Value.AddSeconds(defaultFreshAuthWindowSeconds) > now;
} }
private static ConsoleVulnerabilityQuery BuildVulnerabilityQuery(HttpRequest request)
{
var builder = new ConsoleVulnerabilityQueryBuilder()
.SetPageSize(ParseInt(request.Query["pageSize"], 50))
.SetPageToken(request.Query.TryGetValue("pageToken", out var tokenValues) ? tokenValues.FirstOrDefault() : null)
.AddSeverity(ReadMulti(request, "severity"))
.AddPolicyBadges(ReadMulti(request, "policyBadge"))
.AddReachability(ReadMulti(request, "reachability"))
.AddProducts(ReadMulti(request, "product"))
.AddVexStates(ReadMulti(request, "vexState"));
var search = request.Query.TryGetValue("search", out var searchValues)
? searchValues
.Where(value => !string.IsNullOrWhiteSpace(value))
.SelectMany(value => value!.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
: Array.Empty<string>();
builder.AddSearchTerms(search);
return builder.Build();
}
private static ConsoleVexQuery BuildVexQuery(HttpRequest request)
{
var builder = new ConsoleVexQueryBuilder()
.SetPageSize(ParseInt(request.Query["pageSize"], 50))
.SetPageToken(request.Query.TryGetValue("pageToken", out var pageValues) ? pageValues.FirstOrDefault() : null)
.AddAdvisories(ReadMulti(request, "advisoryId"))
.AddTypes(ReadMulti(request, "statementType"))
.AddStates(ReadMulti(request, "state"));
return builder.Build();
}
private static IEnumerable<string> ReadMulti(HttpRequest request, string key)
{
if (!request.Query.TryGetValue(key, out var values))
{
return Array.Empty<string>();
}
return values
.Where(value => !string.IsNullOrWhiteSpace(value))
.SelectMany(value => value!.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Where(value => value.Length > 0);
}
private static int ParseInt(StringValues values, int fallback)
{
if (values.Count == 0)
{
return fallback;
}
return int.TryParse(values[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var number)
? number
: fallback;
}
private static IReadOnlyList<string> ExtractRoles(ClaimsPrincipal principal) private static IReadOnlyList<string> ExtractRoles(ClaimsPrincipal principal)
{ {

View File

@@ -0,0 +1,304 @@
using System.Collections.Generic;
using System.Collections.Immutable;
namespace StellaOps.Authority.Console;
internal sealed record ConsoleFacetBucket(string Value, int Count);
internal sealed record ConsoleFacetDistribution(
IReadOnlyList<ConsoleFacetBucket> Severity,
IReadOnlyList<ConsoleFacetBucket> PolicyBadge,
IReadOnlyList<ConsoleFacetBucket> Reachability);
internal sealed record ConsoleFindingTimestamps(
DateTimeOffset FirstSeen,
DateTimeOffset LastSeen,
DateTimeOffset? VexLastUpdated);
internal sealed record ConsoleVulnCoordinates(
string AdvisoryId,
string Package,
string Component,
string Image);
internal sealed record ConsoleVulnVexSummary(
string StatementId,
string State,
string? Justification);
internal sealed record ConsoleReachabilitySummary(
string Status,
DateTimeOffset? LastObserved,
string? SignalsVersion);
internal sealed record ConsoleReachabilityDetail(
string Status,
IReadOnlyList<string> CallPathSamples,
DateTimeOffset? LastObserved,
string? SignalsVersion);
internal sealed record ConsoleVulnEvidenceSummary(
string? SbomDigest,
string? PolicyRunId,
string? AttestationId);
internal sealed record ConsoleEvidenceDetail(
ConsoleVulnEvidenceSummary Summary,
IReadOnlyList<string> ComponentPath,
IReadOnlyList<ConsoleAttestationReference> Attestations);
internal sealed record ConsoleAttestationReference(
string Type,
string AttestationId,
string Signer,
string BundleDigest);
internal sealed record ConsolePolicyBadge(
string PolicyId,
string Verdict,
string? ExplainUrl);
internal sealed record ConsoleVulnVexDetail(
string StatementId,
string State,
string Justification,
string? ImpactStatement,
IReadOnlyList<ConsoleRemediation> Remediations);
internal sealed record ConsoleRemediation(
string Type,
string Description,
DateTimeOffset? Deadline);
internal sealed record ConsoleVulnerabilityFinding(
string Tenant,
string FindingId,
ConsoleVulnCoordinates Coordinates,
string Summary,
string Severity,
double? Cvss,
bool Kev,
string PolicyBadge,
ConsoleVulnVexSummary? Vex,
ConsoleReachabilitySummary? Reachability,
ConsoleVulnEvidenceSummary? Evidence,
ConsoleFindingTimestamps Timestamps);
internal sealed record ConsoleVulnerabilityFindingDetail(
ConsoleVulnerabilityFinding Summary,
string Description,
IReadOnlyList<string> References,
IReadOnlyList<ConsolePolicyBadge> PolicyBadges,
ConsoleVulnVexDetail? Vex,
ConsoleReachabilityDetail? Reachability,
ConsoleEvidenceDetail Evidence);
internal sealed record ConsoleVulnerabilitySearchResponse(
IReadOnlyList<ConsoleVulnerabilityFinding> Items,
ConsoleFacetDistribution Facets,
string? NextPageToken);
internal sealed record ConsoleVulnerabilityQuery(
IReadOnlyList<string> Severity,
IReadOnlyList<string> PolicyBadges,
IReadOnlyList<string> Reachability,
IReadOnlyList<string> Products,
IReadOnlyList<string> VexStates,
IReadOnlyList<string> SearchTerms,
int PageSize,
string? PageToken);
internal sealed record ConsoleVulnerabilityTicketRequest(
IReadOnlyList<string> Selection,
string TargetSystem,
IReadOnlyDictionary<string, string>? Metadata);
internal sealed record ConsoleTicketPayload(
string Version,
string Tenant,
IReadOnlyList<ConsoleTicketSelection> Findings,
string PolicyBadge,
string VexSummary,
IReadOnlyList<ConsoleTicketAttachment> Attachments);
internal sealed record ConsoleTicketSelection(string FindingId, string Severity);
internal sealed record ConsoleTicketAttachment(
string Type,
string Name,
string Digest,
string ContentType,
DateTimeOffset ExpiresAt);
internal sealed record ConsoleVulnerabilityTicketResponse(
string TicketId,
ConsoleTicketPayload Payload,
string AuditEventId);
internal sealed record ConsoleVexStatementSummary(
string Tenant,
string StatementId,
string AdvisoryId,
string Product,
string State,
string Justification,
string StatementType,
DateTimeOffset LastUpdated,
ConsoleVexSourceMetadata Source);
internal sealed record ConsoleVexSourceMetadata(
string Type,
string? ModelBuild,
double? Confidence);
internal sealed record ConsoleVexStatementPage(
IReadOnlyList<ConsoleVexStatementSummary> Items,
string? NextPageToken);
internal sealed record ConsoleVexQuery(
IReadOnlyList<string> AdvisoryIds,
IReadOnlyList<string> StatementTypes,
IReadOnlyList<string> States,
int PageSize,
string? PageToken);
internal interface IConsoleWorkspaceService
{
Task<ConsoleVulnerabilitySearchResponse> SearchFindingsAsync(
string tenant,
ConsoleVulnerabilityQuery query,
CancellationToken cancellationToken);
Task<ConsoleVulnerabilityFindingDetail?> GetFindingAsync(
string tenant,
string findingId,
CancellationToken cancellationToken);
Task<ConsoleVulnerabilityTicketResponse> CreateTicketAsync(
string tenant,
ConsoleVulnerabilityTicketRequest request,
CancellationToken cancellationToken);
Task<ConsoleVexStatementPage> GetVexStatementsAsync(
string tenant,
ConsoleVexQuery query,
CancellationToken cancellationToken);
}
internal sealed class ConsoleVulnerabilityQueryBuilder
{
private readonly HashSet<string> _severity = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _policy = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _reachability = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _products = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _vexStates = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _searchTerms = new(StringComparer.OrdinalIgnoreCase);
private int _pageSize = 50;
private string? _pageToken;
public ConsoleVulnerabilityQueryBuilder SetPageSize(int value)
{
_pageSize = Math.Clamp(value, 1, 200);
return this;
}
public ConsoleVulnerabilityQueryBuilder SetPageToken(string? token)
{
_pageToken = string.IsNullOrWhiteSpace(token) ? null : token;
return this;
}
public ConsoleVulnerabilityQueryBuilder AddSeverity(IEnumerable<string> values)
{
_severity.UnionWith(values);
return this;
}
public ConsoleVulnerabilityQueryBuilder AddPolicyBadges(IEnumerable<string> values)
{
_policy.UnionWith(values);
return this;
}
public ConsoleVulnerabilityQueryBuilder AddReachability(IEnumerable<string> values)
{
_reachability.UnionWith(values);
return this;
}
public ConsoleVulnerabilityQueryBuilder AddProducts(IEnumerable<string> values)
{
_products.UnionWith(values);
return this;
}
public ConsoleVulnerabilityQueryBuilder AddVexStates(IEnumerable<string> values)
{
_vexStates.UnionWith(values);
return this;
}
public ConsoleVulnerabilityQueryBuilder AddSearchTerms(IEnumerable<string> values)
{
_searchTerms.UnionWith(values);
return this;
}
public ConsoleVulnerabilityQuery Build() =>
new(
_severity.ToImmutableArray(),
_policy.ToImmutableArray(),
_reachability.ToImmutableArray(),
_products.ToImmutableArray(),
_vexStates.ToImmutableArray(),
_searchTerms.ToImmutableArray(),
_pageSize,
_pageToken);
}
internal sealed class ConsoleVexQueryBuilder
{
private readonly HashSet<string> _advisories = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _types = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _states = new(StringComparer.OrdinalIgnoreCase);
private int _pageSize = 50;
private string? _pageToken;
public ConsoleVexQueryBuilder SetPageSize(int value)
{
_pageSize = Math.Clamp(value, 1, 200);
return this;
}
public ConsoleVexQueryBuilder SetPageToken(string? token)
{
_pageToken = string.IsNullOrWhiteSpace(token) ? null : token;
return this;
}
public ConsoleVexQueryBuilder AddAdvisories(IEnumerable<string> values)
{
_advisories.UnionWith(values);
return this;
}
public ConsoleVexQueryBuilder AddTypes(IEnumerable<string> values)
{
_types.UnionWith(values);
return this;
}
public ConsoleVexQueryBuilder AddStates(IEnumerable<string> values)
{
_states.UnionWith(values);
return this;
}
public ConsoleVexQuery Build() =>
new(
_advisories.ToImmutableArray(),
_types.ToImmutableArray(),
_states.ToImmutableArray(),
_pageSize,
_pageToken);
}

View File

@@ -0,0 +1,364 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Authority.Console;
internal sealed class ConsoleWorkspaceSampleService : IConsoleWorkspaceService
{
private static readonly ImmutableArray<ConsoleVulnerabilityFindingDetail> SampleFindings;
private static readonly ImmutableArray<ConsoleVexStatementSummary> SampleStatements;
static ConsoleWorkspaceSampleService()
{
var finding1Summary = new ConsoleVulnerabilityFinding(
Tenant: "tenant-default",
FindingId: "tenant-default:advisory-ai:sha256:5d1a",
Coordinates: new ConsoleVulnCoordinates(
AdvisoryId: "CVE-2024-12345",
Package: "pkg:npm/jsonwebtoken@9.0.2",
Component: "jwt-auth-service",
Image: "registry.local/ops/auth:2025.10.0"),
Summary: "jsonwebtoken <10.0.0 allows algorithm downgrade.",
Severity: "high",
Cvss: 8.1,
Kev: true,
PolicyBadge: "fail",
Vex: new ConsoleVulnVexSummary(
StatementId: "vex:tenant-default:jwt-auth:5d1a",
State: "under_investigation",
Justification: "Advisory AI flagged reachable path via Scheduler run 42."),
Reachability: new ConsoleReachabilitySummary(
Status: "reachable",
LastObserved: DateTimeOffset.Parse("2025-11-07T23:11:04Z"),
SignalsVersion: "signals-2025.310.1"),
Evidence: new ConsoleVulnEvidenceSummary(
SbomDigest: "sha256:6c81f2bbd8bd7336f197f3f68fba2f76d7287dd1a5e2a0f0e9f14f23f3c2f917",
PolicyRunId: "policy-run::2025-11-07::ca9f",
AttestationId: "dsse://authority/attest/84a2"),
Timestamps: new ConsoleFindingTimestamps(
FirstSeen: DateTimeOffset.Parse("2025-10-31T04:22:18Z"),
LastSeen: DateTimeOffset.Parse("2025-11-07T23:16:51Z"),
VexLastUpdated: DateTimeOffset.Parse("2025-11-07T23:10:09Z")));
var finding2Summary = new ConsoleVulnerabilityFinding(
Tenant: "tenant-default",
FindingId: "tenant-default:advisory-ai:sha256:9bf4",
Coordinates: new ConsoleVulnCoordinates(
AdvisoryId: "GHSA-xxxx-yyyy-zzzz",
Package: "pkg:docker/library/nginx@1.25.2",
Component: "ingress-gateway",
Image: "registry.local/ops/ingress:2025.09.1"),
Summary: "Heap overflow in nginx HTTP/3 parsing.",
Severity: "critical",
Cvss: 9.8,
Kev: false,
PolicyBadge: "warn",
Vex: new ConsoleVulnVexSummary(
StatementId: "vex:tenant-default:ingress:9bf4",
State: "not_affected",
Justification: "component_not_present"),
Reachability: new ConsoleReachabilitySummary(
Status: "unknown",
LastObserved: null,
SignalsVersion: "signals-2025.309.0"),
Evidence: new ConsoleVulnEvidenceSummary(
SbomDigest: "sha256:99f1e2a7aa0f7c970dcb6674244f0bfb5f37148e3ee09fd4f925d3358dea2239",
PolicyRunId: "policy-run::2025-11-06::b210",
AttestationId: "dsse://authority/attest/1d34"),
Timestamps: new ConsoleFindingTimestamps(
FirstSeen: DateTimeOffset.Parse("2025-10-29T18:03:11Z"),
LastSeen: DateTimeOffset.Parse("2025-11-07T10:45:03Z"),
VexLastUpdated: DateTimeOffset.Parse("2025-11-06T18:44:00Z")));
SampleFindings = ImmutableArray.Create(
new ConsoleVulnerabilityFindingDetail(
Summary: finding1Summary,
Description: "jsonwebtoken accepts untrusted algorithm overrides which allow downgrade attacks.",
References: new[]
{
"https://nvd.nist.gov/vuln/detail/CVE-2024-12345",
"https://github.com/auth0/node-jsonwebtoken/security/advisories/GHSA-45mw-4jw3-g2wg"
},
PolicyBadges: new[]
{
new ConsolePolicyBadge("policy://tenant-default/runtime-hardening", "fail", "https://console.local/policy/runs/policy-run::2025-11-07::ca9f")
},
Vex: new ConsoleVulnVexDetail(
StatementId: "vex:tenant-default:jwt-auth:5d1a",
State: "under_investigation",
Justification: "Runtime telemetry confirmed exploitation path.",
ImpactStatement: "Token exchange service remains exposed until patch 2025.11.2.",
Remediations: new[]
{
new ConsoleRemediation("patch", "Upgrade jwt-auth-service to 2025.11.2.", DateTimeOffset.Parse("2025-11-12T00:00:00Z"))
}),
Reachability: new ConsoleReachabilityDetail(
Status: "reachable",
CallPathSamples: new[]
{
"api-gateway -> jwt-auth-service -> jsonwebtoken.verify"
},
LastObserved: DateTimeOffset.Parse("2025-11-07T23:11:04Z"),
SignalsVersion: "signals-2025.310.1"),
Evidence: new ConsoleEvidenceDetail(
Summary: finding1Summary.Evidence!,
ComponentPath: new[]
{
"/src/jwt-auth/package.json",
"/src/jwt-auth/node_modules/jsonwebtoken"
},
Attestations: new[]
{
new ConsoleAttestationReference("scan-report", "dsse://authority/attest/84a2", "attestor@stella-ops.org", "sha256:e2bb5c7a0a8b2d16ff42e7f8decb4bb8be71ad0a1606dbc5d28be43675fbad32")
})),
new ConsoleVulnerabilityFindingDetail(
Summary: finding2Summary,
Description: "nginx HTTP/3 heap overflow affecting unpatched ingress nodes.",
References: new[]
{
"https://security.nginx.org/announcements/2024/http3-overflow",
},
PolicyBadges: new[]
{
new ConsolePolicyBadge("policy://tenant-default/network-hardening", "warn", null)
},
Vex: new ConsoleVulnVexDetail(
StatementId: "vex:tenant-default:ingress:9bf4",
State: "not_affected",
Justification: "component_not_present",
ImpactStatement: "HTTP/3 disabled on ingress, exposure limited.",
Remediations: Array.Empty<ConsoleRemediation>()),
Reachability: new ConsoleReachabilityDetail(
Status: "unknown",
CallPathSamples: Array.Empty<string>(),
LastObserved: null,
SignalsVersion: "signals-2025.309.0"),
Evidence: new ConsoleEvidenceDetail(
Summary: finding2Summary.Evidence!,
ComponentPath: new[]
{
"/charts/ingress/templates/deployment.yaml"
},
Attestations: new[]
{
new ConsoleAttestationReference("scan-report", "dsse://authority/attest/1d34", "attestor@stella-ops.org", "sha256:91e6dd2c1bbf9a4ac797e050d71bf7f1b958d1a0c27469364c44d8ed74bcb9dc")
})));
SampleStatements = ImmutableArray.Create(
new ConsoleVexStatementSummary(
Tenant: "tenant-default",
StatementId: "vex:tenant-default:jwt-auth:5d1a",
AdvisoryId: "CVE-2024-12345",
Product: "registry.local/ops/auth:2025.10.0",
State: "under_investigation",
Justification: "exploit_observed",
StatementType: "advisory_ai",
LastUpdated: DateTimeOffset.Parse("2025-11-07T23:10:09Z"),
Source: new ConsoleVexSourceMetadata("advisory_ai", "aiai-console-2025-10-28", 0.74)),
new ConsoleVexStatementSummary(
Tenant: "tenant-default",
StatementId: "vex:tenant-default:jwt-auth:5d1a",
AdvisoryId: "CVE-2024-12345",
Product: "registry.local/ops/auth:2025.10.0",
State: "fixed",
Justification: "solution_available",
StatementType: "advisory_ai",
LastUpdated: DateTimeOffset.Parse("2025-11-08T11:44:32Z"),
Source: new ConsoleVexSourceMetadata("advisory_ai", "aiai-console-2025-10-28", 0.92)),
new ConsoleVexStatementSummary(
Tenant: "tenant-default",
StatementId: "vex:tenant-default:ingress:9bf4",
AdvisoryId: "GHSA-xxxx-yyyy-zzzz",
Product: "registry.local/ops/ingress:2025.09.1",
State: "not_affected",
Justification: "component_not_present",
StatementType: "excitor",
LastUpdated: DateTimeOffset.Parse("2025-11-06T18:44:00Z"),
Source: new ConsoleVexSourceMetadata("excitor", null, null)));
}
public Task<ConsoleVulnerabilitySearchResponse> SearchFindingsAsync(
string tenant,
ConsoleVulnerabilityQuery query,
CancellationToken cancellationToken)
{
var filtered = SampleFindings
.Where(detail => IsTenantMatch(tenant, detail.Summary))
.Where(detail => MatchesSeverity(detail, query))
.Where(detail => MatchesPolicy(detail, query))
.Where(detail => MatchesReachability(detail, query))
.Where(detail => MatchesSearch(detail, query))
.Take(query.PageSize)
.Select(detail => detail.Summary)
.ToImmutableArray();
var facets = BuildFacets(tenant);
var response = new ConsoleVulnerabilitySearchResponse(filtered, facets, NextPageToken: null);
return Task.FromResult(response);
}
public Task<ConsoleVulnerabilityFindingDetail?> GetFindingAsync(
string tenant,
string findingId,
CancellationToken cancellationToken)
{
var detail = SampleFindings.FirstOrDefault(f =>
string.Equals(f.Summary.FindingId, findingId, StringComparison.OrdinalIgnoreCase) &&
IsTenantMatch(tenant, f.Summary));
return Task.FromResult(detail);
}
public Task<ConsoleVulnerabilityTicketResponse> CreateTicketAsync(
string tenant,
ConsoleVulnerabilityTicketRequest request,
CancellationToken cancellationToken)
{
ImmutableArray<ConsoleTicketSelection> selection;
if (string.IsNullOrWhiteSpace(tenant))
{
selection = ImmutableArray<ConsoleTicketSelection>.Empty;
}
else
{
selection = request.Selection
.Select(id => SampleFindings.FirstOrDefault(f => string.Equals(f.Summary.FindingId, id, StringComparison.OrdinalIgnoreCase)))
.Where(detail => detail is not null)
.Select(detail => new ConsoleTicketSelection(detail!.Summary.FindingId, detail.Summary.Severity))
.ToImmutableArray();
}
var ticketId = BuildTicketId(tenant, request.Selection);
var attachmentName = $"console-ticket-{DateTimeOffset.UtcNow:yyyyMMdd}.json";
var payload = new ConsoleTicketPayload(
Version: "2025-11-01",
Tenant: tenant,
Findings: selection,
PolicyBadge: selection.Any(sel => string.Equals(sel.Severity, "critical", StringComparison.OrdinalIgnoreCase)) ? "fail" : "warn",
VexSummary: $"{selection.Length} findings included in ticket.",
Attachments: new[]
{
new ConsoleTicketAttachment(
Type: "json",
Name: attachmentName,
Digest: HashAttachmentName(attachmentName),
ContentType: "application/json",
ExpiresAt: DateTimeOffset.UtcNow.AddDays(7))
});
var response = new ConsoleVulnerabilityTicketResponse(
TicketId: ticketId,
Payload: payload,
AuditEventId: $"{ticketId}::audit");
return Task.FromResult(response);
}
public Task<ConsoleVexStatementPage> GetVexStatementsAsync(
string tenant,
ConsoleVexQuery query,
CancellationToken cancellationToken)
{
var filtered = SampleStatements
.Where(statement => string.Equals(statement.Tenant, tenant, StringComparison.OrdinalIgnoreCase))
.Where(statement => MatchesAdvisory(statement, query))
.Where(statement => MatchesState(statement, query))
.Where(statement => MatchesType(statement, query))
.Take(query.PageSize)
.ToImmutableArray();
var page = new ConsoleVexStatementPage(filtered, NextPageToken: null);
return Task.FromResult(page);
}
private static bool MatchesSeverity(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query) =>
query.Severity.Count == 0 ||
query.Severity.Any(sev => string.Equals(sev, detail.Summary.Severity, StringComparison.OrdinalIgnoreCase));
private static bool MatchesPolicy(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query) =>
query.PolicyBadges.Count == 0 ||
query.PolicyBadges.Any(badge => string.Equals(badge, detail.Summary.PolicyBadge, StringComparison.OrdinalIgnoreCase));
private static bool MatchesReachability(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query)
{
if (query.Reachability.Count == 0 || detail.Summary.Reachability is null)
{
return query.Reachability.Count == 0;
}
return query.Reachability.Any(state =>
string.Equals(state, detail.Summary.Reachability.Status, StringComparison.OrdinalIgnoreCase));
}
private static bool MatchesSearch(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query)
{
if (query.SearchTerms.Count == 0)
{
return true;
}
return query.SearchTerms.Any(term =>
Contains(term, detail.Summary.FindingId) ||
Contains(term, detail.Summary.Coordinates.AdvisoryId) ||
Contains(term, detail.Summary.Coordinates.Component));
}
private static bool MatchesAdvisory(ConsoleVexStatementSummary summary, ConsoleVexQuery query) =>
query.AdvisoryIds.Count == 0 ||
query.AdvisoryIds.Any(advisory => string.Equals(advisory, summary.AdvisoryId, StringComparison.OrdinalIgnoreCase));
private static bool MatchesType(ConsoleVexStatementSummary summary, ConsoleVexQuery query) =>
query.StatementTypes.Count == 0 ||
query.StatementTypes.Any(type => string.Equals(type, summary.StatementType, StringComparison.OrdinalIgnoreCase));
private static bool MatchesState(ConsoleVexStatementSummary summary, ConsoleVexQuery query) =>
query.States.Count == 0 ||
query.States.Any(state => string.Equals(state, summary.State, StringComparison.OrdinalIgnoreCase));
private static bool Contains(string term, string value) =>
value?.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
private static bool IsTenantMatch(string tenant, ConsoleVulnerabilityFinding summary) =>
string.Equals(summary.Tenant, tenant, StringComparison.OrdinalIgnoreCase);
private static ConsoleFacetDistribution BuildFacets(string tenant)
{
var findings = SampleFindings.Where(detail => IsTenantMatch(tenant, detail.Summary)).Select(detail => detail.Summary);
return new ConsoleFacetDistribution(
Severity: AggregateFacet(findings, finding => finding.Severity),
PolicyBadge: AggregateFacet(findings, finding => finding.PolicyBadge),
Reachability: AggregateFacet(findings, finding => finding.Reachability?.Status ?? "unknown"));
}
private static IReadOnlyList<ConsoleFacetBucket> AggregateFacet(
IEnumerable<ConsoleVulnerabilityFinding> findings,
Func<ConsoleVulnerabilityFinding, string> selector) =>
findings
.GroupBy(selector, StringComparer.OrdinalIgnoreCase)
.OrderByDescending(group => group.Count())
.ThenBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(group => new ConsoleFacetBucket(group.Key ?? "unknown", group.Count()))
.ToImmutableArray();
private static string BuildTicketId(string tenant, IEnumerable<string> selection)
{
using var sha256 = SHA256.Create();
var joined = string.Join("|", selection.Order());
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(joined));
var prefix = Convert.ToHexString(hash[..8]).ToLowerInvariant();
return $"console-ticket::{tenant}::{prefix}";
}
private static string HashAttachmentName(string name)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(name));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -1,314 +1,319 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Globalization; using System.IO;
using System.Linq;
using System.Reflection; using System.Globalization;
using System.Security.Cryptography;
using System.Text; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Reflection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using StellaOps.Cryptography;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel; using System.Text;
using YamlDotNet.Serialization;
using System.Threading;
namespace StellaOps.Authority.OpenApi;
using System.Threading.Tasks;
internal sealed class AuthorityOpenApiDocumentProvider
{ using Microsoft.Extensions.Hosting;
private readonly string specificationPath;
private readonly ILogger<AuthorityOpenApiDocumentProvider> logger; using Microsoft.Extensions.Logging;
private readonly SemaphoreSlim refreshLock = new(1, 1);
private OpenApiDocumentSnapshot? cached; using YamlDotNet.Core;
public AuthorityOpenApiDocumentProvider(IWebHostEnvironment environment, ILogger<AuthorityOpenApiDocumentProvider> logger) using YamlDotNet.RepresentationModel;
{
ArgumentNullException.ThrowIfNull(environment); using YamlDotNet.Serialization;
ArgumentNullException.ThrowIfNull(logger);
specificationPath = Path.Combine(environment.ContentRootPath, "OpenApi", "authority.yaml");
this.logger = logger; namespace StellaOps.Authority.OpenApi;
}
public async ValueTask<OpenApiDocumentSnapshot> GetDocumentAsync(CancellationToken cancellationToken)
{ internal sealed class AuthorityOpenApiDocumentProvider
var lastWriteUtc = GetLastWriteTimeUtc();
var current = cached; {
if (current is not null && current.LastWriteUtc == lastWriteUtc)
{ private readonly string specificationPath;
return current;
} private readonly ILogger<AuthorityOpenApiDocumentProvider> logger;
await refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false); private readonly ICryptoHash hash;
try
{ private readonly SemaphoreSlim refreshLock = new(1, 1);
current = cached;
lastWriteUtc = GetLastWriteTimeUtc(); private OpenApiDocumentSnapshot? cached;
if (current is not null && current.LastWriteUtc == lastWriteUtc)
{
return current;
} public AuthorityOpenApiDocumentProvider(
var snapshot = LoadSnapshot(lastWriteUtc); IWebHostEnvironment environment,
cached = snapshot;
return snapshot; ILogger<AuthorityOpenApiDocumentProvider> logger,
}
finally ICryptoHash hash)
{
refreshLock.Release(); {
}
} ArgumentNullException.ThrowIfNull(environment);
private DateTime GetLastWriteTimeUtc() ArgumentNullException.ThrowIfNull(logger);
{
var file = new FileInfo(specificationPath); ArgumentNullException.ThrowIfNull(hash);
if (!file.Exists)
{
throw new FileNotFoundException($"Authority OpenAPI specification was not found at '{specificationPath}'.", specificationPath);
} specificationPath = Path.Combine(environment.ContentRootPath, "OpenApi", "authority.yaml");
return file.LastWriteTimeUtc; this.logger = logger;
}
this.hash = hash;
private OpenApiDocumentSnapshot LoadSnapshot(DateTime lastWriteUtc)
{ }
string yamlText;
try
{
yamlText = File.ReadAllText(specificationPath); public async ValueTask<OpenApiDocumentSnapshot> GetDocumentAsync(CancellationToken cancellationToken)
}
catch (Exception ex) {
{
logger.LogError(ex, "Failed to read Authority OpenAPI specification from {Path}.", specificationPath); var lastWriteUtc = GetLastWriteTimeUtc();
throw;
} var current = cached;
var yamlStream = new YamlStream(); if (current is not null && current.LastWriteUtc == lastWriteUtc)
using (var reader = new StringReader(yamlText))
{ {
yamlStream.Load(reader);
} return current;
if (yamlStream.Documents.Count == 0 || yamlStream.Documents[0].RootNode is not YamlMappingNode rootNode) }
{
throw new InvalidOperationException("Authority OpenAPI specification does not contain a valid root mapping node.");
}
await refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var (grants, scopes) = CollectGrantsAndScopes(rootNode);
try
if (!TryGetMapping(rootNode, "info", out var infoNode))
{ {
infoNode = new YamlMappingNode();
rootNode.Children[new YamlScalarNode("info")] = infoNode; current = cached;
}
lastWriteUtc = GetLastWriteTimeUtc();
var serviceName = "authority";
var buildVersion = ResolveBuildVersion(); if (current is not null && current.LastWriteUtc == lastWriteUtc)
ApplyInfoMetadata(infoNode, serviceName, buildVersion, grants, scopes);
{
var apiVersion = TryGetScalar(infoNode, "version", out var version)
? version return current;
: "0.0.0";
}
var updatedYaml = WriteYaml(yamlStream);
var json = ConvertYamlToJson(updatedYaml);
var etag = CreateStrongEtag(json);
var snapshot = LoadSnapshot(lastWriteUtc);
return new OpenApiDocumentSnapshot(
serviceName, cached = snapshot;
apiVersion,
buildVersion, return snapshot;
json,
updatedYaml, }
etag,
lastWriteUtc, finally
grants,
scopes); {
}
refreshLock.Release();
private static (IReadOnlyList<string> Grants, IReadOnlyList<string> Scopes) CollectGrantsAndScopes(YamlMappingNode root)
{ }
if (!TryGetMapping(root, "components", out var components) ||
!TryGetMapping(components, "securitySchemes", out var securitySchemes)) }
{
return (Array.Empty<string>(), Array.Empty<string>());
}
private DateTime GetLastWriteTimeUtc()
var grants = new SortedSet<string>(StringComparer.Ordinal);
var scopes = new SortedSet<string>(StringComparer.Ordinal); {
foreach (var scheme in securitySchemes.Children.Values.OfType<YamlMappingNode>()) var file = new FileInfo(specificationPath);
{
if (!TryGetMapping(scheme, "flows", out var flows)) if (!file.Exists)
{
continue; {
}
throw new FileNotFoundException($"Authority OpenAPI specification was not found at '{specificationPath}'.", specificationPath);
foreach (var flowEntry in flows.Children)
{ }
if (flowEntry.Key is not YamlScalarNode flowNameNode || flowEntry.Value is not YamlMappingNode flowMapping)
{
continue;
} return file.LastWriteTimeUtc;
var grant = NormalizeGrantName(flowNameNode.Value); }
if (grant is not null)
{
grants.Add(grant);
} private OpenApiDocumentSnapshot LoadSnapshot(DateTime lastWriteUtc)
if (TryGetMapping(flowMapping, "scopes", out var scopesMapping)) {
{
foreach (var scope in scopesMapping.Children.Keys.OfType<YamlScalarNode>()) string yamlText;
{
if (!string.IsNullOrWhiteSpace(scope.Value)) try
{
scopes.Add(scope.Value); {
}
} yamlText = File.ReadAllText(specificationPath);
}
}
if (flowMapping.Children.TryGetValue(new YamlScalarNode("refreshUrl"), out var refreshNode) &&
refreshNode is YamlScalarNode refreshScalar && !string.IsNullOrWhiteSpace(refreshScalar.Value)) catch (Exception ex)
{
grants.Add("refresh_token"); {
}
} logger.LogError(ex, "Failed to read Authority OpenAPI specification from {Path}.", specificationPath);
}
throw;
return (
grants.Count == 0 ? Array.Empty<string>() : grants.ToArray(), }
scopes.Count == 0 ? Array.Empty<string>() : scopes.ToArray());
}
private static string? NormalizeGrantName(string? flowName) var yamlStream = new YamlStream();
=> flowName switch
{ using (var reader = new StringReader(yamlText))
null or "" => null,
"authorizationCode" => "authorization_code", {
"clientCredentials" => "client_credentials",
"password" => "password", yamlStream.Load(reader);
"implicit" => "implicit",
"deviceCode" => "device_code", }
_ => flowName
};
private static void ApplyInfoMetadata( if (yamlStream.Documents.Count == 0 || yamlStream.Documents[0].RootNode is not YamlMappingNode rootNode)
YamlMappingNode infoNode,
string serviceName, {
string buildVersion,
IReadOnlyList<string> grants, throw new InvalidOperationException("Authority OpenAPI specification does not contain a valid root mapping node.");
IReadOnlyList<string> scopes)
{ }
infoNode.Children[new YamlScalarNode("x-stella-service")] = new YamlScalarNode(serviceName);
infoNode.Children[new YamlScalarNode("x-stella-build-version")] = new YamlScalarNode(buildVersion);
infoNode.Children[new YamlScalarNode("x-stella-grant-types")] = CreateSequence(grants);
infoNode.Children[new YamlScalarNode("x-stella-scopes")] = CreateSequence(scopes); var (grants, scopes) = CollectGrantsAndScopes(rootNode);
}
private static YamlSequenceNode CreateSequence(IEnumerable<string> values)
{ if (!TryGetMapping(rootNode, "info", out var infoNode))
var sequence = new YamlSequenceNode();
foreach (var value in values) {
{
sequence.Add(new YamlScalarNode(value)); infoNode = new YamlMappingNode();
}
rootNode.Children[new YamlScalarNode("info")] = infoNode;
return sequence;
} }
private static bool TryGetMapping(YamlMappingNode node, string key, out YamlMappingNode mapping)
{
foreach (var entry in node.Children) var serviceName = "authority";
{
if (entry.Key is YamlScalarNode scalar && string.Equals(scalar.Value, key, StringComparison.Ordinal)) var buildVersion = ResolveBuildVersion();
{
if (entry.Value is YamlMappingNode mappingNode) ApplyInfoMetadata(infoNode, serviceName, buildVersion, grants, scopes);
{
mapping = mappingNode;
return true;
} var apiVersion = TryGetScalar(infoNode, "version", out var version)
break; ? version
}
} : "0.0.0";
mapping = null!;
return false;
} var updatedYaml = WriteYaml(yamlStream);
private static bool TryGetScalar(YamlMappingNode node, string key, out string value) var json = ConvertYamlToJson(updatedYaml);
{
foreach (var entry in node.Children) var etag = CreateStrongEtag(json);
{
if (entry.Key is YamlScalarNode scalar && string.Equals(scalar.Value, key, StringComparison.Ordinal))
{
if (entry.Value is YamlScalarNode valueNode) return new OpenApiDocumentSnapshot(
{
value = valueNode.Value ?? string.Empty; serviceName,
return true;
} apiVersion,
break; buildVersion,
}
} json,
value = string.Empty; updatedYaml,
return false;
} etag,
private static string WriteYaml(YamlStream yamlStream) lastWriteUtc,
{
using var writer = new StringWriter(CultureInfo.InvariantCulture); grants,
yamlStream.Save(writer, assignAnchors: false);
return writer.ToString(); scopes);
}
}
private static string ConvertYamlToJson(string yaml)
{
var deserializer = new DeserializerBuilder().Build();
var yamlObject = deserializer.Deserialize(new StringReader(yaml)); private static (IReadOnlyList<string> Grants, IReadOnlyList<string> Scopes) CollectGrantsAndScopes(YamlMappingNode root)
var serializer = new SerializerBuilder() {
.JsonCompatible()
.Build(); if (!TryGetMapping(root, "components", out var components) ||
var json = serializer.Serialize(yamlObject); !TryGetMapping(components, "securitySchemes", out var securitySchemes))
return string.IsNullOrWhiteSpace(json) ? "{}" : json.Trim();
} {
private static string CreateStrongEtag(string jsonRepresentation) return (Array.Empty<string>(), Array.Empty<string>());
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(jsonRepresentation)); }
var hash = Convert.ToHexString(bytes).ToLowerInvariant();
return $"\"{hash}\"";
}
var grants = new SortedSet<string>(StringComparer.Ordinal);
private static string ResolveBuildVersion()
{ var scopes = new SortedSet<string>(StringComparer.Ordinal);
var assembly = typeof(AuthorityOpenApiDocumentProvider).Assembly;
var informational = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion; foreach (var scheme in securitySchemes.Children.Values.OfType<YamlMappingNode>())
if (!string.IsNullOrWhiteSpace(informational)) {
{
return informational!; if (!TryGetMapping(scheme, "flows", out var flows))
}
{
var version = assembly.GetName().Version;
return version?.ToString() ?? "unknown"; continue;
}
} }
internal sealed record OpenApiDocumentSnapshot(
string ServiceName,
string ApiVersion, foreach (var flowEntry in flows.Children)
string BuildVersion,
string Json, {
string Yaml,
string ETag, if (flowEntry.Key is not YamlScalarNode flowNameNode || flowEntry.Value is not YamlMappingNode flowMapping)
DateTime LastWriteUtc,
IReadOnlyList<string> GrantTypes, {
IReadOnlyList<string> Scopes);
continue;
}

View File

@@ -24,8 +24,10 @@ internal static class AuthorityOpenIddictConstants
internal const string DpopConsumedNonceProperty = "authority:dpop_nonce"; internal const string DpopConsumedNonceProperty = "authority:dpop_nonce";
internal const string ConfirmationClaimType = "cnf"; internal const string ConfirmationClaimType = "cnf";
internal const string SenderConstraintClaimType = "authority_sender_constraint"; internal const string SenderConstraintClaimType = "authority_sender_constraint";
internal const string SenderNonceClaimType = "authority_sender_nonce";
internal const string MtlsCertificateThumbprintProperty = "authority:mtls_thumbprint"; internal const string MtlsCertificateThumbprintProperty = "authority:mtls_thumbprint";
internal const string MtlsCertificateHexProperty = "authority:mtls_thumbprint_hex"; internal const string MtlsCertificateHexProperty = "authority:mtls_thumbprint_hex";
internal const string MtlsCertificateHexClaimType = "authority_sender_certificate_hex";
internal const string ClientTenantProperty = "authority:client_tenant"; internal const string ClientTenantProperty = "authority:client_tenant";
internal const string ClientProjectProperty = "authority:client_project"; internal const string ClientProjectProperty = "authority:client_project";
internal const string ClientAttributesProperty = "authority:client_attributes"; internal const string ClientAttributesProperty = "authority:client_attributes";

View File

@@ -1177,6 +1177,50 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
var extraProperties = new List<AuthEventProperty>(); var extraProperties = new List<AuthEventProperty>();
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.SenderConstraintProperty, out var auditSenderConstraintObj) &&
auditSenderConstraintObj is string auditSenderConstraint &&
!string.IsNullOrWhiteSpace(auditSenderConstraint))
{
extraProperties.Add(new AuthEventProperty
{
Name = "sender.constraint",
Value = ClassifiedString.Public(auditSenderConstraint)
});
}
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.DpopKeyThumbprintProperty, out var auditDpopThumbprintObj) &&
auditDpopThumbprintObj is string auditDpopThumbprint &&
!string.IsNullOrWhiteSpace(auditDpopThumbprint))
{
extraProperties.Add(new AuthEventProperty
{
Name = "sender.dpop.jkt",
Value = ClassifiedString.Sensitive(auditDpopThumbprint)
});
}
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateThumbprintProperty, out var auditMtlsThumbprintObj) &&
auditMtlsThumbprintObj is string auditMtlsThumbprint &&
!string.IsNullOrWhiteSpace(auditMtlsThumbprint))
{
extraProperties.Add(new AuthEventProperty
{
Name = "sender.mtls.x5t",
Value = ClassifiedString.Sensitive(auditMtlsThumbprint)
});
}
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateHexProperty, out var auditMtlsHexObj) &&
auditMtlsHexObj is string auditMtlsHex &&
!string.IsNullOrWhiteSpace(auditMtlsHex))
{
extraProperties.Add(new AuthEventProperty
{
Name = "sender.mtls.x5t_hex",
Value = ClassifiedString.Sensitive(auditMtlsHex)
});
}
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.OperatorReasonProperty, out var operatorReasonObj) && if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.OperatorReasonProperty, out var operatorReasonObj) &&
operatorReasonObj is string operatorReason && operatorReasonObj is string operatorReason &&
!string.IsNullOrWhiteSpace(operatorReason)) !string.IsNullOrWhiteSpace(operatorReason))
@@ -1873,6 +1917,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<
record.SenderKeyThumbprint = senderThumbprint; record.SenderKeyThumbprint = senderThumbprint;
} }
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateHexProperty, out var senderCertHexObj) &&
senderCertHexObj is string senderCertHex &&
!string.IsNullOrWhiteSpace(senderCertHex))
{
record.SenderCertificateHex = senderCertHex;
}
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientTenantProperty, out var tenantObj) && if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientTenantProperty, out var tenantObj) &&
tenantObj is string tenantValue && tenantObj is string tenantValue &&
!string.IsNullOrWhiteSpace(tenantValue)) !string.IsNullOrWhiteSpace(tenantValue))
@@ -1976,6 +2027,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<
identity.SetClaim(AuthorityOpenIddictConstants.ConfirmationClaimType, confirmation); identity.SetClaim(AuthorityOpenIddictConstants.ConfirmationClaimType, confirmation);
} }
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.DpopConsumedNonceProperty, out var nonceObj) &&
nonceObj is string consumedNonce &&
!string.IsNullOrWhiteSpace(consumedNonce))
{
identity.SetClaim(AuthorityOpenIddictConstants.SenderNonceClaimType, consumedNonce);
}
break; break;
case AuthoritySenderConstraintKinds.Mtls: case AuthoritySenderConstraintKinds.Mtls:
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateThumbprintProperty, out var mtlsThumbprintObj) && if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateThumbprintProperty, out var mtlsThumbprintObj) &&
@@ -1990,6 +2048,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<
identity.SetClaim(AuthorityOpenIddictConstants.ConfirmationClaimType, confirmation); identity.SetClaim(AuthorityOpenIddictConstants.ConfirmationClaimType, confirmation);
} }
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateHexProperty, out var mtlsHexObj) &&
mtlsHexObj is string mtlsHex &&
!string.IsNullOrWhiteSpace(mtlsHex))
{
identity.SetClaim(AuthorityOpenIddictConstants.MtlsCertificateHexClaimType, mtlsHex);
}
break; break;
} }
} }

View File

@@ -26,11 +26,13 @@ using Microsoft.IdentityModel.Tokens;
namespace StellaOps.Authority.OpenIddict.Handlers; namespace StellaOps.Authority.OpenIddict.Handlers;
internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenRequestContext> internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenRequestContext>
{ {
private readonly StellaOpsAuthorityOptions authorityOptions; private const string AnyDpopKeyThumbprint = "__authority_any_dpop_key__";
private readonly IAuthorityClientStore clientStore;
private readonly IDpopProofValidator proofValidator; private readonly StellaOpsAuthorityOptions authorityOptions;
private readonly IAuthorityClientStore clientStore;
private readonly IDpopProofValidator proofValidator;
private readonly IDpopNonceStore nonceStore; private readonly IDpopNonceStore nonceStore;
private readonly IAuthorityRateLimiterMetadataAccessor metadataAccessor; private readonly IAuthorityRateLimiterMetadataAccessor metadataAccessor;
private readonly IAuthEventSink auditSink; private readonly IAuthEventSink auditSink;
@@ -88,15 +90,34 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
return; return;
} }
var senderConstraint = NormalizeSenderConstraint(clientDocument); var senderConstraint = NormalizeSenderConstraint(clientDocument);
context.Transaction.Properties[AuthorityOpenIddictConstants.ClientSenderConstraintProperty] = senderConstraint; var configuredAudiences = EnsureRequestAudiences(context.Request, clientDocument);
var nonceOptions = senderConstraintOptions.Dpop.Nonce;
if (!string.Equals(senderConstraint, AuthoritySenderConstraintKinds.Dpop, StringComparison.Ordinal))
{ string? matchedNonceAudience = null;
return; if (senderConstraintOptions.Dpop.Enabled && nonceOptions.Enabled)
} {
matchedNonceAudience = ResolveNonceAudience(context.Request, nonceOptions, configuredAudiences);
var configuredAudiences = EnsureRequestAudiences(context.Request, clientDocument); }
var requiresClientSenderConstraint = string.Equals(senderConstraint, AuthoritySenderConstraintKinds.Dpop, StringComparison.Ordinal);
var requiresConfiguredAudience = senderConstraintOptions.Dpop.Enabled && matchedNonceAudience is not null;
var effectiveSenderConstraint = requiresClientSenderConstraint || requiresConfiguredAudience
? AuthoritySenderConstraintKinds.Dpop
: senderConstraint;
context.Transaction.Properties[AuthorityOpenIddictConstants.ClientSenderConstraintProperty] = effectiveSenderConstraint;
if (!requiresClientSenderConstraint && !requiresConfiguredAudience)
{
return;
}
if (requiresConfiguredAudience && !requiresClientSenderConstraint)
{
logger.LogDebug("DPoP enforcement enabled for client {ClientId} targeting audience {Audience}.", clientId, matchedNonceAudience);
}
if (!senderConstraintOptions.Dpop.Enabled) if (!senderConstraintOptions.Dpop.Enabled)
{ {
@@ -125,18 +146,18 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
return; return;
} }
if (!httpRequest.Headers.TryGetValue("DPoP", out StringValues proofHeader) || StringValues.IsNullOrEmpty(proofHeader)) if (!httpRequest.Headers.TryGetValue("DPoP", out StringValues proofHeader) || StringValues.IsNullOrEmpty(proofHeader))
{ {
logger.LogWarning("Missing DPoP header for client credentials request from {ClientId}.", clientId); logger.LogWarning("Missing DPoP header for client credentials request from {ClientId}.", clientId);
await ChallengeNonceAsync( await ChallengeNonceAsync(
context, context,
clientDocument, clientDocument,
audience: null, audience: matchedNonceAudience,
thumbprint: null, thumbprint: null,
reasonCode: "missing_proof", reasonCode: "missing_proof",
description: "DPoP proof is required.", description: "DPoP proof is required.",
senderConstraintOptions, senderConstraintOptions,
httpResponse).ConfigureAwait(false); httpResponse).ConfigureAwait(false);
return; return;
} }
@@ -150,36 +171,36 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
cancellationToken: context.CancellationToken).ConfigureAwait(false); cancellationToken: context.CancellationToken).ConfigureAwait(false);
if (!validationResult.IsValid) if (!validationResult.IsValid)
{ {
var error = string.IsNullOrWhiteSpace(validationResult.ErrorDescription) var error = string.IsNullOrWhiteSpace(validationResult.ErrorDescription)
? "DPoP proof validation failed." ? "DPoP proof validation failed."
: validationResult.ErrorDescription; : validationResult.ErrorDescription;
logger.LogWarning("DPoP proof validation failed for client {ClientId}: {Reason}.", clientId, error);
await ChallengeNonceAsync(
context,
clientDocument,
audience: matchedNonceAudience,
thumbprint: null,
reasonCode: validationResult.ErrorCode ?? "invalid_proof",
description: error,
senderConstraintOptions,
httpResponse).ConfigureAwait(false);
return;
}
logger.LogWarning("DPoP proof validation failed for client {ClientId}: {Reason}.", clientId, error); if (validationResult.PublicKey is not Microsoft.IdentityModel.Tokens.JsonWebKey jwk)
await ChallengeNonceAsync( {
context, logger.LogWarning("DPoP proof for {ClientId} did not expose a JSON Web Key.", clientId);
clientDocument, await ChallengeNonceAsync(
audience: null, context,
thumbprint: null, clientDocument,
reasonCode: validationResult.ErrorCode ?? "invalid_proof", audience: matchedNonceAudience,
description: error, thumbprint: null,
senderConstraintOptions, reasonCode: "invalid_key",
httpResponse).ConfigureAwait(false); description: "DPoP proof must embed a JSON Web Key.",
return; senderConstraintOptions,
} httpResponse).ConfigureAwait(false);
if (validationResult.PublicKey is not Microsoft.IdentityModel.Tokens.JsonWebKey jwk)
{
logger.LogWarning("DPoP proof for {ClientId} did not expose a JSON Web Key.", clientId);
await ChallengeNonceAsync(
context,
clientDocument,
audience: null,
thumbprint: null,
reasonCode: "invalid_key",
description: "DPoP proof must embed a JSON Web Key.",
senderConstraintOptions,
httpResponse).ConfigureAwait(false);
return; return;
} }
@@ -209,8 +230,7 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
context.Transaction.Properties[AuthorityOpenIddictConstants.DpopIssuedAtProperty] = issuedAt; context.Transaction.Properties[AuthorityOpenIddictConstants.DpopIssuedAtProperty] = issuedAt;
} }
var nonceOptions = senderConstraintOptions.Dpop.Nonce; var requiredAudience = matchedNonceAudience;
var requiredAudience = ResolveNonceAudience(context.Request, nonceOptions, configuredAudiences);
if (nonceOptions.Enabled && requiredAudience is not null) if (nonceOptions.Enabled && requiredAudience is not null)
{ {
@@ -232,12 +252,12 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
return; return;
} }
var consumeResult = await nonceStore.TryConsumeAsync( var consumeResult = await ConsumeNonceAsync(
suppliedNonce, suppliedNonce,
requiredAudience, requiredAudience,
clientDocument.ClientId, clientDocument,
thumbprint, thumbprint,
context.CancellationToken).ConfigureAwait(false); context.CancellationToken).ConfigureAwait(false);
switch (consumeResult.Status) switch (consumeResult.Status)
{ {
@@ -442,11 +462,11 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
return null; return null;
} }
private async ValueTask ChallengeNonceAsync( private async ValueTask ChallengeNonceAsync(
OpenIddictServerEvents.ValidateTokenRequestContext context, OpenIddictServerEvents.ValidateTokenRequestContext context,
AuthorityClientDocument clientDocument, AuthorityClientDocument clientDocument,
string? audience, string? audience,
string? thumbprint, string? thumbprint,
string reasonCode, string reasonCode,
string description, string description,
AuthoritySenderConstraintOptions senderConstraintOptions, AuthoritySenderConstraintOptions senderConstraintOptions,
@@ -455,20 +475,25 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
context.Reject(OpenIddictConstants.Errors.InvalidClient, description); context.Reject(OpenIddictConstants.Errors.InvalidClient, description);
metadataAccessor.SetTag("authority.dpop_result", reasonCode); metadataAccessor.SetTag("authority.dpop_result", reasonCode);
string? issuedNonce = null; string? issuedNonce = null;
DateTimeOffset? expiresAt = null; DateTimeOffset? expiresAt = null;
if (audience is not null && thumbprint is not null && senderConstraintOptions.Dpop.Nonce.Enabled) var nonceOptions = senderConstraintOptions.Dpop.Nonce;
{ if (audience is not null && nonceOptions.Enabled)
var issuance = await nonceStore.IssueAsync( {
audience, var issuanceThumbprint = string.IsNullOrWhiteSpace(thumbprint)
clientDocument.ClientId, ? AnyDpopKeyThumbprint
thumbprint, : thumbprint;
senderConstraintOptions.Dpop.Nonce.Ttl,
senderConstraintOptions.Dpop.Nonce.MaxIssuancePerMinute, var issuance = await nonceStore.IssueAsync(
context.CancellationToken).ConfigureAwait(false); audience,
clientDocument.ClientId,
if (issuance.Status == DpopNonceIssueStatus.Success) issuanceThumbprint,
{ nonceOptions.Ttl,
nonceOptions.MaxIssuancePerMinute,
context.CancellationToken).ConfigureAwait(false);
if (issuance.Status == DpopNonceIssueStatus.Success)
{
issuedNonce = issuance.Nonce; issuedNonce = issuance.Nonce;
expiresAt = issuance.ExpiresAt; expiresAt = issuance.ExpiresAt;
} }
@@ -488,20 +513,48 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
} }
} }
await WriteAuditAsync( await WriteAuditAsync(
context, context,
clientDocument, clientDocument,
AuthEventOutcome.Failure, AuthEventOutcome.Failure,
description, description,
thumbprint, thumbprint,
validationResult: null, validationResult: null,
audience, audience,
"authority.dpop.proof.challenge", "authority.dpop.proof.challenge",
reasonCode, reasonCode,
issuedNonce, issuedNonce,
expiresAt) expiresAt)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
private async ValueTask<DpopNonceConsumeResult> ConsumeNonceAsync(
string nonce,
string audience,
AuthorityClientDocument clientDocument,
string keyThumbprint,
CancellationToken cancellationToken)
{
var result = await nonceStore.TryConsumeAsync(
nonce,
audience,
clientDocument.ClientId,
keyThumbprint,
cancellationToken).ConfigureAwait(false);
if (result.Status == DpopNonceConsumeStatus.NotFound &&
!string.Equals(keyThumbprint, AnyDpopKeyThumbprint, StringComparison.Ordinal))
{
result = await nonceStore.TryConsumeAsync(
nonce,
audience,
clientDocument.ClientId,
AnyDpopKeyThumbprint,
cancellationToken).ConfigureAwait(false);
}
return result;
}
private static string BuildAuthenticateHeader(string reasonCode, string description, string? nonce) private static string BuildAuthenticateHeader(string reasonCode, string description, string? nonce)
{ {

View File

@@ -117,6 +117,18 @@ internal sealed class PersistTokensHandler : IOpenIddictServerHandler<OpenIddict
document.SenderConstraint = senderConstraint; document.SenderConstraint = senderConstraint;
} }
var senderNonce = principal.GetClaim(AuthorityOpenIddictConstants.SenderNonceClaimType);
if (!string.IsNullOrWhiteSpace(senderNonce))
{
document.SenderNonce = senderNonce;
}
var senderCertificateHex = principal.GetClaim(AuthorityOpenIddictConstants.MtlsCertificateHexClaimType);
if (!string.IsNullOrWhiteSpace(senderCertificateHex))
{
document.SenderCertificateHex = senderCertificateHex;
}
var serviceAccountId = principal.GetClaim(StellaOpsClaimTypes.ServiceAccount); var serviceAccountId = principal.GetClaim(StellaOpsClaimTypes.ServiceAccount);
if (!string.IsNullOrWhiteSpace(serviceAccountId)) if (!string.IsNullOrWhiteSpace(serviceAccountId))
{ {

View File

@@ -31,6 +31,7 @@ using StellaOps.Authority.Notifications.Ack;
using StellaOps.Authority.Plugins.Abstractions; using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugins; using StellaOps.Authority.Plugins;
using StellaOps.Authority.Bootstrap; using StellaOps.Authority.Bootstrap;
using StellaOps.Authority.Console;
using StellaOps.Authority.Storage.Mongo.Extensions; using StellaOps.Authority.Storage.Mongo.Extensions;
using StellaOps.Authority.Storage.Mongo.Initialization; using StellaOps.Authority.Storage.Mongo.Initialization;
using StellaOps.Authority.Storage.Mongo.Stores; using StellaOps.Authority.Storage.Mongo.Stores;
@@ -115,6 +116,8 @@ builder.Host.UseSerilog((context, _, loggerConfiguration) =>
}); });
var authorityOptions = authorityConfiguration.Options; var authorityOptions = authorityConfiguration.Options;
builder.Services.AddStellaOpsCrypto(authorityOptions.Crypto);
builder.Services.AddHostedService<AuthoritySecretHasherInitializer>();
var issuerUri = authorityOptions.Issuer; var issuerUri = authorityOptions.Issuer;
if (issuerUri is null) if (issuerUri is null)
{ {
@@ -138,6 +141,7 @@ builder.Services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, Defa
builder.Services.AddSingleton<IAuthorityClientCertificateValidator, AuthorityClientCertificateValidator>(); builder.Services.AddSingleton<IAuthorityClientCertificateValidator, AuthorityClientCertificateValidator>();
builder.Services.TryAddSingleton<IAuthorityAirgapAuditService, AuthorityAirgapAuditService>(); builder.Services.TryAddSingleton<IAuthorityAirgapAuditService, AuthorityAirgapAuditService>();
builder.Services.AddSingleton<AuthorityOpenApiDocumentProvider>(); builder.Services.AddSingleton<AuthorityOpenApiDocumentProvider>();
builder.Services.TryAddSingleton<IConsoleWorkspaceService, ConsoleWorkspaceSampleService>();
#if STELLAOPS_AUTH_SECURITY #if STELLAOPS_AUTH_SECURITY
var senderConstraints = authorityOptions.Security.SenderConstraints; var senderConstraints = authorityOptions.Security.SenderConstraints;
@@ -210,7 +214,6 @@ if (requiresKms)
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, KmsAuthoritySigningKeySource>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, KmsAuthoritySigningKeySource>());
} }
builder.Services.AddStellaOpsCrypto();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, FileAuthoritySigningKeySource>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, FileAuthoritySigningKeySource>());
builder.Services.AddSingleton<AuthoritySigningKeyManager>(); builder.Services.AddSingleton<AuthoritySigningKeyManager>();
builder.Services.AddSingleton<AuthorityAckTokenKeyManager>(); builder.Services.AddSingleton<AuthorityAckTokenKeyManager>();

View File

@@ -0,0 +1,40 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Configuration;
using StellaOps.Cryptography;
namespace StellaOps.Authority.Security;
internal sealed class AuthoritySecretHasherInitializer : IHostedService
{
private readonly ICryptoHash hash;
private readonly IOptions<StellaOpsAuthorityOptions> authorityOptions;
private readonly ILogger<AuthoritySecretHasherInitializer> logger;
public AuthoritySecretHasherInitializer(
ICryptoHash hash,
IOptions<StellaOpsAuthorityOptions> authorityOptions,
ILogger<AuthoritySecretHasherInitializer> logger)
{
this.hash = hash ?? throw new ArgumentNullException(nameof(hash));
this.authorityOptions = authorityOptions ?? throw new ArgumentNullException(nameof(authorityOptions));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public Task StartAsync(CancellationToken cancellationToken)
{
var options = authorityOptions.Value;
var algorithm = options?.Crypto?.DefaultHashAlgorithm;
AuthoritySecretHasher.Configure(hash, algorithm);
logger.LogInformation("Authority secret hasher configured with default algorithm {Algorithm}.",
string.IsNullOrWhiteSpace(algorithm) ? HashAlgorithms.Sha256 : algorithm);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -1,181 +1,183 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Globalization;
using System.Globalization; using System.Text;
using System.Text; using System.Text.Json;
using System.Text.Json; using System.Text.Json.Serialization;
using System.Text.Json.Serialization; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options; using StellaOps.Configuration;
using StellaOps.Configuration; using StellaOps.Cryptography;
using StellaOps.Cryptography;
namespace StellaOps.Authority.Signing;
namespace StellaOps.Authority.Signing;
internal sealed class AuthorityJwksService
internal sealed class AuthorityJwksService {
{ private const string CacheKey = "authority:jwks:current";
private const string CacheKey = "authority:jwks:current"; private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) {
{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
};
private readonly ICryptoProviderRegistry registry;
private readonly ICryptoProviderRegistry registry; private readonly ICryptoHash hash;
private readonly ILogger<AuthorityJwksService> logger; private readonly ILogger<AuthorityJwksService> logger;
private readonly IMemoryCache cache; private readonly IMemoryCache cache;
private readonly TimeProvider timeProvider; private readonly TimeProvider timeProvider;
private readonly StellaOpsAuthorityOptions authorityOptions; private readonly StellaOpsAuthorityOptions authorityOptions;
public AuthorityJwksService( public AuthorityJwksService(
ICryptoProviderRegistry registry, ICryptoProviderRegistry registry,
ILogger<AuthorityJwksService> logger, ICryptoHash hash,
IMemoryCache cache, ILogger<AuthorityJwksService> logger,
TimeProvider timeProvider, IMemoryCache cache,
IOptions<StellaOpsAuthorityOptions> authorityOptions) TimeProvider timeProvider,
{ IOptions<StellaOpsAuthorityOptions> authorityOptions)
this.registry = registry ?? throw new ArgumentNullException(nameof(registry)); {
this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); this.hash = hash ?? throw new ArgumentNullException(nameof(hash));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
if (authorityOptions is null) this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
{ this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
throw new ArgumentNullException(nameof(authorityOptions)); if (authorityOptions is null)
} {
throw new ArgumentNullException(nameof(authorityOptions));
this.authorityOptions = authorityOptions.Value ?? throw new ArgumentNullException(nameof(authorityOptions)); }
}
this.authorityOptions = authorityOptions.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
public AuthorityJwksResult Get() }
{
if (cache.TryGetValue(CacheKey, out AuthorityJwksCacheEntry? cached) && public AuthorityJwksResult Get()
cached is not null && {
cached.ExpiresAt > timeProvider.GetUtcNow()) if (cache.TryGetValue(CacheKey, out AuthorityJwksCacheEntry? cached) &&
{ cached is not null &&
return cached.Result; cached.ExpiresAt > timeProvider.GetUtcNow())
} {
return cached.Result;
var response = new AuthorityJwksResponse(BuildKeys()); }
var signingOptions = authorityOptions.Signing;
var lifetime = signingOptions.JwksCacheLifetime > TimeSpan.Zero var response = new AuthorityJwksResponse(BuildKeys());
? signingOptions.JwksCacheLifetime var signingOptions = authorityOptions.Signing;
: TimeSpan.FromMinutes(5); var lifetime = signingOptions.JwksCacheLifetime > TimeSpan.Zero
var expires = timeProvider.GetUtcNow().Add(lifetime); ? signingOptions.JwksCacheLifetime
var etag = ComputeEtag(response, expires); : TimeSpan.FromMinutes(5);
var cacheControl = $"public, max-age={(int)lifetime.TotalSeconds}"; var expires = timeProvider.GetUtcNow().Add(lifetime);
var etag = ComputeEtag(response, expires);
var result = new AuthorityJwksResult(response, etag, expires, cacheControl); var cacheControl = $"public, max-age={(int)lifetime.TotalSeconds}";
var entry = new AuthorityJwksCacheEntry(result, expires);
var result = new AuthorityJwksResult(response, etag, expires, cacheControl);
cache.Set(CacheKey, entry, new MemoryCacheEntryOptions var entry = new AuthorityJwksCacheEntry(result, expires);
{
AbsoluteExpirationRelativeToNow = lifetime cache.Set(CacheKey, entry, new MemoryCacheEntryOptions
}); {
return result; AbsoluteExpirationRelativeToNow = lifetime
} });
return result;
public void Invalidate() }
{
cache.Remove(CacheKey); public void Invalidate()
} {
cache.Remove(CacheKey);
private IReadOnlyCollection<JwksKeyEntry> BuildKeys() }
{
var keys = new List<JwksKeyEntry>(); private IReadOnlyCollection<JwksKeyEntry> BuildKeys()
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); {
var keys = new List<JwksKeyEntry>();
foreach (var provider in registry.Providers) var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
{
foreach (var signingKey in provider.GetSigningKeys()) foreach (var provider in registry.Providers)
{ {
var keyId = signingKey.Reference.KeyId; foreach (var signingKey in provider.GetSigningKeys())
if (!seen.Add(keyId)) {
{ var keyId = signingKey.Reference.KeyId;
continue; if (!seen.Add(keyId))
} {
continue;
try }
{
var signer = provider.GetSigner(signingKey.AlgorithmId, signingKey.Reference); try
var jwk = signer.ExportPublicJsonWebKey(); {
var keyUse = signingKey.Metadata.TryGetValue("use", out var metadataUse) && !string.IsNullOrWhiteSpace(metadataUse) var signer = provider.GetSigner(signingKey.AlgorithmId, signingKey.Reference);
? metadataUse var jwk = signer.ExportPublicJsonWebKey();
: jwk.Use; var keyUse = signingKey.Metadata.TryGetValue("use", out var metadataUse) && !string.IsNullOrWhiteSpace(metadataUse)
? metadataUse
if (string.IsNullOrWhiteSpace(keyUse)) : jwk.Use;
{
keyUse = "sig"; if (string.IsNullOrWhiteSpace(keyUse))
} {
keyUse = "sig";
var entry = new JwksKeyEntry }
{
Kid = jwk.Kid, var entry = new JwksKeyEntry
Kty = jwk.Kty, {
Use = keyUse, Kid = jwk.Kid,
Alg = jwk.Alg, Kty = jwk.Kty,
Crv = jwk.Crv, Use = keyUse,
X = jwk.X, Alg = jwk.Alg,
Y = jwk.Y, Crv = jwk.Crv,
Status = signingKey.Metadata.TryGetValue("status", out var status) ? status : "active" X = jwk.X,
}; Y = jwk.Y,
keys.Add(entry); Status = signingKey.Metadata.TryGetValue("status", out var status) ? status : "active"
} };
catch (Exception ex) keys.Add(entry);
{ }
logger.LogWarning(ex, "Failed to export JWKS entry for key {KeyId}.", keyId); catch (Exception ex)
} {
} logger.LogWarning(ex, "Failed to export JWKS entry for key {KeyId}.", keyId);
} }
}
keys.Sort(static (left, right) => string.Compare(left.Kid, right.Kid, StringComparison.Ordinal)); }
return keys;
} keys.Sort(static (left, right) => string.Compare(left.Kid, right.Kid, StringComparison.Ordinal));
return keys;
private static string ComputeEtag(AuthorityJwksResponse response, DateTimeOffset expiresAt) }
{
var payload = JsonSerializer.Serialize(response, SerializerOptions); private string ComputeEtag(AuthorityJwksResponse response, DateTimeOffset expiresAt)
var buffer = Encoding.UTF8.GetBytes(payload + "|" + expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)); {
var hash = SHA256.HashData(buffer); var payload = JsonSerializer.Serialize(response, SerializerOptions);
return $"\"{Convert.ToHexString(hash)}\""; var buffer = Encoding.UTF8.GetBytes(payload + "|" + expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
} var digest = hash.ComputeHash(buffer, HashAlgorithms.Sha256);
return $"\"{Convert.ToHexString(digest)}\"";
private sealed record AuthorityJwksCacheEntry(AuthorityJwksResult Result, DateTimeOffset ExpiresAt); }
}
private sealed record AuthorityJwksCacheEntry(AuthorityJwksResult Result, DateTimeOffset ExpiresAt);
internal sealed record AuthorityJwksResponse([property: JsonPropertyName("keys")] IReadOnlyCollection<JwksKeyEntry> Keys); }
internal sealed record AuthorityJwksResult( internal sealed record AuthorityJwksResponse([property: JsonPropertyName("keys")] IReadOnlyCollection<JwksKeyEntry> Keys);
AuthorityJwksResponse Response,
string ETag, internal sealed record AuthorityJwksResult(
DateTimeOffset ExpiresAt, AuthorityJwksResponse Response,
string CacheControl); string ETag,
DateTimeOffset ExpiresAt,
internal sealed class JwksKeyEntry string CacheControl);
{
[JsonPropertyName("kty")] internal sealed class JwksKeyEntry
public string? Kty { get; set; } {
[JsonPropertyName("kty")]
[JsonPropertyName("use")] public string? Kty { get; set; }
public string? Use { get; set; }
[JsonPropertyName("use")]
[JsonPropertyName("kid")] public string? Use { get; set; }
public string? Kid { get; set; }
[JsonPropertyName("kid")]
[JsonPropertyName("alg")] public string? Kid { get; set; }
public string? Alg { get; set; }
[JsonPropertyName("alg")]
[JsonPropertyName("crv")] public string? Alg { get; set; }
public string? Crv { get; set; }
[JsonPropertyName("crv")]
[JsonPropertyName("x")] public string? Crv { get; set; }
public string? X { get; set; }
[JsonPropertyName("x")]
[JsonPropertyName("y")] public string? X { get; set; }
public string? Y { get; set; }
[JsonPropertyName("y")]
[JsonPropertyName("status")] public string? Y { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Status { get; set; } [JsonPropertyName("status")]
} [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Status { get; set; }
}

View File

@@ -1,170 +1,182 @@
# Authority Host Task Board — Epic 1: Aggregation-Only Contract # Authority Host Task Board — Epic 1: Aggregation-Only Contract
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
> 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003. > 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003.
> 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests. > 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests.
> 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun. > 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun.
> 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates). > 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates).
> 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients. > 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients.
> 2025-10-31: Client credentials and password grants now reject advisory/vex read or signals scopes without `aoc:verify`, enforce tenant assignment for `aoc:verify`, tag violations via `authority.aoc_scope_violation`, extend tests, and refresh scope catalogue docs/sample roles. > 2025-10-31: Client credentials and password grants now reject advisory/vex read or signals scopes without `aoc:verify`, enforce tenant assignment for `aoc:verify`, tag violations via `authority.aoc_scope_violation`, extend tests, and refresh scope catalogue docs/sample roles.
## Link-Not-Merge v1 | AUTH-CRYPTO-90-001 | DOING (2025-11-08) | Authority Core & Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Migrate signing/key-loading paths (`KmsAuthoritySigningKeySource`, `FileAuthoritySigningKeySource`, `AuthorityJwksService`, secret hashers) to `ICryptoProviderRegistry` so regional bundles can pick `ru.cryptopro.csp` / `ru.pkcs11` providers as defined in `docs/security/crypto-routing-audit-2025-11-07.md`. | All signing + hashing code paths resolve registry providers; Authority config exposes provider selection; JWKS output references sovereign keys; regression tests updated. |
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | ## Link-Not-Merge v1
|----|--------|----------|------------|-------------|---------------|
> 2025-10-29: Rejected legacy `concelier.merge` scope during client credential validation, removed it from known scope catalog, blocked discovery/issuance, added regression tests, and refreshed scope documentation. | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
## Policy Engine v2 > 2025-10-29: Rejected legacy `concelier.merge` scope during client credential validation, removed it from known scope catalog, blocked discovery/issuance, added regression tests, and refreshed scope documentation.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | ## Policy Engine v2
|----|--------|----------|------------|-------------|---------------|
> 2025-10-26: Restricted `effective:write` to Policy Engine service identities with tenant requirement, registered full scope set, and tightened resource server default scope enforcement (unit tests pass). | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
> 2025-10-26: Authority docs now detail policy scopes/service identity guardrails with checklist; `authority.yaml.sample` includes `properties.serviceIdentity` example. |----|--------|----------|------------|-------------|---------------|
> 2025-10-26: Restricted `effective:write` to Policy Engine service identities with tenant requirement, registered full scope set, and tightened resource server default scope enforcement (unit tests pass).
## Graph Explorer v1 > 2025-10-26: Authority docs now detail policy scopes/service identity guardrails with checklist; `authority.yaml.sample` includes `properties.serviceIdentity` example.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | ## Graph Explorer v1
|----|--------|----------|------------|-------------|---------------|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
## Policy Engine + Editor v1 |----|--------|----------|------------|-------------|---------------|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | ## Policy Engine + Editor v1
|----|--------|----------|------------|-------------|---------------|
| AUTH-POLICY-23-002 | BLOCKED (2025-10-29) | Authority Core & Security Guild | AUTH-POLICY-23-001 | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. | Activation endpoint enforces rule; audit logs contain approver IDs; tests cover 2-person path. | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
> Blocked: Policy Engine/Studio have not yet exposed activation workflow endpoints or approval payloads needed to enforce dual-control (`WEB-POLICY-23-002`, `POLICY-ENGINE-23-002`). Revisit once activation contract lands. |----|--------|----------|------------|-------------|---------------|
| AUTH-POLICY-23-003 | BLOCKED (2025-10-29) | Authority Core & Docs Guild | AUTH-POLICY-23-001 | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. | Docs updated with reviewer checklist; configuration examples validated. | | AUTH-DPOP-11-001 | DOING (2025-11-07) | Authority Core & Security Guild | AUTH-AOC-19-002 | Enforce DPoP sender constraints for all Authority token flows (nonce store selection, algorithm allowlist, `cnf.jkt` persistence, structured telemetry). | `/token` enforces configured DPoP policies (nonce, allowed algorithms); cnf claims verified in integration tests; docs/runbooks updated with configuration guidance. |
> Blocked pending AUTH-POLICY-23-002 dual-approval implementation so docs can capture final activation behaviour. > 2025-11-07: Joint Authority/DevOps stand-up committed to shipping nonce store + telemetry updates by 2025-11-10; config samples and integration tests being updated in tandem.
> 2025-10-27: Added `policy-cli` defaults to Authority config/secrets, refreshed CLI/CI documentation with the new scope bundle, recorded release migration guidance, and introduced `scripts/verify-policy-scopes.py` to guard against regressions. | AUTH-MTLS-11-002 | DOING (2025-11-07) | Authority Core & Security Guild | AUTH-DPOP-11-001 | Add mTLS-bound access token issuance/validation (client certificate thumbprints, JWKS rotation hooks) for high-assurance tenants and services. | mTLS certificate binding validated end-to-end; audit logs capture cert hashes; docs describe bootstrap/rotation steps. |
> 2025-11-08: Wiring cert thumbprint persistence + audit hooks now that DPoP nonce enforcement is in place; targeting shared delivery window with DEVOPS-AIRGAP-57-002.
## Graph & Vuln Explorer v1 > 2025-11-07: Same stand-up aligned on 2025-11-10 target for mTLS enforcement + JWKS rotation docs so plugin mitigations can unblock.
| AUTH-POLICY-23-001 | DONE (2025-10-27) | Authority Core & Docs Guild | AUTH-AOC-19-002 | Introduce fine-grained policy scopes (`policy:read`, `policy:author`, `policy:review`, `policy:simulate`, `findings:read`) for CLI/service identities; refresh discovery metadata, issuer templates, and offline defaults. | Scope catalogue and sample configs updated; `policy-cli` seed credentials rotated; docs recorded migration steps. |
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | AUTH-POLICY-23-002 | DONE (2025-11-08) | Authority Core & Security Guild | AUTH-POLICY-23-001 | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. | Activation endpoint enforces rule; audit logs contain approver IDs; tests cover 2-person path. |
|----|--------|----------|------------|-------------|---------------| > 2025-11-08: Policy Engine enforces pending_second_approval when dual-control toggles demand it, activation auditor emits structured `policy.activation.*` scopes, and tests cover settings/audits.
> 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged. > Blocked: Policy Engine/Studio have not yet exposed activation workflow endpoints or approval payloads needed to enforce dual-control (`WEB-POLICY-23-002`, `POLICY-ENGINE-23-002`). Revisit once activation contract lands.
| AUTH-POLICY-23-003 | DONE (2025-11-08) | Authority Core & Docs Guild | AUTH-POLICY-23-001 | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. | Docs updated with reviewer checklist; configuration examples validated. |
## Orchestrator Dashboard > 2025-11-08: Docs refreshed for dual-control activation (console workflow, compliance checklist, sample YAML) and linked to new Policy Engine activation options.
> 2025-11-07: Scope migration landed (AUTH-POLICY-23-001); dual-approval + documentation tasks now waiting on pairing.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | > 2025-10-27: Added `policy-cli` defaults to Authority config/secrets, refreshed CLI/CI documentation with the new scope bundle, recorded release migration guidance, and introduced `scripts/verify-policy-scopes.py` to guard against regressions.
|----|--------|----------|------------|-------------|---------------|
> 2025-10-31: Picked up during Console/Orchestrator alignment; focusing on scope catalog + tenant enforcement first. ## Graph & Vuln Explorer v1
> 2025-10-31: `orch:read` added to scope catalogue and Authority runtime, Console defaults include the scope, `Orch.Viewer` role documented, and client-credential tests enforce tenant requirements.
> 2025-10-27: Added `orch:operate` scope, enforced `operator_reason`/`operator_ticket` on token issuance, updated Authority configs/docs, and captured audit metadata for control actions. | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
> 2025-10-28: Policy gateway + scanner now pass the expanded token client signature (`null` metadata by default), test stubs capture the optional parameters, and Policy Gateway/Scanner suites are green after fixing the Concelier storage build break. |----|--------|----------|------------|-------------|---------------|
> 2025-10-28: Authority password-grant tests now hit the new constructors but still need updates to drop obsolete `IOptions` arguments before the suite can pass. > 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged.
| AUTH-ORCH-34-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-ORCH-33-001 | Introduce `Orch.Admin` role with quota/backfill scopes, enforce audit reason on quota changes, and update offline defaults/docs. | Admin role available; quotas/backfills require scope + reason; tests confirm tenant isolation; documentation updated. |
> 2025-11-02: `orch:backfill` scope added with mandatory `backfill_reason`/`backfill_ticket`, client-credential validation and resource authorization paths emit audit fields, CLI picks up new configuration/env vars, and Authority docs/config samples updated for `Orch.Admin`. ## Orchestrator Dashboard
## StellaOps Console (Sprint 23) | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | > 2025-10-31: Picked up during Console/Orchestrator alignment; focusing on scope catalog + tenant enforcement first.
|----|--------|----------|------------|-------------|---------------| > 2025-10-31: `orch:read` added to scope catalogue and Authority runtime, Console defaults include the scope, `Orch.Viewer` role documented, and client-credential tests enforce tenant requirements.
> 2025-10-29: Authorization code flow enabled with PKCE requirement, console client seeded in `authority.yaml.sample`, discovery docs updated, and console runbook guidance added. > 2025-10-27: Added `orch:operate` scope, enforced `operator_reason`/`operator_ticket` on token issuance, updated Authority configs/docs, and captured audit metadata for control actions.
> 2025-10-31: Added `/console/tenants`, `/console/profile`, `/console/token/introspect` endpoints with tenant header filter, scope enforcement (`ui.read`, `authority:tenants.read`), and structured audit events. Console test harness covers success/mismatch cases. > 2025-10-28: Policy gateway + scanner now pass the expanded token client signature (`null` metadata by default), test stubs capture the optional parameters, and Policy Gateway/Scanner suites are green after fixing the Concelier storage build break.
> 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120s OpTok, 300s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task. > 2025-10-28: Authority password-grant tests now hit the new constructors but still need updates to drop obsolete `IOptions` arguments before the suite can pass.
> 2025-10-31: Security guide expanded for `/console` endpoints & orchestrator scope, sample YAML annotated, ops runbook updated, and release note `docs/updates/2025-10-31-console-security-refresh.md` published. | AUTH-ORCH-34-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-ORCH-33-001 | Introduce `Orch.Admin` role with quota/backfill scopes, enforce audit reason on quota changes, and update offline defaults/docs. | Admin role available; quotas/backfills require scope + reason; tests confirm tenant isolation; documentation updated. |
> 2025-10-31: Default access-token lifetime reduced to 120s, console tests updated with dual auth schemes, docs/config/ops notes refreshed, release note logged. > 2025-11-02: `orch:backfill` scope added with mandatory `backfill_reason`/`backfill_ticket`, client-credential validation and resource authorization paths emit audit fields, CLI picks up new configuration/env vars, and Authority docs/config samples updated for `Orch.Admin`.
## Policy Studio (Sprint 27) ## StellaOps Console (Sprint 23)
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
> 2025-10-31: Added Policy Studio scope family (`policy:author/review/operate/audit`), updated OpenAPI + discovery headers, enforced tenant requirements in grant handlers, seeded new roles in Authority config/offline kit docs, and refreshed CLI/Console documentation + tests to validate the new catalogue. > 2025-10-29: Authorization code flow enabled with PKCE requirement, console client seeded in `authority.yaml.sample`, discovery docs updated, and console runbook guidance added.
| AUTH-POLICY-27-002 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. | > 2025-10-31: Added `/console/tenants`, `/console/profile`, `/console/token/introspect` endpoints with tenant header filter, scope enforcement (`ui.read`, `authority:tenants.read`), and structured audit events. Console test harness covers success/mismatch cases.
> Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work. > 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120s OpTok, 300s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task.
> 2025-11-02: Added `policy:publish`/`policy:promote` scopes with interactive-only enforcement, metadata parameters (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth token validation, audit augmentations, and updated config/docs references. > 2025-10-31: Security guide expanded for `/console` endpoints & orchestrator scope, sample YAML annotated, ops runbook updated, and release note `docs/updates/2025-10-31-console-security-refresh.md` published.
| AUTH-POLICY-27-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. | > 2025-10-31: Default access-token lifetime reduced to 120s, console tests updated with dual auth schemes, docs/config/ops notes refreshed, release note logged.
> 2025-11-04: Policy Studio roles/scopes documented across `docs/11_AUTHORITY.md`, sample configs, and OpenAPI; compliance checklist appended and Authority tests rerun to validate fresh-auth + scope enforcement.
## Policy Studio (Sprint 27)
## Exceptions v1
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | |----|--------|----------|------------|-------------|---------------|
|----|--------|----------|------------|-------------|---------------| > 2025-10-31: Added Policy Studio scope family (`policy:author/review/operate/audit`), updated OpenAPI + discovery headers, enforced tenant requirements in grant handlers, seeded new roles in Authority config/offline kit docs, and refreshed CLI/Console documentation + tests to validate the new catalogue.
> 2025-10-29: Added exception scopes + routing template options, enforced MFA requirement in password grant handlers, updated configuration samples. | AUTH-POLICY-27-002 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. |
> 2025-10-31: Authority scopes/routing docs updated (`docs/security/authority-scopes.md`, `docs/11_AUTHORITY.md`, `docs/policy/exception-effects.md`), monitoring guide covers new MFA audit events, and `etc/authority.yaml.sample` now demonstrates exception clients/templates. > Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work.
> 2025-11-02: Added `policy:publish`/`policy:promote` scopes with interactive-only enforcement, metadata parameters (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth token validation, audit augmentations, and updated config/docs references.
## Reachability v1 | AUTH-POLICY-27-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. |
> 2025-11-04: Policy Studio roles/scopes documented across `docs/11_AUTHORITY.md`, sample configs, and OpenAPI; compliance checklist appended and Authority tests rerun to validate fresh-auth + scope enforcement.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| ## Exceptions v1
> 2025-10-29: Signals scopes added with tenant + aoc:verify enforcement; sensors guided via SignalsUploader template; tests cover gating.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
## Vulnerability Explorer (Sprint 29) |----|--------|----------|------------|-------------|---------------|
> 2025-10-29: Added exception scopes + routing template options, enforced MFA requirement in password grant handlers, updated configuration samples.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | > 2025-10-31: Authority scopes/routing docs updated (`docs/security/authority-scopes.md`, `docs/11_AUTHORITY.md`, `docs/policy/exception-effects.md`), monitoring guide covers new MFA audit events, and `etc/authority.yaml.sample` now demonstrates exception clients/templates.
|----|--------|----------|------------|-------------|---------------|
| 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. | ## Reachability v1
| 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 | DONE (2025-11-04) | 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. | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
> 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. |----|--------|----------|------------|-------------|---------------|
> 2025-11-04: Verified Vuln Explorer RBAC/ABAC coverage in Authority docs/security guides, attachment token guidance, and offline samples; Authority tests rerun confirming ledger-token + anti-forgery behaviours. > 2025-10-29: Signals scopes added with tenant + aoc:verify enforcement; sensors guided via SignalsUploader template; tests cover gating.
## Advisory AI (Sprint 31) ## Vulnerability Explorer (Sprint 29)
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
| AUTH-AIAI-31-001 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Advisory AI scopes (`advisory-ai:view`, `advisory-ai:operate`, `advisory-ai:admin`) and remote inference toggles; update discovery metadata/offline defaults. | Scopes/flags published; integration tests cover RBAC + opt-in settings; docs 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-AIAI-31-002 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-AIAI-31-001, AIAI-31-006 | Enforce anonymized prompt logging, tenant consent for remote inference, and audit logging of assistant tasks. | Logging/audit flows verified; privacy review passed; docs updated. | | 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 | DONE (2025-11-04) | 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. |
## Export Center > 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.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | > 2025-11-04: Verified Vuln Explorer RBAC/ABAC coverage in Authority docs/security guides, attachment token guidance, and offline samples; Authority tests rerun confirming ledger-token + anti-forgery behaviours.
|----|--------|----------|------------|-------------|---------------|
## Advisory AI (Sprint 31)
## Notifications Studio
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
| AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Authority Core & Security Guild | — | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit defaults refreshed. | | AUTH-AIAI-31-001 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Advisory AI scopes (`advisory-ai:view`, `advisory-ai:operate`, `advisory-ai:admin`) and remote inference toggles; update discovery metadata/offline defaults. | Scopes/flags published; integration tests cover RBAC + opt-in settings; docs updated. |
| AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001 | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. | Ack tokens signed/rotated; webhook allowlists enforced; admin enforcement validated; audit logs capture ack/resolution. | | AUTH-AIAI-31-002 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-AIAI-31-001, AIAI-31-006 | Enforce anonymized prompt logging, tenant consent for remote inference, and audit logging of assistant tasks. | Logging/audit flows verified; privacy review passed; docs updated. |
> 2025-11-02: `/notify/ack-tokens/rotate` exposed (notify.admin), emits `notify.ack.key_rotated|notify.ack.key_rotation_failed`, and DSSE rotation tests cover allowlist + scope enforcement.
| AUTH-NOTIFY-42-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Investigate ack token rotation 500 errors (test Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure still failing). Capture logs, identify root cause, and patch handler. | Failure mode understood; fix merged; regression test passes. | ## Export Center
> 2025-11-02: Aliased `StellaOpsBearer` to the test auth handler, corrected bootstrap `/notifications/ack-tokens/rotate` defaults, and validated `Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure` via targeted `dotnet test`. | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
## CLI Parity & Task Packs ## Notifications Studio
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
| AUTH-PACKS-41-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AOC-19-001 | Define CLI SSO profiles and pack scopes (`Packs.Read`, `Packs.Write`, `Packs.Run`, `Packs.Approve`), update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit templates refreshed. | | AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Authority Core & Security Guild | — | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit defaults refreshed. |
> 2025-11-02: Added Pack scope policies, Authority role defaults, and CLI profile guidance covering operator/publisher/approver flows. | AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001 | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. | Ack tokens signed/rotated; webhook allowlists enforced; admin enforcement validated; audit logs capture ack/resolution. |
> 2025-11-02: Shared OpenSSL 1.1 shim feeds Authority & Signals Mongo2Go harnesses so pack scope coverage keeps running on OpenSSL 3 hosts (AUTH-PACKS-41-001). > 2025-11-02: `/notify/ack-tokens/rotate` exposed (notify.admin), emits `notify.ack.key_rotated|notify.ack.key_rotation_failed`, and DSSE rotation tests cover allowlist + scope enforcement.
> 2025-11-04: Discovery metadata/OpenAPI advertise packs scopes, configs/offline kit templates bundle new roles, and Authority tests re-run to validate tenant gating for `packs.*`. | AUTH-NOTIFY-42-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Investigate ack token rotation 500 errors (test Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure still failing). Capture logs, identify root cause, and patch handler. | Failure mode understood; fix merged; regression test passes. |
| AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Authority Core & Security Guild | AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101 | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. | Signing policies enforced; approvals require correct roles; CI token scope tests pass; audit logs recorded. | > 2025-11-02: Aliased `StellaOpsBearer` to the test auth handler, corrected bootstrap `/notifications/ack-tokens/rotate` defaults, and validated `Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure` via targeted `dotnet test`.
> Blocked: Task Runner approval APIs (`ORCH-SVC-42-101`, `TASKRUN-42-001`) still outstanding. Pack scope catalog (AUTH-PACKS-41-001) landed 2025-11-04; resume once execution/approval contracts are published.
## Authority-Backed Scopes & Tenancy (Epic 14) ## CLI Parity & Task Packs
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
> 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-PACKS-41-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AOC-19-001 | Define CLI SSO profiles and pack scopes (`Packs.Read`, `Packs.Write`, `Packs.Run`, `Packs.Approve`), update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit templates refreshed. |
| AUTH-TEN-49-001 | DONE (2025-11-04) | 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: Added Pack scope policies, Authority role defaults, and CLI profile guidance covering operator/publisher/approver flows.
> 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: Shared OpenSSL 1.1 shim feeds Authority & Signals Mongo2Go harnesses so pack scope coverage keeps running on OpenSSL 3 hosts (AUTH-PACKS-41-001).
> 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-04: Discovery metadata/OpenAPI advertise packs scopes, configs/offline kit templates bundle new roles, and Authority tests re-run to validate tenant gating for `packs.*`.
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour. | AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Authority Core & Security Guild | AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101 | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. | Signing policies enforced; approvals require correct roles; CI token scope tests pass; audit logs recorded. |
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios. > Blocked: ORCH-SVC-42-101 (Orchestrator log streaming/approvals API) still TODO. AUTH-PACKS-41-001 + TASKRUN-42-001 are DONE (2025-11-04); resume once Orchestrator publishes contracts.
> 2025-11-04: Confirmed service-account docs/config examples, quota tuning, and audit stream wiring; Authority suite re-executed to cover issuance/listing/revocation flows.
## Authority-Backed Scopes & Tenancy (Epic 14)
## Observability & Forensics (Epic 15) | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | > 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 | DONE (2025-11-04) | 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. |
| AUTH-OBS-50-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce scopes `obs:read`, `timeline:read`, `timeline:write`, `evidence:create`, `evidence:read`, `evidence:hold`, `attest:read`, and `obs:incident` (all tenant-scoped). Update discovery metadata, offline defaults, and scope grammar docs. | Scopes exposed via metadata; issuer templates updated; offline kit seeded; integration tests cover new scopes. | > 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
| AUTH-OBS-52-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-OBS-50-001, TIMELINE-OBS-52-003, EVID-OBS-53-003 | Configure resource server policies for Timeline Indexer, Evidence Locker, Exporter, and Observability APIs enforcing new scopes + tenant claims. Emit audit events including scope usage and trace IDs. | Policies deployed; unauthorized access blocked; audit logs prove scope usage; contract tests updated. | > 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.
| AUTH-OBS-55-001 | DONE (2025-11-02) | Authority Core & Security Guild, Ops Guild | AUTH-OBS-50-001, WEB-OBS-55-001 | Harden incident mode authorization: require `obs:incident` scope + fresh auth, log activation reason, and expose verification endpoint for auditors. Update docs/runbooks. | Incident activate/deactivate requires scope; audit entries logged; docs updated with imposed rule reminder. | > 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
## Air-Gapped Mode (Epic 16) > 2025-11-04: Confirmed service-account docs/config examples, quota tuning, and audit stream wiring; Authority suite re-executed to cover issuance/listing/revocation flows.
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | ## Observability & Forensics (Epic 15)
|----|--------|----------|------------|-------------|---------------|
| AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Authority Core & Security Guild | AIRGAP-CTL-56-001 | Provision new scopes (`airgap:seal`, `airgap:import`, `airgap:status:read`) in configuration metadata, offline kit defaults, and issuer templates. | Scopes exposed in discovery docs; offline kit updated; integration tests cover issuance. | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
| AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001 | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. | Audit records persisted; endpoint paginates results; tests cover RBAC + filtering. | |----|--------|----------|------------|-------------|---------------|
> 2025-11-04: Airgap scope constants are wired through discovery metadata, `etc/authority.yaml.sample`, and offline kit docs; scope issuance tests executed via `dotnet test`. | AUTH-OBS-50-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce scopes `obs:read`, `timeline:read`, `timeline:write`, `evidence:create`, `evidence:read`, `evidence:hold`, `attest:read`, and `obs:incident` (all tenant-scoped). Update discovery metadata, offline defaults, and scope grammar docs. | Scopes exposed via metadata; issuer templates updated; offline kit seeded; integration tests cover new scopes. |
> 2025-11-04: `/authority/audit/airgap` API persists tenant-scoped audit entries with pagination and authorization guards validated by the Authority integration suite (187 tests). | AUTH-OBS-52-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-OBS-50-001, TIMELINE-OBS-52-003, EVID-OBS-53-003 | Configure resource server policies for Timeline Indexer, Evidence Locker, Exporter, and Observability APIs enforcing new scopes + tenant claims. Emit audit events including scope usage and trace IDs. | Policies deployed; unauthorized access blocked; audit logs prove scope usage; contract tests updated. |
| AUTH-AIRGAP-57-001 | BLOCKED (2025-11-01) | Authority Core & Security Guild, DevOps Guild | AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002 | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. | Awaiting clarified sealed-confirmation contract and configuration structure before implementation. | | AUTH-OBS-55-001 | DONE (2025-11-02) | Authority Core & Security Guild, Ops Guild | AUTH-OBS-50-001, WEB-OBS-55-001 | Harden incident mode authorization: require `obs:incident` scope + fresh auth, log activation reason, and expose verification endpoint for auditors. Update docs/runbooks. | Incident activate/deactivate requires scope; audit entries logged; docs updated with imposed rule reminder. |
> 2025-11-01: AUTH-AIRGAP-57-001 blocked pending guidance on sealed-confirmation contract and configuration expectations before gating changes (Authority Core & Security Guild, DevOps Guild).
## Air-Gapped Mode (Epic 16)
## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
> 2025-10-28: Auth OpenAPI authored at `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml` covering `/token`, `/introspect`, `/revoke`, `/jwks`, scope catalog, and error envelopes; parsed via PyYAML sanity check and referenced in Epic 17 docs. | AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Authority Core & Security Guild | AIRGAP-CTL-56-001 | Provision new scopes (`airgap:seal`, `airgap:import`, `airgap:status:read`) in configuration metadata, offline kit defaults, and issuer templates. | Scopes exposed in discovery docs; offline kit updated; integration tests cover issuance. |
> 2025-10-28: Added `/.well-known/openapi` endpoint wiring cached spec metadata, YAML/JSON negotiation, HTTP cache headers, and tests verifying ETag + Accept handling. Authority spec (`src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`) now includes grant/scope extensions. | AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001 | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. | Audit records persisted; endpoint paginates results; tests cover RBAC + filtering. |
| AUTH-OAS-62-001 | DONE (2025-11-02) | Authority Core & Security Guild, SDK Generator Guild | AUTH-OAS-61-001, SDKGEN-63-001 | Provide SDK helpers for OAuth2/PAT flows, tenancy override header; add integration tests. | SDKs expose auth helpers; tests cover token issuance; docs updated. | > 2025-11-04: Airgap scope constants are wired through discovery metadata, `etc/authority.yaml.sample`, and offline kit docs; scope issuance tests executed via `dotnet test`.
> 2025-11-02: `AddStellaOpsApiAuthentication` shipped (OAuth2 + PAT), tenant header injection added, and client tests updated for caching behaviour. > 2025-11-04: `/authority/audit/airgap` API persists tenant-scoped audit entries with pagination and authorization guards validated by the Authority integration suite (187 tests).
| AUTH-OAS-63-001 | DONE (2025-11-02) | Authority Core & Security Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and notifications for legacy auth endpoints. | Headers emitted; notifications verified; migration guide published. | | AUTH-AIRGAP-57-001 | DOING (2025-11-08) | Authority Core & Security Guild, DevOps Guild | AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002 | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. | Implement Authority-side sealed-mode checks once DevOps publishes sealed CI artefacts + contract (target 2025-11-10). |
> 2025-11-02: AUTH-OAS-63-001 completed — legacy OAuth shims emit Deprecation/Sunset/Warning headers, audit events captured, and migration guide published (Authority Core & Security Guild, API Governance Guild). > 2025-11-08: Picked up in tandem with DEVOPS-AIRGAP-57-002 — validating sealed confirmation payload + wiring Authority gating tests against ops/devops/sealed-mode-ci artefacts.
> 2025-11-08: `/token`/`/introspect` now reject mTLS-bound tokens without the recorded certificate; `authority_mtls_mismatch_total` metric + docs updated for plugin consumers.
> 2025-11-08: DevOps sealed-mode CI now emits `authority-sealed-ci.json`; ingest that contract next to unblock enforcement switch.
## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
> 2025-10-28: Auth OpenAPI authored at `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml` covering `/token`, `/introspect`, `/revoke`, `/jwks`, scope catalog, and error envelopes; parsed via PyYAML sanity check and referenced in Epic 17 docs.
> 2025-10-28: Added `/.well-known/openapi` endpoint wiring cached spec metadata, YAML/JSON negotiation, HTTP cache headers, and tests verifying ETag + Accept handling. Authority spec (`src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`) now includes grant/scope extensions.
| AUTH-OAS-62-001 | DONE (2025-11-02) | Authority Core & Security Guild, SDK Generator Guild | AUTH-OAS-61-001, SDKGEN-63-001 | Provide SDK helpers for OAuth2/PAT flows, tenancy override header; add integration tests. | SDKs expose auth helpers; tests cover token issuance; docs updated. |
> 2025-11-02: `AddStellaOpsApiAuthentication` shipped (OAuth2 + PAT), tenant header injection added, and client tests updated for caching behaviour.
| AUTH-OAS-63-001 | DONE (2025-11-02) | Authority Core & Security Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and notifications for legacy auth endpoints. | Headers emitted; notifications verified; migration guide published. |
> 2025-11-02: AUTH-OAS-63-001 completed — legacy OAuth shims emit Deprecation/Sunset/Warning headers, audit events captured, and migration guide published (Authority Core & Security Guild, API Governance Guild).

View File

@@ -43,6 +43,7 @@ internal static class CommandFactory
root.Add(BuildConfigCommand(options)); root.Add(BuildConfigCommand(options));
root.Add(BuildKmsCommand(services, verboseOption, cancellationToken)); root.Add(BuildKmsCommand(services, verboseOption, cancellationToken));
root.Add(BuildVulnCommand(services, verboseOption, cancellationToken)); root.Add(BuildVulnCommand(services, verboseOption, cancellationToken));
root.Add(BuildCryptoCommand(services, verboseOption, cancellationToken));
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>(); var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger); var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger);
@@ -180,8 +181,8 @@ internal static class CommandFactory
return scan; return scan;
} }
private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken) private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{ {
var kms = new Command("kms", "Manage file-backed signing keys."); var kms = new Command("kms", "Manage file-backed signing keys.");
var export = new Command("export", "Export key material to a portable bundle."); var export = new Command("export", "Export key material to a portable bundle.");
@@ -381,9 +382,39 @@ internal static class CommandFactory
db.Add(fetch); db.Add(fetch);
db.Add(merge); db.Add(merge);
db.Add(export); db.Add(export);
return db; return db;
} }
private static Command BuildCryptoCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var crypto = new Command("crypto", "Inspect StellaOps cryptography providers.");
var providers = new Command("providers", "List registered crypto providers and keys.");
var jsonOption = new Option<bool>("--json")
{
Description = "Emit JSON output."
};
var profileOption = new Option<string?>("--profile")
{
Description = "Temporarily override the active registry profile when computing provider order."
};
providers.Add(jsonOption);
providers.Add(profileOption);
providers.SetAction((parseResult, _) =>
{
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
var profile = parseResult.GetValue(profileOption);
return CommandHandlers.HandleCryptoProvidersAsync(services, verbose, json, profile, cancellationToken);
});
crypto.Add(providers);
return crypto;
}
private static Command BuildSourcesCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken) private static Command BuildSourcesCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{ {

View File

@@ -18,7 +18,8 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using StellaOps.Auth.Client; using StellaOps.Auth.Client;
@@ -28,7 +29,8 @@ using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models; using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi; using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Telemetry; using StellaOps.Cli.Telemetry;
using StellaOps.Cryptography; using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Kms; using StellaOps.Cryptography.Kms;
namespace StellaOps.Cli.Commands; namespace StellaOps.Cli.Commands;
@@ -6437,35 +6439,223 @@ internal static class CommandHandlers
return source; return source;
} }
private static async Task TriggerJobAsync( private static async Task TriggerJobAsync(
IBackendOperationsClient client, IBackendOperationsClient client,
ILogger logger, ILogger logger,
string jobKind, string jobKind,
IDictionary<string, object?> parameters, IDictionary<string, object?> parameters,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false); JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false);
if (result.Success) if (result.Success)
{ {
if (!string.IsNullOrWhiteSpace(result.Location)) if (!string.IsNullOrWhiteSpace(result.Location))
{ {
logger.LogInformation("Job accepted. Track status at {Location}.", result.Location); logger.LogInformation("Job accepted. Track status at {Location}.", result.Location);
} }
else if (result.Run is not null) else if (result.Run is not null)
{ {
logger.LogInformation("Job accepted. RunId: {RunId} Status: {Status}", result.Run.RunId, result.Run.Status); logger.LogInformation("Job accepted. RunId: {RunId} Status: {Status}", result.Run.RunId, result.Run.Status);
} }
else else
{ {
logger.LogInformation("Job accepted."); logger.LogInformation("Job accepted.");
} }
Environment.ExitCode = 0; Environment.ExitCode = 0;
} }
else else
{ {
logger.LogError("Job '{JobKind}' failed: {Message}", jobKind, result.Message); logger.LogError("Job '{JobKind}' failed: {Message}", jobKind, result.Message);
Environment.ExitCode = 1; Environment.ExitCode = 1;
} }
} }
}
public static Task HandleCryptoProvidersAsync(
IServiceProvider services,
bool verbose,
bool jsonOutput,
string? profileOverride,
CancellationToken cancellationToken)
{
using var scope = services.CreateScope();
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("crypto-providers");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
var previousLevel = verbosity.MinimumLevel;
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
using var activity = CliActivitySource.Instance.StartActivity("cli.crypto.providers", ActivityKind.Internal);
using var duration = CliMetrics.MeasureCommandDuration("crypto providers");
try
{
var registry = scope.ServiceProvider.GetService<ICryptoProviderRegistry>();
if (registry is null)
{
logger.LogWarning("Crypto provider registry not available in this environment.");
AnsiConsole.MarkupLine("[yellow]Crypto subsystem is not configured in this environment.[/]");
return Task.CompletedTask;
}
var optionsMonitor = scope.ServiceProvider.GetService<IOptionsMonitor<CryptoProviderRegistryOptions>>();
var registryOptions = optionsMonitor?.CurrentValue ?? new CryptoProviderRegistryOptions();
var preferredOrder = DeterminePreferredOrder(registryOptions, profileOverride);
var providers = registry.Providers
.Select(provider => new ProviderInfo(
provider.Name,
provider.GetType().FullName ?? provider.GetType().Name,
DescribeProviderKeys(provider).ToList()))
.ToList();
if (jsonOutput)
{
var payload = new
{
activeProfile = registryOptions.ActiveProfile,
preferredOrder,
providers = providers.Select(info => new
{
info.Name,
info.Type,
keys = info.Keys.Select(k => new
{
k.KeyId,
k.AlgorithmId,
Metadata = k.Metadata
})
})
};
Console.WriteLine(JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
WriteIndented = true
}));
Environment.ExitCode = 0;
return Task.CompletedTask;
}
RenderCryptoProviders(preferredOrder, providers);
Environment.ExitCode = 0;
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
return Task.CompletedTask;
}
private static void RenderCryptoProviders(
IReadOnlyList<string> preferredOrder,
IReadOnlyCollection<ProviderInfo> providers)
{
if (preferredOrder.Count > 0)
{
AnsiConsole.MarkupLine("[cyan]Preferred order:[/] {0}", Markup.Escape(string.Join(", ", preferredOrder)));
}
else
{
AnsiConsole.MarkupLine("[yellow]Preferred order is not configured; using registration order.[/]");
}
var table = new Table().Border(TableBorder.Rounded);
table.AddColumn("Provider");
table.AddColumn("Type");
table.AddColumn("Keys");
foreach (var provider in providers)
{
var keySummary = provider.Keys.Count == 0
? "[grey]No signing keys exposed (managed externally).[/]"
: string.Join(Environment.NewLine, provider.Keys.Select(FormatDescriptor));
table.AddRow(
Markup.Escape(provider.Name),
Markup.Escape(provider.Type),
keySummary);
}
AnsiConsole.Write(table);
}
private static IReadOnlyList<CryptoProviderKeyDescriptor> DescribeProviderKeys(ICryptoProvider provider)
{
if (provider is ICryptoProviderDiagnostics diagnostics)
{
return diagnostics.DescribeKeys().ToList();
}
var signingKeys = provider.GetSigningKeys();
if (signingKeys.Count == 0)
{
return Array.Empty<CryptoProviderKeyDescriptor>();
}
var descriptors = new List<CryptoProviderKeyDescriptor>(signingKeys.Count);
foreach (var signingKey in signingKeys)
{
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["kind"] = signingKey.Kind.ToString(),
["createdAt"] = signingKey.CreatedAt.UtcDateTime.ToString("O"),
["providerHint"] = signingKey.Reference.ProviderHint
};
if (signingKey.ExpiresAt.HasValue)
{
metadata["expiresAt"] = signingKey.ExpiresAt.Value.UtcDateTime.ToString("O");
}
foreach (var pair in signingKey.Metadata)
{
metadata[$"meta.{pair.Key}"] = pair.Value;
}
descriptors.Add(new CryptoProviderKeyDescriptor(
provider.Name,
signingKey.Reference.KeyId,
signingKey.AlgorithmId,
metadata));
}
return descriptors;
}
private static IReadOnlyList<string> DeterminePreferredOrder(
CryptoProviderRegistryOptions? options,
string? overrideProfile)
{
if (options is null)
{
return Array.Empty<string>();
}
if (!string.IsNullOrWhiteSpace(overrideProfile) &&
options.Profiles.TryGetValue(overrideProfile, out var profile) &&
profile.PreferredProviders.Count > 0)
{
return profile.PreferredProviders
.Where(static provider => !string.IsNullOrWhiteSpace(provider))
.Select(static provider => provider.Trim())
.ToArray();
}
return options.ResolvePreferredProviders();
}
private static string FormatDescriptor(CryptoProviderKeyDescriptor descriptor)
{
if (descriptor.Metadata.Count == 0)
{
return $"{Markup.Escape(descriptor.KeyId)} ({Markup.Escape(descriptor.AlgorithmId)})";
}
var metadataText = string.Join(
", ",
descriptor.Metadata.Select(pair => $"{pair.Key}={pair.Value}"));
return $"{Markup.Escape(descriptor.KeyId)} ({Markup.Escape(descriptor.AlgorithmId)}){Environment.NewLine}[grey]{Markup.Escape(metadataText)}[/]";
}
private sealed record ProviderInfo(string Name, string Type, IReadOnlyList<CryptoProviderKeyDescriptor> Keys);
}

View File

@@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using StellaOps.Auth.Abstractions; using System.IO;
using System.IO; using StellaOps.Auth.Abstractions;
using StellaOps.Configuration;
namespace StellaOps.Cli.Configuration; namespace StellaOps.Cli.Configuration;
@@ -31,8 +32,11 @@ public sealed class StellaOpsCliOptions
public StellaOpsCliOfflineOptions Offline { get; set; } = new(); public StellaOpsCliOfflineOptions Offline { get; set; } = new();
public StellaOpsCliPluginOptions Plugins { get; set; } = new(); public StellaOpsCliPluginOptions Plugins { get; set; } = new();
}
public StellaOpsCryptoOptions Crypto { get; set; } = new();
}
public sealed class StellaOpsCliAuthorityOptions public sealed class StellaOpsCliAuthorityOptions
{ {
@@ -79,15 +83,15 @@ public sealed class StellaOpsCliOfflineOptions
public string? MirrorUrl { get; set; } public string? MirrorUrl { get; set; }
} }
public sealed class StellaOpsCliPluginOptions public sealed class StellaOpsCliPluginOptions
{ {
public string BaseDirectory { get; set; } = string.Empty; public string BaseDirectory { get; set; } = string.Empty;
public string Directory { get; set; } = "plugins/cli"; public string Directory { get; set; } = "plugins/cli";
public IList<string> SearchPatterns { get; set; } = new List<string>(); public IList<string> SearchPatterns { get; set; } = new List<string>();
public IList<string> PluginOrder { get; set; } = new List<string>(); public IList<string> PluginOrder { get; set; } = new List<string>();
public string ManifestSearchPattern { get; set; } = "*.manifest.json"; public string ManifestSearchPattern { get; set; } = "*.manifest.json";
} }

View File

@@ -12,6 +12,7 @@ using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services; using StellaOps.Cli.Services;
using StellaOps.Cli.Telemetry; using StellaOps.Cli.Telemetry;
using StellaOps.AirGap.Policy; using StellaOps.AirGap.Policy;
using StellaOps.Configuration;
namespace StellaOps.Cli; namespace StellaOps.Cli;
@@ -22,12 +23,14 @@ internal static class Program
var (options, configuration) = CliBootstrapper.Build(args); var (options, configuration) = CliBootstrapper.Build(args);
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(configuration); services.AddSingleton(configuration);
services.AddSingleton(options); services.AddSingleton(options);
services.AddOptions();
var verbosityState = new VerbosityState();
var verbosityState = new VerbosityState();
services.AddSingleton(verbosityState); services.AddSingleton(verbosityState);
services.AddAirGapEgressPolicy(configuration); services.AddAirGapEgressPolicy(configuration);
services.AddStellaOpsCrypto(options.Crypto);
services.AddLogging(builder => services.AddLogging(builder =>
{ {
@@ -168,6 +171,7 @@ internal static class Program
finalExit = 130; // Typical POSIX cancellation exit code finalExit = 130; // Typical POSIX cancellation exit code
} }
return finalExit; return finalExit;
} }
}
}

View File

@@ -41,6 +41,8 @@
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.CryptoPro/StellaOps.Cryptography.Plugin.CryptoPro.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.Pkcs11Gost/StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" /> <ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" /> <ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" /> <ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />

View File

@@ -1,22 +1,36 @@
using System.Diagnostics.Metrics; using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace StellaOps.Concelier.WebService.Diagnostics;
namespace StellaOps.Concelier.WebService.Diagnostics;
internal static class IngestionMetrics
{ internal static class IngestionMetrics
internal const string MeterName = "StellaOps.Concelier.WebService.Ingestion"; {
internal const string MeterName = "StellaOps.Concelier.WebService.Ingestion";
private static readonly Meter Meter = new(MeterName);
private static readonly Meter Meter = new(MeterName);
internal static readonly Counter<long> WriteCounter = Meter.CreateCounter<long>(
"ingestion_write_total", internal static readonly Counter<long> IngestionWriteCounter = Meter.CreateCounter<long>(
description: "Counts raw advisory ingestion attempts, segmented by tenant, source, and result."); "ingestion_write_total",
unit: "count",
internal static readonly Counter<long> ViolationCounter = Meter.CreateCounter<long>( description: "Number of advisory ingestion attempts processed by the web service.");
"aoc_violation_total",
description: "Counts Aggregation-Only Contract violations detected during ingestion."); internal static readonly Counter<long> VerificationCounter = Meter.CreateCounter<long>(
"verify_runs_total",
internal static readonly Counter<long> VerificationCounter = Meter.CreateCounter<long>( unit: "count",
"verify_runs_total", description: "Number of AOC verification requests processed by the web service.");
description: "Counts AOC verification runs initiated via the API.");
} internal static KeyValuePair<string, object?>[] BuildWriteTags(string tenant, string source, string result) =>
new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("source", source),
new KeyValuePair<string, object?>("result", result),
};
internal static KeyValuePair<string, object?>[] BuildVerifyTags(string tenant, string result) =>
new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("result", result),
};
}

View File

@@ -1,9 +1,10 @@
using System.Collections.Immutable; using System.Collections.Generic;
using System.Text.Json; using System.Collections.Immutable;
using StellaOps.Concelier.RawModels; using System.Text.Json;
using StellaOps.Concelier.WebService.Contracts; using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Contracts;
namespace StellaOps.Concelier.WebService.Extensions;
namespace StellaOps.Concelier.WebService.Extensions;
internal static class AdvisoryRawRequestMapper internal static class AdvisoryRawRequestMapper
{ {
@@ -14,13 +15,13 @@ internal static class AdvisoryRawRequestMapper
ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(timeProvider);
var sourceRequest = request.Source ?? throw new ArgumentException("source section is required.", nameof(request)); var sourceRequest = request.Source ?? throw new ArgumentException("source section is required.", nameof(request));
var upstreamRequest = request.Upstream ?? throw new ArgumentException("upstream section is required.", nameof(request)); var upstreamRequest = request.Upstream ?? throw new ArgumentException("upstream section is required.", nameof(request));
var contentRequest = request.Content ?? throw new ArgumentException("content section is required.", nameof(request)); var contentRequest = request.Content ?? throw new ArgumentException("content section is required.", nameof(request));
var identifiersRequest = request.Identifiers ?? throw new ArgumentException("identifiers section is required.", nameof(request)); var identifiersRequest = request.Identifiers ?? throw new ArgumentException("identifiers section is required.", nameof(request));
var source = new RawSourceMetadata( var source = new RawSourceMetadata(
sourceRequest.Vendor, sourceRequest.Vendor,
sourceRequest.Connector, sourceRequest.Connector,
sourceRequest.Version, sourceRequest.Version,
string.IsNullOrWhiteSpace(sourceRequest.Stream) ? null : sourceRequest.Stream); string.IsNullOrWhiteSpace(sourceRequest.Stream) ? null : sourceRequest.Stream);
@@ -33,22 +34,21 @@ internal static class AdvisoryRawRequestMapper
string.IsNullOrWhiteSpace(signatureRequest.Certificate) ? null : signatureRequest.Certificate, string.IsNullOrWhiteSpace(signatureRequest.Certificate) ? null : signatureRequest.Certificate,
string.IsNullOrWhiteSpace(signatureRequest.Digest) ? null : signatureRequest.Digest); string.IsNullOrWhiteSpace(signatureRequest.Digest) ? null : signatureRequest.Digest);
var retrievedAt = upstreamRequest.RetrievedAt ?? timeProvider.GetUtcNow(); var retrievedAt = upstreamRequest.RetrievedAt ?? timeProvider.GetUtcNow();
var upstream = new RawUpstreamMetadata(
var upstream = new RawUpstreamMetadata( upstreamRequest.UpstreamId,
upstreamRequest.UpstreamId, string.IsNullOrWhiteSpace(upstreamRequest.DocumentVersion) ? null : upstreamRequest.DocumentVersion,
string.IsNullOrWhiteSpace(upstreamRequest.DocumentVersion) ? null : upstreamRequest.DocumentVersion, retrievedAt,
retrievedAt, upstreamRequest.ContentHash,
upstreamRequest.ContentHash, signature,
signature, NormalizeDictionary(upstreamRequest.Provenance));
NormalizeDictionary(upstreamRequest.Provenance));
var rawContent = NormalizeRawContent(contentRequest.Raw);
var rawContent = NormalizeRawContent(contentRequest.Raw); var content = new RawContent(
var content = new RawContent( contentRequest.Format,
contentRequest.Format, string.IsNullOrWhiteSpace(contentRequest.SpecVersion) ? null : contentRequest.SpecVersion,
string.IsNullOrWhiteSpace(contentRequest.SpecVersion) ? null : contentRequest.SpecVersion, rawContent,
rawContent, string.IsNullOrWhiteSpace(contentRequest.Encoding) ? null : contentRequest.Encoding);
string.IsNullOrWhiteSpace(contentRequest.Encoding) ? null : contentRequest.Encoding);
var aliases = NormalizeStrings(identifiersRequest.Aliases); var aliases = NormalizeStrings(identifiersRequest.Aliases);
if (aliases.IsDefault) if (aliases.IsDefault)
@@ -56,11 +56,15 @@ internal static class AdvisoryRawRequestMapper
aliases = ImmutableArray<string>.Empty; aliases = ImmutableArray<string>.Empty;
} }
var identifiers = new RawIdentifiers( var identifiers = new RawIdentifiers(
aliases, aliases,
identifiersRequest.Primary); identifiersRequest.Primary);
var advisoryKey = NormalizeAdvisoryKey(
var linksetRequest = request.Linkset; identifiersRequest.Primary,
aliases,
upstreamRequest.UpstreamId);
var linksetRequest = request.Linkset;
var linkset = new RawLinkset var linkset = new RawLinkset
{ {
Aliases = NormalizeStrings(linksetRequest?.Aliases), Aliases = NormalizeStrings(linksetRequest?.Aliases),
@@ -71,6 +75,8 @@ internal static class AdvisoryRawRequestMapper
Notes = NormalizeDictionary(linksetRequest?.Notes) Notes = NormalizeDictionary(linksetRequest?.Notes)
}; };
var links = BuildLinks(advisoryKey, aliases, upstreamRequest.UpstreamId);
return new AdvisoryRawDocument( return new AdvisoryRawDocument(
tenant.Trim().ToLowerInvariant(), tenant.Trim().ToLowerInvariant(),
source, source,
@@ -78,8 +84,8 @@ internal static class AdvisoryRawRequestMapper
content, content,
identifiers, identifiers,
linkset, linkset,
AdvisoryKey: string.Empty, AdvisoryKey: advisoryKey,
Links: ImmutableArray<RawLink>.Empty); Links: links);
} }
internal static ImmutableArray<string> NormalizeStrings(IEnumerable<string>? values) internal static ImmutableArray<string> NormalizeStrings(IEnumerable<string>? values)
@@ -124,11 +130,11 @@ internal static class AdvisoryRawRequestMapper
return builder.ToImmutable(); return builder.ToImmutable();
} }
private static ImmutableArray<RawReference> NormalizeReferences(IEnumerable<AdvisoryLinksetReferenceRequest>? references) private static ImmutableArray<RawReference> NormalizeReferences(IEnumerable<AdvisoryLinksetReferenceRequest>? references)
{ {
if (references is null) if (references is null)
{ {
return ImmutableArray<RawReference>.Empty; return ImmutableArray<RawReference>.Empty;
} }
var builder = ImmutableArray.CreateBuilder<RawReference>(); var builder = ImmutableArray.CreateBuilder<RawReference>();
@@ -150,10 +156,59 @@ internal static class AdvisoryRawRequestMapper
return builder.Count == 0 ? ImmutableArray<RawReference>.Empty : builder.ToImmutable(); return builder.Count == 0 ? ImmutableArray<RawReference>.Empty : builder.ToImmutable();
} }
private static JsonElement NormalizeRawContent(JsonElement element) private static JsonElement NormalizeRawContent(JsonElement element)
{ {
var json = element.ValueKind == JsonValueKind.Undefined ? "{}" : element.GetRawText(); var json = element.ValueKind == JsonValueKind.Undefined ? "{}" : element.GetRawText();
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json); using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json);
return document.RootElement.Clone(); return document.RootElement.Clone();
} }
}
private static string NormalizeAdvisoryKey(string? primaryId, ImmutableArray<string> aliases, string upstreamId)
{
if (!string.IsNullOrWhiteSpace(primaryId))
{
return primaryId.Trim();
}
foreach (var alias in aliases)
{
if (!string.IsNullOrWhiteSpace(alias))
{
return alias.Trim();
}
}
return string.IsNullOrWhiteSpace(upstreamId) ? string.Empty : upstreamId.Trim();
}
private static ImmutableArray<RawLink> BuildLinks(string advisoryKey, ImmutableArray<string> aliases, string upstreamId)
{
var builder = ImmutableArray.CreateBuilder<RawLink>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddLink(string scheme, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
var normalized = value.Trim();
var key = $"{scheme}:{normalized}";
if (seen.Add(key))
{
builder.Add(new RawLink(scheme, normalized));
}
}
AddLink("PRIMARY", advisoryKey);
foreach (var alias in aliases)
{
AddLink("ALIAS", alias);
}
AddLink("UPSTREAM", upstreamId);
return builder.Count == 0 ? ImmutableArray<RawLink>.Empty : builder.ToImmutable();
}
}

View File

@@ -10,10 +10,11 @@ using OpenTelemetry.Trace;
using Serilog; using Serilog;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Connector.Common.Telemetry; using StellaOps.Concelier.Connector.Common.Telemetry;
using StellaOps.Concelier.WebService.Diagnostics; using StellaOps.Concelier.WebService.Diagnostics;
using StellaOps.Concelier.WebService.Options; using StellaOps.Concelier.WebService.Options;
using StellaOps.Ingestion.Telemetry;
namespace StellaOps.Concelier.WebService.Extensions; namespace StellaOps.Concelier.WebService.Extensions;
@@ -65,13 +66,14 @@ public static class TelemetryExtensions
if (telemetry.EnableTracing) if (telemetry.EnableTracing)
{ {
openTelemetry.WithTracing(tracing => openTelemetry.WithTracing(tracing =>
{ {
tracing tracing
.AddSource(JobDiagnostics.ActivitySourceName) .AddSource(JobDiagnostics.ActivitySourceName)
.AddSource(SourceDiagnostics.ActivitySourceName) .AddSource(SourceDiagnostics.ActivitySourceName)
.AddAspNetCoreInstrumentation() .AddSource(IngestionTelemetry.ActivitySourceName)
.AddHttpClientInstrumentation(); .AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
ConfigureExporters(telemetry, tracing); ConfigureExporters(telemetry, tracing);
}); });
@@ -84,7 +86,7 @@ public static class TelemetryExtensions
metrics metrics
.AddMeter(JobDiagnostics.MeterName) .AddMeter(JobDiagnostics.MeterName)
.AddMeter(SourceDiagnostics.MeterName) .AddMeter(SourceDiagnostics.MeterName)
.AddMeter(IngestionMetrics.MeterName) .AddMeter(IngestionTelemetry.MeterName)
.AddMeter("StellaOps.Concelier.Connector.CertBund") .AddMeter("StellaOps.Concelier.Connector.CertBund")
.AddMeter("StellaOps.Concelier.Connector.Nvd") .AddMeter("StellaOps.Concelier.Connector.Nvd")
.AddMeter("StellaOps.Concelier.Connector.Vndr.Chromium") .AddMeter("StellaOps.Concelier.Connector.Vndr.Chromium")

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StellaOps.Configuration;
namespace StellaOps.Concelier.WebService.Options; namespace StellaOps.Concelier.WebService.Options;
@@ -19,6 +20,8 @@ public sealed class ConcelierOptions
public FeaturesOptions Features { get; set; } = new(); public FeaturesOptions Features { get; set; } = new();
public AdvisoryChunkOptions AdvisoryChunks { get; set; } = new(); public AdvisoryChunkOptions AdvisoryChunks { get; set; } = new();
public StellaOpsCryptoOptions Crypto { get; } = new();
public sealed class StorageOptions public sealed class StorageOptions
{ {

View File

@@ -82,6 +82,8 @@ builder.Services.AddOptions<ConcelierOptions>()
}) })
.ValidateOnStart(); .ValidateOnStart();
builder.Services.AddStellaOpsCrypto(concelierOptions.Crypto);
builder.ConfigureConcelierTelemetry(concelierOptions); builder.ConfigureConcelierTelemetry(concelierOptions);
builder.Services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System); builder.Services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
@@ -387,6 +389,14 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
return authorizationError; return authorizationError;
} }
using var ingestScope = logger.BeginScope(new Dictionary<string, object?>(StringComparer.Ordinal)
{
["tenant"] = tenant,
["source.vendor"] = ingestRequest.Source.Vendor,
["upstream.upstreamId"] = ingestRequest.Upstream.UpstreamId,
["contentHash"] = ingestRequest.Upstream.ContentHash ?? "(null)"
});
AdvisoryRawDocument document; AdvisoryRawDocument document;
try try
{ {
@@ -423,12 +433,12 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
context.Response.Headers.Location = $"/advisories/raw/{Uri.EscapeDataString(result.Record.Id)}"; context.Response.Headers.Location = $"/advisories/raw/{Uri.EscapeDataString(result.Record.Id)}";
} }
IngestionMetrics.WriteCounter.Add(1, new[] IngestionMetrics.IngestionWriteCounter.Add(
{ 1,
new KeyValuePair<string, object?>("tenant", tenant), IngestionMetrics.BuildWriteTags(
new KeyValuePair<string, object?>("source", result.Record.Document.Source.Vendor), tenant,
new KeyValuePair<string, object?>("result", result.Inserted ? "inserted" : "duplicate") ingestRequest.Source.Vendor ?? "(unknown)",
}); result.Inserted ? "inserted" : "duplicate"));
return JsonResult(response, statusCode); return JsonResult(response, statusCode);
} }
@@ -443,12 +453,12 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
string.IsNullOrWhiteSpace(document.Upstream.ContentHash) ? "(empty)" : document.Upstream.ContentHash, string.IsNullOrWhiteSpace(document.Upstream.ContentHash) ? "(empty)" : document.Upstream.ContentHash,
string.Join(',', guardException.Violations.Select(static violation => violation.ErrorCode))); string.Join(',', guardException.Violations.Select(static violation => violation.ErrorCode)));
IngestionMetrics.ViolationCounter.Add(1, new[] IngestionMetrics.IngestionWriteCounter.Add(
{ 1,
new KeyValuePair<string, object?>("tenant", tenant), IngestionMetrics.BuildWriteTags(
new KeyValuePair<string, object?>("source", document.Source.Vendor), tenant,
new KeyValuePair<string, object?>("code", guardException.PrimaryErrorCode) ingestRequest.Source.Vendor ?? "(unknown)",
}); "rejected"));
return MapAocGuardException(context, guardException); return MapAocGuardException(context, guardException);
} }
@@ -467,25 +477,8 @@ advisoryIngestEndpoint.RequireAocGuard<AdvisoryIngestRequest>(request =>
return Array.Empty<object?>(); return Array.Empty<object?>();
} }
var linkset = request.Linkset ?? new AdvisoryLinksetRequest( var guardDocument = AdvisoryRawRequestMapper.Map(request, "guard-tenant", TimeProvider.System);
Array.Empty<string>(), return new object?[] { guardDocument };
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<AdvisoryLinksetReferenceRequest>(),
Array.Empty<string>(),
new Dictionary<string, string>(StringComparer.Ordinal));
var payload = new
{
tenant = "guard-tenant",
source = request.Source,
upstream = request.Upstream,
content = request.Content,
identifiers = request.Identifiers,
linkset
};
return new object?[] { payload };
}, guardOptions: advisoryIngestGuardOptions); }, guardOptions: advisoryIngestGuardOptions);
if (authorityConfigured) if (authorityConfigured)
@@ -796,11 +789,9 @@ var aocVerifyEndpoint = app.MapPost("/aoc/verify", async (
var verificationOutcome = response.Truncated var verificationOutcome = response.Truncated
? "truncated" ? "truncated"
: (violationResponses.Length == 0 ? "ok" : "violations"); : (violationResponses.Length == 0 ? "ok" : "violations");
IngestionMetrics.VerificationCounter.Add(1, new[] IngestionMetrics.VerificationCounter.Add(
{ 1,
new KeyValuePair<string, object?>("tenant", tenant), IngestionMetrics.BuildVerifyTags(tenant, verificationOutcome));
new KeyValuePair<string, object?>("result", verificationOutcome)
});
return JsonResult(response); return JsonResult(response);
}); });

View File

@@ -1,7 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
@@ -10,6 +9,7 @@ using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.WebService.Services; namespace StellaOps.Concelier.WebService.Services;
@@ -28,14 +28,18 @@ internal sealed class OpenApiDiscoveryDocumentProvider
]; ];
private readonly EndpointDataSource _endpointDataSource; private readonly EndpointDataSource _endpointDataSource;
private readonly ICryptoHash _hash;
private readonly object _syncRoot = new(); private readonly object _syncRoot = new();
private string? _cachedDocumentJson; private string? _cachedDocumentJson;
private string? _cachedEtag; private string? _cachedEtag;
public OpenApiDiscoveryDocumentProvider(EndpointDataSource endpointDataSource) public OpenApiDiscoveryDocumentProvider(
EndpointDataSource endpointDataSource,
ICryptoHash hash)
{ {
_endpointDataSource = endpointDataSource; _endpointDataSource = endpointDataSource ?? throw new ArgumentNullException(nameof(endpointDataSource));
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
} }
public (string Payload, string ETag) GetDocument() public (string Payload, string ETag) GetDocument()
@@ -58,7 +62,7 @@ internal sealed class OpenApiDiscoveryDocumentProvider
}); });
var bytes = Encoding.UTF8.GetBytes(json); var bytes = Encoding.UTF8.GetBytes(json);
var hash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); var hash = _hash.ComputeHashHex(bytes);
var computedEtag = $"\"{hash}\""; var computedEtag = $"\"{hash}\"";
_cachedDocumentJson = json; _cachedDocumentJson = json;

View File

@@ -30,6 +30,8 @@
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" /> <ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" /> <ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" /> <ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" /> <ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />

View File

@@ -1,94 +1,98 @@
# TASKS — Epic 1: Aggregation-Only Contract # TASKS — Epic 1: Aggregation-Only Contract
> **AOC Reminder:** service links and exposes raw data only—no precedence, severity, or hint computation inside Concelier APIs. > **AOC Reminder:** service links and exposes raw data only—no precedence, severity, or hint computation inside Concelier APIs.
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|---|---|---|---|---| |---|---|---|---|---|
> Docs alignment (2025-10-26): Endpoint expectations + scope requirements detailed in `docs/ingestion/aggregation-only-contract.md` and `docs/security/authority-scopes.md`. > Docs alignment (2025-10-26): Endpoint expectations + scope requirements detailed in `docs/ingestion/aggregation-only-contract.md` and `docs/security/authority-scopes.md`.
> 2025-10-28: Added coverage for pagination, tenancy enforcement, and ingestion/verification metrics; verified guard handling paths end-to-end. > 2025-10-28: Added coverage for pagination, tenancy enforcement, and ingestion/verification metrics; verified guard handling paths end-to-end.
| CONCELIER-WEB-AOC-19-002 `AOC observability` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-WEB-AOC-19-001 | Emit `ingestion_write_total`, `aoc_violation_total`, latency histograms, and tracing spans (`ingest.fetch/transform/write`, `aoc.guard`). Wire structured logging to include tenant, source vendor, upstream id, and content hash. | | CONCELIER-WEB-AOC-19-002 `AOC observability` | DONE (2025-11-07) | Concelier WebService Guild, Observability Guild | CONCELIER-WEB-AOC-19-001 | Emit `ingestion_write_total`, `aoc_violation_total`, latency histograms, and tracing spans (`ingest.fetch/transform/write`, `aoc.guard`). Wire structured logging to include tenant, source vendor, upstream id, and content hash. |
> Docs alignment (2025-10-26): Metrics/traces/log schema in `docs/observability/observability.md`. > Docs alignment (2025-10-26): Metrics/traces/log schema in `docs/observability/observability.md`.
| CONCELIER-WEB-AOC-19-003 `Schema/guard unit tests` | TODO | QA Guild | CONCELIER-WEB-AOC-19-001 | Add unit tests covering schema validation failures, forbidden field rejections (`ERR_AOC_001/002/006/007`), idempotent upserts, and supersedes chains using deterministic fixtures. | | CONCELIER-WEB-AOC-19-003 `Schema/guard unit tests` | TODO | QA Guild | CONCELIER-WEB-AOC-19-001 | Add unit tests covering schema validation failures, forbidden field rejections (`ERR_AOC_001/002/006/007`), idempotent upserts, and supersedes chains using deterministic fixtures. |
> Docs alignment (2025-10-26): Guard rules + error codes documented in AOC reference §5 and CLI guide. > Docs alignment (2025-10-26): Guard rules + error codes documented in AOC reference §5 and CLI guide.
| CONCELIER-WEB-AOC-19-004 `End-to-end ingest verification` | TODO | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-003, CONCELIER-CORE-AOC-19-002 | Create integration tests ingesting large advisory batches (cold/warm) validating linkset enrichment, metrics emission, and reproducible outputs. Capture load-test scripts + doc notes for Offline Kit dry runs. | | CONCELIER-WEB-AOC-19-004 `End-to-end ingest verification` | TODO | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-003, CONCELIER-CORE-AOC-19-002 | Create integration tests ingesting large advisory batches (cold/warm) validating linkset enrichment, metrics emission, and reproducible outputs. Capture load-test scripts + doc notes for Offline Kit dry runs. |
> Docs alignment (2025-10-26): Offline verification workflow referenced in `docs/deploy/containers.md` §5. > Docs alignment (2025-10-26): Offline verification workflow referenced in `docs/deploy/containers.md` §5.
| CONCELIER-WEB-AOC-19-005 `Chunk evidence regression` | TODO (2025-11-08) | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-002 | Fix `/advisories/{key}/chunks` seeded fixtures so AdvisoryChunksEndpoint tests stop returning 404/not-found when raw documents are pre-populated; ensure Mongo migrations no longer emit “Unable to locate advisory_raw documents” during test boot. |
## Policy Engine v2 | CONCELIER-WEB-AOC-19-006 `Allowlist ingest auth parity` | TODO (2025-11-08) | Concelier WebService Guild | CONCELIER-WEB-AOC-19-002 | Align WebService auth defaults with the test tokens so the allowlisted tenant can create an advisory before forbidden tenants are rejected in `AdvisoryIngestEndpoint_RejectsTenantOutsideAllowlist`. |
| CONCELIER-WEB-AOC-19-007 `AOC verify violation codes` | TODO (2025-11-08) | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-002 | Update AOC verify logic/fixtures so guard failures produce the expected `ERR_AOC_001` payload (current regression returns `ERR_AOC_004`) while keeping the mapper/guard parity exercised by the new tests. |
| ID | Status | Owner(s) | Depends on | Notes | | CONCELIER-CRYPTO-90-001 `Crypto provider adoption` | DOING (2025-11-08) | Concelier WebService Guild, Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Route hashing/signing in OpenAPI discovery, Mirror connectors, and RU advisory adapters through `ICryptoProviderRegistry` so RootPack_RU uses CryptoPro/PKCS#11 keys. Reference `docs/security/crypto-routing-audit-2025-11-07.md`. |
|----|--------|----------|------------|-------|
| CONCELIER-POLICY-20-001 `Policy selection endpoints` | TODO | Concelier WebService Guild | WEB-POLICY-20-001, CONCELIER-CORE-AOC-19-004 | Add batch advisory lookup APIs (`/policy/select/advisories`, `/policy/select/vex`) optimized for PURL/ID lists with pagination, tenant scoping, and explain metadata. | ## Policy Engine v2
## StellaOps Console (Sprint 23) | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| ID | Status | Owner(s) | Depends on | Notes | | CONCELIER-POLICY-20-001 `Policy selection endpoints` | TODO | Concelier WebService Guild | WEB-POLICY-20-001, CONCELIER-CORE-AOC-19-004 | Add batch advisory lookup APIs (`/policy/select/advisories`, `/policy/select/vex`) optimized for PURL/ID lists with pagination, tenant scoping, and explain metadata. |
## StellaOps Console (Sprint 23)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-CONSOLE-23-001 `Advisory aggregation views` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-201, CONCELIER-LNM-21-202 | Expose `/console/advisories` endpoints returning aggregation groups (per linkset) with source chips, provider-reported severity columns (no local consensus), and provenance metadata for Console list + dashboard cards. Support filters by source, ecosystem, published/modified window, tenant enforcement. | | CONCELIER-CONSOLE-23-001 `Advisory aggregation views` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-201, CONCELIER-LNM-21-202 | Expose `/console/advisories` endpoints returning aggregation groups (per linkset) with source chips, provider-reported severity columns (no local consensus), and provenance metadata for Console list + dashboard cards. Support filters by source, ecosystem, published/modified window, tenant enforcement. |
| CONCELIER-CONSOLE-23-002 `Dashboard deltas API` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001, CONCELIER-LNM-21-203 | Provide aggregated advisory delta counts (new, modified, conflicting) for Console dashboard + live status ticker; emit structured events for queue lag metrics. Ensure deterministic counts across repeated queries. | | CONCELIER-CONSOLE-23-002 `Dashboard deltas API` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001, CONCELIER-LNM-21-203 | Provide aggregated advisory delta counts (new, modified, conflicting) for Console dashboard + live status ticker; emit structured events for queue lag metrics. Ensure deterministic counts across repeated queries. |
| CONCELIER-CONSOLE-23-003 `Search fan-out helpers` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001 | Deliver fast lookup endpoints for CVE/GHSA/purl search (linksets, observations) returning evidence fragments for Console global search; implement caching + scope guards. | | CONCELIER-CONSOLE-23-003 `Search fan-out helpers` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001 | Deliver fast lookup endpoints for CVE/GHSA/purl search (linksets, observations) returning evidence fragments for Console global search; implement caching + scope guards. |
## Graph Explorer v1 ## Graph Explorer v1
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
## Link-Not-Merge v1 ## Link-Not-Merge v1
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-LNM-21-201 `Observation APIs` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-001 | Add REST endpoints for advisory observations (`GET /advisories/observations`) with filters (alias, purl, source), pagination, and tenancy enforcement. | | CONCELIER-LNM-21-201 `Observation APIs` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-001 | Add REST endpoints for advisory observations (`GET /advisories/observations`) with filters (alias, purl, source), pagination, and tenancy enforcement. |
| CONCELIER-LNM-21-202 `Linkset APIs` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-002, CONCELIER-LNM-21-003 | Implement linkset read/export endpoints (`/advisories/linksets/{id}`, `/advisories/by-purl/{purl}`, `/advisories/linksets/{id}/export`, `/evidence`) with correlation/conflict payloads and `ERR_AGG_*` mapping. | | CONCELIER-LNM-21-202 `Linkset APIs` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-002, CONCELIER-LNM-21-003 | Implement linkset read/export endpoints (`/advisories/linksets/{id}`, `/advisories/by-purl/{purl}`, `/advisories/linksets/{id}/export`, `/evidence`) with correlation/conflict payloads and `ERR_AGG_*` mapping. |
| CONCELIER-LNM-21-203 `Ingest events` | TODO | Concelier WebService Guild, Platform Events Guild | CONCELIER-LNM-21-005 | Publish NATS/Redis events for new observations/linksets and ensure idempotent consumer contracts; document event schemas. | | CONCELIER-LNM-21-203 `Ingest events` | TODO | Concelier WebService Guild, Platform Events Guild | CONCELIER-LNM-21-005 | Publish NATS/Redis events for new observations/linksets and ensure idempotent consumer contracts; document event schemas. |
## Graph & Vuln Explorer v1 ## Graph & Vuln Explorer v1
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-GRAPH-24-101 `Advisory summary API` | TODO | Concelier WebService Guild | CONCELIER-GRAPH-24-001 | Expose `/advisories/summary` returning raw linkset/observation metadata for overlay services; no derived severity or fix hints. | | CONCELIER-GRAPH-24-101 `Advisory summary API` | TODO | Concelier WebService Guild | CONCELIER-GRAPH-24-001 | Expose `/advisories/summary` returning raw linkset/observation metadata for overlay services; no derived severity or fix hints. |
| CONCELIER-GRAPH-28-102 `Evidence batch API` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-201 | Add batch fetch for advisory observations/linksets keyed by component sets to feed Graph overlay tooltips efficiently. | | CONCELIER-GRAPH-28-102 `Evidence batch API` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-201 | Add batch fetch for advisory observations/linksets keyed by component sets to feed Graph overlay tooltips efficiently. |
## VEX Lens (Sprint 30) ## VEX Lens (Sprint 30)
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-VEXLENS-30-001 `Advisory rationale bridges` | TODO | Concelier WebService Guild, VEX Lens Guild | CONCELIER-VULN-29-001, VEXLENS-30-005 | Guarantee advisory key consistency and cross-links for consensus rationale; Label: VEX-Lens. | | CONCELIER-VEXLENS-30-001 `Advisory rationale bridges` | TODO | Concelier WebService Guild, VEX Lens Guild | CONCELIER-VULN-29-001, VEXLENS-30-005 | Guarantee advisory key consistency and cross-links for consensus rationale; Label: VEX-Lens. |
## Vulnerability Explorer (Sprint 29) ## Vulnerability Explorer (Sprint 29)
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-VULN-29-001 `Advisory key canonicalization` | DONE (2025-11-07) | Concelier WebService Guild, Data Integrity Guild | CONCELIER-LNM-21-001 | Canonicalize (lossless) advisory identifiers (CVE/GHSA/vendor) into `advisory_key`, persist `links[]`, expose raw payload snapshots for Explorer evidence tabs; AOC-compliant: no merge, no derived fields, no suppression. Include migration/backfill scripts. | | CONCELIER-VULN-29-001 `Advisory key canonicalization` | DONE (2025-11-07) | Concelier WebService Guild, Data Integrity Guild | CONCELIER-LNM-21-001 | Canonicalize (lossless) advisory identifiers (CVE/GHSA/vendor) into `advisory_key`, persist `links[]`, expose raw payload snapshots for Explorer evidence tabs; AOC-compliant: no merge, no derived fields, no suppression. Include migration/backfill scripts. |
| CONCELIER-VULN-29-002 `Evidence retrieval API` | DOING (2025-11-07) | Concelier WebService Guild | CONCELIER-VULN-29-001, VULN-API-29-003 | Provide `/vuln/evidence/advisories/{advisory_key}` returning raw advisory docs with provenance, filtering by tenant and source. | | CONCELIER-VULN-29-002 `Evidence retrieval API` | DOING (2025-11-07) | Concelier WebService Guild | CONCELIER-VULN-29-001, VULN-API-29-003 | Provide `/vuln/evidence/advisories/{advisory_key}` returning raw advisory docs with provenance, filtering by tenant and source. |
| CONCELIER-VULN-29-004 `Observability enhancements` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-VULN-29-001 | Instrument metrics/logs for observation + linkset pipelines (identifier collisions, withdrawn flags) and emit events consumed by Vuln Explorer resolver. | | CONCELIER-VULN-29-004 `Observability enhancements` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-VULN-29-001 | Instrument metrics/logs for observation + linkset pipelines (identifier collisions, withdrawn flags) and emit events consumed by Vuln Explorer resolver. |
## Advisory AI (Sprint 31) ## Advisory AI (Sprint 31)
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-AIAI-31-001 `Paragraph anchors` | DONE | Concelier WebService Guild | CONCELIER-VULN-29-001 | Expose advisory chunk API returning paragraph anchors, section metadata, and token-safe text for Advisory AI retrieval. See docs/updates/2025-11-07-concelier-advisory-chunks.md. | | CONCELIER-AIAI-31-001 `Paragraph anchors` | DONE | Concelier WebService Guild | CONCELIER-VULN-29-001 | Expose advisory chunk API returning paragraph anchors, section metadata, and token-safe text for Advisory AI retrieval. See docs/updates/2025-11-07-concelier-advisory-chunks.md. |
| CONCELIER-AIAI-31-002 `Structured fields` | TODO | Concelier WebService Guild | CONCELIER-AIAI-31-001 | Ensure observation APIs expose upstream workaround/fix/CVSS fields with provenance; add caching for summary queries. | | CONCELIER-AIAI-31-002 `Structured fields` | TODO | Concelier WebService Guild | CONCELIER-AIAI-31-001 | Ensure observation APIs expose upstream workaround/fix/CVSS fields with provenance; add caching for summary queries. |
| CONCELIER-AIAI-31-003 `Advisory AI telemetry` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-AIAI-31-001 | Emit metrics/logs for chunk requests, cache hits, and guardrail blocks triggered by advisory payloads. | | CONCELIER-AIAI-31-003 `Advisory AI telemetry` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-AIAI-31-001 | Emit metrics/logs for chunk requests, cache hits, and guardrail blocks triggered by advisory payloads. |
## Observability & Forensics (Epic 15) ## Observability & Forensics (Epic 15)
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-WEB-OBS-50-001 `Telemetry adoption` | TODO | Concelier WebService Guild | TELEMETRY-OBS-50-001, CONCELIER-OBS-50-001 | Adopt telemetry core in web service host, ensure ingest + read endpoints emit trace/log fields (`tenant_id`, `route`, `decision_effect`), and add correlation IDs to responses. | | CONCELIER-WEB-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | Concelier WebService Guild | TELEMETRY-OBS-50-001, CONCELIER-OBS-50-001 | Adopt telemetry core in web service host, ensure ingest + read endpoints emit trace/log fields (`tenant_id`, `route`, `decision_effect`), and add correlation IDs to responses. |
| CONCELIER-WEB-OBS-51-001 `Observability APIs` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, WEB-OBS-51-001 | Surface ingest health metrics, queue depth, and SLO status via `/obs/concelier/health` endpoint for Console widgets, with caching and tenant partitioning. | | CONCELIER-WEB-OBS-51-001 `Observability APIs` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, WEB-OBS-51-001 | Surface ingest health metrics, queue depth, and SLO status via `/obs/concelier/health` endpoint for Console widgets, with caching and tenant partitioning. |
| CONCELIER-WEB-OBS-52-001 `Timeline streaming` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, TIMELINE-OBS-52-003 | Provide SSE stream `/obs/concelier/timeline` bridging to Timeline Indexer with paging tokens, guardrails, and audit logging. | | CONCELIER-WEB-OBS-52-001 `Timeline streaming` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, TIMELINE-OBS-52-003 | Provide SSE stream `/obs/concelier/timeline` bridging to Timeline Indexer with paging tokens, guardrails, and audit logging. |
| CONCELIER-WEB-OBS-53-001 `Evidence locker integration` | TODO | Concelier WebService Guild, Evidence Locker Guild | CONCELIER-OBS-53-001, EVID-OBS-53-003 | Add `/evidence/advisories/*` routes invoking evidence locker snapshots, verifying tenant scopes (`evidence:read`), and returning signed manifest metadata. | | CONCELIER-WEB-OBS-53-001 `Evidence locker integration` | TODO | Concelier WebService Guild, Evidence Locker Guild | CONCELIER-OBS-53-001, EVID-OBS-53-003 | Add `/evidence/advisories/*` routes invoking evidence locker snapshots, verifying tenant scopes (`evidence:read`), and returning signed manifest metadata. |
| CONCELIER-WEB-OBS-54-001 `Attestation exposure` | TODO | Concelier WebService Guild | CONCELIER-OBS-54-001, PROV-OBS-54-001 | Provide `/attestations/advisories/*` read APIs surfacing DSSE status, verification summary, and provenance chain for Console/CLI. | | CONCELIER-WEB-OBS-54-001 `Attestation exposure` | TODO | Concelier WebService Guild | CONCELIER-OBS-54-001, PROV-OBS-54-001 | Provide `/attestations/advisories/*` read APIs surfacing DSSE status, verification summary, and provenance chain for Console/CLI. |
| CONCELIER-WEB-OBS-55-001 `Incident mode toggles` | TODO | Concelier WebService Guild, DevOps Guild | CONCELIER-OBS-55-001, WEB-OBS-55-001 | Implement incident mode toggle endpoints, propagate to orchestrator/locker, and document cooldown/backoff semantics. | | CONCELIER-WEB-OBS-55-001 `Incident mode toggles` | TODO | Concelier WebService Guild, DevOps Guild | CONCELIER-OBS-55-001, WEB-OBS-55-001 | Implement incident mode toggle endpoints, propagate to orchestrator/locker, and document cooldown/backoff semantics. |
## Air-Gapped Mode (Epic 16) ## Air-Gapped Mode (Epic 16)
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-WEB-AIRGAP-56-001 `Mirror import APIs` | TODO | Concelier WebService Guild | AIRGAP-IMP-58-001, CONCELIER-AIRGAP-56-001 | Extend ingestion endpoints to register mirror bundle sources, expose bundle catalog queries, and block external feed URLs in sealed mode. | | CONCELIER-WEB-AIRGAP-56-001 `Mirror import APIs` | TODO | Concelier WebService Guild | AIRGAP-IMP-58-001, CONCELIER-AIRGAP-56-001 | Extend ingestion endpoints to register mirror bundle sources, expose bundle catalog queries, and block external feed URLs in sealed mode. |
| CONCELIER-WEB-AIRGAP-56-002 `Airgap status surfaces` | TODO | Concelier WebService Guild | CONCELIER-AIRGAP-57-002, AIRGAP-CTL-56-002 | Add staleness metadata and bundle provenance to advisory APIs (`/advisories/observations`, `/advisories/linksets`). | | CONCELIER-WEB-AIRGAP-56-002 `Airgap status surfaces` | TODO | Concelier WebService Guild | CONCELIER-AIRGAP-57-002, AIRGAP-CTL-56-002 | Add staleness metadata and bundle provenance to advisory APIs (`/advisories/observations`, `/advisories/linksets`). |
| CONCELIER-WEB-AIRGAP-57-001 `Error remediation` | TODO | Concelier WebService Guild, AirGap Policy Guild | AIRGAP-POL-56-001 | Map sealed-mode violations to `AIRGAP_EGRESS_BLOCKED` responses with user guidance. | | CONCELIER-WEB-AIRGAP-57-001 `Error remediation` | TODO | Concelier WebService Guild, AirGap Policy Guild | AIRGAP-POL-56-001 | Map sealed-mode violations to `AIRGAP_EGRESS_BLOCKED` responses with user guidance. |
| CONCELIER-WEB-AIRGAP-58-001 `Import timeline emission` | TODO | Concelier WebService Guild, AirGap Importer Guild | CONCELIER-WEB-AIRGAP-56-001, TIMELINE-OBS-53-001 | Emit timeline events for bundle ingestion operations with bundle ID, scope, and actor metadata. | | CONCELIER-WEB-AIRGAP-58-001 `Import timeline emission` | TODO | Concelier WebService Guild, AirGap Importer Guild | CONCELIER-WEB-AIRGAP-56-001, TIMELINE-OBS-53-001 | Emit timeline events for bundle ingestion operations with bundle ID, scope, and actor metadata. |
## SDKs & OpenAPI (Epic 17) ## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Notes | | ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------| |----|--------|----------|------------|-------|
| CONCELIER-WEB-OAS-61-001 `/.well-known/openapi` | DONE (2025-11-02) | Concelier WebService Guild | OAS-61-001 | Implement discovery endpoint emitting Concelier spec with version metadata and ETag. | | CONCELIER-WEB-OAS-61-001 `/.well-known/openapi` | DONE (2025-11-02) | Concelier WebService Guild | OAS-61-001 | Implement discovery endpoint emitting Concelier spec with version metadata and ETag. |
| CONCELIER-WEB-OAS-61-002 `Error envelope migration` | TODO | Concelier WebService Guild | APIGOV-61-001 | Ensure all API responses use standardized error envelope; update controllers/tests. | | CONCELIER-WEB-OAS-61-002 `Error envelope migration` | TODO | Concelier WebService Guild | APIGOV-61-001 | Ensure all API responses use standardized error envelope; update controllers/tests. |
| CONCELIER-WEB-OAS-62-001 `Examples expansion` | TODO | Concelier WebService Guild | CONCELIER-OAS-61-002 | Add curated examples for advisory observations/linksets/conflicts; integrate into dev portal. | | CONCELIER-WEB-OAS-62-001 `Examples expansion` | TODO | Concelier WebService Guild | CONCELIER-OAS-61-002 | Add curated examples for advisory observations/linksets/conflicts; integrate into dev portal. |
| CONCELIER-WEB-OAS-63-001 `Deprecation headers` | TODO | Concelier WebService Guild, API Governance Guild | APIGOV-63-001 | Add Sunset/Deprecation headers for retiring endpoints and update documentation/notifications. | | CONCELIER-WEB-OAS-63-001 `Deprecation headers` | TODO | Concelier WebService Guild, API Governance Guild | APIGOV-63-001 | Add Sunset/Deprecation headers for retiring endpoints and update documentation/notifications. |

View File

@@ -187,6 +187,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Analyzers", "__Analyzers"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Analyzers", "__Analyzers\StellaOps.Concelier.Analyzers\StellaOps.Concelier.Analyzers.csproj", "{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Analyzers", "__Analyzers\StellaOps.Concelier.Analyzers\StellaOps.Concelier.Analyzers.csproj", "{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -1265,6 +1267,18 @@ Global
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x64.Build.0 = Release|Any CPU {39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x64.Build.0 = Release|Any CPU
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x86.ActiveCfg = Release|Any CPU {39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x86.ActiveCfg = Release|Any CPU
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x86.Build.0 = Release|Any CPU {39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x86.Build.0 = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x64.ActiveCfg = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x64.Build.0 = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x86.ActiveCfg = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x86.Build.0 = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|Any CPU.Build.0 = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x64.ActiveCfg = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x64.Build.0 = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x86.ActiveCfg = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -1349,5 +1363,6 @@ Global
{9006A5A2-01D8-4A70-AEA7-B7B1987C4A62} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642} {9006A5A2-01D8-4A70-AEA7-B7B1987C4A62} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{664A2577-6DA1-42DA-A213-3253017FA4BF} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642} {664A2577-6DA1-42DA-A213-3253017FA4BF} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F} = {176B5A8A-7857-3ECD-1128-3C721BC7F5C6} {39C1D44C-389F-4502-ADCF-E4AC359E8F8F} = {176B5A8A-7857-3ECD-1128-3C721BC7F5C6}
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -25,17 +25,18 @@ namespace StellaOps.Concelier.Connector.Cccs;
public sealed class CccsConnector : IFeedConnector public sealed class CccsConnector : IFeedConnector
{ {
private static readonly JsonSerializerOptions RawSerializerOptions = new(JsonSerializerDefaults.Web) private static readonly JsonSerializerOptions RawSerializerOptions = new(JsonSerializerDefaults.Web)
{ {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
}; };
private static readonly JsonSerializerOptions DtoSerializerOptions = new(JsonSerializerDefaults.Web) private static readonly JsonSerializerOptions DtoSerializerOptions = new(JsonSerializerDefaults.Web)
{ {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
}; };
private const string DtoSchemaVersion = "cccs.dto.v1"; private static readonly Uri CanonicalBaseUri = new("https://www.cyber.gc.ca", UriKind.Absolute);
private const string DtoSchemaVersion = "cccs.dto.v1";
private readonly CccsFeedClient _feedClient; private readonly CccsFeedClient _feedClient;
private readonly RawDocumentStorage _rawDocumentStorage; private readonly RawDocumentStorage _rawDocumentStorage;
@@ -482,24 +483,37 @@ public sealed class CccsConnector : IFeedConnector
} }
} }
private static string BuildDocumentUri(CccsFeedItem item, CccsFeedEndpoint feed) private static string BuildDocumentUri(CccsFeedItem item, CccsFeedEndpoint feed)
{ {
if (!string.IsNullOrWhiteSpace(item.Url)) var candidate = item.Url?.Trim();
{ if (!string.IsNullOrWhiteSpace(candidate))
if (Uri.TryCreate(item.Url, UriKind.Absolute, out var absolute)) {
{ if (Uri.TryCreate(candidate, UriKind.Absolute, out var absolute))
return absolute.ToString(); {
} if (IsHttpScheme(absolute.Scheme))
{
var baseUri = new Uri("https://www.cyber.gc.ca", UriKind.Absolute); return absolute.ToString();
if (Uri.TryCreate(baseUri, item.Url, out var combined)) }
{
return combined.ToString(); candidate = absolute.PathAndQuery;
} if (!string.IsNullOrEmpty(absolute.Fragment))
} {
candidate += absolute.Fragment;
return $"https://www.cyber.gc.ca/api/cccs/threats/{feed.Language}/{item.Nid}"; }
} }
if (!string.IsNullOrWhiteSpace(candidate) && Uri.TryCreate(CanonicalBaseUri, candidate, out var combined))
{
return combined.ToString();
}
}
return new Uri(CanonicalBaseUri, $"/api/cccs/threats/{feed.Language}/{item.Nid}").ToString();
}
private static bool IsHttpScheme(string? scheme)
=> string.Equals(scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
|| string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
private static CccsRawAdvisoryDocument CreateRawDocument(CccsFeedItem item, CccsFeedEndpoint feed, IReadOnlyDictionary<int, string> taxonomy) private static CccsRawAdvisoryDocument CreateRawDocument(CccsFeedItem item, CccsFeedEndpoint feed, IReadOnlyDictionary<int, string> taxonomy)
{ {

View File

@@ -125,11 +125,16 @@ public sealed class CccsFeedEndpoint
throw new InvalidOperationException("Feed endpoint URI must be configured before building taxonomy URI."); throw new InvalidOperationException("Feed endpoint URI must be configured before building taxonomy URI.");
} }
var language = Uri.GetQueryParameterValueOrDefault("lang", Language); var language = Uri.GetQueryParameterValueOrDefault("lang", Language);
var builder = $"https://www.cyber.gc.ca/api/cccs/taxonomy/v1/get?lang={language}&vocabulary=cccs_alert_type"; var taxonomyBuilder = new UriBuilder(Uri)
return new Uri(builder, UriKind.Absolute); {
} Path = "/api/cccs/taxonomy/v1/get",
} Query = $"lang={language}&vocabulary=cccs_alert_type"
};
return taxonomyBuilder.Uri;
}
}
internal static class CccsUriExtensions internal static class CccsUriExtensions
{ {

View File

@@ -348,19 +348,21 @@ public sealed class CccsHtmlParser
private static string? NormalizeReferenceUrl(string? href, Uri? baseUri, string language) private static string? NormalizeReferenceUrl(string? href, Uri? baseUri, string language)
{ {
if (string.IsNullOrWhiteSpace(href)) if (string.IsNullOrWhiteSpace(href))
{ {
return null; return null;
} }
if (!Uri.TryCreate(href, UriKind.Absolute, out var absolute)) var candidate = href.Trim();
{ var hasAbsolute = Uri.TryCreate(candidate, UriKind.Absolute, out var absolute);
if (baseUri is null || !Uri.TryCreate(baseUri, href, out absolute)) if (!hasAbsolute || string.Equals(absolute.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
{ {
return null; if (baseUri is null || !Uri.TryCreate(baseUri, candidate, out absolute))
} {
} return null;
}
}
var builder = new UriBuilder(absolute) var builder = new UriBuilder(absolute)
{ {
Fragment = string.Empty, Fragment = string.Empty,

View File

@@ -319,12 +319,19 @@ public sealed class KisaDetailParser
} }
var headerRow = labelCell.ParentElement as IHtmlTableRowElement; var headerRow = labelCell.ParentElement as IHtmlTableRowElement;
var columnIndex = labelCell.CellIndex; var columnIndex = headerRow is null
? -1
: Array.FindIndex(headerRow.Cells.ToArray(), cell => ReferenceEquals(cell, labelCell));
if (headerRow is null) if (headerRow is null)
{ {
return null; return null;
} }
if (columnIndex < 0)
{
return null;
}
var rows = ownerTable.Rows.ToArray(); var rows = ownerTable.Rows.ToArray();
var headerIndex = Array.FindIndex(rows, row => ReferenceEquals(row, headerRow)); var headerIndex = Array.FindIndex(rows, row => ReferenceEquals(row, headerRow));
if (headerIndex < 0) if (headerIndex < 0)

View File

@@ -2,10 +2,9 @@ using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Security.Cryptography; using System.Linq;
using System.Linq; using System.Text.Json;
using System.Text.Json; using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -17,10 +16,11 @@ using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Ru.Bdu.Configuration; using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal; using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage.Mongo; using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories; using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos; using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin; using StellaOps.Plugin;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Ru.Bdu; namespace StellaOps.Concelier.Connector.Ru.Bdu;
@@ -44,8 +44,9 @@ public sealed class RuBduConnector : IFeedConnector
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
private readonly ILogger<RuBduConnector> _logger; private readonly ILogger<RuBduConnector> _logger;
private readonly string _cacheDirectory; private readonly string _cacheDirectory;
private readonly string _archiveCachePath; private readonly string _archiveCachePath;
private readonly ICryptoHash _hash;
public RuBduConnector( public RuBduConnector(
SourceFetchService fetchService, SourceFetchService fetchService,
@@ -55,9 +56,10 @@ public sealed class RuBduConnector : IFeedConnector
IAdvisoryStore advisoryStore, IAdvisoryStore advisoryStore,
ISourceStateRepository stateRepository, ISourceStateRepository stateRepository,
IOptions<RuBduOptions> options, IOptions<RuBduOptions> options,
RuBduDiagnostics diagnostics, RuBduDiagnostics diagnostics,
TimeProvider? timeProvider, TimeProvider? timeProvider,
ILogger<RuBduConnector> logger) ILogger<RuBduConnector> logger,
ICryptoHash cryptoHash)
{ {
_fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService)); _fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
_rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage)); _rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
@@ -69,8 +71,9 @@ public sealed class RuBduConnector : IFeedConnector
_options.Validate(); _options.Validate();
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory); _hash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory);
_archiveCachePath = Path.Combine(_cacheDirectory, "vulxml.zip"); _archiveCachePath = Path.Combine(_cacheDirectory, "vulxml.zip");
EnsureCacheDirectory(); EnsureCacheDirectory();
} }
@@ -398,7 +401,7 @@ public sealed class RuBduConnector : IFeedConnector
} }
var payload = JsonSerializer.SerializeToUtf8Bytes(dto, SerializerOptions); var payload = JsonSerializer.SerializeToUtf8Bytes(dto, SerializerOptions);
var sha = Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant(); var sha = _hash.ComputeHashHex(payload);
var documentUri = BuildDocumentUri(dto.Identifier); var documentUri = BuildDocumentUri(dto.Identifier);
var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false); var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false);

View File

@@ -14,6 +14,7 @@
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" /> <ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" /> <ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" /> <ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,23 +4,23 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Net; using System.Net;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Text;
using System.Text; using System.Text.Json;
using System.Text.Json; using System.Text.Json.Serialization;
using System.Text.Json.Serialization; using AngleSharp.Html.Parser;
using AngleSharp.Html.Parser; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options; using MongoDB.Bson;
using MongoDB.Bson; using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration; using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal; using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo; using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Advisories; using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Storage.Mongo.Dtos; using StellaOps.Plugin;
using StellaOps.Plugin; using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Ru.Nkcki; namespace StellaOps.Concelier.Connector.Ru.Nkcki;
@@ -55,11 +55,12 @@ public sealed class RuNkckiConnector : IFeedConnector
private readonly ISourceStateRepository _stateRepository; private readonly ISourceStateRepository _stateRepository;
private readonly RuNkckiOptions _options; private readonly RuNkckiOptions _options;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
private readonly RuNkckiDiagnostics _diagnostics; private readonly RuNkckiDiagnostics _diagnostics;
private readonly ILogger<RuNkckiConnector> _logger; private readonly ILogger<RuNkckiConnector> _logger;
private readonly string _cacheDirectory; private readonly string _cacheDirectory;
private readonly ICryptoHash _hash;
private readonly HtmlParser _htmlParser = new();
private readonly HtmlParser _htmlParser = new();
public RuNkckiConnector( public RuNkckiConnector(
SourceFetchService fetchService, SourceFetchService fetchService,
@@ -69,9 +70,10 @@ public sealed class RuNkckiConnector : IFeedConnector
IAdvisoryStore advisoryStore, IAdvisoryStore advisoryStore,
ISourceStateRepository stateRepository, ISourceStateRepository stateRepository,
IOptions<RuNkckiOptions> options, IOptions<RuNkckiOptions> options,
RuNkckiDiagnostics diagnostics, RuNkckiDiagnostics diagnostics,
TimeProvider? timeProvider, TimeProvider? timeProvider,
ILogger<RuNkckiConnector> logger) ILogger<RuNkckiConnector> logger,
ICryptoHash cryptoHash)
{ {
_fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService)); _fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
_rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage)); _rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
@@ -79,12 +81,13 @@ public sealed class RuNkckiConnector : IFeedConnector
_dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore)); _dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore));
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore)); _advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
_stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository)); _stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options)); _options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
_options.Validate(); _options.Validate();
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory); _hash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory);
EnsureCacheDirectory(); EnsureCacheDirectory();
} }
@@ -597,7 +600,7 @@ public sealed class RuNkckiConnector : IFeedConnector
} }
var payload = JsonSerializer.SerializeToUtf8Bytes(dto, SerializerOptions); var payload = JsonSerializer.SerializeToUtf8Bytes(dto, SerializerOptions);
var sha = Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant(); var sha = _hash.ComputeHashHex(payload);
var documentUri = BuildDocumentUri(dto); var documentUri = BuildDocumentUri(dto);
var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false); var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false);

View File

@@ -18,6 +18,7 @@
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" /> <ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" /> <ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" /> <ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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