diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 09d29f76c4..eb980476f9 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -415,6 +415,9 @@ async function proxyRequestToAxios( } }) .catch((error) => { + // The error-data was made available with request library via "error" but now on + // axios via "response.data" so copy information over to keep it compatible + error.error = error.response.data; error.statusCode = error.response.status; reject(error); }); diff --git a/packages/nodes-base/credentials/SplunkApi.credentials.ts b/packages/nodes-base/credentials/SplunkApi.credentials.ts new file mode 100644 index 0000000000..99560572ad --- /dev/null +++ b/packages/nodes-base/credentials/SplunkApi.credentials.ts @@ -0,0 +1,32 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class SplunkApi implements ICredentialType { + name = 'splunkApi'; + displayName = 'Splunk API'; + documentationUrl = 'splunk'; + properties: INodeProperties[] = [ + { + displayName: 'Auth Token', + name: 'authToken', + type: 'string', + default: '', + }, + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string', + description: 'Protocol, domain and port', + placeholder: 'e.g. https://localhost:8089', + default: '', + }, + { + displayName: 'Allow Self-Signed Certificates', + name: 'allowUnauthorizedCerts', + type: 'boolean', + default: false, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Splunk/GenericFunctions.ts b/packages/nodes-base/nodes/Splunk/GenericFunctions.ts new file mode 100644 index 0000000000..4be67818fa --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/GenericFunctions.ts @@ -0,0 +1,268 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + NodeApiError, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +import { + parseString, +} from 'xml2js'; + +import { + SplunkCredentials, + SplunkError, + SplunkFeedResponse, + SplunkResultResponse, + SplunkSearchResponse, +} from './types'; + +export async function splunkApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const { + authToken, + baseUrl, + allowUnauthorizedCerts, + } = await this.getCredentials('splunkApi') as SplunkCredentials; + + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${authToken}`, + '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; + } + + try { + return await this.helpers.request!(options).then(parseXml); + } catch (error) { + if (error?.cause?.code === 'ECONNREFUSED') { + throw new NodeApiError(this.getNode(), { ...error, code: 401 }); + } + + const rawError = await parseXml(error.error) as SplunkError; + error = extractErrorDescription(rawError); + + if ('fatal' in error) { + error = { error: error.fatal }; + } + + throw new NodeApiError(this.getNode(), error); + } +} + +// ---------------------------------------- +// utils +// ---------------------------------------- + +export function parseXml(xml: string) { + return 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; +} + +// ---------------------------------------- +// search formatting +// ---------------------------------------- + +export function formatSearch(responseData: SplunkSearchResponse) { + const { entry: entries } = responseData; + + if (!entries) return []; + + return Array.isArray(entries) + ? entries.map(formatEntry) + : [formatEntry(entries)]; +} + +// ---------------------------------------- +// feed formatting +// ---------------------------------------- + +export function formatFeed(responseData: SplunkFeedResponse) { + const { entry: entries } = responseData.feed; + + if (!entries) return []; + + return Array.isArray(entries) + ? entries.map(formatEntry) + : [formatEntry(entries)]; +} + +// ---------------------------------------- +// result formatting +// ---------------------------------------- + +export function formatResults(responseData: SplunkResultResponse) { + const results = responseData.results.result; + if (!results) return []; + + return Array.isArray(results) + ? results.map(r => formatResult(r.field)) + : [formatResult(results.field)]; +} + +/* tslint:disable: no-any */ + +function formatResult(field: any): any { + return field.reduce((acc: any, cur: any) => { + acc = { ...acc, ...compactResult(cur) }; + return acc; + }, {}); +} + +function compactResult(splunkObject: any): any { + if (typeof splunkObject !== 'object') { + return {}; + } + + if ( + Array.isArray(splunkObject?.value) && + splunkObject?.value[0]?.text + ) { + return { + [splunkObject.$.k]: splunkObject.value + .map((v: { text: string }) => v.text) + .join(','), + }; + } + + if (!splunkObject?.$?.k || !splunkObject?.value?.text) { + return {}; + } + + return { + [splunkObject.$.k]: splunkObject.value.text, + }; +} + +// ---------------------------------------- +// entry formatting +// ---------------------------------------- + +function formatEntry(entry: any): any { + const { content, link, ...rest } = entry; + const formattedEntry = { ...rest, ...formatEntryContent(content) }; + + if (formattedEntry.id) { + formattedEntry.entryUrl = formattedEntry.id; + formattedEntry.id = formattedEntry.id.split('/').pop(); + } + + return formattedEntry; +} + +function formatEntryContent(content: any): any { + return content['s:dict']['s:key'].reduce((acc: any, cur: any) => { + acc = { ...acc, ...compactEntryContent(cur) }; + return acc; + }, {}); +} + +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['s:dict']) { + const obj = splunkObject['s:dict']['s:key']; + return { [splunkObject.$.name]: compactEntryContent(obj) }; + } + + if (splunkObject['s:list']) { + const items = splunkObject['s:list']['s:item']; + return { [splunkObject.$.name]: items }; + } + + if (splunkObject._) { + return { + [splunkObject.$.name]: splunkObject._, + }; + } + + return { + [splunkObject.$.name]: '', + }; +} + +// ---------------------------------------- +// param loaders +// ---------------------------------------- + +/** + * Set count of entries to retrieve. + */ +export function setCount(this: IExecuteFunctions, qs: IDataObject) { + qs.count = this.getNodeParameter('returnAll', 0) + ? 0 + : this.getNodeParameter('limit', 0) as number; +} + +export function populate(source: IDataObject, destination: IDataObject) { + if (Object.keys(source).length) { + Object.assign(destination, source); + } +} + +/** + * Retrieve an ID, with tolerance when contained in an endpoint. + * The field `id` in Splunk API responses is a full link. + */ +export function getId( + this: IExecuteFunctions, + i: number, + idType: 'userId' | 'searchJobId' | 'searchConfigurationId', + endpoint: string, +) { + const id = this.getNodeParameter(idType, i) as string; + + return id.includes(endpoint) + ? id.split(endpoint).pop()! + : id; +} diff --git a/packages/nodes-base/nodes/Splunk/Splunk.node.ts b/packages/nodes-base/nodes/Splunk/Splunk.node.ts new file mode 100644 index 0000000000..00a37ea414 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/Splunk.node.ts @@ -0,0 +1,497 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeCredentialTestResult, +} 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 { + SplunkCredentials, + SplunkFeedResponse, +} from './types'; + +import { + OptionsWithUri, +} from 'request'; + +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', + color: '#e20082', + }, + 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: OptionsWithUri = { + 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: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + 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) as IDataObject; + + 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) as IDataObject; + + 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) as IDataObject; + + 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) as IDataObject; + + 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({ error: error.cause.error }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData as IDataObject); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Splunk/descriptions/FiredAlertDescription.ts b/packages/nodes-base/nodes/Splunk/descriptions/FiredAlertDescription.ts new file mode 100644 index 0000000000..b8e2598709 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/descriptions/FiredAlertDescription.ts @@ -0,0 +1,27 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const firedAlertOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'firedAlert', + ], + }, + }, + options: [ + { + name: 'Get Report', + value: 'getReport', + description: 'Retrieve a fired alerts report', + }, + ], + default: 'getReport', + }, +]; diff --git a/packages/nodes-base/nodes/Splunk/descriptions/SearchConfigurationDescription.ts b/packages/nodes-base/nodes/Splunk/descriptions/SearchConfigurationDescription.ts new file mode 100644 index 0000000000..aff086c05c --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/descriptions/SearchConfigurationDescription.ts @@ -0,0 +1,159 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const searchConfigurationOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'searchConfiguration', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a search configuration', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a search configuration', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all search configurations', + }, + ], + default: 'delete', + }, +]; + +export const searchConfigurationFields: INodeProperties[] = [ + // ---------------------------------------- + // searchConfiguration: delete + // ---------------------------------------- + { + displayName: 'Search Configuration ID', + name: 'searchConfigurationId', + description: 'ID of the search configuration to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'searchConfiguration', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // searchConfiguration: get + // ---------------------------------------- + { + displayName: 'Search Configuration ID', + name: 'searchConfigurationId', + description: 'ID of the search configuration to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'searchConfiguration', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // searchConfiguration: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'searchConfiguration', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'searchConfiguration', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'searchConfiguration', + ], + operation: [ + 'getAll', + ], + }, + }, + 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, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Splunk/descriptions/SearchJobDescription.ts b/packages/nodes-base/nodes/Splunk/descriptions/SearchJobDescription.ts new file mode 100644 index 0000000000..6a9890cb96 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/descriptions/SearchJobDescription.ts @@ -0,0 +1,418 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const searchJobOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a search job', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a search job', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a search job', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all search jobs', + }, + ], + default: 'create', + }, +]; + +export const searchJobFields: INodeProperties[] = [ + // ---------------------------------------- + // searchJob: create + // ---------------------------------------- + { + displayName: 'Query', + name: 'search', + description: 'Search language string to execute, in Splunk\'s Search Processing Language', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'create', + ], + }, + }, + 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', + }, + ], + }, + + // ---------------------------------------- + // searchJob: delete + // ---------------------------------------- + { + displayName: 'Search ID', + name: 'searchJobId', + description: 'ID of the search job to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // searchJob: get + // ---------------------------------------- + { + displayName: 'Search ID', + name: 'searchJobId', + description: 'ID of the search job to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // searchJob: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'searchJob', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + 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', + 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', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Splunk/descriptions/SearchResultDescription.ts b/packages/nodes-base/nodes/Splunk/descriptions/SearchResultDescription.ts new file mode 100644 index 0000000000..df11c82adc --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/descriptions/SearchResultDescription.ts @@ -0,0 +1,166 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const searchResultOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'searchResult', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all search results for a search job', + }, + ], + default: 'getAll', + }, +]; + +export const searchResultFields: INodeProperties[] = [ + // ---------------------------------------- + // searchResult: getAll + // ---------------------------------------- + { + displayName: 'Search ID', + name: 'searchJobId', + description: 'ID of the search whose results to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'searchResult', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'searchResult', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'searchResult', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'searchResult', + ], + operation: [ + 'getAll', + ], + }, + }, + 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: {}, + displayOptions: { + show: { + resource: [ + 'searchResult', + ], + operation: [ + 'getAll', + ], + }, + }, + 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, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Splunk/descriptions/UserDescription.ts b/packages/nodes-base/nodes/Splunk/descriptions/UserDescription.ts new file mode 100644 index 0000000000..cfffe78572 --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/descriptions/UserDescription.ts @@ -0,0 +1,300 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an user', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an user', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all users', + }, + { + name: 'Update', + value: 'update', + description: 'Update an user', + }, + ], + default: 'create', + }, +]; + +export const userFields: INodeProperties[] = [ + // ---------------------------------------- + // user: create + // ---------------------------------------- + { + displayName: 'Name', + name: 'name', + description: 'Login name of the user', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Roles', + name: 'roles', + type: 'multiOptions', + description: 'Comma-separated list of roles to assign to the user', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getRoles', + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'realname', + type: 'string', + default: '', + description: 'Full name of the user', + }, + ], + }, + + // ---------------------------------------- + // user: delete + // ---------------------------------------- + { + displayName: 'User ID', + name: 'userId', + description: 'ID of the user to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // user: get + // ---------------------------------------- + { + displayName: 'User ID', + name: 'userId', + description: 'ID of the user to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // user: getAll + // ---------------------------------------- +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------------- + // user: update + // ---------------------------------------- + { + displayName: 'User ID', + name: 'userId', + description: 'ID of the user to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'realname', + type: 'string', + default: '', + description: 'Full name of the user', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + }, + { + displayName: 'Roles', + name: 'roles', + type: 'multiOptions', + description: 'Comma-separated list of roles to assign to the user', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getRoles', + }, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Splunk/descriptions/index.ts b/packages/nodes-base/nodes/Splunk/descriptions/index.ts new file mode 100644 index 0000000000..69565d677b --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/descriptions/index.ts @@ -0,0 +1,5 @@ +export * from './FiredAlertDescription'; +export * from './SearchConfigurationDescription'; +export * from './SearchJobDescription'; +export * from './SearchResultDescription'; +export * from './UserDescription'; diff --git a/packages/nodes-base/nodes/Splunk/splunk.svg b/packages/nodes-base/nodes/Splunk/splunk.svg new file mode 100644 index 0000000000..808255d4fb --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/splunk.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/nodes-base/nodes/Splunk/types.d.ts b/packages/nodes-base/nodes/Splunk/types.d.ts new file mode 100644 index 0000000000..fd8cd218bb --- /dev/null +++ b/packages/nodes-base/nodes/Splunk/types.d.ts @@ -0,0 +1,30 @@ +export type SplunkCredentials = { + authToken: string; + baseUrl: string; + allowUnauthorizedCerts: boolean; +}; + +export type SplunkFeedResponse = { + feed: { + entry: { title: string }; + }; +}; + +export type SplunkSearchResponse = { + entry: { title: string }; +}; + +export type SplunkResultResponse = { + results: { result: Array<{ field: string }> } | { result: { field: string } }; +}; + +export type SplunkError = { + response?: { + messages?: { + msg: { + $: { type: string }; + _: string; + } + } + } +}; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0e09038a09..a202828a8d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -255,6 +255,7 @@ "dist/credentials/SshPrivateKey.credentials.js", "dist/credentials/Sftp.credentials.js", "dist/credentials/Signl4Api.credentials.js", + "dist/credentials/SplunkApi.credentials.js", "dist/credentials/SpontitApi.credentials.js", "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/StoryblokContentApi.credentials.js", @@ -559,6 +560,7 @@ "dist/nodes/Sms77/Sms77.node.js", "dist/nodes/Snowflake/Snowflake.node.js", "dist/nodes/SplitInBatches.node.js", + "dist/nodes/Splunk/Splunk.node.js", "dist/nodes/Spontit/Spontit.node.js", "dist/nodes/Spotify/Spotify.node.js", "dist/nodes/SpreadsheetFile.node.js",