Files
cve-dashboard/docs/bug-reports/ivanti-panel-bugs-2026-05-12.md

104 lines
6.1 KiB
Markdown

# Ivanti Workflows Panel Bug Fixes — 2026-05-12
## Summary
Multiple bugs in the Ivanti Workflows panel on the homepage caused incorrect data display, a React crash on archive card clicks, and missing BU scope filtering. These issues emerged after the PostgreSQL migration (timestamps changed format) and the multi-BU tenancy feature (archive panel was never wired to respect the admin scope toggle).
**Commit:** `8c93e86` on `master`
---
## Bug 1: "Synced Invalid Date"
**Symptom:** The Ivanti Workflows panel and Vulnerability Triage page both display "Synced Invalid Date" instead of the last sync timestamp.
**Cause:** The frontend date parsing code was written for SQLite's `datetime('now')` format (`2024-01-15 10:30:00`) and applied `.replace(' ', 'T') + 'Z'` to convert it to ISO. After the PostgreSQL migration, the `pg` driver returns timestamps as ISO strings already (`2026-05-12T16:29:39.154Z`). Appending a second `Z` produces `"2026-05-12T16:29:39.154ZZ"` which is unparseable.
**Fix:** Removed the `.replace(' ', 'T') + 'Z'` transformation. Now uses `new Date(syncedAt)` directly, which handles ISO strings correctly.
**Files changed:**
- `frontend/src/App.js` — home page sync display
- `frontend/src/components/pages/ReportingPage.js` — triage page sync display
---
## Bug 2: Incorrect Total Workflows Count
**Symptom:** The "Total Workflows" number displayed a value far higher than the actual number of workflows visible in the list (e.g., 6443 instead of 13).
**Cause:** The backend `readState()` function returned `row.total` from the database, which stored the Ivanti API's `page.totalElements` value. This represents the total matching the user filter across all pages on the Ivanti platform — not the count of workflows actually cached locally. The Ivanti API's total count appears unreliable (returns inflated numbers that don't match the actual page content).
**Fix:** Changed `readState()` to return `workflows.length` — the actual number of workflows in the cached JSON array.
**Files changed:**
- `backend/routes/ivantiWorkflows.js`
---
## Bug 3: React Crash on Archive Card Click (Blank Page)
**Symptom:** Clicking any state card (Active, Archived, Returned, Closed) in the ArchiveSummaryBar caused the entire page to go blank. No error message visible to the user.
**Cause:** PostgreSQL returns `numeric` columns as strings (e.g., `"9.38"` instead of `9.38`). The archive list rendering called `.toFixed(1)` directly on these string values. Strings do not have a `.toFixed()` method, causing a `TypeError` that crashed the React component tree (no ErrorBoundary exists to catch it).
**Fix:** Wrapped severity values in `Number()` before calling `.toFixed()`:
- `Number(a.last_severity).toFixed(1)` for archive severity
- `Number(a.related_active.severity).toFixed(1)` for related finding severity
**Files changed:**
- `frontend/src/App.js`
---
## Bug 4: Archive Panel Not Respecting BU Scope
**Symptom:** The ArchiveSummaryBar stat cards (Active, Archived, Returned, Closed) always showed counts for ALL business units, even when the admin scope toggle was set to "My Teams" (STEAM, ACCESS-ENG). Clicking a card also showed findings from all BUs.
**Cause:** The archive endpoints (`/api/ivanti/archive/stats` and `/api/ivanti/archive`) had no `teams` query parameter support. The `ArchiveSummaryBar` component fetched stats without passing the active scope. The `handleArchiveStateClick` function in `App.js` fetched the archive list without a teams filter. This was a gap from the multi-BU tenancy implementation — the archive panel was never wired to respect the admin scope.
**Fix:**
- Backend: Added `teams` query parameter to both `/api/ivanti/archive/stats` and `/api/ivanti/archive` endpoints. When provided, the endpoints JOIN with `ivanti_findings` on `finding_id` to filter by `bu_ownership ILIKE ANY(patterns)`.
- Frontend: `ArchiveSummaryBar` now accepts a `teamsParam` prop and passes it to the stats fetch. `App.js` passes `getActiveTeamsParam()` to the component and to `handleArchiveStateClick`.
- The stats and list re-fetch automatically when the admin scope toggle changes.
**Files changed:**
- `backend/routes/ivantiArchive.js`
- `frontend/src/components/pages/ArchiveSummaryBar.js`
- `frontend/src/App.js`
---
## Bug 5: ACTIVE State Click Returns Empty
**Symptom:** Clicking the "Active" stat card showed "No active findings" even though the count displayed a non-zero number.
**Cause:** The stats endpoint counted ACTIVE findings from `ivanti_findings WHERE state = 'open'` (correct), but the list endpoint queried `ivanti_finding_archives WHERE current_state = 'ACTIVE'` — which has zero records. Active findings are not archived; they live in `ivanti_findings`. The archive table only contains ARCHIVED, RETURNED, CLOSED, and CLOSED_GONE states.
**Fix:** When `state=ACTIVE` is requested, the list endpoint now queries `ivanti_findings` directly instead of `ivanti_finding_archives`. Results are formatted to match the archive card shape (finding_id, finding_title, host_name, ip_address, last_severity, etc.).
**Files changed:**
- `backend/routes/ivantiArchive.js`
---
## Bug 6: CLOSED_GONE Records Not Counted
**Symptom:** The "Closed" stat card showed a lower count than expected. 63 findings with `current_state = 'CLOSED_GONE'` were not included in the Closed count or shown when clicking the Closed card.
**Cause:** The stats endpoint only counted records matching the four known states (ACTIVE, ARCHIVED, RETURNED, CLOSED). The `CLOSED_GONE` state — added during the findings count investigation on 2026-04-24 — was not mapped to any display category.
**Fix:** `CLOSED_GONE` records are now rolled into the CLOSED count in stats, and clicking the Closed card queries for both `CLOSED` and `CLOSED_GONE` states.
**Files changed:**
- `backend/routes/ivantiArchive.js`
---
## Verification
- Frontend build passes without errors
- Backend syntax validated with `node -c`
- Archive stats with teams filter returns scoped counts (64 active for STEAM+ACCESS-ENG vs 6545 unfiltered)
- `new Date(syncedAt).toLocaleString()` produces valid date strings
- `Number("9.38").toFixed(1)` returns `"9.4"` without throwing