Sprint 7+8: Journey UX fixes + identity envelope shared middleware

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) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-16 18:27:46 +02:00
parent 1acc87a25d
commit 4d8a48a05f
14 changed files with 482 additions and 142 deletions

View File

@@ -84,6 +84,28 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
</div>
</div>
<!-- Setup Guidance (shown when no channels and no rules exist) -->
@if (!loading() && channels().length === 0 && rules().length === 0) {
<div class="setup-guidance">
<h2 class="setup-guidance__title">Get started with notifications</h2>
<p class="setup-guidance__desc">Follow these three steps to start receiving alerts for findings, gate decisions, and freshness events.</p>
<ol class="setup-guidance__steps">
<li>
<strong>Create a channel</strong> (Slack, webhook, or email)
<br><button class="btn-link" (click)="activeTab.set('channels')">Go to Channels tab</button>
</li>
<li>
<strong>Create a notification rule</strong> (trigger on findings, gates, or freshness)
<br><button class="btn-link" (click)="activeTab.set('rules')">Go to Rules tab</button>
</li>
<li>
<strong>Test your channel</strong> to verify delivery
<br><a routerLink="simulator" class="btn-link">Go to Simulator</a>
</li>
</ol>
</div>
}
<!-- Tabs -->
<div class="tabs">
<button class="tab" [class.active]="activeTab() === 'channels'" (click)="activeTab.set('channels')">
@@ -518,6 +540,21 @@ type NotifyAdminTab = 'channels' | 'rules' | 'templates' | 'deliveries' | 'incid
.status-acknowledged { background: var(--color-status-warning-bg); color: var(--color-status-warning); }
.status-resolved { background: var(--color-status-success-border); color: var(--color-status-success-text); }
.setup-guidance {
margin-bottom: 1.5rem;
padding: 1.25rem 1.5rem;
border: 1px dashed var(--color-border-primary);
border-radius: var(--radius-lg);
background: var(--color-surface-secondary);
}
.setup-guidance__title { margin: 0 0 0.35rem; font-size: 1.1rem; }
.setup-guidance__desc { margin: 0 0 1rem; color: var(--color-text-secondary); font-size: 0.875rem; }
.setup-guidance__steps { margin: 0; padding-left: 1.25rem; display: grid; gap: 0.85rem; }
.setup-guidance__steps li { line-height: 1.6; }
.setup-guidance__steps strong { color: var(--color-text-primary); }
.btn-link { background: none; border: none; color: var(--color-status-info-text); cursor: pointer; padding: 0; font-size: 0.85rem; font-weight: var(--font-weight-semibold); text-decoration: underline; }
a.btn-link { text-decoration: underline; }
.empty-state { text-align: center; padding: 3rem; color: var(--color-text-secondary); }
.loading { text-align: center; padding: 2rem; color: var(--color-text-secondary); }
.error-banner { background: var(--color-status-error-bg); color: var(--color-status-error-text); padding: 1rem; border-radius: var(--radius-sm); margin-top: 1rem; }

View File

@@ -546,10 +546,28 @@ import {
</button>
} @else {
<div class="empty-state">
<p>Validation has not been run yet.</p>
<button type="button" class="btn btn--primary btn--sm" (click)="runValidation()">
Run Validation
</button>
@if (validationFailed()) {
<p>Validation requires deployed infrastructure. You can skip validation and complete setup.</p>
<div class="empty-state__actions">
<button type="button" class="btn btn--primary btn--sm" (click)="runValidation()">
Retry Validation
</button>
<button type="button" class="btn btn--secondary btn--sm" (click)="skipValidation()">
Skip Validation
</button>
</div>
} @else {
<p>Validation has not been run yet.</p>
<button type="button" class="btn btn--primary btn--sm" (click)="runValidation()">
Run Validation
</button>
}
</div>
}
@if (wizard.validateSkipped()) {
<div class="skip-notice">
Validation was skipped. You can run it later from the topology dashboard.
</div>
}
</section>
@@ -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);
}
}

View File

@@ -89,6 +89,7 @@ export class TopologyWizardService {
readonly createdTarget = signal<Target | null>(null);
readonly selectedAgent = signal<Agent | null>(null);
readonly agentSkipped = signal(false);
readonly validateSkipped = signal(false);
readonly resolvedBindings = signal<ResolvedBindings | null>(null);
readonly readinessReport = signal<ReadinessReport | null>(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);

View File

@@ -96,6 +96,10 @@ interface PlatformListResponse<T> {
Generate PDF
</button>
</div>
<p class="vex-export-note">
VEX decision exports are available on the
<a class="vex-export-note__link" (click)="activeTab.set('vex')">VEX Ledger tab</a>.
</p>
<app-security-risk-overview></app-security-risk-overview>
</section>
}
@@ -241,8 +245,27 @@ interface PlatformListResponse<T> {
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;
}
}

View File

@@ -16,6 +16,15 @@ import { RouterLink } from '@angular/router';
</p>
</div>
<div class="trust-overview__empty-state">
<div class="trust-overview__empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
</div>
<p class="trust-overview__empty-title">No signing keys configured</p>
<p class="trust-overview__empty-desc">Generate a signing key to enable attestation signing on releases.</p>
<a routerLink="keys" class="trust-overview__empty-action">Go to Signing Keys</a>
</div>
<div class="trust-overview__grid">
<article class="trust-overview__card">
<h3>Signing Keys</h3>
@@ -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 {}