258 Commits

Author SHA1 Message Date
Jordan Ramos
416bfd2e28 Add Infoblox DNS lookup, Scan Posture page, and Ivanti OS fields
- 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
2026-08-21 10:33:46 -06:00
Jordan Ramos
7484e81b9a Add interactive report viewer to Exports page
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)
2026-08-18 14:58:27 -06:00
Jordan Ramos
a178e5e772 Make legacy Jira migrations skip gracefully when tickets table exists
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'.
2026-08-18 14:26:05 -06:00
Jordan Ramos
04d96730e2 Fix migration: add OVERRIDING SYSTEM VALUE and reset sequence before Archer insert
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.
2026-08-18 14:15:26 -06:00
Jordan Ramos
5ef535426d Unify Archer and Jira tickets into single tickets table
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.
2026-08-18 14:04:27 -06:00
Jordan Ramos
9d1d4cdeb5 Fix scan_type migration: add process.exit() for standalone execution
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).
2026-08-18 11:25:16 -06:00
Jordan Ramos
d3adc8e1fc Add scan type indicator (agent vs network appliance) to Ivanti findings
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
2026-08-18 10:54:23 -06:00
Jordan Ramos
3aa7a6e49e Surface NetBox IPAM records when IP has no assigned device
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.
2026-08-10 15:40:53 -06:00
Jordan Ramos
bd5eb2b47a Add connection retry to CARD API for DNS round-robin dead nodes
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.
2026-08-10 14:32:23 -06:00
Jordan Ramos
b146bbb0fb Update summary query mock to match combined vertical IS NULL OR NTS_AEO query
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.
2026-08-10 14:00:48 -06:00
Jordan Ramos
d0f6460c0a Fix compliance upload: case-insensitive column matching, summary query, page persistence
- 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
2026-08-10 13:55:10 -06:00
Jordan Ramos
70a718a8df Fix compliance reconcile not removing stale core columns
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.
2026-08-10 10:51:11 -06:00
Jordan Ramos
405f63cbfc Fix TLS for GitLab feedback integration after v19 upgrade
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
2026-08-10 09:41:40 -06:00
Jordan Ramos
eb0d55a35b Add NetBoxBadge to reporting page, CARD hostname extraction, docs
- NetBoxBadge inline pill on ReportingPage host column
- CARD owner endpoint now returns hostname from card_flags/ivanti_assets
- card-to-granite-field-mapping reference doc
- Supplemental workbook sample in docs
- netbox-dry-run diagnostic script
2026-08-03 11:57:35 -06:00
Jordan Ramos
e3dce7dbbc Add Granite supplemental workbook ingest with reconciliation
Separate upload path for the NTS_AEO_(supp only) workbook that tracks
Granite CMDB hygiene findings (Missing OS, Missing App ID, Missing Device
Function, Retired App ID) with weekly history and team scoping.

Backend:
- Python parser for supplemental xlsx (4 recognized sheets)
- supplemental_uploads and supplemental_items tables with per-sheet scoped resolution
- supplemental_notes table for per-device notes
- resolution_date and remediation_plan on supplemental items
- Full route module: preview, commit, summary, items, trends, uploads, rollback,
  device detail, metadata PATCH, notes CRUD

Frontend:
- SupplementalUploadModal with per-sheet diff preview
- GraniteHygieneSection on CompliancePage with cards, device drill-down, pagination
- SupplementalDetailPanel slide-out with findings, metadata editing, and notes
- Upload Supplemental button in CompliancePage header
2026-08-03 11:54:12 -06:00
Jordan Ramos
121d044fb6 Add NetBox device inventory integration framework
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
2026-07-06 15:19:33 -06:00
Jordan Ramos
e8a5bdc196 Fix EQUIP_INST_ID extraction fallbacks and minor cleanups
- Add additional fallback sources for EQUIP_INST_ID in CARD extractGraniteFields
  (card_flags, ivanti_assets, top-level asset fields)
