- 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
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
Bugs fixed:
- knowledgeBase.js: logAudit calls converted from positional args to object signature
- archerTickets.js: targetType/targetId renamed to entityType/entityId
- server.js: single CVE delete now has cascade/compliance check for Standard_User
Unprotected endpoints secured:
- ivantiTodoQueue.js: POST/PUT/DELETE now require Admin or Standard_User
- ivantiFindings.js: PUT note and POST sync now require Admin or Standard_User
- compliance.js: POST notes now requires Admin or Standard_User
- ivantiWorkflows.js: POST sync now requires Admin or Standard_User
- auth.js: cleanup-sessions now requires Admin via requireAuth + requireGroup
Additional fixes:
- ExportsPage.js: canExport() guard blocks Read_Only users
- knowledgeBase.js: Standard_User delete checks created_by ownership
- Migration: added INSERT/UPDATE triggers to enforce valid user_group values
- 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
Add time-based open/closed tracking for Ivanti findings (Tier 2 from
the reporting recommendations doc) and rename the Reporting page to
Vulnerability Triage to better reflect its purpose.
Backend — ivantiFindings.js:
- Create ivanti_counts_history table (appended on every sync, never
overwritten — Option B from design discussion)
- INSERT snapshot after each successful syncClosedCount() call
- GET /api/ivanti/findings/counts/history endpoint — returns last
snapshot per calendar day using ROW_NUMBER window function, so
multiple daily syncs collapse to the end-of-day value
Frontend:
- New IvantiCountsChart component: collapsible dual-line chart
(open vs closed) with dark tooltip, delta label showing change
since previous day, and graceful no-data states
- Chart placed between the donut metrics panel and the findings table
on the Vulnerability Triage page
- Renamed page: 'reporting' → 'triage' (page ID, nav label, component
export, all cross-file references)
- ComplianceDetailPanel "View in Reporting" link updated to "View in
Triage" and navigates to the correct page ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renamed the existing FP chart to "FP Finding Status" (counts findings per
workflow state) and added a new "FP Workflow Status" chart that counts
unique FP# ticket IDs per state — so 10 findings under one FP# ticket
counts as 1 ticket, not 10.
Backend: extractFPWorkflow now returns { id, state }; syncFPWorkflowCounts
builds both a finding-count map and a deduped FP# ID map, storing them in
separate columns (fp_workflow_counts_json, fp_id_counts_json). The endpoint
returns findingCounts/findingTotal and idCounts/idTotal.
Frontend: FPWorkflowDonut accepts a centerLabel prop; both donuts share the
same component fed with their respective data slices from the single fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The FP Workflow Status donut was reading from the in-memory open findings
array, so Approved FPs (which close the finding and remove it from the
open cache) were invisible.
Backend: during each sync, compute FP workflow state counts from open
findings then sweep all pages of closed findings to capture Approved
(and any other closed-state) FP workflows. Counts are stored in a new
fp_workflow_counts_json column on ivanti_counts_cache and exposed via
GET /api/ivanti/findings/fp-workflow-counts.
Frontend: FPWorkflowDonut now receives counts/total props from the new
endpoint (fetched on load and refreshed after manual sync) instead of
deriving them from the findings prop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- New ivanti_finding_overrides table (finding_id, field, value) with
UNIQUE(finding_id, field) — same survival-across-sync pattern as notes
- PUT /api/ivanti/findings/:id/override (editor/admin only) — saves or
clears a field override; empty value = revert to Ivanti
- Overrides merged into findings at read time via readOverrides()
- Whitelisted fields: hostName, dns
Frontend:
- OverrideCell component — click to edit inline (editor/admin only),
Enter/blur to save, Escape to cancel
- Amber dot indicator on cells with an active local override
- Hover tooltip shows original Ivanti value when overridden
- RotateCcw button reverts cell back to Ivanti value in one click
- canWrite() gating via useAuth — viewers see the value, can't edit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: adds ivanti_counts_cache table, fetches Closed count (page 0,
size 1) from Ivanti after each Open sync, and exposes GET /counts endpoint.
Frontend: replaces the Metrics placeholder with an SVG donut chart showing
Open vs Closed proportions with counts and percentages. Counts are fetched
on mount and refreshed after manual sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: only extract FP# workflows; SYS# auto-generated tickets
are no longer stored or shown (not actionable for triage purposes).
Findings with no FP# ticket show blank in the workflow column.
- Frontend: recolor workflow badges by action urgency —
Expired/Rejected = red (act now), Reworked/Actionable = amber
(resubmit), Requested = blue (waiting on approval).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: extractFinding now flattens all workflowDistribution buckets
and prioritises FP# (False Positive) tickets over SYS# workflows.
Falls back to workflowGeneratedNames for FP# IDs not yet in distribution.
- Frontend: Add Workflow column (sortable, filterable) with state-coloured
badge (green=Approved, blue=Requested, amber=Reworked/Actionable,
red=Rejected, grey=Expired/unknown).
- Bump localStorage key to v2 so the new column appears on all clients
without needing a manual cache clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend extracts cves[] array from f.vulnerabilities.vulnInfoList[].cve
- Frontend shows up to 2 CVE badges (purple) with "+N more" overflow tooltip
- Filter is multi-value aware: selecting a CVE matches any finding containing it
- FilterDropdown expands multi-value arrays into individual checkbox options
- Sort by CVE count (number of associated CVEs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- buOwnership field extracted from assetCustomAttributes['1550_host_1'][0]
and stored in SQLite cache; badge-styled cell (sky=STEAM, amber=ACCESS-ENG)
- All columns except Notes get a funnel filter button in the header
- FilterDropdown uses ReactDOM.createPortal + fixed positioning to escape
overflowX:auto clipping; shows unique value checkboxes with search input,
Select All, Clear, and a selected/total count footer
- Severity filter groups by vrrGroup label (CRITICAL/HIGH) not numeric value
- columnFilters state gates a useMemo filtered array before sorting
- Active filter count shown in panel header with amber badge; Clear Filters
button appears in the toolbar when any filters are active
- Empty Set filter (Clear All) hides all rows, consistent with Excel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Extract dueDate from statusEmbedded.dueDate (strip time portion)
- Remove discoveredOn and source from extractFinding (not needed)
Frontend:
- Add Due Date column (color-coded: red=past due, amber=within 30d, gray=future)
- Remove Discovered and Source columns
- ColumnManager component: gear button opens popover with drag-to-reorder and
eye toggle per column; column state persisted to localStorage
- Column order/visibility survives page refresh and syncs
- SortIcon, TableCell, NoteCell all driven by current visible column list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>