Files
cve-dashboard/frontend/src/utils/jiraConsolidation.js
Jordan Ramos 6b805ee633 Add multi-item Jira ticket creation from Ivanti Queue
Select multiple queue items and create a single consolidated Jira ticket
with aggregated summary and description. Adds multi-select mode with
checkboxes, floating action bar, consolidation modal, and junction table
to track which queue items contributed to each ticket.

- Migration: jira_ticket_queue_items junction table
- POST /api/jira-tickets/:id/queue-items endpoint
- GET /api/ivanti/todo-queue/ticket-links endpoint
- ConsolidationModal component with aggregation logic
- IvantiTodoQueuePage with selection mode and ticket link badges
- Pure utility functions for summary/description generation
- 34 tests passing (backend + frontend)
2026-05-22 11:12:45 -06:00

93 lines
2.9 KiB
JavaScript

/**
* Pure utility functions for consolidating multiple Ivanti queue items
* into a single Jira ticket's summary, description, CVE, and vendor fields.
*/
/**
* Generate a consolidated summary for a multi-item Jira ticket.
* Format: "[N findings] vendor - first_finding_title", truncated to 255 chars.
*
* @param {Array} items - Array of queue item objects
* @returns {string} Generated summary, at most 255 characters
*/
export function generateConsolidatedSummary(items) {
const count = items.length;
const vendors = [...new Set(items.map(i => i.vendor).filter(Boolean))];
const vendorLabel = vendors.length === 1 ? vendors[0] : 'Multiple Vendors';
const firstTitle = items[0]?.finding_title || 'Untitled';
const raw = `[${count} findings] ${vendorLabel} - ${firstTitle}`;
return raw.slice(0, 255);
}
/**
* Generate a structured description grouped by vendor for a consolidated Jira ticket.
*
* @param {Array} items - Array of queue item objects
* @returns {string} Structured description with header and vendor-grouped items
*/
export function generateConsolidatedDescription(items) {
const header = `Consolidated Jira ticket covering ${items.length} Ivanti queue findings.\n\n`;
// Group by vendor
const grouped = {};
for (const item of items) {
const vendor = item.vendor || 'Unknown Vendor';
if (!grouped[vendor]) grouped[vendor] = [];
grouped[vendor].push(item);
}
let body = '';
for (const [vendor, vendorItems] of Object.entries(grouped)) {
body += `== ${vendor} ==\n`;
for (const item of vendorItems) {
let cves = 'None';
if (item.cves_json) {
try {
const parsed = JSON.parse(item.cves_json);
if (Array.isArray(parsed) && parsed.length > 0) {
cves = parsed.join(', ');
}
} catch (e) {
cves = 'None';
}
}
body += `- ${item.finding_title}\n`;
body += ` CVEs: ${cves}\n`;
body += ` Host: ${item.hostname || 'N/A'} (${item.ip_address || 'N/A'})\n\n`;
}
}
return header + body;
}
/**
* Extract the first CVE from the first item that has a non-empty cves_json array.
*
* @param {Array} items - Array of queue item objects
* @returns {string} First CVE ID found, or empty string if none
*/
export function extractFirstCve(items) {
for (const item of items) {
if (item.cves_json) {
try {
const cves = JSON.parse(item.cves_json);
if (Array.isArray(cves) && cves.length > 0) return cves[0];
} catch (e) {
// Skip items with invalid JSON
}
}
}
return '';
}
/**
* Extract the common vendor if all items share the same vendor.
*
* @param {Array} items - Array of queue item objects
* @returns {string} Common vendor name if all items share it, empty string otherwise
*/
export function extractCommonVendor(items) {
const vendors = [...new Set(items.map(i => i.vendor).filter(Boolean))];
return vendors.length === 1 ? vendors[0] : '';
}