feat: Implement IsolatedReplayContext for deterministic audit replay

- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls.
- Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation.
- Created supporting interfaces and options for context configuration.

feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison

- Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison.
- Implemented detailed drift detection and error handling during replay execution.
- Added interfaces for policy evaluation and replay execution options.

feat: Add ScanSnapshotFetcher for fetching scan data and snapshots

- Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation.
- Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements.
- Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
StellaOps Bot
2025-12-23 07:46:34 +02:00
parent e47627cfff
commit 7e384ab610
77 changed files with 153346 additions and 209 deletions

View File

@@ -1,4 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { of, throwError } from 'rxjs';
import { ExceptionApprovalQueueComponent } from './exception-approval-queue.component';
@@ -50,7 +51,17 @@ describe('ExceptionApprovalQueueComponent', () => {
await TestBed.configureTestingModule({
imports: [ExceptionApprovalQueueComponent],
providers: [{ provide: EXCEPTION_API, useValue: mockExceptionApi }],
providers: [
{ provide: EXCEPTION_API, useValue: mockExceptionApi },
{
provide: ActivatedRoute,
useValue: {
snapshot: { paramMap: { get: () => null } },
params: of({}),
queryParams: of({}),
},
},
],
}).compileComponents();
fixture = TestBed.createComponent(ExceptionApprovalQueueComponent);

View File

@@ -9,9 +9,7 @@
class="status-chip"
[style.borderColor]="col.color"
[class.active]="filter().status?.includes(col.status)"
(click)="updateFilter('status', filter().status?.includes(col.status)
? filter().status?.filter(s => s !== col.status)
: [...(filter().status || []), col.status])"
(click)="toggleStatusFilter(col.status)"
>
{{ col.label }}
<span class="chip-count">{{ statusCounts()[col.status] || 0 }}</span>
@@ -70,9 +68,7 @@
<button
class="filter-chip"
[class.active]="filter().type?.includes($any(type))"
(click)="updateFilter('type', filter().type?.includes($any(type))
? filter().type?.filter(t => t !== type)
: [...(filter().type || []), type])"
(click)="toggleTypeFilter($any(type))"
>
{{ type | titlecase }}
</button>
@@ -88,9 +84,7 @@
class="filter-chip"
[class]="'sev-' + sev"
[class.active]="filter().severity?.includes(sev)"
(click)="updateFilter('severity', filter().severity?.includes(sev)
? filter().severity?.filter(s => s !== sev)
: [...(filter().severity || []), sev])"
(click)="toggleSeverityFilter(sev)"
>
{{ sev | titlecase }}
</button>
@@ -105,9 +99,7 @@
<button
class="filter-chip tag"
[class.active]="filter().tags?.includes(tag)"
(click)="updateFilter('tags', filter().tags?.includes(tag)
? filter().tags?.filter(t => t !== tag)
: [...(filter().tags || []), tag])"
(click)="toggleTagFilter(tag)"
>
{{ tag }}
</button>

View File

@@ -152,6 +152,38 @@ export class ExceptionCenterComponent {
this.showFilters.update((v) => !v);
}
toggleStatusFilter(status: ExceptionStatus): void {
const current = this.filter().status || [];
const newStatuses = current.includes(status)
? current.filter((s) => s !== status)
: [...current, status];
this.updateFilter('status', newStatuses.length > 0 ? newStatuses : undefined);
}
toggleTypeFilter(type: ExceptionType): void {
const current = this.filter().type || [];
const newTypes = current.includes(type)
? current.filter((t) => t !== type)
: [...current, type];
this.updateFilter('type', newTypes.length > 0 ? newTypes : undefined);
}
toggleSeverityFilter(severity: string): void {
const current = this.filter().severity || [];
const newSeverities = current.includes(severity)
? current.filter((s) => s !== severity)
: [...current, severity];
this.updateFilter('severity', newSeverities.length > 0 ? newSeverities : undefined);
}
toggleTagFilter(tag: string): void {
const current = this.filter().tags || [];
const newTags = current.includes(tag)
? current.filter((t) => t !== tag)
: [...current, tag];
this.updateFilter('tags', newTags.length > 0 ? newTags : undefined);
}
updateFilter(key: keyof ExceptionFilter, value: unknown): void {
this.filter.update((f) => ({ ...f, [key]: value }));
}
@@ -193,22 +225,22 @@ export class ExceptionCenterComponent {
);
}
getStatusIcon(status: ExceptionStatus): string {
switch (status) {
case 'draft':
return '[D]';
case 'pending_review':
return '[?]';
case 'approved':
return '[+]';
case 'rejected':
return '[~]';
case 'expired':
return '[X]';
case 'revoked':
return '[!]';
default:
return '[-]';
getStatusIcon(status: ExceptionStatus): string {
switch (status) {
case 'draft':
return '[D]';
case 'pending_review':
return '[?]';
case 'approved':
return '[+]';
case 'rejected':
return '[~]';
case 'expired':
return '[X]';
case 'revoked':
return '[!]';
default:
return '[-]';
}
}

View File

@@ -1,9 +1,9 @@
@if (exception() as exc) {
@if (exception()) {
<div class="detail-container">
<header class="detail-header">
<div>
<h3 class="detail-title">{{ exc.displayName ?? exc.name }}</h3>
<p class="detail-subtitle">{{ exc.exceptionId }}</p>
<h3 class="detail-title">{{ exception()!.displayName ?? exception()!.name }}</h3>
<p class="detail-subtitle">{{ exception()!.exceptionId }}</p>
</div>
<button class="btn-link" (click)="closePanel()">Close</button>
</header>
@@ -16,19 +16,19 @@
<div class="detail-grid">
<div>
<span class="detail-label">Status</span>
<span class="detail-value">{{ exc.status | titlecase }}</span>
<span class="detail-value">{{ exception()!.status | titlecase }}</span>
</div>
<div>
<span class="detail-label">Severity</span>
<span class="detail-value">{{ exc.severity | titlecase }}</span>
<span class="detail-value">{{ exception()!.severity | titlecase }}</span>
</div>
<div>
<span class="detail-label">Created</span>
<span class="detail-value">{{ formatDate(exc.createdAt) }}</span>
<span class="detail-value">{{ formatDate(exception()!.createdAt) }}</span>
</div>
<div>
<span class="detail-label">Expires</span>
<span class="detail-value">{{ formatDate(exc.timebox.endDate) }}</span>
<span class="detail-value">{{ formatDate(exception()!.timebox.endDate) }}</span>
</div>
</div>
</section>
@@ -154,11 +154,11 @@
<section class="detail-section">
<h4 class="section-title">Audit trail</h4>
@if ((exc.auditTrail ?? []).length === 0) {
@if ((exception()!.auditTrail ?? []).length === 0) {
<span class="detail-value">No audit entries available.</span>
} @else {
<ul class="audit-list">
@for (entry of exc.auditTrail ?? []; track entry.auditId) {
@for (entry of exception()!.auditTrail ?? []; track entry.auditId) {
<li>
<span class="detail-label">{{ entry.action }}</span>
<span class="detail-value">{{ formatDate(entry.timestamp) }} by {{ entry.actor }}</span>

File diff suppressed because it is too large Load Diff