fix(web): remediate orphan revival regressions
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# Sprint 20260308-024 - FE Orphan Revival Regression Remediation
|
||||
|
||||
## Topic & Scope
|
||||
- Repair the concrete regressions found in review across the orphan-revival implementation batch before any more revival work proceeds.
|
||||
- Fix the frontend build break, restore canonical evidence-thread navigation, restore lost audit and trust filtering capabilities, and remove fabricated finding evidence from mounted shells.
|
||||
- Keep the remediation bounded to the shipped frontend and its verification/docs; do not reopen unrelated orphan candidates in this sprint.
|
||||
- Working directory: `src/Web/StellaOps.Web`.
|
||||
- Allowed coordination edits: `docs/modules/ui/orphan-revival-batch/README.md`, `docs/modules/ui/TASKS.md`, `docs/modules/ui/implementation_plan.md`, `docs/features/checked/web/`, `src/Web/StellaOps.Web/src/app/shared/components/policy/`, `src/Web/StellaOps.Web/src/app/shared/directives/`, `src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/`, `src/Web/StellaOps.Web/src/app/routes/evidence.routes.ts`, `src/Web/StellaOps.Web/src/app/features/evidence-thread/`, `src/Web/StellaOps.Web/src/app/features/audit-log/`, `src/Web/StellaOps.Web/src/app/features/trust-admin/`, `src/Web/StellaOps.Web/src/app/features/findings/`, and `src/Web/StellaOps.Web/src/app/features/release-orchestrator/`.
|
||||
- Expected evidence: green Angular build, focused frontend tests for each repaired area, one checked-feature note, and sprint execution-log updates.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Hard dependency inside the orphan revival batch: none.
|
||||
- External prerequisite already satisfied: the affected orphan-revival commits are already landed on `main` and are the baseline for this remediation.
|
||||
- Safe parallelism:
|
||||
- Do not staff this sprint in parallel with new orphan-revival implementation work on the same files.
|
||||
- This sprint owns remediation of the reviewed regressions across sprints `015`, `019`, `020`, and `021`.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `docs/modules/ui/orphan-revival-batch/README.md`
|
||||
- `docs/modules/ui/implementation_plan.md`
|
||||
- `src/Web/StellaOps.Web/AGENTS.md`
|
||||
- `src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/filter-bar.component.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/features/evidence-thread/evidence-thread.routes.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail.component.ts`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### FE-ORM-001 - Clear build blockers in revived shared policy and glossary code
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- Repair the broken shared policy-widget imports and strict TypeScript issues introduced by the orphan revival batch so the Angular application builds again.
|
||||
- Keep the fix minimal and bounded to the revived components and directive; do not redesign the widgets in this task.
|
||||
|
||||
Completion criteria:
|
||||
- [x] `npm run build` succeeds in `src/Web/StellaOps.Web`.
|
||||
- [x] Shared policy widget imports resolve correctly.
|
||||
- [x] The glossary directive no longer violates strict-null or typing rules.
|
||||
|
||||
### FE-ORM-002 - Restore canonical evidence-thread navigation behavior
|
||||
Status: DONE
|
||||
Dependency: FE-ORM-001
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- Repair the evidence-thread list/detail navigation so the reconnected `/evidence/threads/*` URLs are used consistently by row-click, back navigation, and any route-focused regression coverage.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Evidence-thread list rows navigate to `/evidence/threads/:artifactDigest`.
|
||||
- [x] Evidence-thread detail back navigation returns to `/evidence/threads`.
|
||||
- [x] Focused route tests cover the repaired canonical URLs.
|
||||
|
||||
### FE-ORM-003 - Restore full mounted audit and trust filter semantics
|
||||
Status: DONE
|
||||
Dependency: FE-ORM-001
|
||||
Owners: Developer (FE), UX
|
||||
Task description:
|
||||
- Restore the filter capabilities lost during the shared filter-bar migration on mounted audit pages, including actor filtering, custom date support, and any multi-select semantics that the pages previously exposed.
|
||||
- The end result may extend the shared filter bar or use bounded page-local controls, but the mounted pages must regain their previously available filtering behavior.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Audit log page supports actor filtering, custom date ranges, and multi-value module/action/severity filters again.
|
||||
- [x] Trust audit log page supports start and end date filtering again.
|
||||
- [x] Focused tests assert the restored filter-state behavior and request-shape wiring.
|
||||
|
||||
### FE-ORM-004 - Remove fabricated finding evidence from revived mounted consumers
|
||||
Status: DONE
|
||||
Dependency: FE-ORM-001
|
||||
Owners: Developer (FE), Product Manager
|
||||
Task description:
|
||||
- Eliminate fabricated timestamps, synthetic scores, and invented PURLs from the revived findings and release-security consumers.
|
||||
- If the shared finding-list contract cannot be satisfied truthfully for a mounted host, back that host out to its prior truthful presentation instead of synthesizing data.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Mounted findings and release-security surfaces no longer fabricate `last_seen`, `risk_score`, or package identity fields.
|
||||
- [x] Any remaining revived shared finding-list adoption is backed by truthful source data.
|
||||
- [x] Focused tests assert truthful rendering or the bounded rollback decision for the affected hosts.
|
||||
|
||||
### FE-ORM-005 - Verify, document, and sync the remediation
|
||||
Status: DONE
|
||||
Dependency: FE-ORM-002
|
||||
Owners: Test Automation, Documentation author
|
||||
Task description:
|
||||
- Run the focused build and test matrix for the repaired areas, record the outcomes, and sync the UI plan/docs with the remediation result.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Focused frontend tests cover policy build blockers, evidence-thread routing, audit/trust filtering, and findings/release-security behavior.
|
||||
- [x] Checked-feature note exists under `docs/features/checked/web/`.
|
||||
- [x] UI plan/task docs reflect the remediation status and any bounded rollback decisions.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-08 | Sprint created to remediate the concrete regressions found during review of orphan-revival sprints 015, 019, 020, and 021. | Project Manager |
|
||||
| 2026-03-08 | Repaired build blockers in revived policy widgets and glossary tooltip directive so the Angular production build compiles again. | Developer (FE) |
|
||||
| 2026-03-08 | Repaired evidence-thread row and back navigation to use canonical `/evidence/threads/*` URLs. | Developer (FE) |
|
||||
| 2026-03-08 | Restored full mounted audit and trust filter semantics by rolling the audit-log and trust-audit pages back to truthful page-local controls where the shared filter bar could not preserve behavior. | Developer (FE) |
|
||||
| 2026-03-08 | Removed fabricated finding evidence from mounted findings and release-security hosts by rolling those consumers back to their truthful bespoke list/table rendering. | Developer (FE) |
|
||||
| 2026-03-08 | Verified remediation with `npm run build` and focused Angular tests covering policy hosts, evidence-thread routing, audit/trust filtering, and truthful findings/release rendering. | Test Automation |
|
||||
|
||||
## Decisions & Risks
|
||||
- Decision: this sprint prioritizes truthful mounted behavior over preserving every individual orphan adoption. If a revived component forces fabricated data, the mounted host may be rolled back to its prior truthful UI.
|
||||
- Decision: `audit-log-table` and `trust-audit-log` keep page-local filter controls for now; the shared `FilterBarComponent` remains adopted only where it does not remove operator capabilities.
|
||||
- Decision: the shared `FindingListComponent` is no longer used on mounted findings and release-security hosts until a truthful data contract exists for those surfaces.
|
||||
- Risk: older checked-feature notes for orphan revival sprints can drift from current shipped behavior after bounded rollbacks.
|
||||
- Mitigation: record the remediation in a dedicated checked-feature note and sync the UI plan plus orphan-revival batch docs with the rollback decisions.
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-03-08: build blockers fixed and verified.
|
||||
- 2026-03-09: routing, filter, and findings regressions resolved with focused test evidence.
|
||||
@@ -12,6 +12,8 @@ SPRINT_20260308_021_FE_unreachable_evidence_thread_and_persona_workspaces_routes
|
||||
## Description
|
||||
Reconnected the disconnected evidence-thread and persona-workspace route families under the canonical `/evidence` route shell. Evidence threads and auditor/developer workspaces are now reachable through Evidence-owned URLs, acting as evidence lenses rather than a parallel product shell.
|
||||
|
||||
Sprint `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` completed the cutover by fixing evidence-thread list-row and back-button navigation to use the same canonical `/evidence/threads/*` URLs instead of the dead legacy `/evidence-thread` frontdoor.
|
||||
|
||||
## Canonical URL Contract
|
||||
- `/evidence/threads` - Evidence thread list
|
||||
- `/evidence/threads/:artifactDigest` - Evidence thread detail
|
||||
|
||||
@@ -4,9 +4,13 @@ Sprint: `SPRINT_20260308_015_FE_orphan_filter_bar_unification.md`
|
||||
Feature IDs: FE-OFB-001 through FE-OFB-004
|
||||
Status: Shipped
|
||||
|
||||
## Post-remediation note
|
||||
|
||||
Sprint `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` partially rolled back this adoption on mounted audit hosts. `audit-log-table` and `trust-audit-log` no longer use the shared filter bar because that migration removed actor, multi-select, and date-filter behavior. The shared bar remains mounted only on the adopters where it preserves real page semantics.
|
||||
|
||||
## What shipped
|
||||
|
||||
The dormant shared `FilterBarComponent` (`src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/filter-bar.component.ts`) has been adopted across all seven eligible mounted list pages. Each page's bespoke filter toolbar markup has been replaced by a single `<app-filter-bar>` element that renders search input, single-select filter dropdowns, active-filter chips, and a clear-all button through the shared component's existing API.
|
||||
The dormant shared `FilterBarComponent` (`src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/filter-bar.component.ts`) was adopted across seven mounted list pages in the original sprint. After remediation, five mounted pages still use the shared bar; the two audit-family pages listed below reverted to page-local controls to preserve truthful operator workflows.
|
||||
|
||||
## Shared contract (unchanged)
|
||||
|
||||
@@ -26,7 +30,7 @@ The `FilterBarComponent` API was sufficient without extension:
|
||||
|
||||
| Page | File | Filter options exposed |
|
||||
| --- | --- | --- |
|
||||
| Audit Log Table | `features/audit-log/audit-log-table.component.ts` | module, action, severity, dateRange |
|
||||
| Audit Log Table | `features/audit-log/audit-log-table.component.ts` | rolled back in sprint `024`; page-local filters restored |
|
||||
| Secret Findings List | `features/secret-detection/secret-findings-list.component.ts` | severity, status, category |
|
||||
| Console Admin Audit Log | `features/console-admin/audit/audit-log.component.ts` | eventType (16 event types) |
|
||||
|
||||
@@ -36,7 +40,7 @@ The `FilterBarComponent` API was sufficient without extension:
|
||||
| --- | --- | --- |
|
||||
| Release List | `features/release-orchestrator/releases/release-list/release-list.component.ts` | type, stage, gate, risk, blocked, needsApproval, hotfixLane, replayMismatch |
|
||||
| Evidence Pack List | `features/evidence-pack/evidence-pack-list.component.ts` | (search only, no dropdowns) |
|
||||
| Trust Audit Log | `features/trust-admin/trust-audit-log.component.ts` | resourceType, severity |
|
||||
| Trust Audit Log | `features/trust-admin/trust-audit-log.component.ts` | rolled back in sprint `024`; start/end date filters restored via page-local controls |
|
||||
| Certificate Inventory | `features/trust-admin/certificate-inventory.component.ts` | status, type |
|
||||
|
||||
## Exclusions
|
||||
@@ -46,10 +50,9 @@ The `FilterBarComponent` API was sufficient without extension:
|
||||
|
||||
## Design decisions
|
||||
|
||||
- Multi-select dropdowns on the audit-log-table were simplified to single-select to match the shared bar's existing contract. The underlying filter arrays still work but now hold at most one value.
|
||||
- Date-range inputs on the audit-log-table were mapped to preset dropdown options (24h, 7d, 30d, 90d).
|
||||
- Date inputs on trust-admin pages were removed from the filter bar; dates remain in component state for API queries but are not exposed through the shared bar.
|
||||
- Audit-log and trust-audit pages were reverted to bespoke controls because the shared bar could not preserve their real semantics.
|
||||
- Query-state persistence on the release-list via `buildQueryParams()` and route subscription remains intact.
|
||||
- Future shared filter-bar expansion must prove parity before replacing mounted audit-family pages again.
|
||||
|
||||
## Test coverage
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation
|
||||
|
||||
## Post-remediation note
|
||||
|
||||
Sprint `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` rolled back both mounted adopters described below. The shared `FindingListComponent` is no longer mounted on `FindingsContainerComponent` or the `ReleaseDetailComponent` security tab because those hosts required fabricated timestamps, severity-derived risk scores, and invented package identities to satisfy the shared contract.
|
||||
|
||||
## Summary
|
||||
|
||||
Revived the dormant shared `FindingListComponent` and `FindingRowComponent` by adopting them on two mounted surfaces that previously used bespoke finding list rendering:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Orphan Revival Regression Remediation
|
||||
|
||||
Sprint: `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md`
|
||||
Status: Shipped
|
||||
|
||||
## What changed
|
||||
|
||||
This remediation sprint closed the concrete regressions introduced by the orphan-revival batch and kept mounted behavior truthful where the revived shared components did not fit the live hosts.
|
||||
|
||||
- Revived shared policy widgets build again: broken `policy-interop.models` imports were corrected in the shared policy components, and the glossary tooltip directive was fixed for strict-null and typing compliance.
|
||||
- Evidence-thread navigation now stays inside the canonical Evidence shell. Row click and back navigation both use `/evidence/threads` instead of dead `/evidence-thread` paths.
|
||||
- Audit and trust filtering capabilities were restored on mounted pages. `audit-log-table` and `trust-audit-log` were intentionally rolled back to page-local controls so actor filters, multi-select filters, and start/end date filters remain usable.
|
||||
- Fabricated finding evidence was removed from mounted consumers. `FindingsContainerComponent` and the `ReleaseDetailComponent` security tab no longer synthesize `last_seen`, `risk_score`, or fake package identities just to satisfy the shared `FindingListComponent` contract.
|
||||
|
||||
## Bounded rollback decisions
|
||||
|
||||
- `FilterBarComponent` remains valid, but not for every list page. Audit-log and trust-audit hosts keep their bespoke filter controls until the shared bar can support their real semantics without loss.
|
||||
- `FindingListComponent` remains available for future truthful adopters, but it is no longer mounted on findings and release-security hosts that would require invented data.
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm run build`
|
||||
- Result: passed
|
||||
- Notes: only the existing Angular bundle-budget warnings remained
|
||||
- `npm test -- --watch=false --include src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts --include src/tests/evidence/evidence-thread-browser.component.spec.ts --include src/tests/releases/release-detail.live-refresh.spec.ts --include src/app/routes/evidence.routes.spec.ts`
|
||||
- Result: passed
|
||||
- Coverage: policy host rendering, evidence-thread canonical routes, audit/trust filter semantics, truthful findings rendering, release-security rendering, and evidence route declarations
|
||||
|
||||
## Current shipped state
|
||||
|
||||
- Canonical evidence-thread routes are usable and internally consistent.
|
||||
- Shared policy widgets stay mounted on the active policy surfaces without breaking the app build.
|
||||
- Audit and trust pages retain their full filtering workflows.
|
||||
- Findings and release-security views favor truthful bespoke rendering over forced shared-component reuse.
|
||||
@@ -1,9 +1,9 @@
|
||||
# UI Task Board
|
||||
|
||||
## Active Sprint Links
|
||||
- None currently. Completed UI delivery and verification sprints are archived under `docs-archived/implplan/`.
|
||||
- [DONE] `docs/implplan/SPRINT_20260308_014_FE_orphan_copy_inline_truncate_adoption.md` - CopyToClipboard, InlineCode, TruncatePipe adoption on console-admin, offline-kit, and triage replay-command surfaces.
|
||||
- [DONE] `docs/implplan/SPRINT_20260308_015_FE_orphan_filter_bar_unification.md` - FilterBarComponent adoption on 7 mounted list pages (audit-log-table, secret-findings-list, console-admin audit-log, release-list, evidence-pack-list, trust-audit-log, certificate-inventory).
|
||||
- [DONE] `docs/implplan/SPRINT_20260308_015_FE_orphan_filter_bar_unification.md` - Initial FilterBarComponent adoption batch; audit-log-table and trust-audit-log were later rolled back in sprint `024` to restore lost semantics.
|
||||
- [DONE] `docs-archived/implplan/SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` - Fixed reviewed orphan-revival regressions: build blockers cleared, canonical evidence-thread navigation restored, audit/trust filter capabilities restored, and fabricated finding evidence removed from mounted hosts.
|
||||
|
||||
## Queued Sprint Links
|
||||
- `docs/modules/ui/orphan-revival-batch/README.md` - review index for the orphan shared-component and disconnected-route revival batch.
|
||||
|
||||
@@ -6,13 +6,16 @@ Provide a living plan for UI deliverables, dependencies, and evidence.
|
||||
## Active work
|
||||
- Track current sprints under `docs/implplan/SPRINT_*.md` for this module.
|
||||
- Update this file when new scoped work is approved.
|
||||
- No active UI remediation sprint is open right now.
|
||||
|
||||
## Near-term deliverables
|
||||
- No active UI deliverables are currently staged in `docs/implplan`.
|
||||
- The next queued batch is `docs/modules/ui/orphan-revival-batch/README.md`, which stages independent review-ready sprints for orphan shared-component adoption and disconnected-route integration.
|
||||
- The queued orphan batch currently spans `SPRINT_20260308_013` through `SPRINT_20260308_023` and is intentionally not marked active until product review approves staffing.
|
||||
- Sprint `014` (CopyToClipboard, InlineCode, TruncatePipe adoption) is DONE. See `docs/features/checked/web/orphan-copy-inline-truncate-adoption.md`.
|
||||
- Sprint `015` (FilterBarComponent adoption on 7 mounted list pages) is DONE. See `docs/features/checked/web/filter-bar-unification.md`.
|
||||
- Sprint `015` (FilterBarComponent adoption) shipped, then was partially rolled back on audit-family pages to restore lost filter semantics. See `docs/features/checked/web/filter-bar-unification.md` and `docs/features/checked/web/orphan-revival-regression-remediation-ui.md`.
|
||||
- Sprint `020` (FindingListComponent consolidation) shipped, then was rolled back on mounted findings and release-security hosts because the shared contract required fabricated data. See `docs/features/checked/web/orphan-finding-list-consolidation.md` and `docs/features/checked/web/orphan-revival-regression-remediation-ui.md`.
|
||||
- Sprint `021` (Evidence thread and persona workspace reconnection) shipped, and its internal thread navigation was completed in the remediation sprint. See `docs/features/checked/web/evidence-thread-persona-workspaces-routes.md` and `docs/features/checked/web/orphan-revival-regression-remediation-ui.md`.
|
||||
|
||||
## Latest evidence
|
||||
- `docs/modules/ui/component-preservation-map/README.md` - root index for the first-pass preservation map.
|
||||
@@ -54,7 +57,8 @@ Provide a living plan for UI deliverables, dependencies, and evidence.
|
||||
- `docs/modules/ui/contextual-actions-patterns/README.md` - shared placement contract for stray actions, pages, drawers, and tabs.
|
||||
- `docs/modules/ui/unified-audit-surfaces/README.md` - shipped canonical audit owner, alias contract, and secondary entry-point rules for cross-module audit browsing.
|
||||
- `docs/modules/ui/orphan-revival-batch/README.md` - queued execution batch for reviving selected orphan shared components and reconnecting disconnected route families without reopening duplicate top-level products.
|
||||
- `docs/features/checked/web/filter-bar-unification.md` - shipped verification note for the shared FilterBarComponent adoption on 7 mounted list pages (audit, security, release, evidence, trust families).
|
||||
- `docs/features/checked/web/filter-bar-unification.md` - shipped verification note for the shared FilterBarComponent adoption, now superseded on audit-family pages by the remediation rollback that restored missing filter semantics.
|
||||
- `docs/features/checked/web/orphan-revival-regression-remediation-ui.md` - shipped verification note for the orphan-revival regression remediation, including the audit/trust filter rollback, truthful findings/release rollback, policy build fixes, and canonical evidence-thread navigation repair.
|
||||
|
||||
## Dependencies
|
||||
- `docs/modules/ui/architecture.md`
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- The preservation-map and restoration-topic work already resolved the major product-shape questions for Policy Decisioning Studio, Reachability Witnessing, Workflow Visualization, Watchlist, Triage Explainability, and the consolidated Operations and Setup shells.
|
||||
- This batch covers the remaining lower-level orphaned shared components and disconnected route files that still look worth reviving after those larger product merges landed.
|
||||
- These sprints are intentionally queued for review. They are not started by default.
|
||||
- Follow-up remediation sprint `024` already proved that some orphan adoptions need bounded rollback when the shared contract removes mounted behavior or forces fabricated data. Treat sprint `015` and sprint `020` as cautionary examples, not as proof that every orphan shared component should stay mounted wherever it first lands.
|
||||
|
||||
## Corrections To The External Scan
|
||||
- `EvidenceDrawerComponent` is already mounted in `features/vulnerabilities/vulnerability-detail.component.html`; it is not a valid "finish the wiring" target in the current repo snapshot.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* @file audit-log-table.component.spec.ts
|
||||
* @sprint SPRINT_20260308_015_FE (FE-OFB-004)
|
||||
* @description Focused tests verifying FilterBarComponent adoption on the audit-log-table page.
|
||||
* @description Unit tests for AuditLogTableComponent filter behavior.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
@@ -11,7 +10,7 @@ import { AuditLogTableComponent } from './audit-log-table.component';
|
||||
import { AuditLogClient } from '../../core/api/audit-log.client';
|
||||
import { AuditEventsPagedResponse } from '../../core/api/audit-log.models';
|
||||
|
||||
describe('AuditLogTableComponent (filter-bar adoption)', () => {
|
||||
describe('AuditLogTableComponent', () => {
|
||||
let component: AuditLogTableComponent;
|
||||
let fixture: ComponentFixture<AuditLogTableComponent>;
|
||||
|
||||
@@ -44,72 +43,79 @@ describe('AuditLogTableComponent (filter-bar adoption)', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the shared filter bar', () => {
|
||||
const filterBar = fixture.nativeElement.querySelector('app-filter-bar');
|
||||
expect(filterBar).toBeTruthy();
|
||||
it('renders the audit-specific filter toolbar with actor search', () => {
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.querySelector('.filters-bar')).toBeTruthy();
|
||||
expect(element.textContent).toContain('Actor');
|
||||
expect(element.querySelector('app-filter-bar')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should expose four filter option groups (module, action, severity, dateRange)', () => {
|
||||
expect(component.filterOptions.length).toBe(4);
|
||||
expect(component.filterOptions.map(f => f.key)).toEqual(['module', 'action', 'severity', 'dateRange']);
|
||||
it('builds filters with multi-select arrays and actor name', () => {
|
||||
component.selectedModules = ['policy', 'scanner'];
|
||||
component.selectedActions = ['approve', 'replay'];
|
||||
component.selectedSeverities = ['warning', 'critical'];
|
||||
component.actorFilter = 'ops@stella.local';
|
||||
|
||||
expect(component.buildFilters()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
modules: ['policy', 'scanner'],
|
||||
actions: ['approve', 'replay'],
|
||||
severities: ['warning', 'critical'],
|
||||
actorName: 'ops@stella.local',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should populate module options from allModules array', () => {
|
||||
const moduleFilter = component.filterOptions.find(f => f.key === 'module');
|
||||
expect(moduleFilter).toBeTruthy();
|
||||
expect(moduleFilter!.options.length).toBe(component.allModules.length);
|
||||
expect(moduleFilter!.options[0].value).toBe('authority');
|
||||
it('builds custom start and end dates when custom range is selected', () => {
|
||||
component.dateRange = 'custom';
|
||||
component.customStartDate = '2026-03-01';
|
||||
component.customEndDate = '2026-03-07';
|
||||
|
||||
expect(component.buildFilters()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-07',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update selectedModules on onFilterChanged with module key', () => {
|
||||
component.onFilterChanged({ key: 'module', value: 'policy', label: 'Policy' });
|
||||
expect(component.selectedModules).toEqual(['policy']);
|
||||
it('resets paging state when applyFilters is called', () => {
|
||||
component.selectedModules = ['scanner'];
|
||||
component.cursor.set('cursor-1');
|
||||
(component as { cursorStack: string[] }).cursorStack = ['cursor-0'];
|
||||
component.hasPrev.set(true);
|
||||
|
||||
component.applyFilters();
|
||||
|
||||
expect(component.cursor()).toBeNull();
|
||||
expect(component.hasPrev()).toBeFalse();
|
||||
expect(mockClient.getEvents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear selectedModules on onFilterRemoved with module key', () => {
|
||||
component.selectedModules = ['policy'];
|
||||
component.onFilterRemoved({ key: 'module', value: 'policy', label: 'Module: Policy' });
|
||||
expect(component.selectedModules).toEqual([]);
|
||||
});
|
||||
it('clears all restored filters', () => {
|
||||
component.selectedModules = ['scanner'];
|
||||
component.selectedActions = ['replay'];
|
||||
component.selectedSeverities = ['error'];
|
||||
component.dateRange = 'custom';
|
||||
component.customStartDate = '2026-03-01';
|
||||
component.customEndDate = '2026-03-07';
|
||||
component.searchQuery = 'digest';
|
||||
component.actorFilter = 'operator';
|
||||
|
||||
it('should update dateRange on onFilterChanged with dateRange key', () => {
|
||||
component.onFilterChanged({ key: 'dateRange', value: '30d', label: 'Last 30 days' });
|
||||
expect(component.dateRange).toBe('30d');
|
||||
});
|
||||
|
||||
it('should reset dateRange to 7d on onFilterRemoved with dateRange key', () => {
|
||||
component.dateRange = '30d';
|
||||
component.onFilterRemoved({ key: 'dateRange', value: '30d', label: 'Date: Last 30 days' });
|
||||
expect(component.dateRange).toBe('7d');
|
||||
});
|
||||
|
||||
it('should update searchQuery on onSearchChange', () => {
|
||||
component.onSearchChange('test keyword');
|
||||
expect(component.searchQuery).toBe('test keyword');
|
||||
});
|
||||
|
||||
it('should rebuild active filter list when a filter is applied', () => {
|
||||
component.onFilterChanged({ key: 'severity', value: 'critical', label: 'Critical' });
|
||||
const active = component.activeFilterList();
|
||||
expect(active.length).toBeGreaterThanOrEqual(1);
|
||||
expect(active.find(f => f.key === 'severity')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clear all filters and reset active filter list', () => {
|
||||
component.onFilterChanged({ key: 'module', value: 'scanner', label: 'Scanner' });
|
||||
component.onFilterChanged({ key: 'severity', value: 'error', label: 'Error' });
|
||||
component.clearFilters();
|
||||
|
||||
expect(component.selectedModules.length).toBe(0);
|
||||
expect(component.selectedActions.length).toBe(0);
|
||||
expect(component.selectedSeverities.length).toBe(0);
|
||||
expect(component.dateRange).toBe('7d');
|
||||
expect(component.searchQuery).toBe('');
|
||||
expect(component.activeFilterList().length).toBe(0);
|
||||
expect(component.actorFilter).toBe('');
|
||||
expect(component.customStartDate).toBe('');
|
||||
expect(component.customEndDate).toBe('');
|
||||
});
|
||||
|
||||
it('should not include dateRange chip when dateRange is 7d (default)', () => {
|
||||
component.onFilterChanged({ key: 'module', value: 'vex', label: 'VEX' });
|
||||
const active = component.activeFilterList();
|
||||
expect(active.find(f => f.key === 'dateRange')).toBeFalsy();
|
||||
it('formats modules for the multi-select options', () => {
|
||||
expect(component.formatModule('authority')).toBe('Authority');
|
||||
expect(component.formatModule('jobengine')).toBe('JobEngine');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Sprint: SPRINT_20251229_028_FE - Unified Audit Log Viewer
|
||||
// Filter bar adoption: SPRINT_20260308_015_FE (FE-OFB-002)
|
||||
import { Component, OnInit, inject, signal, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AuditLogClient } from '../../core/api/audit-log.client';
|
||||
import { AuditEvent, AuditLogFilters, AuditModule, AuditAction, AuditSeverity } from '../../core/api/audit-log.models';
|
||||
import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/filter-bar/filter-bar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-audit-log-table',
|
||||
imports: [CommonModule, RouterModule, FormsModule, FilterBarComponent],
|
||||
imports: [CommonModule, RouterModule, FormsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="audit-table-page">
|
||||
@@ -21,15 +19,66 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/
|
||||
<h1>Audit Events</h1>
|
||||
</header>
|
||||
|
||||
<app-filter-bar
|
||||
searchPlaceholder="Search events by keyword or actor..."
|
||||
[filters]="filterOptions"
|
||||
[activeFilters]="activeFilterList()"
|
||||
(searchChange)="onSearchChange($event)"
|
||||
(filterChange)="onFilterChanged($event)"
|
||||
(filterRemove)="onFilterRemoved($event)"
|
||||
(filtersCleared)="clearFilters()"
|
||||
></app-filter-bar>
|
||||
<div class="filters-bar">
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label>Modules</label>
|
||||
<select multiple [(ngModel)]="selectedModules" (change)="applyFilters()">
|
||||
@for (m of allModules; track m) {
|
||||
<option [value]="m">{{ formatModule(m) }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Actions</label>
|
||||
<select multiple [(ngModel)]="selectedActions" (change)="applyFilters()">
|
||||
@for (a of allActions; track a) {
|
||||
<option [value]="a">{{ a }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Severity</label>
|
||||
<select multiple [(ngModel)]="selectedSeverities" (change)="applyFilters()">
|
||||
@for (s of allSeverities; track s) {
|
||||
<option [value]="s">{{ s }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Date Range</label>
|
||||
<select [(ngModel)]="dateRange" (change)="applyFilters()">
|
||||
<option value="24h">Last 24 hours</option>
|
||||
<option value="7d">Last 7 days</option>
|
||||
<option value="30d">Last 30 days</option>
|
||||
<option value="90d">Last 90 days</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
@if (dateRange === 'custom') {
|
||||
<div class="filter-group">
|
||||
<label>Start</label>
|
||||
<input type="date" [(ngModel)]="customStartDate" (change)="applyFilters()" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>End</label>
|
||||
<input type="date" [(ngModel)]="customEndDate" (change)="applyFilters()" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="filter-group search-group">
|
||||
<label>Search</label>
|
||||
<input type="text" [(ngModel)]="searchQuery" placeholder="Search events..." (keyup.enter)="applyFilters()" />
|
||||
<button class="btn-sm" (click)="applyFilters()">Search</button>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Actor</label>
|
||||
<input type="text" [(ngModel)]="actorFilter" placeholder="Username or email" (keyup.enter)="applyFilters()" />
|
||||
</div>
|
||||
<button class="btn-secondary" (click)="clearFilters()">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="loading">Loading events...</div>
|
||||
@@ -194,7 +243,18 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/
|
||||
.breadcrumb { font-size: 0.85rem; color: var(--color-text-secondary); margin-bottom: 0.5rem; }
|
||||
.breadcrumb a { color: var(--color-brand-primary); text-decoration: none; }
|
||||
.page-header h1 { margin: 0; }
|
||||
/* Filter bar styles handled by shared FilterBarComponent */
|
||||
.filters-bar { background: var(--color-surface-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-lg); padding: 1rem; margin-bottom: 1.5rem; }
|
||||
.filter-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
||||
.filter-row:last-child { margin-bottom: 0; }
|
||||
.filter-group { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.filter-group label { font-size: 0.75rem; color: var(--color-text-secondary); }
|
||||
.filter-group select, .filter-group input { padding: 0.5rem; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); font-size: 0.9rem; }
|
||||
.filter-group select[multiple] { height: 80px; }
|
||||
.search-group { flex: 1; min-width: 200px; flex-direction: row; align-items: flex-end; }
|
||||
.search-group label { display: none; }
|
||||
.search-group input { flex: 1; }
|
||||
.btn-sm { padding: 0.5rem 0.75rem; cursor: pointer; background: var(--color-brand-primary); color: var(--color-text-heading); border: none; border-radius: var(--radius-sm); }
|
||||
.btn-secondary { padding: 0.5rem 1rem; cursor: pointer; background: var(--color-surface-elevated); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); align-self: flex-end; }
|
||||
.loading { text-align: center; padding: 3rem; color: var(--color-text-secondary); }
|
||||
.events-table { width: 100%; border-collapse: collapse; background: var(--color-surface-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.events-table th, .events-table td { padding: 0.75rem 0.5rem; text-align: left; border-bottom: 1px solid var(--color-border-primary); font-size: 0.85rem; }
|
||||
@@ -286,37 +346,6 @@ export class AuditLogTableComponent implements OnInit {
|
||||
readonly allActions: AuditAction[] = ['create', 'update', 'delete', 'promote', 'demote', 'revoke', 'issue', 'refresh', 'test', 'fail', 'complete', 'start', 'submit', 'approve', 'reject', 'sign', 'verify', 'rotate', 'enable', 'disable', 'deadletter', 'replay'];
|
||||
readonly allSeverities: AuditSeverity[] = ['info', 'warning', 'error', 'critical'];
|
||||
|
||||
// Shared filter bar options
|
||||
readonly filterOptions: FilterOption[] = [
|
||||
{
|
||||
key: 'module',
|
||||
label: 'Module',
|
||||
options: this.allModules.map(m => ({ value: m, label: this.formatModule(m) })),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
label: 'Action',
|
||||
options: this.allActions.map(a => ({ value: a, label: a })),
|
||||
},
|
||||
{
|
||||
key: 'severity',
|
||||
label: 'Severity',
|
||||
options: this.allSeverities.map(s => ({ value: s, label: s.charAt(0).toUpperCase() + s.slice(1) })),
|
||||
},
|
||||
{
|
||||
key: 'dateRange',
|
||||
label: 'Date Range',
|
||||
options: [
|
||||
{ value: '24h', label: 'Last 24 hours' },
|
||||
{ value: '7d', label: 'Last 7 days' },
|
||||
{ value: '30d', label: 'Last 30 days' },
|
||||
{ value: '90d', label: 'Last 90 days' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
readonly activeFilterList = signal<ActiveFilter[]>([]);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadEvents();
|
||||
}
|
||||
@@ -365,7 +394,6 @@ export class AuditLogTableComponent implements OnInit {
|
||||
this.cursor.set(null);
|
||||
this.cursorStack = [];
|
||||
this.hasPrev.set(false);
|
||||
this.rebuildActiveFilters();
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
@@ -381,65 +409,6 @@ export class AuditLogTableComponent implements OnInit {
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onSearchChange(value: string): void {
|
||||
this.searchQuery = value;
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onFilterChanged(filter: ActiveFilter): void {
|
||||
switch (filter.key) {
|
||||
case 'module':
|
||||
this.selectedModules = [filter.value as AuditModule];
|
||||
break;
|
||||
case 'action':
|
||||
this.selectedActions = [filter.value as AuditAction];
|
||||
break;
|
||||
case 'severity':
|
||||
this.selectedSeverities = [filter.value as AuditSeverity];
|
||||
break;
|
||||
case 'dateRange':
|
||||
this.dateRange = filter.value;
|
||||
break;
|
||||
}
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onFilterRemoved(filter: ActiveFilter): void {
|
||||
switch (filter.key) {
|
||||
case 'module':
|
||||
this.selectedModules = [];
|
||||
break;
|
||||
case 'action':
|
||||
this.selectedActions = [];
|
||||
break;
|
||||
case 'severity':
|
||||
this.selectedSeverities = [];
|
||||
break;
|
||||
case 'dateRange':
|
||||
this.dateRange = '7d';
|
||||
break;
|
||||
}
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
private rebuildActiveFilters(): void {
|
||||
const filters: ActiveFilter[] = [];
|
||||
if (this.selectedModules.length > 0) {
|
||||
filters.push({ key: 'module', value: this.selectedModules[0], label: 'Module: ' + this.formatModule(this.selectedModules[0]) });
|
||||
}
|
||||
if (this.selectedActions.length > 0) {
|
||||
filters.push({ key: 'action', value: this.selectedActions[0], label: 'Action: ' + this.selectedActions[0] });
|
||||
}
|
||||
if (this.selectedSeverities.length > 0) {
|
||||
filters.push({ key: 'severity', value: this.selectedSeverities[0], label: 'Severity: ' + this.selectedSeverities[0] });
|
||||
}
|
||||
if (this.dateRange !== '7d') {
|
||||
const labels: Record<string, string> = { '24h': 'Last 24 hours', '30d': 'Last 30 days', '90d': 'Last 90 days' };
|
||||
filters.push({ key: 'dateRange', value: this.dateRange, label: 'Date: ' + (labels[this.dateRange] || this.dateRange) });
|
||||
}
|
||||
this.activeFilterList.set(filters);
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.cursor()) {
|
||||
this.cursorStack.push(this.cursor()!);
|
||||
|
||||
@@ -156,7 +156,7 @@ export class EvidenceThreadListComponent implements OnInit {
|
||||
|
||||
onRowClick(thread: EvidenceThread): void {
|
||||
const encodedDigest = encodeURIComponent(thread.artifactDigest);
|
||||
this.router.navigate(['/evidence-thread', encodedDigest]);
|
||||
this.router.navigate(['/evidence/threads', encodedDigest]);
|
||||
}
|
||||
|
||||
onRefresh(): void {
|
||||
|
||||
@@ -158,7 +158,7 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onBack(): void {
|
||||
this.router.navigate(['/evidence-thread']);
|
||||
this.router.navigate(['/evidence/threads']);
|
||||
}
|
||||
|
||||
getVerdictLabel(verdict?: EvidenceVerdict): string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation
|
||||
// Task: FE-OFL-004 - Verify shared FindingListComponent adoption on FindingsContainerComponent
|
||||
// Sprint: SPRINT_20260308_024_FE_orphan_revival_regression_remediation
|
||||
// Task: FE-ORM-004 - Verify truthful findings-container rollback on mounted surface
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -15,7 +15,7 @@ import { ViewPreferenceService, FindingsViewMode } from '../../../core/services/
|
||||
import { CompareService } from '../../compare/services/compare.service';
|
||||
import { SECURITY_FINDINGS_API } from '../../../core/api/security-findings.client';
|
||||
|
||||
describe('FindingsContainerComponent (shared FindingList adoption)', () => {
|
||||
describe('FindingsContainerComponent (truthful mounted findings view)', () => {
|
||||
let component: FindingsContainerComponent;
|
||||
let fixture: ComponentFixture<FindingsContainerComponent>;
|
||||
let queryParamMap$: BehaviorSubject<any>;
|
||||
@@ -110,99 +110,30 @@ describe('FindingsContainerComponent (shared FindingList adoption)', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should compute findingEvidenceItems from raw findings', async () => {
|
||||
it('loads live findings into the mounted findings list contract', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
expect(items.length).toBe(3);
|
||||
expect(component.findings().length).toBe(3);
|
||||
expect(component.findings()[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
id: 'finding-1',
|
||||
advisoryId: 'CVE-2026-8001',
|
||||
packageName: 'backend-api',
|
||||
packageVersion: '2.5.0',
|
||||
severity: 'critical',
|
||||
status: 'open',
|
||||
publishedAt: '2026-02-10T09:30:00Z',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should map finding_id from Finding.id', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
expect(items[0].finding_id).toBe('finding-1');
|
||||
expect(items[1].finding_id).toBe('finding-2');
|
||||
expect(items[2].finding_id).toBe('finding-3');
|
||||
});
|
||||
|
||||
it('should map cve from Finding.advisoryId', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
expect(items[0].cve).toBe('CVE-2026-8001');
|
||||
expect(items[1].cve).toBe('CVE-2026-8002');
|
||||
});
|
||||
|
||||
it('should map component name and version', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
expect(items[0].component?.name).toBe('backend-api');
|
||||
expect(items[0].component?.version).toBe('2.5.0');
|
||||
expect(items[1].component?.name).toBe('frontend-lib');
|
||||
expect(items[1].component?.version).toBe('1.0.3');
|
||||
});
|
||||
|
||||
it('should map severity to risk_score', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
// critical -> 90
|
||||
expect(items[0].score_explain?.risk_score).toBe(90);
|
||||
// high -> 70
|
||||
expect(items[1].score_explain?.risk_score).toBe(70);
|
||||
// low -> 20
|
||||
expect(items[2].score_explain?.risk_score).toBe(20);
|
||||
});
|
||||
|
||||
it('should map fixed status to VEX fixed', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
// finding-2 has vexStatus 'fixed', delta 'resolved' -> status = 'fixed'
|
||||
expect(items[1].vex?.status).toBe('fixed');
|
||||
});
|
||||
|
||||
it('should map excepted status to VEX not_affected', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.findingEvidenceItems();
|
||||
// finding-3 has vexStatus 'not_affected' -> status = 'excepted'
|
||||
expect(items[2].vex?.status).toBe('not_affected');
|
||||
});
|
||||
|
||||
it('should render shared finding list host element in detail view', async () => {
|
||||
it('renders the truthful bespoke findings list instead of the shared evidence row list', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
const host = el.querySelector('[data-testid="shared-finding-list-host"]');
|
||||
expect(host).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render stella-finding-list element in detail view', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
const list = el.querySelector('stella-finding-list');
|
||||
expect(list).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render bespoke app-findings-list element', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
const bespoke = el.querySelector('app-findings-list');
|
||||
expect(bespoke).toBeFalsy();
|
||||
expect(el.querySelector('app-findings-list')).toBeTruthy();
|
||||
expect(el.querySelector('stella-finding-list')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// findings-container.component.ts
|
||||
// Sprint: SPRINT_1227_0005_0001_FE_diff_first_default
|
||||
// Task: T3 — Container component for findings with view switching
|
||||
// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation
|
||||
// Task: FE-OFL-002 — Adopt shared FindingListComponent on canonical findings surface
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core';
|
||||
@@ -20,9 +18,7 @@ import { forkJoin, of } from 'rxjs';
|
||||
import { ViewPreferenceService, FindingsViewMode } from '../../../core/services/view-preference.service';
|
||||
import { FindingsViewToggleComponent } from '../../../shared/components/findings-view-toggle/findings-view-toggle.component';
|
||||
import { CompareViewComponent } from '../../compare/components/compare-view/compare-view.component';
|
||||
import { FindingListComponent } from '../../../shared/components/finding-list.component';
|
||||
import type { FindingEvidenceResponse } from '../../../core/api/triage-evidence.models';
|
||||
import { type Finding } from '../findings-list.component';
|
||||
import { FindingsListComponent, type Finding } from '../findings-list.component';
|
||||
import { CompareService } from '../../compare/services/compare.service';
|
||||
import {
|
||||
SECURITY_FINDINGS_API,
|
||||
@@ -102,7 +98,7 @@ function mapFinding(source: DetailFindingSource): Finding {
|
||||
MatProgressSpinnerModule,
|
||||
FindingsViewToggleComponent,
|
||||
CompareViewComponent,
|
||||
FindingListComponent,
|
||||
FindingsListComponent
|
||||
],
|
||||
template: `
|
||||
<div class="findings-container">
|
||||
@@ -153,11 +149,9 @@ function mapFinding(source: DetailFindingSource): Finding {
|
||||
<p>{{ message }}</p>
|
||||
</section>
|
||||
} @else {
|
||||
<stella-finding-list
|
||||
[findings]="findingEvidenceItems()"
|
||||
[loading]="loading()"
|
||||
data-testid="shared-finding-list-host"
|
||||
(findingSelected)="onFindingSelected($event)" />
|
||||
<app-findings-list
|
||||
[findings]="findings()"
|
||||
(findingSelect)="onFindingSelect($event)" />
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,15 +257,9 @@ export class FindingsContainerComponent implements OnInit {
|
||||
// Loading state
|
||||
readonly loading = signal(false);
|
||||
|
||||
// Findings for detail view (raw Finding model)
|
||||
// Findings for detail view
|
||||
readonly findings = signal<Finding[]>([]);
|
||||
|
||||
// Adapter: map Finding[] to FindingEvidenceResponse[] for the shared FindingListComponent
|
||||
// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-002)
|
||||
readonly findingEvidenceItems = computed<FindingEvidenceResponse[]>(() => {
|
||||
return this.findings().map((f) => this.mapFindingToEvidence(f));
|
||||
});
|
||||
|
||||
// Detail view data failure state
|
||||
readonly detailError = signal<string | null>(null);
|
||||
|
||||
@@ -352,52 +340,6 @@ export class FindingsContainerComponent implements OnInit {
|
||||
console.log('Selected finding:', finding.id);
|
||||
}
|
||||
|
||||
/** Handler for the shared FindingListComponent's findingSelected output (emits finding_id string). */
|
||||
onFindingSelected(findingId: string): void {
|
||||
const finding = this.findings().find((f) => f.id === findingId);
|
||||
if (finding) {
|
||||
this.onFindingSelect(finding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter: map the bespoke Finding interface to FindingEvidenceResponse
|
||||
* so the shared FindingListComponent can render it.
|
||||
* Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-002)
|
||||
*/
|
||||
private mapFindingToEvidence(finding: Finding): FindingEvidenceResponse {
|
||||
const severityScore: Record<string, number> = {
|
||||
critical: 90,
|
||||
high: 70,
|
||||
medium: 45,
|
||||
low: 20,
|
||||
unknown: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
finding_id: finding.id,
|
||||
cve: finding.advisoryId,
|
||||
component: {
|
||||
purl: `pkg:generic/${finding.packageName}@${finding.packageVersion}`,
|
||||
name: finding.packageName,
|
||||
version: finding.packageVersion,
|
||||
type: 'generic',
|
||||
},
|
||||
score_explain: {
|
||||
kind: 'ews',
|
||||
risk_score: severityScore[finding.severity] ?? 0,
|
||||
last_seen: finding.publishedAt ?? new Date().toISOString(),
|
||||
summary: `Severity: ${finding.severity}, Status: ${finding.status}`,
|
||||
},
|
||||
vex: finding.status === 'fixed'
|
||||
? { status: 'fixed' }
|
||||
: finding.status === 'excepted'
|
||||
? { status: 'not_affected', justification: 'Excepted' }
|
||||
: undefined,
|
||||
last_seen: finding.publishedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private readViewMode(raw: string | null): FindingsViewMode | null {
|
||||
return raw === 'diff' || raw === 'detail' ? raw : null;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation
|
||||
// Task: FE-OFL-004 - Verify shared FindingListComponent adoption on ReleaseDetailComponent
|
||||
// Sprint: SPRINT_20260308_024_FE_orphan_revival_regression_remediation
|
||||
// Task: FE-ORM-004 - Verify truthful release security rendering on mounted surface
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { of, BehaviorSubject, EMPTY } from 'rxjs';
|
||||
import { BehaviorSubject, EMPTY } from 'rxjs';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
import { ReleaseDetailComponent } from './release-detail.component';
|
||||
import { PlatformContextStore } from '../../../../core/context/platform-context.store';
|
||||
import { ReleaseManagementStore } from '../release.store';
|
||||
|
||||
describe('ReleaseDetailComponent (shared FindingList adoption)', () => {
|
||||
describe('ReleaseDetailComponent (truthful release security rendering)', () => {
|
||||
let component: ReleaseDetailComponent;
|
||||
let fixture: ComponentFixture<ReleaseDetailComponent>;
|
||||
|
||||
@@ -36,14 +36,14 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => {
|
||||
const queryParamMap$ = new BehaviorSubject(convertToParamMap({}));
|
||||
|
||||
const mockContext = {
|
||||
environment: signal('production'),
|
||||
region: signal('us-east-1'),
|
||||
tenant: signal('demo-prod'),
|
||||
selectedRegions: signal<string[]>([]),
|
||||
selectedEnvironments: signal<string[]>([]),
|
||||
};
|
||||
|
||||
const mockStore = {
|
||||
selectedRelease: signal(mockRelease),
|
||||
releases: signal([mockRelease]),
|
||||
selectRelease: jasmine.createSpy('selectRelease'),
|
||||
loadRelease: () => EMPTY,
|
||||
};
|
||||
|
||||
@@ -69,6 +69,8 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => {
|
||||
|
||||
fixture = TestBed.createComponent(ReleaseDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.mode.set('version');
|
||||
component.activeTab.set('security-inputs');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -76,46 +78,8 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should compute securityFindingEvidenceItems from empty findings', () => {
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should compute securityFindingEvidenceItems from security findings', () => {
|
||||
// Set findings directly via the signal
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
{
|
||||
findingId: 'f-002',
|
||||
cveId: 'CVE-2026-9002',
|
||||
severity: 'high',
|
||||
componentName: 'auth-lib',
|
||||
releaseId: 'rel-001',
|
||||
reachable: false,
|
||||
reachabilityScore: 0,
|
||||
effectiveDisposition: 'review_required',
|
||||
vexStatus: 'not_affected',
|
||||
exceptionStatus: 'approved',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should map finding_id from SecurityFindingProjection.findingId', () => {
|
||||
(component as any).findings.set([
|
||||
it('renders the mounted release security table with truthful fields', () => {
|
||||
component.findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
@@ -130,148 +94,21 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].finding_id).toBe('f-001');
|
||||
fixture.detectChanges();
|
||||
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.querySelector('stella-finding-list')).toBeFalsy();
|
||||
expect(element.textContent).toContain('CVE-2026-9001');
|
||||
expect(element.textContent).toContain('libcrypto');
|
||||
expect(element.textContent).toContain('action_required');
|
||||
expect(element.textContent).toContain('yes (85)');
|
||||
});
|
||||
|
||||
it('should map cve from SecurityFindingProjection.cveId', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
]);
|
||||
it('shows the legacy empty-state row when the release has no findings', () => {
|
||||
component.findings.set([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].cve).toBe('CVE-2026-9001');
|
||||
});
|
||||
|
||||
it('should map component name from SecurityFindingProjection.componentName', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].component?.name).toBe('libcrypto');
|
||||
});
|
||||
|
||||
it('should map severity to risk_score (critical=90)', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].score_explain?.risk_score).toBe(90);
|
||||
});
|
||||
|
||||
it('should map reachable=true to non-empty reachable_path', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].reachable_path).toBeDefined();
|
||||
expect(items[0].reachable_path!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should map reachable=false to undefined reachable_path', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-002',
|
||||
cveId: 'CVE-2026-9002',
|
||||
severity: 'high',
|
||||
componentName: 'auth-lib',
|
||||
releaseId: 'rel-001',
|
||||
reachable: false,
|
||||
reachabilityScore: 0,
|
||||
effectiveDisposition: 'review_required',
|
||||
vexStatus: 'not_affected',
|
||||
exceptionStatus: 'approved',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].reachable_path).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should map vexStatus not_affected to VEX evidence', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-002',
|
||||
cveId: 'CVE-2026-9002',
|
||||
severity: 'high',
|
||||
componentName: 'auth-lib',
|
||||
releaseId: 'rel-001',
|
||||
reachable: false,
|
||||
reachabilityScore: 0,
|
||||
effectiveDisposition: 'review_required',
|
||||
vexStatus: 'not_affected',
|
||||
exceptionStatus: 'approved',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].vex?.status).toBe('not_affected');
|
||||
});
|
||||
|
||||
it('should not produce VEX evidence for under_investigation status', () => {
|
||||
(component as any).findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = component.securityFindingEvidenceItems();
|
||||
expect(items[0].vex).toBeUndefined();
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.textContent).toContain('No findings.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,6 @@ import { ReleaseManagementStore } from '../release.store';
|
||||
import { getEvidencePostureLabel, getGateStatusLabel, getRiskTierLabel } from '../../../../core/api/release-management.models';
|
||||
import type { ManagedRelease } from '../../../../core/api/release-management.models';
|
||||
import { DegradedStateBannerComponent } from '../../../../shared/components/degraded-state-banner/degraded-state-banner.component';
|
||||
import { FindingListComponent } from '../../../../shared/components/finding-list.component';
|
||||
import type { FindingEvidenceResponse } from '../../../../core/api/triage-evidence.models';
|
||||
import { buildContextReturnTo } from '../../../../shared/ui/context-route-state/context-route-state';
|
||||
import { AuditorOnlyDirective } from '../../../../shared/directives/auditor-only.directive';
|
||||
import { OperatorOnlyDirective } from '../../../../shared/directives/operator-only.directive';
|
||||
@@ -140,7 +138,7 @@ interface ReloadOptions {
|
||||
@Component({
|
||||
selector: 'app-release-detail',
|
||||
standalone: true,
|
||||
imports: [RouterLink, FormsModule, DegradedStateBannerComponent, FindingListComponent, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
imports: [RouterLink, FormsModule, DegradedStateBannerComponent, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="workbench">
|
||||
@@ -269,13 +267,17 @@ interface ReloadOptions {
|
||||
@case ('security-inputs') {
|
||||
<article>
|
||||
<h3>Release-Scoped Security</h3>
|
||||
<stella-finding-list
|
||||
[findings]="securityFindingEvidenceItems()"
|
||||
[showApprove]="false"
|
||||
[emptyMessage]="'No findings for this release.'"
|
||||
data-testid="release-security-finding-list-host"
|
||||
(findingSelected)="onSecurityFindingSelected($event)"
|
||||
/>
|
||||
<table>
|
||||
<thead><tr><th>CVE</th><th>Component</th><th>Severity</th><th>Reachable</th><th>VEX</th><th>Exception</th><th>Blocks Promotion</th></tr></thead>
|
||||
<tbody>
|
||||
@for (item of findings(); track item.findingId) {
|
||||
<tr>
|
||||
<td>{{ item.cveId }}</td><td>{{ item.componentName }}</td><td>{{ item.severity }}</td><td>{{ item.reachable ? 'yes' : 'no' }} ({{ item.reachabilityScore }})</td>
|
||||
<td>{{ item.vexStatus }}</td><td>{{ item.exceptionStatus }}</td><td>{{ item.effectiveDisposition === 'action_required' ? 'yes' : 'no' }}</td>
|
||||
</tr>
|
||||
} @empty { <tr><td colspan="7">No findings.</td></tr> }
|
||||
</tbody>
|
||||
</table>
|
||||
<p><button type="button" (click)="openGlobalFindings()">Open Findings</button> <button type="button" (click)="openReachabilityWorkspace()">Open Reachability</button> <button type="button" (click)="createException()">Create Exception</button> <button type="button" (click)="openDecisioningStudio()">Open Decisioning Studio</button> <button type="button" (click)="openTab('rollback')">Compare Baseline</button> <button type="button" class="primary" (click)="exportSecurityEvidence()">Export Security Evidence</button></p>
|
||||
</article>
|
||||
}
|
||||
@@ -420,12 +422,6 @@ export class ReleaseDetailComponent {
|
||||
readonly diffRows = signal<SecuritySbomDiffRow[]>([]);
|
||||
readonly diffMode = signal<'sbom'|'findings'|'policy'|'topology'>('sbom');
|
||||
|
||||
// Adapter: map SecurityFindingProjection[] to FindingEvidenceResponse[] for the shared FindingListComponent
|
||||
// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-003)
|
||||
readonly securityFindingEvidenceItems = computed<FindingEvidenceResponse[]>(() => {
|
||||
return this.findings().map((f) => this.mapSecurityFindingToEvidence(f));
|
||||
});
|
||||
|
||||
readonly selectedTimelineId = signal<string | null>(null);
|
||||
readonly selectedTargets = signal<Set<string>>(new Set<string>());
|
||||
|
||||
@@ -1278,52 +1274,5 @@ export class ReleaseDetailComponent {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Handler for the shared FindingListComponent's findingSelected output. */
|
||||
onSecurityFindingSelected(findingId: string): void {
|
||||
const finding = this.findings().find((f) => f.findingId === findingId);
|
||||
if (finding) {
|
||||
void this.router.navigate(['/security/triage'], {
|
||||
queryParams: { releaseId: this.releaseContextId(), findingId: finding.findingId, cve: finding.cveId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter: map SecurityFindingProjection to FindingEvidenceResponse
|
||||
* for the shared FindingListComponent.
|
||||
* Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-003)
|
||||
*/
|
||||
private mapSecurityFindingToEvidence(f: SecurityFindingProjection): FindingEvidenceResponse {
|
||||
const severityScore: Record<string, number> = {
|
||||
critical: 90,
|
||||
high: 70,
|
||||
medium: 45,
|
||||
low: 20,
|
||||
};
|
||||
|
||||
return {
|
||||
finding_id: f.findingId,
|
||||
cve: f.cveId,
|
||||
component: {
|
||||
purl: `pkg:generic/${f.componentName}`,
|
||||
name: f.componentName,
|
||||
version: '',
|
||||
type: 'generic',
|
||||
},
|
||||
reachable_path: f.reachable ? ['reachable'] : undefined,
|
||||
score_explain: {
|
||||
kind: 'ews',
|
||||
risk_score: severityScore[f.severity.toLowerCase()] ?? 0,
|
||||
last_seen: new Date().toISOString(),
|
||||
summary: `Severity: ${f.severity}, Reachability: ${f.reachabilityScore}, Disposition: ${f.effectiveDisposition}`,
|
||||
},
|
||||
vex: f.vexStatus && f.vexStatus !== 'under_investigation'
|
||||
? { status: f.vexStatus as 'not_affected' | 'affected' | 'fixed' | 'under_investigation' }
|
||||
: undefined,
|
||||
last_seen: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* @file trust-audit-log.component.ts
|
||||
* @sprint SPRINT_20251229_018c_FE
|
||||
* @description Trust audit log viewer
|
||||
* Filter bar adoption: SPRINT_20260308_015_FE (FE-OFB-003)
|
||||
*/
|
||||
|
||||
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit } from '@angular/core';
|
||||
@@ -17,16 +16,76 @@ import {
|
||||
ListAuditEventsParams,
|
||||
TrustAuditFilter,
|
||||
} from '../../core/api/trust.models';
|
||||
import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/filter-bar/filter-bar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-trust-audit-log',
|
||||
imports: [CommonModule, FormsModule, FilterBarComponent],
|
||||
imports: [CommonModule, FormsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="audit-log">
|
||||
<!-- Shared Filter Bar -->
|
||||
<div class="audit-log__actions">
|
||||
<!-- Filters -->
|
||||
<div class="audit-log__filters">
|
||||
<div class="filter-group">
|
||||
<label for="search">Search</label>
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
placeholder="Search events..."
|
||||
[ngModel]="searchQuery()"
|
||||
(ngModelChange)="searchQuery.set($event); onSearch()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="resourceType">Resource Type</label>
|
||||
<select
|
||||
id="resourceType"
|
||||
[ngModel]="selectedResourceType()"
|
||||
(ngModelChange)="selectedResourceType.set($event); onFilterChange()"
|
||||
>
|
||||
<option value="all">All Resources</option>
|
||||
<option value="key">Keys</option>
|
||||
<option value="issuer">Issuers</option>
|
||||
<option value="certificate">Certificates</option>
|
||||
<option value="config">Config</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="severity">Severity</label>
|
||||
<select
|
||||
id="severity"
|
||||
[ngModel]="selectedSeverity()"
|
||||
(ngModelChange)="selectedSeverity.set($event); onFilterChange()"
|
||||
>
|
||||
<option value="all">All Severities</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="startDate">Start Date</label>
|
||||
<input
|
||||
id="startDate"
|
||||
type="date"
|
||||
[ngModel]="startDate()"
|
||||
(ngModelChange)="startDate.set($event); onFilterChange()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="endDate">End Date</label>
|
||||
<input
|
||||
id="endDate"
|
||||
type="date"
|
||||
[ngModel]="endDate()"
|
||||
(ngModelChange)="endDate.set($event); onFilterChange()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-export"
|
||||
@@ -35,17 +94,13 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/
|
||||
>
|
||||
{{ exporting() ? 'Exporting...' : 'Export' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<app-filter-bar
|
||||
searchPlaceholder="Search events..."
|
||||
[filters]="trustFilterOptions"
|
||||
[activeFilters]="activeTrustFilters()"
|
||||
(searchChange)="onTrustSearch($event)"
|
||||
(filterChange)="onTrustFilterChanged($event)"
|
||||
(filterRemove)="onTrustFilterRemoved($event)"
|
||||
(filtersCleared)="clearFilters()"
|
||||
></app-filter-bar>
|
||||
@if (hasFilters()) {
|
||||
<button type="button" class="btn-link" (click)="clearFilters()">
|
||||
Clear filters
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Events List -->
|
||||
<div class="audit-log__content">
|
||||
@@ -174,12 +229,39 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Filter bar styles handled by shared FilterBarComponent */
|
||||
|
||||
.audit-log__actions {
|
||||
.audit-log__filters {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select {
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
padding: 0.5rem 0.75rem;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filter-group input:focus,
|
||||
.filter-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-status-info);
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
@@ -476,14 +558,6 @@ export class TrustAuditLogComponent implements OnInit {
|
||||
this.endDate() !== ''
|
||||
);
|
||||
|
||||
// Shared filter bar integration
|
||||
readonly trustFilterOptions: FilterOption[] = [
|
||||
{ key: 'resourceType', label: 'Resource Type', options: [{ value: 'key', label: 'Keys' }, { value: 'issuer', label: 'Issuers' }, { value: 'certificate', label: 'Certificates' }, { value: 'config', label: 'Config' }] },
|
||||
{ key: 'severity', label: 'Severity', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }, { value: 'critical', label: 'Critical' }] },
|
||||
];
|
||||
|
||||
readonly activeTrustFilters = signal<ActiveFilter[]>([]);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadEvents();
|
||||
}
|
||||
@@ -526,52 +600,14 @@ export class TrustAuditLogComponent implements OnInit {
|
||||
|
||||
onSearch(): void {
|
||||
this.pageNumber.set(1);
|
||||
this.rebuildActiveTrustFilters();
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
onFilterChange(): void {
|
||||
this.pageNumber.set(1);
|
||||
this.rebuildActiveTrustFilters();
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
onTrustSearch(value: string): void {
|
||||
this.searchQuery.set(value);
|
||||
this.onSearch();
|
||||
}
|
||||
|
||||
onTrustFilterChanged(filter: ActiveFilter): void {
|
||||
if (filter.key === 'resourceType') {
|
||||
this.selectedResourceType.set(filter.value as any);
|
||||
} else if (filter.key === 'severity') {
|
||||
this.selectedSeverity.set(filter.value as any);
|
||||
}
|
||||
this.onFilterChange();
|
||||
}
|
||||
|
||||
onTrustFilterRemoved(filter: ActiveFilter): void {
|
||||
if (filter.key === 'resourceType') {
|
||||
this.selectedResourceType.set('all');
|
||||
} else if (filter.key === 'severity') {
|
||||
this.selectedSeverity.set('all');
|
||||
}
|
||||
this.onFilterChange();
|
||||
}
|
||||
|
||||
private rebuildActiveTrustFilters(): void {
|
||||
const filters: ActiveFilter[] = [];
|
||||
if (this.selectedResourceType() !== 'all') {
|
||||
const opt = this.trustFilterOptions[0].options.find(o => o.value === this.selectedResourceType());
|
||||
filters.push({ key: 'resourceType', value: this.selectedResourceType(), label: 'Resource: ' + (opt?.label || this.selectedResourceType()) });
|
||||
}
|
||||
if (this.selectedSeverity() !== 'all') {
|
||||
const opt = this.trustFilterOptions[1].options.find(o => o.value === this.selectedSeverity());
|
||||
filters.push({ key: 'severity', value: this.selectedSeverity() as string, label: 'Severity: ' + (opt?.label || this.selectedSeverity()) });
|
||||
}
|
||||
this.activeTrustFilters.set(filters);
|
||||
}
|
||||
|
||||
onPageChange(page: number): void {
|
||||
this.pageNumber.set(page);
|
||||
this.loadEvents();
|
||||
@@ -583,7 +619,6 @@ export class TrustAuditLogComponent implements OnInit {
|
||||
this.selectedSeverity.set('all');
|
||||
this.startDate.set('');
|
||||
this.endDate.set('');
|
||||
this.activeTrustFilters.set([]);
|
||||
this.pageNumber.set(1);
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RemediationHintComponent } from './remediation-hint.component';
|
||||
import { PolicyEvaluateResponse, GateEvaluation } from '../../core/api/policy-interop.models';
|
||||
import { PolicyEvaluateResponse } from '../../../core/api/policy-interop.models';
|
||||
|
||||
@Component({
|
||||
selector: 'stella-policy-evaluate-panel',
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
PolicyGateDefinition,
|
||||
PolicyRuleDefinition,
|
||||
PolicyGateTypes,
|
||||
} from '../../core/api/policy-interop.models';
|
||||
} from '../../../core/api/policy-interop.models';
|
||||
|
||||
interface GateEditState {
|
||||
gate: PolicyGateDefinition;
|
||||
@@ -269,7 +269,7 @@ export class PolicyPackEditorComponent {
|
||||
if (value) {
|
||||
this._document.set(value);
|
||||
this._gateStates.set(
|
||||
(value.spec.gates || []).map((g) => ({ gate: { ...g }, expanded: false }))
|
||||
(value.spec.gates || []).map((gate: PolicyGateDefinition) => ({ gate: { ...gate }, expanded: false }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { RemediationHint } from '../../core/api/policy-interop.models';
|
||||
import { RemediationHint } from '../../../core/api/policy-interop.models';
|
||||
|
||||
@Component({
|
||||
selector: 'stella-remediation-hint',
|
||||
|
||||
@@ -147,10 +147,11 @@ export class GlossaryTooltipDirective implements OnInit, OnDestroy {
|
||||
private showTooltip(entry: GlossaryEntry, anchor: HTMLElement): void {
|
||||
this.hideTooltip();
|
||||
|
||||
this.tooltipElement = this.renderer.createElement('div');
|
||||
this.renderer.addClass(this.tooltipElement, 'glossary-tooltip');
|
||||
this.renderer.setAttribute(this.tooltipElement, 'role', 'tooltip');
|
||||
this.renderer.setAttribute(this.tooltipElement, 'id', `glossary-tooltip-${entry.term.toLowerCase()}`);
|
||||
const tooltip = this.renderer.createElement('div') as HTMLElement;
|
||||
this.tooltipElement = tooltip;
|
||||
this.renderer.addClass(tooltip, 'glossary-tooltip');
|
||||
this.renderer.setAttribute(tooltip, 'role', 'tooltip');
|
||||
this.renderer.setAttribute(tooltip, 'id', `glossary-tooltip-${entry.term.toLowerCase()}`);
|
||||
|
||||
const content = `
|
||||
<div class="glossary-tooltip__header">
|
||||
@@ -164,8 +165,8 @@ export class GlossaryTooltipDirective implements OnInit, OnDestroy {
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.tooltipElement.innerHTML = content;
|
||||
this.renderer.appendChild(document.body, this.tooltipElement);
|
||||
this.renderer.setProperty(tooltip, 'innerHTML', content);
|
||||
this.renderer.appendChild(document.body, tooltip);
|
||||
|
||||
// Position tooltip
|
||||
this.positionTooltip(anchor);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router, provideRouter } from '@angular/router';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
|
||||
@@ -67,7 +66,7 @@ describe('Evidence thread browser', () => {
|
||||
it('navigates to encoded thread digest when a row is opened', () => {
|
||||
component.onRowClick(thread);
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/evidence-thread',
|
||||
'/evidence/threads',
|
||||
encodeURIComponent('sha256:artifact-1'),
|
||||
]);
|
||||
});
|
||||
@@ -76,6 +75,7 @@ describe('Evidence thread browser', () => {
|
||||
describe('EvidenceThreadViewComponent', () => {
|
||||
let fixture: ComponentFixture<EvidenceThreadViewComponent>;
|
||||
let component: EvidenceThreadViewComponent;
|
||||
let router: Router;
|
||||
let routeParams$: BehaviorSubject<Record<string, string>>;
|
||||
|
||||
const graph: EvidenceThreadGraph = {
|
||||
@@ -154,6 +154,9 @@ describe('Evidence thread browser', () => {
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
|
||||
|
||||
fixture = TestBed.createComponent(EvidenceThreadViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
@@ -164,40 +167,10 @@ describe('Evidence thread browser', () => {
|
||||
expect(component.thread()?.thread.id).toBe('thread-1');
|
||||
});
|
||||
|
||||
it('shows snackbar when clipboard API is unavailable', () => {
|
||||
const openSpy = spyOn((component as unknown as { snackBar: MatSnackBar }).snackBar, 'open');
|
||||
routeParams$.next({ artifactDigest: '' });
|
||||
fixture.detectChanges();
|
||||
it('navigates back to the canonical evidence threads list', () => {
|
||||
component.onBack();
|
||||
|
||||
component.copyDigest();
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith('Clipboard not available', 'Dismiss', {
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows snackbar when clipboard write rejects', async () => {
|
||||
const openSpy = spyOn((component as unknown as { snackBar: MatSnackBar }).snackBar, 'open');
|
||||
if (!navigator.clipboard) {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText: (_value: string) => Promise.resolve() },
|
||||
});
|
||||
}
|
||||
|
||||
const writeText = spyOn(navigator.clipboard, 'writeText').and.returnValue(
|
||||
Promise.reject(new Error('clipboard denied'))
|
||||
);
|
||||
|
||||
component.copyDigest();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(writeText).toHaveBeenCalled();
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith('Failed to copy digest', 'Dismiss', {
|
||||
duration: 3000,
|
||||
});
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/evidence/threads']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
|
||||
import { BehaviorSubject, EMPTY, of } from 'rxjs';
|
||||
|
||||
import { AuditLogTableComponent } from '../../app/features/audit-log/audit-log-table.component';
|
||||
import { FindingsContainerComponent } from '../../app/features/findings/container/findings-container.component';
|
||||
import { PolicyDecisioningGatesPageComponent } from '../../app/features/policy-decisioning/policy-decisioning-gates-page.component';
|
||||
import { ReleaseDetailComponent } from '../../app/features/release-orchestrator/releases/release-detail/release-detail.component';
|
||||
import { TrustAuditLogComponent } from '../../app/features/trust-admin/trust-audit-log.component';
|
||||
import { AuditLogClient } from '../../app/core/api/audit-log.client';
|
||||
import { SECURITY_FINDINGS_API } from '../../app/core/api/security-findings.client';
|
||||
import { PlatformContextStore } from '../../app/core/context/platform-context.store';
|
||||
import { ReleaseManagementStore } from '../../app/features/release-orchestrator/releases/release.store';
|
||||
import { CompareService } from '../../app/features/compare/services/compare.service';
|
||||
import { MockScoringApi, SCORING_API } from '../../app/core/services/scoring.service';
|
||||
import { ViewPreferenceService, FindingsViewMode } from '../../app/core/services/view-preference.service';
|
||||
import { TRUST_API } from '../../app/core/api/trust.client';
|
||||
import { PolicyPackEditorComponent } from '../../app/shared/components/policy/policy-pack-editor.component';
|
||||
|
||||
describe('Orphan revival regression remediation', () => {
|
||||
describe('policy hosts', () => {
|
||||
it('renders the policy evaluation panel on the mounted gates page', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyDecisioningGatesPageComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParams: of({}),
|
||||
queryParamMap: of(convertToParamMap({})),
|
||||
snapshot: {
|
||||
root: {
|
||||
queryParams: {},
|
||||
queryParamMap: convertToParamMap({}),
|
||||
},
|
||||
queryParams: {},
|
||||
queryParamMap: convertToParamMap({}),
|
||||
paramMap: convertToParamMap({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(PolicyDecisioningGatesPageComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.querySelector('stella-policy-evaluate-panel')).toBeTruthy();
|
||||
expect(element.textContent).toContain('Decision:');
|
||||
expect(element.textContent).toContain('WARN');
|
||||
expect(element.textContent).toContain('Gate Results');
|
||||
});
|
||||
|
||||
it('renders the shared policy pack editor with a valid pack document', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyPackEditorComponent],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(PolicyPackEditorComponent);
|
||||
fixture.componentInstance.policyPack = {
|
||||
apiVersion: 'policy.stella/v2',
|
||||
kind: 'PolicyPack',
|
||||
metadata: {
|
||||
name: 'Release Safety',
|
||||
version: '3.1.0',
|
||||
digest: 'sha256:pack',
|
||||
},
|
||||
spec: {
|
||||
settings: {
|
||||
default_action: 'block',
|
||||
deterministic_mode: true,
|
||||
},
|
||||
gates: [
|
||||
{
|
||||
id: 'cvss-threshold',
|
||||
type: 'CvssThresholdGate',
|
||||
enabled: true,
|
||||
config: { threshold: 8 },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.textContent).toContain('Release Safety');
|
||||
expect(element.textContent).toContain('Gates');
|
||||
});
|
||||
});
|
||||
|
||||
describe('audit filtering', () => {
|
||||
let fixture: ComponentFixture<AuditLogTableComponent>;
|
||||
let component: AuditLogTableComponent;
|
||||
const auditClient = {
|
||||
getEvents: jasmine.createSpy('getEvents').and.returnValue(
|
||||
of({ items: [], cursor: null, hasMore: false }),
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AuditLogTableComponent],
|
||||
providers: [provideRouter([]), { provide: AuditLogClient, useValue: auditClient }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AuditLogTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
auditClient.getEvents.calls.reset();
|
||||
});
|
||||
|
||||
it('restores actor filtering and custom date ranges', () => {
|
||||
component.actorFilter = 'auditor@stella.local';
|
||||
component.dateRange = 'custom';
|
||||
component.customStartDate = '2026-03-01';
|
||||
component.customEndDate = '2026-03-08';
|
||||
|
||||
expect(component.buildFilters()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
actorName: 'auditor@stella.local',
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-08',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves multi-select module, action, and severity filters', () => {
|
||||
component.selectedModules = ['policy', 'scanner'];
|
||||
component.selectedActions = ['approve', 'replay'];
|
||||
component.selectedSeverities = ['warning', 'critical'];
|
||||
|
||||
expect(component.buildFilters()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
modules: ['policy', 'scanner'],
|
||||
actions: ['approve', 'replay'],
|
||||
severities: ['warning', 'critical'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trust audit filtering', () => {
|
||||
let fixture: ComponentFixture<TrustAuditLogComponent>;
|
||||
let component: TrustAuditLogComponent;
|
||||
const trustApi = {
|
||||
listAuditEvents: jasmine.createSpy('listAuditEvents').and.returnValue(
|
||||
of({ items: [], totalCount: 0, pageNumber: 1, pageSize: 20 }),
|
||||
),
|
||||
exportAuditLog: jasmine.createSpy('exportAuditLog').and.returnValue(
|
||||
of(new Blob(['[]'], { type: 'application/json' })),
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TrustAuditLogComponent],
|
||||
providers: [{ provide: TRUST_API, useValue: trustApi }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TrustAuditLogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
trustApi.listAuditEvents.calls.reset();
|
||||
});
|
||||
|
||||
it('keeps start and end date filters in the request model', () => {
|
||||
component.startDate.set('2026-03-01');
|
||||
component.endDate.set('2026-03-08');
|
||||
|
||||
component.onFilterChange();
|
||||
|
||||
expect(trustApi.listAuditEvents).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
filter: jasmine.objectContaining({
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-08',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('truthful findings and release security rendering', () => {
|
||||
it('renders the mounted findings container with the bespoke findings list', async () => {
|
||||
const queryParamMap$ = new BehaviorSubject(convertToParamMap({ view: 'detail' }));
|
||||
const paramMap$ = new BehaviorSubject(convertToParamMap({ scanId: 'scan-1' }));
|
||||
const viewPreference = jasmine.createSpyObj(
|
||||
'ViewPreferenceService',
|
||||
['getViewMode', 'setViewMode'],
|
||||
{ viewMode: signal<FindingsViewMode>('detail').asReadonly() },
|
||||
);
|
||||
viewPreference.getViewMode.and.returnValue('detail');
|
||||
|
||||
const compareService = jasmine.createSpyObj('CompareService', [
|
||||
'getBaselineRecommendations',
|
||||
'computeDelta',
|
||||
]);
|
||||
compareService.getBaselineRecommendations.and.returnValue(
|
||||
of({ selectedDigest: null, selectionReason: 'none', alternatives: [], autoSelectEnabled: true }),
|
||||
);
|
||||
compareService.computeDelta.and.returnValue(of({ categories: [], items: [] }));
|
||||
|
||||
const findingsApi = jasmine.createSpyObj('SecurityFindingsApi', ['listFindings']);
|
||||
findingsApi.listFindings.and.returnValue(
|
||||
of([
|
||||
{
|
||||
id: 'finding-1',
|
||||
advisoryId: 'CVE-2026-8001',
|
||||
package: 'backend-api',
|
||||
version: '2.5.0',
|
||||
severity: 'CRITICAL',
|
||||
vexStatus: 'affected',
|
||||
delta: 'new',
|
||||
firstSeen: '2026-02-10T09:30:00Z',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FindingsContainerComponent, NoopAnimationsModule, HttpClientTestingModule],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ViewPreferenceService, useValue: viewPreference },
|
||||
{ provide: CompareService, useValue: compareService },
|
||||
{ provide: SCORING_API, useClass: MockScoringApi },
|
||||
{ provide: SECURITY_FINDINGS_API, useValue: findingsApi },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
paramMap: paramMap$,
|
||||
queryParamMap: queryParamMap$,
|
||||
snapshot: {
|
||||
paramMap: convertToParamMap({ scanId: 'scan-1' }),
|
||||
queryParamMap: convertToParamMap({ view: 'detail' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(FindingsContainerComponent);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.querySelector('app-findings-list')).toBeTruthy();
|
||||
expect(element.querySelector('stella-finding-list')).toBeFalsy();
|
||||
expect(fixture.componentInstance.findings()[0].publishedAt).toBe('2026-02-10T09:30:00Z');
|
||||
});
|
||||
|
||||
it('renders the release security table instead of a synthetic shared finding list', async () => {
|
||||
const mockRelease = {
|
||||
releaseId: 'rel-001',
|
||||
name: 'Test Release',
|
||||
version: 'v1.0.0',
|
||||
digest: 'sha256:abc',
|
||||
releaseType: 'standard',
|
||||
targetRegion: 'us-east-1',
|
||||
gateStatus: 'passed',
|
||||
evidencePosture: 'complete',
|
||||
riskTier: 'low',
|
||||
gateBlockingCount: 0,
|
||||
replayMismatch: false,
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReleaseDetailComponent, HttpClientTestingModule],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: PlatformContextStore,
|
||||
useValue: {
|
||||
initialize: jasmine.createSpy('initialize'),
|
||||
contextVersion: signal(0).asReadonly(),
|
||||
selectedRegions: signal<string[]>([]),
|
||||
selectedEnvironments: signal<string[]>([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ReleaseManagementStore,
|
||||
useValue: {
|
||||
selectedRelease: signal(mockRelease),
|
||||
releases: signal([mockRelease]),
|
||||
selectRelease: jasmine.createSpy('selectRelease'),
|
||||
loadRelease: () => EMPTY,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: of({ semanticObject: 'version' }),
|
||||
paramMap: of(convertToParamMap({ releaseId: 'rel-001' })),
|
||||
queryParamMap: of(convertToParamMap({})),
|
||||
snapshot: {
|
||||
paramMap: convertToParamMap({ releaseId: 'rel-001' }),
|
||||
queryParamMap: convertToParamMap({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(ReleaseDetailComponent);
|
||||
const component = fixture.componentInstance;
|
||||
component.mode.set('version');
|
||||
component.activeTab.set('security-inputs');
|
||||
component.findings.set([
|
||||
{
|
||||
findingId: 'f-001',
|
||||
cveId: 'CVE-2026-9001',
|
||||
severity: 'critical',
|
||||
componentName: 'libcrypto',
|
||||
releaseId: 'rel-001',
|
||||
reachable: true,
|
||||
reachabilityScore: 85,
|
||||
effectiveDisposition: 'action_required',
|
||||
vexStatus: 'under_investigation',
|
||||
exceptionStatus: 'none',
|
||||
},
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const element: HTMLElement = fixture.nativeElement;
|
||||
expect(element.querySelector('stella-finding-list')).toBeFalsy();
|
||||
expect(element.textContent).toContain('CVE-2026-9001');
|
||||
expect(element.textContent).toContain('libcrypto');
|
||||
expect(element.textContent).toContain('yes (85)');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user