FindingsListComponent

Comprehensive findings list with Evidence-Weighted Score (EWS) integration, filtering, and bulk selection.

Overview

The FindingsListComponent displays a sortable, filterable table of vulnerability findings with Evidence-Weighted Score display. The list sends no score request when it opens: a score is computed only through an explicit action by a session that holds vuln:operate (see Score Computation).

Selector

<app-findings-list />

Inputs

InputTypeDefaultDescription
findingsFinding[][]Array of findings to display. A new array clears the computed scores and the bucket and flag filters
serverTotalnumber | nullnullServer-side total behind findings; when it exceeds the loaded rows the list states that it shows a bounded slice

Outputs

OutputTypeDescription
findingSelectEventEmitter<ScoredFinding>Emits when a finding row is clicked
selectionChangeEventEmitter<string[]>Emits finding IDs when selection changes

Data Structures

Finding

interface Finding {
  id: string;               // Unique finding ID
  advisoryId: string;       // CVE/GHSA ID
  packageName: string;
  packageVersion: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  status: 'open' | 'in_progress' | 'fixed' | 'excepted';
  publishedAt?: string;     // ISO 8601
}

ScoredFinding

interface ScoredFinding extends Finding {
  score?: EvidenceWeightedScoreResult;
  scoreLoading: boolean;
}

Table Columns

ColumnDescriptionSortable
CheckboxBulk selectionNo
ScoreEWS score pill with flagsYes
AdvisoryCVE/GHSA identifierYes
PackagePackage name and versionYes
SeverityCVSS severity levelYes
StatusFinding statusYes

Features

Score Computation

The list never requests scores when it opens or when findings changes. Findings serves POST /api/findings/v1/ledger/scores as an audited computation (each uncached finding is computed, cached and recorded in the score history), and every Findings scoring route requires the vuln:operate scope, which no base role holds. The component checks the session with AUTH_SERVICE.hasScope(FINDINGS_SCORING_SCOPE) (FINDINGS_SCORING_SCOPE is vuln:operate; the Console’s scope check treats admin as satisfying it, while Findings itself authorizes vuln:operate).

Bucket Filtering

The bucket chips appear only once a computed score exists; while some loaded findings have no score, a line states how many of them the counts cover. Unscored findings are never counted as zero. Select a chip to filter by its priority bucket; select the active chip again to clear it:

Flag Filtering

Filter by active score flags. Flags come from computed scores, so the checkboxes are disabled, with the reason as their title, until a computed score exists:

Row Visual Indicators

Hard Fail Rows

Findings with hard-fail flag are highlighted with:

Anchored Rows

Findings with anchored flag display:

Note: If a finding is both anchored and hard-fail, hard-fail styling takes visual precedence.

Text search across advisory ID and package name.

Sorting

Click column headers to sort. Click again to reverse order.

Bulk Selection

Usage Examples

Basic Usage

<app-findings-list
  [findings]="findings"
  (findingSelect)="openFinding($event)"
/>

With Selection Handling

<app-findings-list
  [findings]="findings"
  (selectionChange)="onSelectionChange($event)"
/>

<div class="bulk-actions" *ngIf="selectedIds.length > 0">
  <button (click)="acknowledgeSelected()">
    Acknowledge ({{ selectedIds.length }})
  </button>
</div>
selectedIds: string[] = [];

onSelectionChange(ids: string[]): void {
  this.selectedIds = ids;
}

Full Feature Example

@Component({
  selector: 'app-vulnerability-dashboard',
  template: `
    <div class="dashboard">
      <h1>Vulnerabilities</h1>

      <app-findings-list
        [findings]="findings()"
        (findingSelect)="openFindingDetail($event)"
        (selectionChange)="updateSelection($event)"
      />

      @if (selectedFinding()) {
        <app-finding-detail-panel
          [finding]="selectedFinding()"
          (close)="closeFindingDetail()"
        />
      }
    </div>
  `
})
export class VulnerabilityDashboardComponent {
  findings = signal<Finding[]>([]);
  selectedFinding = signal<ScoredFinding | null>(null);
  selectedIds = signal<string[]>([]);

  constructor(private findingsService: FindingsService) {
    this.loadFindings();
  }

  async loadFindings(): Promise<void> {
    this.findings.set(await this.findingsService.getFindings());
  }

  openFindingDetail(finding: ScoredFinding): void {
    this.selectedFinding.set(finding);
  }

  closeFindingDetail(): void {
    this.selectedFinding.set(null);
  }

  updateSelection(ids: string[]): void {
    this.selectedIds.set(ids);
  }
}

Dependency Injection

The component computes scores through ScoringService, which calls the SCORING_API provider. The application registers HttpScoringApi for it in app.config.ts:

import { SCORING_API, HttpScoringApi } from '@app/core/services/scoring.service';

providers: [
  HttpScoringApi,
  { provide: SCORING_API, useExisting: HttpScoringApi },
]

It reads the session’s scopes through the optional AUTH_SERVICE token. A host without an auth session holds no scope, so the list shows the scores-not-available state.

Mock API for Testing

import { FixtureScoringApi } from '@app/core/testing/scoring.testing';

TestBed.configureTestingModule({
  providers: [
    { provide: SCORING_API, useClass: FixtureScoringApi }
  ]
});

Empty States

No Findings

<!-- Displays: "No findings to display" -->
<app-findings-list [findings]="[]" />

No Matches

When filters result in no matches:

<!-- Displays: "No findings match the current filters" -->

Accessibility

Styling

app-findings-list {
  --table-header-bg: #f9fafb;
  --table-border-color: #e5e7eb;
  --table-row-hover: #f3f4f6;
  --table-row-selected: #eff6ff;
}