Files
cve-dashboard/.kiro/specs/archive-finding-clarity/design.md

9.8 KiB

Design Document: Archive Finding Clarity

Overview

This feature enhances the Ivanti Archive Findings panel on the STEAM Security Dashboard homepage to provide clearer context for archived findings. The changes span both backend (related active finding detection) and frontend (card rendering improvements).

The core additions are:

  1. Finding ID display — Show the Ivanti finding ID on each archive card for cross-referencing
  2. Historical severity labeling — Prefix severity with "Last seen:" to clarify it's a snapshot
  3. Related active finding detection — Server-side matching of archived findings against the current findings cache by hostname + title
  4. Visual status indicators — Icon and border color distinctions based on whether a related active finding exists

All matching is performed server-side in a single pass to avoid per-card API calls and keep the archive panel responsive.

Architecture

The feature touches two layers:

flowchart LR
    subgraph Backend
        A[GET /api/ivanti/archive?state=X] --> B[Query ivanti_finding_archives]
        B --> C[Parse ivanti_findings_cache JSON once]
        C --> D[Match each archive record against active findings]
        D --> E[Return archives with related_active field]
    end
    subgraph Frontend
        E --> F[App.js archiveList.map]
        F --> G[Render enhanced Archive Cards]
    end

Key design decision: The related finding lookup is embedded in the existing GET /api/ivanti/archive endpoint rather than exposed as a separate endpoint. This avoids N+1 API calls from the frontend and keeps the archive panel's fetch pattern unchanged (single request per state filter click).

Data Flow

  1. User clicks a state card in ArchiveSummaryBar → triggers handleArchiveStateClick(state) in App.js
  2. Frontend calls GET /api/ivanti/archive?state={state}
  3. Backend queries ivanti_finding_archives for matching state
  4. Backend reads ivanti_findings_cache row (id=1), parses findings_json once
  5. For each archive record, backend runs the matching function against the parsed active findings
  6. Backend returns { archives: [...], total: N } where each archive object now includes a related_active field
  7. Frontend renders each archive card with the new fields: finding ID, "Last seen:" severity, optional badge, icon/border

Components and Interfaces

Backend: Modified Archive Route (backend/routes/ivantiArchive.js)

Changes to GET / handler:

// New matching function added to the module
function findRelatedActive(archive, activeFindings) {
  // Returns { id, title, severity } or null
}

findRelatedActive(archive, activeFindings) logic:

  • Input: one archive record, array of parsed active findings
  • Filter active findings where:
    • hostName exactly matches archive.host_name (case-sensitive, matching existing DB convention)
    • AND the archive's finding_title is a case-insensitive substring of the active finding's title, OR vice versa
    • AND the active finding's id is NOT equal to archive.finding_id
  • If multiple matches, return the one with the highest severity
  • If no matches, return null

Modified response shape:

// Before
{ id, finding_id, finding_title, host_name, ip_address, current_state, last_severity, ... }

// After — same fields plus:
{ ...existing, related_active: null | { id: string, title: string, severity: number } }

Frontend: Modified Archive Card Rendering (frontend/src/App.js)

