Files
cve-dashboard/.kiro/specs/sync-anomaly-detection/requirements.md

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.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.