import type { IExecuteFunctions, IDataObject, ILoadOptionsFunctions, JsonObject, IRequestOptions, IHttpRequestMethods, } from 'n8n-workflow'; import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import { parseString } from 'xml2js'; import { SPLUNK, type SplunkCredentials, type SplunkError, type SplunkFeedResponse, type SplunkResultResponse, type SplunkSearchResponse, } from './types'; // ---------------------------------------- // entry formatting // ---------------------------------------- function compactEntryContent(splunkObject: any): any { if (typeof splunkObject !== 'object') { return {}; } if (Array.isArray(splunkObject)) { return splunkObject.reduce((acc, cur) => { acc = { ...acc, ...compactEntryContent(cur) }; return acc; }, {}); } if (splunkObject[SPLUNK.DICT]) { const obj = splunkObject[SPLUNK.DICT][SPLUNK.KEY]; return { [splunkObject.$.name]: compactEntryContent(obj) }; } if (splunkObject[SPLUNK.LIST]) { const items = splunkObject[SPLUNK.LIST][SPLUNK.ITEM]; return { [splunkObject.$.name]: items }; } if (splunkObject._) { return { [splunkObject.$.name]: splunkObject._, }; } return { [splunkObject.$.name]: '', }; } function formatEntryContent(content: any): any { return content[SPLUNK.DICT][SPLUNK.KEY].reduce((acc: any, cur: any) => { acc = { ...acc, ...compactEntryContent(cur) }; return acc; }, {}); } function formatEntry(entry: any): any { const { content, link, ...rest } = entry; const formattedEntry = { ...rest, ...formatEntryContent(content) }; if (formattedEntry.id) { formattedEntry.entryUrl = formattedEntry.id; formattedEntry.id = formattedEntry.id.split('/').pop(); } return formattedEntry; } // ---------------------------------------- // search formatting // ---------------------------------------- export function formatSearch(responseData: SplunkSearchResponse) { const { entry: entries } = responseData; if (!entries) return []; return Array.isArray(entries) ? entries.map(formatEntry) : [formatEntry(entries)]; } // ---------------------------------------- // utils // ---------------------------------------- export async function parseXml(xml: string) { return await new Promise((resolve, reject) => { parseString(xml, { explicitArray: false }, (error, result) => { error ? reject(error) : resolve(result); }); }); } export function extractErrorDescription(rawError: SplunkError) { const messages = rawError.response?.messages; return messages ? { [messages.msg.$.type.toLowerCase()]: messages.msg._ } : rawError; } export function toUnixEpoch(timestamp: string) { return Date.parse(timestamp) / 1000; } export async function splunkApiRequest( this: IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, body: IDataObject = {}, qs: IDataObject = {}, ): Promise { const { baseUrl, allowUnauthorizedCerts } = (await this.getCredentials( 'splunkApi', )) as SplunkCredentials; const options: IRequestOptions = { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, method, form: body, qs, uri: `${baseUrl}${endpoint}`, json: true, rejectUnauthorized: !allowUnauthorizedCerts, useQuerystring: true, // serialize roles array as `roles=A&roles=B` }; if (!Object.keys(body).length) { delete options.body; } if (!Object.keys(qs).length) { delete options.qs; } let result; try { let attempts = 0; do { try { const response = await this.helpers.requestWithAuthentication.call( this, 'splunkApi', options, ); result = await parseXml(response); return result; } catch (error) { if (attempts >= 5) { throw error; } await sleep(1000); attempts++; } } while (true); } catch (error) { if (result === undefined) { throw new NodeOperationError(this.getNode(), 'No response from API call', { description: "Try to use 'Retry On Fail' option from node's settings", }); } if (error?.cause?.code === 'ECONNREFUSED') { throw new NodeApiError(this.getNode(), { ...(error as JsonObject), code: 401 }); } const rawError = (await parseXml(error.error as string)) as SplunkError; error = extractErrorDescription(rawError); if ('fatal' in error) { error = { error: error.fatal }; } throw new NodeApiError(this.getNode(), error as JsonObject); } } // ---------------------------------------- // feed formatting // ---------------------------------------- export function formatFeed(responseData: SplunkFeedResponse) { const { entry: entries } = responseData.feed; if (!entries) return []; return Array.isArray(entries) ? entries.map(formatEntry) : [formatEntry(entries)]; } // ---------------------------------------- // result formatting // ---------------------------------------- function compactResult(splunkObject: any): any { if (typeof splunkObject !== 'object') { return {}; } if (Array.isArray(splunkObject?.value) && splunkObject?.value[0]?.text) { return { [splunkObject.$.k]: splunkObject.value.map((v: { text: string }) => v.text).join(','), }; } if (!splunkObject?.$?.k || !splunkObject?.value?.text) { return {}; } return { [splunkObject.$.k]: splunkObject.value.text, }; } function formatResult(field: any): any { return field.reduce((acc: any, cur: any) => { acc = { ...acc, ...compactResult(cur) }; return acc; }, {}); } export function formatResults(responseData: SplunkResultResponse) { const results = responseData.results.result; if (!results) return []; return Array.isArray(results) ? results.map((r) => formatResult(r.field)) : [formatResult(results.field)]; } // ---------------------------------------- // param loaders // ---------------------------------------- /** * Set count of entries to retrieve. */ export function setCount(this: IExecuteFunctions, qs: IDataObject) { qs.count = this.getNodeParameter('returnAll', 0) ? 0 : this.getNodeParameter('limit', 0); } export function populate(source: IDataObject, destination: IDataObject) { if (Object.keys(source).length) { Object.assign(destination, source); } } /** * Retrieve an ID, with tolerance when contained in an endpoint. * The field `id` in Splunk API responses is a full link. */ export function getId( this: IExecuteFunctions, i: number, idType: 'userId' | 'searchJobId' | 'searchConfigurationId', endpoint: string, ) { const id = this.getNodeParameter(idType, i) as string; return id.includes(endpoint) ? id.split(endpoint).pop() : id; }