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.

+
    +
  1. + Create a channel (Slack, webhook, or email) +
    +
  2. +
  3. + Create a notification rule (trigger on findings, gates, or freshness) +
    +
  4. +
  5. + Test your channel to verify delivery +
    Go to Simulator +
  6. +
+
+ } +
} @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 {}