From ca9a155c69537775bbbb63e19e0541e0c9cbab30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 22 Sep 2021 17:48:50 +0200 Subject: [PATCH] :sparkles: Elastic Security node (#2206) * :sparkles: Create Elastic Security node * :hammer: Place Elastic nodes in Elastic dir * :zap: Improvements * :hammer: Split credentials * :art: Fix formatting * :zap: Tolerate trailing slash * :shirt: Fix lint * :shirt: Fix lint * :bug: Fix tags filter in case:getAll * :hammer: Refactor sort options in case:getAll * :pencil2: Reword param descriptions * :fire: Remove descriptions per feedback * :bug: Fix case:getStatus operation * :pencil2: Reword param and error message * :pencil2: Reword param descriptions * :hammer: Account for empty string in owner * :pencil2: Reword param description * :pencil2: Add more tooltip descriptions * :zap: Add cred test * :pencil2: Add param description * :pencil2: Add comment dividers * :zap: Improve UX for third-party service params * :hammer: Minor tweaks per feedback * :hammer: Make getStatus naming consistent * :zap: Fix operation Co-authored-by: ricardo Co-authored-by: Mutasem Co-authored-by: Jan Oberhauser --- .../ElasticSecurityApi.credentials.ts | 38 + .../ElasticsearchApi.credentials.ts | 14 +- .../ElasticSecurity/ElasticSecurity.node.ts | 625 +++++++++++++++++ .../ElasticSecurity/GenericFunctions.ts | 163 +++++ .../descriptions/CaseCommentDescription.ts | 345 +++++++++ .../descriptions/CaseDescription.ts | 659 ++++++++++++++++++ .../descriptions/CaseTagDescription.ts | 117 ++++ .../descriptions/ConnectorDescription.ts | 268 +++++++ .../ElasticSecurity/descriptions/index.ts | 4 + .../ElasticSecurity/elasticSecurity.svg | 11 + .../nodes/Elastic/ElasticSecurity/types.d.ts | 56 ++ .../Elasticsearch/Elasticsearch.node.json | 0 .../Elasticsearch/Elasticsearch.node.ts | 0 .../Elasticsearch/GenericFunctions.ts | 0 .../descriptions/DocumentDescription.ts | 0 .../descriptions/IndexDescription.ts | 0 .../Elasticsearch/descriptions/index.ts | 0 .../descriptions/placeholders.ts | 0 .../Elasticsearch/elasticsearch.svg | 0 .../{ => Elastic}/Elasticsearch/types.d.ts | 0 packages/nodes-base/package.json | 4 +- 21 files changed, 2296 insertions(+), 8 deletions(-) create mode 100644 packages/nodes-base/credentials/ElasticSecurityApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseCommentDescription.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseDescription.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseTagDescription.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/ConnectorDescription.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/elasticSecurity.svg create mode 100644 packages/nodes-base/nodes/Elastic/ElasticSecurity/types.d.ts rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/Elasticsearch.node.json (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/Elasticsearch.node.ts (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/GenericFunctions.ts (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/descriptions/DocumentDescription.ts (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/descriptions/IndexDescription.ts (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/descriptions/index.ts (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/descriptions/placeholders.ts (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/elasticsearch.svg (100%) rename packages/nodes-base/nodes/{ => Elastic}/Elasticsearch/types.d.ts (100%) diff --git a/packages/nodes-base/credentials/ElasticSecurityApi.credentials.ts b/packages/nodes-base/credentials/ElasticSecurityApi.credentials.ts new file mode 100644 index 0000000000..1fea691158 --- /dev/null +++ b/packages/nodes-base/credentials/ElasticSecurityApi.credentials.ts @@ -0,0 +1,38 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class ElasticSecurityApi implements ICredentialType { + name = 'elasticSecurityApi'; + displayName = 'Elastic Security API'; + documentationUrl = 'elasticSecurity'; + properties: INodeProperties[] = [ + { + displayName: 'Username', + name: 'username', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + required: true, + }, + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string', + default: '', + placeholder: 'e.g. https://mydeployment.kb.us-central1.gcp.cloud.es.io:9243', + description: 'Referred to as Kibana \'endpoint\' in the Elastic deployment dashboard', + required: true, + }, + ]; +} diff --git a/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts b/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts index 0ebb10ab7e..eb3a35243d 100644 --- a/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts +++ b/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts @@ -1,23 +1,23 @@ import { ICredentialType, - NodePropertyTypes, + INodeProperties, } from 'n8n-workflow'; export class ElasticsearchApi implements ICredentialType { name = 'elasticsearchApi'; displayName = 'Elasticsearch API'; documentationUrl = 'elasticsearch'; - properties = [ + properties: INodeProperties[] = [ { displayName: 'Username', name: 'username', - type: 'string' as NodePropertyTypes, + type: 'string', default: '', }, { displayName: 'Password', name: 'password', - type: 'string' as NodePropertyTypes, + type: 'string', typeOptions: { password: true, }, @@ -26,10 +26,10 @@ export class ElasticsearchApi implements ICredentialType { { displayName: 'Base URL', name: 'baseUrl', - type: 'string' as NodePropertyTypes, + type: 'string', default: '', - placeholder: 'https://abc.elastic-cloud.com:9243', - description: 'Referred to as \'endpoint\' in the Elasticsearch dashboard.', + placeholder: 'https://mydeployment.es.us-central1.gcp.cloud.es.io:9243', + description: 'Referred to as Elasticsearch \'endpoint\' in the Elastic deployment dashboard', }, ]; } diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts new file mode 100644 index 0000000000..5e2b6ca69a --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts @@ -0,0 +1,625 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeCredentialTestResult, + NodeOperationError, +} from 'n8n-workflow'; + +import { + elasticSecurityApiRequest, + getConnector, + getVersion, + handleListing, + throwOnEmptyUpdate, + tolerateTrailingSlash, +} from './GenericFunctions'; + +import { + caseCommentFields, + caseCommentOperations, + caseFields, + caseOperations, + caseTagFields, + caseTagOperations, + connectorFields, + connectorOperations, +} from './descriptions'; + +import { + Connector, + ConnectorCreatePayload, + ConnectorType, + ElasticSecurityApiCredentials, +} from './types'; + +import { + OptionsWithUri, +} from 'request'; + +export class ElasticSecurity implements INodeType { + description: INodeTypeDescription = { + displayName: 'Elastic Security', + name: 'elasticSecurity', + icon: 'file:elasticSecurity.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Elastic Security API', + defaults: { + name: 'Elastic Security', + color: '#f3d337', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'elasticSecurityApi', + required: true, + testedBy: 'elasticSecurityApiTest', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + noDataExpression: true, + type: 'options', + options: [ + { + name: 'Case', + value: 'case', + }, + { + name: 'Case Comment', + value: 'caseComment', + }, + { + name: 'Case Tag', + value: 'caseTag', + }, + { + name: 'Connector', + value: 'connector', + }, + ], + default: 'case', + }, + ...caseOperations, + ...caseFields, + ...caseCommentOperations, + ...caseCommentFields, + ...caseTagOperations, + ...caseTagFields, + ...connectorOperations, + ...connectorFields, + ], + }; + + methods = { + loadOptions: { + async getTags(this: ILoadOptionsFunctions): Promise { + const tags = await elasticSecurityApiRequest.call(this, 'GET', '/cases/tags') as string[]; + return tags.map(tag => ({ name: tag, value: tag })); + }, + + async getConnectors(this: ILoadOptionsFunctions): Promise { + const endpoint = '/cases/configure/connectors/_find'; + const connectors = await elasticSecurityApiRequest.call(this, 'GET', endpoint) as Connector[]; + return connectors.map(({ name, id }) => ({ name, value: id })); + }, + }, + credentialTest: { + async elasticSecurityApiTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const { + username, + password, + baseUrl: rawBaseUrl, + } = credential.data as ElasticSecurityApiCredentials; + + const baseUrl = tolerateTrailingSlash(rawBaseUrl); + + const token = Buffer.from(`${username}:${password}`).toString('base64'); + + const endpoint = '/cases/status'; + + const options: OptionsWithUri = { + headers: { + Authorization: `Basic ${token}`, + 'kbn-xsrf': true, + }, + method: 'GET', + body: {}, + qs: {}, + uri: `${baseUrl}/api${endpoint}`, + json: true, + }; + + 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 === 'case') { + + // ********************************************************************** + // case + // ********************************************************************** + + if (operation === 'create') { + + // ---------------------------------------- + // case: create + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-create.html + + const body = { + title: this.getNodeParameter('title', i), + connector: {}, + owner: 'securitySolution', + description: '', + tags: [], // set via `caseTag: add` but must be present + settings: { + syncAlerts: this.getNodeParameter('additionalFields.syncAlerts', i, false), + }, + } as IDataObject; + + const connectorId = this.getNodeParameter('connectorId', i) as ConnectorType; + + const { + id: fetchedId, + name: fetchedName, + type: fetchedType, + } = await getConnector.call(this, connectorId); + + const selectedConnectorType = this.getNodeParameter('connectorType', i) as ConnectorType; + + if (fetchedType !== selectedConnectorType) { + throw new NodeOperationError( + this.getNode(), + 'Connector Type does not match the type of the connector in Connector Name', + ); + } + + const connector = { + id: fetchedId, + name: fetchedName, + type: fetchedType, + fields: {}, + }; + + if (selectedConnectorType === '.jira') { + connector.fields = { + issueType: this.getNodeParameter('issueType', i), + priority: this.getNodeParameter('priority', i), + parent: null, // required but unimplemented + }; + } else if (selectedConnectorType === '.servicenow') { + connector.fields = { + urgency: this.getNodeParameter('urgency', i), + severity: this.getNodeParameter('severity', i), + impact: this.getNodeParameter('impact', i), + category: this.getNodeParameter('category', i), + subcategory: null, // required but unimplemented + }; + } else if (selectedConnectorType === '.resilient') { + const rawIssueTypes = this.getNodeParameter('issueTypes', i) as string; + connector.fields = { + issueTypes: rawIssueTypes.split(',').map(Number), + severityCode: this.getNodeParameter('severityCode', i) as number, + incidentTypes: null, // required but undocumented + }; + } + + body.connector = connector; + + const { + syncAlerts, // ignored because already set + ...rest + } = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(rest).length) { + Object.assign(body, rest); + } + + responseData = await elasticSecurityApiRequest.call(this, 'POST', '/cases', body); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // case: delete + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-delete-case.html + + const caseId = this.getNodeParameter('caseId', i); + await elasticSecurityApiRequest.call(this, 'DELETE', `/cases?ids=["${caseId}"]`); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // case: get + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-get-case.html + + const caseId = this.getNodeParameter('caseId', i); + responseData = await elasticSecurityApiRequest.call(this, 'GET', `/cases/${caseId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // case: getAll + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-find-cases.html + + const qs = {} as IDataObject; + const { + tags, + status, + } = this.getNodeParameter('filters', i) as IDataObject & { tags: string[], status: string }; + const sortOptions = this.getNodeParameter('sortOptions', i) as IDataObject; + + qs.sortField = sortOptions.sortField ?? 'createdAt'; + qs.sortOrder = sortOptions.sortOrder ?? 'asc'; + + if (status) { + qs.status = status; + } + + if (tags?.length) { + qs.tags = tags.join(','); + } + + responseData = await handleListing.call(this, 'GET', '/cases/_find', {}, qs); + + } else if (operation === 'getStatus') { + + // ---------------------------------------- + // case: getStatus + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-get-status.html + + responseData = await elasticSecurityApiRequest.call(this, 'GET', '/cases/status'); + + } else if (operation === 'update') { + + // ---------------------------------------- + // case: update + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-update.html + + const caseId = this.getNodeParameter('caseId', i); + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (!Object.keys(updateFields).length) { + throwOnEmptyUpdate.call(this, resource); + } + + const { syncAlerts, ...rest } = updateFields; + + Object.assign(body, { + cases: [ + { + id: caseId, + version: await getVersion.call(this, `/cases/${caseId}`), + ...(syncAlerts && { settings: { syncAlerts } }), + ...rest, + }, + ], + }); + + responseData = await elasticSecurityApiRequest.call(this, 'PATCH', '/cases', body); + + } + + } else if (resource === 'caseTag') { + + // ********************************************************************** + // caseTag + // ********************************************************************** + + if (operation === 'add') { + + // ---------------------------------------- + // caseTag: add + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-create.html + + const caseId = this.getNodeParameter('caseId', i); + + const { + title, + connector, + owner, + description, + settings, + tags, + } = await elasticSecurityApiRequest.call(this, 'GET', `/cases/${caseId}`); + + const tagToAdd = this.getNodeParameter('tag', i); + + if (tags.includes(tagToAdd)) { + throw new NodeOperationError( + this.getNode(), + `Cannot add tag "${tagToAdd}" to case ID ${caseId} because this case already has this tag.`, + ); + } + + const body = {}; + + Object.assign(body, { + cases: [ + { + id: caseId, + title, + connector, + owner, + description, + settings, + version: await getVersion.call(this, `/cases/${caseId}`), + tags: [...tags, tagToAdd], + }, + ], + }); + + responseData = await elasticSecurityApiRequest.call(this, 'PATCH', '/cases', body); + + } else if (operation === 'remove') { + + // https://www.elastic.co/guide/en/security/current/cases-api-update.html + + const caseId = this.getNodeParameter('caseId', i); + const tagToRemove = this.getNodeParameter('tag', i) as string; + + const { + title, + connector, + owner, + description, + settings, + tags, + } = await elasticSecurityApiRequest.call(this, 'GET', `/cases/${caseId}`) as IDataObject & { tags: string[] }; + + if (!tags.includes(tagToRemove)) { + throw new NodeOperationError(this.getNode(), `Cannot remove tag "${tagToRemove}" from case ID ${caseId} because this case does not have this tag.`); + } + + const body = {}; + + Object.assign(body, { + cases: [ + { + id: caseId, + title, + connector, + owner, + description, + settings, + version: await getVersion.call(this, `/cases/${caseId}`), + tags: tags.filter((tag) => tag !== tagToRemove), + }, + ], + }); + + responseData = await elasticSecurityApiRequest.call(this, 'PATCH', '/cases', body); + + } + + } else if (resource === 'caseComment') { + + // ********************************************************************** + // caseComment + // ********************************************************************** + + if (operation === 'add') { + + // ---------------------------------------- + // caseComment: add + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-add-comment.html + + const simple = this.getNodeParameter('simple', i) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body = { + comment: this.getNodeParameter('comment', i), + type: 'user', + owner: additionalFields.owner || 'securitySolution', + } as IDataObject; + + const caseId = this.getNodeParameter('caseId', i); + const endpoint = `/cases/${caseId}/comments`; + responseData = await elasticSecurityApiRequest.call(this, 'POST', endpoint, body); + + if (simple === true) { + const { comments } = responseData; + responseData = comments[comments.length - 1]; + } + + } else if (operation === 'get') { + + // ---------------------------------------- + // caseComment: get + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-get-comment.html + + const caseId = this.getNodeParameter('caseId', i); + const commentId = this.getNodeParameter('commentId', i); + + const endpoint = `/cases/${caseId}/comments/${commentId}`; + responseData = await elasticSecurityApiRequest.call(this, 'GET', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // caseComment: getAll + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-get-all-case-comments.html + + const caseId = this.getNodeParameter('caseId', i); + + const endpoint = `/cases/${caseId}/comments`; + responseData = await handleListing.call(this, 'GET', endpoint); + + } else if (operation === 'remove') { + + // ---------------------------------------- + // caseComment: remove + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-delete-comment.html + + const caseId = this.getNodeParameter('caseId', i); + const commentId = this.getNodeParameter('commentId', i); + + const endpoint = `/cases/${caseId}/comments/${commentId}`; + await elasticSecurityApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + + } else if (operation === 'update') { + + // ---------------------------------------- + // caseComment: update + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/cases-api-update-comment.html + + const simple = this.getNodeParameter('simple', i) as boolean; + const caseId = this.getNodeParameter('caseId', i); + const commentId = this.getNodeParameter('commentId', i); + + const body = { + comment: this.getNodeParameter('comment', i), + id: commentId, + type: 'user', + owner: 'securitySolution', + version: await getVersion.call(this, `/cases/${caseId}/comments/${commentId}`), + } as IDataObject; + + const patchEndpoint = `/cases/${caseId}/comments`; + responseData = await elasticSecurityApiRequest.call(this, 'PATCH', patchEndpoint, body); + + if (simple === true) { + const { comments } = responseData; + responseData = comments[comments.length - 1]; + } + + } + + } else if (resource === 'connector') { + + if (operation === 'create') { + + // ---------------------------------------- + // connector: create + // ---------------------------------------- + + // https://www.elastic.co/guide/en/security/current/register-connector.html + + const connectorType = this.getNodeParameter('connectorType', i) as ConnectorType; + + const body: ConnectorCreatePayload = { + connector_type_id: connectorType, + name: this.getNodeParameter('name', i) as string, + }; + + if (connectorType === '.jira') { + body.config = { + apiUrl: this.getNodeParameter('apiUrl', i) as string, + projectKey: this.getNodeParameter('projectKey', i) as string, + }; + body.secrets = { + email: this.getNodeParameter('email', i) as string, + apiToken: this.getNodeParameter('apiToken', i) as string, + }; + } else if (connectorType === '.resilient') { + body.config = { + apiUrl: this.getNodeParameter('apiUrl', i) as string, + orgId: this.getNodeParameter('orgId', i) as string, + }; + body.secrets = { + apiKeyId: this.getNodeParameter('apiKeyId', i) as string, + apiKeySecret: this.getNodeParameter('apiKeySecret', i) as string, + }; + } else if (connectorType === '.servicenow') { + body.config = { + apiUrl: this.getNodeParameter('apiUrl', i) as string, + }; + body.secrets = { + username: this.getNodeParameter('username', i) as string, + password: this.getNodeParameter('password', i) as string, + }; + } + + responseData = await elasticSecurityApiRequest.call(this, 'POST', '/actions/connector', body); + + } + + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/GenericFunctions.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/GenericFunctions.ts new file mode 100644 index 0000000000..cf6aa17406 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/GenericFunctions.ts @@ -0,0 +1,163 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +import { + Connector, + ElasticSecurityApiCredentials, +} from './types'; + +export async function elasticSecurityApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const { + username, + password, + baseUrl: rawBaseUrl, + } = await this.getCredentials('elasticSecurityApi') as ElasticSecurityApiCredentials; + + const baseUrl = tolerateTrailingSlash(rawBaseUrl); + + const token = Buffer.from(`${username}:${password}`).toString('base64'); + + const options: OptionsWithUri = { + headers: { + Authorization: `Basic ${token}`, + 'kbn-xsrf': true, + }, + method, + body, + qs, + uri: `${baseUrl}/api${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + if (error?.error?.error === 'Not Acceptable' && error?.error?.message) { + error.error.error = `${error.error.error}: ${error.error.message}`; + } + + throw new NodeApiError(this.getNode(), error); + } +} + +export async function elasticSecurityApiRequestAllItems( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + let page = 1; + const returnData: IDataObject[] = []; + let responseData: any; // tslint:disable-line + + const resource = this.getNodeParameter('resource', 0) as 'case' | 'caseComment'; + + do { + responseData = await elasticSecurityApiRequest.call(this, method, endpoint, body, qs); + page++; + + const items = resource === 'case' + ? responseData.cases + : responseData; + + returnData.push(...items); + } while (returnData.length < responseData.total); + + return returnData; +} + +export async function handleListing( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll) { + return await elasticSecurityApiRequestAllItems.call(this, method, endpoint, body, qs); + } + + const responseData = await elasticSecurityApiRequestAllItems.call(this, method, endpoint, body, qs); + const limit = this.getNodeParameter('limit', 0) as number; + + return responseData.slice(0, limit); +} + +/** + * Retrieve a connector name and type from a connector ID. + * + * https://www.elastic.co/guide/en/kibana/master/get-connector-api.html + */ +export async function getConnector( + this: IExecuteFunctions, + connectorId: string, +) { + const endpoint = `/actions/connector/${connectorId}`; + const { + id, + name, + connector_type_id: type, + } = await elasticSecurityApiRequest.call(this, 'GET', endpoint) as Connector; + + return { id, name, type }; +} + +export function throwOnEmptyUpdate( + this: IExecuteFunctions, + resource: string, +) { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}`, + ); +} + +export async function getVersion( + this: IExecuteFunctions, + endpoint: string, +) { + const { version } = await elasticSecurityApiRequest.call(this, 'GET', endpoint) as { + version?: string; + }; + + if (!version) { + throw new NodeOperationError(this.getNode(), 'Cannot retrieve version for resource'); + } + + return version; +} + +export function tolerateTrailingSlash(baseUrl: string) { + return baseUrl.endsWith('/') + ? baseUrl.substr(0, baseUrl.length - 1) + : baseUrl; +} diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseCommentDescription.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseCommentDescription.ts new file mode 100644 index 0000000000..c438932b34 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseCommentDescription.ts @@ -0,0 +1,345 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const caseCommentOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a comment to a case', + }, + { + name: 'Get', + value: 'get', + description: 'Get a case comment', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all case comments', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a comment from a case', + }, + { + name: 'Update', + value: 'update', + description: 'Update a comment in a case', + }, + ], + default: 'add', + }, +]; + +export const caseCommentFields: INodeProperties[] = [ + // ---------------------------------------- + // caseComment: add + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + description: 'ID of the case containing the comment to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Comment', + name: 'comment', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Simplify Response', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'add', + ], + }, + }, + default: true, + description: 'Whether to return a simplified version of the response instead of the raw data', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'add', + ], + }, + }, + options: [ + { + displayName: 'Owner', + name: 'owner', + type: 'string', + description: 'Valid application owner registered within the Cases Role Based Access Control system', + default: '', + }, + ], + }, + + // ---------------------------------------- + // caseComment: get + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + description: 'ID of the case containing the comment to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Comment ID', + name: 'commentId', + description: 'ID of the case comment to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // caseComment: getAll + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + 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: [ + 'caseComment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------------- + // caseComment: remove + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + description: 'ID of the case containing the comment to remove', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'remove', + ], + }, + }, + }, + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'remove', + ], + }, + }, + }, + + // ---------------------------------------- + // caseComment: update + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + description: 'ID of the case containing the comment to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Comment', + name: 'comment', + description: 'Text to replace current comment message', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Simplify Response', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'caseComment', + ], + operation: [ + 'update', + ], + }, + }, + default: true, + description: 'Whether to return a simplified version of the response instead of the raw data', + }, +]; diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseDescription.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseDescription.ts new file mode 100644 index 0000000000..5511add9e5 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseDescription.ts @@ -0,0 +1,659 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const caseOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'case', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a case', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a case', + }, + { + name: 'Get', + value: 'get', + description: 'Get a case', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all cases', + }, + { + name: 'Get Status', + value: 'getStatus', + description: 'Retrieve a summary of all case activity', + }, + { + name: 'Update', + value: 'update', + description: 'Update a case', + }, + ], + default: 'create', + }, +]; + +export const caseFields: INodeProperties[] = [ + // ---------------------------------------- + // case: create + // ---------------------------------------- + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Connector Name', + name: 'connectorId', + description: 'Connectors allow you to send Elastic Security cases into other systems (only ServiceNow, Jira, or IBM Resilient)', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getConnectors', + }, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Connector Type', + name: 'connectorType', + type: 'options', + required: true, + default: '.jira', + options: [ + { + name: 'IBM Resilient', + value: '.resilient', + }, + { + name: 'Jira', + value: '.jira', + }, + { + name: 'ServiceNow ITSM', + value: '.servicenow', + }, + ], + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Issue Type', + name: 'issueType', + description: 'Type of the Jira issue to create for this case', + type: 'string', + placeholder: 'Task', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.jira', + ], + }, + }, + }, + { + displayName: 'Priority', + name: 'priority', + description: 'Priority of the Jira issue to create for this case', + type: 'string', + placeholder: 'High', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.jira', + ], + }, + }, + }, + { + displayName: 'Urgency', + name: 'urgency', + description: 'Urgency of the ServiceNow ITSM issue to create for this case', + type: 'options', + required: true, + default: 1, + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.servicenow', + ], + }, + }, + }, + { + displayName: 'Severity', + name: 'severity', + description: 'Severity of the ServiceNow ITSM issue to create for this case', + type: 'options', + required: true, + default: 1, + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.servicenow', + ], + }, + }, + }, + { + displayName: 'Impact', + name: 'impact', + description: 'Impact of the ServiceNow ITSM issue to create for this case', + type: 'options', + required: true, + default: 1, + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.servicenow', + ], + }, + }, + }, + { + displayName: 'Category', + name: 'category', + type: 'string', + description: 'Category of the ServiceNow ITSM issue to create for this case', + required: true, + default: '', + placeholder: 'Helpdesk', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.servicenow', + ], + }, + }, + }, + { + displayName: 'Issue Types', + name: 'issueTypes', + description: 'Comma-separated list of numerical types of the IBM Resilient issue to create for this case', + type: 'string', + placeholder: '123,456', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.resilient', + ], + }, + }, + }, + { + displayName: 'Severity Code', + name: 'severityCode', + description: 'Severity code of the IBM Resilient issue to create for this case', + type: 'number', + typeOptions: { + minValue: 0, + }, + required: true, + default: 1, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + connectorType: [ + '.resilient', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + description: 'Valid application owner registered within the Cases Role Based Access Control system', + default: '', + }, + { + displayName: 'Sync Alerts', + name: 'syncAlerts', + description: 'Whether to synchronize with alerts', + type: 'boolean', + default: false, + }, + ], + }, + + // ---------------------------------------- + // case: delete + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // case: get + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // case: 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: [ + 'case', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +{ + displayName: 'Filters', + name: 'filters', + type: 'collection', + default: {}, + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'In Progress', + value: 'in-progress', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: 'open', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + default: [], + typeOptions: { + loadOptionsMethod: 'getTags', + }, + }, + ], + }, + { + displayName: 'Sort', + name: 'sortOptions', + type: 'fixedCollection', + placeholder: 'Add Sort Options', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Sort Options', + name: 'sortOptionsProperties', + values: [ + { + displayName: 'Sort Key', + name: 'sortField', + type: 'options', + options: [ + { + name: 'Created At', + value: 'createdAt', + }, + { + name: 'Updated At', + value: 'updatedAt', + }, + ], + default: 'createdAt', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'asc', + }, + { + name: 'Descending', + value: 'desc', + }, + ], + default: 'asc', + }, + ], + }, + ], + }, + + // ---------------------------------------- + // case: update + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'open', + options: [ + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Open', + value: 'open', + }, + { + name: 'In Progress', + value: 'in-progress', + }, + ], + }, + { + displayName: 'Sync Alerts', + name: 'syncAlerts', + description: 'Whether to synchronize with alerts', + type: 'boolean', + default: false, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Version', + name: 'version', + type: 'string', + default: '', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseTagDescription.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseTagDescription.ts new file mode 100644 index 0000000000..d942ceba14 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/CaseTagDescription.ts @@ -0,0 +1,117 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const caseTagOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'caseTag', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a tag to a case', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a tag from a case', + }, + ], + default: 'add', + }, +]; + +export const caseTagFields: INodeProperties[] = [ + // ---------------------------------------- + // caseTag: add + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseTag', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Tag', + name: 'tag', + type: 'options', + description: 'Tag to attach to the case. Choose from the list or enter a new one with an expression.', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + displayOptions: { + show: { + resource: [ + 'caseTag', + ], + operation: [ + 'add', + ], + }, + }, + }, + + // ---------------------------------------- + // caseTag: remove + // ---------------------------------------- + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'caseTag', + ], + operation: [ + 'remove', + ], + }, + }, + }, + { + displayName: 'Tag', + name: 'tag', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + displayOptions: { + show: { + resource: [ + 'caseTag', + ], + operation: [ + 'remove', + ], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/ConnectorDescription.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/ConnectorDescription.ts new file mode 100644 index 0000000000..8fee7a4fee --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/ConnectorDescription.ts @@ -0,0 +1,268 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const connectorOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'connector', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a connector', + }, + ], + default: 'create', + }, +]; + +export const connectorFields: INodeProperties[] = [ + // ---------------------------------------- + // connector: create + // ---------------------------------------- + { + displayName: 'Connector Name', + name: 'name', + description: 'Connectors allow you to send Elastic Security cases into other systems (only ServiceNow, Jira, or IBM Resilient)', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Connector Type', + name: 'connectorType', + type: 'options', + required: true, + default: '.jira', + options: [ + { + name: 'IBM Resilient', + value: '.resilient', + }, + { + name: 'Jira', + value: '.jira', + }, + { + name: 'ServiceNow ITSM', + value: '.servicenow', + }, + ], + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'API URL', + name: 'apiUrl', + type: 'string', + description: 'URL of the third-party instance', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + description: 'Jira-registered email', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.jira', + ], + }, + }, + }, + { + displayName: 'API Token', + name: 'apiToken', + description: 'Jira API token', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.jira', + ], + }, + }, + }, + { + displayName: 'Project Key', + name: 'projectKey', + description: 'Jira Project Key', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.jira', + ], + }, + }, + }, + { + displayName: 'Username', + name: 'username', + description: 'ServiceNow ITSM username', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.servicenow', + ], + }, + }, + }, + { + displayName: 'Password', + name: 'password', + description: 'ServiceNow ITSM password', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.servicenow', + ], + }, + }, + }, + { + displayName: 'API Key ID', + name: 'apiKeyId', + description: 'IBM Resilient API key ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.resilient', + ], + }, + }, + }, + { + displayName: 'API Key Secret', + name: 'apiKeySecret', + description: 'IBM Resilient API key secret', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.resilient', + ], + }, + }, + }, + { + displayName: 'Organization ID', + name: 'orgId', + description: 'IBM Resilient organization ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'connector', + ], + operation: [ + 'create', + ], + connectorType: [ + '.resilient', + ], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/index.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/index.ts new file mode 100644 index 0000000000..0989371296 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/descriptions/index.ts @@ -0,0 +1,4 @@ +export * from './CaseDescription'; +export * from './CaseCommentDescription'; +export * from './CaseTagDescription'; +export * from './ConnectorDescription'; diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/elasticSecurity.svg b/packages/nodes-base/nodes/Elastic/ElasticSecurity/elasticSecurity.svg new file mode 100644 index 0000000000..307e912c45 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/elasticSecurity.svg @@ -0,0 +1,11 @@ + + + security-logo-color-32px + Created with Sketch. + + + + + + + diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/types.d.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/types.d.ts new file mode 100644 index 0000000000..7788a04255 --- /dev/null +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/types.d.ts @@ -0,0 +1,56 @@ +export type ElasticSecurityApiCredentials = { + username: string; + password: string; + baseUrl: string; +}; + +export type ConnectorType = '.jira' | '.servicenow' | '.resilient'; + +export type Connector = { + id: string; + name: string; + connector_type_id: ConnectorType +}; + +export type ConnectorCreatePayload = + | ServiceNowConnectorCreatePayload + | JiraConnectorCreatePayload + | IbmResilientConnectorCreatePayload; + +type ServiceNowConnectorCreatePayload = { + connector_type_id: '.servicenow', + name: string, + secrets?: { + username: string; + password: string; + }, + config?: { + apiUrl: string; + }, +}; + +type JiraConnectorCreatePayload = { + connector_type_id: '.jira', + name: string, + secrets?: { + email: string; + apiToken: string; + }, + config?: { + apiUrl: string; + projectKey: string; + }, +}; + +type IbmResilientConnectorCreatePayload = { + connector_type_id: '.resilient', + name: string, + secrets?: { + apiKeyId: string; + apiKeySecret: string; + }, + config?: { + apiUrl: string; + orgId: string; + }, +}; diff --git a/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.json b/packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.json similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.json rename to packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.json diff --git a/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/Elasticsearch.node.ts diff --git a/packages/nodes-base/nodes/Elasticsearch/GenericFunctions.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/GenericFunctions.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/DocumentDescription.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/DocumentDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/descriptions/DocumentDescription.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/DocumentDescription.ts diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/IndexDescription.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/IndexDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/descriptions/IndexDescription.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/IndexDescription.ts diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/index.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/index.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/descriptions/index.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/index.ts diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/placeholders.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/placeholders.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/descriptions/placeholders.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/descriptions/placeholders.ts diff --git a/packages/nodes-base/nodes/Elasticsearch/elasticsearch.svg b/packages/nodes-base/nodes/Elastic/Elasticsearch/elasticsearch.svg similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/elasticsearch.svg rename to packages/nodes-base/nodes/Elastic/Elasticsearch/elasticsearch.svg diff --git a/packages/nodes-base/nodes/Elasticsearch/types.d.ts b/packages/nodes-base/nodes/Elastic/Elasticsearch/types.d.ts similarity index 100% rename from packages/nodes-base/nodes/Elasticsearch/types.d.ts rename to packages/nodes-base/nodes/Elastic/Elasticsearch/types.d.ts diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ff7fcaff3d..004cbc0a4c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -80,6 +80,7 @@ "dist/credentials/DropboxOAuth2Api.credentials.js", "dist/credentials/EgoiApi.credentials.js", "dist/credentials/ElasticsearchApi.credentials.js", + "dist/credentials/ElasticSecurityApi.credentials.js", "dist/credentials/EmeliaApi.credentials.js", "dist/credentials/ERPNextApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", @@ -374,7 +375,8 @@ "dist/nodes/Dropbox/Dropbox.node.js", "dist/nodes/EditImage.node.js", "dist/nodes/Egoi/Egoi.node.js", - "dist/nodes/Elasticsearch/Elasticsearch.node.js", + "dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js", + "dist/nodes/Elastic/Elasticsearch/Elasticsearch.node.js", "dist/nodes/EmailReadImap.node.js", "dist/nodes/EmailSend.node.js", "dist/nodes/Emelia/Emelia.node.js",