Files
cve-dashboard/.kiro/specs/finding-archive-tracking/tasks.md
jramos 9bd5a52661 feat: implement finding archive tracking system
- Add migration script for ivanti_finding_archives and ivanti_archive_transitions tables
- Add archive detection logic (detectArchiveChanges, detectClosedFindings) in sync pipeline
- Add archive API router with list, stats, and history endpoints at /api/ivanti/archive
- Add ArchiveSummaryBar UI component with four state cards (ACTIVE, ARCHIVED, RETURNED, CLOSED)
- Integrate ArchiveSummaryBar into Ivanti findings page in App.js
- Register archive router in server.js
2026-04-03 15:20:04 -06:00

8.4 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.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 }. Return 400 with message "Invalid state parameter. Valid values: ACTIVE, ARCHIVED, RETURNED, CLOSED" if an unrecognized state value is provided.
      • 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)