The archiveList.map() block in App.js is updated to render:

  1. Finding title (existing, unchanged)
  2. Finding ID — new line below title, monospace, muted color (#64748B), font size 0.6rem. Truncated with ellipsis at 20 characters, full value in title attribute for tooltip.
  3. Severity badge — changed from raw number to "Last seen: X.X" format. Null/zero shows "Last seen: —".
  4. Related active badge — conditional. When related_active is non-null, shows "Similar finding active" with the related finding's ID and severity, styled with accent color (#0EA5E9).
  5. IconAlertTriangle (from lucide-react) when related_active is non-null, CheckCircle when null.
  6. Left border#F59E0B (amber) when related_active is non-null, #10B981 (green) when null.

No New Components

The archive card is rendered inline in App.js (not a separate component), consistent with the existing pattern. The changes modify the existing archiveList.map() JSX block. No new React components are introduced.

No New API Endpoints

The related finding detection is added to the existing GET /api/ivanti/archive route. The ArchiveSummaryBar component and its /stats endpoint are unchanged.

Data Models

Existing Tables (unchanged)

ivanti_finding_archives

Column Type Description
id INTEGER PK Auto-increment row ID
finding_id TEXT UNIQUE Ivanti finding identifier
finding_title TEXT Finding title at archive time
host_name TEXT Hostname
ip_address TEXT IP address
current_state TEXT ARCHIVED, RETURNED, or CLOSED
last_severity REAL Severity at last transition
first_archived_at DATETIME First archive timestamp
last_transition_at DATETIME Last state change timestamp

ivanti_findings_cache (row id=1)

Column Type Description
findings_json TEXT JSON array of active findings
total INTEGER Count of cached findings

Each entry in findings_json has the shape produced by extractFinding() in ivantiFindings.js:

{ id, title, severity, vrrGroup, hostName, ipAddress, dns, status, slaStatus, dueDate, lastFoundOn, buOwnership, cves, workflow }

No Schema Changes

This feature requires no database migrations. All data needed for the matching logic already exists in the two tables above.

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Finding ID display with truncation

For any archive record, the rendered card SHALL display the finding_id. If the finding_id is longer than 20 characters, the displayed text SHALL be truncated to 20 characters followed by an ellipsis. If the finding_id is 20 characters or fewer, it SHALL be displayed in full.

Validates: Requirements 1.1, 1.2

Property 2: Historical severity labeling

For any archive record, the rendered severity display SHALL contain the text "Last seen:" followed by the severity value formatted to one decimal place. When the severity is null or zero, the display SHALL show "Last seen: —".

Validates: Requirements 2.1, 2.3

For any request to the archive API and for any archive record in the response, the record SHALL contain a related_active field that is either null or an object with id (string), title (string), and severity (number) properties.

Validates: Requirements 3.1, 3.4

Property 4: Matching logic — hostname and title substring

For any archived finding and for any active finding, the active finding is a related match if and only if: (a) the active finding's hostname exactly equals the archive's hostname, AND (b) the archive's title is a case-insensitive substring of the active finding's title OR the active finding's title is a case-insensitive substring of the archive's title, AND (c) the active finding's ID is not equal to the archive's finding_id.

Validates: Requirements 3.2, 3.5

Property 5: Highest severity selection

For any archived finding with multiple matching active findings, the related_active field SHALL contain the match with the highest severity value.

Validates: Requirements 3.3

For any archive record, the "Similar finding active" badge SHALL be displayed if and only if the related_active field is non-null. When displayed, the badge SHALL include the related finding's ID and severity.

Validates: Requirements 4.1, 4.3

For any archive record, regardless of its lifecycle state (ARCHIVED, RETURNED, or CLOSED), the icon and left border color SHALL be determined solely by whether related_active is non-null (alert icon + amber border) or null (check icon + green border).

Validates: Requirements 5.1, 5.2, 5.3

Error Handling

Backend

Scenario Handling
findings_json is malformed or unparseable Catch JSON.parse error, log warning, treat as empty array (all related_active fields become null)
findings_json column is NULL Default to empty array
ivanti_findings_cache row missing (id=1) Default to empty array — no related matches
Database query failure on archive records Return 500 with { error: 'Failed to fetch archive records' } (existing behavior)
Database query failure on findings cache Log error, continue with empty active findings (graceful degradation)

Frontend

Scenario Handling
related_active field missing from response Treat as null (no badge, green/check styling)
finding_id is empty string Display finding title only (existing fallback behavior)
last_severity is undefined Display "Last seen: —"
API returns error Existing error handling in handleArchiveStateClick already catches and shows empty state

Testing Strategy

Testing is performed manually on the dev server. No automated tests are required for this feature.