diff --git a/.kiro/specs/batch-finding-disposition/.config.kiro b/.kiro/specs/batch-finding-disposition/.config.kiro new file mode 100644 index 0000000..6291b54 --- /dev/null +++ b/.kiro/specs/batch-finding-disposition/.config.kiro @@ -0,0 +1 @@ +{"specId": "9f5c16d4-43ea-4d7a-beb1-9329d79a5acc", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/batch-finding-disposition/design.md b/.kiro/specs/batch-finding-disposition/design.md new file mode 100644 index 0000000..b3201e1 --- /dev/null +++ b/.kiro/specs/batch-finding-disposition/design.md @@ -0,0 +1,331 @@ +# Design Document: Batch Finding Disposition + +## Overview + +This feature adds multi-select capability to the Vulnerability Triage page's findings table, enabling engineers to select multiple findings and add them all to the Ivanti Queue in a single operation. The current flow requires clicking each finding individually, configuring a popover, and submitting one at a time — this design replaces that with a batch selection toolbar and a bulk-add API endpoint while preserving the existing single-select popover for one-off additions. + +The design touches three layers: +1. A new `POST /api/ivanti/todo-queue/batch` backend endpoint that accepts an array of findings in a single transactional insert +2. Frontend multi-select state management (selection set, shift-click range select, select-all) +3. A sticky Selection Toolbar component with workflow type toggles, vendor input, and batch submit + +## Architecture + +The feature extends the existing Ivanti Queue subsystem without introducing new services or tables. The `ivanti_todo_queue` table schema is unchanged — batch add simply inserts multiple rows in a single SQLite transaction. + +```mermaid +flowchart TD + subgraph Frontend ["Frontend (ReportingPage.js)"] + CB[Row Checkboxes] --> SS[Selection State
Set of finding IDs] + SS --> ST[Selection Toolbar] + ST -->|"Add to Queue"| BA[Batch API Call] + CB -->|"No selection + click"| PO[AddToQueuePopover
existing single-add] + end + + subgraph Backend ["Backend (ivantiTodoQueue.js)"] + BA -->|"POST /batch"| BH[Batch Handler] + BH -->|"BEGIN TRANSACTION"| DB[(ivanti_todo_queue)] + BH -->|"logAudit()"| AL[(audit_logs)] + PO -->|"POST /"| SH[Single Handler
existing] + SH --> DB + end +``` + +### Key Design Decisions + +1. **No new database table or migration** — batch insert reuses the existing `ivanti_todo_queue` schema. Each finding becomes its own row, identical to what the single-add endpoint creates. + +2. **SQLite transaction for atomicity** — all findings in a batch are inserted inside `db.serialize()` with `BEGIN TRANSACTION` / `COMMIT`. If any insert fails, the entire batch is rolled back. This satisfies the all-or-nothing requirement (Req 3.7, 3.8, 3.11). + +3. **Selection state lives in the VulnerabilityTriagePage component** — a `Set` of finding IDs managed via `useState`. This keeps the selection co-located with the existing `findings`, `sorted`, `filtered`, and `queueItems` state. No new context or global store needed. + +4. **Dual-mode checkbox behavior** — when no findings are selected, clicking a checkbox opens the existing `AddToQueuePopover` (preserving the single-select flow per Req 5). Once one or more findings are selected, subsequent checkbox clicks toggle selection instead. This is the simplest UX that satisfies both Req 1 and Req 5. + +5. **Selection Toolbar as inline sticky bar** — rendered between the table header controls and the `` element, using `position: sticky` to stay visible during scroll. This avoids portal complexity and keeps the toolbar visually anchored to the table. + +6. **200-item batch limit** — prevents oversized payloads and keeps SQLite transaction time reasonable. The findings table typically has 200-800 rows, so this covers most realistic batch sizes. + +## Components and Interfaces + +### Backend + +#### `POST /api/ivanti/todo-queue/batch` + +Added to the existing `createIvantiTodoQueueRouter` factory in `backend/routes/ivantiTodoQueue.js`. + +**Request body:** +```json +{ + "findings": [ + { + "finding_id": "FID-12345", + "finding_title": "OpenSSL vulnerability", + "cves": ["CVE-2024-0001"], + "ip_address": "10.0.1.50" + } + ], + "workflow_type": "FP", + "vendor": "Juniper" +} +``` + +**Validation rules:** +- `findings` — array, 1–200 items +- Each item: `finding_id` required, non-empty string; `finding_title`, `cves`, `ip_address` optional +- `workflow_type` — must be `FP`, `Archer`, or `CARD` +- `vendor` — required non-empty string (≤200 chars) for FP/Archer; ignored for CARD +- If any finding fails validation, reject entire batch with 400 + +**Auth:** `requireAuth(db)`, `requireGroup('Admin', 'Standard_User')` + +**Response (201):** +```json +{ + "items": [ + { + "id": 42, + "user_id": 1, + "finding_id": "FID-12345", + "finding_title": "OpenSSL vulnerability", + "cves_json": "[\"CVE-2024-0001\"]", + "ip_address": "10.0.1.50", + "vendor": "Juniper", + "workflow_type": "FP", + "status": "pending", + "created_at": "2025-01-15 12:00:00", + "updated_at": "2025-01-15 12:00:00", + "cves": ["CVE-2024-0001"] + } + ] +} +``` + +**Error responses:** +- `400` — validation failure (descriptive message) +- `401` — not authenticated +- `403` — insufficient permissions +- `500` — database transaction failure (all inserts rolled back) + +### Frontend + +#### Selection State (in VulnerabilityTriagePage) + +New state variables added to the main component: + +```javascript +const [selectedIds, setSelectedIds] = useState(new Set()); // Set of finding IDs +const [lastClickedId, setLastClickedId] = useState(null); // for shift-click range select +const [batchSubmitting, setBatchSubmitting] = useState(false); // loading state +const [batchError, setBatchError] = useState(null); // error message from failed batch +const [batchWorkflowType, setBatchWorkflowType] = useState('FP'); +const [batchVendor, setBatchVendor] = useState(''); +``` + +#### Checkbox Click Logic + +``` +onClick(finding, event): + if finding is already queued → return (no-op) + if selectedIds.size === 0 AND not shift-click: + → open AddToQueuePopover (existing single-select flow) + else: + if shift-click AND lastClickedId exists: + → range-select all visible findings between lastClickedId and finding.id + else: + → toggle finding.id in selectedIds + set lastClickedId = finding.id +``` + +#### SelectionToolbar Component + +Rendered inline above the table when `selectedIds.size > 0`. Contains: +- Selected count badge +- "Clear Selection" button +- Workflow type toggle buttons (FP / Archer / CARD) with existing color scheme +- Vendor text input (hidden when CARD selected) +- "Add to Queue" submit button (disabled until valid) +- Error message display area + +#### Selection Persistence Across Filters + +When `columnFilters`, `actionFilter`, or `excFilter` change, the selection set is pruned to only include IDs that remain in the `filtered` array. This is done via a `useEffect` that intersects `selectedIds` with the current filtered finding IDs. + +#### Select All / Deselect All + +The checkbox column header renders a "Select All" control when `selectedIds.size > 0` or as a standard header otherwise. Clicking it: +- If not all visible non-queued findings are selected → selects all visible non-queued findings +- If all are already selected → deselects all + +## Data Models + +### Database Schema (unchanged) + +The `ivanti_todo_queue` table is reused as-is: + +```sql +CREATE TABLE ivanti_todo_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + finding_id TEXT NOT NULL, + finding_title TEXT, + cves_json TEXT, + ip_address TEXT, + vendor TEXT NOT NULL, + workflow_type TEXT NOT NULL CHECK(workflow_type IN ('FP', 'Archer', 'CARD')), + status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'complete')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +``` + +Each batch-added finding creates one row, identical to single-add. The `vendor` and `workflow_type` are shared across all findings in a batch (set once in the toolbar). + +### API Request Schema + +``` +BatchAddRequest { + findings: Array<{ + finding_id: string (required, non-empty, trimmed) + finding_title: string | null (max 500 chars) + cves: string[] | null + ip_address: string | null (max 64 chars) + }> (1–200 items) + workflow_type: "FP" | "Archer" | "CARD" + vendor: string (required for FP/Archer, ≤200 chars; empty/absent for CARD) +} +``` + +### Frontend State Shape + +``` +Selection State: + selectedIds: Set — finding IDs currently selected + lastClickedId: string | null — last checkbox clicked (for shift-range) + batchSubmitting: boolean — true while POST /batch in flight + batchError: string | null — error message from last failed batch + batchWorkflowType: "FP" | "Archer" | "CARD" + batchVendor: string +``` + + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Selection pruning preserves only visible findings + +*For any* set of selected finding IDs and any set of currently visible (filtered) finding IDs, pruning the selection after a filter change should produce exactly the intersection of the two sets — every ID in the result is both selected and visible, and no visible selected ID is lost. + +**Validates: Requirements 1.4** + +### Property 2: Select-all produces the complete visible non-queued set + +*For any* list of visible findings and any set of queued finding IDs, the select-all operation should produce a set containing exactly the IDs of visible findings that are not in the queued set — no queued findings included, no non-queued visible findings omitted. + +**Validates: Requirements 1.6** + +### Property 3: Submit button enabled state matches validation rule + +*For any* workflow type (FP, Archer, CARD) and any vendor string, the "Add to Queue" button should be enabled if and only if the workflow type is CARD, or the vendor string trimmed is non-empty. No other combination should enable the button. + +**Validates: Requirements 2.7** + +### Property 4: Batch size validation accepts only 1–200 items + +*For any* integer N representing the number of findings in a batch request, the endpoint should accept the request (assuming all other fields are valid) if and only if 1 ≤ N ≤ 200. Arrays of size 0 or greater than 200 should be rejected with a 400 response. + +**Validates: Requirements 3.2** + +### Property 5: Vendor validation is conditional on workflow type + +*For any* workflow type and any vendor string, the batch endpoint should require a non-empty vendor of 200 characters or fewer when workflow_type is FP or Archer, and should accept any vendor value (including empty or absent) when workflow_type is CARD. + +**Validates: Requirements 3.5, 3.6** + +### Property 6: One invalid finding rejects the entire batch + +*For any* valid batch of findings, if exactly one finding is replaced with an invalid finding (empty finding_id, missing finding_id, or non-string finding_id) at any position in the array, the entire batch should be rejected with a 400 response and zero rows should be inserted. + +**Validates: Requirements 3.3, 3.8** + +### Property 7: Successful batch response matches request + +*For any* valid batch request of N findings, the 201 response should contain exactly N items, each with a unique numeric `id`, and the set of `finding_id` values in the response should equal the set of `finding_id` values in the request. + +**Validates: Requirements 3.9** + +### Property 8: Shift-click range select covers exactly the between range + +*For any* sorted list of visible findings, any last-clicked index, and any current-click index, the shift-click range select should produce a set containing exactly the non-queued findings between those two indices (inclusive), regardless of which index is larger. + +**Validates: Requirements 6.1** + +## Error Handling + +### Backend Errors + +| Scenario | Response | Behavior | +|----------|----------|----------| +| Empty findings array or > 200 items | 400 | `{ error: "findings array must contain 1-200 items." }` | +| Any finding missing/empty finding_id | 400 | `{ error: "Each finding must have a non-empty finding_id string." }` | +| Invalid workflow_type | 400 | `{ error: "workflow_type must be FP, Archer, or CARD." }` | +| Missing vendor for FP/Archer | 400 | `{ error: "vendor is required for FP and Archer workflows." }` | +| Vendor exceeds 200 chars | 400 | `{ error: "vendor must be under 200 chars." }` | +| Not authenticated | 401 | Standard auth middleware response | +| Insufficient permissions (Read_Only) | 403 | Standard group middleware response | +| SQLite transaction failure | 500 | Transaction rolled back, `{ error: "Internal server error." }` | + +### Frontend Errors + +| Scenario | Behavior | +|----------|----------| +| Batch POST returns 4xx/5xx | Display error message in Selection Toolbar, keep selection intact | +| Network failure during batch POST | Display "Network error — please try again" in toolbar, keep selection | +| Batch POST timeout | Same as network failure handling | + +### Edge Cases + +- **Duplicate finding_ids in batch**: Allowed — the same finding could appear on multiple hosts. The backend does not enforce uniqueness on finding_id within a batch. +- **Finding already in queue**: The frontend prevents selecting already-queued findings (checkbox is disabled), so duplicates should not reach the API. No server-side duplicate check is added to keep the endpoint simple. +- **Concurrent batch submissions**: The SQLite transaction serializes writes. If two users submit overlapping batches, both succeed independently (each user has their own queue scoped by user_id). +- **Selection of 0 findings**: The "Add to Queue" button is only rendered when selectedIds.size > 0, so this state cannot be reached through the UI. The backend still validates for it. + +## Testing Strategy + +### Unit Tests + +Focus on specific examples and edge cases: + +- **Backend validation**: Test each validation rule with concrete valid/invalid inputs (empty array, 201 items, missing finding_id, invalid workflow_type, vendor edge cases) +- **Transaction rollback**: Mock a database error mid-insert, verify no rows are committed +- **Frontend checkbox dual-mode**: Test that clicking with empty selection opens popover, clicking with existing selection toggles selection +- **Toolbar visibility**: Test toolbar appears/disappears based on selection state +- **Clear selection**: Test that clear button empties selection +- **Escape key**: Test that Escape clears selection +- **Select-all toggle**: Test select-all and deselect-all behavior +- **Queue panel update**: Test that successful batch updates queueItems state + +### Property-Based Tests + +Using [fast-check](https://github.com/dubzzz/fast-check) for JavaScript property-based testing. + +Each property test runs a minimum of 100 iterations with randomly generated inputs. Tests are tagged with their corresponding design property. + +| Property | What's Generated | What's Verified | +|----------|-----------------|-----------------| +| Property 1: Selection pruning | Random sets of selected IDs and filtered IDs | Result = intersection of both sets | +| Property 2: Select-all | Random finding lists and queued ID sets | Result = visible IDs minus queued IDs | +| Property 3: Submit enabled | Random workflow types and vendor strings | Enabled iff CARD or non-empty vendor | +| Property 4: Batch size | Random integers 0–300 | Accepted iff 1 ≤ N ≤ 200 | +| Property 5: Vendor validation | Random workflow types and vendor strings (0–300 chars) | Conditional acceptance rule | +| Property 6: Invalid finding rejection | Valid batches with one injected invalid item | Entire batch rejected, 0 rows inserted | +| Property 7: Response shape | Valid batches of 1–50 findings | Response count matches, IDs match | +| Property 8: Range select | Random sorted lists and two index positions | Correct range of non-queued findings | + +### Integration Tests + +- End-to-end batch submission: POST valid batch, verify rows in database, verify response shape +- Auth enforcement: Verify 401 for unauthenticated, 403 for Read_Only users +- Transaction atomicity: Verify rollback on database error +- Frontend → Backend: Mock API, verify correct request payload from toolbar submit diff --git a/.kiro/specs/batch-finding-disposition/requirements.md b/.kiro/specs/batch-finding-disposition/requirements.md new file mode 100644 index 0000000..e045b35 --- /dev/null +++ b/.kiro/specs/batch-finding-disposition/requirements.md @@ -0,0 +1,97 @@ +# Requirements Document + +## Introduction + +The Batch Finding Disposition feature adds multi-select capability to the Vulnerability Triage page's findings table, allowing engineers to select multiple findings at once and add them all to the Ivanti Queue with a shared workflow type and vendor in a single operation. Currently, each finding must be individually clicked, configured via a popover, and submitted — a repetitive process that slows down triage when working through many findings. This feature replaces that one-at-a-time flow with a batch selection toolbar and a bulk-add API endpoint. + +## Glossary + +- **Findings_Table**: The sortable, filterable table of Ivanti host findings rendered in the VulnerabilityTriagePage component (`ReportingPage.js`), where each row represents one finding. +- **Selection_Toolbar**: A floating toolbar that appears above the Findings_Table when one or more findings are selected via their row checkboxes, displaying the count of selected findings and batch action controls. +- **Batch_Add_Panel**: The inline panel within the Selection_Toolbar that provides workflow type selection (FP, Archer, CARD), an optional vendor input, and a submit button for adding all selected findings to the queue in one operation. +- **Todo_Queue_API**: The backend Express router at `/api/ivanti/todo-queue` that manages CRUD operations on the `ivanti_todo_queue` table. +- **Queue_Panel**: The existing right-side slide-out panel (`QueuePanel` component) that displays the user's current queue items grouped by vendor. +- **Workflow_Type**: One of three disposition categories: FP (false positive), Archer (risk acceptance), or CARD (remediation card). Each finding added to the queue is assigned exactly one Workflow_Type. +- **Finding**: A single Ivanti host vulnerability record containing an ID, title, CVEs, IP address, severity, and other metadata. + +## Requirements + +### Requirement 1: Multi-Select Findings via Row Checkboxes + +**User Story:** As an engineer, I want to select multiple findings using checkboxes so that I can batch-process them instead of handling each one individually. + +#### Acceptance Criteria + +1. THE Findings_Table SHALL render a checkbox in the first column of each finding row that is not already in the queue. +2. WHEN a user clicks a finding row's checkbox, THE Findings_Table SHALL toggle that Finding's selected state without opening the AddToQueuePopover. +3. WHEN one or more findings are selected, THE Findings_Table SHALL visually distinguish selected rows from unselected rows using a highlighted background. +4. THE Findings_Table SHALL maintain the selected findings set across sort and filter changes, removing only findings that are no longer visible after filtering. +5. WHEN a finding is already in the queue, THE Findings_Table SHALL display that row's checkbox as checked and disabled, preventing re-selection. +6. WHILE findings are selected, THE Findings_Table SHALL display a "Select All (visible)" control in the checkbox column header that selects all visible, non-queued findings. +7. WHEN the "Select All" control is clicked while all visible non-queued findings are already selected, THE Findings_Table SHALL deselect all findings. + +### Requirement 2: Selection Toolbar with Batch Actions + +**User Story:** As an engineer, I want a toolbar that appears when I have findings selected so that I can see how many are selected and take batch actions on them. + +#### Acceptance Criteria + +1. WHEN one or more findings are selected, THE Selection_Toolbar SHALL appear as a sticky bar above the Findings_Table header row. +2. THE Selection_Toolbar SHALL display the count of currently selected findings. +3. THE Selection_Toolbar SHALL provide a "Clear Selection" button that deselects all findings and hides the Selection_Toolbar. +4. THE Selection_Toolbar SHALL provide workflow type toggle buttons for FP, Archer, and CARD, matching the existing color scheme (FP: amber, Archer: blue, CARD: green). +5. WHEN the selected Workflow_Type is FP or Archer, THE Selection_Toolbar SHALL display a vendor text input field. +6. WHEN the selected Workflow_Type is CARD, THE Selection_Toolbar SHALL hide the vendor input field and display a "No vendor required" indicator. +7. THE Selection_Toolbar SHALL provide an "Add to Queue" submit button that is enabled only when a Workflow_Type is selected and vendor is provided (for FP/Archer) or Workflow_Type is CARD. +8. THE Selection_Toolbar SHALL follow the existing dark theme design system (monospace fonts, dark gradient backgrounds, accent-colored borders). + +### Requirement 3: Bulk Add to Queue API Endpoint + +**User Story:** As an engineer, I want the backend to accept multiple findings in a single request so that batch additions are processed efficiently. + +#### Acceptance Criteria + +1. THE Todo_Queue_API SHALL expose a `POST /api/ivanti/todo-queue/batch` endpoint that accepts an array of finding objects with a shared workflow_type and vendor. +2. THE Todo_Queue_API SHALL validate that the findings array contains between 1 and 200 items. +3. THE Todo_Queue_API SHALL validate that each finding object contains a non-empty finding_id string. +4. THE Todo_Queue_API SHALL validate that workflow_type is one of FP, Archer, or CARD. +5. WHEN workflow_type is FP or Archer, THE Todo_Queue_API SHALL validate that vendor is a non-empty string of 200 characters or fewer. +6. WHEN workflow_type is CARD, THE Todo_Queue_API SHALL accept an empty or absent vendor field. +7. THE Todo_Queue_API SHALL insert all valid findings into the `ivanti_todo_queue` table within a single database transaction. +8. IF any finding in the batch fails validation, THEN THE Todo_Queue_API SHALL reject the entire batch and return a 400 response with a descriptive error message. +9. THE Todo_Queue_API SHALL return a 201 response containing the array of newly created queue items with their assigned IDs. +10. THE Todo_Queue_API SHALL require authentication and the Admin or Standard_User group. +11. IF a database error occurs during the transaction, THEN THE Todo_Queue_API SHALL roll back all inserts and return a 500 response. + +### Requirement 4: Frontend Batch Submission Flow + +**User Story:** As an engineer, I want clicking "Add to Queue" on the toolbar to submit all selected findings at once so that I save time during triage. + +#### Acceptance Criteria + +1. WHEN the user clicks "Add to Queue" on the Selection_Toolbar, THE Findings_Table SHALL send a single POST request to `POST /api/ivanti/todo-queue/batch` containing all selected findings with the chosen workflow_type and vendor. +2. WHILE the batch request is in progress, THE Selection_Toolbar SHALL disable the "Add to Queue" button and display a loading indicator. +3. WHEN the batch request succeeds, THE Findings_Table SHALL add all returned queue items to the local queue state, clear the selection, and hide the Selection_Toolbar. +4. WHEN the batch request succeeds, THE Findings_Table SHALL update each newly queued finding's row checkbox to show the checked-and-disabled (already queued) state. +5. IF the batch request fails, THEN THE Selection_Toolbar SHALL display the error message returned by the API and keep the current selection intact. +6. WHEN the batch request succeeds and the Queue_Panel is open, THE Queue_Panel SHALL reflect the newly added items immediately without requiring a manual refresh. + +### Requirement 5: Preserve Single-Select Popover Flow + +**User Story:** As an engineer, I want to still be able to add a single finding to the queue quickly without going through the batch flow, so that simple one-off additions remain fast. + +#### Acceptance Criteria + +1. WHEN no findings are currently selected and a user clicks a finding row's checkbox, THE Findings_Table SHALL open the existing AddToQueuePopover for that single finding. +2. WHEN one or more findings are already selected and a user clicks another finding row's checkbox, THE Findings_Table SHALL add that finding to the selection set instead of opening the AddToQueuePopover. +3. THE AddToQueuePopover SHALL continue to use the existing single-item `POST /api/ivanti/todo-queue` endpoint for individual additions. + +### Requirement 6: Keyboard Accessibility for Multi-Select + +**User Story:** As an engineer, I want to use keyboard shortcuts to speed up multi-select so that I can triage even faster. + +#### Acceptance Criteria + +1. WHEN a user holds Shift and clicks a finding row's checkbox, THE Findings_Table SHALL select all visible findings between the last clicked checkbox and the current checkbox (range select). +2. THE Selection_Toolbar SHALL be navigable via keyboard Tab order, with all interactive elements (workflow buttons, vendor input, submit button) reachable by Tab key. +3. WHEN the Escape key is pressed while the Selection_Toolbar is visible, THE Findings_Table SHALL clear the selection and hide the Selection_Toolbar. diff --git a/.kiro/specs/batch-finding-disposition/tasks.md b/.kiro/specs/batch-finding-disposition/tasks.md new file mode 100644 index 0000000..60b6989 --- /dev/null +++ b/.kiro/specs/batch-finding-disposition/tasks.md @@ -0,0 +1,116 @@ +# Implementation Plan: Batch Finding Disposition + +## Overview + +Add multi-select capability to the Vulnerability Triage findings table with a batch-add-to-queue API endpoint. The backend gets a new `POST /api/ivanti/todo-queue/batch` route in `ivantiTodoQueue.js`. The frontend gets selection state, checkbox dual-mode logic, a SelectionToolbar component, shift-click range select, select-all, and Escape-to-clear — all within `ReportingPage.js`. + +## Tasks + +- [x] 1. Add `POST /api/ivanti/todo-queue/batch` endpoint + - [x] 1.1 Add batch route handler to `backend/routes/ivantiTodoQueue.js` + - Add `POST /batch` route inside `createIvantiTodoQueueRouter`, before the `POST /` route + - Apply `requireAuth(db)` and `requireGroup('Admin', 'Standard_User')` middleware + - Validate request body: `findings` array (1–200 items), each with non-empty `finding_id` string + - Validate `workflow_type` is one of `FP`, `Archer`, `CARD` + - Validate `vendor`: required non-empty string ≤200 chars for FP/Archer; ignored for CARD + - If any validation fails, return 400 with descriptive error message and reject entire batch + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.8, 3.10_ + - [x] 1.2 Implement transactional batch insert with SQLite + - Use `db.serialize()` with `BEGIN TRANSACTION` / `COMMIT` to insert all findings atomically + - For each finding: insert row into `ivanti_todo_queue` with `user_id`, `finding_id`, `finding_title`, `cves_json`, `ip_address`, `vendor`, `workflow_type` + - On success: fetch all inserted rows, parse `cves_json` back to arrays, return 201 with `{ items: [...] }` + - On any DB error: `ROLLBACK` the transaction and return 500 + - _Requirements: 3.7, 3.8, 3.9, 3.11_ + - [x] 1.3 Add audit logging for batch additions + - After successful commit, call `logAudit(db, { ... })` with action `'batch_add_to_queue'`, entityType `'ivanti_todo_queue'`, and details including the count and workflow_type + - Import `logAudit` from `../helpers/auditLog` + - _Requirements: 3.7_ + +- [x] 2. Checkpoint — Verify backend endpoint + - Ensure the batch endpoint is syntactically correct and the route file has no errors. Ask the user if questions arise. + +- [x] 3. Add multi-select state and checkbox dual-mode logic to `ReportingPage.js` + - [x] 3.1 Add selection state variables to `VulnerabilityTriagePage` + - Add `selectedIds` (`new Set()`), `lastClickedId` (null), `batchSubmitting` (false), `batchError` (null), `batchWorkflowType` ('FP'), `batchVendor` ('') as new `useState` hooks + - _Requirements: 1.1, 2.1_ + - [x] 3.2 Implement checkbox dual-mode click handler + - Replace the existing `
` onClick in the checkbox cell with new logic: + - If finding is already queued → no-op (existing behavior) + - If `selectedIds.size === 0` AND not shift-click → open `AddToQueuePopover` (preserves single-select flow) + - If shift-click AND `lastClickedId` exists → range-select all visible non-queued findings between `lastClickedId` and current finding in the `sorted` array + - Otherwise → toggle finding.id in `selectedIds` + - Always update `lastClickedId` when toggling selection + - _Requirements: 1.1, 1.2, 5.1, 5.2, 6.1_ + - [x] 3.3 Add visual highlighting for selected rows + - When a finding's ID is in `selectedIds`, apply a highlighted background (e.g. `rgba(14,165,233,0.12)`) to the row + - Override the existing alternating row background and hover for selected rows + - _Requirements: 1.3_ + - [x] 3.4 Disable checkbox for already-queued findings + - Keep existing behavior: queued findings show checked + disabled checkbox, preventing re-selection + - Ensure queued findings are excluded from shift-click range select and select-all + - _Requirements: 1.5_ + +- [x] 4. Implement Select All / Deselect All in column header + - Modify the checkbox column `` to render a clickable "Select All" checkbox when `selectedIds.size > 0` or when the user interacts with it + - Click behavior: if not all visible non-queued findings are selected → select all visible non-queued; if all are selected → deselect all + - _Requirements: 1.6, 1.7_ + +- [x] 5. Add selection pruning on filter changes + - Add a `useEffect` that watches `filtered` (the filtered findings array) and prunes `selectedIds` to only include IDs still present in the filtered set + - This ensures selection stays consistent when `columnFilters`, `actionFilter`, or `excFilter` change + - _Requirements: 1.4_ + +- [x] 6. Implement SelectionToolbar component + - [x] 6.1 Create the `SelectionToolbar` inline component in `ReportingPage.js` + - Render between the panel header controls and the `` element, only when `selectedIds.size > 0` + - Use `position: sticky` with appropriate `top` value to stay visible during scroll + - Follow the dark theme design system: monospace fonts, dark gradient background, accent-colored borders + - _Requirements: 2.1, 2.8_ + - [x] 6.2 Add toolbar controls: count badge, Clear Selection, workflow toggles, vendor input, submit button + - Display selected count badge (e.g. "12 selected") + - "Clear Selection" button that empties `selectedIds` and hides toolbar + - Workflow type toggle buttons (FP / Archer / CARD) using existing color scheme: FP = amber (`#F59E0B`), Archer = blue (`#0EA5E9`), CARD = green (`#10B981`) + - Vendor text input (hidden when CARD is selected, show "No vendor required" indicator for CARD) + - "Add to Queue" submit button — enabled only when workflow_type is CARD, or vendor is non-empty + - _Requirements: 2.2, 2.3, 2.4, 2.5, 2.6, 2.7_ + +- [x] 7. Implement batch submission flow + - [x] 7.1 Add `submitBatch` async function to `VulnerabilityTriagePage` + - Build request payload from `selectedIds` (map each ID to its finding object from `sorted`/`filtered` for `finding_id`, `finding_title`, `cves`, `ip_address`), plus `batchWorkflowType` and `batchVendor` + - POST to `${API_BASE}/ivanti/todo-queue/batch` with `credentials: 'include'` + - Set `batchSubmitting = true` before request, `false` after + - _Requirements: 4.1, 4.2_ + - [x] 7.2 Handle batch success response + - On 201: merge returned items into `queueItems` state (sorted by vendor then id, matching existing pattern) + - Clear `selectedIds`, reset `batchWorkflowType` to 'FP', reset `batchVendor` to '', clear `batchError` + - The newly queued findings will automatically show as checked+disabled via the existing `isQueued()` helper + - _Requirements: 4.3, 4.4, 4.6_ + - [x] 7.3 Handle batch error response + - On 4xx/5xx: parse error message from response JSON, set `batchError` to display in toolbar + - On network failure: set `batchError` to "Network error — please try again" + - Keep selection intact on error so user can retry + - _Requirements: 4.5_ + +- [x] 8. Add Escape key handler to clear selection + - Add a `useEffect` with a `keydown` listener for Escape that clears `selectedIds` when the SelectionToolbar is visible (i.e. `selectedIds.size > 0`) + - Ensure it doesn't conflict with the existing Escape handler on `AddToQueuePopover` + - _Requirements: 6.3_ + +- [x] 9. Ensure keyboard Tab accessibility for SelectionToolbar + - Verify all interactive elements in the toolbar (workflow buttons, vendor input, submit button, clear button) are focusable via Tab key + - Use native `
@@ -2796,8 +3016,31 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { background: 'rgb(10, 20, 36)', position: 'sticky', top: 0, zIndex: 10, boxShadow: '0 1px 0 rgba(14,165,233,0.2)', + textAlign: 'center', }} - /> + > + {canWrite() && ( + 0 && sorted.filter((f) => !isQueued(f.id)).length > 0 && sorted.filter((f) => !isQueued(f.id)).every((f) => selectedIds.has(f.id))} + onChange={() => { + const nonQueued = sorted.filter((f) => !isQueued(f.id)); + const allSelected = nonQueued.length > 0 && nonQueued.every((f) => selectedIds.has(f.id)); + if (allSelected) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(nonQueued.map((f) => f.id))); + } + }} + style={{ + accentColor: '#0EA5E9', + width: '13px', height: '13px', + cursor: 'pointer', + }} + title="Select all visible findings" + /> + )} + {visibleCols.map((col) => { const def = COLUMN_DEFS[col.key]; const active = sort.field === col.key; @@ -2849,31 +3092,60 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { {sorted.map((finding, idx) => { - const rowBg = idx % 2 === 0 ? 'rgba(15,26,46,0.4)' : 'rgba(10,18,32,0.4)'; + const isSelected = selectedIds.has(finding.id); + const rowBg = isSelected ? 'rgba(14,165,233,0.12)' : (idx % 2 === 0 ? 'rgba(15,26,46,0.4)' : 'rgba(10,18,32,0.4)'); const queued = isQueued(finding.id); return ( e.currentTarget.style.background = 'rgba(14,165,233,0.05)'} - onMouseLeave={(e) => e.currentTarget.style.background = rowBg} + onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'rgba(14,165,233,0.05)'; }} + onMouseLeave={(e) => { e.currentTarget.style.background = rowBg; }} > {/* Checkbox cell */}
{ if (queued) return; - const rect = e.currentTarget.getBoundingClientRect(); - setAddPopover({ finding, anchorRect: rect }); - setQueueForm({ vendor: '', workflowType: 'FP' }); + // If nothing selected and not shift-click, open single-add popover + if (selectedIds.size === 0 && !e.shiftKey) { + const rect = e.currentTarget.getBoundingClientRect(); + setAddPopover({ finding, anchorRect: rect }); + setQueueForm({ vendor: '', workflowType: 'FP' }); + return; + } + // Shift-click range select + if (e.shiftKey && lastClickedId) { + const lastIdx = sorted.findIndex((f) => f.id === lastClickedId); + const currIdx = sorted.findIndex((f) => f.id === finding.id); + if (lastIdx !== -1 && currIdx !== -1) { + const start = Math.min(lastIdx, currIdx); + const end = Math.max(lastIdx, currIdx); + setSelectedIds((prev) => { + const next = new Set(prev); + for (let i = start; i <= end; i++) { + if (!isQueued(sorted[i].id)) next.add(sorted[i].id); + } + return next; + }); + } + } else { + // Toggle selection + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(finding.id)) next.delete(finding.id); else next.add(finding.id); + return next; + }); + } + setLastClickedId(finding.id); }} >