docs: seed vuln parity sbom list with available fixtures

This commit is contained in:
StellaOps Bot
2025-12-06 10:10:45 +00:00
parent 3954615e81
commit 95ff83e0f0
7 changed files with 214 additions and 180 deletions

View File

@@ -12,10 +12,10 @@ Use this list for PG-T5b.35b.4 parity runs (Mongo vs Postgres). Keep counts d
| # | SBOM path | Ecosystem | Size | Hash (SHA256) | Notes | | # | SBOM path | Ecosystem | Size | Hash (SHA256) | Notes |
|---|-----------|-----------|------|---------------|-------| |---|-----------|-----------|------|---------------|-------|
| 1 | docs/scripts/sbom-vex/sbom.json | npm | ~95 KB | <fill> | Deterministic compose sample used in sbom-vex proof. | | 1 | docs/scripts/sbom-vex/sbom.json | npm | ~95 KB | <fill> | Deterministic compose sample used in sbom-vex proof. |
| 2 | <add> | go | <fill> | TODO: pick Go SBOM fixture; store under docs/db/reports/assets/vuln-parity-20251211/. | | 2 | docs/examples/policies/sample-sbom.json | npm | small | <fill> | Tiny npm sample for quick parity sanity. |
| 3 | <add> | pypi | <fill> | TODO: pick Python SBOM fixture. | | 3 | tests/Graph/StellaOps.Graph.Indexer.Tests/Fixtures/v1/sbom-snapshot.json | mixed | <fill> | Graph indexer SBOM snapshot used in tests. |
| 4 | <add> | maven | <fill> | TODO: pick Java/Maven SBOM fixture. | | 4 | <add: go> | go | <fill> | TODO: create/store Go SBOM under docs/db/reports/assets/vuln-parity-20251211/. |
| 5 | <add> | rpm/deb | <fill> | TODO: pick OS package SBOM fixture (if available). | | 5 | <add: pypi/maven/os> | pypi or maven or rpm/deb | <fill> | TODO: add one non-npm ecosystem SBOM for coverage. |
## Determinism guardrails ## Determinism guardrails
- Do not change sample set after hashes recorded. - Do not change sample set after hashes recorded.

View File

@@ -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-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 | 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 | 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 |

View File

@@ -12,11 +12,12 @@ export default defineConfig({
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`, baseURL: process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`,
trace: 'retain-on-failure', trace: 'retain-on-failure',
}, },
webServer: { webServer: {
command: 'npm run serve:test', command: 'npm run serve:test',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
url: `http://127.0.0.1:${port}`, url: `http://127.0.0.1:${port}`,
stdout: 'ignore', stdout: 'ignore',
stderr: 'ignore', stderr: 'ignore',
}, timeout: 120_000,
}); },
});

View File