- Add CARD_DEBUG env var gate for troubleshooting extraction logic
- Clarify anomaly/latest endpoint comment (only significant rows returned)
- Remove stale convention comment in LoaderModal.js
2026-07-06 15:18:56 -06:00
Jordan Ramos
92096f2b66 Auto-convert .msg attachments to PDF for Ivanti FP workflow
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)
2026-06-30 15:27:29 -06:00
Jordan Ramos
5037d68613 Fix anomaly banner BU reassignment detail display
- 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)
2026-06-29 12:01:03 -06:00
Jordan Ramos
61e4cc5f32 Add dual-port TLS support for FQDN access
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.
2026-06-29 11:27:46 -06:00
Jordan Ramos
93de7bbca7 Add SVP org hierarchy discovery to BU Lookup panel
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
2026-06-26 09:33:45 -06:00
Jordan Ramos
58996cf4cf Add BU Lookup tool to Admin panel
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
2026-06-26 09:20:53 -06:00
Jordan Ramos
5d3d4b1eab Allow Admin scope toggle to filter data via ?teams= param
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.
2026-06-24 17:04:06 -06:00
Jordan Ramos
221eb6a1a1 Hide admin-only actions from non-Admin activity feed
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.
2026-06-24 17:01:49 -06:00
Jordan Ramos
e34f9e567c Extend team enforcement to Atlas and Archive routes, update schema reference
- 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.
2026-06-24 13:41:16 -06:00
Jordan Ramos
0e17318cba Hide impersonation events from non-Admin activity feed
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.
2026-06-24 13:01:15 -06:00
Jordan Ramos
8c789ce765 Add View As (impersonation) feature for Admin users
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
2026-06-24 12:57:57 -06:00
Jordan Ramos
11d9fec3ec Add page visibility by group with centralized matrix
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
2026-06-24 11:41:50 -06:00
Jordan Ramos
a003091b6a Add backend team enforcement via requireTeam() middleware
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
2026-06-24 11:36:25 -06:00
Jordan Ramos
f119cca1d7 Add recent activity feed and tabbed sidebar layout
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.
2026-06-23 12:16:40 -06:00
Jordan Ramos
55795710d9 Add TLS/HTTPS support with auto-detection
- 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
2026-06-19 14:44:04 -06:00
Jordan Ramos
e9d6038636 Add Granite Loader to AEO Compliance page with CARD enrichment and pagination
- 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
2026-06-19 13:49:26 -06:00
Jordan Ramos
c7274be66d Bump compliance upload limit to 100MB
NTS_AVVOC vertical xlsx is 72MB — 50MB was still too low.
2026-06-18 08:45:01 -06:00
Jordan Ramos
ba6e67c639 Increase compliance upload limit to 50MB
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.
2026-06-18 08:38:57 -06:00
Jordan Ramos
f257cfad88 Skip BU history entries when previous_bu is unknown
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
2026-06-17 14:58:01 -06:00
Jordan Ramos
a95fd03f5e Rebrand STEAM → AEGIS, fix BU drift checker previous_bu bug
- 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
2026-06-17 14:40:38 -06:00
Jordan Ramos
479c61b88f Restrict VCL/CCP Metrics page to Admin and Leadership groups
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.
2026-06-17 09:27:01 -06:00
Jordan Ramos
28714eed47 Cache plan IDs from Atlas create responses
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.
2026-06-16 16:10:54 -06:00
Jordan Ramos
c0e3139503 Fix atlas_known — parse response body to detect 'not found' hosts
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).
2026-06-16 15:45:43 -06:00
Jordan Ramos
09db1c2ae9 Fix atlas_known — managed BU hosts always show badge regardless of 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.
2026-06-16 15:40:51 -06:00
Jordan Ramos
93efb70d1c Fix KB content/download failing for relative file paths
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.
2026-06-16 13:23:20 -06:00
Jordan Ramos
a8877728e0 Fix drift checker re-classifying same archived findings every sync
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.
2026-06-16 13:13:15 -06:00
Jordan Ramos
a2234ccc1a Write BU history records from drift checker for anomaly banner detail view
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.
2026-06-15 09:29:46 -06:00
Jordan Ramos
e45e40d617 Allow CVE/Vendor editing and separate completed Jira tickets
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.
2026-06-12 15:23:29 -06:00
Jordan Ramos
150a534943 Add atlas_known distinction to prevent badge noise for untracked hosts
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
2026-06-12 13:25:00 -06:00
Jordan Ramos
5105ee2ff8 Scope Atlas sync and metrics to active BU teams
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.
2026-06-12 12:38:45 -06:00
Jordan Ramos
356ce23462 Add BU reassignment from/to detail view in anomaly banner
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.
2026-06-12 12:12:59 -06:00
Jordan Ramos
0f83f48cc6 Per-user Ivanti identity for FP workflow filtering
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
2026-06-10 11:22:51 -06:00
Jordan Ramos
032a8df403 Merge update_token from getOwner when asset-search omits it
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.
2026-06-09 14:23:56 -06:00
Jordan Ramos
32ed65eb79 Fix owner-lookup hostId fast path — use asset-search owner data directly
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.
2026-06-09 14:03:31 -06:00