- Add BU drift checker that classifies archived findings as BU reassignment, severity drift, closure, or decommission via unfiltered Ivanti API queries - Add post-sync anomaly summary with significance threshold and classification breakdown stored in ivanti_sync_anomaly_log table - Add per-finding BU tracking that detects BU changes across syncs and records them in ivanti_finding_bu_history table - Add drift guard that skips trend history writes when total drops more than 50% - Add CLOSED_GONE archive state for findings that vanish from the closed set - Add anomaly banner UI on Vulnerability Triage page for significant sync changes - Add API endpoints for anomaly latest/history and BU change tracking - Add diagnostic scripts for drift checking and BU reassignment verification - Add investigation document and xlsx export for the April 2026 BU reassignment incident where 109 findings were moved to SDIT-CSD-ITLS-PIES - Migrations required: add_closed_gone_state.js, add_sync_anomaly_tables.js
113 lines
13 KiB
Markdown
113 lines
13 KiB
Markdown
# Requirements Document
|
|
|
|
## Introduction
|
|
|
|
The Sync Anomaly Detection and BU Drift Monitoring feature extends the Ivanti sync pipeline to automatically classify why findings disappear from sync results. The current archive system detects disappearances but cannot distinguish between BU reassignment, severity score drift, and host decommission. This feature adds three capabilities: post-sync BU drift spot-checks against the unfiltered Ivanti API, a structured sync anomaly summary that breaks down count changes by cause, and per-finding BU tracking that records the business unit on each cached finding and detects BU changes across syncs. Together, these close the visibility gap exposed by the April 2026 incident where 109 findings silently disappeared due to a bulk BU reassignment from NTS-AEO-STEAM to SDIT-CSD-ITLS-PIES.
|
|
|
|
## Glossary
|
|
|
|
- **Sync_Pipeline**: The existing Ivanti/RiskSense host finding sync process in `backend/routes/ivantiFindings.js` that fetches open and closed 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, cached in `ivanti_findings_cache`.
|
|
- **Archive_Detector**: The existing logic within the Sync_Pipeline that compares previous sync results against current results to identify disappeared and returned findings, writing to `ivanti_finding_archives` and `ivanti_archive_transitions`.
|
|
- **BU_Drift_Checker**: New post-sync logic that queries the Ivanti API without BU filters for a sample of archived findings to determine whether they were reassigned to a different business unit.
|
|
- **Anomaly_Summary**: A structured report generated after each sync that categorizes finding count changes by cause (BU reassignment, severity drift, closure, decommission) and stores the results for API retrieval and UI display.
|
|
- **Sync_Anomaly_Log**: A new database table (`ivanti_sync_anomaly_log`) that stores one row per sync cycle containing the anomaly summary breakdown and metadata.
|
|
- **Finding_BU_History**: A new database table (`ivanti_finding_bu_history`) that records BU changes detected on individual findings across syncs.
|
|
- **BU_Field**: The `assetCustomAttributes.1550_host_1` attribute on an Ivanti host finding that identifies the owning business unit (e.g., `NTS-AEO-STEAM`, `NTS-AEO-ACCESS-ENG`).
|
|
- **Anomaly_Banner**: A React UI component displayed on the Vulnerability Triage page that surfaces the most recent sync anomaly summary when significant count changes are detected.
|
|
- **Unfiltered_Query**: An Ivanti API call that searches by finding ID only, without BU or severity filters, used to determine the current BU and severity of a finding that disappeared from filtered results.
|
|
- **Spot_Check**: A targeted Unfiltered_Query for a batch of recently archived findings, performed after each sync to classify disappearance causes without querying every archived finding.
|
|
|
|
## Requirements
|
|
|
|
### Requirement 1: BU Drift Detection on Sync
|
|
|
|
**User Story:** As a security analyst, I want the system to automatically check whether archived findings were reassigned to a different BU, so that I can distinguish BU reassignment from score drift or decommission without running manual diagnostic scripts.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. WHEN the Sync_Pipeline completes a successful sync and new findings have been archived, THE BU_Drift_Checker SHALL query the Ivanti API using an Unfiltered_Query for the finding IDs of all newly archived findings from that sync cycle.
|
|
2. WHEN the Unfiltered_Query returns a finding with a BU_Field value different from `NTS-AEO-ACCESS-ENG` or `NTS-AEO-STEAM`, THE BU_Drift_Checker SHALL classify that finding as `bu_reassignment` and record the new BU value in the archive transition reason.
|
|
3. WHEN the Unfiltered_Query returns a finding with a severity below 8.5 and the BU_Field still matches `NTS-AEO-ACCESS-ENG` or `NTS-AEO-STEAM`, THE BU_Drift_Checker SHALL classify that finding as `severity_drift` and record the new severity in the archive transition.
|
|
4. WHEN the Unfiltered_Query does not return a finding at all, THE BU_Drift_Checker SHALL classify that finding as `decommissioned`.
|
|
5. WHEN the Unfiltered_Query returns a finding with a state of `Closed` and the BU_Field still matches the expected BUs, THE BU_Drift_Checker SHALL classify that finding as `closed_on_platform`.
|
|
6. IF the Unfiltered_Query fails due to an API error, THEN THE BU_Drift_Checker SHALL log the error and leave the archive transition reason as the existing default (`severity_score_drift`) without blocking the sync completion.
|
|
7. THE BU_Drift_Checker SHALL batch finding IDs into groups of 50 for the Unfiltered_Query to stay within Ivanti API limits.
|
|
|
|
### Requirement 2: Sync Anomaly Summary
|
|
|
|
**User Story:** As a security analyst, I want a post-sync summary that explains significant count changes, so that I have immediate visibility into what happened without manual investigation.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. WHEN the Sync_Pipeline completes a successful sync, THE Anomaly_Summary SHALL compute the difference between the current open count and the previous open count, and between the current closed count and the previous closed count.
|
|
2. WHEN findings have been newly archived during the sync, THE Anomaly_Summary SHALL include a breakdown by classification: count of `bu_reassignment`, count of `severity_drift`, count of `closed_on_platform`, and count of `decommissioned`.
|
|
3. WHEN findings have transitioned from ARCHIVED to RETURNED during the sync, THE Anomaly_Summary SHALL include the count of returned findings.
|
|
4. THE Anomaly_Summary SHALL be stored as a row in the Sync_Anomaly_Log table with the sync timestamp, open count delta, closed count delta, classification breakdown as a JSON object, and the total number of newly archived findings.
|
|
5. WHEN a GET request is made to `/api/ivanti/findings/anomaly/latest`, THE Sync_Pipeline SHALL return the most recent Anomaly_Summary row.
|
|
6. WHEN a GET request is made to `/api/ivanti/findings/anomaly/history`, THE Sync_Pipeline SHALL return the last 30 Anomaly_Summary rows ordered by sync timestamp descending.
|
|
7. WHEN the total number of newly archived findings in a single sync exceeds 5, THE Anomaly_Summary SHALL flag the sync as `significant` in the stored record.
|
|
|
|
### Requirement 3: Finding-Level BU Tracking
|
|
|
|
**User Story:** As a security analyst, I want the system to store the BU on each cached finding and detect when a finding's BU changes across syncs, so that BU reassignment is tracked as a distinct event from disappearance.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. THE Sync_Pipeline SHALL store the BU_Field value (`buOwnership`) on each finding in the `ivanti_findings_cache` JSON payload, preserving the value extracted from `assetCustomAttributes.1550_host_1`.
|
|
2. WHEN the Sync_Pipeline processes a sync result, THE Sync_Pipeline SHALL compare each finding's current BU_Field value against the previously cached BU_Field value for the same finding ID.
|
|
3. WHEN a finding's BU_Field value differs from the previously cached value and both values are non-empty, THE Sync_Pipeline SHALL insert a row into the Finding_BU_History table recording the finding_id, previous BU, new BU, and detection timestamp.
|
|
4. WHEN a GET request is made to `/api/ivanti/findings/bu-changes`, THE Sync_Pipeline SHALL return all Finding_BU_History rows ordered by detected_at descending.
|
|
5. WHEN a GET request is made to `/api/ivanti/findings/:findingId/bu-history`, THE Sync_Pipeline SHALL return the Finding_BU_History rows for the specified finding ordered by detected_at descending.
|
|
6. IF a finding appears in the sync results for the first time with no previously cached BU_Field value, THEN THE Sync_Pipeline SHALL store the BU_Field value without recording a BU change event.
|
|
|
|
### Requirement 4: Database Schema for Anomaly and BU Tracking
|
|
|
|
**User Story:** As a developer, I want the anomaly and BU tracking data stored in normalized SQLite tables, so that the data model supports efficient queries and integrates with the existing migration pattern.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. THE migration script SHALL create an `ivanti_sync_anomaly_log` table with columns for id (autoincrement primary key), sync_timestamp (datetime), open_count_delta (integer), closed_count_delta (integer), newly_archived_count (integer), returned_count (integer), classification_json (text storing the breakdown object), is_significant (boolean integer), and created_at (datetime).
|
|
2. THE migration script SHALL create an `ivanti_finding_bu_history` table with columns for id (autoincrement primary key), finding_id (text), finding_title (text), host_name (text), previous_bu (text), new_bu (text), detected_at (datetime), and created_at (datetime).
|
|
3. THE migration script SHALL create an index on `ivanti_sync_anomaly_log(sync_timestamp)` for efficient latest-record queries.
|
|
4. THE migration script SHALL create an index on `ivanti_finding_bu_history(finding_id)` for efficient per-finding history lookups.
|
|
5. THE migration script SHALL create an index on `ivanti_finding_bu_history(detected_at)` for efficient chronological queries.
|
|
6. THE migration script SHALL be located at `backend/migrations/add_sync_anomaly_tables.js` and use `CREATE TABLE IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS` to be idempotent.
|
|
|
|
### Requirement 5: Anomaly Banner UI
|
|
|
|
**User Story:** As a security analyst, I want a banner on the Vulnerability Triage page that surfaces the latest sync anomaly summary, so that significant count changes are immediately visible without navigating to a separate page.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. WHEN the Vulnerability Triage page loads, THE Anomaly_Banner SHALL fetch the latest Anomaly_Summary from `/api/ivanti/findings/anomaly/latest`.
|
|
2. WHEN the latest Anomaly_Summary has `is_significant` set to true, THE Anomaly_Banner SHALL display a warning banner showing the total count change and the classification breakdown (e.g., "45 findings archived — 38 BU reassignment, 5 closed, 2 decommissioned").
|
|
3. WHEN the latest Anomaly_Summary has `is_significant` set to false, THE Anomaly_Banner SHALL not display a banner.
|
|
4. THE Anomaly_Banner SHALL include a dismiss button that hides the banner for the current session.
|
|
5. THE Anomaly_Banner SHALL use amber (#F59E0B) background tint and the AlertTriangle icon from Lucide, consistent with the existing dashboard warning patterns.
|
|
6. THE Anomaly_Banner SHALL use monospace typography and the dark theme color palette defined in DESIGN_SYSTEM.md.
|
|
7. WHEN the user clicks the classification breakdown text in the Anomaly_Banner, THE Anomaly_Banner SHALL expand to show a detailed list of affected findings grouped by classification.
|
|
|
|
### Requirement 6: Archive Transition Reason Enhancement
|
|
|
|
**User Story:** As a security analyst, I want archive transitions to record the specific reason a finding disappeared (BU reassignment, severity drift, decommission, closure), so that the archive history provides actionable context instead of a generic "severity_score_drift" label.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. WHEN the BU_Drift_Checker classifies a newly archived finding as `bu_reassignment`, THE Archive_Detector SHALL update the corresponding `ivanti_archive_transitions` row's reason field to `bu_reassignment:<new_bu>` where `<new_bu>` is the BU the finding was reassigned to.
|
|
2. WHEN the BU_Drift_Checker classifies a newly archived finding as `severity_drift`, THE Archive_Detector SHALL update the corresponding `ivanti_archive_transitions` row's reason field to `severity_drift:<new_severity>` where `<new_severity>` is the finding's current severity score.
|
|
3. WHEN the BU_Drift_Checker classifies a newly archived finding as `decommissioned`, THE Archive_Detector SHALL update the corresponding `ivanti_archive_transitions` row's reason field to `decommissioned`.
|
|
4. WHEN the BU_Drift_Checker classifies a newly archived finding as `closed_on_platform`, THE Archive_Detector SHALL update the corresponding `ivanti_archive_transitions` row's reason field to `closed_on_platform`.
|
|
5. THE existing archive transition rows with reason `severity_score_drift` SHALL remain unchanged — the enhanced reasons apply only to transitions created after this feature is deployed.
|
|
|
|
### Requirement 7: Anomaly History API for Trend Analysis
|
|
|
|
**User Story:** As a security analyst, I want to view anomaly history over time, so that I can identify patterns in BU reassignments and score drift across multiple sync cycles.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
1. WHEN a GET request is made to `/api/ivanti/findings/anomaly/history` with optional query parameters `from` and `to` (ISO date strings), THE Sync_Pipeline SHALL return Anomaly_Summary rows within the specified date range.
|
|
2. WHEN no `from` or `to` parameters are provided, THE Sync_Pipeline SHALL return the last 30 Anomaly_Summary rows.
|
|
3. WHEN an unauthenticated request is made to any anomaly or BU history endpoint, THE Sync_Pipeline SHALL return a 401 status code.
|
|
4. THE anomaly history response SHALL include each row's sync_timestamp, open_count_delta, closed_count_delta, newly_archived_count, returned_count, classification_json (parsed as an object), and is_significant flag.
|