diff --git a/docs/db/reports/vuln-parity-sbom-sample-20251209.md b/docs/db/reports/vuln-parity-sbom-sample-20251209.md index 6cdeeb89d..259bec20e 100644 --- a/docs/db/reports/vuln-parity-sbom-sample-20251209.md +++ b/docs/db/reports/vuln-parity-sbom-sample-20251209.md @@ -12,10 +12,10 @@ Use this list for PG-T5b.3–5b.4 parity runs (Mongo vs Postgres). Keep counts d | # | SBOM path | Ecosystem | Size | Hash (SHA256) | Notes | |---|-----------|-----------|------|---------------|-------| | 1 | docs/scripts/sbom-vex/sbom.json | npm | ~95 KB | | Deterministic compose sample used in sbom-vex proof. | -| 2 | | go | | TODO: pick Go SBOM fixture; store under docs/db/reports/assets/vuln-parity-20251211/. | -| 3 | | pypi | | TODO: pick Python SBOM fixture. | -| 4 | | maven | | TODO: pick Java/Maven SBOM fixture. | -| 5 | | rpm/deb | | TODO: pick OS package SBOM fixture (if available). | +| 2 | docs/examples/policies/sample-sbom.json | npm | small | | Tiny npm sample for quick parity sanity. | +| 3 | tests/Graph/StellaOps.Graph.Indexer.Tests/Fixtures/v1/sbom-snapshot.json | mixed | | Graph indexer SBOM snapshot used in tests. | +| 4 | | go | | TODO: create/store Go SBOM under docs/db/reports/assets/vuln-parity-20251211/. | +| 5 | | pypi or maven or rpm/deb | | TODO: add one non-npm ecosystem SBOM for coverage. | ## Determinism guardrails - Do not change sample set after hashes recorded. diff --git a/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md b/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md index 093b5564e..f63ff4b9a 100644 --- a/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md +++ b/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md @@ -74,3 +74,4 @@ | 2025-11-30 | Normalised sprint to standard template and renamed file from `SPRINT_211_ui_iii.md` to `SPRINT_0211_0001_0003_ui_iii.md`; no task status changes. | Planning | | 2025-12-06 | Corrected working directory to `src/Web/StellaOps.Web`; unblocked Delivery Tracker items accordingly. Reachability fixtures still required. | Implementer | | 2025-12-06 | Added Policy Studio scope help text to Console Profile and introduced policy auth fixtures + seeding helper (`src/Web/StellaOps.Web/src/app/testing/auth-*.ts`) with APP_INITIALIZER hook (`window.__stellaopsTestSession`) for Cypress/e2e stubbing. | Implementer | +| 2025-12-06 | Tightened approvals guard (requires `policy:read` + review/approve) and updated workspace scope hints; attempted Playwright `tests/e2e/auth.spec.ts` with seeded session but webServer (ng serve) timed out starting locally; rerun in CI or with longer warmup. | Implementer | diff --git a/src/Web/StellaOps.Web/playwright.config.ts b/src/Web/StellaOps.Web/playwright.config.ts index 4ce8bc338..369053e2c 100644 --- a/src/Web/StellaOps.Web/playwright.config.ts +++ b/src/Web/StellaOps.Web/playwright.config.ts @@ -12,11 +12,12 @@ export default defineConfig({ baseURL: process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`, trace: 'retain-on-failure', }, - webServer: { - command: 'npm run serve:test', - reuseExistingServer: !process.env.CI, - url: `http://127.0.0.1:${port}`, - stdout: 'ignore', - stderr: 'ignore', - }, -}); + webServer: { + command: 'npm run serve:test', + reuseExistingServer: !process.env.CI, + url: `http://127.0.0.1:${port}`, + stdout: 'ignore', + stderr: 'ignore', + timeout: 120_000, + }, +}); diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts index 5b06dd091..8208f9c87 100644 --- a/src/Web/StellaOps.Web/src/app/app.routes.ts +++ b/src/Web/StellaOps.Web/src/app/app.routes.ts @@ -99,7 +99,7 @@ export const routes: Routes = [ }, { path: 'policy-studio/packs/:packId/approvals', - canMatch: [requirePolicyReviewerGuard], + canMatch: [requirePolicyReviewOrApproveGuard], loadComponent: () => import('./features/policy-studio/approvals/policy-approvals.component').then( (m) => m.PolicyApprovalsComponent diff --git a/src/Web/StellaOps.Web/src/app/core/auth/auth.guard.ts b/src/Web/StellaOps.Web/src/app/core/auth/auth.guard.ts index b0d8ebb76..870717b06 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/auth.guard.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/auth.guard.ts @@ -101,6 +101,28 @@ export function requireAnyScopeGuard( }; } +/** + * Guard requiring policy:read and either policy:review or policy:approve for approval workflows. + */ +export const requirePolicyReviewOrApproveGuard: CanMatchFn = () => { + const auth = inject(AuthSessionStore); + const router = inject(Router); + + if (!auth.isAuthenticated()) { + return router.createUrlTree(['/welcome']); + } + + const scopes = auth.session()?.scopes ?? []; + if (scopes.includes(StellaOpsScopes.ADMIN)) return true; + + const hasRead = scopes.includes(StellaOpsScopes.POLICY_READ); + const hasReviewOrApprove = + scopes.includes(StellaOpsScopes.POLICY_REVIEW) || scopes.includes(StellaOpsScopes.POLICY_APPROVE); + + if (hasRead && hasReviewOrApprove) return true; + return router.createUrlTree(['/console/profile']); +}; + // Pre-built guards for common scope requirements (UI-ORCH-32-001) /** diff --git a/src/Web/StellaOps.Web/src/app/features/console/console-profile.component.html b/src/Web/StellaOps.Web/src/app/features/console/console-profile.component.html index 984252efa..3f0bceb88 100644 --- a/src/Web/StellaOps.Web/src/app/features/console/console-profile.component.html +++ b/src/Web/StellaOps.Web/src/app/features/console/console-profile.component.html @@ -35,6 +35,7 @@
  • Approver: policy:read, policy:review, policy:approve, policy:simulate
  • Operator: policy:read, policy:operate, policy:activate, policy:run, policy:simulate
  • Audit: policy:read, policy:audit
  • +
  • Admin: policy:author/review/approve/operate/audit/simulate/read (or admin)
  • Use this list to verify your token covers the flows you need (editor, simulate, approvals, dashboard, audit exports). diff --git a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts index 82b0990e8..41c096f90 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts @@ -1,177 +1,186 @@ -import { CommonModule } from '@angular/common'; -import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; -import { AuthService, AUTH_SERVICE } from '../../../core/auth'; -import { RouterLink } from '@angular/router'; - -import { PolicyPackSummary } from '../models/policy.models'; -import { PolicyPackStore } from '../services/policy-pack.store'; - -@Component({ - selector: 'app-policy-workspace', - standalone: true, - imports: [CommonModule, RouterLink], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` -

    -
    -
    -

    Policy Studio · Workspace

    -

    Policy packs

    -

    Deterministic list sorted by modified date desc, tie-breaker id.

    -
    -
    - -
    - {{ scopeHint }} — some actions are disabled. Request scopes from your admin. -
    - -
    -
    -
    -
    -

    {{ pack.status | titlecase }}

    -

    {{ pack.name }}

    -

    {{ pack.description || 'No description provided.' }}

    -
    -
    - v{{ pack.version }} - {{ pack.modifiedAt | date: 'medium' }} -
    -
    - -
      -
    • {{ tag }}
    • -
    - - - -
    -
    -
    Created
    -
    {{ pack.createdAt | date: 'medium' }}
    -
    -
    -
    Authors
    -
    {{ pack.createdBy || 'unknown' }}
    -
    -
    -
    Owner
    -
    {{ pack.modifiedBy || 'unknown' }}
    -
    -
    -
    -
    - -
    - `, - styles: [ - ` - :host { display: block; background: #0b1224; color: #e5e7eb; min-height: 100vh; } - .workspace { max-width: 1200px; margin: 0 auto; padding: 1.5rem; } - .workspace__header { margin-bottom: 1rem; } - .workspace__eyebrow { margin: 0; color: #22d3ee; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.8rem; } - .workspace__lede { margin: 0.2rem 0 0; color: #94a3b8; } - .workspace__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; } - .pack-card { background: #0f172a; border: 1px solid #1f2937; border-radius: 12px; padding: 1rem; box-shadow: 0 12px 30px rgba(0,0,0,0.28); display: grid; gap: 0.6rem; } - .pack-card__head { display: flex; justify-content: space-between; gap: 0.75rem; align-items: flex-start; } - .pack-card__eyebrow { margin: 0; color: #a5b4fc; font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase; } - .pack-card__desc { margin: 0.2rem 0 0; color: #cbd5e1; } - .pack-card__meta { display: grid; justify-items: end; gap: 0.2rem; color: #94a3b8; font-size: 0.9rem; } - .pack-card__tags { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.35rem; } - .pack-card__tags li { padding: 0.2rem 0.45rem; border: 1px solid #1f2937; border-radius: 999px; background: #0b162e; } - .pack-card__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; } - .pack-card__actions a { color: #e5e7eb; border: 1px solid #334155; border-radius: 8px; padding: 0.35rem 0.6rem; text-decoration: none; } - .pack-card__actions a:hover { border-color: #22d3ee; } - .pack-card__actions a.action-disabled { opacity: 0.5; pointer-events: none; border-style: dashed; } - .pack-card__detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.35rem 1rem; margin: 0; } - dt { color: #94a3b8; font-size: 0.85rem; margin: 0; } - dd { margin: 0; color: #e5e7eb; } - .workspace__banner { background: #1f2937; border: 1px solid #334155; color: #fbbf24; padding: 0.75rem 1rem; border-radius: 10px; margin: 0.5rem 0 1rem; } - .workspace__footer { margin-top: 0.8rem; } - .workspace__footer button { background: #2563eb; border: 1px solid #2563eb; color: #e5e7eb; border-radius: 8px; padding: 0.45rem 0.8rem; } - `, - ], -}) -export class PolicyWorkspaceComponent { - protected loading = false; - protected packs: PolicyPackSummary[] = []; - protected canAuthor = false; - protected canSimulate = false; +import { CommonModule } from '@angular/common'; +import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { AuthService, AUTH_SERVICE } from '../../../core/auth'; +import { RouterLink } from '@angular/router'; + +import { PolicyPackSummary } from '../models/policy.models'; +import { PolicyPackStore } from '../services/policy-pack.store'; + +@Component({ + selector: 'app-policy-workspace', + standalone: true, + imports: [CommonModule, RouterLink], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
    +
    +
    +

    Policy Studio · Workspace

    +

    Policy packs

    +

    Deterministic list sorted by modified date desc, tie-breaker id.

    +
    +
    + +
    + {{ scopeHint }} — some actions are disabled. Request scopes from your admin. +
    + +
    +
    +
    +
    +

    {{ pack.status | titlecase }}

    +

    {{ pack.name }}

    +

    {{ pack.description || 'No description provided.' }}

    +
    +
    + v{{ pack.version }} + {{ pack.modifiedAt | date: 'medium' }} +
    +
    + +
      +
    • {{ tag }}
    • +
    + + + +
    +
    +
    Created
    +
    {{ pack.createdAt | date: 'medium' }}
    +
    +
    +
    Authors
    +
    {{ pack.createdBy || 'unknown' }}
    +
    +
    +
    Owner
    +
    {{ pack.modifiedBy || 'unknown' }}
    +
    +
    +
    +
    + +
    + `, + styles: [ + ` + :host { display: block; background: #0b1224; color: #e5e7eb; min-height: 100vh; } + .workspace { max-width: 1200px; margin: 0 auto; padding: 1.5rem; } + .workspace__header { margin-bottom: 1rem; } + .workspace__eyebrow { margin: 0; color: #22d3ee; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.8rem; } + .workspace__lede { margin: 0.2rem 0 0; color: #94a3b8; } + .workspace__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; } + .pack-card { background: #0f172a; border: 1px solid #1f2937; border-radius: 12px; padding: 1rem; box-shadow: 0 12px 30px rgba(0,0,0,0.28); display: grid; gap: 0.6rem; } + .pack-card__head { display: flex; justify-content: space-between; gap: 0.75rem; align-items: flex-start; } + .pack-card__eyebrow { margin: 0; color: #a5b4fc; font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase; } + .pack-card__desc { margin: 0.2rem 0 0; color: #cbd5e1; } + .pack-card__meta { display: grid; justify-items: end; gap: 0.2rem; color: #94a3b8; font-size: 0.9rem; } + .pack-card__tags { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.35rem; } + .pack-card__tags li { padding: 0.2rem 0.45rem; border: 1px solid #1f2937; border-radius: 999px; background: #0b162e; } + .pack-card__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; } + .pack-card__actions a { color: #e5e7eb; border: 1px solid #334155; border-radius: 8px; padding: 0.35rem 0.6rem; text-decoration: none; } + .pack-card__actions a:hover { border-color: #22d3ee; } + .pack-card__actions a.action-disabled { opacity: 0.5; pointer-events: none; border-style: dashed; } + .pack-card__detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.35rem 1rem; margin: 0; } + dt { color: #94a3b8; font-size: 0.85rem; margin: 0; } + dd { margin: 0; color: #e5e7eb; } + .workspace__banner { background: #1f2937; border: 1px solid #334155; color: #fbbf24; padding: 0.75rem 1rem; border-radius: 10px; margin: 0.5rem 0 1rem; } + .workspace__footer { margin-top: 0.8rem; } + .workspace__footer button { background: #2563eb; border: 1px solid #2563eb; color: #e5e7eb; border-radius: 8px; padding: 0.45rem 0.8rem; } + `, + ], +}) +export class PolicyWorkspaceComponent { + protected loading = false; + protected packs: PolicyPackSummary[] = []; + protected canAuthor = false; + protected canSimulate = false; protected canReview = false; - protected canView = false; - protected scopeHint = ''; - protected refreshing = false; - - private readonly packStore = inject(PolicyPackStore); - private readonly auth = inject(AUTH_SERVICE) as AuthService; - - constructor() { - this.loading = true; - this.applyScopes(); - this.packStore.getPacks().subscribe((packs) => { - this.packs = [...packs].sort((a, b) => - b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id) - ); - this.loading = false; - }); - } - - refresh(): void { - this.refreshing = true; - this.packStore.refresh(); - this.packStore.getPacks().subscribe((packs) => { - this.packs = [...packs].sort((a, b) => - b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id) - ); - this.refreshing = false; - }); - } - - private applyScopes(): void { - this.canAuthor = this.auth.canAuthorPolicies?.() ?? false; - this.canSimulate = this.auth.canSimulatePolicies?.() ?? false; - this.canReview = this.auth.canReviewPolicies?.() ?? false; + protected canApprove = false; + protected canOperate = false; + protected canAudit = false; + protected canView = false; + protected scopeHint = ''; + protected refreshing = false; + + private readonly packStore = inject(PolicyPackStore); + private readonly auth = inject(AUTH_SERVICE) as AuthService; + + constructor() { + this.loading = true; + this.applyScopes(); + this.packStore.getPacks().subscribe((packs) => { + this.packs = [...packs].sort((a, b) => + b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id) + ); + this.loading = false; + }); + } + + refresh(): void { + this.refreshing = true; + this.packStore.refresh(); + this.packStore.getPacks().subscribe((packs) => { + this.packs = [...packs].sort((a, b) => + b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id) + ); + this.refreshing = false; + }); + } + + private applyScopes(): void { + this.canAuthor = this.auth.canAuthorPolicies?.() ?? false; + this.canSimulate = this.auth.canSimulatePolicies?.() ?? false; + this.canReview = this.auth.canReviewPolicies?.() ?? false; this.canView = this.auth.canViewPolicies?.() ?? false; + this.canApprove = this.auth.canApprovePolicies?.() ?? false; + this.canOperate = this.auth.canOperatePolicies?.() ?? false; + this.canAudit = this.auth.canAuditPolicies?.() ?? false; const missing: string[] = []; if (!this.canView) missing.push('policy:read'); if (!this.canAuthor) missing.push('policy:author'); if (!this.canSimulate) missing.push('policy:simulate'); if (!this.canReview) missing.push('policy:review'); + if (!this.canApprove) missing.push('policy:approve'); + if (!this.canOperate) missing.push('policy:operate'); + if (!this.canAudit) missing.push('policy:audit'); this.scopeHint = missing.length ? `Missing scopes: ${missing.join(', ')}` : ''; } }