@@ -99,7 +99,7 @@ export const routes: Routes = [
}, },
{ {
path: 'policy-studio/packs/:packId/approvals', path: 'policy-studio/packs/:packId/approvals',
canMatch: [requirePolicyReviewerGuard], canMatch: [requirePolicyReviewOrApproveGuard],
loadComponent: () => loadComponent: () =>
import('./features/policy-studio/approvals/policy-approvals.component').then( import('./features/policy-studio/approvals/policy-approvals.component').then(
(m) => m.PolicyApprovalsComponent (m) => m.PolicyApprovalsComponent

View File

@@ -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) // Pre-built guards for common scope requirements (UI-ORCH-32-001)
/** /**

View File

@@ -35,6 +35,7 @@
<li><strong>Approver</strong>: policy:read, policy:review, policy:approve, policy:simulate</li> <li><strong>Approver</strong>: policy:read, policy:review, policy:approve, policy:simulate</li>
<li><strong>Operator</strong>: policy:read, policy:operate, policy:activate, policy:run, policy:simulate</li> <li><strong>Operator</strong>: policy:read, policy:operate, policy:activate, policy:run, policy:simulate</li>
<li><strong>Audit</strong>: policy:read, policy:audit</li> <li><strong>Audit</strong>: policy:read, policy:audit</li>
<li><strong>Admin</strong>: policy:author/review/approve/operate/audit/simulate/read (or admin)</li>
</ul> </ul>
<p class="console-profile__hint"> <p class="console-profile__hint">
Use this list to verify your token covers the flows you need (editor, simulate, approvals, dashboard, audit exports). Use this list to verify your token covers the flows you need (editor, simulate, approvals, dashboard, audit exports).

View File

@@ -1,177 +1,186 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { AuthService, AUTH_SERVICE } from '../../../core/auth'; import { AuthService, AUTH_SERVICE } from '../../../core/auth';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { PolicyPackSummary } from '../models/policy.models'; import { PolicyPackSummary } from '../models/policy.models';
import { PolicyPackStore } from '../services/policy-pack.store'; import { PolicyPackStore } from '../services/policy-pack.store';
@Component({ @Component({
selector: 'app-policy-workspace', selector: 'app-policy-workspace',
standalone: true, standalone: true,
imports: [CommonModule, RouterLink], imports: [CommonModule, RouterLink],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<section class="workspace" aria-busy="{{ loading }}"> <section class="workspace" aria-busy="{{ loading }}">
<header class="workspace__header"> <header class="workspace__header">
<div> <div>
<p class="workspace__eyebrow">Policy Studio · Workspace</p> <p class="workspace__eyebrow">Policy Studio · Workspace</p>
<h1>Policy packs</h1> <h1>Policy packs</h1>
<p class="workspace__lede">Deterministic list sorted by modified date desc, tie-breaker id.</p> <p class="workspace__lede">Deterministic list sorted by modified date desc, tie-breaker id.</p>
</div> </div>
</header> </header>
<div class="workspace__banner" *ngIf="scopeHint"> <div class="workspace__banner" *ngIf="scopeHint">
{{ scopeHint }} — some actions are disabled. Request scopes from your admin. {{ scopeHint }} — some actions are disabled. Request scopes from your admin.
</div> </div>
<div class="workspace__grid"> <div class="workspace__grid">
<article class="pack-card" *ngFor="let pack of packs"> <article class="pack-card" *ngFor="let pack of packs">
<header class="pack-card__head"> <header class="pack-card__head">
<div> <div>
<p class="pack-card__eyebrow">{{ pack.status | titlecase }}</p> <p class="pack-card__eyebrow">{{ pack.status | titlecase }}</p>
<h2>{{ pack.name }}</h2> <h2>{{ pack.name }}</h2>
<p class="pack-card__desc">{{ pack.description || 'No description provided.' }}</p> <p class="pack-card__desc">{{ pack.description || 'No description provided.' }}</p>
</div> </div>
<div class="pack-card__meta"> <div class="pack-card__meta">
<span>v{{ pack.version }}</span> <span>v{{ pack.version }}</span>
<span>{{ pack.modifiedAt | date: 'medium' }}</span> <span>{{ pack.modifiedAt | date: 'medium' }}</span>
</div> </div>
</header> </header>
<ul class="pack-card__tags"> <ul class="pack-card__tags">
<li *ngFor="let tag of pack.tags">{{ tag }}</li> <li *ngFor="let tag of pack.tags">{{ tag }}</li>
</ul> </ul>
<div class="pack-card__actions"> <div class="pack-card__actions">
<a <a
[routerLink]="['/policy-studio/packs', pack.id, 'editor']" [routerLink]="['/policy-studio/packs', pack.id, 'editor']"
[class.action-disabled]="!canAuthor" [class.action-disabled]="!canAuthor"
[attr.aria-disabled]="!canAuthor" [attr.aria-disabled]="!canAuthor"
[title]="canAuthor ? '' : 'Requires policy:author scope'" [title]="canAuthor ? '' : 'Requires policy:author scope'"
> >
Edit Edit
</a> </a>
<a <a
[routerLink]="['/policy-studio/packs', pack.id, 'simulate']" [routerLink]="['/policy-studio/packs', pack.id, 'simulate']"
[class.action-disabled]="!canSimulate" [class.action-disabled]="!canSimulate"
[attr.aria-disabled]="!canSimulate" [attr.aria-disabled]="!canSimulate"
[title]="canSimulate ? '' : 'Requires policy:simulate scope'" [title]="canSimulate ? '' : 'Requires policy:simulate scope'"
> >
Simulate Simulate
</a> </a>
<a <a
[routerLink]="['/policy-studio/packs', pack.id, 'approvals']" [routerLink]="['/policy-studio/packs', pack.id, 'approvals']"
[class.action-disabled]="!canReview" [class.action-disabled]="!canReview"
[attr.aria-disabled]="!canReview" [attr.aria-disabled]="!canReview"
[title]="canReview ? '' : 'Requires policy:review scope'" [title]="canReview ? '' : 'Requires policy:review scope'"
> >
Approvals Approvals
</a> </a>
<a <a
[routerLink]="['/policy-studio/packs', pack.id, 'dashboard']" [routerLink]="['/policy-studio/packs', pack.id, 'dashboard']"
[class.action-disabled]="!canView" [class.action-disabled]="!canView"
[attr.aria-disabled]="!canView" [attr.aria-disabled]="!canView"
[title]="canView ? '' : 'Requires policy:read scope'" [title]="canView ? '' : 'Requires policy:read scope'"
> >
Dashboard Dashboard
</a> </a>
</div> </div>
<dl class="pack-card__detail"> <dl class="pack-card__detail">
<div> <div>
<dt>Created</dt> <dt>Created</dt>
<dd>{{ pack.createdAt | date: 'medium' }}</dd> <dd>{{ pack.createdAt | date: 'medium' }}</dd>
</div> </div>
<div> <div>
<dt>Authors</dt> <dt>Authors</dt>
<dd>{{ pack.createdBy || 'unknown' }}</dd> <dd>{{ pack.createdBy || 'unknown' }}</dd>
</div> </div>
<div> <div>
<dt>Owner</dt> <dt>Owner</dt>
<dd>{{ pack.modifiedBy || 'unknown' }}</dd> <dd>{{ pack.modifiedBy || 'unknown' }}</dd>
</div> </div>
</dl> </dl>
</article> </article>
</div> </div>
<div class="workspace__footer"> <div class="workspace__footer">
<button type="button" (click)="refresh()" [disabled]="refreshing">{{ refreshing ? 'Refreshing…' : 'Refresh packs' }}</button> <button type="button" (click)="refresh()" [disabled]="refreshing">{{ refreshing ? 'Refreshing…' : 'Refresh packs' }}</button>
</div> </div>
</section> </section>
`, `,
styles: [ styles: [
` `
:host { display: block; background: #0b1224; color: #e5e7eb; min-height: 100vh; } :host { display: block; background: #0b1224; color: #e5e7eb; min-height: 100vh; }
.workspace { max-width: 1200px; margin: 0 auto; padding: 1.5rem; } .workspace { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
.workspace__header { margin-bottom: 1rem; } .workspace__header { margin-bottom: 1rem; }
.workspace__eyebrow { margin: 0; color: #22d3ee; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.8rem; } .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__lede { margin: 0.2rem 0 0; color: #94a3b8; }
.workspace__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; } .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 { 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__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__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__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__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 { 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__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 { 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 { 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:hover { border-color: #22d3ee; }
.pack-card__actions a.action-disabled { opacity: 0.5; pointer-events: none; border-style: dashed; } .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; } .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; } dt { color: #94a3b8; font-size: 0.85rem; margin: 0; }
dd { margin: 0; color: #e5e7eb; } 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__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 { margin-top: 0.8rem; }
.workspace__footer button { background: #2563eb; border: 1px solid #2563eb; color: #e5e7eb; border-radius: 8px; padding: 0.45rem 0.8rem; } .workspace__footer button { background: #2563eb; border: 1px solid #2563eb; color: #e5e7eb; border-radius: 8px; padding: 0.45rem 0.8rem; }
`, `,
], ],
}) })
export class PolicyWorkspaceComponent { export class PolicyWorkspaceComponent {
protected loading = false; protected loading = false;
protected packs: PolicyPackSummary[] = []; protected packs: PolicyPackSummary[] = [];
protected canAuthor = false; protected canAuthor = false;
protected canSimulate = false; protected canSimulate = false;
protected canReview = false; protected canReview = false;
protected canView = false; protected canApprove = false;
protected scopeHint = ''; protected canOperate = false;
protected refreshing = false; protected canAudit = false;
protected canView = false;
private readonly packStore = inject(PolicyPackStore); protected scopeHint = '';
private readonly auth = inject(AUTH_SERVICE) as AuthService; protected refreshing = false;
constructor() { private readonly packStore = inject(PolicyPackStore);
this.loading = true; private readonly auth = inject(AUTH_SERVICE) as AuthService;
this.applyScopes();
this.packStore.getPacks().subscribe((packs) => { constructor() {
this.packs = [...packs].sort((a, b) => this.loading = true;
b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id) this.applyScopes();
); this.packStore.getPacks().subscribe((packs) => {
this.loading = false; 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) => { refresh(): void {
this.packs = [...packs].sort((a, b) => this.refreshing = true;
b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id) this.packStore.refresh();
); this.packStore.getPacks().subscribe((packs) => {
this.refreshing = false; 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; 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.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[] = []; const missing: string[] = [];
if (!this.canView) missing.push('policy:read'); if (!this.canView) missing.push('policy:read');
if (!this.canAuthor) missing.push('policy:author'); if (!this.canAuthor) missing.push('policy:author');
if (!this.canSimulate) missing.push('policy:simulate'); if (!this.canSimulate) missing.push('policy:simulate');
if (!this.canReview) missing.push('policy:review'); 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(', ')}` : ''; this.scopeHint = missing.length ? `Missing scopes: ${missing.join(', ')}` : '';
} }
} }