8.3 KiB
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.jsmigration script- Create
ivanti_finding_archivestable 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_transitionstable 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 EXISTSandCREATE INDEX IF NOT EXISTSfor 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
- Create
-
* 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 tobackend/routes/ivantiFindings.js- Create both archive tables inline (same pattern as existing
initTables) so they exist on startup - Call from
createIvantiFindingsRouterduring init alongside existinginitTables - Requirements: 3.1, 3.2, 3.3, 3.4
- Create both archive tables inline (same pattern as existing
-
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.runwith callbacks wrapped in promises (matching existingdbRunhelper 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_cacheaspreviousFindings - After successful cache update, call
detectArchiveChanges(db, previousFindings, currentFindings) - Skip archive detection if sync encountered an error (requirement 1.5)
- Call
detectClosedFindingsduringsyncClosedCountwith closed finding IDs - Requirements: 1.1, 1.5, 2.3
- Before updating the cache, read the current findings from
-
* 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.jsroute 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
- Export factory function
-
4.2 Register archive router in
backend/server.js- Import
createIvantiArchiveRouterfrom./routes/ivantiArchive - Mount at
/api/ivanti/archivewithrequireAuthmiddleware - Requirements: 4.1
- Import
-
* 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/statson 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
onStateClickcallback prop andactiveFilterprop 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
- Fetch stats from
-
6.2 Integrate Archive Summary Bar into the Ivanti findings page
- Import and render
ArchiveSummaryBarin the Ivanti findings section ofApp.js(or the relevant page component) - Wire
onStateClickto manage a state filter for the archive list display - Requirements: 5.3
- Import and render
-
-
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-checklibrary 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)