2026-04-07 16:20:24 -06:00
|
|
|
// Shared Ivanti / RiskSense API helpers
|
|
|
|
|
// Centralizes HTTP calls so ivantiWorkflows.js, ivantiFindings.js, and
|
|
|
|
|
// ivantiFpWorkflow.js all use the same implementation.
|
|
|
|
|
|
|
|
|
|
const https = require('https');
|
|
|
|
|
|
|
|
|
|
const IVANTI_URL_BASE = 'https://platform4.risksense.com/api/v1';
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// JSON POST — used for search, workflow creation, etc.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function ivantiPost(urlPath, body, apiKey, skipTls) {
|
|
|
|
|
const bodyStr = JSON.stringify(body);
|
|
|
|
|
const fullUrl = new URL(IVANTI_URL_BASE + urlPath);
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const options = {
|
|
|
|
|
hostname: fullUrl.hostname,
|
|
|
|
|
path: fullUrl.pathname + fullUrl.search,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'accept': '*/*',
|
|
|
|
|
'content-type': 'application/json',
|
|
|
|
|
'x-api-key': apiKey,
|
|
|
|
|
'x-http-client-type': 'browser',
|
|
|
|
|
'content-length': Buffer.byteLength(bodyStr)
|
|
|
|
|
},
|
|
|
|
|
rejectUnauthorized: !skipTls,
|
|
|
|
|
timeout: 15000
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const req = https.request(options, (res) => {
|
|
|
|
|
let data = '';
|
|
|
|
|
res.on('data', (chunk) => { data += chunk; });
|
|
|
|
|
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.on('timeout', () => req.destroy(new Error('Request timed out')));
|
|
|
|
|
req.on('error', reject);
|
|
|
|
|
req.write(bodyStr);
|
|
|
|
|
req.end();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Multipart POST — used for file attachment uploads.
|
|
|
|
|
// Constructs multipart/form-data manually using Node's https module.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function ivantiMultipartPost(urlPath, fileBuffer, fileName, apiKey, skipTls) {
|
|
|
|
|
const boundary = '----IvantiUpload' + Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
|
|
|
const fullUrl = new URL(IVANTI_URL_BASE + urlPath);
|
|
|
|
|
|
|
|
|
|
// Build multipart body
|
|
|
|
|
const preamble = Buffer.from(
|
|
|
|
|
`--${boundary}\r\n` +
|
|
|
|
|
`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
|
|
|
|
|
`Content-Type: application/octet-stream\r\n\r\n`
|
|
|
|
|
);
|
|
|
|
|
const epilogue = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
|
|
|
const bodyBuffer = Buffer.concat([preamble, fileBuffer, epilogue]);
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const options = {
|
|
|
|
|
hostname: fullUrl.hostname,
|
|
|
|
|
path: fullUrl.pathname + fullUrl.search,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'accept': '*/*',
|
|
|
|
|
'content-type': `multipart/form-data; boundary=${boundary}`,
|
|
|
|
|
'x-api-key': apiKey,
|
|
|
|
|
'x-http-client-type': 'browser',
|
|
|
|
|
'content-length': bodyBuffer.length
|
|
|
|
|
},
|
|
|
|
|
rejectUnauthorized: !skipTls,
|
|
|
|
|
timeout: 30000
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const req = https.request(options, (res) => {
|
|
|
|
|
let data = '';
|
|
|
|
|
res.on('data', (chunk) => { data += chunk; });
|
|
|
|
|
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.on('timeout', () => req.destroy(new Error('Request timed out')));
|
|
|
|
|
req.on('error', reject);
|
|
|
|
|
req.write(bodyBuffer);
|
|
|
|
|
req.end();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 10:18:45 -06:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Multipart form POST — used for endpoints that accept mixed form fields + files.
|
|
|
|
|
// fields: array of { name, value } for text form fields
|
|
|
|
|
// files: array of { name, buffer, filename } for file uploads
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function ivantiFormPost(urlPath, fields, files, apiKey, skipTls) {
|
|
|
|
|
const boundary = '----IvantiForm' + Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
|
|
|
const fullUrl = new URL(IVANTI_URL_BASE + urlPath);
|
|
|
|
|
|
|
|
|
|
const parts = [];
|
|
|
|
|
|
|
|
|
|
// Text fields
|
|
|
|
|
for (const { name, value } of fields) {
|
|
|
|
|
parts.push(Buffer.from(
|
|
|
|
|
`--${boundary}\r\n` +
|
|
|
|
|
`Content-Disposition: form-data; name="${name}"\r\n\r\n` +
|
|
|
|
|
`${value}\r\n`
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// File fields
|
2026-04-13 14:07:13 -06:00
|
|
|
for (const { name, buffer, filename, contentType } of files) {
|
2026-04-08 10:18:45 -06:00
|
|
|
parts.push(Buffer.from(
|
|
|
|
|
`--${boundary}\r\n` +
|
|
|
|
|
`Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n` +
|
2026-04-13 14:07:13 -06:00
|
|
|
`Content-Type: ${contentType || 'application/octet-stream'}\r\n\r\n`
|
2026-04-08 10:18:45 -06:00
|
|
|
));
|
|
|
|
|
parts.push(buffer);
|
|
|
|
|
parts.push(Buffer.from('\r\n'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
|
|
|
const bodyBuffer = Buffer.concat(parts);
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const options = {
|
|
|
|
|
hostname: fullUrl.hostname,
|
|
|
|
|
path: fullUrl.pathname + fullUrl.search,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'accept': '*/*',
|
|
|
|
|
'content-type': `multipart/form-data; boundary=${boundary}`,
|
|
|
|
|
'x-api-key': apiKey,
|
|
|
|
|
'x-http-client-type': 'browser',
|
|
|
|
|
'content-length': bodyBuffer.length
|
|
|
|
|
},
|
|
|
|
|
rejectUnauthorized: !skipTls,
|
|
|
|
|
timeout: 60000
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const req = https.request(options, (res) => {
|
|
|
|
|
let data = '';
|
|
|
|
|
res.on('data', (chunk) => { data += chunk; });
|
|
|
|
|
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.on('timeout', () => req.destroy(new Error('Request timed out')));
|
|
|
|
|
req.on('error', reject);
|
|
|
|
|
req.write(bodyBuffer);
|
|
|
|
|
req.end();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { IVANTI_URL_BASE, ivantiPost, ivantiMultipartPost, ivantiFormPost };
|