fix: map findings one at a time via JSON POST, only mark successfully mapped queue items as complete

This commit is contained in:
jramos
2026-04-13 15:59:55 -06:00
parent 4583d09750
commit fa3b045a2f

View File

@@ -887,36 +887,39 @@ function createIvantiFpWorkflowRouter(db, requireAuth) {
const mapUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/${encodeURIComponent(mapUuid)}/map`; const mapUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/${encodeURIComponent(mapUuid)}/map`;
// Use multipart form (same format as the create endpoint) // Map each finding individually via JSON POST (Ivanti map endpoint only accepts one finding per call)
const formFields = [{ name: 'subjectFilterRequest', value: buildSubjectFilterRequest(findingIds) }]; const mappedIds = [];
const failedIds = [];
let mapResult; for (const fid of findingIds) {
try { const mapBody = {
mapResult = await ivantiFormPost(mapUrl, formFields, [], apiKey, skipTls); subject: 'hostFinding',
} catch (networkErr) { filterRequest: {
logAudit(db, { filters: [{
userId: req.user.id, username: req.user.username, field: 'id',
action: 'ivanti_fp_findings_add_failed', entityType: 'ivanti_workflow', exclusive: false,
details: { error: networkErr.message, submissionId, findingIds }, operator: 'EXACT',
ipAddress: req.ip value: String(fid)
}); }]
return res.status(502).json({ success: false, error: 'Failed to connect to Ivanti API.', details: networkErr.message });
} }
if (mapResult.status !== 200 && mapResult.status !== 201 && mapResult.status !== 202) {
const errorMap = {
401: 'Ivanti API key is invalid or missing. Contact your administrator.',
419: 'API key lacks permissions for this operation.',
429: 'Ivanti API rate limit reached. Please try again in a few minutes.'
}; };
const errorMsg = mapResult.status >= 500 try {
? 'Ivanti API is temporarily unavailable. Please try again later.' const result = await ivantiPost(mapUrl, mapBody, apiKey, skipTls);
: (errorMap[mapResult.status] || `Operation failed: ${mapResult.status}`); if (result.status === 200 || result.status === 201 || result.status === 202) {
return res.status(mapResult.status === 429 ? 429 : 502).json({ success: false, error: errorMsg }); mappedIds.push(fid);
} else {
failedIds.push({ id: fid, status: result.status });
}
} catch (err) {
failedIds.push({ id: fid, error: err.message });
}
} }
// 5. Merge finding IDs if (mappedIds.length === 0) {
const mergedJson = mergeFindings(submission.finding_ids_json, findingIds); return res.status(502).json({ success: false, error: 'Failed to map any findings to the workflow.' });
}
// 5. Merge only successfully mapped finding IDs
const mergedJson = mergeFindings(submission.finding_ids_json, mappedIds);
const now = new Date().toISOString(); const now = new Date().toISOString();
try { try {
@@ -931,25 +934,34 @@ function createIvantiFpWorkflowRouter(db, requireAuth) {
console.error('Failed to update finding_ids_json:', dbErr); console.error('Failed to update finding_ids_json:', dbErr);
} }
// 6. Mark queue items complete // 6. Mark only successfully mapped queue items as complete
let queueItemsUpdated = 0; let queueItemsUpdated = 0;
// Build a set of successfully mapped finding IDs to match against queue items
const mappedSet = new Set(mappedIds.map(String));
const successQueueIds = queueItemIds.filter((qid, idx) => {
const queueItem = queueRows.find(r => r.id === qid);
return queueItem && mappedSet.has(String(findingIds[idx]));
});
if (successQueueIds.length > 0) {
try { try {
const queuePlaceholders = queueItemIds.map(() => '?').join(','); const queuePlaceholders = successQueueIds.map(() => '?').join(',');
queueItemsUpdated = await new Promise((resolve, reject) => { queueItemsUpdated = await new Promise((resolve, reject) => {
db.run( db.run(
`UPDATE ivanti_todo_queue SET status='complete', updated_at=CURRENT_TIMESTAMP WHERE id IN (${queuePlaceholders}) AND user_id=?`, `UPDATE ivanti_todo_queue SET status='complete', updated_at=CURRENT_TIMESTAMP WHERE id IN (${queuePlaceholders}) AND user_id=?`,
[...queueItemIds, req.user.id], [...successQueueIds, req.user.id],
function (err) { if (err) reject(err); else resolve(this.changes); } function (err) { if (err) reject(err); else resolve(this.changes); }
); );
}); });
} catch (queueErr) { } catch (queueErr) {
console.error('Failed to update queue items:', queueErr); console.error('Failed to update queue items:', queueErr);
} }
}
// 7. Insert history row // 7. Insert history row
const historyEntry = buildSubmissionHistoryEntry('findings_added', { const historyEntry = buildSubmissionHistoryEntry('findings_added', {
addedFindingIds: findingIds, addedFindingIds: mappedIds,
queueItemIds: queueItemIds failedFindingIds: failedIds.map(f => f.id || f),
queueItemIds: successQueueIds
}, req.user.id, req.user.username); }, req.user.id, req.user.username);
try { try {
@@ -974,7 +986,7 @@ function createIvantiFpWorkflowRouter(db, requireAuth) {
ipAddress: req.ip ipAddress: req.ip
}); });
res.json({ success: true, addedFindings: findingIds, queueItemsUpdated }); res.json({ success: true, addedFindings: mappedIds, failedFindings: failedIds, queueItemsUpdated });
})().catch((unexpectedErr) => { })().catch((unexpectedErr) => {
console.error('Unexpected error in POST /submissions/:id/findings:', unexpectedErr); console.error('Unexpected error in POST /submissions/:id/findings:', unexpectedErr);
res.status(500).json({ success: false, error: 'Internal server error.' }); res.status(500).json({ success: false, error: 'Internal server error.' });