added kiro specs
This commit is contained in:
293
.kiro/specs/finding-archive-tracking/design.md
Normal file
293
.kiro/specs/finding-archive-tracking/design.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Design Document: Finding Archive Tracking
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Finding Archive Tracking system adds a detection layer to the existing Ivanti sync pipeline that identifies findings which disappear from sync results due to severity score drift. It tracks these findings through a four-state lifecycle (ACTIVE → ARCHIVED → RETURNED → CLOSED) with full transition history stored in two new SQLite tables. Three new API endpoints expose archive data, and an Archive Summary Bar UI component provides at-a-glance state counts on the Ivanti dashboard.
|
||||||
|
|
||||||
|
The system integrates directly into the existing `syncFindings()` function in `ivantiFindings.js`, comparing current sync results against the previous set to detect disappearances and reappearances. This approach requires no additional API calls to Ivanti and leverages the already-cached findings data.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph Ivanti Sync Pipeline
|
||||||
|
A[syncFindings] --> B[Fetch all pages from Ivanti API]
|
||||||
|
B --> C[Store findings in ivanti_findings_cache]
|
||||||
|
C --> D[Archive Detection]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Archive Detection
|
||||||
|
D --> E{Compare previous vs current finding IDs}
|
||||||
|
E -->|Missing from current| F[Create/Update Archive Record → ARCHIVED]
|
||||||
|
E -->|Returned in current| G[Update Archive Record → RETURNED]
|
||||||
|
E -->|Closed in Ivanti| H[Update Archive Record → CLOSED]
|
||||||
|
F --> I[Insert Transition History]
|
||||||
|
G --> I
|
||||||
|
H --> I
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Archive API
|
||||||
|
J[GET /api/ivanti/archive] --> K[(ivanti_finding_archives)]
|
||||||
|
L[GET /api/ivanti/archive/:findingId/history] --> M[(ivanti_archive_transitions)]
|
||||||
|
N[GET /api/ivanti/archive/stats] --> K
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Frontend
|
||||||
|
O[Archive Summary Bar] -->|fetch stats| N
|
||||||
|
O -->|click state| J
|
||||||
|
P[Transition History Panel] -->|fetch history| L
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
|
||||||
|
1. **Sync Pipeline Hook**: Archive detection runs after `syncFindings()` successfully stores new findings in the cache. It reads the previous findings from the cache before the update, then compares against the new set.
|
||||||
|
2. **Route Registration**: The archive router is mounted at `/api/ivanti/archive` in `server.js`, following the same factory pattern as existing Ivanti routes.
|
||||||
|
3. **Frontend Integration**: The Archive Summary Bar is rendered on the existing Ivanti findings page, above the findings table.
|
||||||
|
|
||||||
|
## Components and Interfaces
|
||||||
|
|
||||||
|
### 1. Archive Detection Module (`detectArchiveChanges`)
|
||||||
|
|
||||||
|
Located within `backend/routes/ivantiFindings.js`, this async function runs after a successful sync.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Compare previous and current finding sets to detect archive state changes.
|
||||||
|
* @param {sqlite3.Database} db - SQLite database instance
|
||||||
|
* @param {Array} previousFindings - Findings from before the sync update
|
||||||
|
* @param {Array} currentFindings - Findings from the latest sync
|
||||||
|
*/
|
||||||
|
async function detectArchiveChanges(db, previousFindings, currentFindings) {
|
||||||
|
// 1. Build ID sets from previous and current
|
||||||
|
// 2. Disappeared = in previous but not in current → ARCHIVED
|
||||||
|
// 3. Returned = in current AND has existing ARCHIVED record → RETURNED
|
||||||
|
// 4. For each state change, upsert archive record + insert transition
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Closed Finding Detection (`detectClosedFindings`)
|
||||||
|
|
||||||
|
Runs during the closed count sync to detect findings that transitioned to CLOSED in Ivanti.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Check archived findings against Ivanti closed findings to detect remediation.
|
||||||
|
* @param {sqlite3.Database} db - SQLite database instance
|
||||||
|
* @param {Array} closedFindingIds - IDs of findings confirmed closed in Ivanti
|
||||||
|
*/
|
||||||
|
async function detectClosedFindings(db, closedFindingIds) {
|
||||||
|
// For each archived/returned finding, if it appears in closed set → CLOSED
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Archive API Router (`createIvantiArchiveRouter`)
|
||||||
|
|
||||||
|
Located at `backend/routes/ivantiArchive.js`, follows the existing factory pattern.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* @param {sqlite3.Database} db - SQLite database instance
|
||||||
|
* @param {Function} requireAuth - Auth middleware factory
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
|
function createIvantiArchiveRouter(db, requireAuth) {
|
||||||
|
const router = express.Router();
|
||||||
|
router.use(requireAuth(db));
|
||||||
|
|
||||||
|
// GET / - List archive records, optional ?state= filter
|
||||||
|
// GET /stats - Summary counts by state
|
||||||
|
// GET /:findingId/history - Transition history for a finding
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Archive Summary Bar Component (`ArchiveSummaryBar`)
|
||||||
|
|
||||||
|
Located at `frontend/src/components/pages/ArchiveSummaryBar.js`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Displays four stat cards for ACTIVE, ARCHIVED, RETURNED, CLOSED counts.
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Function} props.onStateClick - Callback when a state card is clicked
|
||||||
|
* @param {string|null} props.activeFilter - Currently selected state filter
|
||||||
|
*/
|
||||||
|
function ArchiveSummaryBar({ onStateClick, activeFilter }) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoint Specifications
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth | Query Params | Response |
|
||||||
|
|----------|--------|------|-------------|----------|
|
||||||
|
| `/api/ivanti/archive` | GET | Required | `state` (optional: ACTIVE, ARCHIVED, RETURNED, CLOSED) | `{ archives: [...], total: N }` |
|
||||||
|
| `/api/ivanti/archive/stats` | GET | Required | None | `{ ACTIVE: N, ARCHIVED: N, RETURNED: N, CLOSED: N, total: N }` |
|
||||||
|
| `/api/ivanti/archive/:findingId/history` | GET | Required | None | `{ finding_id: "...", transitions: [...] }` |
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### `ivanti_finding_archives` Table
|
||||||
|
|
||||||
|
| Column | Type | Constraints | Description |
|
||||||
|
|--------|------|-------------|-------------|
|
||||||
|
| `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | Row ID |
|
||||||
|
| `finding_id` | TEXT | NOT NULL UNIQUE | Ivanti finding identifier |
|
||||||
|
| `finding_title` | TEXT | NOT NULL DEFAULT '' | Finding title at time of archival |
|
||||||
|
| `host_name` | TEXT | NOT NULL DEFAULT '' | Host name at time of archival |
|
||||||
|
| `ip_address` | TEXT | NOT NULL DEFAULT '' | IP address at time of archival |
|
||||||
|
| `current_state` | TEXT | NOT NULL CHECK(IN ('ACTIVE','ARCHIVED','RETURNED','CLOSED')) | Current lifecycle state |
|
||||||
|
| `last_severity` | REAL | NOT NULL DEFAULT 0 | Last known severity score |
|
||||||
|
| `first_archived_at` | DATETIME | NOT NULL DEFAULT CURRENT_TIMESTAMP | When first archived |
|
||||||
|
| `last_transition_at` | DATETIME | NOT NULL DEFAULT CURRENT_TIMESTAMP | When last state change occurred |
|
||||||
|
| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | Row creation time |
|
||||||
|
|
||||||
|
**Indexes:**
|
||||||
|
- `idx_archive_finding_id` on `finding_id`
|
||||||
|
- `idx_archive_current_state` on `current_state`
|
||||||
|
|
||||||
|
### `ivanti_archive_transitions` Table
|
||||||
|
|
||||||
|
| Column | Type | Constraints | Description |
|
||||||
|
|--------|------|-------------|-------------|
|
||||||
|
| `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | Row ID |
|
||||||
|
| `archive_id` | INTEGER | NOT NULL, FK → ivanti_finding_archives(id) | Parent archive record |
|
||||||
|
| `from_state` | TEXT | NOT NULL | Previous state (or 'NONE' for initial) |
|
||||||
|
| `to_state` | TEXT | NOT NULL | New state |
|
||||||
|
| `severity_at_transition` | REAL | NOT NULL DEFAULT 0 | Severity score at time of transition |
|
||||||
|
| `reason` | TEXT | NOT NULL DEFAULT '' | Human-readable reason |
|
||||||
|
| `transitioned_at` | DATETIME | NOT NULL DEFAULT CURRENT_TIMESTAMP | When transition occurred |
|
||||||
|
|
||||||
|
**Indexes:**
|
||||||
|
- `idx_transition_archive_id` on `archive_id`
|
||||||
|
|
||||||
|
### State Transition Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> ACTIVE : Finding present in sync
|
||||||
|
ACTIVE --> ARCHIVED : Disappeared from sync (score drift)
|
||||||
|
ARCHIVED --> RETURNED : Reappeared in sync
|
||||||
|
ARCHIVED --> CLOSED : Confirmed remediated in Ivanti
|
||||||
|
RETURNED --> ARCHIVED : Disappeared again
|
||||||
|
RETURNED --> CLOSED : Confirmed remediated in Ivanti
|
||||||
|
```
|
||||||
|
|
||||||
|
### Valid State Transitions
|
||||||
|
|
||||||
|
| From State | To State | Reason |
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| NONE | ACTIVE | `initial_sync` |
|
||||||
|
| ACTIVE → | ARCHIVED | `severity_score_drift` |
|
||||||
|
| ARCHIVED → | RETURNED | `reappeared_in_sync` |
|
||||||
|
| ARCHIVED → | CLOSED | `remediated_in_ivanti` |
|
||||||
|
| RETURNED → | ARCHIVED | `severity_score_drift` |
|
||||||
|
| RETURNED → | CLOSED | `remediated_in_ivanti` |
|
||||||
|
|
||||||
|
|
||||||
|
## 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: Disappeared findings are archived with complete metadata
|
||||||
|
|
||||||
|
*For any* set of previous findings and current findings, every finding present in the previous set but absent from the current set should have an Archive_Record with state ARCHIVED, and that record should contain the correct finding_id, finding_title, host_name, ip_address, and last_severity matching the original finding's data.
|
||||||
|
|
||||||
|
**Validates: Requirements 1.1, 1.2, 2.2**
|
||||||
|
|
||||||
|
### Property 2: Returned findings transition from ARCHIVED to RETURNED
|
||||||
|
|
||||||
|
*For any* finding that has an Archive_Record with state ARCHIVED, if that finding reappears in the current sync results, the Archive_Record state should be updated to RETURNED and the last_severity should reflect the finding's current severity score.
|
||||||
|
|
||||||
|
**Validates: Requirements 1.3**
|
||||||
|
|
||||||
|
### Property 3: Re-disappeared findings transition from RETURNED to ARCHIVED
|
||||||
|
|
||||||
|
*For any* finding that has an Archive_Record with state RETURNED, if that finding disappears from the current sync results, the Archive_Record state should be updated back to ARCHIVED.
|
||||||
|
|
||||||
|
**Validates: Requirements 1.4**
|
||||||
|
|
||||||
|
### Property 4: Every state transition produces a history record with all required fields
|
||||||
|
|
||||||
|
*For any* state transition on an Archive_Record, a Transition_History row should be inserted containing a valid archive_id, the correct from_state and to_state, a severity_at_transition value, a non-empty reason string, and a transitioned_at timestamp.
|
||||||
|
|
||||||
|
**Validates: Requirements 2.1**
|
||||||
|
|
||||||
|
### Property 5: Closed findings transition to CLOSED state
|
||||||
|
|
||||||
|
*For any* finding that has an Archive_Record with state ARCHIVED or RETURNED, if that finding appears in the Ivanti closed findings set, the Archive_Record state should be updated to CLOSED and the transition reason should be "remediated_in_ivanti".
|
||||||
|
|
||||||
|
**Validates: Requirements 2.3**
|
||||||
|
|
||||||
|
### Property 6: State filter returns only matching records
|
||||||
|
|
||||||
|
*For any* set of Archive_Records with various states, querying the archive list endpoint with a state filter should return only records whose current_state matches the filter, and the count should equal the number of records in that state.
|
||||||
|
|
||||||
|
**Validates: Requirements 4.1**
|
||||||
|
|
||||||
|
### Property 7: Transition history is ordered by timestamp descending
|
||||||
|
|
||||||
|
*For any* finding with multiple Transition_History entries, the history endpoint should return entries ordered by transitioned_at descending, such that each entry's timestamp is greater than or equal to the next entry's timestamp.
|
||||||
|
|
||||||
|
**Validates: Requirements 4.2**
|
||||||
|
|
||||||
|
### Property 8: Stats counts match actual record distribution
|
||||||
|
|
||||||
|
*For any* set of Archive_Records, the stats endpoint should return counts where the sum of all state counts equals the total number of Archive_Records, and each individual state count matches the actual number of records in that state.
|
||||||
|
|
||||||
|
**Validates: Requirements 4.3**
|
||||||
|
|
||||||
|
### Property 9: Migration idempotency
|
||||||
|
|
||||||
|
*For any* number of consecutive executions of the migration script, the resulting database schema should be identical and no errors should occur on subsequent runs.
|
||||||
|
|
||||||
|
**Validates: Requirements 6.2**
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Scenario | Handling |
|
||||||
|
|----------|----------|
|
||||||
|
| Sync fails (API error, timeout) | Archive detection is skipped entirely for that cycle. No archive records are created or modified. The sync error is logged as usual. |
|
||||||
|
| Database error during archive upsert | Log the error, continue processing remaining findings. Do not abort the entire archive detection pass. |
|
||||||
|
| Database error during transition insert | Log the error. The archive record state may have been updated but the transition history may be incomplete. This is acceptable as the current state is the source of truth. |
|
||||||
|
| Invalid state transition attempted | The detection logic only performs valid transitions per the state diagram. Invalid transitions (e.g., CLOSED → ARCHIVED) are not possible by design since closed findings are excluded from the sync pipeline. |
|
||||||
|
| Missing finding metadata | Use empty string defaults for finding_title, host_name, ip_address if the finding object lacks these fields. Severity defaults to 0. |
|
||||||
|
| Archive API query with invalid state parameter | Return all records (ignore the filter) rather than returning an error, for resilience. |
|
||||||
|
| History query for non-existent finding | Return 200 with empty transitions array (not 404), per requirement 4.5. |
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
Unit tests cover specific examples and edge cases:
|
||||||
|
|
||||||
|
- Migration script creates both tables and all indexes (example, Req 3.1–3.4)
|
||||||
|
- Archive detection skips when sync errors occur (example, Req 1.5)
|
||||||
|
- Unauthenticated requests return 401 (example, Req 4.4)
|
||||||
|
- History endpoint returns empty array for unknown finding (edge case, Req 4.5)
|
||||||
|
- Archive Summary Bar renders four stat cards (example, Req 5.1)
|
||||||
|
- Archive Summary Bar fetches stats on mount (example, Req 5.2)
|
||||||
|
- Clicking a state card triggers filter callback (example, Req 5.3)
|
||||||
|
|
||||||
|
### Property-Based Tests
|
||||||
|
|
||||||
|
Property-based tests use a PBT library (e.g., `fast-check`) to verify universal properties across generated inputs. Each test runs a minimum of 100 iterations.
|
||||||
|
|
||||||
|
| Property | Test Description | Tag |
|
||||||
|
|----------|-----------------|-----|
|
||||||
|
| Property 1 | Generate random previous/current finding sets, run detection, verify all disappeared findings have correct ARCHIVED records | **Feature: finding-archive-tracking, Property 1: Disappeared findings are archived with complete metadata** |
|
||||||
|
| Property 2 | Generate archived findings, add some back to current set, verify RETURNED state | **Feature: finding-archive-tracking, Property 2: Returned findings transition from ARCHIVED to RETURNED** |
|
||||||
|
| Property 3 | Generate returned findings, remove some from current set, verify ARCHIVED state | **Feature: finding-archive-tracking, Property 3: Re-disappeared findings transition from RETURNED to ARCHIVED** |
|
||||||
|
| Property 4 | Generate random state transitions, verify each produces a complete history row | **Feature: finding-archive-tracking, Property 4: Every state transition produces a history record** |
|
||||||
|
| Property 5 | Generate archived/returned findings, mark some as closed, verify CLOSED state and reason | **Feature: finding-archive-tracking, Property 5: Closed findings transition to CLOSED state** |
|
||||||
|
| Property 6 | Generate archive records with random states, query with filter, verify only matching records returned | **Feature: finding-archive-tracking, Property 6: State filter returns only matching records** |
|
||||||
|
| Property 7 | Generate multiple transitions for a finding, query history, verify descending order | **Feature: finding-archive-tracking, Property 7: Transition history is ordered by timestamp descending** |
|
||||||
|
| Property 8 | Generate archive records with random states, query stats, verify counts match | **Feature: finding-archive-tracking, Property 8: Stats counts match actual record distribution** |
|
||||||
|
| Property 9 | Run migration N times, verify no errors and schema is consistent | **Feature: finding-archive-tracking, Property 9: Migration idempotency** |
|
||||||
|
|
||||||
|
### Testing Tools
|
||||||
|
|
||||||
|
- **Test runner**: Jest (via react-scripts for frontend, direct for backend)
|
||||||
|
- **Property-based testing**: `fast-check` library
|
||||||
|
- **Database**: In-memory SQLite (`:memory:`) for isolated test runs
|
||||||
|
- **HTTP testing**: `supertest` for API endpoint tests
|
||||||
86
.kiro/specs/finding-archive-tracking/requirements.md
Normal file
86
.kiro/specs/finding-archive-tracking/requirements.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The Finding Archive Tracking system extends the Ivanti sync pipeline in the STEAM Security Dashboard to detect and track findings that disappear from sync results due to severity score drift (not remediation). Findings follow a four-state lifecycle (ACTIVE → ARCHIVED → RETURNED → CLOSED) with full transition history, enabling the security team to maintain visibility into findings that fall below the severity threshold and may reappear.
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- **Sync_Pipeline**: The existing Ivanti/RiskSense host finding sync process that fetches open findings matching BU and severity filters on a daily schedule.
|
||||||
|
- **Finding**: A single host-level vulnerability record identified by a unique `finding_id` from Ivanti/RiskSense.
|
||||||
|
- **Archive_Record**: A database row in the `ivanti_finding_archives` table tracking a finding's current lifecycle state and metadata.
|
||||||
|
- **Transition_History**: A database row in the `ivanti_archive_transitions` table recording a single state change event with timestamps, severity scores, and reason.
|
||||||
|
- **Archive_Detector**: The logic within the sync pipeline that compares previous sync results against current results to identify disappeared and returned findings.
|
||||||
|
- **Archive_Summary_Bar**: A React UI component displaying counts for each lifecycle state (ACTIVE, ARCHIVED, RETURNED, CLOSED) with click-through navigation.
|
||||||
|
- **Archive_API**: The set of three Express route endpoints serving archived finding data, transition history, and summary statistics.
|
||||||
|
- **Lifecycle_State**: One of four states a finding can occupy: ACTIVE (present in sync results), ARCHIVED (disappeared from sync results due to score drift), RETURNED (reappeared after being archived), CLOSED (remediated in Ivanti).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: Archive Detection During Sync
|
||||||
|
|
||||||
|
**User Story:** As a security analyst, I want the system to automatically detect findings that disappear from sync results, so that I can track findings lost due to severity score drift rather than actual remediation.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN the Sync_Pipeline completes a sync, THE Archive_Detector SHALL compare the current sync result finding IDs against the previous sync result finding IDs to identify findings that are no longer present.
|
||||||
|
2. WHEN a finding is present in the previous sync but absent from the current sync, THE Archive_Detector SHALL create an Archive_Record with state ARCHIVED, recording the finding metadata, last known severity score, and a timestamp.
|
||||||
|
3. WHEN a finding already has an Archive_Record with state ARCHIVED and the finding reappears in the current sync results, THE Archive_Detector SHALL update the Archive_Record state to RETURNED and record the new severity score.
|
||||||
|
4. WHEN a finding has an Archive_Record with state RETURNED and the finding disappears again from sync results, THE Archive_Detector SHALL update the Archive_Record state to ARCHIVED and record the severity score at time of disappearance.
|
||||||
|
5. IF the Sync_Pipeline encounters a sync error, THEN THE Archive_Detector SHALL skip archive detection for that sync cycle to avoid false positives from incomplete data.
|
||||||
|
|
||||||
|
### Requirement 2: Lifecycle State Transitions
|
||||||
|
|
||||||
|
**User Story:** As a security analyst, I want every state change to be recorded with context, so that I can audit the full history of a finding's lifecycle.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN an Archive_Record changes state, THE Sync_Pipeline SHALL insert a Transition_History row containing the previous state, new state, timestamp, severity score at time of transition, and a reason string.
|
||||||
|
2. THE Archive_Record SHALL store the finding_id, finding_title, host_name, ip_address, current state, last known severity score, initial archive timestamp, and last transition timestamp.
|
||||||
|
3. WHEN a finding is confirmed as remediated (closed) in Ivanti, THE Sync_Pipeline SHALL update the Archive_Record state to CLOSED and record a Transition_History entry with reason "remediated_in_ivanti".
|
||||||
|
4. THE Transition_History SHALL store the archive_record_id, from_state, to_state, transition timestamp, severity_at_transition, and reason.
|
||||||
|
|
||||||
|
### Requirement 3: Database Schema
|
||||||
|
|
||||||
|
**User Story:** As a developer, I want the archive data stored in two normalized SQLite tables, so that the data model supports efficient queries and maintains referential integrity.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Sync_Pipeline SHALL create an `ivanti_finding_archives` table with columns for id, finding_id (unique), finding_title, host_name, ip_address, current_state, last_severity, first_archived_at, last_transition_at, and created_at.
|
||||||
|
2. THE Sync_Pipeline SHALL create an `ivanti_archive_transitions` table with columns for id, archive_id (foreign key to ivanti_finding_archives), from_state, to_state, severity_at_transition, reason, and transitioned_at.
|
||||||
|
3. THE Sync_Pipeline SHALL create indexes on ivanti_finding_archives(finding_id) and ivanti_finding_archives(current_state) for query performance.
|
||||||
|
4. THE Sync_Pipeline SHALL create an index on ivanti_archive_transitions(archive_id) for efficient history lookups.
|
||||||
|
|
||||||
|
### Requirement 4: Archive API Endpoints
|
||||||
|
|
||||||
|
**User Story:** As a frontend developer, I want REST API endpoints to query archived findings, transition history, and summary statistics, so that I can build the archive tracking UI.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN a GET request is made to `/api/ivanti/archive`, THE Archive_API SHALL return a list of all Archive_Records with optional filtering by current_state query parameter.
|
||||||
|
2. WHEN a GET request is made to `/api/ivanti/archive/:findingId/history`, THE Archive_API SHALL return the Transition_History entries for the specified finding ordered by transitioned_at descending.
|
||||||
|
3. WHEN a GET request is made to `/api/ivanti/archive/stats`, THE Archive_API SHALL return an object containing the count of Archive_Records in each Lifecycle_State (ACTIVE, ARCHIVED, RETURNED, CLOSED).
|
||||||
|
4. WHEN an unauthenticated request is made to any Archive_API endpoint, THE Archive_API SHALL return a 401 status code.
|
||||||
|
5. WHEN a GET request is made to `/api/ivanti/archive/:findingId/history` with a finding_id that has no Archive_Record, THE Archive_API SHALL return an empty transitions array with a 200 status code.
|
||||||
|
|
||||||
|
### Requirement 5: Archive Summary Bar UI
|
||||||
|
|
||||||
|
**User Story:** As a security analyst, I want a visual summary bar on the Ivanti dashboard showing counts for each archive state, so that I can quickly assess the archive landscape and navigate to details.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Archive_Summary_Bar SHALL display four stat cards showing the count of findings in each Lifecycle_State: ACTIVE, ARCHIVED, RETURNED, and CLOSED.
|
||||||
|
2. WHEN the Archive_Summary_Bar loads, THE Archive_Summary_Bar SHALL fetch data from the `/api/ivanti/archive/stats` endpoint.
|
||||||
|
3. WHEN a user clicks a state card in the Archive_Summary_Bar, THE Archive_Summary_Bar SHALL filter the displayed archive list to show only findings in that state.
|
||||||
|
4. THE Archive_Summary_Bar SHALL use the existing design system colors: sky blue (#0EA5E9) for ACTIVE, amber (#F59E0B) for ARCHIVED, emerald (#10B981) for RETURNED, and red (#EF4444) for CLOSED.
|
||||||
|
5. THE Archive_Summary_Bar SHALL use Lucide icons and monospace typography consistent with the existing dashboard design system.
|
||||||
|
|
||||||
|
### Requirement 6: Migration Script
|
||||||
|
|
||||||
|
**User Story:** As a developer, I want a standalone migration script to create the archive tables, so that the schema can be applied to existing deployments following the established migration pattern.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE migration script SHALL be located at `backend/migrations/add_finding_archive_tables.js` and follow the existing migration pattern of opening the database, running DDL statements in `db.serialize()`, and closing the connection.
|
||||||
|
2. THE migration script SHALL use `CREATE TABLE IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS` to be idempotent.
|
||||||
|
3. WHEN the migration script is executed, THE migration script SHALL log progress messages for each table and index created.
|
||||||
134
.kiro/specs/finding-archive-tracking/tasks.md
Normal file
134
.kiro/specs/finding-archive-tracking/tasks.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Implementation Plan: Finding Archive Tracking
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement the Finding Archive Tracking system by creating the database migration, archive detection logic within the existing sync pipeline, three API endpoints via a new route module, and an Archive Summary Bar UI component. Each task builds incrementally — schema first, then detection logic, then API, then frontend.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [ ] 1. Create database migration and archive tables
|
||||||
|
- [ ] 1.1 Create `backend/migrations/add_finding_archive_tables.js` migration script
|
||||||
|
- Create `ivanti_finding_archives` table with columns: id, finding_id (UNIQUE), finding_title, host_name, ip_address, current_state (CHECK constraint for ACTIVE/ARCHIVED/RETURNED/CLOSED), last_severity, first_archived_at, last_transition_at, created_at
|
||||||
|
- Create `ivanti_archive_transitions` table with columns: id, archive_id (FK), from_state, to_state, severity_at_transition, reason, transitioned_at
|
||||||
|
- Create indexes: idx_archive_finding_id, idx_archive_current_state, idx_transition_archive_id
|
||||||
|
- Use `CREATE TABLE IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS` for idempotency
|
||||||
|
- Follow existing migration pattern: open db, `db.serialize()`, log progress, close db
|
||||||
|
- _Requirements: 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3_
|
||||||
|
|
||||||
|
- [ ]* 1.2 Write property test for migration idempotency
|
||||||
|
- **Property 9: Migration idempotency**
|
||||||
|
- Run migration logic multiple times against in-memory SQLite, verify no errors and schema is consistent
|
||||||
|
- **Validates: Requirements 6.2**
|
||||||
|
|
||||||
|
- [ ] 2. Implement archive detection logic in sync pipeline
|
||||||
|
- [ ] 2.1 Add `initArchiveTables(db)` function to `backend/routes/ivantiFindings.js`
|
||||||
|
- Create both archive tables inline (same pattern as existing `initTables`) so they exist on startup
|
||||||
|
- Call from `createIvantiFindingsRouter` during init alongside existing `initTables`
|
||||||
|
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
||||||
|
|
||||||
|
- [ ] 2.2 Implement `detectArchiveChanges(db, previousFindings, currentFindings)` function
|
||||||
|
- Build ID sets from previous and current findings
|
||||||
|
- For disappeared findings (in previous, not in current): upsert archive record with state ARCHIVED, insert transition history
|
||||||
|
- For returned findings (in current, has ARCHIVED record): update to RETURNED, insert transition history
|
||||||
|
- For re-disappeared findings (has RETURNED record, not in current): update to ARCHIVED, insert transition history
|
||||||
|
- Use `db.run` with callbacks wrapped in promises (matching existing `dbRun` helper pattern)
|
||||||
|
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2_
|
||||||
|
|
||||||
|
- [ ] 2.3 Implement `detectClosedFindings(db, closedFindingIds)` function
|
||||||
|
- Query archive records with state ARCHIVED or RETURNED
|
||||||
|
- For any that appear in the closed findings set, update to CLOSED with reason "remediated_in_ivanti"
|
||||||
|
- Insert transition history for each state change
|
||||||
|
- _Requirements: 2.3_
|
||||||
|
|
||||||
|
- [ ] 2.4 Integrate archive detection into `syncFindings()` flow
|
||||||
|
- Before updating the cache, read the current findings from `ivanti_findings_cache` as `previousFindings`
|
||||||
|
- After successful cache update, call `detectArchiveChanges(db, previousFindings, currentFindings)`
|
||||||
|
- Skip archive detection if sync encountered an error (requirement 1.5)
|
||||||
|
- Call `detectClosedFindings` during `syncClosedCount` with closed finding IDs
|
||||||
|
- _Requirements: 1.1, 1.5, 2.3_
|
||||||
|
|
||||||
|
- [ ]* 2.5 Write property test for archive detection — disappeared findings
|
||||||
|
- **Property 1: Disappeared findings are archived with complete metadata**
|
||||||
|
- Generate random previous/current finding sets using fast-check, run detectArchiveChanges against in-memory SQLite, verify all disappeared findings have ARCHIVED records with correct metadata
|
||||||
|
- **Validates: Requirements 1.1, 1.2, 2.2**
|
||||||
|
|
||||||
|
- [ ]* 2.6 Write property test for archive detection — returned findings
|
||||||
|
- **Property 2: Returned findings transition from ARCHIVED to RETURNED**
|
||||||
|
- Generate archived findings, add some back to current set, verify RETURNED state and updated severity
|
||||||
|
- **Validates: Requirements 1.3**
|
||||||
|
|
||||||
|
- [ ]* 2.7 Write property test for archive detection — re-disappeared findings
|
||||||
|
- **Property 3: Re-disappeared findings transition from RETURNED to ARCHIVED**
|
||||||
|
- Generate returned findings, remove some from current set, verify ARCHIVED state
|
||||||
|
- **Validates: Requirements 1.4**
|
||||||
|
|
||||||
|
- [ ]* 2.8 Write property test for transition history completeness
|
||||||
|
- **Property 4: Every state transition produces a history record with all required fields**
|
||||||
|
- Generate random state transitions, verify each produces a complete history row with archive_id, from_state, to_state, severity_at_transition, reason, transitioned_at
|
||||||
|
- **Validates: Requirements 2.1**
|
||||||
|
|
||||||
|
- [ ]* 2.9 Write property test for closed finding detection
|
||||||
|
- **Property 5: Closed findings transition to CLOSED state**
|
||||||
|
- Generate archived/returned findings, mark some as closed, verify CLOSED state and reason "remediated_in_ivanti"
|
||||||
|
- **Validates: Requirements 2.3**
|
||||||
|
|
||||||
|
- [ ] 3. Checkpoint — Verify archive detection logic
|
||||||
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
|
- [ ] 4. Implement Archive API endpoints
|
||||||
|
- [ ] 4.1 Create `backend/routes/ivantiArchive.js` route module
|
||||||
|
- Export factory function `createIvantiArchiveRouter(db, requireAuth)` returning Express Router
|
||||||
|
- Apply `requireAuth(db)` middleware to all routes
|
||||||
|
- Implement GET `/` — list archive records with optional `?state=` filter, return `{ archives: [...], total: N }`
|
||||||
|
- Implement GET `/stats` — return `{ ACTIVE: N, ARCHIVED: N, RETURNED: N, CLOSED: N, total: N }`
|
||||||
|
- Implement GET `/:findingId/history` — return `{ finding_id, transitions: [...] }` ordered by transitioned_at DESC, return empty array for unknown finding_id
|
||||||
|
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
|
||||||
|
|
||||||
|
- [ ] 4.2 Register archive router in `backend/server.js`
|
||||||
|
- Import `createIvantiArchiveRouter` from `./routes/ivantiArchive`
|
||||||
|
- Mount at `/api/ivanti/archive` with `requireAuth` middleware
|
||||||
|
- _Requirements: 4.1_
|
||||||
|
|
||||||
|
- [ ]* 4.3 Write property test for state filtering
|
||||||
|
- **Property 6: State filter returns only matching records**
|
||||||
|
- Generate archive records with random states, query with filter, verify only matching records returned
|
||||||
|
- **Validates: Requirements 4.1**
|
||||||
|
|
||||||
|
- [ ]* 4.4 Write property test for history ordering
|
||||||
|
- **Property 7: Transition history is ordered by timestamp descending**
|
||||||
|
- Generate multiple transitions for a finding, query history, verify descending timestamp order
|
||||||
|
- **Validates: Requirements 4.2**
|
||||||
|
|
||||||
|
- [ ]* 4.5 Write property test for stats accuracy
|
||||||
|
- **Property 8: Stats counts match actual record distribution**
|
||||||
|
- Generate archive records with random states, query stats, verify counts match actual distribution
|
||||||
|
- **Validates: Requirements 4.3**
|
||||||
|
|
||||||
|
- [ ] 5. Checkpoint — Verify API endpoints
|
||||||
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
|
- [ ] 6. Implement Archive Summary Bar UI component
|
||||||
|
- [ ] 6.1 Create `frontend/src/components/pages/ArchiveSummaryBar.js`
|
||||||
|
- Fetch stats from `/api/ivanti/archive/stats` on mount
|
||||||
|
- Render four stat cards: ACTIVE (sky blue #0EA5E9), ARCHIVED (amber #F59E0B), RETURNED (emerald #10B981), CLOSED (red #EF4444)
|
||||||
|
- Each card shows the count and state label with Lucide icons and monospace typography
|
||||||
|
- Accept `onStateClick` callback prop and `activeFilter` prop for highlighting the selected state
|
||||||
|
- Use inline style objects matching the existing design system (dark gradients, glows, hover effects)
|
||||||
|
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
|
||||||
|
|
||||||
|
- [ ] 6.2 Integrate Archive Summary Bar into the Ivanti findings page
|
||||||
|
- Import and render `ArchiveSummaryBar` in the Ivanti findings section of `App.js` (or the relevant page component)
|
||||||
|
- Wire `onStateClick` to manage a state filter for the archive list display
|
||||||
|
- _Requirements: 5.3_
|
||||||
|
|
||||||
|
- [ ] 7. Final checkpoint — Verify full integration
|
||||||
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||||
|
- Each task references specific requirements for traceability
|
||||||
|
- Checkpoints ensure incremental validation
|
||||||
|
- Property tests use `fast-check` library with minimum 100 iterations per test
|
||||||
|
- All backend code uses callback-based SQLite API wrapped in promises (matching existing patterns)
|
||||||
|
- All frontend code uses plain JavaScript (no TypeScript)
|
||||||
Reference in New Issue
Block a user