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:
- Finding ID display — Show the Ivanti finding ID on each archive card for cross-referencing
- Historical severity labeling — Prefix severity with "Last seen:" to clarify it's a snapshot
- Related active finding detection — Server-side matching of archived findings against the current findings cache by hostname + title
- 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
- User clicks a state card in
ArchiveSummaryBar→ triggershandleArchiveStateClick(state)inApp.js - Frontend calls
GET /api/ivanti/archive?state={state} - Backend queries
ivanti_finding_archivesfor matching state - Backend reads
ivanti_findings_cacherow (id=1), parsesfindings_jsononce - For each archive record, backend runs the matching function against the parsed active findings
- Backend returns
{ archives: [...], total: N }where each archive object now includes arelated_activefield - 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:
hostNameexactly matchesarchive.host_name(case-sensitive, matching existing DB convention)- AND the archive's
finding_titleis a case-insensitive substring of the active finding'stitle, OR vice versa - AND the active finding's
idis NOT equal toarchive.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:
- Finding title (existing, unchanged)
- Finding ID — new line below title, monospace, muted color (
#64748B), font size0.6rem. Truncated with ellipsis at 20 characters, full value intitleattribute for tooltip. - Severity badge — changed from raw number to "Last seen: X.X" format. Null/zero shows "Last seen: —".
- Related active badge — conditional. When
related_activeis non-null, shows "Similar finding active" with the related finding's ID and severity, styled with accent color (#0EA5E9). - Icon —
AlertTriangle(from lucide-react) whenrelated_activeis non-null,CheckCirclewhen null. - Left border —
#F59E0B(amber) whenrelated_activeis 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
Property 3: API response structure — related_active always present
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
Property 6: Badge visibility matches related_active presence
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
Property 7: Icon and border determined by related_active, not lifecycle state
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.