- Add DECOM to queue workflow types (red badge, inventory-style display)
- When findings are added as DECOM, auto-set note to 'DECOM' and hide row
- Hidden rows are excluded from donut charts (removes from pending count)
- Show CVEs on CARD/GRANITE/DECOM queue items (was previously omitted)
- Add backend/migrations/run-all.js for CI/CD auto-migration execution
- Pipeline now runs migrations before service restart on both staging and prod
- Add add_decom_workflow_type.js migration (updates CHECK constraint)
- Rewrite /fp-workflow-counts endpoint to query ivanti_findings table
directly with optional teams ILIKE filter (replaces pre-computed JSON blob)
- Frontend passes getActiveTeamsParam() to FP counts fetch
- FP counts refresh on scope toggle change alongside open/closed counts
- Both FP Finding Status and FP Workflow Status donuts now respect BU scope
- Create ivanti_counts_history_by_bu table (bu_ownership, state, count per sync)
- Sync writes per-BU snapshot alongside global history on each sync
- Seed table with current counts for immediate first data point
- GET /counts/history accepts ?teams param — queries per-BU table when filtered
- IvantiCountsChart accepts teamsParam prop, re-fetches on scope change
- ReportingPage passes getActiveTeamsParam() to the chart
- Historical per-BU data accumulates from this point forward
- Global history (no filter) still uses the original aggregate table
- docs/guides/postgres-migration-plan.md: full migration manual with
phases, port allocation, rollback plan, and timeline
- .kiro/specs/postgres-migration/: requirements, design, and tasks
- Replaces findings_json blob with individual indexed rows
- Enables per-BU closed counts via SQL queries
- Uses existing Postgres instance (port 5432), new cve_dashboard DB
- Testing on port 3003, cutover to 3001 with 30s downtime
- Fetch ALL findings once on mount (no teams param to backend)
- Filter client-side via scopedFindings useMemo keyed on adminScope
- Eliminates 5-10s round-trip on every scope change
- Open vs Closed donut now uses scopedFindings.length for open count
- Closed count remains global (no per-BU closed data available)
- Action Coverage donut automatically scoped via visibleFindings chain
- Remove server-side teams param from counts fetch (client handles it)
- Add bu_teams column to users table (migration + fresh schema)
- Create shared KNOWN_TEAMS constant and validateTeams helper
- Expose user teams in auth middleware, login, and /me responses
- Add bu_teams CRUD to user management routes with audit logging
- Make Ivanti FINDINGS_FILTERS configurable via IVANTI_BU_FILTER env var
- Add query-time team filtering to GET /findings and /findings/counts
- Update AuthContext with teams helpers and admin scope toggle
- Create AdminScopeToggle component (My Teams / All BUs)
- Scope ReportingPage findings fetch by user teams
- Scope CompliancePage team selector by user teams
- Scope ExportsPage findings exports by user teams
- Add BU teams multi-select to UserManagement create/edit forms
- Display team badges in user list table
- 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
Exports page:
- Add Atlas Action Plans export card with three reports: Full Status,
Coverage Gaps, and Full Report (multi-sheet with active, gaps, history)
- Reports join Atlas cache with Ivanti findings for hostname, IP, BU context
Atlas icon:
- Add AtlasIcon SVG component matching the Atlas InfoSec logo (badge with globe)
- Replace Database icon with AtlasIcon on exports card, sync button, and panel header
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
- Add GET /api/ivanti/fp-workflow/documents/search endpoint for querying the document library
- Update POST /api/ivanti/fp-workflow to accept libraryDocIds for attaching library documents on create
- Update POST .../submissions/:id/attachments to accept libraryDocIds on edit
- Add AttachmentSourcePicker component with local upload and library search modes
- Integrate picker into FpWorkflowModal (create) and FpEditModal (edit)
- Track attachment source (local/library) in attachment_results_json for traceability
- Add GRANITE to VALID_WORKFLOW_TYPES in backend (no vendor required, same as CARD)
- Update vendor validation and error messages across all endpoints (single add, batch, PUT, redirect)
- Add GRANITE option to RedirectModal with warm slate color (#A1887F)
- Rename QueuePanel CARD section to Inventory, group CARD + GRANITE with sub-divider
- Add GRANITE to AddToQueuePopover and SelectionToolbar
- Update spec docs (requirements, design, tasks)
- Revert workflow badge to static (non-clickable) — queue panel is the entry point
- Fix migration: use DEFAULT NULL for updated_at (SQLite disallows CURRENT_TIMESTAMP in ALTER TABLE)
- Add useMemo enrichment to cross-reference submission lifecycle_status with actual Ivanti workflow state from findings data
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
- Add GET /api/cves/:cveId/tooltip backend endpoint with description truncation
- Create CveTooltip portal component with caching, severity badges, and viewport-aware positioning
- Integrate tooltip into ReportingPage with 300ms hover delay on CVE badge spans
- Add migration to add hostname column to ivanti_todo_queue table
- Update POST and batch POST endpoints to accept and store hostname
- Pass hostName from findings data when adding items to queue
- Display hostname and IP address in CARD queue section
- Display hostname and IP address in vendor (FP/Archer) queue sections
- Fix copy-paste bug in ivantiFpWorkflow.js where both ternary branches
returned 'partial'; simplified to direct assignment
- Remove unused shouldShowFpButton() from ReportingPage.js (canWrite
from useAuth() is used instead)
- Hoist repeated isCreateFpButtonEnabled() calls into a single variable
in QueuePanel render
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>
Table: removed disabled={queued} from the row checkbox so accentColor
renders properly — checked rows now show a solid blue tick instead of
the greyed-out browser default.
Queue panel: each item now has a small red selection checkbox (opacity
0.35 when idle, full when selected). Selecting any items reveals a red
'Delete (N)' button in the footer alongside 'Clear Completed'. Bulk
deletes run in parallel; selection state is automatically pruned when
items are removed via the individual trash button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ip_address column to ivanti_todo_queue so CARD entries carry the
host IP needed to locate the asset in CARD.
- Migration: ALTER TABLE ADD COLUMN ip_address TEXT (safe to re-run)
- Backend: accepts ip_address in POST body, stores up to 64 chars
- Frontend: captures finding.ipAddress when adding to queue; CARD items
in the queue panel show the IP in green instead of the CVE list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CARD workflow type no longer requires a vendor/platform entry since
asset disposition is handled entirely within CARD. In the popover the
vendor field is replaced with a note when CARD is selected, and the
Add button is enabled immediately.
In the queue panel, CARD items are separated into their own top section
(green header) rather than being mixed into vendor groups.
Backend validation updated to skip vendor requirement for CARD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Popover now flips above the row when it would overflow the bottom of the
viewport, and clamps horizontally to stay within the window.
Adds CARD as a third workflow type (for out-of-team asset disposition in
CARD) alongside FP and Archer. CARD is styled in green (#10B981) across
the popover toggle and queue panel badge.
DB: new migration (add_card_workflow_type.js) recreates ivanti_todo_queue
with an updated CHECK constraint to allow 'CARD'; run manually on dev.
App-level validation in the route is updated to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Columns that contain any blank values now show a '— empty —' entry at the
top of the filter dropdown. Selecting only that entry shows findings with
nothing in that column (e.g. workflow with no FP# ticket assigned).
Uses an EMPTY_SENTINEL constant ('__EMPTY__') in the filter Set so blank
cells are handled distinctly from non-blank values. Works for both
single-value and multi-value (CVEs) columns.
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>
Adds a new SVG donut chart showing the distribution of FP workflow states
(Actionable, Requested, Reworked, Approved, Rejected, Expired, Unknown)
for all findings that have an associated FP# workflow ticket.
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>
Replace FP# Workflow chart with a 3-segment Action Coverage donut:
- FP Request — finding has an Ivanti FP# workflow
- Archer Exception — note matches EXC-\d+ pattern
- Pending — no action taken yet
Clicking a segment filters the findings table to that category with a
colored badge in the action bar (click again or × to clear).
Home page: each Archer ticket now has a filter icon button that navigates
directly to the Reporting page pre-filtered to findings whose notes
reference that EXC number. The EXC badge appears in the table action bar
with a one-click clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>