From e5c324753fb41752f9722d61c5d336d6e5c67cca Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:07:17 +0300 Subject: [PATCH] feat(Splunk Node): Overhaul (#9813) --- .../credentials/SplunkApi.credentials.ts | 24 +- .../nodes-base/nodes/Splunk/Splunk.node.ts | 507 +---------------- packages/nodes-base/nodes/Splunk/splunk.svg | 515 +++++++++++++----- .../nodes/Splunk/test/v2/node/alert.test.ts | 37 ++ .../nodes/Splunk/test/v2/node/report.test.ts | 103 ++++ .../nodes/Splunk/test/v2/node/search.test.ts | 104 ++++ .../nodes/Splunk/test/v2/node/user.test.ts | 125 +++++ .../nodes/Splunk/test/v2/utils.test.ts | 267 +++++++++ .../nodes/Splunk/{ => v1}/GenericFunctions.ts | 35 +- .../nodes/Splunk/v1/SplunkV1.node.ts | 457 ++++++++++++++++ .../descriptions/FiredAlertDescription.ts | 0 .../SearchConfigurationDescription.ts | 0 .../descriptions/SearchJobDescription.ts | 0 .../descriptions/SearchResultDescription.ts | 0 .../{ => v1}/descriptions/UserDescription.ts | 0 .../Splunk/{ => v1}/descriptions/index.ts | 0 .../nodes-base/nodes/Splunk/{ => v1}/types.ts | 7 + .../nodes/Splunk/v2/SplunkV2.node.ts | 28 + .../v2/actions/alert/getMetrics.operation.ts | 24 + .../v2/actions/alert/getReport.operation.ts | 26 + .../nodes/Splunk/v2/actions/alert/index.ts | 38 ++ .../nodes/Splunk/v2/actions/node.type.ts | 10 + .../v2/actions/report/create.operation.ts | 52 ++ .../actions/report/deleteReport.operation.ts | 29 + .../Splunk/v2/actions/report/get.operation.ts | 29 + .../v2/actions/report/getAll.operation.ts | 79 +++ .../nodes/Splunk/v2/actions/report/index.ts | 54 ++ .../nodes/Splunk/v2/actions/router.ts | 74 +++ .../v2/actions/search/create.operation.ts | 255 +++++++++ .../v2/actions/search/deleteJob.operation.ts | 29 + .../Splunk/v2/actions/search/get.operation.ts | 29 + .../v2/actions/search/getAll.operation.ts | 123 +++++ .../v2/actions/search/getResult.operation.ts | 125 +++++ .../nodes/Splunk/v2/actions/search/index.ts | 62 +++ .../v2/actions/user/create.operation.ts | 99 ++++ .../v2/actions/user/deleteUser.operation.ts | 29 + .../Splunk/v2/actions/user/get.operation.ts | 29 + .../v2/actions/user/getAll.operation.ts | 53 ++ .../nodes/Splunk/v2/actions/user/index.ts | 62 +++ .../v2/actions/user/update.operation.ts | 86 +++ .../Splunk/v2/actions/versionDescription.ts | 60 ++ .../Splunk/v2/helpers/descriptions/index.ts | 1 + .../helpers/descriptions/rlc.description.ts | 79 +++ .../nodes/Splunk/v2/helpers/interfaces.ts | 24 + .../nodes/Splunk/v2/helpers/utils.ts | 107 ++++ .../nodes/Splunk/v2/methods/index.ts | 2 + .../nodes/Splunk/v2/methods/listSearch.ts | 74 +++ .../nodes/Splunk/v2/methods/loadOptions.ts | 13 + .../nodes/Splunk/v2/transport/index.ts | 153 ++++++ 49 files changed, 3484 insertions(+), 634 deletions(-) create mode 100644 packages/nodes-base/nodes/Splunk/test/v2/node/alert.test.ts create mode 100644 packages/nodes-base/nodes/Splunk/test/v2/node/report.test.ts create mode 100644 packages/nodes-base/nodes/Splunk/test/v2/node/search.test.ts create mode 100644 packages/nodes-base/nodes/Splunk/test/v2/node/user.test.ts create mode 100644 packages/nodes-base/nodes/Splunk/test/v2/utils.test.ts rename packages/nodes-base/nodes/Splunk/{ => v1}/GenericFunctions.ts (90%) create mode 100644 packages/nodes-base/nodes/Splunk/v1/SplunkV1.node.ts rename packages/nodes-base/nodes/Splunk/{ => v1}/descriptions/FiredAlertDescription.ts (100%) rename packages/nodes-base/nodes/Splunk/{ => v1}/descriptions/SearchConfigurationDescription.ts (100%) rename packages/nodes-base/nodes/Splunk/{ => v1}/descriptions/SearchJobDescription.ts (100%) rename packages/nodes-base/nodes/Splunk/{ => v1}/descriptions/SearchResultDescription.ts (100%) rename packages/nodes-base/nodes/Splunk/{ => v1}/descriptions/UserDescription.ts (100%) rename packages/nodes-base/nodes/Splunk/{ => v1}/descriptions/index.ts (100%) rename packages/nodes-base/nodes/Splunk/{ => v1}/types.ts (84%) create mode 100644 packages/nodes-base/nodes/Splunk/v2/SplunkV2.node.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/alert/getMetrics.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/alert/getReport.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/alert/index.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/node.type.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/report/create.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/report/deleteReport.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/report/get.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/report/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/report/index.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/router.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/search/create.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/search/deleteJob.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/search/get.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/search/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/search/getResult.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/search/index.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/user/create.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/user/deleteUser.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/user/get.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/user/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/user/index.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/user/update.operation.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/actions/versionDescription.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/rlc.description.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/helpers/interfaces.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/helpers/utils.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/methods/index.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/methods/listSearch.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/methods/loadOptions.ts create mode 100644 packages/nodes-base/nodes/Splunk/v2/transport/index.ts diff --git a/packages/nodes-base/credentials/SplunkApi.credentials.ts b/packages/nodes-base/credentials/SplunkApi.credentials.ts index b963eb61e1..cbedc042ee 100644 --- a/packages/nodes-base/credentials/SplunkApi.credentials.ts +++ b/packages/nodes-base/credentials/SplunkApi.credentials.ts @@ -1,4 +1,9 @@ -import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; export class SplunkApi implements ICredentialType { name = 'splunkApi'; @@ -31,4 +36,21 @@ export class SplunkApi implements ICredentialType { default: false, }, ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials?.authToken}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + url: '={{$credentials.baseUrl}}/services/alerts/fired_alerts', + method: 'GET', + skipSslCertificateValidation: '={{$credentials?.allowUnauthorizedCerts}}', + }, + }; } diff --git a/packages/nodes-base/nodes/Splunk/Splunk.node.ts b/packages/nodes-base/nodes/Splunk/Splunk.node.ts index c857418026..f8449708cf 100644 --- a/packages/nodes-base/nodes/Splunk/Splunk.node.ts +++ b/packages/nodes-base/nodes/Splunk/Splunk.node.ts @@ -1,486 +1,25 @@ -import { - type IExecuteFunctions, - type ICredentialsDecrypted, - type ICredentialTestFunctions, - type IDataObject, - type ILoadOptionsFunctions, - type INodeCredentialTestResult, - type INodeExecutionData, - type INodeType, - type INodeTypeDescription, - type IRequestOptions, - NodeApiError, - NodeOperationError, -} from 'n8n-workflow'; - -import set from 'lodash/set'; -import { - formatFeed, - formatResults, - formatSearch, - getId, - populate, - setCount, - splunkApiRequest, - toUnixEpoch, -} from './GenericFunctions'; - -import { - firedAlertOperations, - searchConfigurationFields, - searchConfigurationOperations, - searchJobFields, - searchJobOperations, - searchResultFields, - searchResultOperations, - userFields, - userOperations, -} from './descriptions'; - -import type { SplunkCredentials, SplunkFeedResponse } from './types'; - -export class Splunk implements INodeType { - description: INodeTypeDescription = { - displayName: 'Splunk', - name: 'splunk', - icon: 'file:splunk.svg', - group: ['transform'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume the Splunk Enterprise API', - defaults: { - name: 'Splunk', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'splunkApi', - required: true, - testedBy: 'splunkApiTest', - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Fired Alert', - value: 'firedAlert', - }, - { - name: 'Search Configuration', - value: 'searchConfiguration', - }, - { - name: 'Search Job', - value: 'searchJob', - }, - { - name: 'Search Result', - value: 'searchResult', - }, - { - name: 'User', - value: 'user', - }, - ], - default: 'searchJob', - }, - ...firedAlertOperations, - ...searchConfigurationOperations, - ...searchConfigurationFields, - ...searchJobOperations, - ...searchJobFields, - ...searchResultOperations, - ...searchResultFields, - ...userOperations, - ...userFields, - ], - }; - - methods = { - loadOptions: { - async getRoles(this: ILoadOptionsFunctions) { - const endpoint = '/services/authorization/roles'; - const responseData = (await splunkApiRequest.call( - this, - 'GET', - endpoint, - )) as SplunkFeedResponse; - const { entry: entries } = responseData.feed; - - return Array.isArray(entries) - ? entries.map((entry) => ({ name: entry.title, value: entry.title })) - : [{ name: entries.title, value: entries.title }]; - }, - }, - credentialTest: { - async splunkApiTest( - this: ICredentialTestFunctions, - credential: ICredentialsDecrypted, - ): Promise { - const { authToken, baseUrl, allowUnauthorizedCerts } = credential.data as SplunkCredentials; - - const endpoint = '/services/alerts/fired_alerts'; - - const options: IRequestOptions = { - headers: { - Authorization: `Bearer ${authToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - method: 'GET', - form: {}, - qs: {}, - uri: `${baseUrl}${endpoint}`, - json: true, - rejectUnauthorized: !allowUnauthorizedCerts, - }; - - try { - await this.helpers.request(options); - return { - status: 'OK', - message: 'Authentication successful', - }; - } catch (error) { - return { - status: 'Error', - message: error.message, - }; - } - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - - let responseData; - - for (let i = 0; i < items.length; i++) { - try { - if (resource === 'firedAlert') { - // ********************************************************************** - // firedAlert - // ********************************************************************** - - if (operation === 'getReport') { - // ---------------------------------------- - // firedAlert: getReport - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#alerts.2Ffired_alerts - - const endpoint = '/services/alerts/fired_alerts'; - responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatFeed); - } - } else if (resource === 'searchConfiguration') { - // ********************************************************************** - // searchConfiguration - // ********************************************************************** - - if (operation === 'delete') { - // ---------------------------------------- - // searchConfiguration: delete - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches.2F.7Bname.7D - - const partialEndpoint = '/services/saved/searches/'; - const searchConfigurationId = getId.call( - this, - i, - 'searchConfigurationId', - '/search/saved/searches/', - ); // id endpoint differs from operation endpoint - const endpoint = `${partialEndpoint}/${searchConfigurationId}`; - - responseData = await splunkApiRequest.call(this, 'DELETE', endpoint); - } else if (operation === 'get') { - // ---------------------------------------- - // searchConfiguration: get - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches.2F.7Bname.7D - - const partialEndpoint = '/services/saved/searches/'; - const searchConfigurationId = getId.call( - this, - i, - 'searchConfigurationId', - '/search/saved/searches/', - ); // id endpoint differs from operation endpoint - const endpoint = `${partialEndpoint}/${searchConfigurationId}`; - - responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatFeed); - } else if (operation === 'getAll') { - // ---------------------------------------- - // searchConfiguration: getAll - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches - - const qs = {} as IDataObject; - const options = this.getNodeParameter('options', i); - - populate(options, qs); - setCount.call(this, qs); - - const endpoint = '/services/saved/searches'; - responseData = await splunkApiRequest - .call(this, 'GET', endpoint, {}, qs) - .then(formatFeed); - } - } else if (resource === 'searchJob') { - // ********************************************************************** - // searchJob - // ********************************************************************** - - if (operation === 'create') { - // ---------------------------------------- - // searchJob: create - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs - - const body = { - search: this.getNodeParameter('search', i), - } as IDataObject; - - const { earliest_time, latest_time, index_earliest, index_latest, ...rest } = - this.getNodeParameter('additionalFields', i) as IDataObject & { - earliest_time?: string; - latest_time?: string; - index_earliest?: string; - index_latest?: string; - }; - - populate( - { - ...(earliest_time && { earliest_time: toUnixEpoch(earliest_time) }), - ...(latest_time && { latest_time: toUnixEpoch(latest_time) }), - ...(index_earliest && { index_earliest: toUnixEpoch(index_earliest) }), - ...(index_latest && { index_latest: toUnixEpoch(index_latest) }), - ...rest, - }, - body, - ); - - const endpoint = '/services/search/jobs'; - responseData = await splunkApiRequest.call(this, 'POST', endpoint, body); - - const getEndpoint = `/services/search/jobs/${responseData.response.sid}`; - responseData = await splunkApiRequest.call(this, 'GET', getEndpoint).then(formatSearch); - } else if (operation === 'delete') { - // ---------------------------------------- - // searchJob: delete - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D - - const partialEndpoint = '/services/search/jobs/'; - const searchJobId = getId.call(this, i, 'searchJobId', partialEndpoint); - const endpoint = `${partialEndpoint}/${searchJobId}`; - responseData = await splunkApiRequest.call(this, 'DELETE', endpoint); - } else if (operation === 'get') { - // ---------------------------------------- - // searchJob: get - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D - - const partialEndpoint = '/services/search/jobs/'; - const searchJobId = getId.call(this, i, 'searchJobId', partialEndpoint); - const endpoint = `${partialEndpoint}/${searchJobId}`; - responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatSearch); - } else if (operation === 'getAll') { - // ---------------------------------------- - // searchJob: getAll - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs - - const qs = {} as IDataObject; - const options = this.getNodeParameter('options', i); - - populate(options, qs); - setCount.call(this, qs); - - const endpoint = '/services/search/jobs'; - responseData = (await splunkApiRequest.call( - this, - 'GET', - endpoint, - {}, - qs, - )) as SplunkFeedResponse; - responseData = formatFeed(responseData); - } - } else if (resource === 'searchResult') { - // ********************************************************************** - // searchResult - // ********************************************************************** - - if (operation === 'getAll') { - // ---------------------------------------- - // searchResult: getAll - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D.2Fresults - - const searchJobId = this.getNodeParameter('searchJobId', i); - - const qs = {} as IDataObject; - const filters = this.getNodeParameter('filters', i) as IDataObject & { - keyValueMatch?: { keyValuePair?: { key: string; value: string } }; - }; - const options = this.getNodeParameter('options', i); - - const keyValuePair = filters?.keyValueMatch?.keyValuePair; - - if (keyValuePair?.key && keyValuePair?.value) { - qs.search = `search ${keyValuePair.key}=${keyValuePair.value}`; - } - - populate(options, qs); - setCount.call(this, qs); - - const endpoint = `/services/search/jobs/${searchJobId}/results`; - responseData = await splunkApiRequest - .call(this, 'GET', endpoint, {}, qs) - .then(formatResults); - } - } else if (resource === 'user') { - // ********************************************************************** - // user - // ********************************************************************** - - if (operation === 'create') { - // ---------------------------------------- - // user: create - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers - - const roles = this.getNodeParameter('roles', i) as string[]; - - const body = { - name: this.getNodeParameter('name', i), - roles, - password: this.getNodeParameter('password', i), - } as IDataObject; - - const additionalFields = this.getNodeParameter('additionalFields', i); - - populate(additionalFields, body); - - const endpoint = '/services/authentication/users'; - responseData = (await splunkApiRequest.call( - this, - 'POST', - endpoint, - body, - )) as SplunkFeedResponse; - responseData = formatFeed(responseData); - } else if (operation === 'delete') { - // ---------------------------------------- - // user: delete - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D - - const partialEndpoint = '/services/authentication/users'; - const userId = getId.call(this, i, 'userId', partialEndpoint); - const endpoint = `${partialEndpoint}/${userId}`; - await splunkApiRequest.call(this, 'DELETE', endpoint); - responseData = { success: true }; - } else if (operation === 'get') { - // ---------------------------------------- - // user: get - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D - - const partialEndpoint = '/services/authentication/users/'; - const userId = getId.call(this, i, 'userId', '/services/authentication/users/'); - const endpoint = `${partialEndpoint}/${userId}`; - responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatFeed); - } else if (operation === 'getAll') { - // ---------------------------------------- - // user: getAll - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers - - const qs = {} as IDataObject; - setCount.call(this, qs); - - const endpoint = '/services/authentication/users'; - responseData = await splunkApiRequest - .call(this, 'GET', endpoint, {}, qs) - .then(formatFeed); - } else if (operation === 'update') { - // ---------------------------------------- - // user: update - // ---------------------------------------- - - // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D - - const body = {} as IDataObject; - const { roles, ...rest } = this.getNodeParameter('updateFields', i) as IDataObject & { - roles: string[]; - }; - - populate( - { - ...(roles && { roles }), - ...rest, - }, - body, - ); - - const partialEndpoint = '/services/authentication/users/'; - const userId = getId.call(this, i, 'userId', partialEndpoint); - const endpoint = `${partialEndpoint}/${userId}`; - responseData = await splunkApiRequest - .call(this, 'POST', endpoint, body) - .then(formatFeed); - } - } - } catch (error) { - if (this.continueOnFail(error)) { - returnData.push({ json: { error: error.cause.error }, pairedItem: { item: i } }); - continue; - } - - if (error instanceof NodeApiError) { - set(error, 'context.itemIndex', i); - } - - if (error instanceof NodeOperationError && error?.context?.itemIndex === undefined) { - set(error, 'context.itemIndex', i); - } - - throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); - } - - if (Array.isArray(responseData)) { - for (const item of responseData) { - returnData.push({ json: item, pairedItem: { item: i } }); - } - } else { - returnData.push({ json: responseData, pairedItem: { item: i } }); - } - } - - return [returnData]; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { SplunkV1 } from './v1/SplunkV1.node'; +import { SplunkV2 } from './v2/SplunkV2.node'; + +export class Splunk extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Splunk', + name: 'splunk', + icon: 'file:splunk.svg', + group: ['transform'], + description: 'Consume the Splunk Enterprise API', + defaultVersion: 2, + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new SplunkV1(baseDescription), + 2: new SplunkV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Splunk/splunk.svg b/packages/nodes-base/nodes/Splunk/splunk.svg index 8eb25b4a26..1cd8bc1c40 100644 --- a/packages/nodes-base/nodes/Splunk/splunk.svg +++ b/packages/nodes-base/nodes/Splunk/splunk.svg @@ -1,134 +1,381 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/packages/nodes-base/nodes/Splunk/test/v2/node/alert.test.ts b/packages/nodes-base/nodes/Splunk/test/v2/node/alert.test.ts new file mode 100644 index 0000000000..5857e14cd4 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/test/v2/node/alert.test.ts @@ -0,0 +1,37 @@ +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions } from 'n8n-workflow'; +import * as alert from '../../../v2/actions/alert'; +import * as transport from '../../../v2/transport'; + +jest.mock('../../../v2/transport', () => ({ + splunkApiJsonRequest: jest.fn(), +})); + +describe('Splunk, alert resource', () => { + const response = [{ id: '123' }, { id: '345' }]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + test('getMetrics operation', async () => { + const executeFunctions = mock(); + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue(response); + const responseData = await alert.getMetrics.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/alerts/metric_alerts', + ); + expect(responseData).toEqual(response); + }); + + test('getReport operation', async () => { + const executeFunctions = mock(); + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue(response); + const responseData = await alert.getReport.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/alerts/fired_alerts', + ); + expect(responseData).toEqual(response); + }); +}); diff --git a/packages/nodes-base/nodes/Splunk/test/v2/node/report.test.ts b/packages/nodes-base/nodes/Splunk/test/v2/node/report.test.ts new file mode 100644 index 0000000000..118ca8dd60 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/test/v2/node/report.test.ts @@ -0,0 +1,103 @@ +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions } from 'n8n-workflow'; +import * as report from '../../../v2/actions/report'; +import * as transport from '../../../v2/transport'; +import { SPLUNK } from '../../../v1/types'; + +jest.mock('../../../v2/transport', () => ({ + splunkApiJsonRequest: jest.fn(), + splunkApiRequest: jest.fn(), +})); + +describe('Splunk, report resource', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('create operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('name', 0).mockReturnValue('someName'); + executeFunctions.getNodeParameter.calledWith('searchJobId', 0).mockReturnValue('12345'); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([ + { + id: '12345', + cronSchedule: '*/5 * * * *', + earliestTime: '2020-01-01T00:00:00.000Z', + latestTime: '2020-01-01T00:05:00.000Z', + isScheduled: true, + search: 'search index=_internal | stats count by source', + name: 'someName', + }, + ]); + + const response = { + feed: { + entry: [ + { + id: '1', + content: { [SPLUNK.DICT]: { [SPLUNK.KEY]: [{ $: { name: 'key1' }, _: 'value1' }] } }, + }, + ], + }, + }; + (transport.splunkApiRequest as jest.Mock).mockReturnValue(Promise.resolve(response)); + + const responseData = await report.create.execute.call(executeFunctions, 0); + + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/search/jobs/12345', + ); + expect(transport.splunkApiRequest).toHaveBeenCalledWith('POST', '/services/saved/searches', { + alert_type: 'always', + cron_schedule: '*/5 * * * *', + 'dispatch.earliest_time': '2020-01-01T00:00:00.000Z', + 'dispatch.latest_time': '2020-01-01T00:05:00.000Z', + is_scheduled: true, + name: 'someName', + search: 'search index=_internal | stats count by source', + }); + expect(responseData).toEqual([{ entryUrl: '1', id: '1', key1: 'value1' }]); + }); + + test('deleteReport operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.mockReturnValue('12345'); + (transport.splunkApiRequest as jest.Mock).mockReturnValue({}); + const responseData = await report.deleteReport.execute.call(executeFunctions, 0); + expect(transport.splunkApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/services/saved/searches/12345', + ); + expect(responseData).toEqual({ success: true }); + }); + + test('get operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('reportId', 0).mockReturnValue('12345'); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await report.get.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/saved/searches/12345', + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); + + test('getAll operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('options', 0).mockReturnValue({}); + executeFunctions.getNodeParameter.calledWith('returnAll', 0).mockReturnValue(true); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await report.getAll.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/saved/searches', + {}, + { count: 0 }, + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); +}); diff --git a/packages/nodes-base/nodes/Splunk/test/v2/node/search.test.ts b/packages/nodes-base/nodes/Splunk/test/v2/node/search.test.ts new file mode 100644 index 0000000000..d8e02ec86b --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/test/v2/node/search.test.ts @@ -0,0 +1,104 @@ +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions } from 'n8n-workflow'; +import * as search from '../../../v2/actions/search'; +import * as transport from '../../../v2/transport'; + +jest.mock('../../../v2/transport', () => ({ + splunkApiJsonRequest: jest.fn(), + splunkApiRequest: jest.fn(), +})); +describe('Splunk, search resource', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('create operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter + .calledWith('search', 0) + .mockReturnValue('search index=_internal | stats count by source'); + executeFunctions.getNodeParameter.calledWith('additionalFields', 0).mockReturnValue({ + earliest_time: '2020-01-01T00:00:00.000Z', + latest_time: '2020-01-01T00:05:00.000Z', + index_earliest: '2020-01-01T00:00:00.000Z', + index_latest: '2020-01-01T00:05:00.000Z', + }); + (transport.splunkApiRequest as jest.Mock).mockReturnValue({ response: { sid: '12345' } }); + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await search.create.execute.call(executeFunctions, 0); + expect(transport.splunkApiRequest).toHaveBeenCalledWith('POST', '/services/search/jobs', { + earliest_time: 1577836800, + index_earliest: 1577836800, + index_latest: 1577837100, + latest_time: 1577837100, + search: 'search index=_internal | stats count by source', + }); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/search/jobs/12345', + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); + + test('deleteJob operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.mockReturnValue('12345'); + (transport.splunkApiRequest as jest.Mock).mockReturnValue({}); + const responseData = await search.deleteJob.execute.call(executeFunctions, 0); + expect(transport.splunkApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/services/search/jobs/12345', + ); + expect(responseData).toEqual({ success: true }); + }); + + test('get operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('searchJobId', 0).mockReturnValue('12345'); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await search.get.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/search/jobs/12345', + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); + + test('getAll operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('sort.values', 0).mockReturnValue({}); + executeFunctions.getNodeParameter.calledWith('returnAll', 0).mockReturnValue(true); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await search.getAll.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/search/jobs', + {}, + { count: 0 }, + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); + + test('getResult operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('searchJobId', 0).mockReturnValue('12345'); + executeFunctions.getNodeParameter.calledWith('filters', 0).mockReturnValue({ + keyValueMatch: { keyValuePair: { key: 'key1', value: 'test1' } }, + }); + executeFunctions.getNodeParameter.calledWith('returnAll', 0).mockReturnValue(false); + executeFunctions.getNodeParameter.calledWith('limit', 0).mockReturnValue(10); + executeFunctions.getNodeParameter.calledWith('options', 0).mockReturnValue({}); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await search.getResult.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/search/jobs/12345/results', + {}, + { count: 10, search: 'search key1=test1' }, + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); +}); diff --git a/packages/nodes-base/nodes/Splunk/test/v2/node/user.test.ts b/packages/nodes-base/nodes/Splunk/test/v2/node/user.test.ts new file mode 100644 index 0000000000..c33a38dc56 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/test/v2/node/user.test.ts @@ -0,0 +1,125 @@ +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions } from 'n8n-workflow'; +import * as user from '../../../v2/actions/user'; +import * as transport from '../../../v2/transport'; +import { SPLUNK } from '../../../v1/types'; + +jest.mock('../../../v2/transport', () => ({ + splunkApiJsonRequest: jest.fn(), + splunkApiRequest: jest.fn(), +})); +describe('Splunk, user resource', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('create operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('roles', 0).mockReturnValue(['role1', 'role2']); + executeFunctions.getNodeParameter.calledWith('name', 0).mockReturnValue('John Doe'); + executeFunctions.getNodeParameter.calledWith('password', 0).mockReturnValue('password'); + executeFunctions.getNodeParameter.calledWith('additionalFields', 0).mockReturnValue({}); + + (transport.splunkApiRequest as jest.Mock).mockReturnValue({ + feed: { + entry: [ + { + id: '1', + content: { [SPLUNK.DICT]: { [SPLUNK.KEY]: [{ $: { name: 'test' }, _: 'test1' }] } }, + }, + ], + }, + }); + + const responseData = await user.create.execute.call(executeFunctions, 0); + expect(transport.splunkApiRequest).toHaveBeenCalledWith( + 'POST', + '/services/authentication/users', + { name: 'John Doe', password: 'password', roles: ['role1', 'role2'] }, + ); + + expect(responseData).toEqual([ + { + id: '1', + test: 'test1', + entryUrl: '1', + }, + ]); + }); + + test('deleteUser operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.mockReturnValue('12345'); + (transport.splunkApiRequest as jest.Mock).mockReturnValue({}); + const responseData = await user.deleteUser.execute.call(executeFunctions, 0); + expect(transport.splunkApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/services/authentication/users/12345', + ); + expect(responseData).toEqual({ success: true }); + }); + + test('get operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('userId', 0).mockReturnValue('12345'); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await user.get.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/authentication/users/12345', + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); + + test('getAll operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter.calledWith('returnAll', 0).mockReturnValue(true); + + (transport.splunkApiJsonRequest as jest.Mock).mockReturnValue([{ test: 'test' }]); + const responseData = await user.getAll.execute.call(executeFunctions, 0); + expect(transport.splunkApiJsonRequest).toHaveBeenCalledWith( + 'GET', + '/services/authentication/users', + {}, + { count: 0 }, + ); + expect(responseData).toEqual([{ test: 'test' }]); + }); + + test('update operation', async () => { + const executeFunctions = mock(); + executeFunctions.getNodeParameter + .calledWith('updateFields', 0) + .mockReturnValue({ roles: ['role1', 'role2'], email: 'testW@example.com' }); + executeFunctions.getNodeParameter.calledWith('userId', 0).mockReturnValue('12345'); + + (transport.splunkApiRequest as jest.Mock).mockReturnValue( + Promise.resolve({ + feed: { + entry: [ + { + id: '1', + content: { [SPLUNK.DICT]: { [SPLUNK.KEY]: [{ $: { name: 'test' }, _: 'test1' }] } }, + }, + ], + }, + }), + ); + + const responseData = await user.update.execute.call(executeFunctions, 0); + expect(transport.splunkApiRequest).toHaveBeenCalledWith( + 'POST', + '/services/authentication/users/12345', + { email: 'testW@example.com', roles: ['role1', 'role2'] }, + ); + + expect(responseData).toEqual([ + { + id: '1', + test: 'test1', + entryUrl: '1', + }, + ]); + }); +}); diff --git a/packages/nodes-base/nodes/Splunk/test/v2/utils.test.ts b/packages/nodes-base/nodes/Splunk/test/v2/utils.test.ts new file mode 100644 index 0000000000..425c52daab --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/test/v2/utils.test.ts @@ -0,0 +1,267 @@ +import { mock } from 'jest-mock-extended'; +import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; +import { + formatEntry, + extractErrorDescription, + getId, + populate, + formatFeed, + setReturnAllOrLimit, + parseXml, +} from '../../v2/helpers/utils'; +import { SPLUNK } from '../../v1/types'; + +describe('Splunk, formatEntry', () => { + test('should format the entry correctly when doNotFormatContent is false', () => { + const entry = { + id: 'http://example.com/id/123', + content: { + [SPLUNK.DICT]: { + [SPLUNK.KEY]: [ + { $: { name: 'key1' }, _: 'value1' }, + { $: { name: 'key2' }, _: 'value2' }, + ], + }, + }, + link: 'http://example.com/link', + otherField: 'otherValue', + }; + + const expectedFormattedEntry = { + otherField: 'otherValue', + key1: 'value1', + key2: 'value2', + entryUrl: 'http://example.com/id/123', + id: '123', + }; + + const result = formatEntry(entry); + expect(result).toEqual(expectedFormattedEntry); + }); + + test('should format the entry correctly when doNotFormatContent is true', () => { + const entry = { + id: 'http://example.com/id/123', + key1: 'value1', + key2: 'value2', + }; + + const expectedFormattedEntry = { + key1: 'value1', + key2: 'value2', + entryUrl: 'http://example.com/id/123', + id: '123', + }; + + const result = formatEntry(entry, true); + expect(result).toEqual(expectedFormattedEntry); + }); + + test('should handle entries without id correctly', () => { + const entry = { + content: { + [SPLUNK.DICT]: { + [SPLUNK.KEY]: [ + { $: { name: 'key1' }, _: 'value1' }, + { $: { name: 'key2' }, _: 'value2' }, + ], + }, + }, + otherField: 'otherValue', + }; + + const expectedFormattedEntry = { + otherField: 'otherValue', + key1: 'value1', + key2: 'value2', + }; + + const result = formatEntry(entry); + expect(result).toEqual(expectedFormattedEntry); + }); +}); + +describe('Splunk, extractErrorDescription', () => { + test('should extract the error description correctly when messages are present', () => { + const rawError = { + response: { + messages: { + msg: { + $: { type: 'ERROR' }, + _: 'This is an error message', + }, + }, + }, + }; + + const expectedErrorDescription = { + error: 'This is an error message', + }; + + const result = extractErrorDescription(rawError); + expect(result).toEqual(expectedErrorDescription); + }); + + test('should return the raw error when messages are not present', () => { + const rawError = { response: {} }; + + const result = extractErrorDescription(rawError); + expect(result).toEqual(rawError); + }); + + test('should return the raw error when response is not present', () => { + const rawError = {}; + + const result = extractErrorDescription(rawError); + expect(result).toEqual(rawError); + }); +}); + +describe('Splunk, getId', () => { + test('should return id extracted from the id parameter if it is url', () => { + const executeFunctionsMock = mock(); + const endpoint = 'http://example.com/endpoint/admin'; + + executeFunctionsMock.getNodeParameter.mockReturnValueOnce(endpoint); + const id = getId.call(executeFunctionsMock, 0, 'userId', 'http://example.com/endpoint/'); + + expect(id).toBe('admin'); + }); + + test('should return the unchanged id', () => { + const executeFunctionsMock = mock(); + + executeFunctionsMock.getNodeParameter.mockReturnValueOnce('123'); + const id = getId.call(executeFunctionsMock, 0, 'searchConfigurationId', 'endpoint'); + + expect(id).toBe('123'); + }); +}); + +describe('Splunk, populate', () => { + test('should populate destination object with source object properties', () => { + const source = { + key1: 'value1', + key2: 'value2', + }; + + const destination = { + existingKey: 'existingValue', + }; + + populate(source, destination); + + expect(destination).toEqual({ + existingKey: 'existingValue', + key1: 'value1', + key2: 'value2', + }); + }); + + test('should not modify destination object if source object is empty', () => { + const source = {}; + + const destination = { + existingKey: 'existingValue', + }; + + populate(source, destination); + + expect(destination).toEqual({ + existingKey: 'existingValue', + }); + }); +}); + +describe('Splunk, formatFeed', () => { + test('should return an empty array when feed entries are not present', () => { + const responseData = { + feed: { + entry: [], + }, + }; + + const result = formatFeed(responseData); + expect(result).toEqual([]); + }); + + test('should format feed entries correctly when entries are an array', () => { + const responseData = { + feed: { + entry: [ + { + id: '1', + content: { [SPLUNK.DICT]: { [SPLUNK.KEY]: [{ $: { name: 'key1' }, _: 'value1' }] } }, + }, + { + id: '2', + content: { [SPLUNK.DICT]: { [SPLUNK.KEY]: [{ $: { name: 'key2' }, _: 'value2' }] } }, + }, + ], + }, + }; + + const expectedFormattedEntries = [ + { id: '1', key1: 'value1', entryUrl: '1' }, + { id: '2', key2: 'value2', entryUrl: '2' }, + ]; + + const result = formatFeed(responseData); + expect(result).toEqual(expectedFormattedEntries); + }); + + test('should format feed entry correctly when entry is a single object', () => { + const responseData = { + feed: { + entry: { + id: '1', + content: { [SPLUNK.DICT]: { [SPLUNK.KEY]: [{ $: { name: 'key1' }, _: 'value1' }] } }, + }, + }, + }; + + const expectedFormattedEntries = [{ id: '1', key1: 'value1', entryUrl: '1' }]; + + const result = formatFeed(responseData); + expect(result).toEqual(expectedFormattedEntries); + }); +}); + +describe('Splunk, setCount', () => { + test('should set count to 0 if returnAll', () => { + const executeFunctionsMock = mock(); + const qs: IDataObject = {}; + executeFunctionsMock.getNodeParameter.calledWith('returnAll', 0).mockReturnValue(true); + + setReturnAllOrLimit.call(executeFunctionsMock, qs); + + expect(qs.count).toBe(0); + }); + + test('should set count to limit if returnAll is false', () => { + const executeFunctionsMock = mock(); + const qs: IDataObject = {}; + executeFunctionsMock.getNodeParameter.calledWith('returnAll', 0).mockReturnValue(false); + executeFunctionsMock.getNodeParameter.calledWith('limit', 0).mockReturnValue(10); + + setReturnAllOrLimit.call(executeFunctionsMock, qs); + + expect(qs.count).toBe(10); + }); +}); + +describe('Splunk, parseXml', () => { + test('should parse valid XML string correctly', async () => { + const xmlString = 'John30'; + + const result = await parseXml(xmlString); + + expect(result).toEqual({ root: { name: 'John', age: '30' } }); + }); + + test('should throw an error if XML string is invalid', async () => { + const xmlString = ''; + + await expect(parseXml(xmlString)).rejects.toThrow(); + }); +}); diff --git a/packages/nodes-base/nodes/Splunk/GenericFunctions.ts b/packages/nodes-base/nodes/Splunk/v1/GenericFunctions.ts similarity index 90% rename from packages/nodes-base/nodes/Splunk/GenericFunctions.ts rename to packages/nodes-base/nodes/Splunk/v1/GenericFunctions.ts index 5d463efaf5..3c329f2ca5 100644 --- a/packages/nodes-base/nodes/Splunk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Splunk/v1/GenericFunctions.ts @@ -10,17 +10,19 @@ import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import { parseString } from 'xml2js'; -import type { - SplunkCredentials, - SplunkError, - SplunkFeedResponse, - SplunkResultResponse, - SplunkSearchResponse, +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 {}; @@ -33,13 +35,13 @@ function compactEntryContent(splunkObject: any): any { }, {}); } - if (splunkObject['s:dict']) { - const obj = splunkObject['s:dict']['s:key']; + if (splunkObject[SPLUNK.DICT]) { + const obj = splunkObject[SPLUNK.DICT][SPLUNK.KEY]; return { [splunkObject.$.name]: compactEntryContent(obj) }; } - if (splunkObject['s:list']) { - const items = splunkObject['s:list']['s:item']; + if (splunkObject[SPLUNK.LIST]) { + const items = splunkObject[SPLUNK.LIST][SPLUNK.ITEM]; return { [splunkObject.$.name]: items }; } @@ -55,7 +57,7 @@ function compactEntryContent(splunkObject: any): any { } function formatEntryContent(content: any): any { - return content['s:dict']['s:key'].reduce((acc: any, cur: any) => { + return content[SPLUNK.DICT][SPLUNK.KEY].reduce((acc: any, cur: any) => { acc = { ...acc, ...compactEntryContent(cur) }; return acc; }, {}); @@ -113,13 +115,12 @@ export async function splunkApiRequest( body: IDataObject = {}, qs: IDataObject = {}, ): Promise { - const { authToken, baseUrl, allowUnauthorizedCerts } = (await this.getCredentials( + const { baseUrl, allowUnauthorizedCerts } = (await this.getCredentials( 'splunkApi', )) as SplunkCredentials; const options: IRequestOptions = { headers: { - Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, method, @@ -145,7 +146,11 @@ export async function splunkApiRequest( do { try { - const response = await this.helpers.request(options); + const response = await this.helpers.requestWithAuthentication.call( + this, + 'splunkApi', + options, + ); result = await parseXml(response); return result; } catch (error) { @@ -257,5 +262,5 @@ export function getId( ) { const id = this.getNodeParameter(idType, i) as string; - return id.includes(endpoint) ? id.split(endpoint).pop()! : id; + return id.includes(endpoint) ? id.split(endpoint).pop() : id; } diff --git a/packages/nodes-base/nodes/Splunk/v1/SplunkV1.node.ts b/packages/nodes-base/nodes/Splunk/v1/SplunkV1.node.ts new file mode 100644 index 0000000000..048b8b2f37 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v1/SplunkV1.node.ts @@ -0,0 +1,457 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IExecuteFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; + +import { + formatFeed, + formatResults, + formatSearch, + getId, + populate, + setCount, + splunkApiRequest, + toUnixEpoch, +} from './GenericFunctions'; + +import { + firedAlertOperations, + searchConfigurationFields, + searchConfigurationOperations, + searchJobFields, + searchJobOperations, + searchResultFields, + searchResultOperations, + userFields, + userOperations, +} from './descriptions'; + +import type { SplunkFeedResponse } from './types'; +import set from 'lodash/set'; +import { oldVersionNotice } from '../../../utils/descriptions'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Splunk', + name: 'splunk', + icon: 'file:splunk.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Splunk Enterprise API', + defaults: { + name: 'Splunk', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'splunkApi', + required: true, + }, + ], + properties: [ + oldVersionNotice, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Fired Alert', + value: 'firedAlert', + }, + { + name: 'Search Configuration', + value: 'searchConfiguration', + }, + { + name: 'Search Job', + value: 'searchJob', + }, + { + name: 'Search Result', + value: 'searchResult', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'searchJob', + }, + ...firedAlertOperations, + ...searchConfigurationOperations, + ...searchConfigurationFields, + ...searchJobOperations, + ...searchJobFields, + ...searchResultOperations, + ...searchResultFields, + ...userOperations, + ...userFields, + ], +}; + +export class SplunkV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + async getRoles(this: ILoadOptionsFunctions) { + const endpoint = '/services/authorization/roles'; + const responseData = (await splunkApiRequest.call( + this, + 'GET', + endpoint, + )) as SplunkFeedResponse; + const { entry: entries } = responseData.feed; + + return Array.isArray(entries) + ? entries.map((entry) => ({ name: entry.title, value: entry.title })) + : [{ name: entries.title, value: entries.title }]; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + let responseData; + + for (let i = 0; i < items.length; i++) { + try { + if (resource === 'firedAlert') { + // ********************************************************************** + // firedAlert + // ********************************************************************** + + if (operation === 'getReport') { + // ---------------------------------------- + // firedAlert: getReport + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#alerts.2Ffired_alerts + + const endpoint = '/services/alerts/fired_alerts'; + responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatFeed); + } + } else if (resource === 'searchConfiguration') { + // ********************************************************************** + // searchConfiguration + // ********************************************************************** + + if (operation === 'delete') { + // ---------------------------------------- + // searchConfiguration: delete + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches.2F.7Bname.7D + + const partialEndpoint = '/services/saved/searches/'; + const searchConfigurationId = getId.call( + this, + i, + 'searchConfigurationId', + '/search/saved/searches/', + ); // id endpoint differs from operation endpoint + const endpoint = `${partialEndpoint}/${searchConfigurationId}`; + + responseData = await splunkApiRequest.call(this, 'DELETE', endpoint); + } else if (operation === 'get') { + // ---------------------------------------- + // searchConfiguration: get + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches.2F.7Bname.7D + + const partialEndpoint = '/services/saved/searches/'; + const searchConfigurationId = getId.call( + this, + i, + 'searchConfigurationId', + '/search/saved/searches/', + ); // id endpoint differs from operation endpoint + const endpoint = `${partialEndpoint}/${searchConfigurationId}`; + + responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatFeed); + } else if (operation === 'getAll') { + // ---------------------------------------- + // searchConfiguration: getAll + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches + + const qs = {} as IDataObject; + const options = this.getNodeParameter('options', i); + + populate(options, qs); + setCount.call(this, qs); + + const endpoint = '/services/saved/searches'; + responseData = await splunkApiRequest + .call(this, 'GET', endpoint, {}, qs) + .then(formatFeed); + } + } else if (resource === 'searchJob') { + // ********************************************************************** + // searchJob + // ********************************************************************** + + if (operation === 'create') { + // ---------------------------------------- + // searchJob: create + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs + + const body = { + search: this.getNodeParameter('search', i), + } as IDataObject; + + const { earliest_time, latest_time, index_earliest, index_latest, ...rest } = + this.getNodeParameter('additionalFields', i) as IDataObject & { + earliest_time?: string; + latest_time?: string; + index_earliest?: string; + index_latest?: string; + }; + + populate( + { + ...(earliest_time && { earliest_time: toUnixEpoch(earliest_time) }), + ...(latest_time && { latest_time: toUnixEpoch(latest_time) }), + ...(index_earliest && { index_earliest: toUnixEpoch(index_earliest) }), + ...(index_latest && { index_latest: toUnixEpoch(index_latest) }), + ...rest, + }, + body, + ); + + const endpoint = '/services/search/jobs'; + responseData = await splunkApiRequest.call(this, 'POST', endpoint, body); + + const getEndpoint = `/services/search/jobs/${responseData.response.sid}`; + responseData = await splunkApiRequest.call(this, 'GET', getEndpoint).then(formatSearch); + } else if (operation === 'delete') { + // ---------------------------------------- + // searchJob: delete + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D + + const partialEndpoint = '/services/search/jobs/'; + const searchJobId = getId.call(this, i, 'searchJobId', partialEndpoint); + const endpoint = `${partialEndpoint}/${searchJobId}`; + responseData = await splunkApiRequest.call(this, 'DELETE', endpoint); + } else if (operation === 'get') { + // ---------------------------------------- + // searchJob: get + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D + + const partialEndpoint = '/services/search/jobs/'; + const searchJobId = getId.call(this, i, 'searchJobId', partialEndpoint); + const endpoint = `${partialEndpoint}/${searchJobId}`; + responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatSearch); + } else if (operation === 'getAll') { + // ---------------------------------------- + // searchJob: getAll + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs + + const qs = {} as IDataObject; + const options = this.getNodeParameter('options', i); + + populate(options, qs); + setCount.call(this, qs); + + const endpoint = '/services/search/jobs'; + responseData = (await splunkApiRequest.call( + this, + 'GET', + endpoint, + {}, + qs, + )) as SplunkFeedResponse; + responseData = formatFeed(responseData); + } + } else if (resource === 'searchResult') { + // ********************************************************************** + // searchResult + // ********************************************************************** + + if (operation === 'getAll') { + // ---------------------------------------- + // searchResult: getAll + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D.2Fresults + + const searchJobId = this.getNodeParameter('searchJobId', i); + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i) as IDataObject & { + keyValueMatch?: { keyValuePair?: { key: string; value: string } }; + }; + const options = this.getNodeParameter('options', i); + + const keyValuePair = filters?.keyValueMatch?.keyValuePair; + + if (keyValuePair?.key && keyValuePair?.value) { + qs.search = `search ${keyValuePair.key}=${keyValuePair.value}`; + } + + populate(options, qs); + setCount.call(this, qs); + + const endpoint = `/services/search/jobs/${searchJobId}/results`; + responseData = await splunkApiRequest + .call(this, 'GET', endpoint, {}, qs) + .then(formatResults); + } + } else if (resource === 'user') { + // ********************************************************************** + // user + // ********************************************************************** + + if (operation === 'create') { + // ---------------------------------------- + // user: create + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers + + const roles = this.getNodeParameter('roles', i) as string[]; + + const body = { + name: this.getNodeParameter('name', i), + roles, + password: this.getNodeParameter('password', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + populate(additionalFields, body); + + const endpoint = '/services/authentication/users'; + responseData = (await splunkApiRequest.call( + this, + 'POST', + endpoint, + body, + )) as SplunkFeedResponse; + responseData = formatFeed(responseData); + } else if (operation === 'delete') { + // ---------------------------------------- + // user: delete + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D + + const partialEndpoint = '/services/authentication/users'; + const userId = getId.call(this, i, 'userId', partialEndpoint); + const endpoint = `${partialEndpoint}/${userId}`; + await splunkApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + } else if (operation === 'get') { + // ---------------------------------------- + // user: get + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D + + const partialEndpoint = '/services/authentication/users/'; + const userId = getId.call(this, i, 'userId', '/services/authentication/users/'); + const endpoint = `${partialEndpoint}/${userId}`; + responseData = await splunkApiRequest.call(this, 'GET', endpoint).then(formatFeed); + } else if (operation === 'getAll') { + // ---------------------------------------- + // user: getAll + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers + + const qs = {} as IDataObject; + setCount.call(this, qs); + + const endpoint = '/services/authentication/users'; + responseData = await splunkApiRequest + .call(this, 'GET', endpoint, {}, qs) + .then(formatFeed); + } else if (operation === 'update') { + // ---------------------------------------- + // user: update + // ---------------------------------------- + + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D + + const body = {} as IDataObject; + const { roles, ...rest } = this.getNodeParameter('updateFields', i) as IDataObject & { + roles: string[]; + }; + + populate( + { + ...(roles && { roles }), + ...rest, + }, + body, + ); + + const partialEndpoint = '/services/authentication/users/'; + const userId = getId.call(this, i, 'userId', partialEndpoint); + const endpoint = `${partialEndpoint}/${userId}`; + responseData = await splunkApiRequest + .call(this, 'POST', endpoint, body) + .then(formatFeed); + } + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.cause.error }, pairedItem: { item: i } }); + continue; + } + + if (error instanceof NodeApiError) { + set(error, 'context.itemIndex', i); + } + + if (error instanceof NodeOperationError && error?.context?.itemIndex === undefined) { + set(error, 'context.itemIndex', i); + } + + throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); + } + + if (Array.isArray(responseData)) { + for (const item of responseData) { + returnData.push({ json: item, pairedItem: { item: i } }); + } + } else { + returnData.push({ json: responseData, pairedItem: { item: i } }); + } + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Splunk/descriptions/FiredAlertDescription.ts b/packages/nodes-base/nodes/Splunk/v1/descriptions/FiredAlertDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Splunk/descriptions/FiredAlertDescription.ts rename to packages/nodes-base/nodes/Splunk/v1/descriptions/FiredAlertDescription.ts diff --git a/packages/nodes-base/nodes/Splunk/descriptions/SearchConfigurationDescription.ts b/packages/nodes-base/nodes/Splunk/v1/descriptions/SearchConfigurationDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Splunk/descriptions/SearchConfigurationDescription.ts rename to packages/nodes-base/nodes/Splunk/v1/descriptions/SearchConfigurationDescription.ts diff --git a/packages/nodes-base/nodes/Splunk/descriptions/SearchJobDescription.ts b/packages/nodes-base/nodes/Splunk/v1/descriptions/SearchJobDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Splunk/descriptions/SearchJobDescription.ts rename to packages/nodes-base/nodes/Splunk/v1/descriptions/SearchJobDescription.ts diff --git a/packages/nodes-base/nodes/Splunk/descriptions/SearchResultDescription.ts b/packages/nodes-base/nodes/Splunk/v1/descriptions/SearchResultDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Splunk/descriptions/SearchResultDescription.ts rename to packages/nodes-base/nodes/Splunk/v1/descriptions/SearchResultDescription.ts diff --git a/packages/nodes-base/nodes/Splunk/descriptions/UserDescription.ts b/packages/nodes-base/nodes/Splunk/v1/descriptions/UserDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Splunk/descriptions/UserDescription.ts rename to packages/nodes-base/nodes/Splunk/v1/descriptions/UserDescription.ts diff --git a/packages/nodes-base/nodes/Splunk/descriptions/index.ts b/packages/nodes-base/nodes/Splunk/v1/descriptions/index.ts similarity index 100% rename from packages/nodes-base/nodes/Splunk/descriptions/index.ts rename to packages/nodes-base/nodes/Splunk/v1/descriptions/index.ts diff --git a/packages/nodes-base/nodes/Splunk/types.ts b/packages/nodes-base/nodes/Splunk/v1/types.ts similarity index 84% rename from packages/nodes-base/nodes/Splunk/types.ts rename to packages/nodes-base/nodes/Splunk/v1/types.ts index 61e87490ca..091812a265 100644 --- a/packages/nodes-base/nodes/Splunk/types.ts +++ b/packages/nodes-base/nodes/Splunk/v1/types.ts @@ -28,3 +28,10 @@ export type SplunkError = { }; }; }; + +export const SPLUNK = { + DICT: 's:dict', + LIST: 's:list', + ITEM: 's:item', + KEY: 's:key', +}; diff --git a/packages/nodes-base/nodes/Splunk/v2/SplunkV2.node.ts b/packages/nodes-base/nodes/Splunk/v2/SplunkV2.node.ts new file mode 100644 index 0000000000..0baaed39e6 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/SplunkV2.node.ts @@ -0,0 +1,28 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IExecuteFunctions, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; + +import { router } from './actions/router'; +import { versionDescription } from './actions/versionDescription'; +import { loadOptions, listSearch } from './methods'; + +export class SplunkV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { loadOptions, listSearch }; + + async execute(this: IExecuteFunctions) { + return await router.call(this); + } +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/alert/getMetrics.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/alert/getMetrics.operation.ts new file mode 100644 index 0000000000..aef07773c3 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/alert/getMetrics.operation.ts @@ -0,0 +1,24 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; + +const properties: INodeProperties[] = []; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['getMetrics'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + _i: number, +): Promise { + const endpoint = '/services/alerts/metric_alerts'; + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/alert/getReport.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/alert/getReport.operation.ts new file mode 100644 index 0000000000..b76a3506d5 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/alert/getReport.operation.ts @@ -0,0 +1,26 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; + +const properties: INodeProperties[] = []; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['getReport'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + _i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#alerts.2Ffired_alerts + + const endpoint = '/services/alerts/fired_alerts'; + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/alert/index.ts b/packages/nodes-base/nodes/Splunk/v2/actions/alert/index.ts new file mode 100644 index 0000000000..8d3a1f3342 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/alert/index.ts @@ -0,0 +1,38 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as getReport from './getReport.operation'; +import * as getMetrics from './getMetrics.operation'; + +export { getReport, getMetrics }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['alert'], + }, + }, + options: [ + { + name: 'Get Fired Alerts', + value: 'getReport', + description: 'Retrieve a fired alerts report', + action: 'Get a fired alerts report', + }, + { + name: 'Get Metrics', + value: 'getMetrics', + description: 'Retrieve metrics', + action: 'Get metrics', + }, + ], + default: 'getReport', + }, + + ...getReport.description, + ...getMetrics.description, +]; diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/node.type.ts b/packages/nodes-base/nodes/Splunk/v2/actions/node.type.ts new file mode 100644 index 0000000000..109c72cdff --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/node.type.ts @@ -0,0 +1,10 @@ +import type { AllEntities } from 'n8n-workflow'; + +type NodeMap = { + alert: 'getReport' | 'getMetrics'; + report: 'create' | 'deleteReport' | 'get' | 'getAll'; + search: 'create' | 'deleteJob' | 'get' | 'getAll' | 'getResult'; + user: 'create' | 'deleteUser' | 'get' | 'getAll' | 'update'; +}; + +export type SplunkType = AllEntities; diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/report/create.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/report/create.operation.ts new file mode 100644 index 0000000000..1f3b59e273 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/report/create.operation.ts @@ -0,0 +1,52 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest, splunkApiRequest } from '../../transport'; +import { searchJobRLC } from '../../helpers/descriptions'; +import { formatFeed } from '../../helpers/utils'; + +const properties: INodeProperties[] = [ + searchJobRLC, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the report', + }, +]; + +const displayOptions = { + show: { + resource: ['report'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + const name = this.getNodeParameter('name', i) as string; + const searchJobId = this.getNodeParameter('searchJobId', i, '', { extractValue: true }) as string; + const endpoint = `/services/search/jobs/${searchJobId}`; + + const searchJob = ((await splunkApiJsonRequest.call(this, 'GET', endpoint)) ?? [])[0]; + + const body: IDataObject = { + name, + search: searchJob.search, + alert_type: 'always', + 'dispatch.earliest_time': searchJob.earliestTime, + 'dispatch.latest_time': searchJob.latestTime, + is_scheduled: searchJob.isScheduled, + cron_schedule: searchJob.cronSchedule, + }; + + const returnData = await splunkApiRequest + .call(this, 'POST', '/services/saved/searches', body) + .then(formatFeed); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/report/deleteReport.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/report/deleteReport.operation.ts new file mode 100644 index 0000000000..41aad443d1 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/report/deleteReport.operation.ts @@ -0,0 +1,29 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiRequest } from '../../transport'; +import { reportRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [reportRLC]; + +const displayOptions = { + show: { + resource: ['report'], + operation: ['deleteReport'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches.2F.7Bname.7D + + const reportId = this.getNodeParameter('reportId', i, '', { extractValue: true }) as string; + const endpoint = `/services/saved/searches/${reportId}`; + + await splunkApiRequest.call(this, 'DELETE', endpoint); + + return { success: true }; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/report/get.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/report/get.operation.ts new file mode 100644 index 0000000000..32972c08f3 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/report/get.operation.ts @@ -0,0 +1,29 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; +import { reportRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [reportRLC]; + +const displayOptions = { + show: { + resource: ['report'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches.2F.7Bname.7D + + const reportId = this.getNodeParameter('reportId', i, '', { extractValue: true }) as string; + const endpoint = `/services/saved/searches/${reportId}`; + + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/report/getAll.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/report/getAll.operation.ts new file mode 100644 index 0000000000..9f9ff44e27 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/report/getAll.operation.ts @@ -0,0 +1,79 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { populate, setReturnAllOrLimit } from '../../helpers/utils'; +import { splunkApiJsonRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + returnAll: [false], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Add Orphan Field', + name: 'add_orphan_field', + description: + 'Whether to include a boolean value for each saved search to show whether the search is orphaned, meaning that it has no valid owner', + type: 'boolean', + default: false, + }, + { + displayName: 'List Default Actions', + name: 'listDefaultActionArgs', + type: 'boolean', + default: false, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['report'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#saved.2Fsearches + + const qs = {} as IDataObject; + const options = this.getNodeParameter('options', i); + + populate(options, qs); + setReturnAllOrLimit.call(this, qs); + + const endpoint = '/services/saved/searches'; + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint, {}, qs); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/report/index.ts b/packages/nodes-base/nodes/Splunk/v2/actions/report/index.ts new file mode 100644 index 0000000000..26c13da11e --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/report/index.ts @@ -0,0 +1,54 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteReport from './deleteReport.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; + +export { create, deleteReport, get, getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['report'], + }, + }, + options: [ + { + name: 'Create From Search', + value: 'create', + description: 'Create a search report from a search job', + action: 'Create a search report', + }, + { + name: 'Delete', + value: 'deleteReport', + description: 'Delete a search report', + action: 'Delete a search report', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a search report', + action: 'Get a search report', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve many search reports', + action: 'Get many search reports', + }, + ], + default: 'getAll', + }, + + ...create.description, + ...deleteReport.description, + ...get.description, + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/router.ts b/packages/nodes-base/nodes/Splunk/v2/actions/router.ts new file mode 100644 index 0000000000..289e5d02d7 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/router.ts @@ -0,0 +1,74 @@ +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; + +import * as alert from './alert'; +import * as report from './report'; +import * as search from './search'; +import * as user from './user'; + +import set from 'lodash/set'; +import type { SplunkType } from './node.type'; + +export async function router(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + let returnData: INodeExecutionData[] = []; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + const splunkNodeData = { + resource, + operation, + } as SplunkType; + + let responseData; + + for (let i = 0; i < items.length; i++) { + try { + switch (splunkNodeData.resource) { + case 'alert': + responseData = await alert[splunkNodeData.operation].execute.call(this, i); + break; + case 'report': + responseData = await report[splunkNodeData.operation].execute.call(this, i); + break; + case 'search': + responseData = await search[splunkNodeData.operation].execute.call(this, i); + break; + case 'user': + responseData = await user[splunkNodeData.operation].execute.call(this, i); + break; + default: + throw new NodeOperationError(this.getNode(), 'Resource not found', { itemIndex: i }); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.cause.error }, pairedItem: { item: i } }); + continue; + } + + if (error instanceof NodeApiError) { + set(error, 'context.itemIndex', i); + throw error; + } + + if (error instanceof NodeOperationError) { + if (error?.context?.itemIndex === undefined) { + set(error, 'context.itemIndex', i); + } + throw error; + } + + throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData = returnData.concat(executionData); + } + + return [returnData]; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/search/create.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/search/create.operation.ts new file mode 100644 index 0000000000..b617b162aa --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/search/create.operation.ts @@ -0,0 +1,255 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { populate, toUnixEpoch } from '../../helpers/utils'; +import { splunkApiJsonRequest, splunkApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Query', + name: 'search', + description: + 'Search language string to execute, in Splunk\'s Search Processing Language', + placeholder: 'e.g. search index=_internal | stats count by source', + type: 'string', + required: true, + default: '', + typeOptions: { + rows: 2, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Ad Hoc Search Level', + name: 'adhoc_search_level', + type: 'options', + default: 'verbose', + options: [ + { + name: 'Fast', + value: 'fast', + }, + { + name: 'Smart', + value: 'smart', + }, + { + name: 'Verbose', + value: 'verbose', + }, + ], + }, + { + displayName: 'Auto-Cancel After (Seconds)', + name: 'auto_cancel', + type: 'number', + default: 0, + description: 'Seconds after which the search job automatically cancels', + }, + { + displayName: 'Auto-Finalize After (Num Events)', + name: 'auto_finalize_ec', + type: 'number', + default: 0, + description: 'Auto-finalize the search after at least this many events are processed', + }, + { + displayName: 'Auto Pause After (Seconds)', + name: 'auto_pause', + type: 'number', + default: 0, + description: 'Seconds of inactivity after which the search job automatically pauses', + }, + { + displayName: 'Earliest Index', + name: 'index_earliest', + type: 'dateTime', + default: '', + description: 'The earliest index time for the search (inclusive)', + }, + { + displayName: 'Earliest Time', + name: 'earliest_time', + type: 'dateTime', + default: '', + description: 'The earliest cut-off for the search (inclusive)', + }, + { + displayName: 'Exec Mode', + name: 'exec_mode', + type: 'options', + default: 'blocking', + options: [ + { + name: 'Blocking', + value: 'blocking', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'One Shot', + value: 'oneshot', + }, + ], + }, + { + displayName: 'Indexed Real Time Offset', + name: 'indexedRealtimeOffset', + type: 'number', + default: 0, + description: 'Seconds of disk sync delay for indexed real-time search', + }, + { + displayName: 'Latest Index', + name: 'index_latest', + type: 'dateTime', + default: '', + description: 'The latest index time for the search (inclusive)', + }, + { + displayName: 'Latest Time', + name: 'latest_time', + type: 'dateTime', + default: '', + description: 'The latest cut-off for the search (inclusive)', + }, + { + displayName: 'Max Time', + name: 'max_time', + type: 'number', + default: 0, + description: + 'Number of seconds to run this search before finalizing. Enter 0 to never finalize.', + }, + { + displayName: 'Namespace', + name: 'namespace', + type: 'string', + default: '', + description: 'Application namespace in which to restrict searches', + }, + { + displayName: 'Reduce Frequency', + name: 'reduce_freq', + type: 'number', + default: 0, + description: 'How frequently to run the MapReduce reduce phase on accumulated map values', + }, + { + displayName: 'Remote Server List', + name: 'remote_server_list', + type: 'string', + default: '', + description: + 'Comma-separated list of (possibly wildcarded) servers from which raw events should be pulled. This same server list is to be used in subsearches.', + }, + { + displayName: 'Reuse Limit (Seconds)', + name: 'reuse_max_seconds_ago', + type: 'number', + default: 0, + description: + 'Number of seconds ago to check when an identical search is started and return the job’s search ID instead of starting a new job', + }, + { + displayName: 'Required Field', + name: 'rf', + type: 'string', + default: '', + description: + 'Name of a required field to add to the search. Even if not referenced or used directly by the search, a required field is still included in events and summary endpoints.', + }, + { + displayName: 'Search Mode', + name: 'search_mode', + type: 'options', + default: 'normal', + options: [ + { + name: 'Normal', + value: 'normal', + }, + { + name: 'Real Time', + value: 'realtime', + }, + ], + }, + { + displayName: 'Status Buckets', + name: 'status_buckets', + type: 'number', + default: 0, + description: + 'The most status buckets to generate. Set 0 generate no timeline information.', + }, + { + displayName: 'Timeout', + name: 'timeout', + type: 'number', + default: 86400, + description: 'Number of seconds to keep this search after processing has stopped', + }, + { + displayName: 'Workload Pool', + name: 'workload_pool', + type: 'string', + default: '', + description: 'New workload pool where the existing running search should be placed', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['search'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs + + const body = { + search: this.getNodeParameter('search', i), + } as IDataObject; + + const { earliest_time, latest_time, index_earliest, index_latest, ...rest } = + this.getNodeParameter('additionalFields', i) as IDataObject & { + earliest_time?: string; + latest_time?: string; + index_earliest?: string; + index_latest?: string; + }; + + populate( + { + ...(earliest_time && { earliest_time: toUnixEpoch(earliest_time) }), + ...(latest_time && { latest_time: toUnixEpoch(latest_time) }), + ...(index_earliest && { index_earliest: toUnixEpoch(index_earliest) }), + ...(index_latest && { index_latest: toUnixEpoch(index_latest) }), + ...rest, + }, + body, + ); + + const endpoint = '/services/search/jobs'; + const responseData = await splunkApiRequest.call(this, 'POST', endpoint, body); + + const getEndpoint = `/services/search/jobs/${responseData.response.sid}`; + const returnData = await splunkApiJsonRequest.call(this, 'GET', getEndpoint); + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/search/deleteJob.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/search/deleteJob.operation.ts new file mode 100644 index 0000000000..1bb463a515 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/search/deleteJob.operation.ts @@ -0,0 +1,29 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiRequest } from '../../transport'; +import { searchJobRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [searchJobRLC]; + +const displayOptions = { + show: { + resource: ['search'], + operation: ['deleteJob'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D + + const searchJobId = this.getNodeParameter('searchJobId', i, '', { extractValue: true }) as string; + const endpoint = `/services/search/jobs/${searchJobId}`; + + await splunkApiRequest.call(this, 'DELETE', endpoint); + + return { success: true }; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/search/get.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/search/get.operation.ts new file mode 100644 index 0000000000..1df3721de1 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/search/get.operation.ts @@ -0,0 +1,29 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; +import { searchJobRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [searchJobRLC]; + +const displayOptions = { + show: { + resource: ['search'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D + + const searchJobId = this.getNodeParameter('searchJobId', i, '', { extractValue: true }) as string; + const endpoint = `/services/search/jobs/${searchJobId}`; + + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/search/getAll.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/search/getAll.operation.ts new file mode 100644 index 0000000000..5ea3bb3685 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/search/getAll.operation.ts @@ -0,0 +1,123 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { populate, setReturnAllOrLimit } from '../../helpers/utils'; +import { splunkApiJsonRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + returnAll: [false], + }, + }, + }, + { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'Sort Direction', + name: 'sort_dir', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'asc', + }, + { + name: 'Descending', + value: 'desc', + }, + ], + default: 'asc', + }, + { + displayName: 'Sort Key', + name: 'sort_key', + description: 'Key name to use for sorting', + type: 'string', + placeholder: 'e.g. diskUsage', + default: '', + }, + { + displayName: 'Sort Mode', + name: 'sort_mode', + type: 'options', + options: [ + { + name: 'Automatic', + value: 'auto', + description: + 'If all field values are numeric, collate numerically. Otherwise, collate alphabetically.', + }, + { + name: 'Alphabetic', + value: 'alpha', + description: 'Collate alphabetically, case-insensitive', + }, + { + name: 'Alphabetic and Case-Sensitive', + value: 'alpha_case', + description: 'Collate alphabetically, case-sensitive', + }, + { + name: 'Numeric', + value: 'num', + description: 'Collate numerically', + }, + ], + default: 'auto', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['search'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTsearch#search.2Fjobs + + const qs = {} as IDataObject; + const sort = this.getNodeParameter('sort.values', i, {}) as IDataObject; + + populate(sort, qs); + setReturnAllOrLimit.call(this, qs); + + const endpoint = '/services/search/jobs'; + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint, {}, qs); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/search/getResult.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/search/getResult.operation.ts new file mode 100644 index 0000000000..b80ac3457f --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/search/getResult.operation.ts @@ -0,0 +1,125 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; +import { populate, setReturnAllOrLimit } from '../../helpers/utils'; +import { searchJobRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [ + searchJobRLC, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + returnAll: [false], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Key-Value Match', + name: 'keyValueMatch', + description: + 'Key-value pair to match against. Example: if "Key" is set to user and "Field" is set to john, only the results where user is john will be returned.', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Key-Value Pair', + options: [ + { + displayName: 'Key-Value Pair', + name: 'keyValuePair', + values: [ + { + displayName: 'Key', + name: 'key', + description: 'Key to match against', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + description: 'Value to match against', + type: 'string', + default: '', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Add Summary to Metadata', + name: 'add_summary_to_metadata', + description: 'Whether to include field summary statistics in the response', + type: 'boolean', + default: false, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['search'], + operation: ['getResult'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D.2Fresults + + const searchJobId = this.getNodeParameter('searchJobId', i, '', { extractValue: true }) as string; + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i) as IDataObject & { + keyValueMatch?: { keyValuePair?: { key: string; value: string } }; + }; + const options = this.getNodeParameter('options', i); + + const keyValuePair = filters?.keyValueMatch?.keyValuePair; + + if (keyValuePair?.key && keyValuePair?.value) { + qs.search = `search ${keyValuePair.key}=${keyValuePair.value}`; + } + + populate(options, qs); + setReturnAllOrLimit.call(this, qs); + + const endpoint = `/services/search/jobs/${searchJobId}/results`; + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint, {}, qs); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/search/index.ts b/packages/nodes-base/nodes/Splunk/v2/actions/search/index.ts new file mode 100644 index 0000000000..51eba23c25 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/search/index.ts @@ -0,0 +1,62 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteJob from './deleteJob.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as getResult from './getResult.operation'; + +export { create, deleteJob, get, getAll, getResult }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['search'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a search job', + action: 'Create a search job', + }, + { + name: 'Delete', + value: 'deleteJob', + description: 'Delete a search job', + action: 'Delete a search job', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a search job', + action: 'Get a search job', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve many search jobs', + action: 'Get many search jobs', + }, + { + name: 'Get Result', + value: 'getResult', + description: 'Get the result of a search job', + action: 'Get the result of a search job', + }, + ], + default: 'create', + }, + + ...create.description, + ...deleteJob.description, + ...get.description, + ...getAll.description, + ...getResult.description, +]; diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/user/create.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/user/create.operation.ts new file mode 100644 index 0000000000..e1904ecd20 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/user/create.operation.ts @@ -0,0 +1,99 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { formatFeed, populate } from '../../helpers/utils'; +import { splunkApiRequest } from '../../transport'; +import type { SplunkFeedResponse } from '../../helpers/interfaces'; + +const properties: INodeProperties[] = [ + { + displayName: 'Name', + name: 'name', + description: 'Login name of the user', + type: 'string', + required: true, + default: '', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Roles', + name: 'roles', + type: 'multiOptions', + description: + 'Comma-separated list of roles to assign to the user. Choose from the list, or specify IDs using an expression.', + required: true, + default: ['user'], + typeOptions: { + loadOptionsMethod: 'getRoles', + }, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { password: true }, + required: true, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + }, + { + displayName: 'Full Name', + name: 'realname', + type: 'string', + default: '', + description: 'Full name of the user', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['user'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers + + const roles = this.getNodeParameter('roles', i) as string[]; + + const body = { + name: this.getNodeParameter('name', i), + roles, + password: this.getNodeParameter('password', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + populate(additionalFields, body); + + const endpoint = '/services/authentication/users'; + const responseData = (await splunkApiRequest.call( + this, + 'POST', + endpoint, + body, + )) as SplunkFeedResponse; + const returnData = formatFeed(responseData); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/user/deleteUser.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/user/deleteUser.operation.ts new file mode 100644 index 0000000000..e3d9a05605 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/user/deleteUser.operation.ts @@ -0,0 +1,29 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiRequest } from '../../transport'; +import { userRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [userRLC]; + +const displayOptions = { + show: { + resource: ['user'], + operation: ['deleteUser'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D + + const userId = this.getNodeParameter('userId', i, '', { extractValue: true }) as string; + const endpoint = `/services/authentication/users/${userId}`; + + await splunkApiRequest.call(this, 'DELETE', endpoint); + + return { success: true }; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/user/get.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/user/get.operation.ts new file mode 100644 index 0000000000..46ffad1e9d --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/user/get.operation.ts @@ -0,0 +1,29 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; +import { userRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [userRLC]; + +const displayOptions = { + show: { + resource: ['user'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D + + const userId = this.getNodeParameter('userId', i, '', { extractValue: true }) as string; + const endpoint = `/services/authentication/users/${userId}`; + + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/user/getAll.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/user/getAll.operation.ts new file mode 100644 index 0000000000..822a40f269 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/user/getAll.operation.ts @@ -0,0 +1,53 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { splunkApiJsonRequest } from '../../transport'; +import { setReturnAllOrLimit } from '../../helpers/utils'; + +const properties: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + returnAll: [false], + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['user'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + _i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers + + const qs = {} as IDataObject; + setReturnAllOrLimit.call(this, qs); + + const endpoint = '/services/authentication/users'; + const returnData = await splunkApiJsonRequest.call(this, 'GET', endpoint, {}, qs); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/user/index.ts b/packages/nodes-base/nodes/Splunk/v2/actions/user/index.ts new file mode 100644 index 0000000000..7aeab0e8f3 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/user/index.ts @@ -0,0 +1,62 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteUser from './deleteUser.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as update from './update.operation'; + +export { create, deleteUser, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['user'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an user', + action: 'Create a user', + }, + { + name: 'Delete', + value: 'deleteUser', + description: 'Delete an user', + action: 'Delete a user', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an user', + action: 'Get a user', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve many users', + action: 'Get many users', + }, + { + name: 'Update', + value: 'update', + description: 'Update an user', + action: 'Update a user', + }, + ], + default: 'create', + }, + + ...create.description, + ...deleteUser.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/user/update.operation.ts b/packages/nodes-base/nodes/Splunk/v2/actions/user/update.operation.ts new file mode 100644 index 0000000000..ed19fc7f7a --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/user/update.operation.ts @@ -0,0 +1,86 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../utils/utilities'; +import { formatFeed, populate } from '../../helpers/utils'; +import { splunkApiRequest } from '../../transport'; +import { userRLC } from '../../helpers/descriptions'; + +const properties: INodeProperties[] = [ + userRLC, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + }, + { + displayName: 'Full Name', + name: 'realname', + type: 'string', + default: '', + description: 'Full name of the user', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { password: true }, + default: '', + }, + { + displayName: 'Role Names or IDs', + name: 'roles', + type: 'multiOptions', + description: + 'Comma-separated list of roles to assign to the user. Choose from the list, or specify IDs using an expression.', + default: [], + typeOptions: { + loadOptionsMethod: 'getRoles', + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['user'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, +): Promise { + // https://docs.splunk.com/Documentation/Splunk/8.2.2/RESTREF/RESTaccess#authentication.2Fusers.2F.7Bname.7D + + const body = {} as IDataObject; + const { roles, ...rest } = this.getNodeParameter('updateFields', i) as IDataObject & { + roles: string[]; + }; + + populate( + { + ...(roles && { roles }), + ...rest, + }, + body, + ); + + const userId = this.getNodeParameter('userId', i, '', { extractValue: true }) as string; + const endpoint = `/services/authentication/users/${userId}`; + + const returnData = await splunkApiRequest.call(this, 'POST', endpoint, body).then(formatFeed); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Splunk/v2/actions/versionDescription.ts new file mode 100644 index 0000000000..8924cb02ee --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/actions/versionDescription.ts @@ -0,0 +1,60 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; + +import * as alert from './alert'; +import * as report from './report'; +import * as search from './search'; +import * as user from './user'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Splunk', + name: 'splunk', + icon: 'file:splunk.svg', + group: ['transform'], + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Splunk Enterprise API', + defaults: { + name: 'Splunk', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'splunkApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Alert', + value: 'alert', + }, + { + name: 'Report', + value: 'report', + }, + { + name: 'Search', + value: 'search', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'search', + }, + + ...alert.description, + ...report.description, + ...search.description, + ...user.description, + ], +}; diff --git a/packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/index.ts b/packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/index.ts new file mode 100644 index 0000000000..71fceedc4a --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/index.ts @@ -0,0 +1 @@ +export * from './rlc.description'; diff --git a/packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/rlc.description.ts b/packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/rlc.description.ts new file mode 100644 index 0000000000..67022627d4 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/helpers/descriptions/rlc.description.ts @@ -0,0 +1,79 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const reportRLC: INodeProperties = { + displayName: 'Report', + name: 'reportId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a report...', + typeOptions: { + searchListMethod: 'searchReports', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. Errors%20in%20the%20last%20hour', + }, + ], +}; + +export const searchJobRLC: INodeProperties = { + displayName: 'Search Job', + name: 'searchJobId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a search job...', + typeOptions: { + searchListMethod: 'searchJobs', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. 1718944376.178', + }, + ], +}; + +export const userRLC: INodeProperties = { + displayName: 'User', + name: 'userId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a user...', + typeOptions: { + searchListMethod: 'searchUsers', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. admin', + }, + ], +}; diff --git a/packages/nodes-base/nodes/Splunk/v2/helpers/interfaces.ts b/packages/nodes-base/nodes/Splunk/v2/helpers/interfaces.ts new file mode 100644 index 0000000000..b4bc35e985 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/helpers/interfaces.ts @@ -0,0 +1,24 @@ +import type { IDataObject } from 'n8n-workflow'; + +export type SplunkCredentials = { + authToken: string; + baseUrl: string; + allowUnauthorizedCerts: boolean; +}; + +export type SplunkFeedResponse = { + feed: { + entry: IDataObject[] | IDataObject; + }; +}; + +export type SplunkError = { + response?: { + messages?: { + msg: { + $: { type: string }; + _: string; + }; + }; + }; +}; diff --git a/packages/nodes-base/nodes/Splunk/v2/helpers/utils.ts b/packages/nodes-base/nodes/Splunk/v2/helpers/utils.ts new file mode 100644 index 0000000000..79fc3cc446 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/helpers/utils.ts @@ -0,0 +1,107 @@ +import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; + +import { parseString } from 'xml2js'; + +import type { SplunkError, SplunkFeedResponse } from './interfaces'; +import { SPLUNK } from '../../v1/types'; + +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; + }, {}); +} + +export function formatEntry(entry: any, doNotFormatContent = false): any { + const { content, link, ...rest } = entry; + const formattedContent = doNotFormatContent ? content : formatEntryContent(content); + const formattedEntry = { ...rest, ...formattedContent }; + + if (formattedEntry.id) { + formattedEntry.entryUrl = formattedEntry.id; + formattedEntry.id = formattedEntry.id.split('/').pop(); + } + + return formattedEntry; +} + +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 function formatFeed(responseData: SplunkFeedResponse) { + const { entry: entries } = responseData.feed; + + if (!entries) return []; + + return Array.isArray(entries) + ? entries.map((entry) => formatEntry(entry)) + : [formatEntry(entries)]; +} + +export function setReturnAllOrLimit(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); + } +} + +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; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/methods/index.ts b/packages/nodes-base/nodes/Splunk/v2/methods/index.ts new file mode 100644 index 0000000000..a5508a3e0f --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/methods/index.ts @@ -0,0 +1,2 @@ +export * as loadOptions from './loadOptions'; +export * as listSearch from './listSearch'; diff --git a/packages/nodes-base/nodes/Splunk/v2/methods/listSearch.ts b/packages/nodes-base/nodes/Splunk/v2/methods/listSearch.ts new file mode 100644 index 0000000000..087163cb72 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/methods/listSearch.ts @@ -0,0 +1,74 @@ +import type { IDataObject, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; +import { splunkApiJsonRequest } from '../transport'; + +export async function searchReports( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const qs: IDataObject = {}; + + if (filter) { + qs.search = filter; + } + + const endpoint = '/services/saved/searches'; + const response = await splunkApiJsonRequest.call(this, 'GET', endpoint, undefined, qs); + + return { + results: (response as IDataObject[]).map((entry: IDataObject) => { + return { + name: entry.name as string, + value: entry.id as string, + url: entry.entryUrl as string, + }; + }), + }; +} + +export async function searchJobs( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const qs: IDataObject = {}; + + if (filter) { + qs.search = filter; + } + + const endpoint = '/services/search/jobs'; + const response = await splunkApiJsonRequest.call(this, 'GET', endpoint, undefined, qs); + + return { + results: (response as IDataObject[]).map((entry: IDataObject) => { + return { + name: (entry.name as string).replace(/^\|\s*/, ''), + value: entry.id as string, + url: entry.entryUrl as string, + }; + }), + }; +} + +export async function searchUsers( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const qs: IDataObject = {}; + + if (filter) { + qs.search = filter; + } + + const endpoint = '/services/authentication/users'; + const response = await splunkApiJsonRequest.call(this, 'GET', endpoint, undefined, qs); + + return { + results: (response as IDataObject[]).map((entry: IDataObject) => { + return { + name: entry.name as string, + value: entry.id as string, + url: entry.entryUrl as string, + }; + }), + }; +} diff --git a/packages/nodes-base/nodes/Splunk/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/Splunk/v2/methods/loadOptions.ts new file mode 100644 index 0000000000..a55459fbed --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/methods/loadOptions.ts @@ -0,0 +1,13 @@ +import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; + +import { splunkApiJsonRequest } from '../transport'; + +export async function getRoles(this: ILoadOptionsFunctions): Promise { + const endpoint = '/services/authorization/roles'; + const responseData = await splunkApiJsonRequest.call(this, 'GET', endpoint); + + return (responseData as Array<{ id: string }>).map((entry) => ({ + name: entry.id, + value: entry.id, + })); +} diff --git a/packages/nodes-base/nodes/Splunk/v2/transport/index.ts b/packages/nodes-base/nodes/Splunk/v2/transport/index.ts new file mode 100644 index 0000000000..ee6fd8ea4f --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/v2/transport/index.ts @@ -0,0 +1,153 @@ +import type { + IExecuteFunctions, + IDataObject, + ILoadOptionsFunctions, + JsonObject, + IHttpRequestMethods, + IHttpRequestOptions, + IRequestOptions, +} from 'n8n-workflow'; +import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; + +import type { SplunkCredentials, SplunkError } from '../helpers/interfaces'; +import { extractErrorDescription, formatEntry, parseXml } from '../helpers/utils'; + +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 (error instanceof NodeApiError) throw 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); + } +} + +export async function splunkApiJsonRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const { baseUrl, allowUnauthorizedCerts } = (await this.getCredentials( + 'splunkApi', + )) as SplunkCredentials; + + qs.output_mode = 'json'; + + const options: IHttpRequestOptions = { + method, + body, + qs: qs ?? {}, + url: `${baseUrl}${endpoint}`, + json: true, + skipSslCertificateValidation: allowUnauthorizedCerts, + }; + + if (!Object.keys(body).length) delete options.body; + + let result; + try { + let attempts = 0; + + do { + try { + result = await this.helpers.httpRequestWithAuthentication.call(this, 'splunkApi', options); + + if (result.entry) { + const { entry } = result; + return (entry as IDataObject[]).map((e) => formatEntry(e, true)); + } + + return result; + } catch (error) { + if (attempts >= 5) { + throw error; + } + await sleep(1000); + attempts++; + } + } while (true); + } catch (error) { + if (error instanceof NodeApiError) throw 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 }); + } + + if ('fatal' in error) error = { error: error.fatal }; + + throw new NodeApiError(this.getNode(), error as JsonObject); + } +}