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
|
||||
Reference in New Issue
Block a user