diff --git a/backend/routes/ivantiFpWorkflow.js b/backend/routes/ivantiFpWorkflow.js index ef7e034..c3ff480 100644 --- a/backend/routes/ivantiFpWorkflow.js +++ b/backend/routes/ivantiFpWorkflow.js @@ -887,36 +887,39 @@ function createIvantiFpWorkflowRouter(db, requireAuth) { const mapUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/${encodeURIComponent(mapUuid)}/map`; - // Use multipart form (same format as the create endpoint) - const formFields = [{ name: 'subjectFilterRequest', value: buildSubjectFilterRequest(findingIds) }]; - - let mapResult; - try { - mapResult = await ivantiFormPost(mapUrl, formFields, [], apiKey, skipTls); - } catch (networkErr) { - logAudit(db, { - userId: req.user.id, username: req.user.username, - action: 'ivanti_fp_findings_add_failed', entityType: 'ivanti_workflow', - details: { error: networkErr.message, submissionId, findingIds }, - ipAddress: req.ip - }); - 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.' + // Map each finding individually via JSON POST (Ivanti map endpoint only accepts one finding per call) + const mappedIds = []; + const failedIds = []; + for (const fid of findingIds) { + const mapBody = { + subject: 'hostFinding', + filterRequest: { + filters: [{ + field: 'id', + exclusive: false, + operator: 'EXACT', + value: String(fid) + }] + } }; - const errorMsg = mapResult.status >= 500 - ? 'Ivanti API is temporarily unavailable. Please try again later.' - : (errorMap[mapResult.status] || `Operation failed: ${mapResult.status}`); - return res.status(mapResult.status === 429 ? 429 : 502).json({ success: false, error: errorMsg }); + try { + const result = await ivantiPost(mapUrl, mapBody, apiKey, skipTls); + if (result.status === 200 || result.status === 201 || result.status === 202) { + mappedIds.push(fid); + } else { + failedIds.push({ id: fid, status: result.status }); + } + } catch (err) { + failedIds.push({ id: fid, error: err.message }); + } } - // 5. Merge finding IDs - const mergedJson = mergeFindings(submission.finding_ids_json, findingIds); + if (mappedIds.length === 0) { + 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(); try { @@ -931,25 +934,34 @@ function createIvantiFpWorkflowRouter(db, requireAuth) { 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; - try { - const queuePlaceholders = queueItemIds.map(() => '?').join(','); - queueItemsUpdated = await new Promise((resolve, reject) => { - db.run( - `UPDATE ivanti_todo_queue SET status='complete', updated_at=CURRENT_TIMESTAMP WHERE id IN (${queuePlaceholders}) AND user_id=?`, - [...queueItemIds, req.user.id], - function (err) { if (err) reject(err); else resolve(this.changes); } - ); - }); - } catch (queueErr) { - console.error('Failed to update queue items:', queueErr); + // 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 { + const queuePlaceholders = successQueueIds.map(() => '?').join(','); + queueItemsUpdated = await new Promise((resolve, reject) => { + db.run( + `UPDATE ivanti_todo_queue SET status='complete', updated_at=CURRENT_TIMESTAMP WHERE id IN (${queuePlaceholders}) AND user_id=?`, + [...successQueueIds, req.user.id], + function (err) { if (err) reject(err); else resolve(this.changes); } + ); + }); + } catch (queueErr) { + console.error('Failed to update queue items:', queueErr); + } } // 7. Insert history row const historyEntry = buildSubmissionHistoryEntry('findings_added', { - addedFindingIds: findingIds, - queueItemIds: queueItemIds + addedFindingIds: mappedIds, + failedFindingIds: failedIds.map(f => f.id || f), + queueItemIds: successQueueIds }, req.user.id, req.user.username); try { @@ -974,7 +986,7 @@ function createIvantiFpWorkflowRouter(db, requireAuth) { ipAddress: req.ip }); - res.json({ success: true, addedFindings: findingIds, queueItemsUpdated }); + res.json({ success: true, addedFindings: mappedIds, failedFindings: failedIds, queueItemsUpdated }); })().catch((unexpectedErr) => { console.error('Unexpected error in POST /submissions/:id/findings:', unexpectedErr); res.status(500).json({ success: false, error: 'Internal server error.' });