- Infoblox WAPI integration (read-only) for resolving IPs to FQDNs
- backend/helpers/infobloxApi.js — Basic auth, host/PTR/IPv6 lookups
- backend/routes/infoblox.js — /api/infoblox endpoints
- Globe icon on Reporting page hostName/dns columns for one-click DNS lookup
- Pending: WAPI credentials with API access permissions
- Scan Posture page for Access Ops platform/version tracking
- backend/routes/scanPosture.js
- frontend/src/components/pages/ScanPosturePage.js
- Page visibility and nav drawer entries
- Ivanti findings OS field enrichment
- Migration to add os_name/os_class/os_version columns
- Backfill script for existing findings
- ivantiFindings route updates to persist OS data on sync
Transform Exports page from download-only to interactive report workbench.
All export buttons now open a full-page ReportViewer with sort, filter,
inline edit, row deletion, and xlsx download of the curated data.
New features:
- ReportViewer component with multi-sheet tab support
- Atlas Commitment Dates report (overdue highlighting, hostname resolution from DB)
- Scan Type Coverage report (agent/network/mixed per host with summary bar)
- GET /api/atlas/commitments endpoint (JOINs atlas cache with findings for hostnames)
Detect whether a finding was discovered by Qualys Cloud Agent (authenticated)
or a network appliance scan (unauthenticated) based on the presence of
'Agent ID' in hostAdditionalDetails from the Ivanti API response.
- Add scan_type column to ivanti_findings table (migration)
- Extract scanType in extractFinding() during sync
- Include scan_type in upsert and API response
- Add ScanTypeBadge component (green AGT / orange NET) on ReportingPage
- Add /raw-inspect diagnostic endpoint for inspecting raw Ivanti data
The /devices/by-ip/:ip endpoint previously returned 404 when an IP existed
in NetBox IPAM but wasn't assigned to a device interface. Now returns the
IPAM record (dns_name, status, role, description, comments) with a 200
response and ipamOnly flag.
Frontend NetBoxBadge gains a new 'ipam_only' state — purple 'IP' pill badge
with a dedicated tooltip showing available IPAM metadata (address, status,
DNS name, FQDN, role, VRF, tenant). Distinguishes clearly from full device
records while still surfacing useful infrastructure context.
New features:
- Granite supplemental workbook ingest with reconciliation
- Drag-fill and bulk default cascade in Granite Loader
- OS type searchable dropdown in Granite Loader
- Persist selections across pages + Select All
- NetBox badge on Reporting page with CARD cross-check
- CARD API connection retry for DNS round-robin dead nodes
- Granite Loader export to supplemental hygiene section
Fixes:
- Compliance reconcile stale core columns (type field vs string matching)
- Compliance upload case-insensitive column matching
- Compliance summary query NULL vertical handling
- Compliance page state persistence across uploads
- TLS for GitLab feedback integration after v19 upgrade
Also: updated user guide with all new features, added release comms template.
The compliance_config.json was pruned to remove metrics no longer tracked,
but the frontend category map was not updated in sync. This caused the
compliance-drift-metric-coverage test to fail in CI.
Map the OS (UDA#CONTROLLER CONFIG#OS) and OSTYPE (UDA#EQUIP MIGRATION#OSTYPE)
columns to a picklist of 38 standard OS types extracted from the Granite
Team_Device Loader reference sheet. Both columns now render as searchable
dropdowns in the bulk defaults row and inline cell editing.
- Parser (parse_compliance_xlsx.py): column name matching is now
case-insensitive via _resolve_col() helper. New reports using lowercase
headers (e.g. 'preferred - hostname') are parsed correctly instead of
silently returning 0 items.
- Drift checker (driftChecker.js): core column presence check uses
case-insensitive comparison so lowercase headers no longer trigger
false 'missing from all detail sheets' breaking findings.
- Summary endpoint (compliance.js): query now selects the most recent
upload where vertical IS NULL OR vertical = 'NTS_AEO' in a single
query, instead of preferring NULL-vertical legacy uploads that are
outdated.
- Page persistence (App.js): localStorage page restore no longer checks
canAccessPage during useState init (user is null at that point).
An effect validates the page once auth resolves.
Closes#45
- Selections persist when paging through the device table
- Select All button fetches all hostnames for the active sheet
- Granite Loader button fetches full device data for all selected
(not just current page) before opening the modal
- Clear selection button when items are selected across pages
- Page checkbox adds to existing selection instead of replacing
- Drag-fill: hover bottom-right corner of a cell to get a fill handle,
drag down to fill that value into rows below (Excel-style)
- Bulk default deletion now cascades: clearing a default also removes
per-row overrides that matched that value
- initialDevices now passes equip_inst_id through from supplemental data
Select devices via checkboxes in the Granite Hygiene device table and
generate Granite Loader sheets directly — equip_inst_id is already
present from the supplemental data so no CARD enrichment needed.
When hovering a CVE badge on the Reporting Page that doesn't exist in the
database, the tooltip now shows a 'Not yet tracked' message with an 'Add CVE'
button (permission-gated to Admin/Standard_User). Clicking opens a modal that
auto-populates metadata from NVD and submits via POST /api/cves.
Features:
- Create_Tooltip variant in CveTooltip for exists:false responses
- CveCreateModal with NVD auto-population, form validation, error handling
- 150ms hover bridge so users can reliably click the tooltip button
- Cache invalidation on successful creation
- Success toast notification (4s auto-dismiss)
On retry, only send devices still missing EQUIP_INST_ID to CARD.
Devices that already came back with data on the first pass are
skipped, reducing API calls and wait time when only a subset of
devices timed out.
Add 10 most-used APPIDs (CMTS, Jump Servers, Console Servers, RANCID,
CableOS, NDI, NTP, Vecima, Tools Infra, Adtran) as starred entries
at the top of the dropdown for quick access. Full CMDB list follows
below. Star prefix is stripped during value extraction so cell still
stores clean 'APP_ID - APPREFID' format.
The SearchableSelect shows full context for searching (app name, env)
but when a selection is made, only the 'APP_ID - APPREFID' portion is
stored as the cell value. Applies to both bulk defaults and per-cell
inline edits.
SearchableSelect expects string arrays, not objects. Convert CMDB
options to strings in format 'APP_NAME | APP_ID - APPREFID [ENV]'
so filtering and display work correctly.
Load CMDB app instance data (4797 entries) from static JSON and present
a searchable dropdown in the APP_ID_ASSET_TAG column. Users search by
app name or APP ID, select an entry, and the field populates with the
'APP_ID - APPREFID' format required by Granite.
- Convert Cherwell CMDB CSV export to frontend/public/cmdb-appids.json
- LoaderModal: load APPID options on open, provide via getPicklist()
helper that overrides COLUMN_PICKLISTS for the APP_ID_ASSET_TAG field
- Options display as 'APP_NAME (APP_ID) [ENV]' for easy identification
- Works in both bulk defaults and per-cell inline editing
The BU history records may have been written days before the anomaly
detected them leaving scope. Instead of a fragile time window, fetch
the N most recent BU change records where N matches the classification
count shown in the banner. This ensures the detail always matches
the summary.
- Backend: /anomaly/latest now returns the most recent SIGNIFICANT
anomaly instead of the absolute latest (non-significant ones are
useless to the banner which skips them anyway)
- Frontend: expand BU detail time window from 60 minutes to 25 hours
before the anomaly timestamp to capture changes from the full sync
cycle (syncs run once per 24h)
Same UX improvements as the modal — search bar, group filter chips,
sortable column headers, and filtered count. Applied to the in-page
UserManagementPanel in AdminPage.js so the Admin Panel tab has
parity with the modal.
- Search bar filters users by name, email, or team assignment
- Group filter chips: All, Admin, Standard_User, Leadership, Read_Only, No Teams
- Sortable column headers (User, Group, Teams, Status, Last Login)
- Shows filtered count (e.g., '5 of 12 users')
- 'No Teams' filter quickly identifies users needing team assignment
Backend:
- GET /api/ivanti/findings/org-hierarchy — returns all SVP names and
all BU names from Ivanti suggest API (Admin only)
- GET /api/ivanti/findings/bus-by-svp?svp=<name> — returns BUs under
a specific SVP with finding counts (Admin only)
Frontend (Admin > BU Lookup tab):
- 'Load Org Hierarchy' button fetches all SVPs and BUs
- SVP dropdown (sorted by finding count) to filter BUs by org leader
- Results table shows BU name, finding count, and configured status
(green badge for BUs already in the dashboard, grey for unconfigured)
- Enables admin to discover which BUs roll up under an SVP for correct
team assignment when onboarding new users
Backend:
- GET /api/ivanti/findings/bu-lookup?q=<hostname|ip> — queries the
live Ivanti API to discover which BU a host is assigned to. Returns
deduplicated results with hostName, ipAddress, BU, and hostId.
Admin-only endpoint.
Frontend:
- Add 'BU Lookup' tab to Admin page with search input and results table
- Shows BU assignment badge (blue for tagged, amber for untagged)
- Supports Enter key to search, loading state, error display
- Useful for verifying team assignments when onboarding new users
Headings now render with id attributes derived from their text so
TOC anchor links (#getting-started, etc.) have targets. Internal
anchor clicks use scrollIntoView with smooth behavior instead of
full page navigation.
- UserGuideModal: full-screen overlay rendering user-guide.md as
styled markdown with search filtering
- UserMenu: add 'User Guide' item with BookOpen icon between
Feature Request and Sign Out
- Serve user-guide.md as static file from frontend/public/
- Guide renders with dark theme styling matching the dashboard
aesthetic (blue headers, slate text, amber blockquotes)
Allow Admin users to temporarily view the app as another user to verify
permissions and team scoping without switching accounts.
Backend:
- Migration: add impersonate_user_id column to sessions table
- requireAuth(): when impersonation is active, override req.user with
target user's identity; store real admin identity in req.realUser
- POST /api/auth/impersonate: start impersonation (Admin only, cannot
impersonate self or other Admins)
- POST /api/auth/stop-impersonate: end impersonation, revert to real user
- GET /api/auth/me: returns impersonating flag and realUser when active
- Audit logging on impersonate start/stop
Frontend:
- AuthContext: add impersonating, realUser state; startImpersonation()
and stopImpersonation() helpers
- ImpersonationBanner: fixed amber banner showing target user identity
with Exit button
- UserManagement: Eye icon button on each non-Admin user row to start
View As (visible only to Admin, hidden for self and other Admins)
- App.js: mount ImpersonationBanner at top of authenticated view
Introduce a Page Visibility Matrix that controls which pages each user
group can access, enforced in both frontend and backend:
Frontend:
- Create frontend/src/config/pageVisibility.js with PAGE_VISIBILITY
matrix and canAccessPage() / getAccessiblePages() helpers
- NavDrawer: replace inline requiredGroups with canAccessPage() filter
- App.js: replace per-page isInGroup()/isAdmin() checks with generic
route guard in setCurrentPage; remove VALID_PAGES constant
- localStorage validation: verify persisted page is accessible on load
Backend (page-level access enforcement):
- jiraTickets.js: add router-level requireGroup('Admin','Standard_User')
- archerTemplates.js: add router-level requireGroup('Admin','Standard_User')
- VCL multi-vertical already had requireGroup('Admin','Leadership')
Visibility matrix:
- Home, Knowledge Base: all groups
- Triage, Compliance, Exports: Admin, Standard_User, Leadership
- CCP Metrics: Admin, Leadership
- Jira, Archer Templates: Admin, Standard_User
- Admin Panel: Admin only
- Read_Only sees only Home and Knowledge Base
Replace the old prompt()-based file upload flow with an inline drop zone.
Before: Click 'Upload Doc' → native file picker → prompt() for type →
prompt() for notes → upload. Three modal interruptions.
After: Drop a file (or click to browse) → inline form shows with type
dropdown and notes field → click Upload. Zero browser dialogs.
New component: DocumentDropZone
- Drag-and-drop with visual feedback (border color change on dragover)
- Click-to-browse fallback
- Inline type selector (advisory, email, screenshot, patch, other)
- Inline notes field
- Cancel button to dismiss without uploading
- Shows filename and size before upload
- Uses toast notifications for success/error
Removed from CVECard:
- handleFileUpload function (createElement('input') + prompt() pattern)
- uploadingFile state variable
- Upload (lucide) icon import
New features:
- Recent Activity feed widget shows last 8 actions from audit log
with relative timestamps, auto-refreshes every 60s
- Right sidebar reorganized: Calendar + Activity always visible,
Tickets/Archer/Ivanti behind tab switcher to eliminate dead space
Backend:
- New GET /api/recent-activity endpoint (any authenticated user)
Returns last N audit entries excluding login/logout noise
Lighter than the full admin audit-logs endpoint
Frontend:
- RecentActivityFeed component with action labels, colored dots,
timeAgo formatting, and manual refresh button
- SidebarTabs component with Tickets/Archer/Ivanti tabs
- OpenTicketsPanel and IvantiWorkflowPanel support embedded prop
to render without their own panel wrapper when inside tabs
Layout change:
Before: Calendar | Tickets | Archer | Ivanti (4 stacked panels)
After: Calendar | Activity | [Tickets | Archer | Ivanti] (tabs)
This keeps the sidebar height proportional to the CVE list area
instead of extending far below the main content.
Major restructuring of the monolithic App.js (2484 lines) into focused,
testable components:
Architecture:
- App.js is now a 189-line routing shell (header, nav, page switching)
- HomePage.js orchestrates all home page state and layout
- Each visual section is its own component with clear props API
Extracted components:
- StatsBar: clickable stat cards that filter by severity
- QuickCVELookup: CVE existence check with inline results
- CVEFilters: search + vendor/severity dropdowns
- CVECard: expandable CVE with vendor entries, docs, tickets
- OpenTicketsPanel: right sidebar open JIRA tickets
- IvantiWorkflowPanel: right sidebar Ivanti workflow status + archive
Extracted modals:
- AddCVEModal: self-contained add form with NVD auto-fill
- EditCVEModal: self-contained edit form with NVD update
- JiraTicketModal: unified add/edit JIRA ticket modal
- ArcherTicketModal: unified add/edit Archer ticket modal
Performance optimizations:
- Debounced search (300ms) via useDebounce hook — eliminates
redundant API calls on every keystroke
- Memoized groupedCVEs, openTicketCount, criticalCount via useMemo
- Proper state updates (no direct mutation of cveDocuments)
- useCallback on fetch functions to stabilize effect dependencies
UX improvements:
- Toast notification system replaces all alert() calls
- Stat cards are now clickable to filter CVE list by severity
- onKeyDown replaces deprecated onKeyPress
- aria-labels added to interactive elements
Infrastructure:
- ToastContext with auto-dismiss, typed toasts (success/error/warning/info)
- useDebounce custom hook for reuse across the app
- Toast slide-in animation in App.css
Add position: sticky, top: 0, zIndex: 20 to BulkHideToolbar so the
Atlas Action Plan button remains visible while scrolling through findings.
Matches the existing sticky behavior of SelectionToolbar.
- Add checkbox selection + Granite Loader button to compliance device table
- Integrate LoaderModal for generating loader sheets from compliance devices
- Add direct IP resolve path (resolveAssetId + searchByAssetId) for CARD
enrichment on compliance devices without Ivanti host IDs
- Add searchByAssetId helper for full enriched record via asset-search endpoint
- Include NTS-AEO-ACCESS-OPS in default enrich-batch team search
- Increase CARD quick-mode timeout from 15s to 30s
- Add timeout vs not-found distinction in enrichment error reporting
- Fix LoaderModal enriching state not resetting on modal reopen
- Add pagination to compliance device table (25/50/100/200 per page)
- Page resets on team, tab, filter, or search change
- Replace all STEAM branding with AEGIS (Advanced Engineering Group
Intelligence System) across login, header, nav drawer, manifest, and
browser title
- Add shield logo to login page, main header, and nav drawer
- Fix BU drift checker recording incorrect previous_bu values by
building a previousBuMap snapshot BEFORE the upsert/delete cycle
instead of querying the DB after rows are already gone
- Clean 526 bogus BU history entries generated by the broken logic
- Add docs and scripts from prior session
Add requireGroup('Admin', 'Leadership') as router-level middleware on all
VCL multi-vertical routes. Hide the CCP Metrics nav item from users not in
those groups and guard the page render in App.js with a redirect fallback.
remark-gfm@3.0.1 uses the unified v10 ecosystem but react-markdown@10
requires the unified v11 ecosystem. This caused 'this.getRole is not a
function' at runtime, blanking the KB viewer. Upgraded to remark-gfm@4
which is compatible.
Tables are a GitHub Flavored Markdown extension not supported by
react-markdown's default parser. Added remark-gfm plugin so tables,
strikethrough, and task lists render correctly in KB articles.
The drift checker runs well before computeAnomalySummary writes to the
anomaly log (20+ minute gap in some syncs). The 10-minute window was
too narrow to capture the BU history records written during drift
checking. Widened to 60 minutes to reliably catch all records from
the same sync cycle.
The drift checker now inserts into ivanti_finding_bu_history when it
classifies archived findings as bu_reassignment. Previously only the
inline per-finding BU comparison (for findings still in sync) wrote
history records — archived findings that moved BU were counted in the
anomaly summary but had no detail records for the banner to display.
Also captures title and hostName from the Ivanti API response in the
drift checker for richer detail display, and adjusts the banner's
time window to 10 minutes before sync_timestamp to catch records
written during the drift check phase.
Three changes to the Jira Tickets page:
1. CVE ID and Vendor fields are now editable in the Edit Ticket modal
(previously disabled when editing). Backend PUT endpoint validates
CVE format and vendor length on update.
2. Completed tickets (Closed, Done, Resolved, etc.) are shown in a
separate collapsible section below the active tickets table. This
keeps the active work front-and-center.
3. Sync All skips completed tickets on subsequent syncs. When a ticket
first reaches a completed status via sync it gets updated normally,
but on future syncs it won't be included in the batch query to Jira.
Response now includes skippedCompleted count.
Atlas sync now distinguishes between hosts Atlas actively tracks (returned
plans, active or inactive) vs hosts with empty responses (not in Atlas).
Only atlas_known hosts show the badge — ACCESS-OPS hosts not covered by
Atlas won't show the amber '0' warning badge anymore.
Changes:
- Migration adds atlas_known BOOLEAN column to atlas_action_plans_cache
- Sync sets atlas_known = true only when Atlas returns at least one plan
- Metrics endpoint only counts atlas_known hosts in its aggregation
- Status endpoint includes atlas_known in response
- AtlasBadge renders nothing when atlas_known = false
- Bulk-create and refresh-cache upserts set atlas_known = true
- Backfill marks existing hosts with plans + managed BU hosts as known
Problem 1: Atlas sync was querying ALL host_ids from ivanti_findings
regardless of BU, writing 'no plan' entries for ACCESS-OPS hosts that
Atlas doesn't cover. Now the sync respects the user's active teams scope
(passed via query param) and falls back to IVANTI_MANAGED_BUS when no
scope is provided.
Problem 2: Atlas /metrics and /status endpoints returned unscoped data
from the full cache, so changing scope didn't update the Atlas Coverage
donut or badge counts. Both endpoints now accept a teams query param and
JOIN against ivanti_findings to scope results by BU.
Frontend changes:
- fetchAtlasStatus and fetchAtlasMetrics now pass teams param
- Atlas sync button passes active teams to the sync endpoint
- Scope change (adminScope) triggers Atlas data refresh
Also purged 6,461 polluted cache entries for non-managed BU hosts.
The AnomalyBanner BU reassignment row is now clickable, expanding to show
each affected finding with its host name and the team it moved from/to
(e.g. STEAM → PIES). The backend bu-changes endpoint now supports optional
since and limit query params to scope results to the relevant sync cycle.
Dropdown was opening automatically on render and not closing when clicking
elsewhere. Now opens only on focus/click, closes on blur, selection, Enter,
Escape, and Tab. Selected value persists in the input after selection.
Each user can now have ivanti_first_name and ivanti_last_name configured in
User Management. The workflow sync queries all configured Ivanti identities
and fetches workflows for each. The GET endpoint filters workflows to only
show those belonging to the logged-in user's Ivanti identity.
Users without an Ivanti identity see all workflows (admin fallback).
If no users have identities configured, falls back to IVANTI_FIRST_NAME/
IVANTI_LAST_NAME from .env for backward compatibility.
Changes:
- Migration adds ivanti_first_name, ivanti_last_name to users table
- Users route accepts and returns the new fields
- User Management UI has Ivanti Identity input fields
- Workflow sync iterates all configured user identities
- Workflow GET filters by logged-in user's identity
RESPONSIBLE_TEAM, EQUIP_STATUS, and EQUIPMENT_CLASS now show searchable
dropdown selectors in both the Bulk Defaults section and per-row inline
editing. Type to filter options, use arrow keys to navigate, Enter to select.
Picklist values extracted from docs/Team_Device Loader.xlsx reference sheets.
Per-row cells remain click-to-edit for all columns — picklist columns show
the SearchableSelect, free-text columns show a plain input.