- 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)
After the unify_tickets_table migration runs, jira_tickets is renamed to
jira_tickets_legacy. The four earlier Jira migrations now detect the unified
tickets table and exit cleanly instead of failing with 'table does not exist'.
The Jira tickets preserve their original IDs via OVERRIDING SYSTEM VALUE,
then the sequence is reset to max(id) before Archer rows auto-increment.
Without this, Archer inserts collide with Jira IDs on the primary key.
Add unified tickets table with ticket_type discriminator column ('archer'|'jira').
Migration preserves Jira ticket IDs for FK integrity, updates junction table FK,
and renames old tables to *_legacy for rollback path.
New /api/tickets router provides full CRUD with type-aware validation, plus all
Jira integration endpoints (lookup, sync, create-in-jira). Old routes
(/api/jira-tickets, /api/archer-tickets) refactored as backward-compatible
proxies querying the unified table.
Updated ivantiTodoQueue ticket-links JOIN and server.js CVE cascade queries
to reference the new tickets table.
run-all.js executes each migration as a child process via node. The migration
must call process.exit() or the pool keeps it alive and the runner times out
(exit code null).
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.
nidl.charter.com resolves to multiple A records via round-robin but not all
are reachable from this network. Both acquireToken() and cardRequest() now
retry up to 2 times on ETIMEDOUT/ECONNREFUSED/ECONNRESET before failing.
Also prunes obsolete metric categories from compliance_config.json and fixes
a comment referencing the correct CARD API hostname.
The /summary endpoint now uses a single query instead of two-step fallback.
Update the test mock to match the new SQL pattern while keeping legacy
patterns as fallback for compatibility.
- 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
The reconcileConfig() function used string matching 'is missing core column'
to identify core column findings, but compareSchemaToDrift() produces messages
like 'Core column "X" is missing from all N detail sheet(s)'. The mismatch
meant reconciliation never touched core_cols — stale columns persisted and
blocked uploads indefinitely.
Additionally, the old logic counted findings per column (always 1 since the
detection aggregates) and compared against detailSheetCount. Since the finding
already confirms the column is missing from ALL sheets, the count check was
redundant and always failed.
Fix: match on the actual message format and trust the detection's all-sheets
assertion directly.
The feedback route had a typo: rejectAuthorized (no-op) instead of
rejectUnauthorized. With GitLab now on HTTPS with a self-signed cert,
this caused issue creation and screenshot uploads to fail.
- Fix rejectAuthorized → rejectUnauthorized in both request blocks
- Update GITLAB_URL default from http:// to https:// in .env.example,
configure.js, README, and reference manual
Adds API helper and route module for NetBox DCIM integration as a future
replacement for Granite inventory management.
Helper (helpers/netboxApi.js):
- Token-based auth (Authorization: Token <token>)
- Device CRUD: list, get, create, update, delete
- IP cross-reference: find device by IP via IPAM
- Reference data: sites, device types, device roles
- Connection test, TLS skip support
Routes (routes/netbox.js) mounted at /api/netbox:
- GET /status — config and connectivity check
- GET /devices — list with filters and pagination
- GET /devices/search — general search
- GET /devices/by-ip/:ip — cross-reference IP to device
- GET /devices/:id — single device detail
- POST /devices — create (Admin, Standard_User)
- PATCH /devices/:id — partial update
- DELETE /devices/:id — delete (Admin only)
- GET /sites, /device-types, /device-roles — reference data
Integration is optional — gracefully returns 503 if env vars are unset.
New env vars: NETBOX_API_URL, NETBOX_API_TOKEN, NETBOX_SKIP_TLS
Ivanti rejects .msg (Outlook email) file uploads. Now when a user
attaches a .msg file, the backend:
1. Accepts the upload (added .msg to ALLOWED_EXTENSIONS)
2. Converts it to PDF via Python (extract-msg + reportlab)
3. Sends the PDF to Ivanti instead
The PDF preserves subject, sender, date, recipients, and body.
Conversion applies to both initial FP submission and add-attachments
resubmit endpoint. Returns 422 if conversion fails.
Dependencies: extract-msg, reportlab (pip3 install)
- 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)
When ALSO_LISTEN_PORT is set (e.g., 443), the server binds to both
the primary PORT (3001) and the additional port using the same TLS
certs. This enables clean FQDN access (https://aegis-uat.charterlab.com)
on port 443 while keeping the existing IP:3001 URL working as a
fallback during the DNS transition period.
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
requireTeam() now respects an optional ?teams= query param from Admin
users as a voluntary scope filter. When the Admin Scope Toggle is set
to 'My Teams', the frontend sends ?teams=STEAM,ACCESS-ENG and the
backend applies the filter. When set to 'All BUs' (no param), Admin
gets the full unfiltered view.
Non-admin users continue to be enforced by their bu_teams assignment
regardless of any query param.
Non-Admin users should not see user management events (create, delete,
group changes, password resets), impersonation events, or admin-only
compliance operations (config reconcile, upload rollback) in the
Recent Activity panel.
- Atlas: add requireTeam() at router level; replace client ?teams= param
parsing with req.teamScope in /metrics, /status, and /sync endpoints
- Archive: add requireTeam() at router level; replace client ?teams= param
parsing with req.teamScope in GET / and GET /stats endpoints
- db-schema.sql: add impersonate_user_id column to sessions table reference
The frontend still sends ?teams= as a query param to these endpoints
(harmless no-op since backend ignores it). Frontend cleanup deferred
to avoid churn in the 7000-line ReportingPage component.
Non-Admin users should not see impersonate_start/impersonate_stop
entries in the recent activity feed. The feed now filters these
actions for non-Admin groups alongside the existing login/logout
exclusions.
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
Introduce server-side team-scoped data access enforcement:
- Add TEAM_TO_IVANTI/IVANTI_TO_TEAM mapping to helpers/teams.js
- Add requireTeam() middleware to middleware/auth.js
- Admin bypass (req.teamScope = null)
- 403 for users with no team assignment
- Populates req.teamScope with short and ivanti name arrays
- Ivanti findings: replace client ?teams= param with req.teamScope filtering
on GET /, /counts, /counts/history, /fp-workflow-counts, POST /sync
- Override and note endpoints verify finding is in team scope
- Compliance: add requireTeam() router-level, validate ?team= param against scope
on GET /items and GET /summary
- CARD: validate teamName param on GET /teams/:teamName/assets
- Todo queue: verify findings belong to user's teams on POST /batch
- Clarify IVANTI_BU_FILTER comment (sync-level vs query-time filtering)
- Update 14 test files to include requireTeam in auth middleware mocks
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.
- Server auto-detects cert/key in backend/certs/ and starts HTTPS
- Falls back to plain HTTP if no certs found or TLS_ENABLED=false
- Self-signed cert generated for dev (365-day, gitignored)
- Added TLS env vars to .env.example
- Frontend rebuilt with https:// API URLs for dev server
- 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
SDIT_CSD xlsx files exceed the 10MB general upload limit. Add a
separate multer instance (complianceUpload, 50MB) for the compliance
and VCL multi-vertical routes while keeping the 10MB cap for general
document/KB uploads.
Only record BU reassignment in ivanti_finding_bu_history when the
previous_bu is a known managed BU (from EXPECTED_BUS). Findings that
were never in our sync cache show as UNKNOWN which provides no
actionable insight for asset movement tracking.
Closes#28
- 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.
Single-host PUT and bulk POST now extract and store the action_plan_id
from the Atlas API response in the local cache. Previously only a stub
with plan_type/commit_date was now the actual plan ID iscached
included so it can be referenced for updates/display without re-fetching
from Atlas.
Instead of blanket-marking managed BU hosts, now parses the Atlas API
response: if it returns a valid {active, inactive} structure, the host
is known. If it returns an error or 'not found' message (even with a
2xx status), the host is not known and won't show a badge.
This prevents the shield showing on hosts Atlas doesn't actually track,
while correctly showing it on hosts Atlas recognizes (with or without
plans).
A STEAM/ACCESS-ENG host with zero Atlas plans but tracked in Atlas
(like olt01k7) wasn't showing the amber shield because atlas_known
was only true when plans existed. Now managed BU hosts always get
atlas_known=true so the '0 plans' warning badge renders. Non-managed
BU hosts only show badge if Atlas actually has plan data for them.
res.sendFile requires an absolute path. Article #7 was stored with a
relative path which caused the TypeError. Now both the content and
download endpoints resolve relative paths against the backend directory
before calling existsSync and sendFile.
Root cause: archived findings were never removed from ivanti_findings
(state='open'), so they appeared in previousFindings every sync, got
flagged as 'disappeared' every time, and were re-classified by the
drift checker — inflating the BU reassignment count to ~220/sync
when only a handful of genuinely new reassignments existed.
Fixes:
1. Filter out already-archived findings (archived >2h ago) before
passing to the drift checker — only genuinely new archives get
classified.
2. Delete disappeared findings from ivanti_findings after archive
detection so they don't pollute future syncs.
3. Cleaned up 536 stale findings that were accumulated in the table.
The archive activity bar chart should drop from ~500 to near-zero
on the next sync, with only genuinely new disappearances showing.
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.
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
When the hostId fast path resolves via asset-search but the response lacks
an update_token, do a follow-up getOwner() call using the resolved _id to
fetch the token. Returns the rich owner data from asset-search merged with
the update_token from the owner endpoint.
The asset-search response wraps in { assets: [...] } and includes the full
owner record. Previously we tried to extract just an _id from the top level
(which didn't exist) and then made a separate getOwner() call that returned
empty data for IPv6 assets.
Now when hostId resolves via asset-search, we return the owner data directly
from the search response — no second API call needed. This fixes the tooltip
showing empty confirmed/unconfirmed for IPv6-only findings.