save changes
This commit is contained in:
@@ -265,10 +265,12 @@ services:
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Platform__Authority__Issuer: "https://stella-ops.local"
|
||||
Platform__Authority__RequireHttpsMetadata: "false"
|
||||
Platform__Authority__BypassNetworks__0: "172.19.0.0/16"
|
||||
Platform__Storage__Driver: "postgres"
|
||||
Platform__Storage__PostgresConnectionString: *postgres-connection
|
||||
Platform__EnvironmentSettings__RedirectUri: "https://stella-ops.local/auth/callback"
|
||||
Platform__EnvironmentSettings__PostLogoutRedirectUri: "https://stella-ops.local/"
|
||||
Platform__EnvironmentSettings__Scope: "openid profile email ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit"
|
||||
STELLAOPS_ROUTER_URL: "http://router.stella-ops.local"
|
||||
STELLAOPS_PLATFORM_URL: "http://platform.stella-ops.local"
|
||||
STELLAOPS_AUTHORITY_URL: "http://authority.stella-ops.local"
|
||||
@@ -381,10 +383,12 @@ services:
|
||||
restart: unless-stopped
|
||||
depends_on: *depends-infra
|
||||
environment:
|
||||
ASPNETCORE_URLS: "http://+:8080"
|
||||
ASPNETCORE_URLS: "http://+:80;http://+:8080"
|
||||
<<: *kestrel-cert
|
||||
ConnectionStrings__Default: *postgres-connection
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Gateway__Auth__Authority__Issuer: "https://authority.stella-ops.local/"
|
||||
Gateway__Auth__Authority__RequireHttpsMetadata: "false"
|
||||
volumes:
|
||||
- *cert-volume
|
||||
ports:
|
||||
@@ -743,6 +747,8 @@ services:
|
||||
<<: *kestrel-cert
|
||||
ConnectionStrings__Default: *postgres-connection
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Postgres__ConnectionString: *postgres-connection
|
||||
Postgres__SchemaName: "vexhub"
|
||||
volumes:
|
||||
- *cert-volume
|
||||
ports:
|
||||
@@ -818,8 +824,13 @@ services:
|
||||
<<: *kestrel-cert
|
||||
STELLAOPS_POLICY_ENGINE_Postgres__Policy__ConnectionString: *postgres-connection
|
||||
STELLAOPS_POLICY_ENGINE_ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__Authority: "http://authority.stella-ops.local"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__Authority: "https://authority.stella-ops.local/"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__MetadataAddress: "http://authority.stella-ops.local/.well-known/openid-configuration"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__RequireHttpsMetadata: "false"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__Audiences__0: "/scanner"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__BypassNetworks__0: "172.19.0.0/16"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__BypassNetworks__1: "127.0.0.1/32"
|
||||
STELLAOPS_POLICY_ENGINE_PolicyEngine__ResourceServer__BypassNetworks__2: "::1/128"
|
||||
volumes:
|
||||
- *cert-volume
|
||||
ports:
|
||||
@@ -845,8 +856,14 @@ services:
|
||||
<<: *kestrel-cert
|
||||
ConnectionStrings__Default: *postgres-connection
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Postgres__Policy__ConnectionString: *postgres-connection
|
||||
PolicyGateway__ResourceServer__Authority: "http://authority.stella-ops.local"
|
||||
PolicyGateway__ResourceServer__RequireHttpsMetadata: "false"
|
||||
PolicyGateway__ResourceServer__BypassNetworks__0: "172.19.0.0/16"
|
||||
# Bootstrap-prefixed vars (read by StellaOpsConfigurationBootstrapper before DI)
|
||||
STELLAOPS_POLICY_GATEWAY_PolicyGateway__ResourceServer__Authority: "http://authority.stella-ops.local"
|
||||
STELLAOPS_POLICY_GATEWAY_PolicyGateway__ResourceServer__RequireHttpsMetadata: "false"
|
||||
STELLAOPS_POLICY_GATEWAY_Postgres__Policy__ConnectionString: *postgres-connection
|
||||
volumes:
|
||||
- *cert-volume
|
||||
ports:
|
||||
@@ -1012,6 +1029,7 @@ services:
|
||||
<<: *kestrel-cert
|
||||
ConnectionStrings__Default: *postgres-connection
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Scheduler__Authority__Enabled: "false"
|
||||
volumes:
|
||||
- *cert-volume
|
||||
tmpfs:
|
||||
@@ -1224,6 +1242,7 @@ services:
|
||||
findings__ledger__Database__ConnectionString: *postgres-connection
|
||||
findings__ledger__Authority__Issuer: "http://authority.stella-ops.local"
|
||||
findings__ledger__Authority__RequireHttpsMetadata: "false"
|
||||
findings__ledger__Authority__BypassNetworks__0: "172.19.0.0/16"
|
||||
findings__ledger__Attachments__EncryptionKey: "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI="
|
||||
findings__ledger__Attachments__SignedUrlBase: "http://findings.stella-ops.local/attachments"
|
||||
findings__ledger__Attachments__SignedUrlSecret: "dev-signed-url-secret"
|
||||
@@ -1254,6 +1273,9 @@ services:
|
||||
<<: *kestrel-cert
|
||||
ConnectionStrings__Default: *postgres-connection
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Doctor__Authority__Issuer: "http://authority.stella-ops.local"
|
||||
Doctor__Authority__RequireHttpsMetadata: "false"
|
||||
Doctor__Authority__BypassNetworks__0: "172.19.0.0/16"
|
||||
volumes:
|
||||
- *cert-volume
|
||||
ports:
|
||||
@@ -1373,6 +1395,10 @@ services:
|
||||
NOTIFY_NOTIFY__STORAGE__CONNECTIONSTRING: *postgres-connection
|
||||
NOTIFY_NOTIFY__STORAGE__DATABASE: "notify"
|
||||
NOTIFY_NOTIFY__PLUGINS__BASEDIRECTORY: "/app"
|
||||
NOTIFY_NOTIFY__AUTHORITY__ENABLED: "false"
|
||||
NOTIFY_NOTIFY__AUTHORITY__ALLOWANONYMOUSFALLBACK: "true"
|
||||
NOTIFY_NOTIFY__AUTHORITY__DEVELOPMENTSIGNINGKEY: "StellaOps-Development-Key-NotifyService-2026!!"
|
||||
NOTIFY_Postgres__Notify__ConnectionString: *postgres-connection
|
||||
Postgres__Notify__ConnectionString: *postgres-connection
|
||||
volumes:
|
||||
- ../../etc/notify:/app/etc/notify:ro
|
||||
@@ -1642,6 +1668,7 @@ services:
|
||||
ConnectionStrings__Redis: "cache.stella-ops.local:6379"
|
||||
Authority__ResourceServer__Authority: "http://authority.stella-ops.local"
|
||||
Authority__ResourceServer__RequireHttpsMetadata: "false"
|
||||
Authority__ResourceServer__BypassNetworks__0: "172.19.0.0/16"
|
||||
volumes:
|
||||
- *cert-volume
|
||||
ports:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 16-Feb-2026 - Hybrid Diff Stack for Source and Binary Patching
|
||||
|
||||
## Source
|
||||
- Origin: user-submitted advisory in chat (2026-02-16).
|
||||
- Theme: versioning and patching across source + binaries using a hybrid
|
||||
source-symbol-binary diff pipeline.
|
||||
|
||||
## Advisory summary
|
||||
|
||||
Proposed architecture:
|
||||
|
||||
1. Source-level AST semantic edit scripts with symbol anchors.
|
||||
2. Build-time mapping from source edits to symbol ranges using DWARF/PDB and
|
||||
build-id metadata.
|
||||
3. Binary normalization followed by compact per-symbol delta generation.
|
||||
4. Signed packaging (DSSE + transparency logging) and policy gates based on
|
||||
function-level change scope.
|
||||
|
||||
Proposed deliverables:
|
||||
|
||||
- Builder outputs: `symbol_map.json`, `build_id`, normalized streams.
|
||||
- Differ outputs: `symbol_patch_plan.json`, per-symbol delta payloads,
|
||||
`patch_manifest.json`.
|
||||
- Verifier checks for build-id match, boundary-safe dry-run apply, and
|
||||
source-anchor reconciliation.
|
||||
- Evidence Locker schema extension for hybrid diff artifacts.
|
||||
|
||||
## Review result
|
||||
|
||||
Outcome: **Accepted as partially implemented and requiring additional delivery**.
|
||||
|
||||
Already implemented in repository:
|
||||
|
||||
- Normalized ELF segment hashing and normalization passes in BinaryIndex.
|
||||
- DeltaSig attestation model + CLI flow for signature extraction/sign/verify.
|
||||
- Symbol manifest model carrying debug and source metadata.
|
||||
|
||||
Gaps identified:
|
||||
|
||||
- No first-class AST semantic edit script artifact pipeline.
|
||||
- No canonical source-to-symbol map artifact contract emitted at build stage.
|
||||
- No unified symbol patch plan manifest linking AST anchors to normalized
|
||||
per-symbol delta artifacts.
|
||||
- Function boundary/address accuracy still incomplete in parts of DeltaSig
|
||||
function delta generation.
|
||||
|
||||
## Translated artifacts
|
||||
|
||||
- High-level doc: `docs/hybrid-diff-patching.md`
|
||||
- Module dossier: `docs/modules/binary-index/hybrid-diff-stack.md`
|
||||
- Sprint plan: `docs/implplan/SPRINT_20260216_001_BinaryIndex_hybrid_diff_patch_pipeline.md`
|
||||
|
||||
## De-duplication note
|
||||
|
||||
This advisory extends earlier binary diff and symbol mapping advisory work, not
|
||||
replace it:
|
||||
|
||||
- `docs-archived/product/advisories/30-Dec-2025 - Binary Diff Signatures for Patch Detection.md`
|
||||
- `docs-archived/product/advisories/18-Dec-2025 - Building Better Binary Mapping and Call-Stack Reachability.md`
|
||||
@@ -0,0 +1,24 @@
|
||||
# 16-Feb-2026 - eBPF micro-witness deterministic replay across distros
|
||||
|
||||
## Advisory source
|
||||
- Source: user-provided product advisory text (review session, 2026-02-16 UTC).
|
||||
- Scope: CO-RE eBPF micro-witnesses replayable and deterministic across kernels, distros, and toolchains, with DSSE + Sigstore bundle portability.
|
||||
|
||||
## Outcome
|
||||
- Result: partially aligned implementation with confirmed contract and implementation gaps.
|
||||
- Decision: advisory translated into product/module docs plus an active implementation sprint.
|
||||
|
||||
## Confirmed gap themes
|
||||
- Runtime collector support check is hard-gated on `/sys/kernel/btf/vmlinux`; split-BTF/external-vmlinux fallback behavior is not implemented as a deterministic recorded contract.
|
||||
- Runtime witness payload lacks required deterministic symbolization tuple for cross-distro replay (`symbolizer`, `libc_variant`, `sysroot`, debug/symbol pointers).
|
||||
- Runtime witness generation pipeline is interface-defined but not implemented end-to-end in Scanner.
|
||||
- DSSE witness support exists, but per-witness Sigstore bundle contract (`trace.sigstore.json`) is not standardized in witness storage/export/indexing.
|
||||
|
||||
## Translation artifacts
|
||||
- Active sprint: `docs/implplan/SPRINT_20260216_001_Signals_ebpf_micro_witness_determinism_profile.md`
|
||||
- Product update: `docs/product/ebpf-micro-witness-determinism.md`
|
||||
- Module contract: `docs/modules/signals/contracts/ebpf-micro-witness-determinism-profile.md`
|
||||
|
||||
## Notes
|
||||
- External web fetches: none.
|
||||
- Repository verification inputs included runtime and storage code paths under `src/Signals/`, `src/Scanner/`, `src/RuntimeInstrumentation/`, `src/Attestor/`, and `src/EvidenceLocker/`.
|
||||
@@ -119,6 +119,7 @@ This documentation set is intentionally consolidated and does not maintain compa
|
||||
- Security deep dives: `docs/security/`
|
||||
- Benchmarks and fixtures: `docs/benchmarks/`, `docs/assets/`
|
||||
- Product advisories: `docs/product/advisories/`
|
||||
- Hybrid diff patching blueprint: `docs/hybrid-diff-patching.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -140,3 +141,4 @@ This documentation set is intentionally consolidated and does not maintain compa
|
||||
- **Evidence-linked decisions:** every decision links to concrete evidence artifacts.
|
||||
- **Digest-first identity:** releases are immutable OCI digests, not mutable tags.
|
||||
- **Pluggable integrations:** connectors and steps are extensible; the core evidence chain stays stable.
|
||||
|
||||
|
||||
45
docs/hybrid-diff-patching.md
Normal file
45
docs/hybrid-diff-patching.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Hybrid Diff Patching (Source + Symbols + Binary)
|
||||
|
||||
## Purpose
|
||||
|
||||
This document captures the product-level blueprint for hybrid diff patching:
|
||||
|
||||
- Source semantic edits (AST-level intent).
|
||||
- Build-time symbol mapping (source ranges to binary symbols and addresses).
|
||||
- Normalized binary deltas (stable and compact byte patches).
|
||||
- Signed evidence bundle for policy gating and replay.
|
||||
|
||||
The goal is to make release decisions auditable at function granularity while
|
||||
remaining deterministic and offline-capable.
|
||||
|
||||
## Review outcome (2026-02-16)
|
||||
|
||||
The advisory is directionally aligned with existing Stella Ops work but not
|
||||
fully implemented end-to-end.
|
||||
|
||||
Already present:
|
||||
|
||||
- ELF normalization and delta hashing pipeline in BinaryIndex.
|
||||
- DeltaSig attestation models and CLI flows for extract/author/sign/verify.
|
||||
- Symbol manifest model with debug/code identifiers and source path metadata.
|
||||
|
||||
Missing or incomplete for the full hybrid stack:
|
||||
|
||||
- AST semantic edit-script generation and stable source anchors.
|
||||
- Build artifact contract that emits canonical `symbol_map.json` from DWARF/PDB
|
||||
during build.
|
||||
- Deterministic source-edit -> symbol patch plan artifact.
|
||||
- Verifier workflow that reconciles AST anchors with symbol boundaries and
|
||||
normalized per-symbol deltas in one attested contract.
|
||||
|
||||
## Canonical module dossier
|
||||
|
||||
Detailed contracts, phased implementation, and policy hooks are defined in:
|
||||
|
||||
- `docs/modules/binary-index/hybrid-diff-stack.md`
|
||||
|
||||
## Execution sprint
|
||||
|
||||
Implementation planning for this advisory is tracked in:
|
||||
|
||||
- `docs/implplan/SPRINT_20260216_001_BinaryIndex_hybrid_diff_patch_pipeline.md`
|
||||
@@ -0,0 +1,126 @@
|
||||
# Sprint 20260216-001 - Hybrid Diff Patch Pipeline
|
||||
|
||||
## Topic & Scope
|
||||
- Translate advisory guidance into an executable cross-module delivery plan for source-to-binary patch evidence.
|
||||
- Define deterministic contracts for semantic edit scripts, symbol maps, symbol patch plans, and normalized per-symbol deltas.
|
||||
- Wire policy and verification expectations so Release Orchestrator can gate on function-level change intent and byte-level proof.
|
||||
- Working directory: `src/BinaryIndex/`.
|
||||
- Expected evidence: targeted unit/integration tests, deterministic fixture artifacts, DSSE predicate samples, updated module docs.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Depends on existing DeltaSig v2 predicate baseline in `src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.DeltaSig/`.
|
||||
- Safe parallel workstreams:
|
||||
- source semantic edit artifact generation (`src/Tools/` or `src/ReleaseOrchestrator/` integration)
|
||||
- symbol map extraction contracts (`src/Symbols/`)
|
||||
- normalized delta and verifier integration (`src/BinaryIndex/`, `src/Attestor/`, `src/Doctor/`)
|
||||
- Cross-module edits are explicitly allowed for this sprint in:
|
||||
- `src/Symbols/`
|
||||
- `src/EvidenceLocker/`
|
||||
- `src/Policy/`
|
||||
- `src/ReleaseOrchestrator/`
|
||||
- `src/Attestor/`
|
||||
- `src/Doctor/`
|
||||
- `src/Web/`
|
||||
- `docs/modules/**`
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `docs/hybrid-diff-patching.md`
|
||||
- `docs/modules/binary-index/hybrid-diff-stack.md`
|
||||
- `docs/modules/binary-index/semantic-diffing.md`
|
||||
- `docs/modules/binary-index/deltasig-v2-schema.md`
|
||||
- `docs/modules/evidence-locker/guides/evidence-pack-schema.md`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### BHP-01 - Source semantic edit script artifact
|
||||
Status: TODO
|
||||
Dependency: none
|
||||
Owners: Developer, Documentation author
|
||||
Task description:
|
||||
- Add deterministic source semantic edit artifact generation that emits stable
|
||||
node identifiers and symbol anchors for changed code elements.
|
||||
- Integrate artifact emission into release comparison flow and persist into
|
||||
evidence pipelines.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] A `semantic_edit_script.json` contract is implemented and validated with tests.
|
||||
- [ ] Artifact generation is deterministic across repeated runs with identical inputs.
|
||||
- [ ] Documentation for schema and limits is added to module dossier docs.
|
||||
|
||||
### BHP-02 - Build symbol map contract and build-id binding
|
||||
Status: TODO
|
||||
Dependency: BHP-01
|
||||
Owners: Developer
|
||||
Task description:
|
||||
- Emit canonical `symbol_map.json` with source ranges, symbol boundaries, and
|
||||
build-id metadata from DWARF/PDB capable pipelines.
|
||||
- Ensure map digests and build-id values are linked into DeltaSig/attestation
|
||||
subjects for replay validation.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Symbol map generation is implemented for supported binary formats in scope.
|
||||
- [ ] Build-id and map digest are bound in emitted attestation payloads.
|
||||
- [ ] Tests cover mapping correctness and deterministic ordering.
|
||||
|
||||
### BHP-03 - Symbol patch plan and normalized per-symbol delta manifests
|
||||
Status: TODO
|
||||
Dependency: BHP-02
|
||||
Owners: Developer
|
||||
Task description:
|
||||
- Join semantic edits and symbol maps into `symbol_patch_plan.json` and
|
||||
generate normalized per-symbol deltas and `patch_manifest.json` outputs.
|
||||
- Remove placeholder function address/size derivation in DeltaSig generation
|
||||
where exact boundaries are required for audit claims.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Symbol patch plan artifact exists and links to AST anchors and symbol ids.
|
||||
- [ ] Patch manifest includes pre/post hashes, address ranges, and delta digests.
|
||||
- [ ] DeltaSig function-level outputs use real boundaries and sizes in covered paths.
|
||||
|
||||
### BHP-04 - Verifier and attestation enforcement
|
||||
Status: TODO
|
||||
Dependency: BHP-03
|
||||
Owners: Developer, Test Automation
|
||||
Task description:
|
||||
- Add verifier flow for build-id matching, re-normalization checks, dry-run delta
|
||||
application, and boundary/hash reconciliation.
|
||||
- Extend attestation validation logic in Attestor/Doctor and produce actionable
|
||||
verification evidence for release decisions.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Verifier checks fail closed on build-id mismatch, boundary mismatch, or hash mismatch.
|
||||
- [ ] DSSE validation and replay checks are captured in test evidence.
|
||||
- [ ] CLI/API surfaces expose verification outcome details for operators.
|
||||
|
||||
### BHP-05 - Policy and Evidence Locker integration
|
||||
Status: TODO
|
||||
Dependency: BHP-04
|
||||
Owners: Developer, Product Manager
|
||||
Task description:
|
||||
- Add policy gate inputs for symbol-count change budgets, namespace restrictions,
|
||||
API-surface invariants, and byte budget thresholds.
|
||||
- Store hybrid diff artifacts in Evidence Locker and expose summary/read paths in
|
||||
UI and release records.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Policy rules can gate promotions using hybrid diff metrics.
|
||||
- [ ] Evidence Locker stores and retrieves the full hybrid artifact chain.
|
||||
- [ ] UI/CLI render concise "what changed" summaries with links to signed evidence.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-02-16 | Sprint created from product advisory review for hybrid source-symbol-binary diff pipeline. | Product Manager |
|
||||
|
||||
## Decisions & Risks
|
||||
- Advisory overlap confirmed with archived advisories:
|
||||
- `docs-archived/product/advisories/30-Dec-2025 - Binary Diff Signatures for Patch Detection.md`
|
||||
- `docs-archived/product/advisories/18-Dec-2025 - Building Better Binary Mapping and Call-Stack Reachability.md`
|
||||
- Decision: treat this advisory as an extension that unifies source intent and binary proof in one contract chain, not as a duplicate effort.
|
||||
- Risk: multi-module coordination can drift schemas; mitigation is to keep canonical contracts in BinaryIndex dossier and require digest-linked schema versions in attestations.
|
||||
- Risk: AST differencing backend choice may vary by language; mitigation is a language-agnostic output schema with adapter-specific provenance fields.
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-02-18: Contract freeze review for artifact schemas (`semantic_edit_script`, `symbol_map`, `symbol_patch_plan`, `patch_manifest`).
|
||||
- 2026-02-22: First end-to-end dry run in CI with signed evidence and verifier replay.
|
||||
- 2026-02-26: Policy gate integration demo with allow/deny examples on symbol namespaces.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Sprint SPRINT_20260216_001_Signals_ebpf_micro_witness_determinism_profile - eBPF Micro-Witness Determinism
|
||||
|
||||
## Topic & Scope
|
||||
- Translate the eBPF micro-witness advisory into implementation-ready contracts and sprint tasks.
|
||||
- Close determinism gaps for runtime witness replay across kernel/distro/toolchain variance.
|
||||
- Define one portable evidence profile for DSSE + Sigstore bundle based offline replay.
|
||||
- Working directory: `docs/`.
|
||||
- Cross-module edits explicitly allowed for implementation tasks: `src/Signals/`, `src/Scanner/`, `src/Attestor/`, `src/EvidenceLocker/`.
|
||||
- Expected evidence: contract docs, schema/API updates, targeted module tests, offline verification artifacts.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Upstream contracts: `docs/contracts/witness-v1.md`, `docs/modules/attestor/repro-bundle-profile.md`, `docs/modules/evidence/unified-model.md`.
|
||||
- Safe parallelism:
|
||||
- Signals loader/BTF work can run in parallel with Attestor/Evidence Locker bundle contract work.
|
||||
- Scanner witness model updates should run after profile fields are frozen.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `docs/product/ebpf-micro-witness-determinism.md`
|
||||
- `docs/modules/signals/contracts/ebpf-micro-witness-determinism-profile.md`
|
||||
- `docs/reachability/deployment-guide.md`
|
||||
- `docs/contracts/witness-v1.md`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### MWD-001 - Signals BTF fallback contract and metadata emission
|
||||
Status: TODO
|
||||
Dependency: none
|
||||
Owners: Product Manager, Developer
|
||||
Task description:
|
||||
- Implement deterministic BTF selection order in the runtime collector and emit selected source metadata (`source_kind`, `source_path`, `source_digest`, `selection_reason`) into runtime evidence/witness context.
|
||||
- Ensure behavior is explicit for kernel BTF, external vmlinux BTF, and split-BTF fallback.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Collector no longer fails solely on missing `/sys/kernel/btf/vmlinux` when configured fallback BTF exists.
|
||||
- [ ] Runtime evidence includes immutable BTF selection metadata required for replay.
|
||||
|
||||
### MWD-002 - Runtime witness schema extensions for deterministic symbolization
|
||||
Status: TODO
|
||||
Dependency: MWD-001
|
||||
Owners: Developer, Documentation author
|
||||
Task description:
|
||||
- Extend runtime witness payload schema to include deterministic symbolization tuple: `build_id`, debug/symbol pointer(s), symbolizer identity/version/digest, libc variant, and sysroot digest.
|
||||
- Update witness contracts and validation rules in docs and implementation.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Witness schema and code models carry required symbolization fields.
|
||||
- [ ] Validation rejects witnesses missing required deterministic symbolization inputs.
|
||||
|
||||
### MWD-003 - Implement Scanner runtime witness generation pipeline
|
||||
Status: TODO
|
||||
Dependency: MWD-002
|
||||
Owners: Developer, Test Automation
|
||||
Task description:
|
||||
- Deliver concrete `IRuntimeWitnessGenerator` implementation, integrating runtime observations, witness building, DSSE signing, and storage.
|
||||
- Ensure deterministic ordering/canonicalization for runtime observation payloads.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Runtime witness generation is implemented (not interface-only) and wired into runtime instrumentation flow.
|
||||
- [ ] Determinism tests show stable witness bytes for fixed inputs.
|
||||
|
||||
### MWD-004 - DSSE plus Sigstore bundle witness packaging
|
||||
Status: TODO
|
||||
Dependency: MWD-003
|
||||
Owners: Developer, Documentation author
|
||||
Task description:
|
||||
- Standardize and implement per-witness artifact triplet: `trace.json`, `trace.dsse.json`, `trace.sigstore.json`.
|
||||
- Store and export this profile through Evidence Locker with offline verification compatibility.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Evidence Locker manifest/index model supports the Sigstore bundle artifact and links it to witness identity.
|
||||
- [ ] Offline verify workflow succeeds using bundle-contained material only.
|
||||
|
||||
### MWD-005 - Cross-distro deterministic replay test matrix
|
||||
Status: TODO
|
||||
Dependency: MWD-004
|
||||
Owners: Test Automation, QA
|
||||
Task description:
|
||||
- Add targeted replay verification across kernel/libc matrix (minimum 3 kernels, glibc + musl), asserting byte-identical replay frames for fixed witness artifacts.
|
||||
- Capture command output and evidence artifacts for deterministic QA sign-off.
|
||||
|
||||
Completion criteria:
|
||||
- [ ] Matrix tests run against targeted projects (not solution filters) and show deterministic replay output.
|
||||
- [ ] Execution evidence is recorded with artifact hashes and replay verification logs.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-02-16 | Sprint created from eBPF micro-witness advisory review; gaps confirmed and translated to implementation tasks. | Project Manager |
|
||||
|
||||
## Decisions & Risks
|
||||
- Decision: Adopt a single micro-witness determinism profile defined in `docs/modules/signals/contracts/ebpf-micro-witness-determinism-profile.md`.
|
||||
- Decision: Product-level promise and current baseline are captured in `docs/product/ebpf-micro-witness-determinism.md`.
|
||||
- Decision: Advisory translation record archived at `docs-archived/product/advisories/16-Feb-2026 - eBPF micro-witness deterministic replay across distros.md`.
|
||||
- Risk: Existing runtime collector hard dependency on kernel BTF may block non-BTF kernels until fallback path is implemented.
|
||||
- Risk: Runtime witness generation remains incomplete without a concrete generator implementation; downstream attestation/export is blocked.
|
||||
- Risk: Absence of standardized Sigstore witness bundle may produce non-portable replay evidence across environments.
|
||||
- External web fetches: none.
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-02-18: Contract review sign-off (Signals/Scanner/Attestor/Evidence Locker owners).
|
||||
- 2026-02-21: MWD-001 and MWD-002 implementation readiness checkpoint.
|
||||
- 2026-02-25: First end-to-end deterministic replay demo with DSSE + Sigstore witness bundle.
|
||||
@@ -37,6 +37,7 @@ Key features:
|
||||
## Related Documentation
|
||||
|
||||
- Architecture: `./architecture.md`
|
||||
- Hybrid Diff Stack: `./hybrid-diff-stack.md`
|
||||
- High-Level Architecture: `../../ARCHITECTURE_OVERVIEW.md`
|
||||
- Scanner Architecture: `../scanner/architecture.md`
|
||||
- Concelier Architecture: `../concelier/architecture.md`
|
||||
@@ -63,7 +64,7 @@ A major enhancement to BinaryIndex is planned to enable **semantic-level binary
|
||||
| **Phase 1** | IR-Level Semantic Analysis | +15% accuracy on optimized binaries | Planned |
|
||||
| **Phase 2** | Function Behavior Corpus | +10% coverage on stripped binaries | Planned |
|
||||
| **Phase 3** | Ghidra Integration | +5% edge case handling | Planned |
|
||||
| **Phase 4** | Decompiler & ML Similarity | +10% obfuscation resilience | Planned |
|
||||
| **Phase 4** | Decompiler and ML Similarity | +10% obfuscation resilience | Planned |
|
||||
|
||||
### New Libraries (Planned)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **Ownership:** Scanner Guild + Concelier Guild
|
||||
> **Status:** DRAFT
|
||||
> **Version:** 1.0.0
|
||||
> **Related:** [High-Level Architecture](../../ARCHITECTURE_OVERVIEW.md), [Scanner Architecture](../scanner/architecture.md), [Concelier Architecture](../concelier/architecture.md)
|
||||
> **Related:** [High-Level Architecture](../../ARCHITECTURE_OVERVIEW.md), [Scanner Architecture](../scanner/architecture.md), [Concelier Architecture](../concelier/architecture.md), [Hybrid Diff Stack](./hybrid-diff-stack.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -1774,3 +1774,4 @@ inside `AddNormalizationPipelines()` in `ServiceCollectionExtensions.cs`.
|
||||
|
||||
*Document Version: 1.5.0*
|
||||
*Last Updated: 2026-02-12*
|
||||
|
||||
|
||||
163
docs/modules/binary-index/hybrid-diff-stack.md
Normal file
163
docs/modules/binary-index/hybrid-diff-stack.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Hybrid Diff Stack Architecture (Source -> Symbols -> Normalized Bytes)
|
||||
|
||||
> Status: Planned (advisory translation, 2026-02-16)
|
||||
> Module: BinaryIndex with cross-module contracts (Symbols, EvidenceLocker, Policy, Attestor, ReleaseOrchestrator)
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Produce compact, auditable patch artifacts that preserve developer intent and
|
||||
binary truth at the same time:
|
||||
|
||||
- Source-level intent: semantic edit scripts anchored to classes/functions.
|
||||
- Build-level mapping: symbol map linked to immutable build identity.
|
||||
- Binary-level patching: normalization-first per-symbol deltas.
|
||||
- Release evidence: DSSE-signed contract consumed by policy and replay.
|
||||
|
||||
## 2. Current implementation baseline
|
||||
|
||||
Implemented today:
|
||||
|
||||
- ELF normalization passes and deterministic delta hash generation.
|
||||
- DeltaSig predicate contracts (v1 and v2) with CLI author/sign/verify flows.
|
||||
- Symbol manifest model with debug id, code id, source paths, and line data.
|
||||
|
||||
Gaps for full advisory scope:
|
||||
|
||||
- No AST semantic edit script artifact pipeline in current release workflow.
|
||||
- No canonical builder output for source-range to symbol-address map as a
|
||||
first-class build artifact contract.
|
||||
- No end-to-end "source edits -> symbol patch plan -> normalized deltas"
|
||||
bundle schema consumed by release policy.
|
||||
- Existing function delta composition still contains placeholder address/size
|
||||
behavior in parts of DeltaSig generation.
|
||||
|
||||
## 3. Target contracts
|
||||
|
||||
### 3.1 Source semantic edit script (`semantic_edit_script.json`)
|
||||
|
||||
Required fields:
|
||||
|
||||
- `schemaVersion`
|
||||
- `sourceTreeDigest`
|
||||
- `edits[]` where each edit includes:
|
||||
- `editType`: `add|remove|move|update|rename`
|
||||
- `nodeKind`: `class|method|field|import|statement`
|
||||
- `nodePath`: stable language-specific path
|
||||
- `anchor`: symbol-like identifier (for example `Namespace.Type.Method`)
|
||||
- `pre` and `post` source spans and digests
|
||||
|
||||
Determinism rules:
|
||||
|
||||
- Stable sort by file path, then node path.
|
||||
- Stable source digests and normalized paths.
|
||||
|
||||
### 3.2 Symbol map (`symbol_map.json`)
|
||||
|
||||
Produced during build from DWARF/PDB + build metadata.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `schemaVersion`
|
||||
- `buildId`
|
||||
- `binaryDigest`
|
||||
- `symbols[]`:
|
||||
- `name`
|
||||
- `kind` (`function|object|section`)
|
||||
- `addressStart` and `addressEnd`
|
||||
- `section`
|
||||
- `sourceRanges[]` (`file`, `lineStart`, `lineEnd`)
|
||||
|
||||
Determinism rules:
|
||||
|
||||
- Symbol ordering by address then name.
|
||||
- Build id must match attestation subject.
|
||||
|
||||
### 3.3 Symbol patch plan (`symbol_patch_plan.json`)
|
||||
|
||||
Joins source edits with concrete symbols.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `schemaVersion`
|
||||
- `buildIdBefore` and `buildIdAfter`
|
||||
- `editsDigest`
|
||||
- `symbolMapDigestBefore` and `symbolMapDigestAfter`
|
||||
- `changes[]`:
|
||||
- `symbol`
|
||||
- `changeType` (`added|removed|modified|moved`)
|
||||
- `astAnchors[]`
|
||||
- `preHash` and `postHash`
|
||||
- `deltaRef`
|
||||
|
||||
### 3.4 Patch manifest (`patch_manifest.json`)
|
||||
|
||||
Binds per-symbol normalized deltas to evidence and policy.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `schemaVersion`
|
||||
- `buildId`
|
||||
- `normalizationRecipeId`
|
||||
- `patches[]`:
|
||||
- `symbol`
|
||||
- `addressRange`
|
||||
- `deltaDigest`
|
||||
- `pre` (`size`, `hash`)
|
||||
- `post` (`size`, `hash`)
|
||||
- `attestation` (`predicateType`, `dsseDigest`)
|
||||
|
||||
## 4. Evidence and policy integration
|
||||
|
||||
EvidenceLocker stores four linked artifacts per release comparison:
|
||||
|
||||
1. semantic edit script
|
||||
2. symbol maps (before/after)
|
||||
3. symbol patch plan
|
||||
4. normalized patch manifest + delta blobs
|
||||
|
||||
Policy hooks:
|
||||
|
||||
- Allowlist/denylist by namespace or symbol path.
|
||||
- Max function-count and max byte budget controls.
|
||||
- API surface change checks.
|
||||
- Hot-path and cryptography namespace protection rules.
|
||||
|
||||
## 5. Verifier contract (Attestor/Doctor)
|
||||
|
||||
Verifier must prove all of the following before promotion:
|
||||
|
||||
- Build-id and subject digest alignment.
|
||||
- Re-normalization of target binary with matching recipe id.
|
||||
- Dry-run delta application succeeds within declared symbol boundaries.
|
||||
- Resulting hashes equal manifest `post` values.
|
||||
- AST anchors reconcile to changed symbols in symbol patch plan.
|
||||
- DSSE signatures and transparency references validate per policy.
|
||||
|
||||
## 6. Integration boundaries
|
||||
|
||||
Builder step (CI): emit symbol map and normalized segments.
|
||||
|
||||
ReleaseOrchestrator step: combine source edits, symbol maps, and normalized
|
||||
bytes into patch plan and manifest.
|
||||
|
||||
BinaryIndex/DeltaSig: own normalization and per-symbol diff generation.
|
||||
|
||||
Attestor/Doctor: own verification and attestation checks.
|
||||
|
||||
EvidenceLocker: own storage schema and query surfaces.
|
||||
|
||||
Policy: consume summarized patch-plan metrics and rule evaluations.
|
||||
|
||||
## 7. Implementation tracker
|
||||
|
||||
Execution is tracked in:
|
||||
|
||||
- `docs/implplan/SPRINT_20260216_001_BinaryIndex_hybrid_diff_patch_pipeline.md`
|
||||
|
||||
## 8. Related documents
|
||||
|
||||
- `docs/hybrid-diff-patching.md`
|
||||
- `docs/modules/binary-index/semantic-diffing.md`
|
||||
- `docs/modules/binary-index/deltasig-v2-schema.md`
|
||||
- `docs/modules/scanner/binary-diff-attestation.md`
|
||||
- `docs/modules/evidence-locker/guides/evidence-pack-schema.md`
|
||||
@@ -49,6 +49,7 @@ Key settings:
|
||||
## Related Documentation
|
||||
|
||||
- Architecture: `./architecture.md`
|
||||
- Contract: `./contracts/ebpf-micro-witness-determinism-profile.md`
|
||||
- Policy Engine: `../policy/`
|
||||
- VexLens: `../vex-lens/`
|
||||
- High-Level Architecture: `../../ARCHITECTURE_OVERVIEW.md`
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# eBPF Micro-Witness Determinism Profile v1.0.0
|
||||
|
||||
**Status:** PLANNED
|
||||
**Version:** 1.0.0
|
||||
**Effective:** 2026-02-16
|
||||
**Owner:** Signals Guild + Scanner Guild + Attestor Guild + Evidence Locker Guild
|
||||
**Sprint:** `docs/implplan/SPRINT_20260216_001_Signals_ebpf_micro_witness_determinism_profile.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This profile defines the minimum deterministic contract for runtime eBPF "micro-witnesses" so replay yields the same symbolized result across distros/toolchains and in offline environments.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contract Scope
|
||||
|
||||
- Runtime collection and BTF selection (`Signals`).
|
||||
- Runtime witness payload schema and signing (`Scanner`).
|
||||
- DSSE and transparency evidence shape (`Attestor`).
|
||||
- Portable storage/export/indexing (`Evidence Locker`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Runtime Loader Contract (BTF Selection)
|
||||
|
||||
### 3.1 Selection order (mandatory)
|
||||
1. `/sys/kernel/btf/vmlinux`
|
||||
2. configured full-kernel BTF path (for example distro debug package path)
|
||||
3. split-BTF selected by `{kernel_release, arch}`
|
||||
|
||||
### 3.2 Required emitted metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"kernel_release": "6.8.0-45-generic",
|
||||
"kernel_arch": "x86_64",
|
||||
"btf": {
|
||||
"source_kind": "kernel|external-vmlinux|split-btf",
|
||||
"source_path": "/sys/kernel/btf/vmlinux",
|
||||
"source_digest": "sha256:...",
|
||||
"selection_reason": "kernel_btf_present"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`source_path` and `source_digest` are mandatory for deterministic replay.
|
||||
|
||||
---
|
||||
|
||||
## 4. Deterministic Symbolization Contract
|
||||
|
||||
Each runtime witness must carry deterministic symbolization inputs:
|
||||
|
||||
```json
|
||||
{
|
||||
"symbolization": {
|
||||
"build_id": "gnu-build-id:...",
|
||||
"debug_artifact_uri": "cas://symbols/by-build-id/gnu-build-id:.../artifact.debug",
|
||||
"symbol_table_uri": "cas://symbols/by-build-id/gnu-build-id:.../symtab.json",
|
||||
"symbolizer": {
|
||||
"name": "llvm-symbolizer",
|
||||
"version": "18.1.7",
|
||||
"digest": "sha256:..."
|
||||
},
|
||||
"libc_variant": "glibc|musl",
|
||||
"sysroot_digest": "sha256:..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At least one of `debug_artifact_uri` or `symbol_table_uri` must be present.
|
||||
|
||||
---
|
||||
|
||||
## 5. Witness Packaging Contract
|
||||
|
||||
Each micro-witness must be exportable as:
|
||||
|
||||
1. `trace.json` (canonical payload)
|
||||
2. `trace.dsse.json` (DSSE envelope)
|
||||
3. `trace.sigstore.json` (Sigstore bundle with signature/cert/transparency proof)
|
||||
|
||||
Offline verification must use only bundle-contained material (no network dependency).
|
||||
|
||||
---
|
||||
|
||||
## 6. Evidence Locker Index Contract
|
||||
|
||||
Evidence Locker must index runtime witness artifacts by:
|
||||
|
||||
- `build_id`
|
||||
- `kernel_release`
|
||||
- `probe_id`
|
||||
- `policy_run_id`
|
||||
|
||||
These keys are required for deterministic replay lookup and audit search.
|
||||
|
||||
---
|
||||
|
||||
## 7. Validation Matrix (minimum)
|
||||
|
||||
- Kernel matrix: at least 3 supported kernel lines.
|
||||
- libc matrix: glibc + musl.
|
||||
- Verification modes: online + offline.
|
||||
- Determinism check: byte-identical replayed frame output for fixed input evidence.
|
||||
|
||||
---
|
||||
|
||||
## 8. Confirmed Gaps (2026-02-16 Baseline)
|
||||
|
||||
- Hard BTF dependency with no split-BTF fallback metadata contract in collector:
|
||||
- `src/Signals/__Libraries/StellaOps.Signals.Ebpf/Services/RuntimeSignalCollector.cs`
|
||||
- Probe load path is simulated and does not record selected BTF source:
|
||||
- `src/Signals/__Libraries/StellaOps.Signals.Ebpf/Probes/CoreProbeLoader.cs`
|
||||
- Runtime witness payload lacks required symbolization tuple fields:
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/PathWitness.cs`
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/RuntimeObservation.cs`
|
||||
- Runtime witness generator implementation is missing:
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/IRuntimeWitnessGenerator.cs`
|
||||
- Sigstore bundle (`trace.sigstore.json`) is not yet standardized in witness storage/export:
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/013_witness_storage.sql`
|
||||
- `src/EvidenceLocker/__Libraries/StellaOps.EvidenceLocker.Export/Models/BundleManifest.cs`
|
||||
@@ -12,6 +12,7 @@ Product strategy, competitive analysis, and marketing bridge documents.
|
||||
| [decision-capsules.md](decision-capsules.md) | Decision Capsules concept (audit-grade evidence bundles) |
|
||||
| [evidence-linked-vex.md](evidence-linked-vex.md) | Evidence-linked VEX technical bridge |
|
||||
| [hybrid-reachability.md](hybrid-reachability.md) | Hybrid reachability feature positioning |
|
||||
| [ebpf-micro-witness-determinism.md](ebpf-micro-witness-determinism.md) | eBPF micro-witness deterministic replay profile and current implementation gaps |
|
||||
| [portable-audit-pack-plan.md](portable-audit-pack-plan.md) | Portable supply-chain audit pack rollout plan |
|
||||
| [reachability-benchmark-launch.md](reachability-benchmark-launch.md) | Reachability benchmark launch materials |
|
||||
|
||||
|
||||
36
docs/product/ebpf-micro-witness-determinism.md
Normal file
36
docs/product/ebpf-micro-witness-determinism.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# eBPF Micro-Witness Determinism Profile
|
||||
|
||||
## Status
|
||||
- Advisory translated: 2026-02-16 (UTC)
|
||||
- Current implementation status: gaps confirmed
|
||||
- Implementation sprint: `docs/implplan/SPRINT_20260216_001_Signals_ebpf_micro_witness_determinism_profile.md`
|
||||
|
||||
## Purpose
|
||||
- Define what "replayable and deterministic micro-witnesses" means for Stella Ops runtime evidence.
|
||||
- Align Signals, Scanner, Attestor, and Evidence Locker on one verifiable output profile.
|
||||
- Ensure the same incident replay result across distros/toolchains and in offline mode.
|
||||
|
||||
## Required product behavior
|
||||
1. One CO-RE probe object must run unchanged across supported kernels when BTF is available.
|
||||
2. If kernel BTF is missing, the loader must use deterministic fallback selection and record exactly what BTF source was used.
|
||||
3. Runtime witnesses must include deterministic symbolization inputs (build identity + symbol/debug material + toolchain tuple).
|
||||
4. Witness evidence must be portable as DSSE plus a Sigstore bundle that can be verified offline.
|
||||
|
||||
## Verified current state (2026-02-16)
|
||||
- eBPF support check currently hard-requires `/sys/kernel/btf/vmlinux` with no split-BTF fallback path selection metadata in collector output.
|
||||
- `src/Signals/__Libraries/StellaOps.Signals.Ebpf/Services/RuntimeSignalCollector.cs`
|
||||
- Probe loader path is simulated for runtime attachment lifecycle and does not implement deterministic BTF source recording.
|
||||
- `src/Signals/__Libraries/StellaOps.Signals.Ebpf/Probes/CoreProbeLoader.cs`
|
||||
- Runtime witness model includes `build_id` but does not include symbol bundle pointers or symbolizer/libc/sysroot tuple required for cross-distro deterministic symbolization.
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/PathWitness.cs`
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/RuntimeObservation.cs`
|
||||
- Runtime witness generator is interface-defined but has no production implementation in Scanner.
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/IRuntimeWitnessGenerator.cs`
|
||||
- DSSE envelope support exists; end-to-end per-witness Sigstore bundle contract (`trace.sigstore.json`) is not standardized in witness storage/indexing.
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs`
|
||||
- `src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/013_witness_storage.sql`
|
||||
- `src/EvidenceLocker/__Libraries/StellaOps.EvidenceLocker.Export/Models/BundleManifest.cs`
|
||||
|
||||
## Decision
|
||||
- Advisory is accepted as implementation-required.
|
||||
- Contract and sprint tasks are created to close deterministic replay gaps.
|
||||
@@ -0,0 +1,435 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.BinaryIndex.DeltaSig;
|
||||
|
||||
/// <summary>
|
||||
/// Source file pair used to compute semantic edit scripts.
|
||||
/// </summary>
|
||||
public sealed record SourceFileDiff
|
||||
{
|
||||
/// <summary>
|
||||
/// Source file path (repository-relative).
|
||||
/// </summary>
|
||||
public required string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous file content.
|
||||
/// </summary>
|
||||
public string? BeforeContent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current file content.
|
||||
/// </summary>
|
||||
public string? AfterContent { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source line span.
|
||||
/// </summary>
|
||||
public sealed record SourceSpan
|
||||
{
|
||||
/// <summary>
|
||||
/// 1-based start line.
|
||||
/// </summary>
|
||||
public required int StartLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 1-based end line.
|
||||
/// </summary>
|
||||
public required int EndLine { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic semantic edit script.
|
||||
/// </summary>
|
||||
public sealed record SemanticEditScript
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the full source tree diff input.
|
||||
/// </summary>
|
||||
public required string SourceTreeDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic ordered edits.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SemanticEdit> Edits { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single semantic edit entry.
|
||||
/// </summary>
|
||||
public sealed record SemanticEdit
|
||||
{
|
||||
/// <summary>
|
||||
/// Stable digest-derived ID for dedupe and audit references.
|
||||
/// </summary>
|
||||
public required string StableId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Edit type: add, remove, move, update, rename.
|
||||
/// </summary>
|
||||
public required string EditType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node kind: file, method, class, field, import, statement.
|
||||
/// </summary>
|
||||
public required string NodeKind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic node path.
|
||||
/// </summary>
|
||||
public required string NodePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol anchor used to link to binary symbols.
|
||||
/// </summary>
|
||||
public required string Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-change source span.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public SourceSpan? PreSpan { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Post-change source span.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public SourceSpan? PostSpan { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-change digest.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? PreDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Post-change digest.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? PostDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build-stage symbol map linked to build identity.
|
||||
/// </summary>
|
||||
public sealed record SymbolMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Build ID (ELF build-id, PDB GUID, or equivalent).
|
||||
/// </summary>
|
||||
public required string BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional binary digest for traceability.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? BinaryDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Address derivation source (manifest, synthetic-signature).
|
||||
/// </summary>
|
||||
public string AddressSource { get; init; } = "manifest";
|
||||
|
||||
/// <summary>
|
||||
/// Ordered symbol entries.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SymbolMapEntry> Symbols { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol map entry.
|
||||
/// </summary>
|
||||
public sealed record SymbolMapEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol kind: function, object, section.
|
||||
/// </summary>
|
||||
public string Kind { get; init; } = "function";
|
||||
|
||||
/// <summary>
|
||||
/// Start address.
|
||||
/// </summary>
|
||||
public required ulong AddressStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End address.
|
||||
/// </summary>
|
||||
public required ulong AddressEnd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Containing section.
|
||||
/// </summary>
|
||||
public string Section { get; init; } = ".text";
|
||||
|
||||
/// <summary>
|
||||
/// Source ranges for this symbol.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IReadOnlyList<SourceRange>? SourceRanges { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol size derived from address range.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long Size => checked((long)(AddressEnd >= AddressStart ? AddressEnd - AddressStart + 1UL : 0UL));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source range metadata.
|
||||
/// </summary>
|
||||
public sealed record SourceRange
|
||||
{
|
||||
/// <summary>
|
||||
/// Source file path.
|
||||
/// </summary>
|
||||
public required string File { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 1-based start line.
|
||||
/// </summary>
|
||||
public required int LineStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 1-based end line.
|
||||
/// </summary>
|
||||
public required int LineEnd { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Join artifact linking semantic edits to symbol changes.
|
||||
/// </summary>
|
||||
public sealed record SymbolPatchPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Build ID before patch.
|
||||
/// </summary>
|
||||
public required string BuildIdBefore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Build ID after patch.
|
||||
/// </summary>
|
||||
public required string BuildIdAfter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Semantic script digest.
|
||||
/// </summary>
|
||||
public required string EditsDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Old symbol map digest.
|
||||
/// </summary>
|
||||
public required string SymbolMapDigestBefore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New symbol map digest.
|
||||
/// </summary>
|
||||
public required string SymbolMapDigestAfter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ordered symbol changes.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SymbolPatchChange> Changes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single symbol patch plan entry.
|
||||
/// </summary>
|
||||
public sealed record SymbolPatchChange
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name.
|
||||
/// </summary>
|
||||
public required string Symbol { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Change type: added, removed, modified, moved.
|
||||
/// </summary>
|
||||
public required string ChangeType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Linked source anchors.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<string> AstAnchors { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash before.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? PreHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash after.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? PostHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Delta reference digest.
|
||||
/// </summary>
|
||||
public required string DeltaRef { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalized patch manifest for per-symbol deltas.
|
||||
/// </summary>
|
||||
public sealed record PatchManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Build ID.
|
||||
/// </summary>
|
||||
public required string BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalization recipe identifier.
|
||||
/// </summary>
|
||||
public required string NormalizationRecipeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ordered patch entries.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SymbolPatchArtifact> Patches { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total delta bytes across symbols.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long TotalDeltaBytes => Patches.Sum(p => p.DeltaSizeBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-symbol patch artifact.
|
||||
/// </summary>
|
||||
public sealed record SymbolPatchArtifact
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name.
|
||||
/// </summary>
|
||||
public required string Symbol { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Address range in hex format.
|
||||
/// </summary>
|
||||
public required string AddressRange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digest of patch payload.
|
||||
/// </summary>
|
||||
public required string DeltaDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-patch metrics.
|
||||
/// </summary>
|
||||
public required PatchSizeHash Pre { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Post-patch metrics.
|
||||
/// </summary>
|
||||
public required PatchSizeHash Post { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute byte delta.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long DeltaSizeBytes => Math.Abs(Post.Size - Pre.Size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size/hash tuple.
|
||||
/// </summary>
|
||||
public sealed record PatchSizeHash
|
||||
{
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required long Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash digest.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full hybrid diff evidence bundle.
|
||||
/// </summary>
|
||||
public sealed record HybridDiffEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Semantic edit script.
|
||||
/// </summary>
|
||||
public required SemanticEditScript SemanticEditScript { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Old symbol map.
|
||||
/// </summary>
|
||||
public required SymbolMap OldSymbolMap { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New symbol map.
|
||||
/// </summary>
|
||||
public required SymbolMap NewSymbolMap { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol patch plan.
|
||||
/// </summary>
|
||||
public required SymbolPatchPlan SymbolPatchPlan { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized patch manifest.
|
||||
/// </summary>
|
||||
public required PatchManifest PatchManifest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Semantic edit script digest.
|
||||
/// </summary>
|
||||
public required string SemanticEditScriptDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Old symbol map digest.
|
||||
/// </summary>
|
||||
public required string OldSymbolMapDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New symbol map digest.
|
||||
/// </summary>
|
||||
public required string NewSymbolMapDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol patch plan digest.
|
||||
/// </summary>
|
||||
public required string SymbolPatchPlanDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Patch manifest digest.
|
||||
/// </summary>
|
||||
public required string PatchManifestDigest { get; init; }
|
||||
}
|
||||
@@ -143,6 +143,13 @@ builder.Services.AddAuthorization(options =>
|
||||
? bootstrapOptions.Authority.RequiredScopes.ToArray()
|
||||
: new[] { StellaOpsScopes.VulnOperate };
|
||||
|
||||
// Default policy uses StellaOpsScopeRequirement so bypass evaluator can grant
|
||||
// access for requests from trusted networks (BypassNetworks) without a JWT.
|
||||
options.DefaultPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddRequirements(new StellaOpsScopeRequirement(scopes))
|
||||
.Build();
|
||||
|
||||
options.AddPolicy(LedgerWritePolicy, policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
|
||||
@@ -23,7 +23,7 @@ public static class NotifyPersistenceExtensions
|
||||
IConfiguration configuration,
|
||||
string sectionName = "Postgres:Notify")
|
||||
{
|
||||
services.Configure<PostgresOptions>(sectionName, configuration.GetSection(sectionName));
|
||||
services.Configure<PostgresOptions>(configuration.GetSection(sectionName));
|
||||
services.AddSingleton<NotifyDataSource>();
|
||||
|
||||
// Register repositories
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -85,7 +86,7 @@ public sealed class PlatformAnalyticsQueryExecutor : IPlatformAnalyticsQueryExec
|
||||
SUM(total_vulns) - SUM(vex_mitigated) AS net_exposure,
|
||||
SUM(kev_vulns) AS kev_vulns
|
||||
FROM analytics.daily_vulnerability_counts
|
||||
WHERE snapshot_date >= CURRENT_DATE - (@days || ' days')::INTERVAL
|
||||
WHERE snapshot_date >= CURRENT_DATE - make_interval(days => @days)
|
||||
AND (@environment IS NULL OR environment = @environment)
|
||||
GROUP BY snapshot_date, environment
|
||||
ORDER BY environment, snapshot_date;
|
||||
@@ -100,7 +101,8 @@ public sealed class PlatformAnalyticsQueryExecutor : IPlatformAnalyticsQueryExec
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
command.Parameters.AddWithValue("days", days);
|
||||
command.Parameters.AddWithValue("environment", (object?)environment ?? DBNull.Value);
|
||||
var envParam = command.Parameters.Add("environment", NpgsqlDbType.Text);
|
||||
envParam.Value = (object?)environment ?? DBNull.Value;
|
||||
|
||||
var results = new List<AnalyticsVulnerabilityTrendPoint>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -132,7 +134,7 @@ public sealed class PlatformAnalyticsQueryExecutor : IPlatformAnalyticsQueryExec
|
||||
SUM(total_components) AS total_components,
|
||||
SUM(unique_suppliers) AS unique_suppliers
|
||||
FROM analytics.daily_component_counts
|
||||
WHERE snapshot_date >= CURRENT_DATE - (@days || ' days')::INTERVAL
|
||||
WHERE snapshot_date >= CURRENT_DATE - make_interval(days => @days)
|
||||
AND (@environment IS NULL OR environment = @environment)
|
||||
GROUP BY snapshot_date, environment
|
||||
ORDER BY environment, snapshot_date;
|
||||
@@ -147,7 +149,8 @@ public sealed class PlatformAnalyticsQueryExecutor : IPlatformAnalyticsQueryExec
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
command.Parameters.AddWithValue("days", days);
|
||||
command.Parameters.AddWithValue("environment", (object?)environment ?? DBNull.Value);
|
||||
var envParam2 = command.Parameters.Add("environment", NpgsqlDbType.Text);
|
||||
envParam2.Value = (object?)environment ?? DBNull.Value;
|
||||
|
||||
var results = new List<AnalyticsComponentTrendPoint>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -291,6 +291,17 @@ builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configurationSection: $"{PolicyEngineOptions.SectionName}:ResourceServer");
|
||||
|
||||
// Accept self-signed certificates when HTTPS metadata validation is disabled (dev/Docker)
|
||||
if (!bootstrap.Options.ResourceServer.RequireHttpsMetadata)
|
||||
{
|
||||
builder.Services.AddHttpClient("StellaOps.Auth.ServerIntegration.Metadata")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
});
|
||||
}
|
||||
|
||||
if (bootstrap.Options.Authority.Enabled)
|
||||
{
|
||||
builder.Services.AddStellaOpsAuthClient(clientOptions =>
|
||||
|
||||
@@ -133,6 +133,9 @@ builder.Services.AddAuthentication();
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddStellaOpsScopeHandler();
|
||||
builder.Services.AddPolicyPostgresStorage(builder.Configuration);
|
||||
// Also configure unnamed PostgresOptions so PolicyDataSource (IOptions<PostgresOptions>) resolves the connection string.
|
||||
builder.Services.Configure<StellaOps.Infrastructure.Postgres.Options.PostgresOptions>(
|
||||
builder.Configuration.GetSection("Postgres:Policy"));
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
// Exception services
|
||||
@@ -198,6 +201,20 @@ builder.Services.AddSingleton<IToolAccessEvaluator, ToolAccessEvaluator>();
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configurationSection: $"{PolicyGatewayOptions.SectionName}:ResourceServer");
|
||||
|
||||
// Accept self-signed certificates when HTTPS metadata validation is disabled (dev/Docker)
|
||||
if (!bootstrap.Options.ResourceServer.RequireHttpsMetadata)
|
||||
{
|
||||
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
|
||||
{
|
||||
clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<PolicyGatewayMetrics>();
|
||||
builder.Services.AddSingleton<PolicyGatewayDpopProofGenerator>();
|
||||
builder.Services.AddSingleton<PolicyEngineTokenProvider>();
|
||||
|
||||
@@ -39,13 +39,20 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
"X-Stella-Project",
|
||||
"X-Stella-Actor",
|
||||
"X-Stella-Scopes",
|
||||
// Headers used by downstream services in header-based auth mode
|
||||
"X-Scopes",
|
||||
"X-Tenant-Id",
|
||||
// Raw claim headers (internal/legacy pass-through)
|
||||
"sub",
|
||||
"tid",
|
||||
"scope",
|
||||
"scp",
|
||||
"cnf",
|
||||
"cnf.jkt"
|
||||
"cnf.jkt",
|
||||
// Auth headers consumed by the gateway — strip before proxying
|
||||
// so backends trust identity headers instead of re-validating JWT.
|
||||
"Authorization",
|
||||
"DPoP"
|
||||
];
|
||||
|
||||
public IdentityHeaderPolicyMiddleware(
|
||||
@@ -91,8 +98,18 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
|
||||
private void StripReservedHeaders(HttpContext context)
|
||||
{
|
||||
var preserveAuthHeaders = _options.JwtPassthroughPrefixes.Count > 0
|
||||
&& _options.JwtPassthroughPrefixes.Any(prefix =>
|
||||
context.Request.Path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var header in ReservedHeaders)
|
||||
{
|
||||
// Preserve Authorization/DPoP for routes that need JWT pass-through
|
||||
if (preserveAuthHeaders && (header == "Authorization" || header == "DPoP"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (context.Request.Headers.ContainsKey(header))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
@@ -114,7 +131,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
// In AllowAnonymous mode the Gateway cannot validate identity claims.
|
||||
// Pass through the client-provided tenant so the upstream service
|
||||
// can validate it against the JWT's own tenant claim.
|
||||
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : null;
|
||||
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : "default";
|
||||
|
||||
return new IdentityContext
|
||||
{
|
||||
@@ -192,9 +209,37 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
// Expand coarse OIDC scopes to fine-grained service scopes.
|
||||
// This bridges the gap between Authority-registered scopes (e.g. "scheduler:read")
|
||||
// and the fine-grained scopes that downstream services expect (e.g. "scheduler.runs.read").
|
||||
ExpandCoarseScopes(scopes);
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands coarse OIDC scopes into fine-grained service scopes.
|
||||
/// Pattern: "{service}:{action}" expands to "{service}.{resource}.{action}" for known resources.
|
||||
/// </summary>
|
||||
private static void ExpandCoarseScopes(HashSet<string> scopes)
|
||||
{
|
||||
// scheduler:read -> scheduler.schedules.read, scheduler.runs.read
|
||||
// scheduler:operate -> scheduler.schedules.write, scheduler.runs.write, scheduler.runs.preview, scheduler.runs.manage
|
||||
if (scopes.Contains("scheduler:read"))
|
||||
{
|
||||
scopes.Add("scheduler.schedules.read");
|
||||
scopes.Add("scheduler.runs.read");
|
||||
}
|
||||
|
||||
if (scopes.Contains("scheduler:operate"))
|
||||
{
|
||||
scopes.Add("scheduler.schedules.write");
|
||||
scopes.Add("scheduler.runs.write");
|
||||
scopes.Add("scheduler.runs.preview");
|
||||
scopes.Add("scheduler.runs.manage");
|
||||
}
|
||||
}
|
||||
|
||||
private void StoreIdentityContext(HttpContext context, IdentityContext identity)
|
||||
{
|
||||
context.Items[GatewayContextKeys.IsAnonymous] = identity.IsAnonymous;
|
||||
@@ -248,6 +293,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
if (!string.IsNullOrEmpty(identity.Tenant))
|
||||
{
|
||||
headers["X-StellaOps-Tenant"] = identity.Tenant;
|
||||
headers["X-Tenant-Id"] = identity.Tenant;
|
||||
if (_options.EnableLegacyHeaders)
|
||||
{
|
||||
headers["X-Stella-Tenant"] = identity.Tenant;
|
||||
@@ -270,6 +316,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
var sortedScopes = identity.Scopes.OrderBy(s => s, StringComparer.Ordinal);
|
||||
var scopesValue = string.Join(" ", sortedScopes);
|
||||
headers["X-StellaOps-Scopes"] = scopesValue;
|
||||
headers["X-Scopes"] = scopesValue;
|
||||
if (_options.EnableLegacyHeaders)
|
||||
{
|
||||
headers["X-Stella-Scopes"] = scopesValue;
|
||||
@@ -279,6 +326,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
{
|
||||
// Explicit empty scopes for anonymous to prevent ambiguity
|
||||
headers["X-StellaOps-Scopes"] = string.Empty;
|
||||
headers["X-Scopes"] = string.Empty;
|
||||
if (_options.EnableLegacyHeaders)
|
||||
{
|
||||
headers["X-Stella-Scopes"] = string.Empty;
|
||||
@@ -347,4 +395,13 @@ public sealed class IdentityHeaderPolicyOptions
|
||||
/// Default: false (forbidden for security).
|
||||
/// </summary>
|
||||
public bool AllowScopeHeaderOverride { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Route prefixes where Authorization and DPoP headers should be preserved
|
||||
/// (passed through to the upstream service) instead of stripped.
|
||||
/// Use this for upstream services that require JWT validation themselves
|
||||
/// (e.g., Authority admin API at /console).
|
||||
/// Default: empty (strip auth headers for all routes).
|
||||
/// </summary>
|
||||
public List<string> JwtPassthroughPrefixes { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -124,7 +124,11 @@ builder.Services.AddSingleton<IDpopProofValidator, DpopProofValidator>();
|
||||
builder.Services.AddSingleton(new IdentityHeaderPolicyOptions
|
||||
{
|
||||
EnableLegacyHeaders = bootstrapOptions.Auth.EnableLegacyHeaders,
|
||||
AllowScopeHeaderOverride = bootstrapOptions.Auth.AllowScopeHeader
|
||||
AllowScopeHeaderOverride = bootstrapOptions.Auth.AllowScopeHeader,
|
||||
JwtPassthroughPrefixes = bootstrapOptions.Routes
|
||||
.Where(r => r.PreserveAuthHeaders)
|
||||
.Select(r => r.Path)
|
||||
.ToList()
|
||||
});
|
||||
|
||||
// Route table: resolver + error routes + HTTP client for reverse proxy
|
||||
@@ -222,6 +226,20 @@ static void ConfigureAuthentication(WebApplicationBuilder builder, GatewayOption
|
||||
}
|
||||
});
|
||||
|
||||
// Configure the OIDC metadata HTTP client to accept self-signed certificates
|
||||
// (Authority uses a dev cert in Docker)
|
||||
if (!authOptions.Authority.RequireHttpsMetadata)
|
||||
{
|
||||
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
|
||||
{
|
||||
clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (authOptions.Authority.RequiredScopes.Count > 0)
|
||||
{
|
||||
builder.Services.AddAuthorization(config =>
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"MtlsEnabled": false,
|
||||
"AllowAnonymous": true,
|
||||
"Authority": {
|
||||
"Issuer": "",
|
||||
"RequireHttpsMetadata": true,
|
||||
"MetadataAddress": "",
|
||||
"Issuer": "https://authority.stella-ops.local",
|
||||
"RequireHttpsMetadata": false,
|
||||
"MetadataAddress": "https://authority.stella-ops.local/.well-known/openid-configuration",
|
||||
"Audiences": [],
|
||||
"RequiredScopes": []
|
||||
}
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
"Routes": [
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/release-orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/release-orchestrator" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notify", "TranslatesTo": "http://notify.stella-ops.local/api/v1/notify" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notifier", "TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier" },
|
||||
@@ -78,7 +78,7 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/v1/audit-bundles", "TranslatesTo": "http://evidencelocker.stella-ops.local/v1/audit-bundles" },
|
||||
{ "Type": "ReverseProxy", "Path": "/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/analytics", "TranslatesTo": "http://platform.stella-ops.local/api/analytics" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/release-orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/release-orchestrator" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/releases", "TranslatesTo": "http://orchestrator.stella-ops.local/api/releases" },
|
||||
@@ -87,15 +87,16 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/scanner", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scanner" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/findings", "TranslatesTo": "http://findings.stella-ops.local/api/v1/findings" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/reachability", "TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/attestor", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestor" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/attestations", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/sbom", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/signals", "TranslatesTo": "http://signals.stella-ops.local/api/v1/signals" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/orchestrator" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "https://authority.stella-ops.local/api/v1/authority" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "https://authority.stella-ops.local/api/v1/authority", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/evidence", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/evidence" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/proofs", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/proofs" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/timeline", "TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline" },
|
||||
@@ -127,15 +128,15 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/api/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/sbomservice", "TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/vuln-explorer", "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/vex" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/vex" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/admin", "TranslatesTo": "http://platform.stella-ops.local/api/admin" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api", "TranslatesTo": "http://platform.stella-ops.local/api" },
|
||||
{ "Type": "ReverseProxy", "Path": "/platform", "TranslatesTo": "http://platform.stella-ops.local/platform" },
|
||||
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "https://authority.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "https://authority.stella-ops.local/.well-known" },
|
||||
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "https://authority.stella-ops.local/jwks" },
|
||||
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "https://authority.stella-ops.local/authority" },
|
||||
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "https://authority.stella-ops.local/console" },
|
||||
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "https://authority.stella-ops.local", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "https://authority.stella-ops.local/.well-known", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "https://authority.stella-ops.local/jwks", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "https://authority.stella-ops.local/authority", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "https://authority.stella-ops.local/console", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/gateway", "TranslatesTo": "http://gateway.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/scanner", "TranslatesTo": "http://scanner.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/policyGateway", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
|
||||
@@ -148,7 +149,7 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/signals", "TranslatesTo": "http://signals.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/excititor", "TranslatesTo": "http://excititor.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/findingsLedger", "TranslatesTo": "http://findings.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "http://vexhub.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "https://vexhub.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/taskrunner", "TranslatesTo": "http://taskrunner.stella-ops.local" },
|
||||
@@ -175,7 +176,6 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/airgapController", "TranslatesTo": "http://airgap-controller.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/airgapTime", "TranslatesTo": "http://airgap-time.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/smremote", "TranslatesTo": "http://smremote.stella-ops.local" },
|
||||
{ "Type": "ReverseProxy", "Path": "/envsettings.json", "TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json" },
|
||||
{ "Type": "StaticFiles", "Path": "/", "TranslatesTo": "/app/wwwroot", "Headers": { "x-spa-fallback": "true" } },
|
||||
{ "Type": "NotFoundPage", "Path": "/_error/404", "TranslatesTo": "/app/wwwroot/index.html" },
|
||||
{ "Type": "ServerErrorPage", "Path": "/_error/500", "TranslatesTo": "/app/wwwroot/index.html" }
|
||||
|
||||
@@ -22,4 +22,11 @@ public sealed class StellaOpsRoute
|
||||
public string? TranslatesTo { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When true, the gateway preserves Authorization and DPoP headers instead
|
||||
/// of stripping them. Use for upstream services that perform their own JWT
|
||||
/// validation (e.g., Authority admin API).
|
||||
/// </summary>
|
||||
public bool PreserveAuthHeaders { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
internal sealed class HeaderScopeAuthorizer : IScopeAuthorizer
|
||||
{
|
||||
private const string ScopeHeader = "X-Scopes";
|
||||
private const string ScopeHeader = "X-StellaOps-Scopes";
|
||||
|
||||
public void EnsureScope(HttpContext context, string requiredScope)
|
||||
{
|
||||
@@ -23,9 +23,30 @@ internal sealed class HeaderScopeAuthorizer : IScopeAuthorizer
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!scopes.Contains(requiredScope))
|
||||
if (scopes.Contains(requiredScope))
|
||||
{
|
||||
throw new InvalidOperationException($"Missing required scope '{requiredScope}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Hierarchical match: fine-grained scope "scheduler.runs.read" is satisfied
|
||||
// by OIDC coarse-grained scope "scheduler:read" or "scheduler:admin".
|
||||
// Format: "{service}.{resource}.{action}" -> check "{service}:{action}" and "{service}:admin"
|
||||
var dotParts = requiredScope.Split('.');
|
||||
if (dotParts.Length >= 2)
|
||||
{
|
||||
var service = dotParts[0];
|
||||
var action = dotParts[^1];
|
||||
if (scopes.Contains($"{service}:{action}") || scopes.Contains($"{service}:admin"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Also check "operate" scope for write/manage actions
|
||||
if (action is "write" or "manage" or "preview" && scopes.Contains($"{service}:operate"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Missing required scope '{requiredScope}'.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,42 @@ public sealed record ObservedCallPath
|
||||
public IReadOnlyList<ulong?>? BinaryOffsets { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata describing which BTF source was selected for probe loading.
|
||||
/// </summary>
|
||||
public sealed record RuntimeBtfSelection
|
||||
{
|
||||
/// <summary>
|
||||
/// Selected BTF source kind (kernel, external-vmlinux, split-btf, unsupported, unavailable).
|
||||
/// </summary>
|
||||
public required string SourceKind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path of the selected BTF source.
|
||||
/// </summary>
|
||||
public string? SourcePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 digest of the selected BTF source.
|
||||
/// </summary>
|
||||
public string? SourceDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic reason for the selected source.
|
||||
/// </summary>
|
||||
public required string SelectionReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Kernel release used for BTF lookup.
|
||||
/// </summary>
|
||||
public required string KernelRelease { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Kernel architecture used for BTF lookup.
|
||||
/// </summary>
|
||||
public required string KernelArch { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of runtime signals collected for a container.
|
||||
/// </summary>
|
||||
@@ -265,6 +301,11 @@ public sealed record RuntimeSignalSummary
|
||||
/// Combined hash of all observed paths for summary-level identity.
|
||||
/// </summary>
|
||||
public string? CombinedPathHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BTF source metadata selected for this runtime collection.
|
||||
/// </summary>
|
||||
public RuntimeBtfSelection? BtfSelection { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "config.json",
|
||||
"input": "src/config",
|
||||
|
||||
114
src/Web/StellaOps.Web/debug-auth.mjs
Normal file
114
src/Web/StellaOps.Web/debug-auth.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE = 'http://127.1.0.5';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
|
||||
// Step 1: Sign in
|
||||
console.log('=== SIGNING IN ===');
|
||||
await page.goto(BASE + '/', { waitUntil: 'networkidle', timeout: 15000 });
|
||||
|
||||
const signInBtn = page.locator('button:has-text("Sign In"), a:has-text("Sign In"), [routerLink*="auth"]').first();
|
||||
try { await signInBtn.click({ timeout: 5000 }); } catch { await page.goto(BASE + '/auth/login', { waitUntil: 'networkidle', timeout: 10000 }); }
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
try {
|
||||
await page.locator('input[name="Username"], input[name="username"], input[type="text"]').first().fill('admin', { timeout: 5000 });
|
||||
await page.locator('input[name="Password"], input[name="password"], input[type="password"]').first().fill('Admin@Stella2026!');
|
||||
await page.locator('button[type="submit"], button:has-text("Log in"), button:has-text("Login"), button:has-text("Sign in")').first().click();
|
||||
await page.waitForTimeout(4000);
|
||||
} catch (e) {
|
||||
console.log('Login error: ' + e.message);
|
||||
}
|
||||
|
||||
console.log('After login: ' + page.url());
|
||||
|
||||
// Step 2: Check auth session state
|
||||
const authState = await page.evaluate(() => {
|
||||
// Check sessionStorage and localStorage for tokens
|
||||
const keys = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) keys.push('session:' + sessionStorage.key(i));
|
||||
for (let i = 0; i < localStorage.length; i++) keys.push('local:' + localStorage.key(i));
|
||||
return { keys, url: window.location.href };
|
||||
});
|
||||
console.log('Storage keys:', JSON.stringify(authState.keys));
|
||||
|
||||
// Step 3: Navigate to scheduler and capture FULL request details
|
||||
console.log('\n=== CAPTURING SCHEDULER REQUEST ===');
|
||||
|
||||
page.on('request', (request) => {
|
||||
const url = request.url();
|
||||
if (url.includes('/scheduler/') || url.includes('/api/v1/scheduler')) {
|
||||
console.log('\nREQUEST:');
|
||||
console.log(' URL: ' + url);
|
||||
console.log(' Method: ' + request.method());
|
||||
const headers = request.headers();
|
||||
console.log(' Authorization: ' + (headers['authorization'] || 'NONE'));
|
||||
console.log(' DPoP: ' + (headers['dpop'] ? headers['dpop'].substring(0, 80) + '...' : 'NONE'));
|
||||
console.log(' X-StellaOps-Tenant: ' + (headers['x-stellaops-tenant'] || 'NONE'));
|
||||
console.log(' X-Tenant-Id: ' + (headers['x-tenant-id'] || 'NONE'));
|
||||
console.log(' X-Scopes: ' + (headers['x-scopes'] || 'not set by client'));
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', async (response) => {
|
||||
const url = response.url();
|
||||
if (url.includes('/scheduler/') || url.includes('/api/v1/scheduler')) {
|
||||
console.log('\nRESPONSE:');
|
||||
console.log(' URL: ' + url);
|
||||
console.log(' Status: ' + response.status());
|
||||
try {
|
||||
const body = await response.text();
|
||||
console.log(' Body: ' + body.substring(0, 300));
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
// Also capture token endpoint requests
|
||||
page.on('request', (request) => {
|
||||
const url = request.url();
|
||||
if (url.includes('/connect/token') || url.includes('/authority/connect/token')) {
|
||||
console.log('\nTOKEN REQUEST: ' + url);
|
||||
console.log(' Method: ' + request.method());
|
||||
}
|
||||
});
|
||||
page.on('response', async (response) => {
|
||||
const url = response.url();
|
||||
if (url.includes('/connect/token') || url.includes('/authority/connect/token')) {
|
||||
console.log('TOKEN RESPONSE: ' + response.status());
|
||||
}
|
||||
});
|
||||
|
||||
await page.evaluate((r) => {
|
||||
window.history.pushState({}, '', r);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}, '/operations/scheduler');
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Step 4: Also check what the Angular app thinks its auth state is
|
||||
const appAuthState = await page.evaluate(() => {
|
||||
try {
|
||||
// Try to access Angular's injector
|
||||
const appRef = window.ng?.getComponent(document.querySelector('app-root'));
|
||||
return { hasAppRef: !!appRef };
|
||||
} catch {
|
||||
return { hasAppRef: false };
|
||||
}
|
||||
});
|
||||
console.log('\nApp auth state:', JSON.stringify(appAuthState));
|
||||
|
||||
// Check console errors
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error' || msg.type() === 'warn') {
|
||||
console.log('CONSOLE [' + msg.type() + ']: ' + msg.text().substring(0, 200));
|
||||
}
|
||||
});
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
65
src/Web/StellaOps.Web/probe-services.mjs
Normal file
65
src/Web/StellaOps.Web/probe-services.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE = 'http://127.1.0.5';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
|
||||
// Sign in
|
||||
console.log('=== SIGNING IN ===');
|
||||
await page.goto(BASE + '/', { waitUntil: 'networkidle', timeout: 15000 });
|
||||
const signInBtn = page.locator('button:has-text("Sign In"), a:has-text("Sign In")').first();
|
||||
try { await signInBtn.click({ timeout: 5000 }); } catch {}
|
||||
await page.waitForTimeout(2000);
|
||||
try {
|
||||
await page.locator('input[name="Username"], input[type="text"]').first().fill('admin', { timeout: 5000 });
|
||||
await page.locator('input[type="password"]').first().fill('Admin@Stella2026!');
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForTimeout(4000);
|
||||
} catch (e) { console.log('Login error: ' + e.message); }
|
||||
console.log('Signed in: ' + page.url());
|
||||
|
||||
// Probe specific failing pages
|
||||
const failPages = [
|
||||
'/operations/scheduler',
|
||||
'/operations/notifications',
|
||||
'/evidence/bundles',
|
||||
'/policy',
|
||||
];
|
||||
|
||||
for (const route of failPages) {
|
||||
const apiCalls = [];
|
||||
|
||||
page.on('response', async (response) => {
|
||||
const url = response.url();
|
||||
if (url.startsWith(BASE) && !url.includes('.js') && !url.includes('.css') && !url.includes('.html') && !url.includes('/config.json') && !url.includes('.ico')) {
|
||||
const path = new URL(url).pathname;
|
||||
if (path.startsWith('/api/') || path.startsWith('/v1/') || path.startsWith('/scheduler/') ||
|
||||
path.startsWith('/doctor/') || path.startsWith('/console/') || path.startsWith('/health')) {
|
||||
let body = '';
|
||||
try { body = await response.text(); } catch {}
|
||||
apiCalls.push({ path, status: response.status(), body: body.substring(0, 200) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await page.evaluate((r) => {
|
||||
window.history.pushState({}, '', r);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}, route);
|
||||
|
||||
await page.waitForTimeout(4000);
|
||||
page.removeAllListeners('response');
|
||||
|
||||
console.log('\n--- ' + route + ' ---');
|
||||
for (const c of apiCalls) {
|
||||
console.log(' ' + c.status + ' ' + c.path);
|
||||
if (c.status >= 400) console.log(' Body: ' + c.body);
|
||||
}
|
||||
if (apiCalls.length === 0) console.log(' NO API CALLS');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -1,54 +1,54 @@
|
||||
{
|
||||
"/envsettings.json": {
|
||||
"target": "http://127.1.0.3:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/platform": {
|
||||
"target": "http://127.1.0.3:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/api": {
|
||||
"target": "http://127.1.0.3:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/authority": {
|
||||
"target": "http://127.1.0.4:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/console": {
|
||||
"target": "http://127.1.0.4:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/connect": {
|
||||
"target": "http://127.1.0.4:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/.well-known": {
|
||||
"target": "http://127.1.0.4:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/jwks": {
|
||||
"target": "http://127.1.0.4:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/scanner": {
|
||||
"target": "http://127.1.0.8:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/policyGateway": {
|
||||
"target": "http://127.1.0.14:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/policyEngine": {
|
||||
"target": "http://127.1.0.14:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/concelier": {
|
||||
"target": "http://127.1.0.9:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/attestor": {
|
||||
"target": "http://127.1.0.13:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/gateway": {
|
||||
@@ -56,59 +56,67 @@
|
||||
"secure": false
|
||||
},
|
||||
"/notify": {
|
||||
"target": "http://127.1.0.29:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/scheduler": {
|
||||
"target": "http://127.1.0.19:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/signals": {
|
||||
"target": "http://127.1.0.43:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/excititor": {
|
||||
"target": "http://127.1.0.9:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/findingsLedger": {
|
||||
"target": "http://127.1.0.9:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/vexhub": {
|
||||
"target": "http://127.1.0.11:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/vexlens": {
|
||||
"target": "http://127.1.0.12:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/orchestrator": {
|
||||
"target": "http://127.1.0.17:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/graph": {
|
||||
"target": "http://127.1.0.20:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/doctor": {
|
||||
"target": "http://127.1.0.26:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/integrations": {
|
||||
"target": "http://127.1.0.42:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/replay": {
|
||||
"target": "http://127.1.0.41:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/exportcenter": {
|
||||
"target": "http://127.1.0.40:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/healthz": {
|
||||
"target": "http://127.1.0.3:80",
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/policy": {
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
},
|
||||
"/v1": {
|
||||
"target": "http://127.1.0.5:80",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
|
||||
105
src/Web/StellaOps.Web/scan-pages.mjs
Normal file
105
src/Web/StellaOps.Web/scan-pages.mjs
Normal file
@@ -0,0 +1,105 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE = 'http://127.1.0.5';
|
||||
|
||||
const routes = [
|
||||
'/security',
|
||||
'/security/findings',
|
||||
'/security/exceptions',
|
||||
'/security/vex',
|
||||
'/security/vulnerabilities',
|
||||
'/operations/scheduler',
|
||||
'/operations/doctor',
|
||||
'/operations/feeds',
|
||||
'/operations/notifications',
|
||||
'/operations/health',
|
||||
'/evidence/bundles',
|
||||
'/evidence/export',
|
||||
'/releases',
|
||||
'/releases/environments',
|
||||
'/approvals',
|
||||
'/policy',
|
||||
'/policy/governance',
|
||||
'/triage',
|
||||
'/sources',
|
||||
'/analytics',
|
||||
'/settings/admin',
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
|
||||
// Step 1: Sign in
|
||||
console.log('=== SIGNING IN ===');
|
||||
await page.goto(BASE + '/', { waitUntil: 'networkidle', timeout: 15000 });
|
||||
|
||||
// Click sign in button
|
||||
const signInBtn = page.locator('button:has-text("Sign In"), a:has-text("Sign In"), [routerLink*="auth"]').first();
|
||||
try {
|
||||
await signInBtn.click({ timeout: 5000 });
|
||||
} catch {
|
||||
await page.goto(BASE + '/auth/login', { waitUntil: 'networkidle', timeout: 10000 });
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Login page URL: ' + page.url());
|
||||
|
||||
try {
|
||||
const usernameInput = page.locator('input[name="Username"], input[name="username"], input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[name="Password"], input[name="password"], input[type="password"]').first();
|
||||
|
||||
await usernameInput.fill('admin', { timeout: 5000 });
|
||||
await passwordInput.fill('Admin@Stella2026!');
|
||||
|
||||
const loginBtn = page.locator('button[type="submit"], button:has-text("Log in"), button:has-text("Login"), button:has-text("Sign in")').first();
|
||||
await loginBtn.click();
|
||||
await page.waitForTimeout(3000);
|
||||
console.log('After login URL: ' + page.url());
|
||||
} catch (e) {
|
||||
console.log('Login form error: ' + e.message);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
console.log('Final URL after auth: ' + page.url());
|
||||
|
||||
// Step 2: Navigate to each route using pushState
|
||||
console.log('\n=== PAGE SCAN (with fresh token) ===');
|
||||
|
||||
for (const route of routes) {
|
||||
const apiCalls = [];
|
||||
|
||||
const handler = (response) => {
|
||||
const url = response.url();
|
||||
if (!url.includes('.js') && !url.includes('.css') && !url.includes('.ico') &&
|
||||
!url.includes('.png') && !url.includes('.svg') && !url.includes('.woff') &&
|
||||
!url.includes('/config.json') && !url.includes('.html') &&
|
||||
!url.startsWith('data:') && url.startsWith(BASE)) {
|
||||
const path = new URL(url).pathname;
|
||||
if (path.startsWith('/api/') || path.startsWith('/v1/') || path.startsWith('/platform/') ||
|
||||
path.startsWith('/scanner/') || path.startsWith('/policy/') || path.startsWith('/scheduler/') ||
|
||||
path.startsWith('/doctor/') || path.startsWith('/authority/') || path.startsWith('/console/') ||
|
||||
path.startsWith('/concelier/') || path.startsWith('/attestor/') || path.startsWith('/analytics') ||
|
||||
path.startsWith('/health')) {
|
||||
apiCalls.push({ path, status: response.status() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
page.on('response', handler);
|
||||
|
||||
await page.evaluate((r) => {
|
||||
window.history.pushState({}, '', r);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}, route);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
page.removeListener('response', handler);
|
||||
|
||||
const callSummary = apiCalls.map(c => c.status + ' ' + c.path).join(', ') || 'NO API CALLS';
|
||||
console.log(route + ': ' + callSummary);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
BIN
src/Web/StellaOps.Web/scheduler-debug.png
Normal file
BIN
src/Web/StellaOps.Web/scheduler-debug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -21,15 +21,17 @@ import {
|
||||
NOTIFY_API_BASE_URL,
|
||||
NOTIFY_TENANT_ID,
|
||||
NotifyApiHttpClient,
|
||||
MockNotifyClient,
|
||||
} from './core/api/notify.client';
|
||||
import {
|
||||
EXCEPTION_API,
|
||||
EXCEPTION_API_BASE_URL,
|
||||
ExceptionApiHttpClient,
|
||||
MockExceptionApiService,
|
||||
} from './core/api/exception.client';
|
||||
import { VULNERABILITY_API } from './core/api/vulnerability.client';
|
||||
import { VULNERABILITY_API, MockVulnerabilityApiService } from './core/api/vulnerability.client';
|
||||
import { VULNERABILITY_API_BASE_URL, VulnerabilityHttpClient } from './core/api/vulnerability-http.client';
|
||||
import { RISK_API } from './core/api/risk.client';
|
||||
import { RISK_API, MockRiskApi } from './core/api/risk.client';
|
||||
import { RISK_API_BASE_URL, RiskHttpClient } from './core/api/risk-http.client';
|
||||
import { AppConfigService } from './core/config/app-config.service';
|
||||
import { BackendProbeService } from './core/config/backend-probe.service';
|
||||
@@ -37,6 +39,7 @@ import { AuthHttpInterceptor } from './core/auth/auth-http.interceptor';
|
||||
import { AuthSessionStore } from './core/auth/auth-session.store';
|
||||
import { TenantActivationService } from './core/auth/tenant-activation.service';
|
||||
import { OperatorMetadataInterceptor } from './core/orchestrator/operator-metadata.interceptor';
|
||||
import { TenantHttpInterceptor } from './core/auth/tenant-http.interceptor';
|
||||
import { seedAuthSession, type StubAuthSession } from './testing';
|
||||
import { CVSS_API_BASE_URL } from './core/api/cvss.client';
|
||||
import { AUTH_SERVICE } from './core/auth';
|
||||
@@ -46,32 +49,38 @@ import {
|
||||
ADVISORY_AI_API,
|
||||
ADVISORY_AI_API_BASE_URL,
|
||||
AdvisoryAiApiHttpClient,
|
||||
MockAdvisoryAiClient,
|
||||
} from './core/api/advisory-ai.client';
|
||||
import {
|
||||
ADVISORY_API,
|
||||
ADVISORY_API_BASE_URL,
|
||||
AdvisoryApiHttpClient,
|
||||
MockAdvisoryApiService,
|
||||
} from './core/api/advisories.client';
|
||||
import {
|
||||
VEX_EVIDENCE_API,
|
||||
VEX_EVIDENCE_API_BASE_URL,
|
||||
VexEvidenceHttpClient,
|
||||
MockVexEvidenceClient,
|
||||
} from './core/api/vex-evidence.client';
|
||||
import {
|
||||
VEX_DECISIONS_API,
|
||||
VEX_DECISIONS_API_BASE_URL,
|
||||
VexDecisionsHttpClient,
|
||||
MockVexDecisionsClient,
|
||||
} from './core/api/vex-decisions.client';
|
||||
import { VEX_HUB_API, VEX_HUB_API_BASE_URL, VEX_LENS_API_BASE_URL, VexHubApiHttpClient } from './core/api/vex-hub.client';
|
||||
import { VEX_HUB_API, VEX_HUB_API_BASE_URL, VEX_LENS_API_BASE_URL, VexHubApiHttpClient, MockVexHubClient } from './core/api/vex-hub.client';
|
||||
import {
|
||||
AUDIT_BUNDLES_API,
|
||||
AUDIT_BUNDLES_API_BASE_URL,
|
||||
AuditBundlesHttpClient,
|
||||
MockAuditBundlesClient,
|
||||
} from './core/api/audit-bundles.client';
|
||||
import {
|
||||
POLICY_EXCEPTIONS_API,
|
||||
POLICY_EXCEPTIONS_API_BASE_URL,
|
||||
PolicyExceptionsHttpClient,
|
||||
MockPolicyExceptionsApiService,
|
||||
} from './core/api/policy-exceptions.client';
|
||||
import {
|
||||
POLICY_EVIDENCE_API,
|
||||
@@ -81,29 +90,35 @@ import {
|
||||
ORCHESTRATOR_API,
|
||||
ORCHESTRATOR_API_BASE_URL,
|
||||
OrchestratorHttpClient,
|
||||
MockOrchestratorClient,
|
||||
} from './core/api/orchestrator.client';
|
||||
import {
|
||||
ORCHESTRATOR_CONTROL_API,
|
||||
OrchestratorControlHttpClient,
|
||||
MockOrchestratorControlClient,
|
||||
} from './core/api/orchestrator-control.client';
|
||||
import {
|
||||
FIRST_SIGNAL_API,
|
||||
FirstSignalHttpClient,
|
||||
MockFirstSignalClient,
|
||||
} from './core/api/first-signal.client';
|
||||
import {
|
||||
EXCEPTION_EVENTS_API,
|
||||
EXCEPTION_EVENTS_API_BASE_URL,
|
||||
ExceptionEventsHttpClient,
|
||||
MockExceptionEventsApiService,
|
||||
} from './core/api/exception-events.client';
|
||||
import {
|
||||
EVIDENCE_PACK_API,
|
||||
EVIDENCE_PACK_API_BASE_URL,
|
||||
EvidencePackHttpClient,
|
||||
MockEvidencePackClient,
|
||||
} from './core/api/evidence-pack.client';
|
||||
import {
|
||||
AI_RUNS_API,
|
||||
AI_RUNS_API_BASE_URL,
|
||||
AiRunsHttpClient,
|
||||
MockAiRunsClient,
|
||||
} from './core/api/ai-runs.client';
|
||||
import {
|
||||
RELEASE_DASHBOARD_API,
|
||||
@@ -115,30 +130,37 @@ import {
|
||||
RELEASE_ENVIRONMENT_API,
|
||||
RELEASE_ENVIRONMENT_API_BASE_URL,
|
||||
ReleaseEnvironmentHttpClient,
|
||||
MockReleaseEnvironmentClient,
|
||||
} from './core/api/release-environment.client';
|
||||
import {
|
||||
RELEASE_MANAGEMENT_API,
|
||||
ReleaseManagementHttpClient,
|
||||
MockReleaseManagementClient,
|
||||
} from './core/api/release-management.client';
|
||||
import {
|
||||
WORKFLOW_API,
|
||||
WorkflowHttpClient,
|
||||
MockWorkflowClient,
|
||||
} from './core/api/workflow.client';
|
||||
import {
|
||||
APPROVAL_API,
|
||||
ApprovalHttpClient,
|
||||
MockApprovalClient,
|
||||
} from './core/api/approval.client';
|
||||
import {
|
||||
DEPLOYMENT_API,
|
||||
DeploymentHttpClient,
|
||||
MockDeploymentClient,
|
||||
} from './core/api/deployment.client';
|
||||
import {
|
||||
RELEASE_EVIDENCE_API,
|
||||
ReleaseEvidenceHttpClient,
|
||||
MockReleaseEvidenceClient,
|
||||
} from './core/api/release-evidence.client';
|
||||
import {
|
||||
DOCTOR_API,
|
||||
HttpDoctorClient,
|
||||
MockDoctorClient,
|
||||
} from './features/doctor/services/doctor.client';
|
||||
import {
|
||||
WITNESS_API,
|
||||
@@ -148,18 +170,22 @@ import {
|
||||
NOTIFIER_API,
|
||||
NOTIFIER_API_BASE_URL,
|
||||
NotifierApiHttpClient,
|
||||
MockNotifierClient,
|
||||
} from './core/api/notifier.client';
|
||||
import {
|
||||
POLICY_ENGINE_API,
|
||||
PolicyEngineHttpClient,
|
||||
MockPolicyEngineApi,
|
||||
} from './core/api/policy-engine.client';
|
||||
import {
|
||||
TRUST_API,
|
||||
TrustHttpService,
|
||||
MockTrustApiService,
|
||||
} from './core/api/trust.client';
|
||||
import {
|
||||
VULN_ANNOTATION_API,
|
||||
HttpVulnAnnotationClient,
|
||||
MockVulnAnnotationClient,
|
||||
} from './core/api/vuln-annotation.client';
|
||||
import {
|
||||
AUTHORITY_ADMIN_API,
|
||||
@@ -171,16 +197,46 @@ import {
|
||||
SECURITY_FINDINGS_API,
|
||||
SECURITY_FINDINGS_API_BASE_URL,
|
||||
SecurityFindingsHttpClient,
|
||||
MockSecurityFindingsClient,
|
||||
} from './core/api/security-findings.client';
|
||||
import {
|
||||
SECURITY_OVERVIEW_API,
|
||||
SecurityOverviewHttpClient,
|
||||
MockSecurityOverviewClient,
|
||||
} from './core/api/security-overview.client';
|
||||
import {
|
||||
CONSOLE_VULN_API,
|
||||
ConsoleVulnHttpClient,
|
||||
MockConsoleVulnClient,
|
||||
} from './core/api/console-vuln.client';
|
||||
import {
|
||||
REACHABILITY_API,
|
||||
ReachabilityClient,
|
||||
MockReachabilityApi,
|
||||
} from './core/api/reachability.client';
|
||||
import {
|
||||
SCHEDULER_API,
|
||||
SCHEDULER_API_BASE_URL,
|
||||
SchedulerHttpClient,
|
||||
MockSchedulerClient,
|
||||
} from './core/api/scheduler.client';
|
||||
import { AnalyticsHttpClient, MockAnalyticsClient } from './core/api/analytics.client';
|
||||
import { FEED_MIRROR_API, FEED_MIRROR_API_BASE_URL, FeedMirrorHttpClient } from './core/api/feed-mirror.client';
|
||||
import { ATTESTATION_CHAIN_API, AttestationChainHttpClient } from './core/api/attestation-chain.client';
|
||||
import { CONSOLE_SEARCH_API, ConsoleSearchHttpClient } from './core/api/console-search.client';
|
||||
import { POLICY_GOVERNANCE_API, HttpPolicyGovernanceApi } from './core/api/policy-governance.client';
|
||||
import { POLICY_GATES_API, POLICY_GATES_API_BASE_URL, PolicyGatesHttpClient } from './core/api/policy-gates.client';
|
||||
import { RELEASE_API, ReleaseHttpClient } from './core/api/release.client';
|
||||
import { TRIAGE_EVIDENCE_API, TriageEvidenceHttpClient } from './core/api/triage-evidence.client';
|
||||
import { VERDICT_API, HttpVerdictClient } from './core/api/verdict.client';
|
||||
import { WATCHLIST_API, WatchlistHttpClient } from './core/api/watchlist.client';
|
||||
import { EVIDENCE_API, EVIDENCE_API_BASE_URL, EvidenceHttpClient } from './core/api/evidence.client';
|
||||
import { SBOM_EVIDENCE_API, SbomEvidenceService } from './features/sbom/services/sbom-evidence.service';
|
||||
import { HttpReplayClient } from './core/api/replay.client';
|
||||
import { REPLAY_API } from './features/proofs/proof-replay-dashboard.component';
|
||||
import { HttpScoreClient } from './core/api/score.client';
|
||||
import { SCORE_API } from './features/scores/score-comparison.component';
|
||||
import { AOC_API, AOC_API_BASE_URL, AOC_SOURCES_API_BASE_URL, AocHttpClient } from './core/api/aoc.client';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@@ -207,6 +263,11 @@ export const appConfig: ApplicationConfig = {
|
||||
useClass: OperatorMetadataInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: TenantHttpInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: CONCELIER_EXPORTER_API_BASE_URL,
|
||||
useValue: '/api/v1/concelier/exporters/trivy-db',
|
||||
@@ -278,6 +339,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
RiskHttpClient,
|
||||
MockRiskApi,
|
||||
{
|
||||
provide: RISK_API,
|
||||
useExisting: RiskHttpClient,
|
||||
@@ -298,6 +360,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
VulnerabilityHttpClient,
|
||||
MockVulnerabilityApiService,
|
||||
{
|
||||
provide: VULNERABILITY_API,
|
||||
useExisting: VulnerabilityHttpClient,
|
||||
@@ -329,6 +392,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
AdvisoryAiApiHttpClient,
|
||||
MockAdvisoryAiClient,
|
||||
{
|
||||
provide: ADVISORY_AI_API,
|
||||
useExisting: AdvisoryAiApiHttpClient,
|
||||
@@ -342,6 +406,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
AdvisoryApiHttpClient,
|
||||
MockAdvisoryApiService,
|
||||
{
|
||||
provide: ADVISORY_API,
|
||||
useExisting: AdvisoryApiHttpClient,
|
||||
@@ -381,11 +446,13 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
VexHubApiHttpClient,
|
||||
MockVexHubClient,
|
||||
{
|
||||
provide: VEX_HUB_API,
|
||||
useExisting: VexHubApiHttpClient,
|
||||
},
|
||||
VexEvidenceHttpClient,
|
||||
MockVexEvidenceClient,
|
||||
{
|
||||
provide: VEX_EVIDENCE_API,
|
||||
useExisting: VexEvidenceHttpClient,
|
||||
@@ -399,6 +466,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
VexDecisionsHttpClient,
|
||||
MockVexDecisionsClient,
|
||||
{
|
||||
provide: VEX_DECISIONS_API,
|
||||
useExisting: VexDecisionsHttpClient,
|
||||
@@ -412,6 +480,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
AuditBundlesHttpClient,
|
||||
MockAuditBundlesClient,
|
||||
{
|
||||
provide: AUDIT_BUNDLES_API,
|
||||
useExisting: AuditBundlesHttpClient,
|
||||
@@ -425,6 +494,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
PolicyExceptionsHttpClient,
|
||||
MockPolicyExceptionsApiService,
|
||||
{
|
||||
provide: POLICY_EXCEPTIONS_API,
|
||||
useExisting: PolicyExceptionsHttpClient,
|
||||
@@ -443,16 +513,19 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
OrchestratorHttpClient,
|
||||
MockOrchestratorClient,
|
||||
{
|
||||
provide: ORCHESTRATOR_API,
|
||||
useExisting: OrchestratorHttpClient,
|
||||
},
|
||||
OrchestratorControlHttpClient,
|
||||
MockOrchestratorControlClient,
|
||||
{
|
||||
provide: ORCHESTRATOR_CONTROL_API,
|
||||
useExisting: OrchestratorControlHttpClient,
|
||||
},
|
||||
FirstSignalHttpClient,
|
||||
MockFirstSignalClient,
|
||||
{
|
||||
provide: FIRST_SIGNAL_API,
|
||||
useExisting: FirstSignalHttpClient,
|
||||
@@ -466,6 +539,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
ExceptionEventsHttpClient,
|
||||
MockExceptionEventsApiService,
|
||||
{
|
||||
provide: EXCEPTION_EVENTS_API,
|
||||
useExisting: ExceptionEventsHttpClient,
|
||||
@@ -475,10 +549,16 @@ export const appConfig: ApplicationConfig = {
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
return gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
try {
|
||||
return new URL('/api/policy', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/policy`;
|
||||
}
|
||||
},
|
||||
},
|
||||
ExceptionApiHttpClient,
|
||||
MockExceptionApiService,
|
||||
{
|
||||
provide: EXCEPTION_API,
|
||||
useExisting: ExceptionApiHttpClient,
|
||||
@@ -497,6 +577,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
EvidencePackHttpClient,
|
||||
MockEvidencePackClient,
|
||||
{
|
||||
provide: EVIDENCE_PACK_API,
|
||||
useExisting: EvidencePackHttpClient,
|
||||
@@ -515,6 +596,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
AiRunsHttpClient,
|
||||
MockAiRunsClient,
|
||||
{
|
||||
provide: AI_RUNS_API,
|
||||
useExisting: AiRunsHttpClient,
|
||||
@@ -543,11 +625,12 @@ export const appConfig: ApplicationConfig = {
|
||||
useValue: 'tenant-dev',
|
||||
},
|
||||
NotifyApiHttpClient,
|
||||
MockNotifyClient,
|
||||
{
|
||||
provide: NOTIFY_API,
|
||||
useExisting: NotifyApiHttpClient,
|
||||
},
|
||||
// Release Dashboard API
|
||||
// Release Dashboard API (using mock - no backend endpoint yet)
|
||||
{
|
||||
provide: RELEASE_DASHBOARD_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
@@ -582,42 +665,49 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
ReleaseEnvironmentHttpClient,
|
||||
MockReleaseEnvironmentClient,
|
||||
{
|
||||
provide: RELEASE_ENVIRONMENT_API,
|
||||
useExisting: ReleaseEnvironmentHttpClient,
|
||||
},
|
||||
// Release Management API (Sprint 111_003)
|
||||
// Release Management API (Sprint 111_003 - using mock until backend is available)
|
||||
ReleaseManagementHttpClient,
|
||||
MockReleaseManagementClient,
|
||||
{
|
||||
provide: RELEASE_MANAGEMENT_API,
|
||||
useExisting: ReleaseManagementHttpClient,
|
||||
},
|
||||
// Workflow API (Sprint 111_004)
|
||||
// Workflow API (Sprint 111_004 - using mock until backend is available)
|
||||
WorkflowHttpClient,
|
||||
MockWorkflowClient,
|
||||
{
|
||||
provide: WORKFLOW_API,
|
||||
useExisting: WorkflowHttpClient,
|
||||
},
|
||||
// Approval API (Sprint 111_005)
|
||||
// Approval API (using mock - no backend endpoint yet)
|
||||
ApprovalHttpClient,
|
||||
MockApprovalClient,
|
||||
{
|
||||
provide: APPROVAL_API,
|
||||
useExisting: ApprovalHttpClient,
|
||||
},
|
||||
// Deployment API (Sprint 111_006)
|
||||
// Deployment API (Sprint 111_006 - using mock until backend is available)
|
||||
DeploymentHttpClient,
|
||||
MockDeploymentClient,
|
||||
{
|
||||
provide: DEPLOYMENT_API,
|
||||
useExisting: DeploymentHttpClient,
|
||||
},
|
||||
// Release Evidence API (Sprint 111_007)
|
||||
// Release Evidence API (Sprint 111_007 - using mock until backend is available)
|
||||
ReleaseEvidenceHttpClient,
|
||||
MockReleaseEvidenceClient,
|
||||
{
|
||||
provide: RELEASE_EVIDENCE_API,
|
||||
useExisting: ReleaseEvidenceHttpClient,
|
||||
},
|
||||
// Doctor API (Sprint 20260112_001_008)
|
||||
// Doctor API (HTTP paths corrected; using mock until gateway auth chain is configured)
|
||||
HttpDoctorClient,
|
||||
MockDoctorClient,
|
||||
{
|
||||
provide: DOCTOR_API,
|
||||
useExisting: HttpDoctorClient,
|
||||
@@ -643,22 +733,28 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
NotifierApiHttpClient,
|
||||
MockNotifierClient,
|
||||
{
|
||||
provide: NOTIFIER_API,
|
||||
useExisting: NotifierApiHttpClient,
|
||||
},
|
||||
// Policy Engine API (Bug fix: missing DI provider caused NG0201 on /policy/packs)
|
||||
// Policy Engine API
|
||||
PolicyEngineHttpClient,
|
||||
MockPolicyEngineApi,
|
||||
{
|
||||
provide: POLICY_ENGINE_API,
|
||||
useExisting: PolicyEngineHttpClient,
|
||||
},
|
||||
// Trust API (Bug fix: missing DI provider caused NG0201 on /admin/trust)
|
||||
// Trust API
|
||||
TrustHttpService,
|
||||
MockTrustApiService,
|
||||
{
|
||||
provide: TRUST_API,
|
||||
useExisting: TrustHttpService,
|
||||
},
|
||||
// Vuln Annotation API (Bug fix: missing DI provider caused NG0201 on /vulnerabilities/triage)
|
||||
// Vuln Annotation API (using mock until backend is available)
|
||||
HttpVulnAnnotationClient,
|
||||
MockVulnAnnotationClient,
|
||||
{
|
||||
provide: VULN_ANNOTATION_API,
|
||||
useExisting: HttpVulnAnnotationClient,
|
||||
@@ -674,27 +770,24 @@ export const appConfig: ApplicationConfig = {
|
||||
provide: AUTHORITY_ADMIN_API,
|
||||
useExisting: AuthorityAdminHttpClient,
|
||||
},
|
||||
// Security Findings API (scanner findings via gateway)
|
||||
// Security Findings API (findings ledger via gateway)
|
||||
{
|
||||
provide: SECURITY_FINDINGS_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/scanner', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/scanner`;
|
||||
}
|
||||
return gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
},
|
||||
},
|
||||
SecurityFindingsHttpClient,
|
||||
MockSecurityFindingsClient,
|
||||
{
|
||||
provide: SECURITY_FINDINGS_API,
|
||||
useExisting: SecurityFindingsHttpClient,
|
||||
},
|
||||
// Security Overview API (aggregated security dashboard data)
|
||||
SecurityOverviewHttpClient,
|
||||
MockSecurityOverviewClient,
|
||||
{
|
||||
provide: SECURITY_OVERVIEW_API,
|
||||
useExisting: SecurityOverviewHttpClient,
|
||||
@@ -706,17 +799,180 @@ export const appConfig: ApplicationConfig = {
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/scheduler', gatewayBase).toString();
|
||||
return new URL('/scheduler/api/v1/scheduler', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/scheduler`;
|
||||
return `${normalized}/scheduler/api/v1/scheduler`;
|
||||
}
|
||||
},
|
||||
},
|
||||
SchedulerHttpClient,
|
||||
MockSchedulerClient,
|
||||
{
|
||||
provide: SCHEDULER_API,
|
||||
useExisting: SchedulerHttpClient,
|
||||
},
|
||||
// Console Vuln API
|
||||
ConsoleVulnHttpClient,
|
||||
MockConsoleVulnClient,
|
||||
{
|
||||
provide: CONSOLE_VULN_API,
|
||||
useExisting: ConsoleVulnHttpClient,
|
||||
},
|
||||
// Reachability API
|
||||
ReachabilityClient,
|
||||
MockReachabilityApi,
|
||||
{
|
||||
provide: REACHABILITY_API,
|
||||
useExisting: ReachabilityClient,
|
||||
},
|
||||
// Analytics API
|
||||
AnalyticsHttpClient,
|
||||
// Feed Mirror API (Concelier backend via gateway)
|
||||
{
|
||||
provide: FEED_MIRROR_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/api/v1/concelier', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/v1/concelier`;
|
||||
}
|
||||
},
|
||||
},
|
||||
FeedMirrorHttpClient,
|
||||
{
|
||||
provide: FEED_MIRROR_API,
|
||||
useExisting: FeedMirrorHttpClient,
|
||||
},
|
||||
// Attestation Chain API
|
||||
AttestationChainHttpClient,
|
||||
{
|
||||
provide: ATTESTATION_CHAIN_API,
|
||||
useExisting: AttestationChainHttpClient,
|
||||
},
|
||||
// Console Search API
|
||||
ConsoleSearchHttpClient,
|
||||
{
|
||||
provide: CONSOLE_SEARCH_API,
|
||||
useExisting: ConsoleSearchHttpClient,
|
||||
},
|
||||
// Policy Governance API
|
||||
HttpPolicyGovernanceApi,
|
||||
{
|
||||
provide: POLICY_GOVERNANCE_API,
|
||||
useExisting: HttpPolicyGovernanceApi,
|
||||
},
|
||||
// Policy Gates API (Policy Gateway backend)
|
||||
{
|
||||
provide: POLICY_GATES_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/api/policy', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/policy`;
|
||||
}
|
||||
},
|
||||
},
|
||||
PolicyGatesHttpClient,
|
||||
{
|
||||
provide: POLICY_GATES_API,
|
||||
useExisting: PolicyGatesHttpClient,
|
||||
},
|
||||
// Release API (Release Orchestrator backend)
|
||||
ReleaseHttpClient,
|
||||
{
|
||||
provide: RELEASE_API,
|
||||
useExisting: ReleaseHttpClient,
|
||||
},
|
||||
// Triage Evidence API
|
||||
TriageEvidenceHttpClient,
|
||||
{
|
||||
provide: TRIAGE_EVIDENCE_API,
|
||||
useExisting: TriageEvidenceHttpClient,
|
||||
},
|
||||
// Verdict API
|
||||
HttpVerdictClient,
|
||||
{
|
||||
provide: VERDICT_API,
|
||||
useExisting: HttpVerdictClient,
|
||||
},
|
||||
// Watchlist API
|
||||
WatchlistHttpClient,
|
||||
{
|
||||
provide: WATCHLIST_API,
|
||||
useExisting: WatchlistHttpClient,
|
||||
},
|
||||
// Evidence API (Evidence Locker backend via gateway)
|
||||
{
|
||||
provide: EVIDENCE_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/api/v1/evidence', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/v1/evidence`;
|
||||
}
|
||||
},
|
||||
},
|
||||
EvidenceHttpClient,
|
||||
{
|
||||
provide: EVIDENCE_API,
|
||||
useExisting: EvidenceHttpClient,
|
||||
},
|
||||
// SBOM Evidence API
|
||||
SbomEvidenceService,
|
||||
{
|
||||
provide: SBOM_EVIDENCE_API,
|
||||
useExisting: SbomEvidenceService,
|
||||
},
|
||||
// Replay API
|
||||
HttpReplayClient,
|
||||
{
|
||||
provide: REPLAY_API,
|
||||
useExisting: HttpReplayClient,
|
||||
},
|
||||
// Score API
|
||||
HttpScoreClient,
|
||||
{
|
||||
provide: SCORE_API,
|
||||
useExisting: HttpScoreClient,
|
||||
},
|
||||
// AOC API (Attestor + Sources backend via gateway)
|
||||
{
|
||||
provide: AOC_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/api/v1/attestor', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/v1/attestor`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AOC_SOURCES_API_BASE_URL,
|
||||
deps: [AppConfigService],
|
||||
useFactory: (config: AppConfigService) => {
|
||||
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
|
||||
try {
|
||||
return new URL('/api/v1/sources', gatewayBase).toString();
|
||||
} catch {
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/v1/sources`;
|
||||
}
|
||||
},
|
||||
},
|
||||
AocHttpClient,
|
||||
{ provide: AOC_API, useExisting: AocHttpClient },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -239,15 +239,10 @@ export class AbacOverlayHttpClient implements AbacOverlayApi {
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string): HttpHeaders {
|
||||
let headers = new HttpHeaders()
|
||||
const headers = new HttpHeaders()
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('X-Tenant-Id', tenantId);
|
||||
|
||||
const session = this.authStore.session();
|
||||
if (session?.tokens.accessToken) {
|
||||
headers = headers.set('Authorization', `Bearer ${session.tokens.accessToken}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,10 +70,7 @@ export class AdvisoryApiHttpClient implements AdvisoryApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('AdvisoryApiHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, delay } from 'rxjs/operators';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
import { generateTraceId } from './trace.util';
|
||||
import {
|
||||
@@ -216,3 +216,123 @@ export class AnalyticsHttpClient {
|
||||
return error.message || 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MockAnalyticsClient extends AnalyticsHttpClient {
|
||||
override getSuppliers(
|
||||
_limit?: number,
|
||||
_environment?: string | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsSupplierConcentration>> {
|
||||
return of(this.wrap<AnalyticsSupplierConcentration>([
|
||||
{ supplier: 'Apache Foundation', componentCount: 42, artifactCount: 18, teamCount: 3, criticalVulnCount: 2, highVulnCount: 5, environments: ['dev', 'staging', 'prod'] },
|
||||
{ supplier: 'Google', componentCount: 31, artifactCount: 12, teamCount: 2, criticalVulnCount: 0, highVulnCount: 3, environments: ['dev', 'staging', 'prod'] },
|
||||
{ supplier: 'Microsoft', componentCount: 28, artifactCount: 9, teamCount: 4, criticalVulnCount: 1, highVulnCount: 2, environments: ['dev', 'prod'] },
|
||||
{ supplier: 'Red Hat', componentCount: 19, artifactCount: 7, teamCount: 2, criticalVulnCount: 0, highVulnCount: 1, environments: ['prod'] },
|
||||
{ supplier: 'Community OSS', componentCount: 87, artifactCount: 35, teamCount: 5, criticalVulnCount: 4, highVulnCount: 12, environments: ['dev', 'staging', 'prod'] },
|
||||
])).pipe(delay(200));
|
||||
}
|
||||
|
||||
override getLicenses(
|
||||
_environment?: string | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsLicenseDistribution>> {
|
||||
return of(this.wrap<AnalyticsLicenseDistribution>([
|
||||
{ licenseConcluded: 'Apache-2.0', licenseCategory: 'permissive', componentCount: 89, artifactCount: 34, ecosystems: ['maven', 'npm'] },
|
||||
{ licenseConcluded: 'MIT', licenseCategory: 'permissive', componentCount: 112, artifactCount: 45, ecosystems: ['npm', 'nuget'] },
|
||||
{ licenseConcluded: 'BSD-3-Clause', licenseCategory: 'permissive', componentCount: 23, artifactCount: 11, ecosystems: ['pip', 'npm'] },
|
||||
{ licenseConcluded: 'GPL-2.0-only', licenseCategory: 'copyleft', componentCount: 5, artifactCount: 2, ecosystems: ['maven'] },
|
||||
{ licenseConcluded: null, licenseCategory: 'unknown', componentCount: 8, artifactCount: 3, ecosystems: ['npm'] },
|
||||
])).pipe(delay(200));
|
||||
}
|
||||
|
||||
override getVulnerabilities(
|
||||
_environment?: string | null,
|
||||
_minSeverity?: string | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsVulnerabilityExposure>> {
|
||||
return of(this.wrap<AnalyticsVulnerabilityExposure>([
|
||||
{ vulnId: 'CVE-2025-1234', severity: 'CRITICAL', cvssScore: 9.8, epssScore: 0.87, kevListed: true, fixAvailable: true, rawComponentCount: 5, rawArtifactCount: 3, effectiveComponentCount: 3, effectiveArtifactCount: 2, vexMitigated: 2 },
|
||||
{ vulnId: 'CVE-2025-5678', severity: 'HIGH', cvssScore: 7.5, epssScore: 0.45, kevListed: false, fixAvailable: true, rawComponentCount: 12, rawArtifactCount: 8, effectiveComponentCount: 8, effectiveArtifactCount: 5, vexMitigated: 4 },
|
||||
{ vulnId: 'CVE-2025-9012', severity: 'HIGH', cvssScore: 7.2, epssScore: 0.32, kevListed: false, fixAvailable: false, rawComponentCount: 3, rawArtifactCount: 2, effectiveComponentCount: 3, effectiveArtifactCount: 2, vexMitigated: 0 },
|
||||
{ vulnId: 'CVE-2025-3456', severity: 'MEDIUM', cvssScore: 5.3, epssScore: 0.12, kevListed: false, fixAvailable: true, rawComponentCount: 7, rawArtifactCount: 4, effectiveComponentCount: 5, effectiveArtifactCount: 3, vexMitigated: 2 },
|
||||
])).pipe(delay(200));
|
||||
}
|
||||
|
||||
override getFixableBacklog(
|
||||
_environment?: string | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsFixableBacklogItem>> {
|
||||
return of(this.wrap<AnalyticsFixableBacklogItem>([
|
||||
{ service: 'api-gateway', environment: 'prod', component: 'lodash', version: '4.17.20', vulnId: 'CVE-2025-1234', severity: 'CRITICAL', fixedVersion: '4.17.21' },
|
||||
{ service: 'auth-service', environment: 'prod', component: 'jackson-databind', version: '2.14.0', vulnId: 'CVE-2025-5678', severity: 'HIGH', fixedVersion: '2.14.3' },
|
||||
{ service: 'scanner-worker', environment: 'staging', component: 'express', version: '4.18.1', vulnId: 'CVE-2025-3456', severity: 'MEDIUM', fixedVersion: '4.18.3' },
|
||||
])).pipe(delay(200));
|
||||
}
|
||||
|
||||
override getAttestationCoverage(
|
||||
_environment?: string | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsAttestationCoverage>> {
|
||||
return of(this.wrap<AnalyticsAttestationCoverage>([
|
||||
{ environment: 'prod', team: 'Platform', totalArtifacts: 24, withProvenance: 22, provenancePct: 91.7, slsaLevel2Plus: 18, slsa2Pct: 75.0, missingProvenance: 2 },
|
||||
{ environment: 'staging', team: 'Platform', totalArtifacts: 24, withProvenance: 20, provenancePct: 83.3, slsaLevel2Plus: 15, slsa2Pct: 62.5, missingProvenance: 4 },
|
||||
{ environment: 'dev', team: 'Platform', totalArtifacts: 30, withProvenance: 12, provenancePct: 40.0, slsaLevel2Plus: 8, slsa2Pct: 26.7, missingProvenance: 18 },
|
||||
])).pipe(delay(200));
|
||||
}
|
||||
|
||||
override getVulnerabilityTrends(
|
||||
_environment?: string | null,
|
||||
_days?: number | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsVulnerabilityTrendPoint>> {
|
||||
const now = new Date();
|
||||
const points: AnalyticsVulnerabilityTrendPoint[] = [];
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i);
|
||||
points.push({
|
||||
snapshotDate: d.toISOString().split('T')[0],
|
||||
environment: 'prod',
|
||||
totalVulns: 45 - Math.floor(i * 0.3) + Math.floor(Math.random() * 3),
|
||||
fixableVulns: 20 - Math.floor(i * 0.2),
|
||||
vexMitigated: 8 + Math.floor(i * 0.1),
|
||||
netExposure: 17 - Math.floor(i * 0.1),
|
||||
kevVulns: 2,
|
||||
});
|
||||
}
|
||||
return of(this.wrap(points)).pipe(delay(200));
|
||||
}
|
||||
|
||||
override getComponentTrends(
|
||||
_environment?: string | null,
|
||||
_days?: number | null,
|
||||
_options: AnalyticsRequestOptions = {}
|
||||
): Observable<PlatformListResponse<AnalyticsComponentTrendPoint>> {
|
||||
const now = new Date();
|
||||
const points: AnalyticsComponentTrendPoint[] = [];
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i);
|
||||
points.push({
|
||||
snapshotDate: d.toISOString().split('T')[0],
|
||||
environment: 'prod',
|
||||
totalComponents: 207 + Math.floor(i * 0.5),
|
||||
uniqueSuppliers: 18,
|
||||
});
|
||||
}
|
||||
return of(this.wrap(points)).pipe(delay(200));
|
||||
}
|
||||
|
||||
private wrap<T>(items: T[]): PlatformListResponse<T> {
|
||||
return {
|
||||
tenantId: 'default',
|
||||
actorId: 'mock',
|
||||
dataAsOf: new Date().toISOString(),
|
||||
cached: false,
|
||||
cacheTtlSeconds: 0,
|
||||
items,
|
||||
count: items.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable, InjectionToken } from '@angular/core';
|
||||
import { Observable, of, delay } from 'rxjs';
|
||||
import { Observable, of, delay, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { AppConfigService } from '../config/app-config.service';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
import { generateTraceId } from './trace.util';
|
||||
import {
|
||||
AocMetrics,
|
||||
AocVerificationRequest,
|
||||
@@ -39,6 +42,128 @@ export interface AocApi {
|
||||
*/
|
||||
export const AOC_API = new InjectionToken<AocApi>('AOC_API');
|
||||
|
||||
/**
|
||||
* Base URL injection token for the AOC attestor backend.
|
||||
* Defaults to '/api/v1/attestor' when not provided (gateway-relative).
|
||||
*/
|
||||
export const AOC_API_BASE_URL = new InjectionToken<string>('AOC_API_BASE_URL');
|
||||
|
||||
/**
|
||||
* Base URL injection token for the AOC sources backend (SBOM service).
|
||||
* Defaults to '/api/v1/sources' when not provided (gateway-relative).
|
||||
*/
|
||||
export const AOC_SOURCES_API_BASE_URL = new InjectionToken<string>('AOC_SOURCES_API_BASE_URL');
|
||||
|
||||
// ============================================================================
|
||||
// HTTP Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* HTTP implementation of the AocApi interface.
|
||||
*
|
||||
* Routes through the gateway:
|
||||
* /api/v1/attestor -> attestor.stella-ops.local (dashboard, verification)
|
||||
* /api/v1/attestations -> attestor.stella-ops.local (attestation queries)
|
||||
* /api/v1/sources -> sbomservice.stella-ops.local (source/violation data)
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AocHttpClient implements AocApi {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly authSession = inject(AuthSessionStore);
|
||||
|
||||
private readonly attestorBaseUrl: string;
|
||||
private readonly attestationsBaseUrl: string;
|
||||
private readonly sourcesBaseUrl: string;
|
||||
|
||||
constructor() {
|
||||
const attestorRaw = inject(AOC_API_BASE_URL, { optional: true }) ?? '/api/v1/attestor';
|
||||
this.attestorBaseUrl = this.normalizeUrl(attestorRaw);
|
||||
this.attestationsBaseUrl = this.normalizeUrl(
|
||||
attestorRaw.replace(/\/attestor\/?$/, '/attestations')
|
||||
);
|
||||
this.sourcesBaseUrl = this.normalizeUrl(
|
||||
inject(AOC_SOURCES_API_BASE_URL, { optional: true }) ?? '/api/v1/sources'
|
||||
);
|
||||
}
|
||||
|
||||
getDashboardSummary(): Observable<AocDashboardSummary> {
|
||||
const traceId = generateTraceId();
|
||||
const headers = this.buildHeaders(traceId);
|
||||
|
||||
return this.http.get<AocDashboardSummary>(
|
||||
`${this.attestorBaseUrl}/dashboard/summary`,
|
||||
{ headers }
|
||||
).pipe(
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
|
||||
startVerification(): Observable<AocVerificationRequest> {
|
||||
const traceId = generateTraceId();
|
||||
const headers = this.buildHeaders(traceId);
|
||||
|
||||
return this.http.post<AocVerificationRequest>(
|
||||
`${this.attestorBaseUrl}/verifications`,
|
||||
{},
|
||||
{ headers }
|
||||
).pipe(
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
|
||||
getVerificationStatus(requestId: string): Observable<AocVerificationRequest> {
|
||||
const traceId = generateTraceId();
|
||||
const headers = this.buildHeaders(traceId);
|
||||
|
||||
return this.http.get<AocVerificationRequest>(
|
||||
`${this.attestorBaseUrl}/verifications/${encodeURIComponent(requestId)}`,
|
||||
{ headers }
|
||||
).pipe(
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
|
||||
getViolationsByCode(code: string): Observable<ViolationDetail[]> {
|
||||
const traceId = generateTraceId();
|
||||
const headers = this.buildHeaders(traceId);
|
||||
const params = new HttpParams().set('code', code);
|
||||
|
||||
return this.http.get<ViolationDetail[]>(
|
||||
`${this.attestationsBaseUrl}/violations`,
|
||||
{ headers, params }
|
||||
).pipe(
|
||||
map((res) => Array.isArray(res) ? res : []),
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
|
||||
private buildHeaders(traceId: string): HttpHeaders {
|
||||
const tenantId = this.authSession.getActiveTenantId() || '';
|
||||
return new HttpHeaders({
|
||||
'X-StellaOps-Tenant': tenantId,
|
||||
'X-Stella-Trace-Id': traceId,
|
||||
'X-Stella-Request-Id': traceId,
|
||||
Accept: 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
if (err && typeof err === 'object' && 'status' in err && 'message' in err) {
|
||||
return new Error(
|
||||
`[${traceId}] AOC API error: ${(err as any).status} ${(err as any).statusText ?? (err as any).message}`
|
||||
);
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
return new Error(`[${traceId}] AOC API error: ${err.message}`);
|
||||
}
|
||||
return new Error(`[${traceId}] AOC API error: Unknown error`);
|
||||
}
|
||||
|
||||
private normalizeUrl(url: string): string {
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AocClient {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
@@ -290,7 +290,7 @@ export class AttestationChainMockClient implements AttestationChainApi {
|
||||
subjectDigest: string,
|
||||
options?: AttestationQueryOptions
|
||||
): Observable<AttestationNode[]> {
|
||||
return of(this.mockChain.nodes);
|
||||
return of([...this.mockChain.nodes]);
|
||||
}
|
||||
|
||||
getRekorEntry(uuid: string): Observable<RekorLogEntry> {
|
||||
|
||||
@@ -35,10 +35,6 @@ export class AuditBundlesHttpClient implements AuditBundlesApi {
|
||||
const tenant = this.resolveTenant();
|
||||
const traceId = generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('audit', 'read', ['audit:read'], this.tenantService.activeProjectId() ?? undefined, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing audit:read scope'));
|
||||
}
|
||||
|
||||
const headers = this.buildHeaders(tenant, traceId);
|
||||
return this.http.get<AuditBundleListResponse>(`${this.baseUrl}/v1/audit-bundles`, { headers }).pipe(
|
||||
map((resp) => ({ ...resp, traceId })),
|
||||
@@ -50,10 +46,6 @@ export class AuditBundlesHttpClient implements AuditBundlesApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('audit', 'write', ['audit:write'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing audit:write scope'));
|
||||
}
|
||||
|
||||
const headers = this.buildHeaders(tenant, traceId, options.projectId);
|
||||
return this.http.post<AuditBundleJobResponse>(`${this.baseUrl}/v1/audit-bundles`, request, { headers }).pipe(
|
||||
map((resp) => ({ ...resp, traceId })),
|
||||
@@ -65,10 +57,6 @@ export class AuditBundlesHttpClient implements AuditBundlesApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('audit', 'read', ['audit:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing audit:read scope'));
|
||||
}
|
||||
|
||||
const headers = this.buildHeaders(tenant, traceId, options.projectId);
|
||||
return this.http.get<AuditBundleJobResponse>(`${this.baseUrl}/v1/audit-bundles/${encodeURIComponent(bundleId)}`, { headers }).pipe(
|
||||
map((resp) => ({ ...resp, traceId })),
|
||||
@@ -80,10 +68,6 @@ export class AuditBundlesHttpClient implements AuditBundlesApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('audit', 'read', ['audit:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing audit:read scope'));
|
||||
}
|
||||
|
||||
const headers = this.buildHeaders(tenant, traceId, options.projectId).set('Accept', 'application/octet-stream');
|
||||
return this.http.get(`${this.baseUrl}/v1/audit-bundles/${encodeURIComponent(bundleId)}`, {
|
||||
headers,
|
||||
@@ -100,18 +84,11 @@ export class AuditBundlesHttpClient implements AuditBundlesApi {
|
||||
|
||||
if (projectId) headers = headers.set('X-Stella-Project', projectId);
|
||||
|
||||
const session = this.authSession.session();
|
||||
if (session?.tokens.accessToken) {
|
||||
headers = headers.set('Authorization', `Bearer ${session.tokens.accessToken}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = tenantId ?? this.tenantService.activeTenantId();
|
||||
if (!tenant) throw new Error('AuditBundlesHttpClient requires an active tenant identifier.');
|
||||
return tenant;
|
||||
return tenantId ?? this.tenantService.activeTenantId() ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,12 +65,20 @@ export interface AdminTenant {
|
||||
// API Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface AuthorityAdminApi {
|
||||
listUsers(tenantId?: string): Observable<AdminUser[]>;
|
||||
listRoles(tenantId?: string): Observable<AdminRole[]>;
|
||||
listClients(tenantId?: string): Observable<AdminClient[]>;
|
||||
listTokens(tenantId?: string): Observable<AdminToken[]>;
|
||||
listTenants(): Observable<AdminTenant[]>;
|
||||
createUser(request: CreateUserRequest): Observable<AdminUser>;
|
||||
}
|
||||
|
||||
export const AUTHORITY_ADMIN_API = new InjectionToken<AuthorityAdminApi>('AUTHORITY_ADMIN_API');
|
||||
@@ -118,6 +126,12 @@ export class AuthorityAdminHttpClient implements AuthorityAdminApi {
|
||||
}).pipe(map(r => r.tenants ?? []));
|
||||
}
|
||||
|
||||
createUser(request: CreateUserRequest): Observable<AdminUser> {
|
||||
return this.http.post<AdminUser>(`${this.baseUrl}/users`, request, {
|
||||
headers: this.buildHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
private buildHeaders(tenantOverride?: string): HttpHeaders {
|
||||
const tenantId =
|
||||
(tenantOverride && tenantOverride.trim()) ||
|
||||
@@ -182,4 +196,17 @@ export class MockAuthorityAdminClient implements AuthorityAdminApi {
|
||||
];
|
||||
return of(data).pipe(delay(300));
|
||||
}
|
||||
|
||||
createUser(request: CreateUserRequest): Observable<AdminUser> {
|
||||
const user: AdminUser = {
|
||||
id: 'u-' + Date.now(),
|
||||
username: request.username,
|
||||
email: request.email,
|
||||
displayName: request.displayName,
|
||||
roles: request.roles,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
return of(user).pipe(delay(400));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@ export class ConsoleExportClient {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ConsoleExportClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +246,7 @@ export class ConsoleSearchHttpClient implements ConsoleSearchApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ConsoleSearchClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
@@ -414,7 +411,7 @@ export class MockConsoleSearchClient implements ConsoleSearchApi {
|
||||
// Sort items deterministically: type asc, id asc, format asc
|
||||
const items: DownloadManifestItem[] = [
|
||||
{
|
||||
type: 'advisory',
|
||||
type: 'advisory' as const,
|
||||
id: 'CVE-2024-12345',
|
||||
format: 'json',
|
||||
url: `https://downloads.local/exports/${exportId}/advisory/CVE-2024-12345.json?sig=mock`,
|
||||
@@ -422,7 +419,7 @@ export class MockConsoleSearchClient implements ConsoleSearchApi {
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
type: 'advisory',
|
||||
type: 'advisory' as const,
|
||||
id: 'CVE-2024-67890',
|
||||
format: 'json',
|
||||
url: `https://downloads.local/exports/${exportId}/advisory/CVE-2024-67890.json?sig=mock`,
|
||||
@@ -430,7 +427,7 @@ export class MockConsoleSearchClient implements ConsoleSearchApi {
|
||||
size: 3072,
|
||||
},
|
||||
{
|
||||
type: 'vex',
|
||||
type: 'vex' as const,
|
||||
id: 'vex:tenant-default:jwt-auth:5d1a',
|
||||
format: 'json',
|
||||
url: `https://downloads.local/exports/${exportId}/vex/jwt-auth-5d1a.json?sig=mock`,
|
||||
@@ -438,7 +435,7 @@ export class MockConsoleSearchClient implements ConsoleSearchApi {
|
||||
size: 2048,
|
||||
},
|
||||
{
|
||||
type: 'vuln',
|
||||
type: 'vuln' as const,
|
||||
id: 'tenant-default:advisory-ai:sha256:5d1a',
|
||||
format: 'json',
|
||||
url: `https://downloads.local/exports/${exportId}/vuln/5d1a.json?sig=mock`,
|
||||
@@ -480,6 +477,6 @@ export class MockConsoleSearchClient implements ConsoleSearchApi {
|
||||
tenant: tenantId,
|
||||
};
|
||||
// In production, this would be signed and base64url encoded
|
||||
return Buffer.from(JSON.stringify(cursorData)).toString('base64url');
|
||||
return btoa(JSON.stringify(cursorData));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,6 @@ export class ConsoleStatusClient {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ConsoleStatusClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,10 +211,7 @@ export class ConsoleVexHttpClient implements ConsoleVexApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ConsoleVexClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
|
||||
@@ -182,10 +182,7 @@ export class ConsoleVulnHttpClient implements ConsoleVulnApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ConsoleVulnClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
|
||||
@@ -108,10 +108,6 @@ export class CvssClient {
|
||||
}
|
||||
|
||||
private resolveTenant(): string {
|
||||
const tenant = this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('CvssClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return this.authSession.getActiveTenantId() ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import { Observable, of, delay } from 'rxjs';
|
||||
import { Injectable, InjectionToken, Inject } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Observable, of, delay, firstValueFrom, catchError, throwError } from 'rxjs';
|
||||
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
|
||||
import {
|
||||
EvidenceData,
|
||||
@@ -22,6 +25,110 @@ export interface EvidenceApi {
|
||||
}
|
||||
|
||||
export const EVIDENCE_API = new InjectionToken<EvidenceApi>('EVIDENCE_API');
|
||||
export const EVIDENCE_API_BASE_URL = new InjectionToken<string>('EVIDENCE_API_BASE_URL');
|
||||
|
||||
// ============================================================================
|
||||
// HTTP Implementation
|
||||
// ============================================================================
|
||||
|
||||
@Injectable()
|
||||
export class EvidenceHttpClient implements EvidenceApi {
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
@Inject(EVIDENCE_API_BASE_URL) private readonly baseUrl: string,
|
||||
private readonly authSession: AuthSessionStore,
|
||||
) {}
|
||||
|
||||
getEvidenceForAdvisory(advisoryId: string): Observable<EvidenceData> {
|
||||
return this.http
|
||||
.get<EvidenceData>(
|
||||
`${this.baseUrl}/api/v1/evidence/advisories/${encodeURIComponent(advisoryId)}`,
|
||||
{ headers: this.buildHeaders() },
|
||||
)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
getObservation(observationId: string): Observable<Observation> {
|
||||
return this.http
|
||||
.get<Observation>(
|
||||
`${this.baseUrl}/api/v1/evidence/observations/${encodeURIComponent(observationId)}`,
|
||||
{ headers: this.buildHeaders() },
|
||||
)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
getLinkset(linksetId: string): Observable<Linkset> {
|
||||
return this.http
|
||||
.get<Linkset>(
|
||||
`${this.baseUrl}/api/v1/evidence/linksets/${encodeURIComponent(linksetId)}`,
|
||||
{ headers: this.buildHeaders() },
|
||||
)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
getPolicyEvidence(advisoryId: string): Observable<PolicyEvidence | null> {
|
||||
return this.http
|
||||
.get<PolicyEvidence | null>(
|
||||
`${this.baseUrl}/api/v1/evidence/advisories/${encodeURIComponent(advisoryId)}/policy`,
|
||||
{ headers: this.buildHeaders() },
|
||||
)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
downloadRawDocument(type: 'observation' | 'linkset', id: string): Observable<Blob> {
|
||||
const segment = type === 'observation' ? 'observations' : 'linksets';
|
||||
return this.http
|
||||
.get(
|
||||
`${this.baseUrl}/api/v1/evidence/${segment}/${encodeURIComponent(id)}/raw`,
|
||||
{
|
||||
headers: this.buildHeaders().set('Accept', 'application/json'),
|
||||
responseType: 'blob',
|
||||
},
|
||||
)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
async exportEvidenceBundle(advisoryId: string, format: 'tar.gz' | 'zip'): Promise<Blob> {
|
||||
const mimeType = format === 'tar.gz' ? 'application/gzip' : 'application/zip';
|
||||
const params = new HttpParams().set('format', format);
|
||||
|
||||
return firstValueFrom(
|
||||
this.http
|
||||
.get(
|
||||
`${this.baseUrl}/api/v1/evidence/advisories/${encodeURIComponent(advisoryId)}/export`,
|
||||
{
|
||||
headers: this.buildHeaders().set('Accept', mimeType),
|
||||
params,
|
||||
responseType: 'blob',
|
||||
},
|
||||
)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err)))),
|
||||
);
|
||||
}
|
||||
|
||||
private buildHeaders(): HttpHeaders {
|
||||
const tenantId = this.authSession.getActiveTenantId();
|
||||
const headers: Record<string, string> = {};
|
||||
if (tenantId) {
|
||||
headers['X-StellaOps-Tenant'] = tenantId;
|
||||
}
|
||||
return new HttpHeaders(headers);
|
||||
}
|
||||
|
||||
private normalizeError(err: unknown): Error {
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
return new Error(
|
||||
`Evidence API request failed: ${err.status} ${err.statusText ?? 'Unknown'}`,
|
||||
);
|
||||
}
|
||||
if (err instanceof Error) return err;
|
||||
return new Error('Evidence API request failed');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mock Implementation
|
||||
// ============================================================================
|
||||
|
||||
// Mock data for development
|
||||
const MOCK_OBSERVATIONS: Observation[] = [
|
||||
|
||||
@@ -66,10 +66,7 @@ export class ExceptionEventsHttpClient implements ExceptionEventsApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ExceptionEventsHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(options?.tenantId);
|
||||
const traceId = options?.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'read', ['exception:read'], options?.projectId, traceId)) {
|
||||
if (!this.tenantService.authorize('exception', 'read', ['exceptions:read'], options?.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing exception:read scope'));
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'read', ['exception:read'], options.projectId, traceId)) {
|
||||
if (!this.tenantService.authorize('exception', 'read', ['exceptions:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing exception:read scope'));
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'write', ['exception:write'], options.projectId, traceId)) {
|
||||
if (!this.tenantService.authorize('exception', 'write', ['exceptions:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing exception:write scope'));
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'write', ['exception:write'], options.projectId, traceId)) {
|
||||
if (!this.tenantService.authorize('exception', 'write', ['exceptions:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing exception:write scope'));
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'write', ['exception:write'], options.projectId, traceId)) {
|
||||
if (!this.tenantService.authorize('exception', 'write', ['exceptions:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing exception:write scope'));
|
||||
}
|
||||
|
||||
@@ -133,10 +133,10 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(transition.tenantId);
|
||||
const traceId = transition.traceId ?? generateTraceId();
|
||||
|
||||
const requiredScopes: ('exception:write' | 'exception:approve')[] =
|
||||
const requiredScopes: ('exceptions:read' | 'exceptions:approve')[] =
|
||||
transition.newStatus === 'approved' || transition.newStatus === 'rejected' || transition.newStatus === 'revoked'
|
||||
? ['exception:approve']
|
||||
: ['exception:write'];
|
||||
? ['exceptions:approve']
|
||||
: ['exceptions:read'];
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'transition', requiredScopes, transition.projectId, traceId)) {
|
||||
return throwError(() => new Error(`Unauthorized: missing ${requiredScopes.join(' or ')} scope`));
|
||||
@@ -158,7 +158,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
|
||||
if (!this.tenantService.authorize('exception', 'read', ['exception:read'], options.projectId, traceId)) {
|
||||
if (!this.tenantService.authorize('exception', 'read', ['exceptions:read'], options.projectId, traceId)) {
|
||||
return throwError(() => new Error('Unauthorized: missing exception:read scope'));
|
||||
}
|
||||
|
||||
@@ -168,11 +168,7 @@ export class ExceptionApiHttpClient implements ExceptionApi {
|
||||
}
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ExceptionApiHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId() || 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders {
|
||||
|
||||
@@ -205,10 +205,7 @@ export class ExportCenterHttpClient implements ExportCenterApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('ExportCenterClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Inject, Injectable, InjectionToken } from '@angular/core';
|
||||
import { Observable, of, delay, throwError } from 'rxjs';
|
||||
import {
|
||||
FeedMirror,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
* Injection token for Feed Mirror API client.
|
||||
*/
|
||||
export const FEED_MIRROR_API = new InjectionToken<FeedMirrorApi>('FEED_MIRROR_API');
|
||||
export const FEED_MIRROR_API_BASE_URL = new InjectionToken<string>('FEED_MIRROR_API_BASE_URL');
|
||||
|
||||
/**
|
||||
* Feed Mirror API interface.
|
||||
@@ -333,6 +335,192 @@ const mockOfflineSyncStatus: OfflineSyncStatus = {
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HTTP API Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* HTTP Feed Mirror client.
|
||||
* Communicates with the Concelier backend via the gateway at /api/v1/concelier.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FeedMirrorHttpClient implements FeedMirrorApi {
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
@Inject(FEED_MIRROR_API_BASE_URL) private readonly baseUrl: string
|
||||
) {}
|
||||
|
||||
// ---- Mirror operations ----
|
||||
|
||||
listMirrors(filter?: FeedMirrorFilter): Observable<readonly FeedMirror[]> {
|
||||
let params = new HttpParams();
|
||||
if (filter?.feedTypes?.length) {
|
||||
params = params.set('feedTypes', filter.feedTypes.join(','));
|
||||
}
|
||||
if (filter?.syncStatuses?.length) {
|
||||
params = params.set('syncStatuses', filter.syncStatuses.join(','));
|
||||
}
|
||||
if (filter?.enabled !== undefined) {
|
||||
params = params.set('enabled', String(filter.enabled));
|
||||
}
|
||||
if (filter?.searchTerm) {
|
||||
params = params.set('search', filter.searchTerm);
|
||||
}
|
||||
return this.http.get<readonly FeedMirror[]>(`${this.baseUrl}/mirrors`, { params });
|
||||
}
|
||||
|
||||
getMirror(mirrorId: string): Observable<FeedMirror> {
|
||||
return this.http.get<FeedMirror>(
|
||||
`${this.baseUrl}/mirrors/${encodeURIComponent(mirrorId)}`
|
||||
);
|
||||
}
|
||||
|
||||
updateMirrorConfig(mirrorId: string, config: MirrorConfigUpdate): Observable<FeedMirror> {
|
||||
return this.http.patch<FeedMirror>(
|
||||
`${this.baseUrl}/mirrors/${encodeURIComponent(mirrorId)}`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
triggerSync(request: MirrorSyncRequest): Observable<MirrorSyncResult> {
|
||||
return this.http.post<MirrorSyncResult>(
|
||||
`${this.baseUrl}/mirrors/${encodeURIComponent(request.mirrorId)}/sync`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Snapshot operations ----
|
||||
|
||||
listSnapshots(mirrorId: string): Observable<readonly FeedSnapshot[]> {
|
||||
return this.http.get<readonly FeedSnapshot[]>(
|
||||
`${this.baseUrl}/mirrors/${encodeURIComponent(mirrorId)}/snapshots`
|
||||
);
|
||||
}
|
||||
|
||||
getSnapshot(snapshotId: string): Observable<FeedSnapshot> {
|
||||
return this.http.get<FeedSnapshot>(
|
||||
`${this.baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`
|
||||
);
|
||||
}
|
||||
|
||||
downloadSnapshot(snapshotId: string): Observable<SnapshotDownloadProgress> {
|
||||
return this.http.post<SnapshotDownloadProgress>(
|
||||
`${this.baseUrl}/snapshots/${encodeURIComponent(snapshotId)}/download`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
pinSnapshot(snapshotId: string, pinned: boolean): Observable<FeedSnapshot> {
|
||||
return this.http.patch<FeedSnapshot>(
|
||||
`${this.baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`,
|
||||
{ isPinned: pinned }
|
||||
);
|
||||
}
|
||||
|
||||
deleteSnapshot(snapshotId: string): Observable<void> {
|
||||
return this.http.delete<void>(
|
||||
`${this.baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`
|
||||
);
|
||||
}
|
||||
|
||||
updateRetentionConfig(config: SnapshotRetentionConfig): Observable<SnapshotRetentionConfig> {
|
||||
return this.http.put<SnapshotRetentionConfig>(
|
||||
`${this.baseUrl}/mirrors/${encodeURIComponent(config.mirrorId)}/retention`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
getRetentionConfig(mirrorId: string): Observable<SnapshotRetentionConfig> {
|
||||
return this.http.get<SnapshotRetentionConfig>(
|
||||
`${this.baseUrl}/mirrors/${encodeURIComponent(mirrorId)}/retention`
|
||||
);
|
||||
}
|
||||
|
||||
// ---- AirGap bundle operations ----
|
||||
|
||||
listBundles(): Observable<readonly AirGapBundle[]> {
|
||||
return this.http.get<readonly AirGapBundle[]>(`${this.baseUrl}/bundles`);
|
||||
}
|
||||
|
||||
getBundle(bundleId: string): Observable<AirGapBundle> {
|
||||
return this.http.get<AirGapBundle>(
|
||||
`${this.baseUrl}/bundles/${encodeURIComponent(bundleId)}`
|
||||
);
|
||||
}
|
||||
|
||||
createBundle(request: AirGapBundleRequest): Observable<AirGapBundle> {
|
||||
return this.http.post<AirGapBundle>(`${this.baseUrl}/bundles`, request);
|
||||
}
|
||||
|
||||
deleteBundle(bundleId: string): Observable<void> {
|
||||
return this.http.delete<void>(
|
||||
`${this.baseUrl}/bundles/${encodeURIComponent(bundleId)}`
|
||||
);
|
||||
}
|
||||
|
||||
downloadBundle(bundleId: string): Observable<SnapshotDownloadProgress> {
|
||||
return this.http.post<SnapshotDownloadProgress>(
|
||||
`${this.baseUrl}/bundles/${encodeURIComponent(bundleId)}/download`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// ---- AirGap import operations ----
|
||||
|
||||
validateImport(file: File): Observable<AirGapImportValidation> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
return this.http.post<AirGapImportValidation>(
|
||||
`${this.baseUrl}/imports/validate`,
|
||||
formData
|
||||
);
|
||||
}
|
||||
|
||||
startImport(bundleId: string): Observable<AirGapImportProgress> {
|
||||
return this.http.post<AirGapImportProgress>(
|
||||
`${this.baseUrl}/imports`,
|
||||
{ bundleId }
|
||||
);
|
||||
}
|
||||
|
||||
getImportProgress(importId: string): Observable<AirGapImportProgress> {
|
||||
return this.http.get<AirGapImportProgress>(
|
||||
`${this.baseUrl}/imports/${encodeURIComponent(importId)}`
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Version lock operations ----
|
||||
|
||||
listVersionLocks(): Observable<readonly FeedVersionLock[]> {
|
||||
return this.http.get<readonly FeedVersionLock[]>(`${this.baseUrl}/version-locks`);
|
||||
}
|
||||
|
||||
getVersionLock(feedType: FeedType): Observable<FeedVersionLock | null> {
|
||||
return this.http.get<FeedVersionLock | null>(
|
||||
`${this.baseUrl}/version-locks/${encodeURIComponent(feedType)}`
|
||||
);
|
||||
}
|
||||
|
||||
setVersionLock(request: FeedVersionLockRequest): Observable<FeedVersionLock> {
|
||||
return this.http.put<FeedVersionLock>(
|
||||
`${this.baseUrl}/version-locks/${encodeURIComponent(request.feedType)}`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
removeVersionLock(lockId: string): Observable<void> {
|
||||
return this.http.delete<void>(
|
||||
`${this.baseUrl}/version-locks/${encodeURIComponent(lockId)}`
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Offline status ----
|
||||
|
||||
getOfflineSyncStatus(): Observable<OfflineSyncStatus> {
|
||||
return this.http.get<OfflineSyncStatus>(`${this.baseUrl}/offline-status`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mock API Implementation
|
||||
// ============================================================================
|
||||
|
||||
@@ -329,11 +329,6 @@ export class FindingsLedgerHttpClient implements FindingsLedgerApi {
|
||||
if (projectId) headers = headers.set('X-Stella-Project', projectId);
|
||||
if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId);
|
||||
|
||||
const session = this.authStore.session();
|
||||
if (session?.tokens.accessToken) {
|
||||
headers = headers.set('Authorization', `Bearer ${session.tokens.accessToken}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -341,10 +336,7 @@ export class FindingsLedgerHttpClient implements FindingsLedgerApi {
|
||||
const tenant = tenantId?.trim() ||
|
||||
this.tenantService.activeTenantId() ||
|
||||
this.authStore.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('FindingsLedgerHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private generateCorrelationId(): string {
|
||||
|
||||
@@ -104,10 +104,7 @@ export class FirstSignalHttpClient implements FirstSignalApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('FirstSignalHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders {
|
||||
|
||||
@@ -260,10 +260,7 @@ export class GraphPlatformHttpClient implements GraphPlatformApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('GraphPlatformClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
|
||||
@@ -352,10 +352,7 @@ export class OrchestratorControlHttpClient implements OrchestratorControlApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('OrchestratorControlHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(
|
||||
|
||||
@@ -68,10 +68,7 @@ export class OrchestratorHttpClient implements OrchestratorApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('OrchestratorHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders {
|
||||
|
||||
@@ -234,6 +234,7 @@ export function formatLatency(ms: number): string {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
export function formatErrorRate(rate: number): string {
|
||||
export function formatErrorRate(rate: number | null | undefined): string {
|
||||
if (rate == null) return '0.00%';
|
||||
return `${rate.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
@@ -74,10 +74,7 @@ export class PolicyExceptionsHttpClient implements PolicyExceptionsApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('PolicyExceptionsHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import { Inject, Injectable, InjectionToken } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, of, delay } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
import { TenantActivationService } from '../auth/tenant-activation.service';
|
||||
import { generateTraceId } from './trace.util';
|
||||
import {
|
||||
PolicyProfile,
|
||||
PolicyProfileType,
|
||||
@@ -22,6 +27,7 @@ import {
|
||||
* Injection token for Policy Gates API client.
|
||||
*/
|
||||
export const POLICY_GATES_API = new InjectionToken<PolicyGatesApi>('POLICY_GATES_API');
|
||||
export const POLICY_GATES_API_BASE_URL = new InjectionToken<string>('POLICY_GATES_API_BASE_URL');
|
||||
|
||||
/**
|
||||
* Policy Gates API interface.
|
||||
@@ -438,6 +444,186 @@ const mockBundleSimulation: BundleSimulationResult = {
|
||||
durationMs: 320,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Implementation
|
||||
// =============================================================================
|
||||
|
||||
@Injectable()
|
||||
export class PolicyGatesHttpClient implements PolicyGatesApi {
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
private readonly authSession: AuthSessionStore,
|
||||
private readonly tenantService: TenantActivationService,
|
||||
@Inject(POLICY_GATES_API_BASE_URL) private readonly baseUrl: string
|
||||
) {}
|
||||
|
||||
// ---- Profile management ----
|
||||
|
||||
listProfiles(includeBuiltin = true): Observable<readonly PolicyProfile[]> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
const params: Record<string, string> = {};
|
||||
if (!includeBuiltin) {
|
||||
params['includeBuiltin'] = 'false';
|
||||
}
|
||||
return this.http.get<PolicyProfile[]>(`${this.baseUrl}/gate/profiles`, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
params,
|
||||
}).pipe(
|
||||
catchError(() => of([] as PolicyProfile[]))
|
||||
);
|
||||
}
|
||||
|
||||
getProfile(profileId: string): Observable<PolicyProfile> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.get<PolicyProfile>(`${this.baseUrl}/gate/profiles/${encodeURIComponent(profileId)}`, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
});
|
||||
}
|
||||
|
||||
getProfileByName(name: string): Observable<PolicyProfile | null> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.get<PolicyProfile>(`${this.baseUrl}/gate/profiles/by-name/${encodeURIComponent(name)}`, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
}).pipe(
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
createProfile(request: CreatePolicyProfileRequest): Observable<PolicyProfile> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.post<PolicyProfile>(`${this.baseUrl}/gate/profiles`, request, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
});
|
||||
}
|
||||
|
||||
updateProfile(profileId: string, request: UpdatePolicyProfileRequest): Observable<PolicyProfile> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.patch<PolicyProfile>(
|
||||
`${this.baseUrl}/gate/profiles/${encodeURIComponent(profileId)}`,
|
||||
request,
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
);
|
||||
}
|
||||
|
||||
deleteProfile(profileId: string): Observable<boolean> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.delete<void>(
|
||||
`${this.baseUrl}/gate/profiles/${encodeURIComponent(profileId)}`,
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
).pipe(
|
||||
map(() => true),
|
||||
catchError(() => of(false))
|
||||
);
|
||||
}
|
||||
|
||||
setDefaultProfile(profileId: string): Observable<void> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.post<void>(
|
||||
`${this.baseUrl}/gate/profiles/${encodeURIComponent(profileId)}/set-default`,
|
||||
{},
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
);
|
||||
}
|
||||
|
||||
getEffectiveProfile(): Observable<PolicyProfile> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.get<PolicyProfile>(`${this.baseUrl}/gate/profiles/effective`, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
});
|
||||
}
|
||||
|
||||
validatePolicyYaml(yaml: string): Observable<PolicyValidationResult> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.post<PolicyValidationResult>(
|
||||
`${this.baseUrl}/gate/profiles/validate`,
|
||||
{ yaml },
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Simulation ----
|
||||
|
||||
simulate(request: PolicySimulationRequest): Observable<PolicySimulationResult> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.post<PolicySimulationResult>(
|
||||
`${this.baseUrl}/gate/simulate`,
|
||||
request,
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
);
|
||||
}
|
||||
|
||||
simulateBundle(promotionId: string, profileIdOrName?: string): Observable<BundleSimulationResult> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
const body: Record<string, string> = { promotionId };
|
||||
if (profileIdOrName) {
|
||||
body['profileIdOrName'] = profileIdOrName;
|
||||
}
|
||||
return this.http.post<BundleSimulationResult>(
|
||||
`${this.baseUrl}/gate/simulate/bundle`,
|
||||
body,
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Feed freshness ----
|
||||
|
||||
getFeedFreshnessSummary(): Observable<FeedFreshnessSummary> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.get<FeedFreshnessSummary>(`${this.baseUrl}/gate/feeds/freshness`, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
});
|
||||
}
|
||||
|
||||
getFeedFreshness(feedName: string): Observable<FeedFreshness | null> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.get<FeedFreshness>(
|
||||
`${this.baseUrl}/gate/feeds/freshness/${encodeURIComponent(feedName)}`,
|
||||
{ headers: this.buildHeaders(tenant, traceId) }
|
||||
).pipe(
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Air-gap status ----
|
||||
|
||||
getAirGapStatus(): Observable<AirGapStatus> {
|
||||
const traceId = generateTraceId();
|
||||
const tenant = this.resolveTenant();
|
||||
return this.http.get<AirGapStatus>(`${this.baseUrl}/gate/airgap/status`, {
|
||||
headers: this.buildHeaders(tenant, traceId),
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Private helpers ----
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string): HttpHeaders {
|
||||
return new HttpHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
'X-StellaOps-Tenant': tenantId,
|
||||
'X-Stella-Trace-Id': traceId,
|
||||
'X-Stella-Request-Id': traceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mock API Implementation
|
||||
// =============================================================================
|
||||
|
||||
@@ -427,10 +427,7 @@ export class PolicySimulationHttpClient implements PolicySimulationApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('PolicySimulationHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string): HttpHeaders {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import { Observable, of, delay } from 'rxjs';
|
||||
import { Injectable, InjectionToken, inject } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable, of, delay, catchError, throwError } from 'rxjs';
|
||||
import {
|
||||
Release,
|
||||
ReleaseArtifact,
|
||||
@@ -28,6 +29,64 @@ export interface ReleaseApi {
|
||||
requestBypass(releaseId: string, reason: string): Observable<{ requestId: string }>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTTP Client Implementation
|
||||
// ============================================================================
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ReleaseHttpClient implements ReleaseApi {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly baseUrl = '/api/releases';
|
||||
|
||||
getRelease(releaseId: string): Observable<Release> {
|
||||
return this.http
|
||||
.get<Release>(`${this.baseUrl}/${encodeURIComponent(releaseId)}`)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
listReleases(): Observable<readonly Release[]> {
|
||||
return this.http
|
||||
.get<readonly Release[]>(this.baseUrl)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
publishRelease(releaseId: string): Observable<Release> {
|
||||
return this.http
|
||||
.post<Release>(`${this.baseUrl}/${encodeURIComponent(releaseId)}/publish`, {})
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
cancelRelease(releaseId: string): Observable<Release> {
|
||||
return this.http
|
||||
.post<Release>(`${this.baseUrl}/${encodeURIComponent(releaseId)}/cancel`, {})
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
getFeatureFlags(): Observable<DeterminismFeatureFlags> {
|
||||
return this.http
|
||||
.get<DeterminismFeatureFlags>(`${this.baseUrl}/feature-flags`)
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
requestBypass(releaseId: string, reason: string): Observable<{ requestId: string }> {
|
||||
return this.http
|
||||
.post<{ requestId: string }>(`${this.baseUrl}/${encodeURIComponent(releaseId)}/bypass`, { reason })
|
||||
.pipe(catchError((err) => throwError(() => this.normalizeError(err))));
|
||||
}
|
||||
|
||||
private normalizeError(err: unknown): Error {
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
const message = err.error?.message ?? err.message ?? 'Release API request failed';
|
||||
const normalized = new Error(message);
|
||||
(normalized as any).status = err.status;
|
||||
(normalized as any).statusText = err.statusText;
|
||||
return normalized;
|
||||
}
|
||||
if (err instanceof Error) return err;
|
||||
return new Error('Release API request failed');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mock Data Fixtures
|
||||
// ============================================================================
|
||||
|
||||
@@ -158,9 +158,6 @@ export class RiskHttpClient implements RiskApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('RiskHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
import { Injectable, InjectionToken, Inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
import type {
|
||||
Schedule,
|
||||
@@ -126,3 +127,71 @@ export class SchedulerHttpClient implements SchedulerApi {
|
||||
return new HttpHeaders(headers);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mock Implementation
|
||||
// ============================================================================
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MockSchedulerClient implements SchedulerApi {
|
||||
private schedules: Schedule[] = [
|
||||
{ id: 'sch-1', name: 'Nightly Vulnerability Sync', description: 'Synchronize vulnerability feeds from upstream sources', cronExpression: '0 2 * * *', timezone: 'UTC', enabled: true, taskType: 'vulnerability-sync', taskConfig: { sources: ['osv', 'nvd'] }, lastRunAt: '2026-02-16T02:00:00Z', nextRunAt: '2026-02-17T02:00:00Z', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-02-15T00:00:00Z', createdBy: 'admin', tags: ['security', 'nightly'], retryPolicy: { maxRetries: 3, backoffMultiplier: 2, initialDelayMs: 5000, maxDelayMs: 60000 }, concurrencyLimit: 1 },
|
||||
{ id: 'sch-2', name: 'SBOM Refresh', description: 'Re-scan all registered artifacts for SBOM updates', cronExpression: '0 4 * * 0', timezone: 'UTC', enabled: true, taskType: 'sbom-refresh', taskConfig: { scope: 'all' }, lastRunAt: '2026-02-09T04:00:00Z', nextRunAt: '2026-02-16T04:00:00Z', createdAt: '2026-01-05T00:00:00Z', updatedAt: '2026-02-09T00:00:00Z', createdBy: 'admin', tags: ['sbom', 'weekly'], retryPolicy: { maxRetries: 2, backoffMultiplier: 2, initialDelayMs: 10000, maxDelayMs: 120000 }, concurrencyLimit: 2 },
|
||||
{ id: 'sch-3', name: 'Advisory Update Check', description: 'Check for new security advisories from configured sources', cronExpression: '0 */6 * * *', timezone: 'UTC', enabled: true, taskType: 'advisory-update', taskConfig: { sources: ['cisa', 'mitre'] }, lastRunAt: '2026-02-16T00:00:00Z', nextRunAt: '2026-02-16T06:00:00Z', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-02-01T00:00:00Z', createdBy: 'admin', tags: ['security', 'advisories'], retryPolicy: { maxRetries: 3, backoffMultiplier: 2, initialDelayMs: 5000, maxDelayMs: 60000 }, concurrencyLimit: 1 },
|
||||
{ id: 'sch-4', name: 'Evidence Export', description: 'Export evidence bundles to configured destinations', cronExpression: '0 6 1 * *', timezone: 'UTC', enabled: false, taskType: 'export', taskConfig: { destination: 's3', format: 'bundle' }, lastRunAt: '2026-02-01T06:00:00Z', nextRunAt: undefined, createdAt: '2026-01-15T00:00:00Z', updatedAt: '2026-02-15T10:00:00Z', createdBy: 'admin', tags: ['evidence', 'monthly'], retryPolicy: { maxRetries: 2, backoffMultiplier: 2, initialDelayMs: 15000, maxDelayMs: 300000 }, concurrencyLimit: 1 },
|
||||
];
|
||||
|
||||
listSchedules(): Observable<Schedule[]> {
|
||||
return of([...this.schedules]).pipe(delay(300));
|
||||
}
|
||||
|
||||
getSchedule(id: string): Observable<Schedule> {
|
||||
const s = this.schedules.find(s => s.id === id);
|
||||
return of(s ?? this.schedules[0]).pipe(delay(200));
|
||||
}
|
||||
|
||||
createSchedule(dto: CreateScheduleDto): Observable<Schedule> {
|
||||
const s: Schedule = { ...dto, id: `sch-${Date.now()}`, taskConfig: dto.taskConfig ?? {}, lastRunAt: undefined, nextRunAt: new Date().toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), createdBy: 'admin', tags: dto.tags ?? [], retryPolicy: dto.retryPolicy ?? { maxRetries: 3, backoffMultiplier: 2, initialDelayMs: 5000, maxDelayMs: 60000 }, concurrencyLimit: dto.concurrencyLimit ?? 1 };
|
||||
this.schedules.push(s);
|
||||
return of(s).pipe(delay(300));
|
||||
}
|
||||
|
||||
updateSchedule(id: string, dto: UpdateScheduleDto): Observable<Schedule> {
|
||||
const idx = this.schedules.findIndex(s => s.id === id);
|
||||
if (idx >= 0) { Object.assign(this.schedules[idx], dto, { updatedAt: new Date().toISOString() }); }
|
||||
return of(this.schedules[idx] ?? this.schedules[0]).pipe(delay(300));
|
||||
}
|
||||
|
||||
deleteSchedule(id: string): Observable<void> {
|
||||
this.schedules = this.schedules.filter(s => s.id !== id);
|
||||
return of(void 0).pipe(delay(200));
|
||||
}
|
||||
|
||||
pauseSchedule(id: string): Observable<void> {
|
||||
const s = this.schedules.find(s => s.id === id);
|
||||
if (s) s.enabled = false;
|
||||
return of(void 0).pipe(delay(200));
|
||||
}
|
||||
|
||||
resumeSchedule(id: string): Observable<void> {
|
||||
const s = this.schedules.find(s => s.id === id);
|
||||
if (s) s.enabled = true;
|
||||
return of(void 0).pipe(delay(200));
|
||||
}
|
||||
|
||||
triggerSchedule(_id: string): Observable<void> {
|
||||
return of(void 0).pipe(delay(200));
|
||||
}
|
||||
|
||||
previewImpact(schedule: CreateScheduleDto): Observable<ScheduleImpactPreview> {
|
||||
return of({
|
||||
scheduleId: 'preview',
|
||||
proposedChange: 'enable' as const,
|
||||
affectedRuns: 0,
|
||||
nextRunTime: new Date().toISOString(),
|
||||
estimatedLoad: 0.15,
|
||||
conflicts: [],
|
||||
warnings: schedule.concurrencyLimit && schedule.concurrencyLimit > 3 ? ['High concurrency limit may impact other schedules'] : [],
|
||||
}).pipe(delay(200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,9 @@ export class HttpScoreClient implements ScoreApi {
|
||||
private readonly config = inject(AppConfigService);
|
||||
|
||||
private get baseUrl(): string {
|
||||
return `${this.config.apiBaseUrl}/api/v1/scores`;
|
||||
const gatewayBase = this.config.config.apiBaseUrls.gateway ?? this.config.config.apiBaseUrls.authority;
|
||||
const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase;
|
||||
return `${normalized}/api/v1/scores`;
|
||||
}
|
||||
|
||||
getScoreSummary(scanId: string): Observable<ScoreSummary> {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
import { Injectable, InjectionToken, Inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { delay, map } from 'rxjs/operators';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
|
||||
// ============================================================================
|
||||
@@ -73,14 +74,16 @@ export class SecurityFindingsHttpClient implements SecurityFindingsApi {
|
||||
if (filter?.environment) params = params.set('environment', filter.environment);
|
||||
if (filter?.limit) params = params.set('limit', filter.limit.toString());
|
||||
if (filter?.sort) params = params.set('sort', filter.sort);
|
||||
return this.http.get<FindingDto[]>(`${this.baseUrl}/api/v1/findings`, {
|
||||
return this.http.get<any>(`${this.baseUrl}/api/v1/findings/summaries`, {
|
||||
params,
|
||||
headers: this.buildHeaders(),
|
||||
});
|
||||
}).pipe(
|
||||
map((res: any) => Array.isArray(res) ? res : (res?.items ?? [])),
|
||||
);
|
||||
}
|
||||
|
||||
getFinding(findingId: string): Observable<FindingDetailDto> {
|
||||
return this.http.get<FindingDetailDto>(`${this.baseUrl}/api/v1/findings/${findingId}`, {
|
||||
return this.http.get<FindingDetailDto>(`${this.baseUrl}/api/v1/findings/${findingId}/summary`, {
|
||||
headers: this.buildHeaders(),
|
||||
});
|
||||
}
|
||||
@@ -94,3 +97,45 @@ export class SecurityFindingsHttpClient implements SecurityFindingsApi {
|
||||
return new HttpHeaders(headers);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mock Implementation
|
||||
// ============================================================================
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MockSecurityFindingsClient implements SecurityFindingsApi {
|
||||
listFindings(_filter?: FindingsFilter): Observable<FindingDto[]> {
|
||||
const data: FindingDto[] = [
|
||||
{ id: 'f-1', package: 'lodash', version: '4.17.20', severity: 'CRITICAL', cvss: 9.8, reachable: true, reachabilityConfidence: 0.95, vexStatus: 'affected', releaseId: 'rel-1', releaseVersion: '1.2.3', delta: 'new', environments: ['prod', 'staging'], firstSeen: '2026-02-10T08:00:00Z' },
|
||||
{ id: 'f-2', package: 'jackson-databind', version: '2.14.0', severity: 'HIGH', cvss: 7.5, reachable: true, reachabilityConfidence: 0.88, vexStatus: 'under_investigation', releaseId: 'rel-1', releaseVersion: '1.2.3', delta: 'unchanged', environments: ['prod'], firstSeen: '2026-02-08T10:00:00Z' },
|
||||
{ id: 'f-3', package: 'express', version: '4.18.1', severity: 'MEDIUM', cvss: 5.3, reachable: false, reachabilityConfidence: 0.72, vexStatus: 'not_affected', releaseId: 'rel-2', releaseVersion: '2.0.0', delta: 'unchanged', environments: ['dev', 'staging'], firstSeen: '2026-01-28T12:00:00Z' },
|
||||
{ id: 'f-4', package: 'netty', version: '4.1.86', severity: 'HIGH', cvss: 7.2, reachable: null, vexStatus: 'none', releaseId: 'rel-3', releaseVersion: '3.1.0', delta: 'new', environments: ['prod', 'staging', 'dev'], firstSeen: '2026-02-14T09:00:00Z' },
|
||||
{ id: 'f-5', package: 'openssl', version: '3.0.8', severity: 'CRITICAL', cvss: 9.1, reachable: true, reachabilityConfidence: 0.99, vexStatus: 'affected', releaseId: 'rel-1', releaseVersion: '1.2.3', delta: 'unchanged', environments: ['prod'], firstSeen: '2026-02-01T06:00:00Z' },
|
||||
{ id: 'f-6', package: 'spring-core', version: '6.0.4', severity: 'MEDIUM', cvss: 6.1, reachable: false, vexStatus: 'fixed', releaseId: 'rel-2', releaseVersion: '2.0.0', delta: 'resolved', environments: ['staging'], firstSeen: '2026-01-20T14:00:00Z' },
|
||||
{ id: 'f-7', package: 'axios', version: '1.3.0', severity: 'LOW', cvss: 3.7, reachable: false, vexStatus: 'none', releaseId: 'rel-4', releaseVersion: '4.0.0-beta', delta: 'new', environments: ['dev'], firstSeen: '2026-02-15T11:00:00Z' },
|
||||
];
|
||||
return of(data).pipe(delay(300));
|
||||
}
|
||||
|
||||
getFinding(findingId: string): Observable<FindingDetailDto> {
|
||||
return of({
|
||||
id: findingId,
|
||||
package: 'lodash',
|
||||
version: '4.17.20',
|
||||
severity: 'CRITICAL' as const,
|
||||
cvss: 9.8,
|
||||
reachable: true,
|
||||
reachabilityConfidence: 0.95,
|
||||
vexStatus: 'affected',
|
||||
releaseId: 'rel-1',
|
||||
releaseVersion: '1.2.3',
|
||||
delta: 'new',
|
||||
environments: ['prod', 'staging'],
|
||||
firstSeen: '2026-02-10T08:00:00Z',
|
||||
description: 'Prototype Pollution in lodash allows attackers to manipulate JavaScript objects via crafted input.',
|
||||
references: ['https://nvd.nist.gov/vuln/detail/CVE-2025-1234'],
|
||||
affectedVersions: ['< 4.17.21'],
|
||||
fixedVersions: ['4.17.21'],
|
||||
}).pipe(delay(200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Injectable, InjectionToken, Inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, forkJoin, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { catchError, delay, map } from 'rxjs/operators';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
import { SECURITY_FINDINGS_API_BASE_URL } from './security-findings.client';
|
||||
import { POLICY_EXCEPTIONS_API_BASE_URL } from './policy-exceptions.client';
|
||||
@@ -84,13 +84,16 @@ export class SecurityOverviewHttpClient implements SecurityOverviewApi {
|
||||
getOverviewStats(): Observable<SecurityOverviewData> {
|
||||
const headers = this.buildHeaders();
|
||||
|
||||
const findings$ = this.http.get<any[]>(
|
||||
`${this.scannerBaseUrl}/api/v1/findings`,
|
||||
const findings$ = this.http.get<any>(
|
||||
`${this.scannerBaseUrl}/api/v1/findings/summaries`,
|
||||
{ headers }
|
||||
).pipe(catchError(() => of([] as any[])));
|
||||
).pipe(
|
||||
map((res: any) => Array.isArray(res) ? res : (res?.items ?? [])),
|
||||
catchError(() => of([] as any[])),
|
||||
);
|
||||
|
||||
const exceptions$ = this.http.get<any[]>(
|
||||
`${this.policyBaseUrl}/policyGateway/api/v1/policy/exception/requests`,
|
||||
`${this.policyBaseUrl}/api/policy/exceptions`,
|
||||
{ params: { status: 'active' }, headers }
|
||||
).pipe(catchError(() => of([] as any[])));
|
||||
|
||||
@@ -165,3 +168,34 @@ export class SecurityOverviewHttpClient implements SecurityOverviewApi {
|
||||
return new HttpHeaders(headers);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mock Implementation
|
||||
// ============================================================================
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MockSecurityOverviewClient implements SecurityOverviewApi {
|
||||
getOverviewStats(): Observable<SecurityOverviewData> {
|
||||
return of({
|
||||
stats: { critical: 2, high: 5, medium: 8, low: 12, reachable: 4 },
|
||||
vexStats: { covered: 15, pending: 12 },
|
||||
recentFindings: [
|
||||
{ id: 'f-1', package: 'lodash:4.17.20', severity: 'CRITICAL', reachable: true, time: '2026-02-15T08:00:00Z' },
|
||||
{ id: 'f-2', package: 'jackson-databind:2.14.0', severity: 'HIGH', reachable: true, time: '2026-02-14T10:00:00Z' },
|
||||
{ id: 'f-3', package: 'express:4.18.1', severity: 'MEDIUM', reachable: false, time: '2026-02-13T12:00:00Z' },
|
||||
{ id: 'f-4', package: 'netty:4.1.86', severity: 'HIGH', reachable: false, time: '2026-02-12T09:00:00Z' },
|
||||
{ id: 'f-5', package: 'openssl:3.0.8', severity: 'CRITICAL', reachable: true, time: '2026-02-11T06:00:00Z' },
|
||||
],
|
||||
topPackages: [
|
||||
{ name: 'lodash', version: '4.17.20', critical: 1, high: 2, medium: 1 },
|
||||
{ name: 'jackson-databind', version: '2.14.0', critical: 0, high: 3, medium: 2 },
|
||||
{ name: 'netty', version: '4.1.86', critical: 1, high: 1, medium: 0 },
|
||||
{ name: 'express', version: '4.18.1', critical: 0, high: 0, medium: 3 },
|
||||
],
|
||||
activeExceptions: [
|
||||
{ id: 'ex-1', finding: 'CVE-2025-3456', reason: 'Mitigated by WAF rules', expiresIn: '14 days' },
|
||||
{ id: 'ex-2', finding: 'CVE-2025-7890', reason: 'Not reachable in our deployment', expiresIn: '7 days' },
|
||||
],
|
||||
}).pipe(delay(300));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,11 +377,6 @@ export class VexConsensusHttpClient implements VexConsensusApi {
|
||||
if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId);
|
||||
if (ifNoneMatch) headers = headers.set('If-None-Match', ifNoneMatch);
|
||||
|
||||
const session = this.authStore.session();
|
||||
if (session?.tokens.accessToken) {
|
||||
headers = headers.set('Authorization', `Bearer ${session.tokens.accessToken}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -389,10 +384,7 @@ export class VexConsensusHttpClient implements VexConsensusApi {
|
||||
const tenant = tenantId?.trim() ||
|
||||
this.tenantService.activeTenantId() ||
|
||||
this.authStore.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('VexConsensusHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private cacheStatement(statement: VexConsensusStatement): void {
|
||||
|
||||
@@ -106,11 +106,6 @@ export class VexDecisionsHttpClient implements VexDecisionsApi {
|
||||
if (projectId) headers = headers.set('X-Stella-Project', projectId);
|
||||
if (ifNoneMatch) headers = headers.set('If-None-Match', ifNoneMatch);
|
||||
|
||||
const session = this.authSession.session();
|
||||
if (session?.tokens.accessToken) {
|
||||
headers = headers.set('Authorization', `Bearer ${session.tokens.accessToken}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -127,8 +122,7 @@ export class VexDecisionsHttpClient implements VexDecisionsApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = tenantId ?? this.tenantService.activeTenantId();
|
||||
if (!tenant) throw new Error('VexDecisionsHttpClient requires an active tenant identifier.');
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,10 +128,7 @@ export class VexEvidenceHttpClient implements VexEvidenceApi {
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('VexEvidenceHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders {
|
||||
|
||||
@@ -74,16 +74,16 @@ export class VexHubApiHttpClient implements VexHubApi {
|
||||
const headers = this.buildHeaders(traceId);
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params.cveId) httpParams = httpParams.set('cveId', params.cveId);
|
||||
if (params.product) httpParams = httpParams.set('product', params.product);
|
||||
if (params.cveId) httpParams = httpParams.set('vulnerabilityId', params.cveId);
|
||||
if (params.product) httpParams = httpParams.set('productKey', params.product);
|
||||
if (params.status) httpParams = httpParams.set('status', params.status);
|
||||
if (params.source) httpParams = httpParams.set('source', params.source);
|
||||
if (params.source) httpParams = httpParams.set('sourceId', params.source);
|
||||
if (params.dateFrom) httpParams = httpParams.set('dateFrom', params.dateFrom);
|
||||
if (params.dateTo) httpParams = httpParams.set('dateTo', params.dateTo);
|
||||
if (params.limit) httpParams = httpParams.set('limit', params.limit.toString());
|
||||
if (params.offset) httpParams = httpParams.set('offset', params.offset.toString());
|
||||
|
||||
return this.http.get<VexStatementSearchResponse>(`${this.baseUrl}/statements`, { headers, params: httpParams }).pipe(
|
||||
return this.http.get<VexStatementSearchResponse>(`${this.baseUrl}/search`, { headers, params: httpParams }).pipe(
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export class VexHubApiHttpClient implements VexHubApi {
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
const headers = this.buildHeaders(traceId);
|
||||
|
||||
return this.http.get<VexStatement>(`${this.baseUrl}/statements/${encodeURIComponent(statementId)}`, { headers }).pipe(
|
||||
return this.http.get<VexStatement>(`${this.baseUrl}/statement/${encodeURIComponent(statementId)}`, { headers }).pipe(
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
@@ -119,7 +119,19 @@ export class VexHubApiHttpClient implements VexHubApi {
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
const headers = this.buildHeaders(traceId);
|
||||
|
||||
return this.http.get<VexHubStats>(`${this.baseUrl}/stats`, { headers }).pipe(
|
||||
return this.http.get<any>(`${this.baseUrl}/stats`, { headers }).pipe(
|
||||
map((res: any) => ({
|
||||
totalStatements: res.totalStatements ?? 0,
|
||||
byStatus: res.byStatus ?? {
|
||||
affected: 0,
|
||||
not_affected: 0,
|
||||
fixed: 0,
|
||||
under_investigation: 0,
|
||||
},
|
||||
bySource: res.bySource ?? {},
|
||||
recentActivity: res.recentActivity ?? [],
|
||||
trends: res.trends,
|
||||
} as VexHubStats)),
|
||||
catchError((err) => throwError(() => this.mapError(err, traceId)))
|
||||
);
|
||||
}
|
||||
@@ -202,6 +214,9 @@ export class VexHubApiHttpClient implements VexHubApi {
|
||||
}
|
||||
|
||||
private mapError(err: unknown, traceId: string): Error {
|
||||
if (err && typeof err === 'object' && 'status' in err && 'message' in err) {
|
||||
return new Error(`[${traceId}] VEX Hub error: ${(err as any).status} ${(err as any).statusText ?? (err as any).message}`);
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
return new Error(`[${traceId}] VEX Hub error: ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -490,10 +490,7 @@ export class VulnExportOrchestratorService implements VulnExportOrchestratorApi
|
||||
const tenant = tenantId?.trim() ||
|
||||
this.tenantService.activeTenantId() ||
|
||||
this.authStore.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('VulnExportOrchestratorService requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private createError(code: string, message: string, traceId: string): Error {
|
||||
|
||||
@@ -368,18 +368,6 @@ export class VulnerabilityHttpClient implements VulnerabilityApi {
|
||||
if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId);
|
||||
if (requestId) headers = headers.set('X-Request-Id', requestId);
|
||||
|
||||
// Add anti-forgery token if available
|
||||
const session = this.authSession.session();
|
||||
if (session?.tokens.accessToken) {
|
||||
headers = headers.set('Authorization', `Bearer ${session.tokens.accessToken}`);
|
||||
}
|
||||
|
||||
// Add DPoP proof if available (for proof-of-possession)
|
||||
const dpopThumbprint = session?.dpopKeyThumbprint;
|
||||
if (dpopThumbprint) {
|
||||
headers = headers.set('X-DPoP-Thumbprint', dpopThumbprint);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -388,10 +376,7 @@ export class VulnerabilityHttpClient implements VulnerabilityApi {
|
||||
const tenant = (tenantId && tenantId.trim()) ||
|
||||
this.tenantService.activeTenantId() ||
|
||||
this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
throw new Error('VulnerabilityHttpClient requires an active tenant identifier.');
|
||||
}
|
||||
return tenant;
|
||||
return tenant ?? 'default';
|
||||
}
|
||||
|
||||
private generateRequestId(): string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
const LOGIN_REQUEST_KEY = 'stellaops.auth.login.request';
|
||||
const LOGIN_REQUEST_PREFIX = 'stellaops.auth.login.';
|
||||
|
||||
export interface PendingLoginRequest {
|
||||
readonly state: string;
|
||||
@@ -18,7 +18,11 @@ export class AuthStorageService {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(LOGIN_REQUEST_KEY, JSON.stringify(request));
|
||||
sessionStorage.setItem(
|
||||
LOGIN_REQUEST_PREFIX + request.state,
|
||||
JSON.stringify(request)
|
||||
);
|
||||
this.pruneStaleEntries();
|
||||
}
|
||||
|
||||
consumePendingLogin(expectedState: string): PendingLoginRequest | null {
|
||||
@@ -26,12 +30,13 @@ export class AuthStorageService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = sessionStorage.getItem(LOGIN_REQUEST_KEY);
|
||||
const key = LOGIN_REQUEST_PREFIX + expectedState;
|
||||
const raw = sessionStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(LOGIN_REQUEST_KEY);
|
||||
sessionStorage.removeItem(key);
|
||||
try {
|
||||
const request = JSON.parse(raw) as PendingLoginRequest;
|
||||
if (request.state !== expectedState) {
|
||||
@@ -42,4 +47,27 @@ export class AuthStorageService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove entries older than 10 minutes to prevent sessionStorage bloat. */
|
||||
private pruneStaleEntries(): void {
|
||||
try {
|
||||
const cutoff = Date.now() - 10 * 60 * 1000;
|
||||
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (!key?.startsWith(LOGIN_REQUEST_PREFIX)) continue;
|
||||
const raw = sessionStorage.getItem(key);
|
||||
if (!raw) continue;
|
||||
try {
|
||||
const entry = JSON.parse(raw) as PendingLoginRequest;
|
||||
if (entry.createdAtEpochMs < cutoff) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
} catch {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,23 @@ export const StellaOpsScopes = {
|
||||
|
||||
// Findings scope
|
||||
FINDINGS_READ: 'findings:read',
|
||||
|
||||
// Notify scopes
|
||||
NOTIFY_VIEWER: 'notify.viewer',
|
||||
NOTIFY_OPERATOR: 'notify.operator',
|
||||
NOTIFY_ADMIN: 'notify.admin',
|
||||
|
||||
// Risk scopes
|
||||
RISK_READ: 'risk:read',
|
||||
|
||||
// Health scopes
|
||||
HEALTH_READ: 'health:read',
|
||||
|
||||
// Vulnerability scopes
|
||||
VULN_VIEW: 'vuln:view',
|
||||
VULN_INVESTIGATE: 'vuln:investigate',
|
||||
VULN_OPERATE: 'vuln:operate',
|
||||
VULN_AUDIT: 'vuln:audit',
|
||||
} as const;
|
||||
|
||||
export type StellaOpsScope = (typeof StellaOpsScopes)[keyof typeof StellaOpsScopes];
|
||||
@@ -334,6 +351,19 @@ export const ScopeLabels: Record<StellaOpsScope, string> = {
|
||||
'exceptions:write': 'Create Exceptions',
|
||||
// Findings scope label
|
||||
'findings:read': 'View Policy Findings',
|
||||
// Notify scope labels
|
||||
'notify.viewer': 'View Notifications',
|
||||
'notify.operator': 'Operate Notifications',
|
||||
'notify.admin': 'Administer Notifications',
|
||||
// Risk scope labels
|
||||
'risk:read': 'View Risk Profiles',
|
||||
// Health scope labels
|
||||
'health:read': 'View Platform Health',
|
||||
// Vulnerability scope labels
|
||||
'vuln:view': 'View Vulnerabilities',
|
||||
'vuln:investigate': 'Investigate Vulnerabilities',
|
||||
'vuln:operate': 'Operate Vulnerability Management',
|
||||
'vuln:audit': 'Audit Vulnerability Decisions',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,7 @@ export type TenantScope =
|
||||
| 'advisory-ai'
|
||||
| 'ledger'
|
||||
| 'exception'
|
||||
| 'exceptions'
|
||||
| 'aoc'
|
||||
| 'sbom'
|
||||
| 'attest'
|
||||
@@ -268,6 +269,16 @@ export class TenantActivationService {
|
||||
}
|
||||
|
||||
const grantedScopes = new Set(session.scopes);
|
||||
|
||||
// When scopes are not populated in the session (e.g. not extracted from token),
|
||||
// skip client-side scope checks and let the backend enforce authorization.
|
||||
if (grantedScopes.size === 0) {
|
||||
if (resource && action) {
|
||||
this.emitDecision({ resource, action, requiredScopes, decision: 'allow' });
|
||||
}
|
||||
return { allowed: true, missingScopes: [] };
|
||||
}
|
||||
|
||||
const missingScopes = requiredScopes.filter(scope => !this.scopeMatches(scope, grantedScopes));
|
||||
|
||||
if (missingScopes.length > 0) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AuthSessionStore } from './auth-session.store';
|
||||
*/
|
||||
export const TENANT_HEADERS = {
|
||||
TENANT_ID: 'X-Tenant-Id',
|
||||
STELLAOPS_TENANT: 'X-StellaOps-Tenant',
|
||||
PROJECT_ID: 'X-Project-Id',
|
||||
TRACE_ID: 'X-Stella-Trace-Id',
|
||||
REQUEST_ID: 'X-Request-Id',
|
||||
@@ -67,11 +68,10 @@ export class TenantHttpInterceptor implements HttpInterceptor {
|
||||
private addTenantHeaders(request: HttpRequest<unknown>): HttpRequest<unknown> {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// Add tenant ID
|
||||
const tenantId = this.getTenantId();
|
||||
if (tenantId) {
|
||||
headers[TENANT_HEADERS.TENANT_ID] = tenantId;
|
||||
}
|
||||
// Add tenant ID (use "default" if no active tenant)
|
||||
const tenantId = this.getTenantId() ?? 'default';
|
||||
headers[TENANT_HEADERS.TENANT_ID] = tenantId;
|
||||
headers[TENANT_HEADERS.STELLAOPS_TENANT] = tenantId;
|
||||
|
||||
// Add project ID if active
|
||||
const projectId = this.tenantService.activeProjectId();
|
||||
|
||||
@@ -268,6 +268,7 @@ export class AppConfigService {
|
||||
private normalizeConfig(config: AppConfig): AppConfig {
|
||||
const authority = {
|
||||
...config.authority,
|
||||
...this.normalizeAuthorityUrls(config.authority),
|
||||
dpopAlgorithms:
|
||||
config.authority.dpopAlgorithms?.length ?? 0
|
||||
? config.authority.dpopAlgorithms
|
||||
@@ -301,6 +302,42 @@ export class AppConfigService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts absolute Docker-internal authority URLs to relative paths so the
|
||||
* OIDC flow stays on the same origin (the gateway) instead of redirecting
|
||||
* to unreachable internal hostnames like https://stella-ops.local.
|
||||
*
|
||||
* The gateway proxies /connect/* to the authority service.
|
||||
*/
|
||||
private normalizeAuthorityUrls(auth: AuthorityConfig): Partial<AuthorityConfig> {
|
||||
const overrides: Record<string, string> = {};
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
if (!origin) return overrides;
|
||||
|
||||
const urlFields: (keyof AuthorityConfig)[] = [
|
||||
'issuer', 'authorizeEndpoint', 'tokenEndpoint', 'logoutEndpoint',
|
||||
'redirectUri', 'silentRefreshRedirectUri', 'postLogoutRedirectUri',
|
||||
];
|
||||
|
||||
for (const field of urlFields) {
|
||||
const value = auth[field];
|
||||
if (typeof value !== 'string') continue;
|
||||
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.hostname === 'stella-ops.local' || parsed.hostname.endsWith('.stella-ops.local')) {
|
||||
// Rewrite to current origin so the OIDC flow stays on the gateway.
|
||||
// The URL constructor requires an absolute base for proper URL building.
|
||||
overrides[field] = origin + parsed.pathname + parsed.search + parsed.hash;
|
||||
}
|
||||
} catch {
|
||||
// Not an absolute URL, leave as-is
|
||||
}
|
||||
}
|
||||
|
||||
return overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts absolute Docker-internal URLs (e.g. http://scanner.stella-ops.local)
|
||||
* to relative paths (e.g. /scanner) so requests go through the gateway's
|
||||
@@ -320,6 +357,22 @@ export class AppConfigService {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// The Platform service uses distinct key names (policyGateway, policyEngine,
|
||||
// findingsLedger, vexhub) while frontend clients expect canonical keys
|
||||
// (policy, ledger, vex). When a canonical key is missing, default to empty
|
||||
// string so the gateway's path-based routing dispatches to the correct
|
||||
// backend service automatically.
|
||||
if (!normalized['policy'] && (normalized['policyGateway'] || normalized['policyEngine'])) {
|
||||
normalized['policy'] = '';
|
||||
}
|
||||
if (!normalized['ledger'] && normalized['findingsLedger']) {
|
||||
normalized['ledger'] = '';
|
||||
}
|
||||
if (!normalized['vex'] && normalized['vexhub']) {
|
||||
normalized['vex'] = '';
|
||||
}
|
||||
|
||||
return normalized as unknown as ApiBaseUrlConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,32 +55,61 @@ export class BackendProbeService {
|
||||
try {
|
||||
const authorityBase = this.configService.config.authority.issuer;
|
||||
|
||||
// Relative issuer (e.g. "/authority") means config came from the static
|
||||
// fallback — there is no real backend to probe yet.
|
||||
// Relative issuer after normalization (e.g. "/" from a Docker-internal
|
||||
// URL) is valid — the gateway proxies /.well-known to the authority.
|
||||
// Only the static fallback "/authority" indicates no real backend.
|
||||
if (authorityBase.startsWith('/')) {
|
||||
this.probeStatus.set('unreachable');
|
||||
this.probeError.set('Authority issuer is a relative path; no backend configured.');
|
||||
// Probe same-origin well-known endpoint through the gateway
|
||||
const body = await firstValueFrom(
|
||||
this.http
|
||||
.get('/.well-known/openid-configuration', { responseType: 'text', withCredentials: false })
|
||||
.pipe(timeout(PROBE_TIMEOUT_MS))
|
||||
);
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = JSON.parse(body as string);
|
||||
} catch {
|
||||
this.probeStatus.set('unreachable');
|
||||
this.probeError.set('Authority returned non-JSON response (likely SPA fallback).');
|
||||
return;
|
||||
}
|
||||
if (!parsed['issuer'] || !parsed['authorization_endpoint']) {
|
||||
this.probeStatus.set('unreachable');
|
||||
this.probeError.set('Authority response is not a valid OIDC discovery document.');
|
||||
return;
|
||||
}
|
||||
this.probeStatus.set('reachable');
|
||||
return;
|
||||
}
|
||||
|
||||
let normalized = authorityBase.endsWith('/')
|
||||
? authorityBase.slice(0, -1)
|
||||
: authorityBase;
|
||||
// When the issuer is a Docker-internal URL (e.g. https://stella-ops.local
|
||||
// or https://authority.stella-ops.local), it is not reachable from the
|
||||
// browser. The gateway proxies /.well-known to the authority service,
|
||||
// so probe via same-origin instead.
|
||||
let wellKnownUrl: string;
|
||||
try {
|
||||
const issuerHost = new URL(authorityBase).hostname;
|
||||
if (issuerHost === 'stella-ops.local' || issuerHost.endsWith('.stella-ops.local')) {
|
||||
wellKnownUrl = '/.well-known/openid-configuration';
|
||||
} else {
|
||||
let normalized = authorityBase.endsWith('/')
|
||||
? authorityBase.slice(0, -1)
|
||||
: authorityBase;
|
||||
|
||||
// Upgrade http → https when the SPA itself was loaded over HTTPS.
|
||||
// This prevents mixed-content blocks when envsettings.json specifies
|
||||
// http:// URLs but the browser enforces HTTPS-only fetch from an
|
||||
// HTTPS origin.
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
normalized.startsWith('http://')
|
||||
) {
|
||||
normalized = normalized.replace(/^http:\/\//, 'https://');
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
normalized.startsWith('http://')
|
||||
) {
|
||||
normalized = normalized.replace(/^http:\/\//, 'https://');
|
||||
}
|
||||
|
||||
wellKnownUrl = `${normalized}/.well-known/openid-configuration`;
|
||||
}
|
||||
} catch {
|
||||
wellKnownUrl = '/.well-known/openid-configuration';
|
||||
}
|
||||
|
||||
const wellKnownUrl = `${normalized}/.well-known/openid-configuration`;
|
||||
|
||||
const body = await firstValueFrom(
|
||||
this.http
|
||||
.get(wellKnownUrl, { responseType: 'text', withCredentials: false })
|
||||
|
||||
@@ -176,13 +176,13 @@ export class MockFixVerificationApi implements FixVerificationApi {
|
||||
{
|
||||
functionName: 'vulnerable_func',
|
||||
status: verdict === 'fixed' ? 'modified' : verdict === 'partial' ? 'partially_modified' : 'unchanged',
|
||||
statusIcon: verdict === 'fixed' ? '✓' : verdict === 'partial' ? '◐' : '✗',
|
||||
statusIcon: verdict === 'fixed' ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>' : verdict === 'partial' ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z"/><path d="M12 2a10 10 0 0 1 0 20z" fill="currentColor"/></svg>' : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
||||
details: verdict === 'fixed' ? 'Bounds check inserted' : verdict === 'partial' ? 'Some paths still reachable' : 'No changes detected',
|
||||
children: [
|
||||
{
|
||||
name: 'bb7→bb9',
|
||||
status: verdict === 'fixed' ? 'eliminated' : 'present',
|
||||
statusIcon: verdict === 'fixed' ? '✗' : '○',
|
||||
statusIcon: verdict === 'fixed' ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/></svg>',
|
||||
details: verdict === 'fixed' ? 'Edge removed in patch' : 'Edge still present'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -422,7 +422,7 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
|
||||
}
|
||||
.tab.active { color: var(--color-status-info-text); border-bottom-color: var(--color-status-info-text); font-weight: var(--font-weight-semibold); }
|
||||
|
||||
.tab-content { background: white; border-radius: var(--radius-lg); }
|
||||
.tab-content { background: var(--color-surface-primary); border-radius: var(--radius-lg); }
|
||||
.tab-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
||||
.tab-header h2 { margin: 0; font-size: 1.25rem; }
|
||||
|
||||
@@ -488,7 +488,7 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
|
||||
.config-section { background: var(--color-surface-primary); padding: 1.5rem; border-radius: var(--radius-lg); }
|
||||
.config-section h3 { margin: 0 0 0.5rem; font-size: 1rem; }
|
||||
.section-desc { color: var(--color-text-secondary); font-size: 0.875rem; margin: 0 0 1rem; }
|
||||
.config-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: white; border-radius: var(--radius-sm); margin-bottom: 0.5rem; }
|
||||
.config-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: var(--color-surface-secondary); border-radius: var(--radius-sm); margin-bottom: 0.5rem; }
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
|
||||
@@ -382,7 +382,7 @@ interface ChannelTypeOption {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
}
|
||||
|
||||
.channel-grid {
|
||||
@@ -393,7 +393,7 @@ interface ChannelTypeOption {
|
||||
|
||||
.channel-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: box-shadow 0.2s;
|
||||
@@ -565,7 +565,7 @@ interface ChannelTypeOption {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border: 2px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
@@ -652,7 +652,7 @@ interface ChannelTypeOption {
|
||||
}
|
||||
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
|
||||
.loading-state, .empty-state {
|
||||
display: flex;
|
||||
|
||||
@@ -265,7 +265,7 @@ import { NotifierDeliveryStats, NotifierDelivery } from '../../../core/api/notif
|
||||
}
|
||||
|
||||
.btn { padding: 0.5rem 1rem; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: var(--font-weight-medium); cursor: pointer; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
|
||||
.loading-state { padding: 3rem; text-align: center; color: var(--color-text-secondary); }
|
||||
|
||||
@@ -279,7 +279,7 @@ import { NotifierDeliveryStats, NotifierDelivery } from '../../../core/api/notif
|
||||
|
||||
.metric-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
@@ -360,7 +360,7 @@ import { NotifierDeliveryStats, NotifierDelivery } from '../../../core/api/notif
|
||||
/* Analytics Sections */
|
||||
.analytics-section {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 1rem;
|
||||
@@ -501,7 +501,7 @@ import { NotifierDeliveryStats, NotifierDelivery } from '../../../core/api/notif
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ import {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ import {
|
||||
}
|
||||
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
@@ -567,7 +567,7 @@ import {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
@@ -667,7 +667,7 @@ import {
|
||||
|
||||
.attempt-item {
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ import {
|
||||
.btn { padding: 0.5rem 1rem; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: var(--font-weight-medium); cursor: pointer; }
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; }
|
||||
.btn-icon { padding: 0.25rem 0.5rem; background: transparent; border: none; color: var(--color-status-info-text); font-size: 0.75rem; cursor: pointer; }
|
||||
.btn-icon.btn-danger { color: var(--color-status-error); }
|
||||
@@ -261,7 +261,7 @@ import {
|
||||
|
||||
.policy-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
@@ -390,7 +390,7 @@ import {
|
||||
|
||||
.level-form {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 1rem;
|
||||
@@ -443,7 +443,7 @@ import {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ interface ConfigSubTab {
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
@@ -323,12 +323,12 @@ interface ConfigSubTab {
|
||||
}
|
||||
|
||||
.sub-tab-button:hover {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-status-info-text);
|
||||
}
|
||||
|
||||
.sub-tab-button.active {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-status-info-text);
|
||||
border-color: var(--color-border-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
@@ -336,7 +336,7 @@ interface ConfigSubTab {
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
min-height: 400px;
|
||||
@@ -399,7 +399,7 @@ interface ConfigSubTab {
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-border-secondary);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ import { NotifierPreviewResponse, NotifierChannelType } from '../../../core/api/
|
||||
`,
|
||||
styles: [`
|
||||
.notification-preview {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
@@ -292,7 +292,7 @@ import { NotifierPreviewResponse, NotifierChannelType } from '../../../core/api/
|
||||
|
||||
.teams-card {
|
||||
display: flex;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
@@ -369,7 +369,7 @@ import { NotifierPreviewResponse, NotifierChannelType } from '../../../core/api/
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ import {
|
||||
.action-card {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
@@ -420,7 +420,7 @@ import {
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ import { NotifierRule, NotifierRuleStatus, NotifierSeverity } from '../../../cor
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -238,7 +238,7 @@ import { NotifierRule, NotifierRuleStatus, NotifierSeverity } from '../../../cor
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ import {
|
||||
.btn { padding: 0.5rem 1rem; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: var(--font-weight-medium); cursor: pointer; }
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-icon { padding: 0.25rem 0.5rem; background: transparent; border: none; color: var(--color-status-info-text); font-size: 0.75rem; cursor: pointer; }
|
||||
.btn-icon.btn-danger { color: var(--color-status-error); }
|
||||
|
||||
@@ -289,7 +289,7 @@ import {
|
||||
|
||||
.override-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all 0.2s;
|
||||
@@ -395,7 +395,7 @@ import {
|
||||
.preset-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.preset-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -328,7 +328,7 @@ import {
|
||||
|
||||
.btn { padding: 0.5rem 1rem; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: var(--font-weight-medium); cursor: pointer; }
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; }
|
||||
.btn-icon { padding: 0.25rem 0.5rem; background: transparent; border: none; color: var(--color-status-info-text); font-size: 0.75rem; cursor: pointer; }
|
||||
.btn-icon.btn-danger { color: var(--color-status-error); }
|
||||
@@ -377,7 +377,7 @@ import {
|
||||
|
||||
.override-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color 0.15s;
|
||||
|
||||
@@ -232,7 +232,7 @@ import { NotifierQuietHours, NotifierQuietHoursRequest, NotifierQuietWindow } fr
|
||||
|
||||
.btn { padding: 0.5rem 1rem; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: var(--font-weight-medium); cursor: pointer; }
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; }
|
||||
.btn-icon { padding: 0.25rem 0.5rem; background: transparent; border: none; color: var(--color-status-info-text); font-size: 0.75rem; cursor: pointer; }
|
||||
.btn-icon.btn-danger { color: var(--color-status-error); }
|
||||
@@ -241,7 +241,7 @@ import { NotifierQuietHours, NotifierQuietHoursRequest, NotifierQuietWindow } fr
|
||||
|
||||
.config-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
@@ -315,7 +315,7 @@ import { NotifierQuietHours, NotifierQuietHoursRequest, NotifierQuietWindow } fr
|
||||
.day-checkbox { display: flex; align-items: center; gap: 0.25rem; font-size: 0.75rem; cursor: pointer; }
|
||||
.day-checkbox input { width: 14px; height: 14px; }
|
||||
|
||||
.window-form, .exemption-form { padding: 0.75rem; background: white; border: 1px solid var(--color-border-primary); border-radius: var(--radius-md); margin-bottom: 0.5rem; }
|
||||
.window-form, .exemption-form { padding: 0.75rem; background: var(--color-surface-secondary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-md); margin-bottom: 0.5rem; }
|
||||
|
||||
.form-footer { display: flex; justify-content: flex-end; gap: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border-primary); }
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ import {
|
||||
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
.quick-templates {
|
||||
@@ -365,7 +365,7 @@ import {
|
||||
|
||||
.template-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
@@ -380,7 +380,7 @@ import {
|
||||
|
||||
/* Results Panel */
|
||||
.result-card {
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
|
||||
@@ -347,7 +347,7 @@ import {
|
||||
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; }
|
||||
|
||||
.btn-icon {
|
||||
@@ -424,7 +424,7 @@ import {
|
||||
}
|
||||
|
||||
.preview-result {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1rem;
|
||||
|
||||
@@ -296,7 +296,7 @@ type ThrottleScope = 'global' | 'channel' | 'rule' | 'event';
|
||||
.btn { padding: 0.5rem 1rem; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: var(--font-weight-medium); cursor: pointer; }
|
||||
.btn-primary { background: var(--color-status-info-text); color: var(--color-text-heading); border: none; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-secondary { background: white; color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-secondary { background: var(--color-surface-primary); color: var(--color-text-primary); border: 1px solid var(--color-border-secondary); }
|
||||
.btn-icon { padding: 0.25rem 0.5rem; background: transparent; border: none; color: var(--color-status-info-text); font-size: 0.75rem; cursor: pointer; }
|
||||
.btn-icon.btn-danger { color: var(--color-status-error); }
|
||||
|
||||
@@ -309,7 +309,7 @@ type ThrottleScope = 'global' | 'channel' | 'rule' | 'event';
|
||||
|
||||
.throttle-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
@@ -434,7 +434,7 @@ type ThrottleScope = 'global' | 'channel' | 'rule' | 'event';
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
background: var(--color-surface-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
@@ -466,7 +466,7 @@ type ThrottleScope = 'global' | 'channel' | 'rule' | 'event';
|
||||
.preset-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.preset-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -445,7 +445,7 @@ export interface GateContext {
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
||||
margin: 0 0 0.375rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #F5F0E6;
|
||||
color: var(--color-text-inverse);
|
||||
line-height: 1.3;
|
||||
animation: slide-up 500ms cubic-bezier(0.18, 0.89, 0.32, 1) 200ms both;
|
||||
}
|
||||
@@ -279,7 +279,7 @@ import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
||||
margin-bottom: 1.25rem;
|
||||
border-radius: 50%;
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #f87171;
|
||||
color: var(--color-border-error);
|
||||
animation: fade-in 500ms ease both;
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ import { AuthorityAuthService } from '../../core/auth/authority-auth.service';
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #F5F0E6;
|
||||
color: var(--color-text-inverse);
|
||||
line-height: 1.3;
|
||||
animation: slide-up 500ms ease 100ms both;
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ type ViewMode = 'heatmap' | 'details' | 'matches';
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
font-size: var(--font-size-base);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
@@ -542,7 +542,7 @@ type ViewMode = 'heatmap' | 'details' | 'matches';
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.15s;
|
||||
@@ -634,7 +634,7 @@ type ViewMode = 'heatmap' | 'details' | 'matches';
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ type ViewMode = 'heatmap' | 'details' | 'matches';
|
||||
|
||||
/* Details section */
|
||||
.details-section {
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 24px;
|
||||
@@ -748,7 +748,7 @@ type ViewMode = 'heatmap' | 'details' | 'matches';
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--color-status-info);
|
||||
border-radius: var(--radius-sm);
|
||||
background: white;
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-status-info);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export type UserRole = 'developer' | 'security' | 'audit';
|
||||
|
||||
@if (error()) {
|
||||
<div class="compare-view__error" role="alert">
|
||||
<span>⚠</span> {{ error() }}
|
||||
<span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span> {{ error() }}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { ScanDigest } from '../services/compare.service';
|
||||
<div class="trust-indicator">
|
||||
<span class="trust-indicator__label">Signature</span>
|
||||
<div class="trust-indicator__value" [class]="signatureClass()">
|
||||
<span class="trust-indicator__icon">{{ signatureIcon() }}</span>
|
||||
<span class="trust-indicator__icon" [innerHTML]="signatureIcon()"></span>
|
||||
<span>{{ signatureText() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,11 +144,12 @@ export class TrustIndicatorsComponent {
|
||||
readonly signatureClass = computed(() => this.current()?.signatureStatus ?? 'unknown');
|
||||
|
||||
readonly signatureIcon = computed(() => {
|
||||
const svg = (d: string) => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">${d}</svg>`;
|
||||
switch (this.current()?.signatureStatus) {
|
||||
case 'valid': return '✓';
|
||||
case 'invalid': return '✗';
|
||||
case 'missing': return '?';
|
||||
default: return '—';
|
||||
case 'valid': return svg('<polyline points="20 6 9 17 4 12"/>');
|
||||
case 'invalid': return svg('<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>');
|
||||
case 'missing': return svg('<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>');
|
||||
default: return svg('<line x1="5" y1="12" x2="19" y2="12"/>');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ import { StellaOpsScopes } from '../../../core/auth/scopes';
|
||||
<div class="modal-content" (click)="$event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<h2>Audit Event Details</h2>
|
||||
<button class="btn-close" (click)="closeDetails()">×</button>
|
||||
<button class="btn-close" (click)="closeDetails()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="detail-row">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user