feat: improve archive finding clarity with finding IDs, historical severity labels, and related active finding indicators
This commit is contained in:
1
.kiro/specs/archive-finding-clarity/.config.kiro
Normal file
1
.kiro/specs/archive-finding-clarity/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "acff93bd-0045-4fcd-b948-cc52c7cc5ec6", "workflowType": "requirements-first", "specType": "feature"}
|
||||
196
.kiro/specs/archive-finding-clarity/design.md
Normal file
196
.kiro/specs/archive-finding-clarity/design.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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:
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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. **Icon** — `AlertTriangle` (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`:
|
||||
```javascript
|
||||
{ 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.
|
||||
82
.kiro/specs/archive-finding-clarity/requirements.md
Normal file
82
.kiro/specs/archive-finding-clarity/requirements.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
The Ivanti Archive Findings panel on the STEAM Security Dashboard homepage displays findings that have transitioned through the archive lifecycle (Active, Archived, Returned, Closed). The current archive cards show the finding title, hostname, IP address, and a raw severity number — but lack clarity in several areas. Users cannot see the Ivanti finding ID for cross-referencing, the severity score appears to be a current value when it is actually a historical snapshot, and there is no indication when a related finding with the same title still exists on the same host under a different Ivanti finding ID.
|
||||
|
||||
This feature improves archive card clarity by adding finding IDs, labeling severity as historical, introducing a "related active finding" indicator, and using visual icon distinctions to communicate resolution status at a glance.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Archive_Card**: A single rendered entry in the archive findings list on the homepage, representing one row from the `ivanti_finding_archives` table.
|
||||
- **Archive_Panel**: The section of the homepage that contains the ArchiveSummaryBar stat cards and the expandable archive findings list.
|
||||
- **Finding_ID**: The stable Ivanti-assigned identifier for a host finding (stored as `finding_id` in `ivanti_finding_archives`). Finding IDs do not change with score drift or rescoring.
|
||||
- **Last_Severity**: The severity score recorded at the time a finding was archived or last transitioned between states. It is a historical snapshot, not a live risk assessment.
|
||||
- **Current_Findings_Cache**: The `ivanti_findings_cache` table containing the latest synced findings as a JSON array. Each cached finding has fields including `id`, `title`, `severity`, `hostName`, and `ipAddress`.
|
||||
- **Related_Active_Finding**: A finding in the Current_Findings_Cache that shares the same hostname and a similar title with an archived finding but has a different Finding_ID, indicating a genuinely distinct but related finding is still open on the same host.
|
||||
- **Archive_API**: The backend endpoint `GET /api/ivanti/archive` that returns archive records filtered by lifecycle state.
|
||||
- **Related_Findings_Endpoint**: A new backend endpoint that accepts archived finding details and returns matching active findings from the Current_Findings_Cache.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Display Finding ID on Archive Cards
|
||||
|
||||
**User Story:** As a security analyst, I want to see the Ivanti finding ID on each archive card, so that I can cross-reference archived findings with the Reporting page.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Archive_Card SHALL display the Finding_ID in monospace font below the finding title.
|
||||
2. WHEN the Finding_ID is longer than 20 characters, THE Archive_Card SHALL truncate the Finding_ID with an ellipsis and display the full value in a tooltip on hover.
|
||||
3. THE Archive_Card SHALL render the Finding_ID with a visually distinct style (muted color, smaller font size) so it is clearly secondary to the finding title.
|
||||
|
||||
### Requirement 2: Historical Severity Labeling
|
||||
|
||||
**User Story:** As a security analyst, I want the severity score on archive cards to be clearly labeled as a historical value, so that I do not mistake it for a current risk assessment.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Archive_Card SHALL display the Last_Severity with a "Last seen:" prefix label (e.g., "Last seen: 9.4").
|
||||
2. THE Archive_Card SHALL render the severity label in a muted, secondary style that visually distinguishes it from live severity badges used elsewhere in the dashboard.
|
||||
3. WHEN the Last_Severity value is null or zero, THE Archive_Card SHALL display "Last seen: —" as a placeholder.
|
||||
|
||||
### Requirement 3: Related Active Finding Detection API
|
||||
|
||||
**User Story:** As a security analyst, I want the system to detect when an archived finding has a related active finding on the same host, so that I can understand whether similar risk still exists.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Archive_API SHALL return a `related_active` field for each archive record, containing either `null` (no match) or an object with the matching active finding's `id`, `title`, and `severity`.
|
||||
2. WHEN matching archived findings to active findings, THE Related_Findings_Endpoint SHALL compare by exact hostname match AND case-insensitive substring containment of the archived finding title within the active finding title (or vice versa).
|
||||
3. WHEN multiple active findings match a single archived finding, THE Related_Findings_Endpoint SHALL return the match with the highest severity.
|
||||
4. IF the Current_Findings_Cache contains no findings or is empty, THEN THE Related_Findings_Endpoint SHALL return `null` for all `related_active` fields.
|
||||
5. THE Related_Findings_Endpoint SHALL exclude matches where the active finding's `id` is identical to the archived finding's Finding_ID.
|
||||
|
||||
### Requirement 4: Related Active Finding Indicator on Archive Cards
|
||||
|
||||
**User Story:** As a security analyst, I want to see a visual indicator on archive cards when a related active finding exists on the same host, so that I can quickly identify findings that may still represent active risk.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an archive record has a non-null `related_active` field, THE Archive_Card SHALL display a badge reading "Similar finding active" with the related finding's ID and current severity.
|
||||
2. THE Archive_Card SHALL render the related active badge using the dashboard accent color (#0EA5E9) to distinguish it from the archive card's own severity display.
|
||||
3. WHEN an archive record has a null `related_active` field, THE Archive_Card SHALL not display any related-finding badge.
|
||||
|
||||
### Requirement 5: Visual Icon Distinction by Resolution Status
|
||||
|
||||
**User Story:** As a security analyst, I want archive cards to use different icons and border colors based on whether a related active finding exists, so that I can scan the list and quickly distinguish fully resolved findings from those with ongoing similar risk.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an archive record has a non-null `related_active` field, THE Archive_Card SHALL display an alert-style icon (e.g., `AlertTriangle` from lucide-react) and use a warning-toned left border color (#F59E0B).
|
||||
2. WHEN an archive record has a null `related_active` field, THE Archive_Card SHALL display a check-style icon (e.g., `CheckCircle` from lucide-react) and use a success-toned left border color (#10B981).
|
||||
3. THE Archive_Card SHALL apply the icon and border color consistently regardless of the archive lifecycle state (ARCHIVED, RETURNED, or CLOSED).
|
||||
|
||||
### Requirement 6: Performance of Related Finding Lookup
|
||||
|
||||
**User Story:** As a security analyst, I want the archive panel to load promptly even when checking for related active findings, so that the feature does not degrade the homepage experience.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Archive_API SHALL compute related active finding matches server-side within the existing archive list query, avoiding separate per-card API calls from the frontend.
|
||||
2. WHEN the Current_Findings_Cache JSON is parsed for matching, THE Archive_API SHALL parse the cache once per request and reuse the parsed result across all archive records in the response.
|
||||
3. THE Archive_API response time for a filtered archive list SHALL remain under 500ms for up to 200 archive records.
|
||||
70
.kiro/specs/archive-finding-clarity/tasks.md
Normal file
70
.kiro/specs/archive-finding-clarity/tasks.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Implementation Plan: Archive Finding Clarity
|
||||
|
||||
## Overview
|
||||
|
||||
Enhance the Ivanti Archive Findings panel to display finding IDs, label severity as historical, detect related active findings server-side, and apply visual icon/border distinctions based on resolution status. Changes span `backend/routes/ivantiArchive.js` (matching logic + enriched response) and `frontend/src/App.js` (card rendering updates). No new components, endpoints, or migrations.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Add `findRelatedActive` function and enrich the GET `/` handler in `backend/routes/ivantiArchive.js`
|
||||
- [x] 1.1 Add the `findRelatedActive(archive, activeFindings)` helper function
|
||||
- Add function above `createIvantiArchiveRouter` or inside the module scope
|
||||
- Filter active findings where `hostName` exactly matches `archive.host_name`
|
||||
- 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` as `{ id, title, severity }`
|
||||
- If no matches, return `null`
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.5_
|
||||
|
||||
- [x] 1.2 Modify the `GET /` handler to parse findings cache and enrich archive records
|
||||
- After fetching archive rows, query `ivanti_findings_cache` (id=1) for `findings_json`
|
||||
- Parse `findings_json` once with `JSON.parse`; default to empty array if NULL, missing row, or parse error
|
||||
- Log a warning on parse failure, do not throw
|
||||
- For each archive record, call `findRelatedActive(archive, parsedFindings)` and attach the result as `related_active`
|
||||
- Return the enriched archives array in the existing `{ archives, total }` response shape
|
||||
- _Requirements: 3.1, 3.4, 6.1, 6.2_
|
||||
|
||||
- [x] 2. Checkpoint — Verify backend changes
|
||||
- Ensure the backend starts without errors, ask the user if questions arise.
|
||||
|
||||
- [x] 3. Update archive card rendering in `frontend/src/App.js`
|
||||
- [x] 3.1 Add `AlertTriangle` and `CheckCircle` to the lucide-react import
|
||||
- Locate the existing lucide-react import statement in `App.js`
|
||||
- Add `AlertTriangle` and `CheckCircle` if not already imported
|
||||
- _Requirements: 5.1, 5.2_
|
||||
|
||||
- [x] 3.2 Add Finding ID display below the finding title
|
||||
- Inside the `archiveList.map()` block, add a new line below the title `<span>`
|
||||
- Render `a.finding_id` in monospace font, `0.6rem` size, muted color `#64748B`
|
||||
- If `finding_id` length exceeds 20 characters, truncate displayed text to 20 chars + ellipsis
|
||||
- Set the full `finding_id` as the `title` attribute for hover tooltip
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
|
||||
- [x] 3.3 Change severity badge to "Last seen: X.X" format
|
||||
- In the severity `<span>` within the archive card, replace `{a.last_severity?.toFixed(1) ?? '—'}` with `Last seen: {a.last_severity?.toFixed(1) ?? '—'}`
|
||||
- Null or zero severity displays as "Last seen: —"
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
|
||||
- [x] 3.4 Add conditional "Similar finding active" badge
|
||||
- When `a.related_active` is non-null, render a badge below the host info line
|
||||
- Badge text: "Similar finding active" with the related finding's ID and severity
|
||||
- Style with accent color `#0EA5E9`, monospace font, `0.6rem` size
|
||||
- When `a.related_active` is null, render nothing
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 3.5 Add icon and left border color based on `related_active`
|
||||
- When `a.related_active` is non-null: render `AlertTriangle` icon and set left border to `3px solid #F59E0B` (amber)
|
||||
- When `a.related_active` is null: render `CheckCircle` icon and set left border to `3px solid #10B981` (green)
|
||||
- Place the icon at the left side of the card header row, before the title
|
||||
- Apply consistently regardless of archive lifecycle state (ARCHIVED, RETURNED, CLOSED)
|
||||
- _Requirements: 5.1, 5.2, 5.3_
|
||||
|
||||
- [x] 4. Final checkpoint — Verify full feature
|
||||
- Ensure the frontend compiles without errors, ask the user if questions arise.
|
||||
|
||||
## Notes
|
||||
|
||||
- No automated tests — feature is validated manually on the dev server per user preference
|
||||
- No new components, endpoints, or database migrations required
|
||||
- The `findRelatedActive` function parses the findings cache once per request for performance (Requirement 6.2)
|
||||
- Each task references specific requirements for traceability
|
||||
Reference in New Issue
Block a user