feat: add GRANITE as fourth workflow type in Ivanti queue

- Add GRANITE to VALID_WORKFLOW_TYPES in backend (no vendor required, same as CARD)
- Update vendor validation and error messages across all endpoints (single add, batch, PUT, redirect)
- Add GRANITE option to RedirectModal with warm slate color (#A1887F)
- Rename QueuePanel CARD section to Inventory, group CARD + GRANITE with sub-divider
- Add GRANITE to AddToQueuePopover and SelectionToolbar
- Update spec docs (requirements, design, tasks)
This commit is contained in:
jramos
2026-04-14 15:33:19 -06:00
parent 28bce28fc9
commit 0fe8e94d51
6 changed files with 503 additions and 265 deletions

View File

@@ -2,12 +2,16 @@
## Overview
The Ivanti Queue Redirect feature adds an optional redirect action to completed queue items, allowing users to create a new pending queue item under a different workflow type from an existing completed item. This supports the common scenario where a CARD inventory fix is done but the finding still needs FP or Archer processing, or where an item was assigned to the wrong workflow initially.
The Ivanti Queue Redirect feature adds an optional redirect action to completed queue items, allowing users to create a new pending queue item under a different workflow type from an existing completed item. This supports the common scenario where a CARD inventory fix is done but the finding still needs FP or Archer processing, where an item was assigned to the wrong workflow initially, or where a CARD item with a high asset score (90+) needs to go through the GRANITE program for reassignment or deletion.
The feature consists of three parts:
The feature consists of five parts:
1. A new backend API endpoint (`POST /api/ivanti/todo-queue/:id/redirect`) added to the existing `ivantiTodoQueue.js` route module
2. A redirect modal component in the frontend for collecting target workflow type and vendor
3. A redirect button on completed queue items in the existing QueuePanel
2. GRANITE added as a fourth valid workflow type across all backend endpoints (`VALID_WORKFLOW_TYPES` constant)
3. A redirect modal component in the frontend for collecting target workflow type and vendor
4. A redirect button on completed queue items in the existing QueuePanel
5. Updated QueuePanel grouping: CARD and GRANITE items grouped under an "Inventory" section, with GRANITE also available in the AddToQueue popover
There are four workflow types: FP, Archer, CARD, and GRANITE. FP and Archer require a vendor string; CARD and GRANITE do not. Any completed item can redirect to any other workflow type — there is no fixed ordering between types.
The redirect operation creates a new row in `ivanti_todo_queue` — it does not modify or delete the original completed item. This preserves the audit trail and allows the original item to remain visible as completed.
@@ -35,10 +39,39 @@ sequenceDiagram
RM->>QP: Adds new item to list, closes modal, shows success
```
No new database tables or schema changes are required. The redirect creates a standard `ivanti_todo_queue` row using the existing schema. The only backend change outside the new endpoint is fixing the PUT validation message (Requirement 5).
No new database tables or schema changes are required. The redirect creates a standard `ivanti_todo_queue` row using the existing schema. Backend changes outside the new endpoint include: adding GRANITE to `VALID_WORKFLOW_TYPES`, updating all error messages to list four valid types, and treating GRANITE like CARD for vendor validation (no vendor required).
### QueuePanel Grouping (Layout)
```mermaid
graph TD
subgraph QueuePanel
subgraph Inventory Section
A[CARD items]
B[Sub-divider - only when both exist]
C[GRANITE items]
end
subgraph Vendor Groups
D[Vendor A - FP/Archer items]
E[Vendor B - FP/Archer items]
end
end
```
The QueuePanel groups items into two categories:
- **Inventory section** (top): Contains both CARD and GRANITE items under a single "Inventory" heading. CARD items appear first, followed by a subtle sub-divider (only shown when both types are present), then GRANITE items. Each item retains its workflow type badge (CARD in green, GRANITE in warm slate).
- **Vendor groups** (below): FP and Archer items grouped by vendor, same as current behavior.
## Components and Interfaces
### Backend: VALID_WORKFLOW_TYPES Constant
Updated from `['FP', 'Archer', 'CARD']` to `['FP', 'Archer', 'CARD', 'GRANITE']`.
All endpoints that reference this constant (batch add, single add, PUT, redirect) automatically accept GRANITE. The vendor validation condition changes from `workflow_type !== 'CARD'` to `workflow_type !== 'CARD' && workflow_type !== 'GRANITE'` (or equivalently, checking if the type is FP or Archer). The `vendorVal` assignment similarly treats GRANITE like CARD: `vendorVal = (workflow_type === 'CARD' || workflow_type === 'GRANITE') ? '' : vendor.trim()`.
All error messages for invalid workflow_type are updated to: `"workflow_type must be FP, Archer, CARD, or GRANITE."`.
### Backend: Redirect Endpoint
Added to `backend/routes/ivantiTodoQueue.js` inside the existing `createIvantiTodoQueueRouter` factory function.
@@ -50,8 +83,8 @@ POST /api/ivanti/todo-queue/:id/redirect
**Request body:**
```json
{
"workflow_type": "FP" | "Archer" | "CARD",
"vendor": "string (required for FP/Archer, omitted for CARD)"
"workflow_type": "FP" | "Archer" | "CARD" | "GRANITE",
"vendor": "string (required for FP/Archer, omitted for CARD/GRANITE)"
}
```
@@ -85,18 +118,43 @@ POST /api/ivanti/todo-queue/:id/redirect
**Auth:** `requireAuth(db)`, `requireGroup('Admin', 'Standard_User')`
### Backend: Vendor Validation Logic
The vendor requirement is conditional on workflow type across all endpoints:
| Workflow Type | Vendor Required | vendorVal |
|---------------|----------------|-----------|
| FP | Yes — non-empty, ≤ 200 chars | `vendor.trim()` |
| Archer | Yes — non-empty, ≤ 200 chars | `vendor.trim()` |
| CARD | No | `''` (empty string) |
| GRANITE | No | `''` (empty string) |
The condition for requiring vendor changes from `workflow_type !== 'CARD'` to `workflow_type === 'FP' || workflow_type === 'Archer'` (or equivalently `!['CARD', 'GRANITE'].includes(workflow_type)`).
### Backend: PUT Validation Fix
In the existing PUT `/:id` handler, the error message for invalid `workflow_type` currently says `"workflow_type must be FP or Archer."` — this will be updated to `"workflow_type must be FP, Archer, or CARD."`.
In the existing PUT `/:id` handler, the error message for invalid `workflow_type` is updated to `"workflow_type must be FP, Archer, CARD, or GRANITE."`. The same update applies to the batch add, single add, and redirect endpoints.
### Frontend: RedirectModal Component
A new modal component rendered inside the QueuePanel. It receives the item being redirected and collects:
- Target workflow type (radio buttons or select: FP, Archer, CARD)
A modal component rendered inside the QueuePanel. It receives the item being redirected and collects:
- Target workflow type (radio buttons: FP, Archer, CARD, GRANITE)
- Vendor (text input, shown only when FP or Archer is selected)
The modal displays read-only context: finding title, finding ID, and current workflow type.
**WORKFLOW_OPTIONS constant** (updated to include GRANITE):
```js
const WORKFLOW_OPTIONS = [
{ key: 'FP', label: 'FP', col: '#F59E0B', rgb: '245,158,11' },
{ key: 'Archer', label: 'Archer', col: '#0EA5E9', rgb: '14,165,233' },
{ key: 'CARD', label: 'CARD', col: '#10B981', rgb: '16,185,129' },
{ key: 'GRANITE', label: 'GRANITE', col: '#A1887F', rgb: '161,136,127' },
];
```
The `needsVendor` condition changes from `workflowType === 'FP' || workflowType === 'Archer'` — this remains the same since GRANITE, like CARD, does not need vendor.
Props:
```js
{
@@ -108,9 +166,80 @@ Props:
### Frontend: QueuePanel Changes
- Add a redirect button (e.g., `CornerUpRight` or `ArrowRightLeft` icon from lucide-react) on each completed item row, next to the existing delete button
- Track `redirectItem` state — when set, render the RedirectModal
- On successful redirect, append the new item to the queue items list
#### Grouping Logic
The current grouping logic filters `workflow_type === 'CARD'` into a separate top section. This changes to group both CARD and GRANITE into an "Inventory" section:
```js
const grouped = useMemo(() => {
const inventoryItems = items.filter((i) => i.workflow_type === 'CARD' || i.workflow_type === 'GRANITE');
const cardItems = inventoryItems.filter((i) => i.workflow_type === 'CARD');
const graniteItems = inventoryItems.filter((i) => i.workflow_type === 'GRANITE');
const otherItems = items.filter((i) => i.workflow_type !== 'CARD' && i.workflow_type !== 'GRANITE');
// Vendor groups for FP/Archer items
const map = {};
otherItems.forEach((item) => {
const v = item.vendor || 'Unknown';
if (!map[v]) map[v] = [];
map[v].push(item);
});
const vendorGroups = Object.keys(map).sort().map((vendor) => ({
key: vendor, label: vendor, items: map[vendor], isInventory: false,
}));
return inventoryItems.length > 0
? [{ key: '__INVENTORY__', label: 'Inventory', cardItems, graniteItems, items: inventoryItems, isInventory: true }, ...vendorGroups]
: vendorGroups;
}, [items]);
```
#### Inventory Section Rendering
The Inventory section header uses the existing accent color for the section label. Within the section:
1. CARD items render first
2. A subtle sub-divider appears only when both CARD and GRANITE items exist
3. GRANITE items render below the sub-divider
Each item retains its individual workflow type badge with distinct colors.
#### Workflow Type Color Mapping
Updated to include GRANITE:
```js
const wfColor = item.workflow_type === 'FP' ? { col: '#F59E0B', rgb: '245,158,11' }
: item.workflow_type === 'Archer' ? { col: '#0EA5E9', rgb: '14,165,233' }
: item.workflow_type === 'GRANITE' ? { col: '#A1887F', rgb: '161,136,127' }
: { col: '#10B981', rgb: '16,185,129' };
```
#### GRANITE Item Rendering
GRANITE items render identically to CARD items — showing hostname and ip_address fields (not CVEs), since GRANITE is also an inventory-category workflow. The condition changes from `isCardItem` to `isInventoryItem`:
```js
const isInventoryItem = item.workflow_type === 'CARD' || item.workflow_type === 'GRANITE';
```
#### Redirect Button
A redirect icon button (`CornerUpRight` from lucide-react) on each completed queue item row, next to the existing delete button. Visible only when `item.status === 'complete'` and `canWrite` is true.
### Frontend: AddToQueue Popover
The AddToQueue popover (defined inline in `ReportingPage.js`) adds GRANITE as a fourth workflow type button:
```js
const QUEUE_WORKFLOW_OPTIONS = [
{ key: 'FP', label: 'FP', col: '#F59E0B', rgb: '245,158,11' },
{ key: 'Archer', label: 'Archer', col: '#0EA5E9', rgb: '14,165,233' },
{ key: 'CARD', label: 'CARD', col: '#10B981', rgb: '16,185,129' },
{ key: 'GRANITE', label: 'GRANITE', col: '#A1887F', rgb: '161,136,127' },
];
```
When GRANITE is selected, no vendor field is required — same behavior as CARD. The submit logic uses the same condition: `workflow_type === 'FP' || workflow_type === 'Archer'` to determine if vendor is needed.
### Frontend: API Call
@@ -135,8 +264,8 @@ No schema changes. The redirect creates a standard `ivanti_todo_queue` row:
| cves_json | Copied from original item |
| ip_address | Copied from original item |
| hostname | Copied from original item |
| vendor | From request body (FP/Archer) or empty string (CARD) |
| workflow_type | From request body |
| vendor | From request body (FP/Archer) or empty string (CARD/GRANITE) |
| workflow_type | From request body (FP, Archer, CARD, or GRANITE) |
| status | `'pending'` (always) |
The original completed item remains unchanged.
@@ -148,15 +277,15 @@ The original completed item remains unchanged.
### Property 1: Redirect preserves finding data
*For any* completed queue item with arbitrary finding_id, finding_title, cves_json, ip_address, and hostname values, and *for any* valid target workflow type, redirecting that item SHALL produce a new queue item where finding_id, finding_title, cves_json, ip_address, and hostname are identical to the original, status is "pending", and workflow_type matches the requested target.
*For any* completed queue item with arbitrary finding_id, finding_title, cves_json, ip_address, and hostname values, and *for any* valid target workflow type (FP, Archer, CARD, or GRANITE), redirecting that item SHALL produce a new queue item where finding_id, finding_title, cves_json, ip_address, and hostname are identical to the original, status is "pending", and workflow_type matches the requested target.
**Validates: Requirements 1.1, 1.7**
### Property 2: Vendor requirement is conditional on workflow type
*For any* redirect request, if the target workflow_type is "FP" or "Archer", the request SHALL be accepted if and only if vendor is a non-empty string of 200 characters or fewer. If the target workflow_type is "CARD", the request SHALL be accepted regardless of whether vendor is provided.
*For any* redirect request, if the target workflow_type is "FP" or "Archer", the request SHALL be accepted if and only if vendor is a non-empty string of 200 characters or fewer. If the target workflow_type is "CARD" or "GRANITE", the request SHALL be accepted regardless of whether vendor is provided.
**Validates: Requirements 1.2, 1.3**
**Validates: Requirements 1.2, 1.3, 6.2**
### Property 3: Successful redirect produces correct audit entry
@@ -170,13 +299,13 @@ The original completed item remains unchanged.
|----------|-------------|---------------|----------|
| Item not found or belongs to another user | 404 | "Queue item not found." | Consistent with existing DELETE/PUT pattern |
| Item status is not "complete" | 400 | "Only completed queue items can be redirected." | Prevents redirecting pending items |
| Invalid workflow_type | 400 | "workflow_type must be FP, Archer, or CARD." | Same message as batch/single add |
| Invalid workflow_type | 400 | "workflow_type must be FP, Archer, CARD, or GRANITE." | Same message across all endpoints |
| Missing/invalid vendor for FP/Archer | 400 | "vendor is required for FP and Archer workflows." | Same message as existing endpoints |
| Vendor exceeds 200 chars | 400 | "vendor must be under 200 chars." | Same message as existing endpoints |
| Database insert failure | 500 | "Internal server error." | Consistent with existing error pattern |
| Frontend API error | — | Display error message from API in modal | Modal stays open so user can retry or cancel |
The redirect endpoint reuses the existing `isValidVendor()` helper and `VALID_WORKFLOW_TYPES` constant from `ivantiTodoQueue.js` for consistent validation.
The redirect endpoint reuses the existing `isValidVendor()` helper and `VALID_WORKFLOW_TYPES` constant from `ivantiTodoQueue.js` for consistent validation. All error messages for invalid workflow_type now list all four valid options: FP, Archer, CARD, and GRANITE.
Audit logging uses the existing fire-and-forget pattern — a failed audit log write does not block or fail the redirect response.
@@ -187,22 +316,35 @@ Audit logging uses the existing fire-and-forget pattern — a failed audit log w
Backend:
- Redirect a completed CARD item to FP with vendor → 201, new item returned
- Redirect a completed FP item to CARD without vendor → 201, new item returned
- Redirect a completed FP item to GRANITE without vendor → 201, new item returned
- Redirect a completed GRANITE item to Archer with vendor → 201, new item returned
- Redirect a pending item → 400
- Redirect another user's item → 404
- Redirect with invalid workflow_type → 400
- Redirect with invalid workflow_type → 400 with message listing FP, Archer, CARD, GRANITE
- Redirect to FP without vendor → 400
- Redirect to FP with vendor > 200 chars → 400
- Redirect non-existent item → 404
- PUT with invalid workflow_type returns updated error message text
- PUT with invalid workflow_type returns error message "workflow_type must be FP, Archer, CARD, or GRANITE."
- Batch add with workflow_type GRANITE and no vendor → 201
- Single add with workflow_type GRANITE and no vendor → 201
- Verify audit log is called with correct fields on successful redirect
- Verify VALID_WORKFLOW_TYPES includes all four types
Frontend:
- Redirect button visible on completed items, hidden on pending items
- Clicking redirect button opens modal with correct item context
- Modal shows vendor field for FP/Archer, hides for CARD
- Modal shows all four workflow type options (FP, Archer, CARD, GRANITE)
- Modal shows vendor field for FP/Archer, hides for CARD and GRANITE
- Modal displays finding title, finding ID, current workflow type
- Successful redirect closes modal, adds new item to list, shows notification
- Failed redirect shows error message, modal stays open
- QueuePanel groups CARD and GRANITE items under "Inventory" section
- Sub-divider shown only when both CARD and GRANITE items exist in Inventory section
- "Inventory" heading shown even when only one sub-type present
- GRANITE badge uses warm slate color (#A1887F)
- CARD badge uses green color (#10B981)
- AddToQueue popover shows GRANITE as fourth option with warm slate color
- Selecting GRANITE in AddToQueue popover does not require vendor
### Property-Based Tests
@@ -210,11 +352,11 @@ Library: `fast-check` (JavaScript property-based testing library)
Each property test runs a minimum of 100 iterations.
- **Property 1**: Generate random queue item data (finding_id, finding_title, cves_json, ip_address, hostname with varying lengths, special characters, null optionals) and random valid workflow_type. Mock the database layer. Verify the INSERT parameters preserve all finding fields and set status to "pending".
- **Property 1**: Generate random queue item data (finding_id, finding_title, cves_json, ip_address, hostname with varying lengths, special characters, null optionals) and random valid workflow_type from all four types (FP, Archer, CARD, GRANITE). Mock the database layer. Verify the INSERT parameters preserve all finding fields and set status to "pending".
- Tag: `Feature: ivanti-queue-redirect, Property 1: Redirect preserves finding data`
- **Property 2**: Generate random (workflow_type, vendor) pairs where workflow_type is drawn from valid types and vendor is drawn from a mix of valid strings, empty strings, whitespace, strings of length 200, and strings of length 201. Verify that the validation logic accepts/rejects correctly based on the conditional rule.
- **Property 2**: Generate random (workflow_type, vendor) pairs where workflow_type is drawn from all four valid types and vendor is drawn from a mix of valid strings, empty strings, whitespace, strings of length 200, and strings of length 201. Verify that the validation logic accepts/rejects correctly: FP/Archer require non-empty vendor ≤ 200 chars; CARD/GRANITE accept without vendor.
- Tag: `Feature: ivanti-queue-redirect, Property 2: Vendor requirement is conditional on workflow type`
- **Property 3**: Generate random successful redirect scenarios with varying item data and workflow types. Mock logAudit. Verify the audit call contains the correct action, entityType, entityId, and all required detail fields.
- **Property 3**: Generate random successful redirect scenarios with varying item data and all four workflow types. Mock logAudit. Verify the audit call contains the correct action, entityType, entityId, and all required detail fields.
- Tag: `Feature: ivanti-queue-redirect, Property 3: Successful redirect produces correct audit entry`