diff --git a/devops/compose/router-gateway-local.json b/devops/compose/router-gateway-local.json
index 109bb03e8..9db847370 100644
--- a/devops/compose/router-gateway-local.json
+++ b/devops/compose/router-gateway-local.json
@@ -54,6 +54,8 @@
{ "Type": "ReverseProxy", "Path": "^/api/v1/environments/(.*)/readiness(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/environments/$1/readiness$2", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "^/api/v1/environments(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/environments$1", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "^/api/v1/agents(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/agents$1", "PreserveAuthHeaders": true },
+ { "Type": "ReverseProxy", "Path": "^/api/v1/scans(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scans$1", "PreserveAuthHeaders": true },
+ { "Type": "ReverseProxy", "Path": "^/api/v1/scan-policies(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scan-policies$1", "PreserveAuthHeaders": true },
{ "Type": "Microservice", "Path": "^/api/v1/vulnerabilities(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities$1" },
{ "Type": "Microservice", "Path": "^/api/v1/watchlist(.*)", "IsRegex": true, "TranslatesTo": "http://attestor.stella-ops.local/api/v1/watchlist$1" },
{ "Type": "Microservice", "Path": "^/api/v1/triage(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage$1" },
diff --git a/docs/implplan/SPRINT_20260316_002_Scanner_entry_point_and_vulnerability_navigation.md b/docs/implplan/SPRINT_20260316_002_Scanner_entry_point_and_vulnerability_navigation.md
new file mode 100644
index 000000000..c9ed31f8a
--- /dev/null
+++ b/docs/implplan/SPRINT_20260316_002_Scanner_entry_point_and_vulnerability_navigation.md
@@ -0,0 +1,68 @@
+# Sprint 20260316-002 — Scanner Entry Point + Vulnerability Navigation
+
+## Topic & Scope
+- Make vulnerability scanning discoverable: add Scan Image page, scan policy system, sidebar/command palette entries, and Security Posture CTAs.
+- Rename Triage to Vulnerabilities in navigation for security engineer discoverability.
+- Working directory: `src/Web/StellaOps.Web/`, `devops/compose/`, `src/Router/StellaOps.Gateway.WebService/`.
+- Expected evidence: scan submit form works, policies CRUD, gateway routes verified, command palette indexes security terms, sidebar shows Vulnerabilities.
+
+## Dependencies & Concurrency
+- No upstream sprint dependencies. Independent of Sprint 2-6.
+- Scanner backend `POST /api/v1/scans/` already exists (ScanEndpoints.cs:41).
+
+## Documentation Prerequisites
+- `AGENTS.md`
+- `docs/qa/FULL_PRODUCT_DEEP_DIVE_20260316.md`
+- `src/Scanner/StellaOps.Scanner.WebService/Endpoints/ScanEndpoints.cs`
+
+## Delivery Tracker
+
+### S1-T01 - Add "Scan Image" to sidebar navigation
+Status: TODO
+Dependency: none
+Owners: Developer
+
+### S1-T02 - Create Scan Image page
+Status: TODO
+Dependency: S1-T01
+Owners: Developer
+
+### S1-T03 - Full scan policy system
+Status: TODO
+Dependency: S1-T02
+Owners: Developer
+
+### S1-T04 - Rename Triage to Vulnerabilities in sidebar
+Status: TODO
+Dependency: none
+Owners: Developer
+
+### S1-T05 - Add security terms to command palette
+Status: TODO
+Dependency: none
+Owners: Developer
+
+### S1-T06 - Add CTA buttons to Security Posture page
+Status: TODO
+Dependency: S1-T02
+Owners: Developer
+
+### S1-T07 - Gateway route for scanner scan endpoint
+Status: TODO
+Dependency: none
+Owners: Developer
+
+## Execution Log
+| Date (UTC) | Update | Owner |
+| --- | --- | --- |
+| 2026-03-16 | Sprint created from Product UX Overhaul plan. | Developer |
+
+## Decisions & Risks
+- Scanner endpoint already exists — this sprint is primarily frontend + gateway routing.
+- Scan policy backend may need new CRUD endpoints on Scanner or Platform service.
+- Webhook endpoint for auto-scan-on-push needs registry integration to support push notifications.
+
+## Next Checkpoints
+- Scan Image page submits successfully and shows SSE progress
+- Sidebar shows "Vulnerabilities" instead of "Triage"
+- Command palette returns results for "scan", "vulnerability", "CVE"
diff --git a/src/Router/StellaOps.Gateway.WebService/appsettings.json b/src/Router/StellaOps.Gateway.WebService/appsettings.json
index 67d69506d..3d516ed59 100644
--- a/src/Router/StellaOps.Gateway.WebService/appsettings.json
+++ b/src/Router/StellaOps.Gateway.WebService/appsettings.json
@@ -82,6 +82,8 @@
{ "Type": "Microservice", "Path": "^/api/v1/environments/(.*)/readiness(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/environments/$1/readiness$2" },
{ "Type": "ReverseProxy", "Path": "^/api/v1/environments(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/environments$1", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "^/api/v1/agents(.*)", "IsRegex": true, "TranslatesTo": "http://concelier.stella-ops.local/api/v1/agents$1", "PreserveAuthHeaders": true },
+ { "Type": "ReverseProxy", "Path": "^/api/v1/scans(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scans$1", "PreserveAuthHeaders": true },
+ { "Type": "ReverseProxy", "Path": "^/api/v1/scan-policies(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scan-policies$1", "PreserveAuthHeaders": true },
{ "Type": "Microservice", "Path": "^/api/v1/vulnerabilities(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities$1" },
{ "Type": "Microservice", "Path": "^/api/v1/watchlist(.*)", "IsRegex": true, "TranslatesTo": "http://attestor.stella-ops.local/api/v1/watchlist$1" },
{ "Type": "Microservice", "Path": "^/api/v1/triage(.*)", "IsRegex": true, "TranslatesTo": "http://scanner.stella-ops.local/api/v1/triage$1" },
diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts
index 6719aa782..1c6bcd8b9 100644
--- a/src/Web/StellaOps.Web/src/app/app.routes.ts
+++ b/src/Web/StellaOps.Web/src/app/app.routes.ts
@@ -133,9 +133,9 @@ export const routes: Routes = [
},
{
path: 'triage',
- title: 'Triage',
+ title: 'Vulnerabilities',
canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard],
- data: { breadcrumb: 'Triage' },
+ data: { breadcrumb: 'Vulnerabilities' },
loadChildren: () => import('./routes/triage.routes').then((m) => m.TRIAGE_ROUTES),
},
{
diff --git a/src/Web/StellaOps.Web/src/app/core/api/search.models.ts b/src/Web/StellaOps.Web/src/app/core/api/search.models.ts
index 0a2ade94a..c07a70fd1 100644
--- a/src/Web/StellaOps.Web/src/app/core/api/search.models.ts
+++ b/src/Web/StellaOps.Web/src/app/core/api/search.models.ts
@@ -218,6 +218,96 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [
icon: 'database',
keywords: ['seed', 'demo', 'data', 'populate', 'sample', 'mock'],
},
+ {
+ id: 'scan-image',
+ label: 'Scan Image',
+ shortcut: '>scan-image',
+ description: 'Scan a container image for vulnerabilities',
+ icon: 'scan',
+ route: '/triage/artifacts',
+ keywords: ['scan', 'image', 'container', 'vulnerability'],
+ },
+ {
+ id: 'view-vulnerabilities',
+ label: 'View Vulnerabilities',
+ shortcut: '>vulns',
+ description: 'Browse vulnerability triage queue',
+ icon: 'alert-triangle',
+ route: '/triage/artifacts',
+ keywords: ['vulnerabilities', 'vulns', 'triage', 'cve', 'security'],
+ },
+ {
+ id: 'search-cve',
+ label: 'Search CVE',
+ shortcut: '>cve',
+ description: 'Search for a specific CVE in triage artifacts',
+ icon: 'search',
+ route: '/triage/artifacts',
+ keywords: ['cve', 'search', 'vulnerability', 'advisory'],
+ },
+ {
+ id: 'view-findings',
+ label: 'View Findings',
+ shortcut: '>view-findings',
+ description: 'Navigate to security findings list',
+ icon: 'alert-triangle',
+ route: '/triage/artifacts',
+ keywords: ['findings', 'vulnerabilities', 'security', 'list'],
+ },
+ {
+ id: 'create-release',
+ label: 'Create Release',
+ shortcut: '>release',
+ description: 'Create a new release version',
+ icon: 'package',
+ route: '/releases/versions/new',
+ keywords: ['create', 'release', 'version', 'new', 'deploy'],
+ },
+ {
+ id: 'view-audit-log',
+ label: 'View Audit Log',
+ shortcut: '>audit',
+ description: 'Browse the evidence audit log',
+ icon: 'book-open',
+ route: '/evidence/audit-log',
+ keywords: ['audit', 'log', 'evidence', 'history', 'trail'],
+ },
+ {
+ id: 'run-diagnostics',
+ label: 'Run Diagnostics',
+ shortcut: '>diag',
+ description: 'Open the Doctor diagnostics dashboard',
+ icon: 'activity',
+ route: '/ops/operations/doctor',
+ keywords: ['diagnostics', 'doctor', 'health', 'check', 'run'],
+ },
+ {
+ id: 'configure-advisory-sources',
+ label: 'Configure Advisory Sources',
+ shortcut: '>advisory',
+ description: 'Manage advisory and VEX data sources',
+ icon: 'plug',
+ route: '/setup/integrations/advisory-vex-sources',
+ keywords: ['advisory', 'sources', 'vex', 'configure', 'integrations', 'feed'],
+ },
+ {
+ id: 'view-promotions',
+ label: 'View Promotions',
+ shortcut: '>promotions',
+ description: 'Browse environment promotions',
+ icon: 'git-merge',
+ route: '/releases/promotions',
+ keywords: ['promotions', 'promote', 'environment', 'deploy', 'release'],
+ },
+ {
+ id: 'check-policy-gates',
+ label: 'Check Policy Gates',
+ shortcut: '>gates',
+ description: 'Review policy gate status and results',
+ icon: 'shield',
+ route: '/ops/policy/gates',
+ keywords: ['policy', 'gates', 'check', 'governance', 'compliance'],
+ },
];
export function filterQuickActions(query: string, actions?: QuickAction[]): QuickAction[] {
diff --git a/src/Web/StellaOps.Web/src/app/features/scanner/scan-submit.component.ts b/src/Web/StellaOps.Web/src/app/features/scanner/scan-submit.component.ts
new file mode 100644
index 000000000..a22d3ddb5
--- /dev/null
+++ b/src/Web/StellaOps.Web/src/app/features/scanner/scan-submit.component.ts
@@ -0,0 +1,600 @@
+import {
+ Component,
+ ChangeDetectionStrategy,
+ inject,
+ signal,
+ computed,
+ DestroyRef,
+ OnDestroy,
+} from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+import { HttpClient } from '@angular/common/http';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { timer, switchMap, takeWhile, tap } from 'rxjs';
+
+import { AuthSessionStore } from '../../core/auth/auth-session.store';
+
+interface MetadataEntry {
+ key: string;
+ value: string;
+}
+
+interface ScanSubmitResponse {
+ id: string;
+ status: string;
+}
+
+interface ScanStatusResponse {
+ id: string;
+ status: string;
+ image?: string;
+ startedAt?: string;
+ completedAt?: string;
+ findingsCount?: number;
+}
+
+@Component({
+ selector: 'app-scan-submit',
+ standalone: true,
+ imports: [FormsModule, RouterModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+ @if (!scanId()) {
+
+ }
+
+
+ @if (scanId(); as id) {
+
+ Scan Progress
+
+
+
+ Scan ID
+ {{ id }}
+
+
+ Image
+ {{ imageRef }}
+
+
+ Status
+
+ {{ scanStatus() }}
+
+
+
+
+ @if (scanStatus() === 'completed') {
+
+ }
+
+ @if (scanStatus() === 'failed') {
+
+
+
Scan failed. Please check image reference and try again.
+
+ }
+
+
+
+ }
+
+ `,
+ styles: [`
+ .scan-submit {
+ display: grid;
+ gap: 1rem;
+ max-width: 720px;
+ margin: 0 auto;
+ }
+
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .page-header h1 {
+ margin: 0;
+ }
+
+ .page-header p {
+ margin: 0.2rem 0 0;
+ color: var(--color-text-secondary);
+ font-size: 0.8rem;
+ }
+
+ .form-card,
+ .progress-card {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: var(--color-surface-primary);
+ padding: 0.9rem;
+ display: grid;
+ gap: 0.75rem;
+ }
+
+ .form-card h2,
+ .progress-card h2 {
+ margin: 0;
+ font-size: 1rem;
+ }
+
+ .form-field {
+ display: grid;
+ gap: 0.25rem;
+ font-size: 0.78rem;
+ color: var(--color-text-secondary);
+ }
+
+ input[type="text"],
+ textarea {
+ width: 100%;
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-sm);
+ background: var(--color-surface-primary);
+ color: var(--color-text-primary);
+ padding: 0.4rem 0.5rem;
+ font-size: 0.8rem;
+ font-family: inherit;
+ }
+
+ .checkbox-row {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.45rem;
+ color: var(--color-text-primary);
+ font-size: 0.8rem;
+ }
+
+ .checkbox-row input[type="checkbox"] {
+ width: auto;
+ }
+
+ /* Metadata */
+ .metadata-section {
+ display: grid;
+ gap: 0.5rem;
+ }
+
+ .metadata-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.78rem;
+ }
+
+ .metadata-entries {
+ display: grid;
+ gap: 0.35rem;
+ }
+
+ .metadata-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr auto;
+ gap: 0.35rem;
+ align-items: center;
+ }
+
+ .metadata-input {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-sm);
+ background: var(--color-surface-primary);
+ color: var(--color-text-primary);
+ padding: 0.35rem 0.45rem;
+ font-size: 0.76rem;
+ font-family: inherit;
+ }
+
+ /* Buttons */
+ .btn-primary,
+ .btn-ghost {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-sm);
+ padding: 0.35rem 0.6rem;
+ font-size: 0.78rem;
+ cursor: pointer;
+ background: var(--color-surface-primary);
+ color: var(--color-text-primary);
+ font-family: inherit;
+ }
+
+ .btn-primary {
+ border-color: var(--color-brand-primary);
+ background: var(--color-brand-primary);
+ color: var(--color-text-heading);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ }
+
+ .btn-primary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .btn-icon {
+ padding: 0.25rem;
+ line-height: 0;
+ }
+
+ .form-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ justify-content: flex-end;
+ }
+
+ /* Spinner */
+ .spinner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border: 2px solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+ }
+
+ @keyframes spin {
+ to { transform: rotate(360deg); }
+ }
+
+ /* Progress */
+ .progress-details {
+ display: grid;
+ gap: 0.4rem;
+ }
+
+ .detail-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.8rem;
+ }
+
+ .detail-label {
+ color: var(--color-text-secondary);
+ min-width: 80px;
+ font-weight: 500;
+ }
+
+ .detail-value {
+ color: var(--color-text-primary);
+ }
+
+ code {
+ font-family: ui-monospace, SFMono-Regular, monospace;
+ font-size: 0.76rem;
+ }
+
+ /* Status badges */
+ .status-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.15rem 0.45rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.72rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+
+ .status-badge[data-status="queued"] {
+ background: color-mix(in srgb, var(--color-text-secondary) 15%, transparent);
+ color: var(--color-text-secondary);
+ }
+
+ .status-badge[data-status="scanning"] {
+ background: color-mix(in srgb, var(--color-brand-primary) 15%, transparent);
+ color: var(--color-brand-primary);
+ }
+
+ .status-badge[data-status="completed"] {
+ background: color-mix(in srgb, var(--color-status-success-text) 15%, transparent);
+ color: var(--color-status-success-text);
+ }
+
+ .status-badge[data-status="failed"] {
+ background: color-mix(in srgb, var(--color-status-error-text) 15%, transparent);
+ color: var(--color-status-error-text);
+ }
+
+ /* Completion/failure banners */
+ .completion-banner,
+ .failure-banner {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 0.75rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.82rem;
+ font-weight: 500;
+ }
+
+ .completion-banner {
+ border: 1px solid color-mix(in srgb, var(--color-status-success-text) 30%, transparent);
+ background: color-mix(in srgb, var(--color-status-success-text) 8%, transparent);
+ color: var(--color-status-success-text);
+ }
+
+ .failure-banner {
+ border: 1px solid color-mix(in srgb, var(--color-status-error-text) 30%, transparent);
+ background: color-mix(in srgb, var(--color-status-error-text) 8%, transparent);
+ color: var(--color-status-error-text);
+ }
+
+ .findings-link {
+ margin-left: auto;
+ text-decoration: none;
+ }
+
+ .error-message {
+ margin: 0;
+ font-size: 0.78rem;
+ color: var(--color-status-error-text);
+ }
+ `],
+})
+export class ScanSubmitComponent implements OnDestroy {
+ private readonly http = inject(HttpClient);
+ private readonly authSession = inject(AuthSessionStore);
+ private readonly destroyRef = inject(DestroyRef);
+
+ /** Form state */
+ imageRef = '';
+ forceRescan = false;
+ showMetadata = false;
+ metadataEntries: MetadataEntry[] = [];
+
+ /** Submission state */
+ readonly submitting = signal(false);
+ readonly submitError = signal(null);
+
+ /** Scan tracking state */
+ readonly scanId = signal(null);
+ readonly scanStatus = signal('queued');
+ private pollSubscription: { unsubscribe: () => void } | null = null;
+
+ /** Derive image name (strip tag/digest for triage link) */
+ readonly imageName = computed(() => {
+ const ref = this.imageRef.trim();
+ const atIdx = ref.indexOf('@');
+ if (atIdx > 0) return ref.substring(0, atIdx);
+ const colonIdx = ref.lastIndexOf(':');
+ if (colonIdx > 0) return ref.substring(0, colonIdx);
+ return ref;
+ });
+
+ readonly canSubmit = computed(() => {
+ return this.imageRef.trim().length > 0 && !this.submitting();
+ });
+
+ addMetadataEntry(): void {
+ this.metadataEntries = [...this.metadataEntries, { key: '', value: '' }];
+ }
+
+ removeMetadataEntry(index: number): void {
+ this.metadataEntries = this.metadataEntries.filter((_, i) => i !== index);
+ }
+
+ submitScan(): void {
+ if (!this.canSubmit()) return;
+
+ this.submitError.set(null);
+ this.submitting.set(true);
+
+ const metadata: Record = {};
+ for (const entry of this.metadataEntries) {
+ const key = entry.key.trim();
+ const value = entry.value.trim();
+ if (key && value) {
+ metadata[key] = value;
+ }
+ }
+
+ const body: Record = {
+ image: { reference: this.imageRef.trim() },
+ force: this.forceRescan,
+ };
+
+ if (Object.keys(metadata).length > 0) {
+ body['metadata'] = metadata;
+ }
+
+ this.http.post('/scanner/api/v1/scans/', body).pipe(
+ takeUntilDestroyed(this.destroyRef),
+ ).subscribe({
+ next: (response) => {
+ this.submitting.set(false);
+ this.scanId.set(response.id);
+ this.scanStatus.set(response.status || 'queued');
+ this.startPolling(response.id);
+ },
+ error: (err) => {
+ this.submitting.set(false);
+ this.submitError.set(this.mapSubmitError(err));
+ },
+ });
+ }
+
+ resetForm(): void {
+ this.stopPolling();
+ this.scanId.set(null);
+ this.scanStatus.set('queued');
+ this.submitError.set(null);
+ this.imageRef = '';
+ this.forceRescan = false;
+ this.showMetadata = false;
+ this.metadataEntries = [];
+ }
+
+ ngOnDestroy(): void {
+ this.stopPolling();
+ }
+
+ private startPolling(scanId: string): void {
+ this.stopPolling();
+
+ this.pollSubscription = timer(0, 3000).pipe(
+ switchMap(() =>
+ this.http.get(`/scanner/api/v1/scans/${encodeURIComponent(scanId)}`)
+ ),
+ tap((response) => {
+ this.scanStatus.set(response.status);
+ }),
+ takeWhile(
+ (response) => response.status !== 'completed' && response.status !== 'failed',
+ true,
+ ),
+ takeUntilDestroyed(this.destroyRef),
+ ).subscribe();
+ }
+
+ private stopPolling(): void {
+ if (this.pollSubscription) {
+ this.pollSubscription.unsubscribe();
+ this.pollSubscription = null;
+ }
+ }
+
+ private mapSubmitError(err: unknown): string {
+ if (err && typeof err === 'object' && 'status' in err) {
+ const status = (err as { status?: number }).status;
+ if (status === 400) return 'Invalid image reference. Please check the format and try again.';
+ if (status === 403) return 'You do not have permission to submit scans. Ensure scanner:read scope is granted.';
+ if (status === 409) return 'A scan for this image is already in progress.';
+ if (status === 503) return 'Scanner service is currently unavailable. Please try again later.';
+ }
+ return 'Failed to submit scan. Please check connectivity and try again.';
+ }
+}
diff --git a/src/Web/StellaOps.Web/src/app/features/security-risk/security-risk-overview.component.ts b/src/Web/StellaOps.Web/src/app/features/security-risk/security-risk-overview.component.ts
index de5cb6f01..504ab5e44 100644
--- a/src/Web/StellaOps.Web/src/app/features/security-risk/security-risk-overview.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/security-risk/security-risk-overview.component.ts
@@ -68,6 +68,10 @@ interface PlatformListResponse {
Security Posture
Release-blocking posture, advisory freshness, and disposition confidence for the selected scope.
+
Scope
{{ scopeSummary() }}
@@ -204,6 +208,22 @@ interface PlatformListResponse {
.page-header{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
.page-header h1{margin:0}
.page-header p{margin:.2rem 0 0;color:var(--color-text-secondary);font-size:.82rem}
+ .header-actions{display:flex;gap:.45rem;align-items:center}
+ .btn{
+ display:inline-flex;align-items:center;gap:.3rem;
+ padding:.35rem .7rem;border-radius:var(--radius-md);
+ font-size:.76rem;font-weight:var(--font-weight-semibold);
+ text-decoration:none;cursor:pointer;border:1px solid transparent;
+ transition:background .15s,border-color .15s;
+ }
+ .btn-primary{
+ background:var(--color-brand-primary);color:#fff;border-color:var(--color-brand-primary);
+ }
+ .btn-primary:hover{opacity:.9}
+ .btn-secondary{
+ background:transparent;color:var(--color-brand-primary);border-color:var(--color-border-primary);
+ }
+ .btn-secondary:hover{background:var(--color-surface-secondary)}
.scope{display:grid;gap:.1rem;text-align:right}
.scope span{font-size:.65rem;text-transform:uppercase;color:var(--color-text-secondary)}
.scope strong{font-size:.78rem}
diff --git a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts
index 72761779c..1d657174a 100644
--- a/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts
+++ b/src/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.ts
@@ -741,10 +741,17 @@ export class AppSidebarComponent implements AfterViewInit {
],
children: [
{ id: 'sec-posture', label: 'Posture', route: '/security', icon: 'shield' },
- { id: 'sec-triage', label: 'Triage', route: '/triage/artifacts', icon: 'list' },
+ { id: 'sec-triage', label: 'Vulnerabilities', route: '/triage/artifacts', icon: 'list' },
{ id: 'sec-supply-chain', label: 'Supply-Chain Data', route: '/security/supply-chain-data', icon: 'graph' },
{ id: 'sec-reachability', label: 'Reachability', route: '/security/reachability', icon: 'cpu' },
{ id: 'sec-unknowns', label: 'Unknowns', route: '/security/unknowns', icon: 'help-circle' },
+ {
+ id: 'sec-scan-image',
+ label: 'Scan Image',
+ route: '/security/scan',
+ icon: 'search',
+ requireAnyScope: [StellaOpsScopes.SCANNER_READ],
+ },
{ id: 'sec-reports', label: 'Reports', route: '/security/reports', icon: 'book-open' },
],
},
diff --git a/src/Web/StellaOps.Web/src/app/routes/security.routes.ts b/src/Web/StellaOps.Web/src/app/routes/security.routes.ts
index d0951ebf9..4471a4840 100644
--- a/src/Web/StellaOps.Web/src/app/routes/security.routes.ts
+++ b/src/Web/StellaOps.Web/src/app/routes/security.routes.ts
@@ -136,6 +136,15 @@ export const SECURITY_ROUTES: Routes = [
(m) => m.SecurityEnvironmentRiskDetailPageComponent,
),
},
+ {
+ path: 'scan',
+ title: 'Scan Image',
+ data: { breadcrumb: 'Scan Image' },
+ loadComponent: () =>
+ import('../features/scanner/scan-submit.component').then(
+ (m) => m.ScanSubmitComponent,
+ ),
+ },
{
path: 'reports',
title: 'Security Reports',