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

@@ -3,7 +3,7 @@ const express = require('express');
const { requireGroup } = require('../middleware/auth');
const logAudit = require('../helpers/auditLog');
const VALID_WORKFLOW_TYPES = ['FP', 'Archer', 'CARD'];
const VALID_WORKFLOW_TYPES = ['FP', 'Archer', 'CARD', 'GRANITE'];
const VALID_STATUSES = ['pending', 'complete'];
function isValidVendor(vendor) {
@@ -64,8 +64,8 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
* @body {string[]} [findings[].cves] - Optional array of CVE identifiers
* @body {string} [findings[].ip_address] - Optional IP address (max 64 chars)
* @body {string} [findings[].hostname] - Optional hostname (max 255 chars)
* @body {string} workflow_type - One of 'FP', 'Archer', 'CARD'
* @body {string} vendor - Required for FP/Archer (max 200 chars); optional for CARD
* @body {string} workflow_type - One of 'FP', 'Archer', 'CARD', 'GRANITE'
* @body {string} vendor - Required for FP/Archer (max 200 chars); optional for CARD/GRANITE
*
* @returns {Object} 201 - { items: Array<Object> } array of created queue items,
* each with: id, user_id, finding_id, finding_title, cves_json, ip_address,
@@ -89,10 +89,10 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
}
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' });
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (workflow_type !== 'CARD') {
if (!['CARD', 'GRANITE'].includes(workflow_type)) {
if (!isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
}
@@ -102,7 +102,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim();
const vendorVal = ['CARD', 'GRANITE'].includes(workflow_type) ? '' : vendor.trim();
const userId = req.user.id;
// --- Transactional batch insert ---
@@ -219,8 +219,9 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
* @body {string} [finding_title] - Optional finding title (max 500 chars)
* @body {string[]} [cves] - Optional array of CVE identifiers
* @body {string} [ip_address] - Optional IP address (max 64 chars)
* @body {string} [hostname] - Optional hostname (max 255 chars) * @body {string} vendor - Required for FP/Archer (max 200 chars); optional for CARD
* @body {string} workflow_type - One of 'FP', 'Archer', 'CARD'
* @body {string} [hostname] - Optional hostname (max 255 chars)
* @body {string} workflow_type - One of 'FP', 'Archer', 'CARD', 'GRANITE'
* @body {string} vendor - Required for FP/Archer (max 200 chars); optional for CARD/GRANITE
*
* @returns {Object} 201 - Created queue item with parsed cves array:
* id, user_id, finding_id, finding_title, cves_json, ip_address,
@@ -235,17 +236,17 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
return res.status(400).json({ error: 'finding_id is required.' });
}
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' });
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
// Vendor is required for FP and Archer, optional for CARD
if (workflow_type !== 'CARD' && !isValidVendor(vendor)) {
// Vendor is required for FP and Archer, optional for CARD/GRANITE
if (!['CARD', 'GRANITE'].includes(workflow_type) && !isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
}
if (vendor !== undefined && vendor !== '' && !isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim();
const vendorVal = ['CARD', 'GRANITE'].includes(workflow_type) ? '' : vendor.trim();
const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null;
const ipVal = ip_address && typeof ip_address === 'string' ? ip_address.trim().slice(0, 64) : null;
const hostVal = hostname && typeof hostname === 'string' ? hostname.trim().slice(0, 255) : null;
@@ -294,7 +295,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
*
* @param {string} id - Queue item ID (URL parameter)
* @body {string} [vendor] - New vendor string (max 200 chars)
* @body {string} [workflow_type] - One of 'FP', 'Archer', 'CARD'
* @body {string} [workflow_type] - One of 'FP', 'Archer', 'CARD', 'GRANITE'
* @body {string} [status] - One of 'pending', 'complete'
*
* @returns {Object} 200 - Updated queue item with parsed cves array:
@@ -312,7 +313,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
return res.status(400).json({ error: 'vendor must be a non-empty string (max 200 chars).' });
}
if (workflow_type !== undefined && !VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' });
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (status !== undefined && !VALID_STATUSES.includes(status)) {
return res.status(400).json({ error: 'status must be pending or complete.' });
@@ -394,8 +395,8 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
* Creates a new pending item copying finding data from the original.
*
* @param {string} id - Original queue item ID (URL parameter)
* @body {string} workflow_type - Target workflow type: 'FP', 'Archer', or 'CARD'
* @body {string} [vendor] - Required for FP/Archer (max 200 chars); ignored for CARD
* @body {string} workflow_type - Target workflow type: 'FP', 'Archer', 'CARD', or 'GRANITE'
* @body {string} [vendor] - Required for FP/Archer (max 200 chars); ignored for CARD/GRANITE
*
* @returns {Object} 201 - Newly created queue item with parsed cves array
* @returns {Object} 400 - { error: string } on validation failure or item not complete
@@ -408,10 +409,10 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
// --- Validation ---
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' });
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (workflow_type !== 'CARD') {
if (!['CARD', 'GRANITE'].includes(workflow_type)) {
if (!isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
}
@@ -421,7 +422,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim();
const vendorVal = ['CARD', 'GRANITE'].includes(workflow_type) ? '' : vendor.trim();
// --- Fetch original item scoped to current user ---
db.get(