From 4d8a48a05fe5eb1d0305a090defc70cd70d51a72 Mon Sep 17 00:00:00 2001
From: master <>
Date: Mon, 16 Mar 2026 18:27:46 +0200
Subject: [PATCH] Sprint 7+8: Journey UX fixes + identity envelope shared
middleware
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Sprint 7 — Deep journey fixes:
S7-T01: Trust & Signing empty state with "Go to Signing Keys" CTA
S7-T02: Notifications 3-step setup guide (channel→rule→test)
S7-T03: Topology validate step skip — "Skip Validation" when API fails,
with validateSkipped signal matching agentSkipped pattern
S7-T04: VEX export note on Risk Report tab linking to VEX Ledger
Sprint 8 — Identity envelope shared middleware (ARCHITECTURE):
S8-T01: New UseIdentityEnvelopeAuthentication() extension in
StellaOps.Router.AspNet. Reads X-StellaOps-Identity-Envelope headers,
verifies HMAC-SHA256 via GatewayIdentityEnvelopeCodec, creates
ClaimsPrincipal with sub/tenant/scopes/roles. 5min clock skew.
S8-T02: Concelier refactored — removed 78 lines of inline impl,
now uses shared one-liner
S8-T03: Scanner — UseIdentityEnvelopeAuthentication() added
S8-T04: JobEngine — UseIdentityEnvelopeAuthentication() added
S8-T05: Timeline — UseIdentityEnvelopeAuthentication() added
S8-T06: Integrations — UseIdentityEnvelopeAuthentication() added
S8-T07: docs/modules/router/IDENTITY_ENVELOPE_MIDDLEWARE.md
All services now authenticate ReverseProxy requests via gateway envelope.
Scanner scan submit should now work with authenticated identity.
Angular: 0 errors. .NET (6 services): 0 errors.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
...316_007_FE_deep_journey_remaining_fixes.md | 22 +++
...orm_identity_envelope_shared_middleware.md | 29 ++++
.../router/IDENTITY_ENVELOPE_MIDDLEWARE.md | 113 ++++++++++++++
.../StellaOps.Concelier.WebService/Program.cs | 143 +-----------------
.../Program.cs | 1 +
.../StellaOps.JobEngine.WebService/Program.cs | 1 +
.../IdentityEnvelopeMiddlewareExtensions.cs | 141 +++++++++++++++++
.../StellaOps.Scanner.WebService/Program.cs | 1 +
.../StellaOps.Timeline.WebService/Program.cs | 1 +
.../admin-notifications.component.ts | 37 +++++
.../topology-wizard.component.ts | 50 +++++-
.../topology-wizard.service.ts | 4 +-
.../security-reports-page.component.ts | 25 ++-
.../trust-admin/trust-overview.component.ts | 56 +++++++
14 files changed, 482 insertions(+), 142 deletions(-)
create mode 100644 docs/implplan/SPRINT_20260316_007_FE_deep_journey_remaining_fixes.md
create mode 100644 docs/implplan/SPRINT_20260316_008_Platform_identity_envelope_shared_middleware.md
create mode 100644 docs/modules/router/IDENTITY_ENVELOPE_MIDDLEWARE.md
create mode 100644 src/Router/__Libraries/StellaOps.Router.AspNet/IdentityEnvelopeMiddlewareExtensions.cs
diff --git a/docs/implplan/SPRINT_20260316_007_FE_deep_journey_remaining_fixes.md b/docs/implplan/SPRINT_20260316_007_FE_deep_journey_remaining_fixes.md
new file mode 100644
index 000000000..fc33368e8
--- /dev/null
+++ b/docs/implplan/SPRINT_20260316_007_FE_deep_journey_remaining_fixes.md
@@ -0,0 +1,22 @@
+# Sprint 20260316-007 — Deep Journey Remaining Fixes
+
+## Topic & Scope
+- Fix the 4 remaining UX issues found during deep journey testing (J-05 through J-08).
+- Trust & Signing empty state, Notifications empty state, Topology validate skip, VEX export visibility.
+- Working directory: `src/Web/StellaOps.Web/`.
+
+## Delivery Tracker
+
+### S7-T01 - Trust & Signing empty state guidance
+Status: TODO
+### S7-T02 - Notifications empty state guidance
+Status: TODO
+### S7-T03 - Topology wizard validate step skip
+Status: TODO
+### S7-T04 - VEX export button visibility
+Status: TODO
+
+## Execution Log
+| Date (UTC) | Update | Owner |
+| --- | --- | --- |
+| 2026-03-16 | Sprint created from deep journey findings J-05 to J-08. | Developer |
diff --git a/docs/implplan/SPRINT_20260316_008_Platform_identity_envelope_shared_middleware.md b/docs/implplan/SPRINT_20260316_008_Platform_identity_envelope_shared_middleware.md
new file mode 100644
index 000000000..dceafd2e2
--- /dev/null
+++ b/docs/implplan/SPRINT_20260316_008_Platform_identity_envelope_shared_middleware.md
@@ -0,0 +1,29 @@
+# Sprint 20260316-008 — Identity Envelope Shared Middleware (Architecture)
+
+## Topic & Scope
+- Extract identity envelope pre-auth middleware from Concelier into shared `StellaOps.Router.AspNet` extension.
+- Apply to all 5 services: Concelier (refactor), Scanner, JobEngine, Timeline, Integrations.
+- This unblocks scan submit (J-04) and all future ReverseProxy-routed authenticated endpoints.
+- Working directory: `src/Router/__Libraries/StellaOps.Router.AspNet/`, `src/*/Program.cs`.
+
+## Delivery Tracker
+
+### S8-T01 - Create shared middleware extension
+Status: TODO
+### S8-T02 - Refactor Concelier to use shared extension
+Status: TODO
+### S8-T03 - Add to Scanner
+Status: TODO
+### S8-T04 - Add to JobEngine
+Status: TODO
+### S8-T05 - Add to Timeline
+Status: TODO
+### S8-T06 - Add to Integrations
+Status: TODO
+### S8-T07 - Document the pattern
+Status: TODO
+
+## Execution Log
+| Date (UTC) | Update | Owner |
+| --- | --- | --- |
+| 2026-03-16 | Sprint created from architecture finding. | Developer |
diff --git a/docs/modules/router/IDENTITY_ENVELOPE_MIDDLEWARE.md b/docs/modules/router/IDENTITY_ENVELOPE_MIDDLEWARE.md
new file mode 100644
index 000000000..42d8bbe6a
--- /dev/null
+++ b/docs/modules/router/IDENTITY_ENVELOPE_MIDDLEWARE.md
@@ -0,0 +1,113 @@
+# Identity Envelope Middleware
+
+## What
+
+`UseIdentityEnvelopeAuthentication()` is a shared ASP.NET Core middleware that reads
+gateway-signed identity envelope headers and hydrates `HttpContext.User` with the
+enclosed claims. It lives in `StellaOps.Router.AspNet` and is consumed by every
+downstream microservice that receives proxied requests from the Router gateway.
+
+## Why
+
+When the Router gateway forwards a request via YARP ReverseProxy, it strips the
+original `Authorization: Bearer ` header. Instead, it signs the authenticated
+user's identity into two custom headers:
+
+- `X-StellaOps-Identity-Envelope` -- Base64URL-encoded JSON payload
+- `X-StellaOps-Identity-Envelope-Signature` -- HMAC-SHA256 signature
+
+Downstream services need a way to recover the caller's identity from these headers
+without each service re-implementing the same verification logic. The shared
+middleware eliminates duplication and ensures consistent claim mapping everywhere.
+
+## How to use
+
+In the service's `Program.cs`, register the middleware **before** `UseAuthentication()`:
+
+```csharp
+using StellaOps.Router.AspNet;
+
+// ...
+
+app.UseIdentityEnvelopeAuthentication(); // <-- must come first
+app.UseAuthentication();
+app.UseAuthorization();
+```
+
+The service's `.csproj` must reference `StellaOps.Router.AspNet` (which transitively
+brings in `StellaOps.Router.Common` containing the codec).
+
+## Configuration
+
+The middleware reads the HMAC-SHA256 signing key from one of two sources (first wins):
+
+| Source | Key |
+|---|---|
+| `IConfiguration` | `Router:IdentityEnvelopeSigningKey` |
+| Environment variable | `STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY` |
+
+In the docker-compose environment the key is injected via `x-router-microservice-defaults`
+so all services share the same value automatically.
+
+## Claim mapping
+
+When a valid, non-expired envelope is verified, the middleware creates a
+`ClaimsPrincipal` with authentication type `StellaRouterEnvelope` and the following
+claims:
+
+| Claim type | Source |
+|---|---|
+| `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` | `envelope.Subject` |
+| `sub` | `envelope.Subject` |
+| `stellaops:tenant` | `envelope.Tenant` |
+| `tenant` | `envelope.Tenant` |
+| `stellaops:project` | `envelope.Project` |
+| `project` | `envelope.Project` |
+| `scope` (one per scope) | `envelope.Scopes` |
+| `http://schemas.microsoft.com/ws/2008/06/identity/claims/role` (one per role) | `envelope.Roles` |
+
+## Architecture flow
+
+```
+Browser
+ |
+ | Authorization: Bearer
+ v
+Gateway (StellaOps.Gateway.WebService)
+ | 1. Validates JWT
+ | 2. Signs identity into envelope headers (HMAC-SHA256)
+ | 3. Strips Authorization header
+ | 4. Forwards via YARP ReverseProxy
+ v
+Downstream Service (Scanner, JobEngine, Timeline, Integrations, Concelier, ...)
+ | UseIdentityEnvelopeAuthentication()
+ | 1. Reads X-StellaOps-Identity-Envelope + Signature headers
+ | 2. Verifies HMAC-SHA256 signature
+ | 3. Checks clock skew (5 min tolerance)
+ | 4. Hydrates HttpContext.User
+ v
+ UseAuthentication() / UseAuthorization()
+ (standard ASP.NET pipeline continues with the hydrated principal)
+```
+
+## Error handling
+
+The middleware never throws. All failures (missing headers, missing key, bad signature,
+expired envelope) are logged at Warning level under the `StellaOps.IdentityEnvelope`
+logger category. The request continues unauthenticated, allowing the standard JWT
+bearer handler to attempt authentication normally.
+
+## Adopted services
+
+| Service | File |
+|---|---|
+| Concelier | `src/Concelier/StellaOps.Concelier.WebService/Program.cs` |
+| Scanner | `src/Scanner/StellaOps.Scanner.WebService/Program.cs` |
+| JobEngine | `src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Program.cs` |
+| Timeline | `src/Timeline/StellaOps.Timeline.WebService/Program.cs` |
+| Integrations | `src/Integrations/StellaOps.Integrations.WebService/Program.cs` |
+
+## Source
+
+- Middleware extension: `src/Router/__Libraries/StellaOps.Router.AspNet/IdentityEnvelopeMiddlewareExtensions.cs`
+- Envelope model and codec: `src/Router/__Libraries/StellaOps.Router.Common/Identity/GatewayIdentityEnvelope.cs`
diff --git a/src/Concelier/StellaOps.Concelier.WebService/Program.cs b/src/Concelier/StellaOps.Concelier.WebService/Program.cs
index 8e12231ed..d90e13571 100644
--- a/src/Concelier/StellaOps.Concelier.WebService/Program.cs
+++ b/src/Concelier/StellaOps.Concelier.WebService/Program.cs
@@ -62,7 +62,6 @@ using StellaOps.Plugin.Hosting;
using StellaOps.Provenance;
using StellaOps.Localization;
using StellaOps.Router.AspNet;
-using StellaOps.Router.Common.Identity;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -786,40 +785,8 @@ if (authorityConfigured)
}
});
- // Add identity envelope fallback for ReverseProxy routes (OIDC branch).
- // When the gateway proxies a request via ReverseProxy, it strips the Authorization header
- // and attaches signed X-StellaOps-Identity-Envelope headers. This handler reads those
- // headers and converts them to a ClaimsPrincipal when no JWT token is present.
- builder.Services.PostConfigure(
- StellaOpsAuthenticationDefaults.AuthenticationScheme,
- jwtOptions =>
- {
- var existingOnMessageReceived = jwtOptions.Events?.OnMessageReceived;
- jwtOptions.Events ??= new JwtBearerEvents();
- jwtOptions.Events.OnMessageReceived = async context =>
- {
- if (existingOnMessageReceived is not null)
- {
- await existingOnMessageReceived(context);
- }
-
- // If JWT handler already found a token or succeeded, skip envelope check
- if (!string.IsNullOrWhiteSpace(context.Token) || context.Result?.Succeeded == true)
- {
- return;
- }
-
- var logger = context.HttpContext.RequestServices
- .GetRequiredService()
- .CreateLogger("Concelier.IdentityEnvelope");
-
- if (TryAuthenticateFromIdentityEnvelope(context.HttpContext, logger))
- {
- context.Principal = context.HttpContext.User;
- context.Success();
- }
- };
- });
+ // Identity envelope authentication is now handled by the shared middleware
+ // registered via app.UseIdentityEnvelopeAuthentication() in the pipeline.
}
else
{
@@ -882,12 +849,11 @@ if (authorityConfigured)
if (string.IsNullOrWhiteSpace(token))
{
- // No JWT token — check for gateway identity envelope (ReverseProxy passthrough)
- if (TryAuthenticateFromIdentityEnvelope(context.HttpContext, logger))
+ // No JWT token — check if the shared identity envelope middleware
+ // (UseIdentityEnvelopeAuthentication) already authenticated from
+ // gateway-signed envelope headers (ReverseProxy passthrough).
+ if (context.HttpContext.User?.Identity?.IsAuthenticated == true)
{
- // Envelope authentication succeeded — set the principal and mark as handled.
- // Use context.Principal + context.Success() so the JwtBearer handler
- // reports success without trying to validate a JWT.
context.Principal = context.HttpContext.User;
context.Success();
return Task.CompletedTask;
@@ -1045,17 +1011,7 @@ if (authorityConfigured)
{
// Identity envelope middleware: authenticate ReverseProxy requests from the gateway.
// Must run BEFORE UseAuthentication so the principal is set before JwtBearer evaluates.
- app.Use(async (context, next) =>
- {
- if (context.User?.Identity?.IsAuthenticated != true)
- {
- var envelopeLogger = context.RequestServices
- .GetRequiredService()
- .CreateLogger("Concelier.IdentityEnvelope");
- TryAuthenticateFromIdentityEnvelope(context, envelopeLogger);
- }
- await next();
- });
+ app.UseIdentityEnvelopeAuthentication();
app.UseAuthentication();
@@ -3663,91 +3619,6 @@ static (Advisory Advisory, ImmutableArray Aliases, string Fingerprint) C
return (advisory, aliases, fingerprint);
}
-///
-/// Attempts to authenticate an HTTP request using the gateway's identity envelope headers.
-/// Used for ReverseProxy routes where the gateway strips the JWT Authorization header and
-/// attaches a signed identity envelope instead.
-///
-static bool TryAuthenticateFromIdentityEnvelope(HttpContext httpContext, Microsoft.Extensions.Logging.ILogger logger)
-{
- var headers = httpContext.Request.Headers;
-
- if (!headers.TryGetValue("X-StellaOps-Identity-Envelope", out var envelopePayload) ||
- !headers.TryGetValue("X-StellaOps-Identity-Envelope-Signature", out var envelopeSignature))
- {
- return false;
- }
-
- var config = httpContext.RequestServices.GetService();
- var signingKey = config?.GetValue("Router:IdentityEnvelopeSigningKey")
- ?? Environment.GetEnvironmentVariable("STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY");
-
- if (string.IsNullOrWhiteSpace(signingKey))
- {
- logger.LogWarning("Identity envelope received but signing key not configured");
- return false;
- }
-
- if (!GatewayIdentityEnvelopeCodec.TryVerify(
- envelopePayload.ToString(),
- envelopeSignature.ToString(),
- signingKey,
- out var envelope) || envelope is null)
- {
- logger.LogWarning("Identity envelope signature verification failed for {Path}", httpContext.Request.Path);
- return false;
- }
-
- var now = DateTimeOffset.UtcNow;
- var skew = TimeSpan.FromMinutes(5);
-
- if (envelope.IssuedAtUtc - skew > now || envelope.ExpiresAtUtc + skew < now)
- {
- logger.LogWarning("Identity envelope expired or not yet valid for {Path}", httpContext.Request.Path);
- return false;
- }
-
- var claims = new List
- {
- new(ClaimTypes.NameIdentifier, envelope.Subject),
- new("sub", envelope.Subject)
- };
-
- if (!string.IsNullOrWhiteSpace(envelope.Tenant))
- {
- claims.Add(new Claim("stellaops:tenant", envelope.Tenant));
- claims.Add(new Claim("tenant", envelope.Tenant));
- }
-
- if (!string.IsNullOrWhiteSpace(envelope.Project))
- {
- claims.Add(new Claim("stellaops:project", envelope.Project));
- claims.Add(new Claim("project", envelope.Project));
- }
-
- foreach (var scope in envelope.Scopes.Where(s => !string.IsNullOrWhiteSpace(s)))
- {
- claims.Add(new Claim("scope", scope));
- }
-
- foreach (var role in envelope.Roles.Where(r => !string.IsNullOrWhiteSpace(r)))
- {
- claims.Add(new Claim(ClaimTypes.Role, role));
- }
-
- httpContext.User = new ClaimsPrincipal(
- new ClaimsIdentity(
- claims,
- authenticationType: "StellaRouterEnvelope",
- nameType: ClaimTypes.NameIdentifier,
- roleType: ClaimTypes.Role));
-
- logger.LogInformation("Authenticated via identity envelope for {Path}: subject={Subject} tenant={Tenant} scopes={Scopes}",
- httpContext.Request.Path, envelope.Subject, envelope.Tenant, string.Join(' ', envelope.Scopes.Take(5)));
-
- return true;
-}
-
static ImmutableArray BuildAliasQuery(Advisory advisory)
{
var set = new HashSet(StringComparer.OrdinalIgnoreCase);
diff --git a/src/Integrations/StellaOps.Integrations.WebService/Program.cs b/src/Integrations/StellaOps.Integrations.WebService/Program.cs
index a12dc9e8b..87ec60dde 100644
--- a/src/Integrations/StellaOps.Integrations.WebService/Program.cs
+++ b/src/Integrations/StellaOps.Integrations.WebService/Program.cs
@@ -118,6 +118,7 @@ if (app.Environment.IsDevelopment())
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
+app.UseIdentityEnvelopeAuthentication();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Program.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Program.cs
index 7be4a5d64..5a9b78a15 100644
--- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Program.cs
+++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Program.cs
@@ -149,6 +149,7 @@ if (app.Environment.IsDevelopment())
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
+app.UseIdentityEnvelopeAuthentication();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
diff --git a/src/Router/__Libraries/StellaOps.Router.AspNet/IdentityEnvelopeMiddlewareExtensions.cs b/src/Router/__Libraries/StellaOps.Router.AspNet/IdentityEnvelopeMiddlewareExtensions.cs
new file mode 100644
index 000000000..fb4c497cd
--- /dev/null
+++ b/src/Router/__Libraries/StellaOps.Router.AspNet/IdentityEnvelopeMiddlewareExtensions.cs
@@ -0,0 +1,141 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using StellaOps.Router.Common.Identity;
+
+namespace StellaOps.Router.AspNet;
+
+///
+/// Provides middleware that authenticates incoming requests using the gateway-signed
+/// identity envelope headers. When the Router gateway proxies a request via ReverseProxy,
+/// it strips the Authorization header and attaches X-StellaOps-Identity-Envelope
+/// and X-StellaOps-Identity-Envelope-Signature headers containing a signed
+/// . This middleware verifies the HMAC-SHA256
+/// signature and hydrates with the envelope claims.
+///
+public static class IdentityEnvelopeMiddlewareExtensions
+{
+ private const string EnvelopeHeader = "X-StellaOps-Identity-Envelope";
+ private const string SignatureHeader = "X-StellaOps-Identity-Envelope-Signature";
+ private const string LogCategory = "StellaOps.IdentityEnvelope";
+ private const string AuthenticationType = "StellaRouterEnvelope";
+
+ ///
+ /// Adds identity envelope authentication middleware to the pipeline.
+ /// This must be called before app.UseAuthentication() so the
+ /// is available when the JWT bearer handler runs.
+ ///
+ ///
+ /// The middleware never throws. All errors are logged and the request continues
+ /// unauthenticated, allowing the standard authentication pipeline to handle it.
+ ///
+ public static IApplicationBuilder UseIdentityEnvelopeAuthentication(this IApplicationBuilder app)
+ {
+ return app.Use(async (context, next) =>
+ {
+ if (context.User?.Identity?.IsAuthenticated != true)
+ {
+ TryAuthenticateFromIdentityEnvelope(context);
+ }
+
+ await next();
+ });
+ }
+
+ private static void TryAuthenticateFromIdentityEnvelope(HttpContext httpContext)
+ {
+ var logger = httpContext.RequestServices
+ .GetRequiredService()
+ .CreateLogger(LogCategory);
+
+ try
+ {
+ var headers = httpContext.Request.Headers;
+
+ if (!headers.TryGetValue(EnvelopeHeader, out var envelopePayload) ||
+ !headers.TryGetValue(SignatureHeader, out var envelopeSignature))
+ {
+ return;
+ }
+
+ var config = httpContext.RequestServices.GetService();
+ var signingKey = config?.GetValue("Router:IdentityEnvelopeSigningKey")
+ ?? Environment.GetEnvironmentVariable("STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY");
+
+ if (string.IsNullOrWhiteSpace(signingKey))
+ {
+ logger.LogWarning("Identity envelope received but signing key not configured");
+ return;
+ }
+
+ if (!GatewayIdentityEnvelopeCodec.TryVerify(
+ envelopePayload.ToString(),
+ envelopeSignature.ToString(),
+ signingKey,
+ out var envelope) || envelope is null)
+ {
+ logger.LogWarning("Identity envelope signature verification failed for {Path}", httpContext.Request.Path);
+ return;
+ }
+
+ var now = DateTimeOffset.UtcNow;
+ var skew = TimeSpan.FromMinutes(5);
+
+ if (envelope.IssuedAtUtc - skew > now || envelope.ExpiresAtUtc + skew < now)
+ {
+ logger.LogWarning("Identity envelope expired or not yet valid for {Path}", httpContext.Request.Path);
+ return;
+ }
+
+ var claims = new List
+ {
+ new(ClaimTypes.NameIdentifier, envelope.Subject),
+ new("sub", envelope.Subject)
+ };
+
+ if (!string.IsNullOrWhiteSpace(envelope.Tenant))
+ {
+ claims.Add(new Claim("stellaops:tenant", envelope.Tenant));
+ claims.Add(new Claim("tenant", envelope.Tenant));
+ }
+
+ if (!string.IsNullOrWhiteSpace(envelope.Project))
+ {
+ claims.Add(new Claim("stellaops:project", envelope.Project));
+ claims.Add(new Claim("project", envelope.Project));
+ }
+
+ foreach (var scope in envelope.Scopes.Where(s => !string.IsNullOrWhiteSpace(s)))
+ {
+ claims.Add(new Claim("scope", scope));
+ }
+
+ foreach (var role in envelope.Roles.Where(r => !string.IsNullOrWhiteSpace(r)))
+ {
+ claims.Add(new Claim(ClaimTypes.Role, role));
+ }
+
+ httpContext.User = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ claims,
+ authenticationType: AuthenticationType,
+ nameType: ClaimTypes.NameIdentifier,
+ roleType: ClaimTypes.Role));
+
+ logger.LogInformation(
+ "Authenticated via identity envelope for {Path}: subject={Subject} tenant={Tenant} scopes={Scopes}",
+ httpContext.Request.Path,
+ envelope.Subject,
+ envelope.Tenant,
+ string.Join(' ', envelope.Scopes.Take(5)));
+ }
+ catch (Exception ex)
+ {
+ // Never throw — all errors are logged and the request continues unauthenticated.
+ logger.LogError(ex, "Unexpected error processing identity envelope for {Path}", httpContext.Request.Path);
+ }
+ }
+}
diff --git a/src/Scanner/StellaOps.Scanner.WebService/Program.cs b/src/Scanner/StellaOps.Scanner.WebService/Program.cs
index 493a3f66a..9568ac484 100644
--- a/src/Scanner/StellaOps.Scanner.WebService/Program.cs
+++ b/src/Scanner/StellaOps.Scanner.WebService/Program.cs
@@ -750,6 +750,7 @@ app.UseExceptionHandler(errorApp =>
// Even in anonymous mode, endpoints use RequireAuthorization() which needs the middleware
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
+app.UseIdentityEnvelopeAuthentication();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
diff --git a/src/Timeline/StellaOps.Timeline.WebService/Program.cs b/src/Timeline/StellaOps.Timeline.WebService/Program.cs
index 8eb0da3c3..3283cdfcb 100644
--- a/src/Timeline/StellaOps.Timeline.WebService/Program.cs
+++ b/src/Timeline/StellaOps.Timeline.WebService/Program.cs
@@ -105,6 +105,7 @@ if (app.Environment.IsDevelopment())
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
+app.UseIdentityEnvelopeAuthentication();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
diff --git a/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.component.ts b/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.component.ts
index 5cbddbed4..63a132f30 100644
--- a/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/admin-notifications/admin-notifications.component.ts
@@ -84,6 +84,28 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
+
+ @if (!loading() && channels().length === 0 && rules().length === 0) {
+
+
Get started with notifications
+
Follow these three steps to start receiving alerts for findings, gate decisions, and freshness events.
+
+ -
+ Create a channel (Slack, webhook, or email)
+
+
+ -
+ Create a notification rule (trigger on findings, gates, or freshness)
+
+
+ -
+ Test your channel to verify delivery
+
Go to Simulator
+
+
+
+ }
+
} @else {
-
Validation has not been run yet.
-
+ @if (validationFailed()) {
+
Validation requires deployed infrastructure. You can skip validation and complete setup.
+
+
+
+
+ } @else {
+
Validation has not been run yet.
+
+ }
+
+ }
+
+ @if (wizard.validateSkipped()) {
+
+ Validation was skipped. You can run it later from the topology dashboard.
}
@@ -848,6 +866,23 @@ import {
}
}
+ .empty-state__actions {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: center;
+ }
+
+ .skip-notice {
+ margin-top: 1rem;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--color-status-warning-border, #e6c200);
+ border-radius: var(--radius-md);
+ background: var(--color-status-warning-bg, #fff8e1);
+ color: var(--color-status-warning-text, #7a6100);
+ font-size: 0.82rem;
+ text-align: center;
+ }
+
.skip-agent-link {
margin-top: 1rem;
text-align: center;
@@ -1379,6 +1414,7 @@ export class TopologyWizardComponent implements OnInit, OnDestroy {
readonly environmentsLoading = signal(false);
readonly bindingsLoading = signal(false);
readonly validationLoading = signal(false);
+ readonly validationFailed = signal(false);
// Region creation form
readonly newRegionName = signal('');
@@ -1591,6 +1627,7 @@ export class TopologyWizardComponent implements OnInit, OnDestroy {
if (!targetId) return;
this.validationLoading.set(true);
+ this.validationFailed.set(false);
const sub = this.wizard.validateTarget(targetId).subscribe({
next: (report) => {
this.wizard.readinessReport.set(report);
@@ -1598,9 +1635,14 @@ export class TopologyWizardComponent implements OnInit, OnDestroy {
},
error: (err: unknown) => {
this.wizard.error.set(err instanceof Error ? err.message : 'Validation failed.');
+ this.validationFailed.set(true);
this.validationLoading.set(false);
},
});
this.subscriptions.push(sub);
}
+
+ skipValidation(): void {
+ this.wizard.validateSkipped.set(true);
+ }
}
diff --git a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts
index 8073744d5..1dbf99369 100644
--- a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts
+++ b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts
@@ -89,6 +89,7 @@ export class TopologyWizardService {
readonly createdTarget = signal
(null);
readonly selectedAgent = signal(null);
readonly agentSkipped = signal(false);
+ readonly validateSkipped = signal(false);
readonly resolvedBindings = signal(null);
readonly readinessReport = signal(null);
readonly loading = signal(false);
@@ -107,7 +108,7 @@ export class TopologyWizardService {
case 'target': return this.createdTarget() !== null;
case 'agent': return this.selectedAgent() !== null || this.agentSkipped();
case 'infrastructure': return true;
- case 'validate': return this.readinessReport()?.isReady === true;
+ case 'validate': return this.readinessReport()?.isReady === true || this.validateSkipped();
default: return false;
}
});
@@ -144,6 +145,7 @@ export class TopologyWizardService {
this.createdTarget.set(null);
this.selectedAgent.set(null);
this.agentSkipped.set(false);
+ this.validateSkipped.set(false);
this.resolvedBindings.set(null);
this.readinessReport.set(null);
this.error.set(null);
diff --git a/src/Web/StellaOps.Web/src/app/features/security/security-reports-page.component.ts b/src/Web/StellaOps.Web/src/app/features/security/security-reports-page.component.ts
index abb99f45f..90ab1ba41 100644
--- a/src/Web/StellaOps.Web/src/app/features/security/security-reports-page.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/security/security-reports-page.component.ts
@@ -96,6 +96,10 @@ interface PlatformListResponse {
Generate PDF
+
+ VEX decision exports are available on the
+ VEX Ledger tab.
+
}
@@ -241,8 +245,27 @@ interface PlatformListResponse {
opacity: 0.9;
}
+ .vex-export-note {
+ margin: 0;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.78rem;
+ color: var(--color-text-secondary);
+ border-bottom: 1px solid var(--color-border-primary);
+ }
+
+ .vex-export-note__link {
+ color: var(--color-brand-primary);
+ cursor: pointer;
+ text-decoration: underline;
+ font-weight: 500;
+ }
+
+ .vex-export-note__link:hover {
+ opacity: 0.85;
+ }
+
@media print {
- .tabs, .export-toolbar, .evidence-explainer, .export-center-link {
+ .tabs, .export-toolbar, .evidence-explainer, .export-center-link, .vex-export-note {
display: none !important;
}
}
diff --git a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-overview.component.ts b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-overview.component.ts
index 54c2f0ec8..1cbc8dbdf 100644
--- a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-overview.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-overview.component.ts
@@ -16,6 +16,15 @@ import { RouterLink } from '@angular/router';
+
+
+
No signing keys configured
+
Generate a signing key to enable attestation signing on releases.
+
Go to Signing Keys
+
+
Signing Keys
@@ -98,6 +107,53 @@ import { RouterLink } from '@angular/router';
text-decoration: none;
font-weight: var(--font-weight-medium);
}
+
+ .trust-overview__empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 2rem 1.5rem;
+ border: 1px dashed var(--color-border-primary);
+ border-radius: var(--radius-lg);
+ background: var(--color-surface-secondary);
+ }
+
+ .trust-overview__empty-icon {
+ color: var(--color-text-secondary);
+ margin-bottom: 0.75rem;
+ }
+
+ .trust-overview__empty-title {
+ margin: 0 0 0.35rem;
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ }
+
+ .trust-overview__empty-desc {
+ margin: 0 0 0.85rem;
+ color: var(--color-text-secondary);
+ font-size: 0.875rem;
+ line-height: 1.5;
+ }
+
+ .trust-overview__empty-action {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.45rem 1rem;
+ background: var(--color-brand-primary);
+ color: #fff;
+ border-radius: var(--radius-md);
+ font-size: 0.85rem;
+ font-weight: var(--font-weight-medium);
+ text-decoration: none;
+ transition: opacity 0.15s;
+ }
+
+ .trust-overview__empty-action:hover {
+ opacity: 0.9;
+ }
`],
})
export class TrustOverviewComponent {}