13 KiB
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.jsthat 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_idfrom Ivanti/RiskSense, cached inivanti_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_archivesandivanti_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_1attribute 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
- 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.
- WHEN the Unfiltered_Query returns a finding with a BU_Field value different from
NTS-AEO-ACCESS-ENGorNTS-AEO-STEAM, THE BU_Drift_Checker SHALL classify that finding asbu_reassignmentand record the new BU value in the archive transition reason. - WHEN the Unfiltered_Query returns a finding with a severity below 8.5 and the BU_Field still matches
NTS-AEO-ACCESS-ENGorNTS-AEO-STEAM, THE BU_Drift_Checker SHALL classify that finding asseverity_driftand record the new severity in the archive transition. - WHEN the Unfiltered_Query does not return a finding at all, THE BU_Drift_Checker SHALL classify that finding as
decommissioned. - WHEN the Unfiltered_Query returns a finding with a state of
Closedand the BU_Field still matches the expected BUs, THE BU_Drift_Checker SHALL classify that finding asclosed_on_platform. - 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. - 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
- 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.
- WHEN findings have been newly archived during the sync, THE Anomaly_Summary SHALL include a breakdown by classification: count of
bu_reassignment, count ofseverity_drift, count ofclosed_on_platform, and count ofdecommissioned. - WHEN findings have transitioned from ARCHIVED to RETURNED during the sync, THE Anomaly_Summary SHALL include the count of returned findings.
- 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.
- WHEN a GET request is made to
/api/ivanti/findings/anomaly/latest, THE Sync_Pipeline SHALL return the most recent Anomaly_Summary row. - 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. - WHEN the total number of newly archived findings in a single sync exceeds 5, THE Anomaly_Summary SHALL flag the sync as
significantin 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
- THE Sync_Pipeline SHALL store the BU_Field value (
buOwnership) on each finding in theivanti_findings_cacheJSON payload, preserving the value extracted fromassetCustomAttributes.1550_host_1. - 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.
- 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.
- 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. - 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. - 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
- THE migration script SHALL create an
ivanti_sync_anomaly_logtable 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). - THE migration script SHALL create an
ivanti_finding_bu_historytable 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). - THE migration script SHALL create an index on
ivanti_sync_anomaly_log(sync_timestamp)for efficient latest-record queries. - THE migration script SHALL create an index on
ivanti_finding_bu_history(finding_id)for efficient per-finding history lookups. - THE migration script SHALL create an index on
ivanti_finding_bu_history(detected_at)for efficient chronological queries. - THE migration script SHALL be located at
backend/migrations/add_sync_anomaly_tables.jsand useCREATE TABLE IF NOT EXISTSandCREATE INDEX IF NOT EXISTSto 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
- WHEN the Vulnerability Triage page loads, THE Anomaly_Banner SHALL fetch the latest Anomaly_Summary from
/api/ivanti/findings/anomaly/latest. - WHEN the latest Anomaly_Summary has
is_significantset 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"). - WHEN the latest Anomaly_Summary has
is_significantset to false, THE Anomaly_Banner SHALL not display a banner. - THE Anomaly_Banner SHALL include a dismiss button that hides the banner for the current session.
- THE Anomaly_Banner SHALL use amber (#F59E0B) background tint and the AlertTriangle icon from Lucide, consistent with the existing dashboard warning patterns.
- THE Anomaly_Banner SHALL use monospace typography and the dark theme color palette defined in DESIGN_SYSTEM.md.
- 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
- WHEN the BU_Drift_Checker classifies a newly archived finding as
bu_reassignment, THE Archive_Detector SHALL update the correspondingivanti_archive_transitionsrow's reason field tobu_reassignment:<new_bu>where<new_bu>is the BU the finding was reassigned to. - WHEN the BU_Drift_Checker classifies a newly archived finding as
severity_drift, THE Archive_Detector SHALL update the correspondingivanti_archive_transitionsrow's reason field toseverity_drift:<new_severity>where<new_severity>is the finding's current severity score. - WHEN the BU_Drift_Checker classifies a newly archived finding as
decommissioned, THE Archive_Detector SHALL update the correspondingivanti_archive_transitionsrow's reason field todecommissioned. - WHEN the BU_Drift_Checker classifies a newly archived finding as
closed_on_platform, THE Archive_Detector SHALL update the correspondingivanti_archive_transitionsrow's reason field toclosed_on_platform. - THE existing archive transition rows with reason
severity_score_driftSHALL 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
- WHEN a GET request is made to
/api/ivanti/findings/anomaly/historywith optional query parametersfromandto(ISO date strings), THE Sync_Pipeline SHALL return Anomaly_Summary rows within the specified date range. - WHEN no
fromortoparameters are provided, THE Sync_Pipeline SHALL return the last 30 Anomaly_Summary rows. - WHEN an unauthenticated request is made to any anomaly or BU history endpoint, THE Sync_Pipeline SHALL return a 401 status code.
- 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.