From 73e782e2cf9d4b96d8b3748e74ad93570663e536 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:15:52 +0300 Subject: [PATCH] feat(TheHive Node): Overhaul (#6457) --- .../ResourceMapper/MappingFields.vue | 7 + .../credentials/TheHiveApi.credentials.ts | 9 +- .../TheHiveProjectApi.credentials.ts | 57 ++ .../TheHiveProject/TheHiveProject.node.json | 19 + .../TheHiveProject/TheHiveProject.node.ts | 15 + .../TheHiveProjectTrigger.node.json | 13 + .../TheHiveProjectTrigger.node.ts | 304 ++++++++++ .../actions/alert/create.operation.ts | 195 +++++++ .../actions/alert/deleteAlert.operation.ts | 27 + .../alert/executeResponder.operation.ts | 76 +++ .../actions/alert/get.operation.ts | 98 ++++ .../TheHiveProject/actions/alert/index.ts | 85 +++ .../actions/alert/merge.operation.ts | 41 ++ .../actions/alert/promote.operation.ts | 69 +++ .../actions/alert/search.operation.ts | 60 ++ .../actions/alert/status.operation.ts | 42 ++ .../actions/alert/update.operation.ts | 151 +++++ .../actions/case/addAttachment.operation.ts | 89 +++ .../actions/case/create.operation.ts | 82 +++ .../case/deleteAttachment.operation.ts | 42 ++ .../actions/case/deleteCase.operation.ts | 27 + .../case/executeResponder.operation.ts | 73 +++ .../actions/case/get.operation.ts | 53 ++ .../actions/case/getAttachment.operation.ts | 112 ++++ .../actions/case/getTimeline.operation.ts | 34 ++ .../TheHiveProject/actions/case/index.ts | 104 ++++ .../actions/case/search.operation.ts | 60 ++ .../actions/case/update.operation.ts | 147 +++++ .../actions/comment/add.operation.ts | 86 +++ .../comment/deleteComment.operation.ts | 27 + .../TheHiveProject/actions/comment/index.ts | 50 ++ .../actions/comment/search.operation.ts | 118 ++++ .../actions/comment/update.operation.ts | 51 ++ .../actions/log/addAttachment.operation.ts | 66 +++ .../actions/log/create.operation.ts | 115 ++++ .../actions/log/deleteAttachment.operation.ts | 43 ++ .../actions/log/deleteLog.operation.ts | 27 + .../actions/log/executeResponder.operation.ts | 72 +++ .../actions/log/get.operation.ts | 43 ++ .../nodes/TheHiveProject/actions/log/index.ts | 71 +++ .../actions/log/search.operation.ts | 87 +++ .../actions/node.description.ts | 85 +++ .../nodes/TheHiveProject/actions/node.type.ts | 47 ++ .../actions/observable/create.operation.ts | 189 ++++++ .../observable/deleteObservable.operation.ts | 29 + .../observable/executeAnalyzer.operation.ts | 93 +++ .../observable/executeResponder.operation.ts | 73 +++ .../actions/observable/get.operation.ts | 49 ++ .../actions/observable/index.ts | 71 +++ .../actions/observable/search.operation.ts | 118 ++++ .../actions/observable/update.operation.ts | 140 +++++ .../actions/page/create.operation.ts | 102 ++++ .../actions/page/deletePage.operation.ts | 63 ++ .../TheHiveProject/actions/page/index.ts | 50 ++ .../actions/page/search.operation.ts | 96 ++++ .../actions/page/update.operation.ts | 118 ++++ .../actions/query/executeQuery.operation.ts | 77 +++ .../TheHiveProject/actions/query/index.ts | 29 + .../nodes/TheHiveProject/actions/router.ts | 80 +++ .../actions/task/create.operation.ts | 75 +++ .../actions/task/deleteTask.operation.ts | 27 + .../task/executeResponder.operation.ts | 75 +++ .../actions/task/get.operation.ts | 47 ++ .../TheHiveProject/actions/task/index.ts | 64 +++ .../actions/task/search.operation.ts | 87 +++ .../actions/task/update.operation.ts | 133 +++++ .../descriptions/common.description.ts | 542 +++++++++++++++++ .../descriptions/filter.description.ts | 318 ++++++++++ .../TheHiveProject/descriptions/index.ts | 3 + .../descriptions/rlc.description.ts | 285 +++++++++ .../nodes/TheHiveProject/helpers/constants.ts | 543 ++++++++++++++++++ .../TheHiveProject/helpers/interfaces.ts | 8 + .../nodes/TheHiveProject/helpers/utils.ts | 100 ++++ .../nodes/TheHiveProject/methods/index.ts | 3 + .../TheHiveProject/methods/listSearch.ts | 246 ++++++++ .../TheHiveProject/methods/loadOptions.ts | 348 +++++++++++ .../TheHiveProject/methods/resourceMapping.ts | 442 ++++++++++++++ .../TheHiveProject/test/transport.test.ts | 164 ++++++ .../nodes/TheHiveProject/test/utils.test.ts | 179 ++++++ .../nodes/TheHiveProject/thehiveproject.svg | 1 + .../nodes/TheHiveProject/transport/index.ts | 2 + .../TheHiveProject/transport/queryHelper.ts | 101 ++++ .../TheHiveProject/transport/requestApi.ts | 42 ++ packages/nodes-base/package.json | 3 + packages/workflow/src/Interfaces.ts | 1 + 85 files changed, 8291 insertions(+), 4 deletions(-) create mode 100644 packages/nodes-base/credentials/TheHiveProjectApi.credentials.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.json create mode 100644 packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.json create mode 100644 packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/deleteAlert.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/executeResponder.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/get.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/merge.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/promote.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/status.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/alert/update.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/addAttachment.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/create.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/deleteAttachment.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/deleteCase.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/executeResponder.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/get.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/getAttachment.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/getTimeline.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/case/update.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/comment/add.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/comment/deleteComment.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/comment/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/comment/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/comment/update.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/addAttachment.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/create.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/deleteAttachment.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/deleteLog.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/executeResponder.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/get.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/log/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/node.description.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/node.type.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/create.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/deleteObservable.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/executeAnalyzer.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/executeResponder.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/get.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/observable/update.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/page/create.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/page/deletePage.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/page/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/page/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/page/update.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/query/executeQuery.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/query/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/router.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/create.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/deleteTask.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/executeResponder.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/get.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/search.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/actions/task/update.operation.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/descriptions/common.description.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/descriptions/filter.description.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/descriptions/rlc.description.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/helpers/constants.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/helpers/interfaces.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/helpers/utils.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/methods/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/methods/listSearch.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/methods/loadOptions.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/methods/resourceMapping.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/test/transport.test.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/test/utils.test.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/thehiveproject.svg create mode 100644 packages/nodes-base/nodes/TheHiveProject/transport/index.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/transport/queryHelper.ts create mode 100644 packages/nodes-base/nodes/TheHiveProject/transport/requestApi.ts diff --git a/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue b/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue index 168b7cafed..dd6e295cbb 100644 --- a/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue +++ b/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue @@ -159,7 +159,14 @@ const resourceMapperMode = computed(() => { return resourceMapperTypeOptions.value?.mode; }); +const resourceMapperValuesLabel = computed(() => { + return resourceMapperTypeOptions.value?.valuesLabel; +}); + const valuesLabel = computed(() => { + if (resourceMapperValuesLabel.value) { + return resourceMapperValuesLabel.value; + } if (resourceMapperMode.value && resourceMapperMode.value === 'update') { return locale.baseText('resourceMapper.valuesToUpdate.label'); } diff --git a/packages/nodes-base/credentials/TheHiveApi.credentials.ts b/packages/nodes-base/credentials/TheHiveApi.credentials.ts index 6766f6da78..694cc47967 100644 --- a/packages/nodes-base/credentials/TheHiveApi.credentials.ts +++ b/packages/nodes-base/credentials/TheHiveApi.credentials.ts @@ -36,14 +36,15 @@ export class TheHiveApi implements ICredentialType { description: 'The version of api to be used', options: [ { - name: 'Version 1', + name: 'TheHive 4+ (api v1)', value: 'v1', - description: 'API version supported by TheHive 4', + description: + 'API version with TheHive 4 support, also works with TheHive 5 but not all features are supported', }, { - name: 'Version 0', + name: 'TheHive 3 (api v0)', value: '', - description: 'API version supported by TheHive 3', + description: 'API version with TheHive 3 support', }, ], }, diff --git a/packages/nodes-base/credentials/TheHiveProjectApi.credentials.ts b/packages/nodes-base/credentials/TheHiveProjectApi.credentials.ts new file mode 100644 index 0000000000..65ced26842 --- /dev/null +++ b/packages/nodes-base/credentials/TheHiveProjectApi.credentials.ts @@ -0,0 +1,57 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class TheHiveProjectApi implements ICredentialType { + name = 'theHiveProjectApi'; + + displayName = 'The Hive 5 API'; + + documentationUrl = 'theHive'; + + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'ApiKey', + type: 'string', + default: '', + typeOptions: { + password: true, + }, + }, + { + displayName: 'URL', + name: 'url', + default: '', + type: 'string', + description: 'The URL of TheHive instance', + placeholder: 'https://localhost:9000', + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + description: 'Whether to connect even if SSL certificate validation is not possible', + default: false, + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials?.ApiKey}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials?.url}}', + url: '/api/case', + }, + }; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.json b/packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.json new file mode 100644 index 0000000000..8fe1e4ea76 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.theHiveProject", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development"], + "alias": ["Security", "Monitoring", "Incident", "Response", "Alert"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/theHive" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.thehive/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.ts b/packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.ts new file mode 100644 index 0000000000..257a4b7cc7 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/TheHiveProject.node.ts @@ -0,0 +1,15 @@ +import type { IExecuteFunctions, INodeType, INodeTypeDescription } from 'n8n-workflow'; + +import { description } from './actions/node.description'; +import { router } from './actions/router'; +import { loadOptions, listSearch, resourceMapping } from './methods'; + +export class TheHiveProject implements INodeType { + description: INodeTypeDescription = description; + + methods = { loadOptions, listSearch, resourceMapping }; + + async execute(this: IExecuteFunctions) { + return router.call(this); + } +} diff --git a/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.json b/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.json new file mode 100644 index 0000000000..def7a19826 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.json @@ -0,0 +1,13 @@ +{ + "node": "n8n-nodes-base.theHiveProjectTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development"], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.thehivetrigger/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts b/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts new file mode 100644 index 0000000000..93a6c2315d --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/TheHiveProjectTrigger.node.ts @@ -0,0 +1,304 @@ +import type { + IWebhookFunctions, + IDataObject, + IHookFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import get from 'lodash/get'; + +export class TheHiveProjectTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'TheHive 5 Trigger', + name: 'theHiveProjectTrigger', + icon: 'file:thehiveproject.svg', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when TheHive events occur', + defaults: { + name: 'TheHive Trigger', + }, + inputs: [], + outputs: ['main'], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + default: [], + required: true, + description: 'Events types', + // eslint-disable-next-line n8n-nodes-base/node-param-multi-options-type-unsorted-items + options: [ + { + name: '*', + value: '*', + description: 'Any time any event is triggered (Wildcard Event)', + }, + { + name: 'Alert Created', + value: 'alert_create', + description: 'Triggered when an alert is created', + }, + { + name: 'Alert Deleted', + value: 'alert_delete', + description: 'Triggered when an alert is deleted', + }, + { + name: 'Alert Updated', + value: 'alert_update', + description: 'Triggered when an alert is updated', + }, + { + name: 'Case Created', + value: 'case_create', + description: 'Triggered when a case is created', + }, + { + name: 'Case Deleted', + value: 'case_delete', + description: 'Triggered when a case is deleted', + }, + { + name: 'Case Updated', + value: 'case_update', + description: 'Triggered when a case is updated', + }, + { + name: 'Comment Created', + value: 'comment_create', + description: 'Triggered when a comment is created', + }, + { + name: 'Comment Deleted', + value: 'comment_delete', + description: 'Triggered when a comment is deleted', + }, + { + name: 'Comment Updated', + value: 'comment_update', + description: 'Triggered when a comment is updated', + }, + { + name: 'Observable Created', + value: 'observable_create', + description: 'Triggered when an observable is created', + }, + { + name: 'Observable Deleted', + value: 'observable_delete', + description: 'Triggered when an observable is deleted', + }, + { + name: 'Observable Updated', + value: 'observable_update', + description: 'Triggered when an observable is updated', + }, + { + name: 'Page Created', + value: 'page_create', + description: 'Triggered when an page is created', + }, + { + name: 'Page Deleted', + value: 'page_delete', + description: 'Triggered when an page is deleted', + }, + { + name: 'Page Updated', + value: 'page_update', + description: 'Triggered when an page is updated', + }, + { + name: 'Task Created', + value: 'task_create', + description: 'Triggered when a task is created', + }, + { + name: 'Task Updated', + value: 'task_update', + description: 'Triggered when a task is updated', + }, + { + name: 'Task Log Created', + value: 'log_create', + description: 'Triggered when a task log is created', + }, + { + name: 'Task Log Deleted', + value: 'log_delete', + description: 'Triggered when a task log is deleted', + }, + { + name: 'Task Log Updated', + value: 'log_update', + description: 'Triggered when a task log is updated', + }, + ], + }, + { + displayName: 'Filters', + description: 'Filter any incoming events based on their fields', + name: 'filters', + type: 'fixedCollection', + placeholder: 'Add Filter', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'string', + placeholder: 'e.g. context.severity', + default: '', + hint: 'The field to filter on, supports dot notation', + }, + { + displayName: 'Operator', + name: 'operator', + type: 'options', + options: [ + { + name: 'Equal', + value: 'equal', + description: 'Field is equal to value', + }, + { + name: 'Not Equal', + value: 'notEqual', + description: 'Field is not equal to value', + }, + { + name: 'Includes', + value: 'includes', + description: 'Field includes value', + }, + ], + default: 'equal', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Output Only Data', + description: 'Whether to output data with additional details and omit headers', + name: 'outputOnlyData', + type: 'boolean', + default: false, + }, + ], + }, + ], + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + return true; + }, + async create(this: IHookFunctions): Promise { + return true; + }, + async delete(this: IHookFunctions): Promise { + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + // Get the request body + const bodyData = this.getBodyData(); + const events = this.getNodeParameter('events', []) as string[]; + + if (!bodyData.action || !bodyData.objectType) { + // Don't start the workflow if mandatory fields are not specified + return {}; + } + + const action = (bodyData.action as string).toLowerCase(); + const objectType = (bodyData.objectType as string).toLowerCase(); + const event = `${objectType}_${action}`; + + if (events.indexOf('*') === -1 && events.indexOf(event) === -1) { + return {}; + } + + const filters = this.getNodeParameter('filters.values', []) as IDataObject[]; + + if (filters.length) { + for (const filter of filters) { + const field = filter.field as string; + const operator = filter.operator as string; + const expectedValue = filter.value as string; + const actualValue = get(bodyData, field); + + if (operator === 'equal') { + if (actualValue !== expectedValue) { + return {}; + } + } + if (operator === 'notEqual') { + if (actualValue === expectedValue) { + return {}; + } + } + if (operator === 'includes') { + if (!String(actualValue).includes(expectedValue)) { + return {}; + } + } + } + } + + // The data to return and so start the workflow with + const returnData: IDataObject[] = []; + + const outputOnlyData = this.getNodeParameter('options.outputOnlyData', false) as boolean; + + if (outputOnlyData) { + returnData.push(bodyData); + } else { + returnData.push({ + event, + body: this.getBodyData(), + headers: this.getHeaderData(), + query: this.getQueryData(), + }); + } + + return { + workflowData: [this.helpers.returnJsonArray(returnData)], + }; + } +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts new file mode 100644 index 0000000000..911ebf26b3 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts @@ -0,0 +1,195 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; + +import set from 'lodash/set'; + +import FormData from 'form-data'; +import { fixFieldType, prepareInputItem, splitAndTrim } from '../../helpers/utils'; +import { observableTypeOptions } from '../../descriptions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Fields', + name: 'alertFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getAlertFields', + mode: 'add', + valuesLabel: 'Fields', + }, + }, + }, + { + displayName: 'Observables', + name: 'observableUi', + type: 'fixedCollection', + placeholder: 'Add Observable', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + observableTypeOptions, + { + displayName: 'Data', + name: 'data', + type: 'string', + displayOptions: { + hide: { + dataType: ['file'], + }, + }, + default: '', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + displayOptions: { + show: { + dataType: ['file'], + }, + }, + default: 'data', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let responseData: IDataObject | IDataObject[] = []; + let inputData: IDataObject = {}; + + const dataMode = this.getNodeParameter('alertFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('alertFields.schema', i) as IDataObject[]; + inputData = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const alertFields = this.getNodeParameter('alertFields.value', i, []) as IDataObject; + inputData = alertFields; + } + + inputData = fixFieldType(inputData); + + const body: IDataObject = {}; + + for (const field of Object.keys(inputData)) { + // use set to construct the updateBody, as it allows to process customFields.fieldName + // if customFields provided under customFields property, it will be send as is + set(body, field, inputData[field]); + } + + let multiPartRequest = false; + const formData = new FormData(); + + const observableUi = this.getNodeParameter('observableUi', i) as IDataObject; + if (observableUi) { + const values = observableUi.values as IDataObject[]; + + if (values) { + const observables = []; + + for (const value of values) { + const observable: IDataObject = {}; + + observable.dataType = value.dataType as string; + observable.message = value.message as string; + observable.tags = splitAndTrim(value.tags as string); + + if (value.dataType === 'file') { + multiPartRequest = true; + + const attachmentIndex = `attachment${i}`; + observable.attachment = attachmentIndex; + + const binaryPropertyName = value.binaryProperty as string; + const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); + + formData.append(attachmentIndex, binaryData.data, { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }); + } else { + observable.data = value.data as string; + } + + observables.push(observable); + } + body.observables = observables; + } + } + + if (multiPartRequest) { + formData.append('_json', JSON.stringify(body)); + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/v1/alert', + undefined, + undefined, + undefined, + { + Headers: { + 'Content-Type': 'multipart/form-data', + }, + formData, + }, + ); + } else { + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/alert' as string, body); + } + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/deleteAlert.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/deleteAlert.operation.ts new file mode 100644 index 0000000000..8cb16408f8 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/deleteAlert.operation.ts @@ -0,0 +1,27 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { alertRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [alertRLC]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['deleteAlert'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/alert/${alertId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/executeResponder.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/executeResponder.operation.ts new file mode 100644 index 0000000000..639bc8cab6 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/executeResponder.operation.ts @@ -0,0 +1,76 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { alertRLC, responderOptions } from '../../descriptions'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [{ ...alertRLC, name: 'id' }, responderOptions]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['executeResponder'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const alertId = this.getNodeParameter('id', i, '', { extractValue: true }) as string; + const responderId = this.getNodeParameter('responder', i) as string; + let body: IDataObject; + let response; + responseData = []; + body = { + responderId, + objectId: alertId, + objectType: 'alert', + }; + response = await theHiveApiRequest.call(this, 'POST', '/connector/cortex/action' as string, body); + body = { + query: [ + { + _name: 'listAction', + }, + { + _name: 'filter', + _and: [ + { + _field: 'cortexId', + _value: response.cortexId, + }, + { + _field: 'objectId', + _value: response.objectId, + }, + { + _field: 'startDate', + _value: response.startDate, + }, + ], + }, + ], + }; + + const qs: IDataObject = {}; + + qs.name = 'log-actions'; + + do { + response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + } while (response.status === 'Waiting' || response.status === 'InProgress'); + + responseData = response; + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/get.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/get.operation.ts new file mode 100644 index 0000000000..34fd5fca24 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/get.operation.ts @@ -0,0 +1,98 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { alertRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + alertRLC, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Include Similar Alerts', + name: 'includeSimilarAlerts', + type: 'boolean', + description: 'Whether to include similar cases', + default: false, + }, + { + displayName: 'Include Similar Cases', + name: 'includeSimilarCases', + type: 'boolean', + description: 'Whether to include similar cases', + default: false, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject; + + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + const options = this.getNodeParameter('options', i, {}); + + responseData = await theHiveApiRequest.call(this, 'GET', `/v1/alert/${alertId}`); + + if (responseData && options.includeSimilarAlerts) { + const similarAlerts = await theHiveApiRequest.call(this, 'POST', '/v1/query', { + query: [ + { + _name: 'getAlert', + idOrName: alertId, + }, + { + _name: 'similarAlerts', + }, + ], + }); + + responseData = { + ...responseData, + similarAlerts, + }; + } + + if (responseData && options.includeSimilarCases) { + const similarCases = await theHiveApiRequest.call(this, 'POST', '/v1/query', { + query: [ + { + _name: 'getAlert', + idOrName: alertId, + }, + { + _name: 'similarCases', + }, + ], + }); + + responseData = { + ...responseData, + similarCases, + }; + } + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/index.ts new file mode 100644 index 0000000000..4e3801bf7c --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/index.ts @@ -0,0 +1,85 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as executeResponder from './executeResponder.operation'; +import * as deleteAlert from './deleteAlert.operation'; +import * as get from './get.operation'; +import * as search from './search.operation'; +import * as status from './status.operation'; +import * as merge from './merge.operation'; +import * as promote from './promote.operation'; +import * as update from './update.operation'; + +export { create, executeResponder, deleteAlert, get, search, status, merge, promote, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + required: true, + options: [ + { + name: 'Create', + value: 'create', + action: 'Create an alert', + }, + { + name: 'Delete', + value: 'deleteAlert', + action: 'Delete an alert', + }, + { + name: 'Execute Responder', + value: 'executeResponder', + action: 'Execute responder on an alert', + }, + { + name: 'Get', + value: 'get', + action: 'Get an alert', + }, + { + name: 'Merge Into Case', + value: 'merge', + action: 'Merge an alert into a case', + }, + { + name: 'Promote to Case', + value: 'promote', + action: 'Promote an alert to a case', + }, + { + name: 'Search', + value: 'search', + action: 'Search alerts', + }, + { + name: 'Update', + value: 'update', + action: 'Update an alert', + }, + { + name: 'Update Status', + value: 'status', + action: 'Update an alert status', + }, + ], + displayOptions: { + show: { + resource: ['alert'], + }, + }, + default: 'create', + }, + ...create.description, + ...deleteAlert.description, + ...executeResponder.description, + ...get.description, + ...search.description, + ...status.description, + ...merge.description, + ...promote.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/merge.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/merge.operation.ts new file mode 100644 index 0000000000..04b0654bf1 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/merge.operation.ts @@ -0,0 +1,41 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { alertRLC, caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [alertRLC, caseRLC]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['merge'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/alert/${alertId}/merge/${caseId}`, + {}, + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/promote.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/promote.operation.ts new file mode 100644 index 0000000000..22591f9ce1 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/promote.operation.ts @@ -0,0 +1,69 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { alertRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + alertRLC, + { + displayName: 'Options', + name: 'options', + placeholder: 'Add Field', + type: 'collection', + default: {}, + options: [ + { + displayName: 'Case Template Name or ID', + name: 'caseTemplate', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + default: '', + typeOptions: { + loadOptionsMethod: 'loadCaseTemplate', + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['promote'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + const caseTemplate = this.getNodeParameter('options.caseTemplate', i, '') as string; + + const body: IDataObject = {}; + + // await theHiveApiRequest.call(this, 'POST', '/v1/caseTemplate', { + // name: 'test template 001', + // displayName: 'Test Template 001', + // description: 'test', + // }); + + if (caseTemplate) { + body.caseTemplate = caseTemplate; + } + + responseData = await theHiveApiRequest.call(this, 'POST', `/v1/alert/${alertId}/case`, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/search.operation.ts new file mode 100644 index 0000000000..b400562549 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/search.operation.ts @@ -0,0 +1,60 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + genericFiltersCollection, + returnAllAndLimit, + searchOptions, + sortCollection, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; + +const properties: INodeProperties[] = [ + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + searchOptions, +]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + const { returnCount, extraData } = this.getNodeParameter('options', i); + let limit; + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + { query: 'listAlert' }, + filtersValues, + sortFields, + limit, + returnCount as boolean, + extraData as string[], + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/status.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/status.operation.ts new file mode 100644 index 0000000000..20507f8a20 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/status.operation.ts @@ -0,0 +1,42 @@ +import type { INodeExecutionData, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { alertRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + alertRLC, + { + displayName: 'Status Name or ID', + name: 'status', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + default: '', + required: true, + typeOptions: { + loadOptionsMethod: 'loadAlertStatus', + }, + }, +]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['status'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + const status = this.getNodeParameter('status', i) as string; + + await theHiveApiRequest.call(this, 'PATCH', `/v1/alert/${alertId}`, { status }); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/update.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/update.operation.ts new file mode 100644 index 0000000000..c8beae9de3 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/update.operation.ts @@ -0,0 +1,151 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import set from 'lodash/set'; + +const properties: INodeProperties[] = [ + { + displayName: 'Fields', + name: 'alertUpdateFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getAlertUpdateFields', + mode: 'update', + valuesLabel: 'Fields', + addAllFields: true, + multiKeyMatch: true, + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['alert'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let body: IDataObject = {}; + let updated = 1; + + const dataMode = this.getNodeParameter('alertUpdateFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('alertUpdateFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const alertUpdateFields = this.getNodeParameter( + 'alertUpdateFields.value', + i, + [], + ) as IDataObject; + body = alertUpdateFields; + } + + body = fixFieldType(body); + + const fieldsToMatchOn = this.getNodeParameter('alertUpdateFields.matchingColumns', i) as string[]; + + const updateBody: IDataObject = {}; + const matchFields: IDataObject = {}; + const { id } = body; // id would be used if matching on id, also we need to remove it from the body + + for (const field of Object.keys(body)) { + if (field === 'customFields') { + //in input data customFields sent as an object, parse it extracting customFields that are used for matching + const customFields: IDataObject = {}; + for (const customField of Object.keys(body.customFields || {})) { + const combinedPath = `customFields.${customField}`; + if (fieldsToMatchOn.includes(combinedPath)) { + matchFields[combinedPath] = (body.customFields as IDataObject)[customField]; + } else { + customFields[customField] = (body.customFields as IDataObject)[customField]; + } + } + set(updateBody, 'customFields', customFields); + continue; + } + if (fieldsToMatchOn.includes(field)) { + // if field is in fieldsToMatchOn, we need to exclude it from the updateBody, as values used for matching should not be updated + matchFields[field] = body[field]; + } else { + // use set to construct the updateBody, as it allows to process customFields.fieldName + // if customFields provided under customFields property, it will be send as is + set(updateBody, field, body[field]); + } + } + + if (fieldsToMatchOn.includes('id')) { + await theHiveApiRequest.call(this, 'PATCH', `/v1/alert/${id}`, body); + } else { + const filter = { + _name: 'filter', + _and: fieldsToMatchOn.map((field) => ({ + _eq: { + _field: field, + _value: matchFields[field], + }, + })), + }; + + const queryBody = { + query: [ + { + _name: 'listAlert', + }, + filter, + ], + }; + + const matches = (await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + queryBody, + )) as IDataObject[]; + + if (!matches.length) { + throw new NodeOperationError(this.getNode(), 'No matching alerts found'); + } + const ids = matches.map((match) => match._id); + updated = ids.length; + + updateBody.ids = ids; + + await theHiveApiRequest.call(this, 'PATCH', '/v1/alert/_bulk', updateBody); + } + + const executionData = this.helpers.constructExecutionMetaData( + wrapData({ success: true, updatedAlerts: updated }), + { + itemData: { item: i }, + }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/addAttachment.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/addAttachment.operation.ts new file mode 100644 index 0000000000..b00b9b42f3 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/addAttachment.operation.ts @@ -0,0 +1,89 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { attachmentsUi, caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + caseRLC, + attachmentsUi, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Rename Files', + name: 'canRename', + type: 'boolean', + description: 'Whether to rename the file in case a file with the same name already exists', + default: false, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['addAttachment'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + const canRename = this.getNodeParameter('options.canRename', i, false) as boolean; + + const inputDataFields = ( + this.getNodeParameter('attachmentsUi.values', i, []) as IDataObject[] + ).map((entry) => (entry.field as string).trim()); + + const attachments = []; + + for (const inputDataField of inputDataFields) { + const binaryData = this.helpers.assertBinaryData(i, inputDataField); + const dataBuffer = await this.helpers.getBinaryDataBuffer(i, inputDataField); + + attachments.push({ + value: dataBuffer, + options: { + contentType: binaryData.mimeType, + filename: binaryData.fileName, + }, + }); + } + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/v1/case/${caseId}/attachments`, + undefined, + undefined, + undefined, + { + Headers: { + 'Content-Type': 'multipart/form-data', + }, + formData: { + attachments, + canRename: JSON.stringify(canRename), + }, + }, + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/create.operation.ts new file mode 100644 index 0000000000..42a04f477e --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/create.operation.ts @@ -0,0 +1,82 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; + +import set from 'lodash/set'; + +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; + +const properties: INodeProperties[] = [ + { + displayName: 'Fields', + name: 'caseFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getCaseFields', + mode: 'add', + valuesLabel: 'Fields', + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let responseData: IDataObject | IDataObject[] = []; + let inputData: IDataObject = {}; + + const dataMode = this.getNodeParameter('caseFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('caseFields.schema', i) as IDataObject[]; + inputData = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const caseFields = this.getNodeParameter('caseFields.value', i, []) as IDataObject; + inputData = caseFields; + } + + inputData = fixFieldType(inputData); + + const body: IDataObject = {}; + + for (const field of Object.keys(inputData)) { + // use set to construct the updateBody, as it allows to process customFields.fieldName + // if customFields provided under customFields property, it will be send as is + set(body, field, inputData[field]); + } + + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/case' as string, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/deleteAttachment.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/deleteAttachment.operation.ts new file mode 100644 index 0000000000..af2d685851 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/deleteAttachment.operation.ts @@ -0,0 +1,42 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + caseRLC, + { + displayName: 'Attachment Name or ID', + name: 'attachmentId', + type: 'options', + default: '', + required: true, + description: + 'ID of the attachment. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'loadCaseAttachments', + }, + }, +]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['deleteAttachment'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/case/${caseId}/attachment/${attachmentId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/deleteCase.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/deleteCase.operation.ts new file mode 100644 index 0000000000..056abf91a5 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/deleteCase.operation.ts @@ -0,0 +1,27 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [caseRLC]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['deleteCase'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/case/${caseId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/executeResponder.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/executeResponder.operation.ts new file mode 100644 index 0000000000..9eb7fa35d4 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/executeResponder.operation.ts @@ -0,0 +1,73 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { caseRLC, responderOptions } from '../../descriptions'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [{ ...caseRLC, name: 'id' }, responderOptions]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['executeResponder'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const caseId = this.getNodeParameter('id', i, '', { extractValue: true }) as string; + const responderId = this.getNodeParameter('responder', i) as string; + let body: IDataObject; + let response; + responseData = []; + body = { + responderId, + objectId: caseId, + objectType: 'case', + }; + response = await theHiveApiRequest.call(this, 'POST', '/connector/cortex/action' as string, body); + body = { + query: [ + { + _name: 'listAction', + }, + { + _name: 'filter', + _and: [ + { + _field: 'cortexId', + _value: response.cortexId, + }, + { + _field: 'objectId', + _value: response.objectId, + }, + { + _field: 'startDate', + _value: response.startDate, + }, + ], + }, + ], + }; + const qs: IDataObject = {}; + qs.name = 'log-actions'; + do { + response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + } while (response.status === 'Waiting' || response.status === 'InProgress'); + + responseData = response; + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/get.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/get.operation.ts new file mode 100644 index 0000000000..1787a78f50 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/get.operation.ts @@ -0,0 +1,53 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [caseRLC]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + + const qs: IDataObject = {}; + + const body = { + query: [ + { + _name: 'getCase', + idOrName: caseId, + }, + { + _name: 'page', + from: 0, + to: 10, + extraData: ['attachmentCount'], + }, + ], + }; + + qs.name = `get-case-${caseId}`; + + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/getAttachment.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/getAttachment.operation.ts new file mode 100644 index 0000000000..5ebafe61bc --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/getAttachment.operation.ts @@ -0,0 +1,112 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + caseRLC, + { + displayName: 'Attachment Name or ID', + name: 'attachmentId', + type: 'options', + default: '', + required: true, + description: + 'ID of the attachment. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'loadCaseAttachments', + loadOptionsDependsOn: ['caseId.value'], + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'Rename the file when downloading', + }, + { + displayName: 'Data Property Name', + name: 'dataPropertyName', + type: 'string', + default: 'data', + description: 'Name of the binary property to which write the data of the attachment', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['getAttachment'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + const options = this.getNodeParameter('options', i); + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + + const requestOptions = { + useStream: true, + resolveWithFullResponse: true, + encoding: null, + json: false, + }; + + const response = await theHiveApiRequest.call( + this, + 'GET', + `/v1/case/${caseId}/attachment/${attachmentId}/download`, + undefined, + undefined, + undefined, + requestOptions, + ); + + const mimeType = (response.headers as IDataObject)?.['content-type'] ?? undefined; + + let fileName = (options.fileName as string) || 'attachment'; + + if (!options.fileName && response.headers['content-disposition'] !== undefined) { + const contentDisposition = response.headers['content-disposition'] as string; + const fileNameMatch = contentDisposition.match(/filename="(.+)"/); + if (fileNameMatch !== null) { + fileName = fileNameMatch[1]; + } + } + + const newItem: INodeExecutionData = { + json: { + _id: attachmentId, + caseId, + fileName, + mimeType, + }, + binary: {}, + }; + + newItem.binary![(options.dataPropertyName as string) || 'data'] = + await this.helpers.prepareBinaryData(response.body as Buffer, fileName, mimeType as string); + + const executionData = this.helpers.constructExecutionMetaData([newItem], { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/getTimeline.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/getTimeline.operation.ts new file mode 100644 index 0000000000..100218b659 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/getTimeline.operation.ts @@ -0,0 +1,34 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [caseRLC]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['getTimeline'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + + responseData = await theHiveApiRequest.call(this, 'GET', `/v1/case/${caseId}/timeline`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/index.ts new file mode 100644 index 0000000000..9a2ec7c3ae --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/index.ts @@ -0,0 +1,104 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as addAttachment from './addAttachment.operation'; + +import * as create from './create.operation'; +import * as deleteAttachment from './deleteAttachment.operation'; +import * as deleteCase from './deleteCase.operation'; +import * as executeResponder from './executeResponder.operation'; +import * as get from './get.operation'; +import * as getAttachment from './getAttachment.operation'; +import * as search from './search.operation'; +import * as getTimeline from './getTimeline.operation'; +import * as update from './update.operation'; + +export { + addAttachment, + create, + deleteAttachment, + deleteCase, + executeResponder, + get, + search, + getAttachment, + getTimeline, + update, +}; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + default: 'create', + type: 'options', + noDataExpression: true, + required: true, + options: [ + { + name: 'Add Attachment', + value: 'addAttachment', + action: 'Add attachment to a case', + }, + { + name: 'Create', + value: 'create', + action: 'Create a case', + }, + { + name: 'Delete Attachment', + value: 'deleteAttachment', + action: 'Delete attachment from a case', + }, + { + name: 'Delete Case', + value: 'deleteCase', + action: 'Delete an case', + }, + { + name: 'Execute Responder', + value: 'executeResponder', + action: 'Execute responder on a case', + }, + { + name: 'Get', + value: 'get', + action: 'Get a case', + }, + { + name: 'Get Attachment', + value: 'getAttachment', + action: 'Get attachment from a case', + }, + { + name: 'Get Timeline', + value: 'getTimeline', + action: 'Get timeline of a case', + }, + { + name: 'Search', + value: 'search', + action: 'Search cases', + }, + { + name: 'Update', + value: 'update', + action: 'Update a case', + }, + ], + displayOptions: { + show: { + resource: ['case'], + }, + }, + }, + ...addAttachment.description, + ...create.description, + ...deleteAttachment.description, + ...deleteCase.description, + ...executeResponder.description, + ...get.description, + ...getAttachment.description, + ...search.description, + ...getTimeline.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/search.operation.ts new file mode 100644 index 0000000000..2caee6b170 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/search.operation.ts @@ -0,0 +1,60 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + genericFiltersCollection, + returnAllAndLimit, + searchOptions, + sortCollection, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; + +const properties: INodeProperties[] = [ + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + searchOptions, +]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + const { returnCount, extraData } = this.getNodeParameter('options', i); + let limit; + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + { query: 'listCase' }, + filtersValues, + sortFields, + limit, + returnCount as boolean, + extraData as string[], + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/case/update.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/case/update.operation.ts new file mode 100644 index 0000000000..320397d1fb --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/case/update.operation.ts @@ -0,0 +1,147 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import set from 'lodash/set'; + +const properties: INodeProperties[] = [ + { + displayName: 'Fields', + name: 'caseUpdateFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getCaseUpdateFields', + mode: 'update', + valuesLabel: 'Fields', + addAllFields: true, + multiKeyMatch: true, + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['case'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let body: IDataObject = {}; + let updated = 1; + + const dataMode = this.getNodeParameter('caseUpdateFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('caseUpdateFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const caseUpdateFields = this.getNodeParameter('caseUpdateFields.value', i, []) as IDataObject; + body = caseUpdateFields; + } + + body = fixFieldType(body); + + const fieldsToMatchOn = this.getNodeParameter('caseUpdateFields.matchingColumns', i) as string[]; + + const updateBody: IDataObject = {}; + const matchFields: IDataObject = {}; + const { id } = body; // id would be used if matching on id, also we need to remove it from the body + + for (const field of Object.keys(body)) { + if (field === 'customFields') { + //in input data customFields sent as an object, parse it extracting customFields that are used for matching + const customFields: IDataObject = {}; + for (const customField of Object.keys(body.customFields || {})) { + const customFieldPath = `customFields.${customField}`; + if (fieldsToMatchOn.includes(customFieldPath)) { + matchFields[customFieldPath] = (body.customFields as IDataObject)[customField]; + } else { + customFields[customField] = (body.customFields as IDataObject)[customField]; + } + } + set(updateBody, 'customFields', customFields); + continue; + } + if (fieldsToMatchOn.includes(field)) { + // if field is in fieldsToMatchOn, we need to exclude it from the updateBody, as values used for matching should not be updated + matchFields[field] = body[field]; + } else { + // use set to construct the updateBody, as it allows to process customFields.fieldName + // if customFields provided under customFields property, it will be send as is + set(updateBody, field, body[field]); + } + } + + if (fieldsToMatchOn.includes('id')) { + await theHiveApiRequest.call(this, 'PATCH', `/v1/case/${id}`, body); + } else { + const filter = { + _name: 'filter', + _and: fieldsToMatchOn.map((field) => ({ + _eq: { + _field: field, + _value: matchFields[field], + }, + })), + }; + + const queryBody = { + query: [ + { + _name: 'listCase', + }, + filter, + ], + }; + + const matches = (await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + queryBody, + )) as IDataObject[]; + + if (!matches.length) { + throw new NodeOperationError(this.getNode(), 'No matching alerts found'); + } + const ids = matches.map((match) => match._id); + updated = ids.length; + + updateBody.ids = ids; + + await theHiveApiRequest.call(this, 'PATCH', '/v1/case/_bulk', updateBody); + } + + const executionData = this.helpers.constructExecutionMetaData( + wrapData({ success: true, updated }), + { + itemData: { item: i }, + }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/comment/add.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/comment/add.operation.ts new file mode 100644 index 0000000000..ddb00e469d --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/comment/add.operation.ts @@ -0,0 +1,86 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { alertRLC, caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Add to', + name: 'addTo', + type: 'options', + options: [ + { + name: 'Alert', + value: 'alert', + }, + { + name: 'Case', + value: 'case', + }, + ], + default: 'alert', + }, + { + ...caseRLC, + name: 'id', + displayOptions: { + show: { + addTo: ['case'], + }, + }, + }, + { + ...alertRLC, + name: 'id', + displayOptions: { + show: { + addTo: ['alert'], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + typeOptions: { + rows: 2, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['comment'], + operation: ['add'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const addTo = this.getNodeParameter('addTo', i) as string; + const id = this.getNodeParameter('id', i, '', { extractValue: true }); + const message = this.getNodeParameter('message', i) as string; + + const body: IDataObject = { + message, + }; + + responseData = await theHiveApiRequest.call(this, 'POST', `/v1/${addTo}/${id}/comment`, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/comment/deleteComment.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/comment/deleteComment.operation.ts new file mode 100644 index 0000000000..f237e6cf1d --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/comment/deleteComment.operation.ts @@ -0,0 +1,27 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { commentRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [commentRLC]; + +const displayOptions = { + show: { + resource: ['comment'], + operation: ['deleteComment'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const commentId = this.getNodeParameter('commentId', i, '', { extractValue: true }) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/comment/${commentId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/comment/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/comment/index.ts new file mode 100644 index 0000000000..c4cf802cb6 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/comment/index.ts @@ -0,0 +1,50 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as add from './add.operation'; +import * as deleteComment from './deleteComment.operation'; +import * as search from './search.operation'; +import * as update from './update.operation'; + +export { add, deleteComment, search, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + required: true, + default: 'add', + options: [ + { + name: 'Create', + value: 'add', + action: 'Create a comment in a case or alert', + }, + { + name: 'Delete', + value: 'deleteComment', + action: 'Delete a comment', + }, + { + name: 'Search', + value: 'search', + action: 'Search comments', + }, + { + name: 'Update', + value: 'update', + action: 'Update a comment', + }, + ], + displayOptions: { + show: { + resource: ['comment'], + }, + }, + }, + ...add.description, + ...deleteComment.description, + ...search.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/comment/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/comment/search.operation.ts new file mode 100644 index 0000000000..8e279fd8fe --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/comment/search.operation.ts @@ -0,0 +1,118 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + alertRLC, + caseRLC, + genericFiltersCollection, + returnAllAndLimit, + searchOptions, + sortCollection, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; +import type { QueryScope } from '../../helpers/interfaces'; + +const properties: INodeProperties[] = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Search in', + name: 'searchIn', + type: 'options', + default: 'all', + description: + 'Whether to search for comments in all alerts and cases or in a specific case or alert', + options: [ + { + name: 'Alerts and Cases', + value: 'all', + }, + { + name: 'Alert', + value: 'alert', + }, + { + name: 'Case', + value: 'case', + }, + ], + }, + { + ...caseRLC, + displayOptions: { + show: { + searchIn: ['case'], + }, + }, + }, + { + ...alertRLC, + displayOptions: { + show: { + searchIn: ['alert'], + }, + }, + }, + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + searchOptions, +]; + +const displayOptions = { + show: { + resource: ['comment'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const searchIn = this.getNodeParameter('searchIn', i) as string; + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + const { returnCount, extraData } = this.getNodeParameter('options', i); + + let limit; + let scope: QueryScope; + + if (searchIn === 'all') { + scope = { query: 'listComment' }; + } else if (searchIn === 'alert') { + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + scope = { query: 'getAlert', id: alertId, restrictTo: 'comments' }; + } else if (searchIn === 'case') { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + scope = { query: 'getCase', id: caseId, restrictTo: 'comments' }; + } else { + throw new NodeOperationError(this.getNode(), `Invalid 'Search In ...' value: ${searchIn}`); + } + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + scope, + filtersValues, + sortFields, + limit, + returnCount as boolean, + extraData as string[], + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/comment/update.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/comment/update.operation.ts new file mode 100644 index 0000000000..f4316e5709 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/comment/update.operation.ts @@ -0,0 +1,51 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { commentRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + commentRLC, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + typeOptions: { + rows: 2, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['comment'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const commentId = this.getNodeParameter('commentId', i, '', { extractValue: true }) as string; + const message = this.getNodeParameter('message', i) as string; + + const body: IDataObject = { + message, + }; + + responseData = await theHiveApiRequest.call(this, 'PATCH', `/v1/comment/${commentId}`, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/addAttachment.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/addAttachment.operation.ts new file mode 100644 index 0000000000..bf5cc1d20b --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/addAttachment.operation.ts @@ -0,0 +1,66 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { attachmentsUi, logRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [logRLC, attachmentsUi]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['addAttachment'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const logId = this.getNodeParameter('logId', i, '', { extractValue: true }) as string; + + const inputDataFields = ( + this.getNodeParameter('attachmentsUi.values', i, []) as IDataObject[] + ).map((entry) => (entry.field as string).trim()); + + const attachments = []; + + for (const inputDataField of inputDataFields) { + const binaryData = this.helpers.assertBinaryData(i, inputDataField); + const dataBuffer = await this.helpers.getBinaryDataBuffer(i, inputDataField); + + attachments.push({ + value: dataBuffer, + options: { + contentType: binaryData.mimeType, + filename: binaryData.fileName, + }, + }); + } + + await theHiveApiRequest.call( + this, + 'POST', + `/v1/log/${logId}/attachments`, + undefined, + undefined, + undefined, + { + Headers: { + 'Content-Type': 'multipart/form-data', + }, + formData: { + attachments, + }, + }, + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/create.operation.ts new file mode 100644 index 0000000000..5150acf397 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/create.operation.ts @@ -0,0 +1,115 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; + +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import { attachmentsUi, taskRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + taskRLC, + { + displayName: 'Fields', + name: 'logFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getLogFields', + mode: 'add', + valuesLabel: 'Fields', + }, + }, + }, + attachmentsUi, +]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let responseData: IDataObject | IDataObject[] = []; + let body: IDataObject = {}; + + const dataMode = this.getNodeParameter('logFields.mappingMode', i) as string; + const taskId = this.getNodeParameter('taskId', i, '', { extractValue: true }) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('logFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const logFields = this.getNodeParameter('logFields.value', i, []) as IDataObject; + body = logFields; + } + + body = fixFieldType(body); + + const inputDataFields = ( + this.getNodeParameter('attachmentsUi.values', i, []) as IDataObject[] + ).map((entry) => (entry.field as string).trim()); + + if (inputDataFields.length) { + const binaries = []; + + for (const inputDataField of inputDataFields) { + const binaryData = this.helpers.assertBinaryData(i, inputDataField); + const dataBuffer = await this.helpers.getBinaryDataBuffer(i, inputDataField); + + binaries.push({ + value: dataBuffer, + options: { + contentType: binaryData.mimeType, + filename: binaryData.fileName, + }, + }); + } + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/v1/task/${taskId}/log`, + undefined, + undefined, + undefined, + { + Headers: { + 'Content-Type': 'multipart/form-data', + }, + formData: { + attachments: binaries, + _json: JSON.stringify(body), + }, + }, + ); + } else { + responseData = await theHiveApiRequest.call(this, 'POST', `/v1/task/${taskId}/log`, body); + } + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/deleteAttachment.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/deleteAttachment.operation.ts new file mode 100644 index 0000000000..645ec7957c --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/deleteAttachment.operation.ts @@ -0,0 +1,43 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { logRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + logRLC, + { + displayName: 'Attachment Name or ID', + name: 'attachmentId', + type: 'options', + default: '', + required: true, + description: + 'ID of the attachment. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'loadLogAttachments', + loadOptionsDependsOn: ['logId.value'], + }, + }, +]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['deleteAttachment'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const logId = this.getNodeParameter('logId', i, '', { extractValue: true }) as string; + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/log/${logId}/attachments/${attachmentId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/deleteLog.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/deleteLog.operation.ts new file mode 100644 index 0000000000..de0b3bdf81 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/deleteLog.operation.ts @@ -0,0 +1,27 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { logRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [logRLC]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['deleteLog'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const logId = this.getNodeParameter('logId', i, '', { extractValue: true }) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/log/${logId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/executeResponder.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/executeResponder.operation.ts new file mode 100644 index 0000000000..c7cb4ab6ba --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/executeResponder.operation.ts @@ -0,0 +1,72 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { logRLC, responderOptions } from '../../descriptions'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [{ ...logRLC, name: 'id' }, responderOptions]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['executeResponder'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const logId = this.getNodeParameter('id', i); + const responderId = this.getNodeParameter('responder', i) as string; + let body: IDataObject; + let response; + const qs: IDataObject = {}; + body = { + responderId, + objectId: logId, + objectType: 'case_task_log', + }; + response = await theHiveApiRequest.call(this, 'POST', '/connector/cortex/action' as string, body); + body = { + query: [ + { + _name: 'listAction', + }, + { + _name: 'filter', + _and: [ + { + _field: 'cortexId', + _value: response.cortexId, + }, + { + _field: 'objectId', + _value: response.objectId, + }, + { + _field: 'startDate', + _value: response.startDate, + }, + ], + }, + ], + }; + qs.name = 'log-actions'; + do { + response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + } while (response.status === 'Waiting' || response.status === 'InProgress'); + + responseData = response; + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/get.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/get.operation.ts new file mode 100644 index 0000000000..0222f45557 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/get.operation.ts @@ -0,0 +1,43 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { logRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [logRLC]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const logId = this.getNodeParameter('logId', i, '', { extractValue: true }) as string; + + const body = { + query: [ + { + _name: 'getLog', + idOrName: logId, + }, + ], + }; + + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/index.ts new file mode 100644 index 0000000000..afd47ea08d --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/index.ts @@ -0,0 +1,71 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as addAttachment from './addAttachment.operation'; +import * as create from './create.operation'; +import * as deleteAttachment from './deleteAttachment.operation'; +import * as deleteLog from './deleteLog.operation'; +import * as executeResponder from './executeResponder.operation'; +import * as get from './get.operation'; +import * as search from './search.operation'; + +export { addAttachment, create, deleteAttachment, deleteLog, executeResponder, get, search }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + required: true, + default: 'create', + options: [ + { + name: 'Add Attachment', + value: 'addAttachment', + action: 'Add attachment to a task log', + }, + { + name: 'Create', + value: 'create', + action: 'Create a task log', + }, + { + name: 'Delete', + value: 'deleteLog', + action: 'Delete task log', + }, + { + name: 'Delete Attachment', + value: 'deleteAttachment', + action: 'Delete attachment from a task log', + }, + { + name: 'Execute Responder', + value: 'executeResponder', + action: 'Execute responder on a task log', + }, + { + name: 'Get', + value: 'get', + action: 'Get a task log', + }, + { + name: 'Search', + value: 'search', + action: 'Search task logs', + }, + ], + displayOptions: { + show: { + resource: ['log'], + }, + }, + }, + ...addAttachment.description, + ...create.description, + ...deleteAttachment.description, + ...deleteLog.description, + ...executeResponder.description, + ...get.description, + ...search.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/log/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/log/search.operation.ts new file mode 100644 index 0000000000..96fa58e20b --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/log/search.operation.ts @@ -0,0 +1,87 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + taskRLC, + genericFiltersCollection, + returnAllAndLimit, + sortCollection, + searchOptions, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; +import type { QueryScope } from '../../helpers/interfaces'; + +const properties: INodeProperties[] = [ + { + displayName: 'Search in All Tasks', + name: 'allTasks', + type: 'boolean', + default: true, + description: 'Whether to search in all tasks or only in selected task', + }, + { + ...taskRLC, + displayOptions: { + show: { + allTasks: [false], + }, + }, + }, + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + searchOptions, +]; + +const displayOptions = { + show: { + resource: ['log'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const allTasks = this.getNodeParameter('allTasks', i) as boolean; + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + const { returnCount, extraData } = this.getNodeParameter('options', i); + + let limit; + let scope: QueryScope; + + if (allTasks) { + scope = { query: 'listLog' }; + } else { + const taskId = this.getNodeParameter('taskId', i, '', { extractValue: true }) as string; + scope = { query: 'getTask', id: taskId, restrictTo: 'logs' }; + } + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + scope, + filtersValues, + sortFields, + limit, + returnCount as boolean, + extraData as string[], + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/node.description.ts b/packages/nodes-base/nodes/TheHiveProject/actions/node.description.ts new file mode 100644 index 0000000000..b5af7386c7 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/node.description.ts @@ -0,0 +1,85 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; + +import * as alert from './alert'; +import * as case_ from './case'; +import * as comment from './comment'; +import * as log from './log'; +import * as observable from './observable'; +import * as query from './query'; +import * as task from './task'; +import * as page from './page'; + +export const description: INodeTypeDescription = { + displayName: 'TheHive 5', + name: 'theHiveProject', + icon: 'file:thehiveproject.svg', + group: ['transform'], + subtitle: '={{$parameter["operation"]}} : {{$parameter["resource"]}}', + version: 1, + description: 'Consume TheHive 5 API', + defaults: { + name: 'TheHive 5', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'theHiveProjectApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + required: true, + options: [ + { + name: 'Alert', + value: 'alert', + }, + { + name: 'Case', + value: 'case', + }, + { + name: 'Comment', + value: 'comment', + }, + { + name: 'Observable', + value: 'observable', + }, + { + name: 'Page', + value: 'page', + }, + { + name: 'Query', + value: 'query', + }, + { + name: 'Task', + value: 'task', + }, + { + name: 'Task Log', + value: 'log', + }, + ], + default: 'alert', + }, + + ...alert.description, + ...case_.description, + ...comment.description, + ...log.description, + ...observable.description, + ...page.description, + ...query.description, + ...task.description, + ], +}; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/node.type.ts b/packages/nodes-base/nodes/TheHiveProject/actions/node.type.ts new file mode 100644 index 0000000000..29575ab79e --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/node.type.ts @@ -0,0 +1,47 @@ +import type { AllEntities } from 'n8n-workflow'; + +type NodeMap = { + alert: + | 'create' + | 'deleteAlert' + | 'executeResponder' + | 'get' + | 'search' + | 'status' + | 'merge' + | 'promote' + | 'update'; + case: + | 'addAttachment' + | 'create' + | 'deleteAttachment' + | 'deleteCase' + | 'executeResponder' + | 'get' + | 'search' + | 'getAttachment' + | 'getTimeline' + | 'update'; + comment: 'add' | 'deleteComment' | 'search' | 'update'; + log: + | 'addAttachment' + | 'create' + | 'deleteLog' + | 'deleteAttachment' + | 'executeResponder' + | 'get' + | 'search'; + observable: + | 'create' + | 'deleteObservable' + | 'executeAnalyzer' + | 'executeResponder' + | 'get' + | 'search' + | 'update'; + page: 'create' | 'deletePage' | 'search' | 'update'; + query: 'executeQuery'; + task: 'create' | 'deleteTask' | 'executeResponder' | 'get' | 'search' | 'update'; +}; + +export type TheHiveType = AllEntities; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/create.operation.ts new file mode 100644 index 0000000000..fe0a0f6426 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/create.operation.ts @@ -0,0 +1,189 @@ +import { + NodeOperationError, + type IDataObject, + type IExecuteFunctions, + type INodeExecutionData, + type INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; + +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import { alertRLC, attachmentsUi, caseRLC } from '../../descriptions'; + +import FormData from 'form-data'; + +const properties: INodeProperties[] = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Create in', + name: 'createIn', + type: 'options', + options: [ + { + name: 'Case', + value: 'case', + }, + { + name: 'Alert', + value: 'alert', + }, + ], + default: 'case', + }, + { + ...caseRLC, + name: 'id', + displayOptions: { + show: { + createIn: ['case'], + }, + }, + }, + { + ...alertRLC, + name: 'id', + displayOptions: { + show: { + createIn: ['alert'], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Data Type', + name: 'dataType', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + required: true, + default: 'file', + typeOptions: { + loadOptionsMethod: 'loadObservableTypes', + }, + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + default: '', + required: true, + displayOptions: { + hide: { + dataType: ['file'], + }, + }, + }, + { ...attachmentsUi, required: true, displayOptions: { show: { dataType: ['file'] } } }, + { + displayName: 'Fields', + name: 'observableFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getObservableFields', + mode: 'add', + valuesLabel: 'Fields', + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let responseData: IDataObject = {}; + let body: IDataObject = {}; + + const createIn = this.getNodeParameter('createIn', i) as string; + const id = this.getNodeParameter('id', i, '', { extractValue: true }) as string; + const endpoint = `/v1/${createIn}/${id}/observable`; + + const dataMode = this.getNodeParameter('observableFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('observableFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const observableFields = this.getNodeParameter('observableFields.value', i, []) as IDataObject; + body = observableFields; + } + + body = fixFieldType(body); + + const dataType = this.getNodeParameter('dataType', i) as string; + + body.dataType = dataType; + + if (dataType === 'file') { + const inputDataFields = ( + this.getNodeParameter('attachmentsUi.values', i, []) as IDataObject[] + ).map((entry) => (entry.field as string).trim()); + + const formData = new FormData(); + + for (const inputDataField of inputDataFields) { + const binaryData = this.helpers.assertBinaryData(i, inputDataField); + const dataBuffer = await this.helpers.getBinaryDataBuffer(i, inputDataField); + + formData.append('attachment', dataBuffer, { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }); + } + + formData.append('_json', JSON.stringify(body)); + + responseData = await theHiveApiRequest.call( + this, + 'POST', + endpoint, + undefined, + undefined, + undefined, + { + Headers: { + 'Content-Type': 'multipart/form-data', + }, + formData, + }, + ); + } else { + const data = this.getNodeParameter('data', i) as string; + body.data = data; + responseData = await theHiveApiRequest.call(this, 'POST', endpoint, body); + } + + if (responseData.failure) { + const message = (responseData.failure as IDataObject[]) + .map((error: IDataObject) => error.message) + .join(', '); + throw new NodeOperationError(this.getNode(), message, { itemIndex: i }); + } + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/deleteObservable.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/deleteObservable.operation.ts new file mode 100644 index 0000000000..230bd39508 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/deleteObservable.operation.ts @@ -0,0 +1,29 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { observableRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [observableRLC]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['deleteObservable'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const observableId = this.getNodeParameter('observableId', i, '', { + extractValue: true, + }) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/observable/${observableId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/executeAnalyzer.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/executeAnalyzer.operation.ts new file mode 100644 index 0000000000..011b8efbe4 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/executeAnalyzer.operation.ts @@ -0,0 +1,93 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { observableRLC, observableTypeOptions } from '../../descriptions'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + observableRLC, + observableTypeOptions, + { + displayName: 'Analyzer Names or IDs', + name: 'analyzers', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + required: true, + default: [], + typeOptions: { + loadOptionsDependsOn: ['observableId.value', 'dataType'], + loadOptionsMethod: 'loadAnalyzers', + }, + displayOptions: { + hide: { + id: [''], + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['executeAnalyzer'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject = {}; + + const observableId = this.getNodeParameter('observableId', i, '', { + extractValue: true, + }) as string; + + const analyzers = (this.getNodeParameter('analyzers', i) as string[]).map((analyzer) => { + const parts = analyzer.split('::'); + return { + analyzerId: parts[0], + cortexId: parts[1], + }; + }); + let response: any; + let body: IDataObject; + + const qs: IDataObject = {}; + for (const analyzer of analyzers) { + body = { + ...analyzer, + artifactId: observableId, + }; + // execute the analyzer + response = await theHiveApiRequest.call( + this, + 'POST', + '/connector/cortex/job' as string, + body, + qs, + ); + const jobId = response.id; + qs.name = 'observable-jobs'; + // query the job result (including the report) + do { + responseData = await theHiveApiRequest.call( + this, + 'GET', + `/connector/cortex/job/${jobId}`, + body, + qs, + ); + } while (responseData.status === 'Waiting' || responseData.status === 'InProgress'); + } + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/executeResponder.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/executeResponder.operation.ts new file mode 100644 index 0000000000..adf8b58a0a --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/executeResponder.operation.ts @@ -0,0 +1,73 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { observableRLC, responderOptions } from '../../descriptions'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [{ ...observableRLC, name: 'id' }, responderOptions]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['executeResponder'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const observableId = this.getNodeParameter('id', i); + const responderId = this.getNodeParameter('responder', i) as string; + let body: IDataObject; + let response; + responseData = []; + body = { + responderId, + objectId: observableId, + objectType: 'case_artifact', + }; + response = await theHiveApiRequest.call(this, 'POST', '/connector/cortex/action' as string, body); + body = { + query: [ + { + _name: 'listAction', + }, + { + _name: 'filter', + _and: [ + { + _field: 'cortexId', + _value: response.cortexId, + }, + { + _field: 'objectId', + _value: response.objectId, + }, + { + _field: 'startDate', + _value: response.startDate, + }, + ], + }, + ], + }; + const qs: IDataObject = {}; + qs.name = 'log-actions'; + do { + response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + } while (response.status === 'Waiting' || response.status === 'InProgress'); + + responseData = response; + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/get.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/get.operation.ts new file mode 100644 index 0000000000..fdff6e068d --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/get.operation.ts @@ -0,0 +1,49 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { observableRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [observableRLC]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const observableId = this.getNodeParameter('observableId', i, '', { + extractValue: true, + }) as string; + + const qs: IDataObject = {}; + + const body = { + query: [ + { + _name: 'getObservable', + idOrName: observableId, + }, + ], + }; + + qs.name = `get-observable-${observableId}`; + + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/index.ts new file mode 100644 index 0000000000..c9eb3ecad3 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/index.ts @@ -0,0 +1,71 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteObservable from './deleteObservable.operation'; +import * as executeAnalyzer from './executeAnalyzer.operation'; +import * as executeResponder from './executeResponder.operation'; +import * as get from './get.operation'; +import * as search from './search.operation'; +import * as update from './update.operation'; + +export { create, deleteObservable, executeAnalyzer, executeResponder, get, search, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + required: true, + default: 'create', + options: [ + { + name: 'Create', + value: 'create', + action: 'Create an observable', + }, + { + name: 'Delete', + value: 'deleteObservable', + action: 'Delete an observable', + }, + { + name: 'Execute Analyzer', + value: 'executeAnalyzer', + action: 'Execute analyzer on an observable', + }, + { + name: 'Execute Responder', + value: 'executeResponder', + action: 'Execute responder on an observable', + }, + { + name: 'Get', + value: 'get', + action: 'Get an observable', + }, + { + name: 'Search', + value: 'search', + action: 'Search observables', + }, + { + name: 'Update', + value: 'update', + action: 'Update an observable', + }, + ], + displayOptions: { + show: { + resource: ['observable'], + }, + }, + }, + ...create.description, + ...deleteObservable.description, + ...executeAnalyzer.description, + ...executeResponder.description, + ...get.description, + ...search.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/search.operation.ts new file mode 100644 index 0000000000..514fe1b2e9 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/search.operation.ts @@ -0,0 +1,118 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + alertRLC, + caseRLC, + genericFiltersCollection, + returnAllAndLimit, + searchOptions, + sortCollection, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; +import type { QueryScope } from '../../helpers/interfaces'; + +const properties: INodeProperties[] = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Search in', + name: 'searchIn', + type: 'options', + default: 'all', + description: + 'Whether to search for observables in all alerts and cases or in a specific case or alert', + options: [ + { + name: 'Alerts and Cases', + value: 'all', + }, + { + name: 'Alert', + value: 'alert', + }, + { + name: 'Case', + value: 'case', + }, + ], + }, + { + ...caseRLC, + displayOptions: { + show: { + searchIn: ['case'], + }, + }, + }, + { + ...alertRLC, + displayOptions: { + show: { + searchIn: ['alert'], + }, + }, + }, + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + searchOptions, +]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const searchIn = this.getNodeParameter('searchIn', i) as string; + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + const { returnCount, extraData } = this.getNodeParameter('options', i); + + let limit; + let scope: QueryScope; + + if (searchIn === 'all') { + scope = { query: 'listObservable' }; + } else if (searchIn === 'alert') { + const alertId = this.getNodeParameter('alertId', i, '', { extractValue: true }) as string; + scope = { query: 'getAlert', id: alertId, restrictTo: 'observables' }; + } else if (searchIn === 'case') { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + scope = { query: 'getCase', id: caseId, restrictTo: 'observables' }; + } else { + throw new NodeOperationError(this.getNode(), `Invalid 'Search In ...' value: ${searchIn}`); + } + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + scope, + filtersValues, + sortFields, + limit, + returnCount as boolean, + extraData as string[], + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/observable/update.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/observable/update.operation.ts new file mode 100644 index 0000000000..23ccbcecb1 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/observable/update.operation.ts @@ -0,0 +1,140 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import set from 'lodash/set'; + +const properties: INodeProperties[] = [ + { + displayName: 'Fields', + name: 'observableUpdateFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getObservableUpdateFields', + mode: 'update', + valuesLabel: 'Fields', + addAllFields: true, + multiKeyMatch: true, + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['observable'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let body: IDataObject = {}; + let updated = 1; + + const dataMode = this.getNodeParameter('observableUpdateFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('observableUpdateFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const observableUpdateFields = this.getNodeParameter( + 'observableUpdateFields.value', + i, + [], + ) as IDataObject; + body = observableUpdateFields; + } + + body = fixFieldType(body); + + const fieldsToMatchOn = this.getNodeParameter( + 'observableUpdateFields.matchingColumns', + i, + ) as string[]; + + const updateBody: IDataObject = {}; + const matchFields: IDataObject = {}; + const { id } = body; // id would be used if matching on id, also we need to remove it from the body + + for (const field of Object.keys(body)) { + if (fieldsToMatchOn.includes(field)) { + // if field is in fieldsToMatchOn, we need to exclude it from the updateBody, as values used for matching should not be updated + matchFields[field] = body[field]; + } else { + // use set to construct the updateBody, as it allows to process customFields.fieldName + // if customFields provided under customFields property, it will be send as is + set(updateBody, field, body[field]); + } + } + + if (fieldsToMatchOn.includes('id')) { + await theHiveApiRequest.call(this, 'PATCH', `/v1/observable/${id}`, body); + } else { + const filter = { + _name: 'filter', + _and: fieldsToMatchOn.map((field) => ({ + _eq: { + _field: field, + _value: matchFields[field], + }, + })), + }; + + const queryBody = { + query: [ + { + _name: 'listObservable', + }, + filter, + ], + }; + + const matches = (await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + queryBody, + )) as IDataObject[]; + + if (!matches.length) { + throw new NodeOperationError(this.getNode(), 'No matching alerts found'); + } + const ids = matches.map((match) => match._id); + updated = ids.length; + + updateBody.ids = ids; + + await theHiveApiRequest.call(this, 'PATCH', '/v1/observable/_bulk', updateBody); + } + + const executionData = this.helpers.constructExecutionMetaData( + wrapData({ success: true, updated }), + { + itemData: { item: i }, + }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/page/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/page/create.operation.ts new file mode 100644 index 0000000000..c60c8d6906 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/page/create.operation.ts @@ -0,0 +1,102 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Create in', + name: 'location', + type: 'options', + options: [ + { + name: 'Case', + value: 'case', + }, + { + name: 'Knowledge Base', + value: 'knowledgeBase', + }, + ], + default: 'case', + }, + { + ...caseRLC, + displayOptions: { + show: { + location: ['case'], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Category', + name: 'category', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + required: true, + typeOptions: { + rows: 2, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['page'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const location = this.getNodeParameter('location', i) as string; + const title = this.getNodeParameter('title', i) as string; + const category = this.getNodeParameter('category', i) as string; + const content = this.getNodeParameter('content', i) as string; + + let endpoint; + + if (location === 'case') { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + endpoint = `/v1/case/${caseId}/page`; + } else { + endpoint = '/v1/page'; + } + + const body: IDataObject = { + title, + category, + content, + }; + + responseData = await theHiveApiRequest.call(this, 'POST', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/page/deletePage.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/page/deletePage.operation.ts new file mode 100644 index 0000000000..ad0f278561 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/page/deletePage.operation.ts @@ -0,0 +1,63 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC, pageRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Delete From ...', + name: 'location', + type: 'options', + options: [ + { + name: 'Case', + value: 'case', + }, + { + name: 'Knowledge Base', + value: 'knowledgeBase', + }, + ], + default: 'knowledgeBase', + }, + { + ...caseRLC, + displayOptions: { + show: { + location: ['case'], + }, + }, + }, + pageRLC, +]; + +const displayOptions = { + show: { + resource: ['page'], + operation: ['deletePage'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const location = this.getNodeParameter('location', i) as string; + const pageId = this.getNodeParameter('pageId', i, '', { extractValue: true }) as string; + + let endpoint; + + if (location === 'case') { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + endpoint = `/v1/case/${caseId}/page/${pageId}`; + } else { + endpoint = `/v1/page/${pageId}`; + } + + await theHiveApiRequest.call(this, 'DELETE', endpoint); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/page/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/page/index.ts new file mode 100644 index 0000000000..511f0e0d2f --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/page/index.ts @@ -0,0 +1,50 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deletePage from './deletePage.operation'; +import * as search from './search.operation'; +import * as update from './update.operation'; + +export { create, deletePage, search, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + required: true, + default: 'create', + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a page', + }, + { + name: 'Delete', + value: 'deletePage', + action: 'Delete a page', + }, + { + name: 'Search', + value: 'search', + action: 'Search pages', + }, + { + name: 'Update', + value: 'update', + action: 'Update a page', + }, + ], + displayOptions: { + show: { + resource: ['page'], + }, + }, + }, + ...create.description, + ...deletePage.description, + ...search.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/page/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/page/search.operation.ts new file mode 100644 index 0000000000..99f8073959 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/page/search.operation.ts @@ -0,0 +1,96 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + caseRLC, + genericFiltersCollection, + returnAllAndLimit, + sortCollection, + searchOptions, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; +import type { QueryScope } from '../../helpers/interfaces'; + +const properties: INodeProperties[] = [ + { + displayName: 'Search in Knowledge Base', + name: 'searchInKnowledgeBase', + type: 'boolean', + default: true, + description: 'Whether to search in knowledge base or only in the selected case', + }, + { + ...caseRLC, + displayOptions: { + show: { + searchInKnowledgeBase: [false], + }, + }, + }, + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + { + ...searchOptions, + displayOptions: { + show: { + returnAll: [true], + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['page'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const searchInKnowledgeBase = this.getNodeParameter('searchInKnowledgeBase', i) as boolean; + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + let returnCount = false; + if (!returnAll) { + returnCount = this.getNodeParameter('options.returnCount', i, false) as boolean; + } + + let limit; + let scope: QueryScope; + + if (searchInKnowledgeBase) { + scope = { query: 'listOrganisationPage' }; + } else { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + scope = { query: 'getCase', id: caseId, restrictTo: 'pages' }; + } + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + scope, + filtersValues, + sortFields, + limit, + returnCount, + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/page/update.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/page/update.operation.ts new file mode 100644 index 0000000000..31209a0ea0 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/page/update.operation.ts @@ -0,0 +1,118 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { caseRLC, pageRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Update in', + name: 'location', + type: 'options', + options: [ + { + name: 'Case', + value: 'case', + }, + { + name: 'Knowledge Base', + value: 'knowledgeBase', + }, + ], + default: 'case', + }, + { + ...caseRLC, + displayOptions: { + show: { + location: ['case'], + }, + }, + }, + pageRLC, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + typeOptions: { + rows: 2, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Category', + name: 'category', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Order', + name: 'order', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['page'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const location = this.getNodeParameter('location', i) as string; + const pageId = this.getNodeParameter('pageId', i, '', { extractValue: true }) as string; + const content = this.getNodeParameter('content', i, '') as string; + const options = this.getNodeParameter('options', i, {}); + + let endpoint; + + if (location === 'case') { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + endpoint = `/v1/case/${caseId}/page/${pageId}`; + } else { + endpoint = `/v1/page/${pageId}`; + } + + const body: IDataObject = options; + + if (content) { + body.content = content; + } + + responseData = await theHiveApiRequest.call(this, 'PATCH', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/query/executeQuery.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/query/executeQuery.operation.ts new file mode 100644 index 0000000000..57c9a7367c --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/query/executeQuery.operation.ts @@ -0,0 +1,77 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError, jsonParse } from 'n8n-workflow'; + +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Query', + name: 'queryJson', + type: 'string', + required: true, + default: '=[\n {\n "_name": "listOrganisation"\n }\n]', + description: 'Search for objects with filtering and sorting capabilities', + hint: 'The query should be an array of operations with the required selection and optional filtering, sorting, and pagination. See Query API for more information.', + typeOptions: { + editor: 'json', + rows: 10, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['query'], + operation: ['executeQuery'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const queryJson = this.getNodeParameter('queryJson', i) as string; + + let query: IDataObject = {}; + if (typeof queryJson === 'object') { + query = queryJson; + } else { + query = jsonParse(queryJson, { + errorMessage: 'Query JSON must be a valid JSON object', + }); + } + + if (query.query) { + query = query.query as IDataObject; + } + + if (!Array.isArray(query)) { + throw new NodeOperationError( + this.getNode(), + 'The query should be an array of operations with the required selection and optional filtering, sorting, and pagination', + ); + } + + const body: IDataObject = { + query, + }; + + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + if (typeof responseData !== 'object') { + responseData = { queryResult: responseData }; + } + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/query/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/query/index.ts new file mode 100644 index 0000000000..ccce45dadf --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/query/index.ts @@ -0,0 +1,29 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as executeQuery from './executeQuery.operation'; + +export { executeQuery }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + required: true, + default: 'executeQuery', + options: [ + { + name: 'Execute Query', + value: 'executeQuery', + action: 'Execute a query', + }, + ], + displayOptions: { + show: { + resource: ['query'], + }, + }, + }, + ...executeQuery.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/router.ts b/packages/nodes-base/nodes/TheHiveProject/actions/router.ts new file mode 100644 index 0000000000..04f86e1d8f --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/router.ts @@ -0,0 +1,80 @@ +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import type { TheHiveType } from './node.type'; + +import * as alert from './alert'; +import * as case_ from './case'; +import * as comment from './comment'; +import * as log from './log'; +import * as observable from './observable'; +import * as page from './page'; +import * as query from './query'; +import * as task from './task'; + +export async function router(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + let executionData: INodeExecutionData[] = []; + + const theHiveNodeData = { + resource, + operation, + } as TheHiveType; + + for (let i = 0; i < length; i++) { + try { + switch (theHiveNodeData.resource) { + case 'alert': + executionData = await alert[theHiveNodeData.operation].execute.call(this, i, items[i]); + break; + case 'case': + executionData = await case_[theHiveNodeData.operation].execute.call(this, i, items[i]); + break; + case 'comment': + executionData = await comment[theHiveNodeData.operation].execute.call(this, i); + break; + case 'log': + executionData = await log[theHiveNodeData.operation].execute.call(this, i, items[i]); + break; + case 'observable': + executionData = await observable[theHiveNodeData.operation].execute.call( + this, + i, + items[i], + ); + break; + case 'page': + executionData = await page[theHiveNodeData.operation].execute.call(this, i); + break; + case 'query': + executionData = await query[theHiveNodeData.operation].execute.call(this, i); + break; + case 'task': + executionData = await task[theHiveNodeData.operation].execute.call(this, i, items[i]); + break; + default: + throw new NodeOperationError( + this.getNode(), + `The operation "${operation}" is not supported!`, + ); + } + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + continue; + } + throw error; + } + } + return this.prepareOutputData(returnData); +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/create.operation.ts new file mode 100644 index 0000000000..38aeb35429 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/create.operation.ts @@ -0,0 +1,75 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; + +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import { caseRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + caseRLC, + { + displayName: 'Fields', + name: 'taskFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getTaskFields', + mode: 'add', + valuesLabel: 'Fields', + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let responseData: IDataObject | IDataObject[] = []; + let body: IDataObject = {}; + + const dataMode = this.getNodeParameter('taskFields.mappingMode', i) as string; + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('taskFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const taskFields = this.getNodeParameter('taskFields.value', i, []) as IDataObject; + body = taskFields; + } + + body = fixFieldType(body); + + responseData = await theHiveApiRequest.call(this, 'POST', `/v1/case/${caseId}/task`, body); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/deleteTask.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/deleteTask.operation.ts new file mode 100644 index 0000000000..021c5f47c6 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/deleteTask.operation.ts @@ -0,0 +1,27 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { taskRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [taskRLC]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['deleteTask'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + const taskId = this.getNodeParameter('taskId', i, '', { extractValue: true }) as string; + + await theHiveApiRequest.call(this, 'DELETE', `/v1/task/${taskId}`); + + const executionData = this.helpers.constructExecutionMetaData(wrapData({ success: true }), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/executeResponder.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/executeResponder.operation.ts new file mode 100644 index 0000000000..4474fa5f0e --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/executeResponder.operation.ts @@ -0,0 +1,75 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { responderOptions, taskRLC } from '../../descriptions'; +import { theHiveApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [{ ...taskRLC, name: 'id' }, responderOptions]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['executeResponder'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const taskId = this.getNodeParameter('id', i); + const responderId = this.getNodeParameter('responder', i) as string; + let body: IDataObject; + let response; + responseData = []; + + const qs: IDataObject = {}; + + body = { + responderId, + objectId: taskId, + objectType: 'case_task', + }; + response = await theHiveApiRequest.call(this, 'POST', '/connector/cortex/action' as string, body); + body = { + query: [ + { + _name: 'listAction', + }, + { + _name: 'filter', + _and: [ + { + _field: 'cortexId', + _value: response.cortexId, + }, + { + _field: 'objectId', + _value: response.objectId, + }, + { + _field: 'startDate', + _value: response.startDate, + }, + ], + }, + ], + }; + qs.name = 'task-actions'; + do { + response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + } while (response.status === 'Waiting' || response.status === 'InProgress'); + + responseData = response; + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/get.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/get.operation.ts new file mode 100644 index 0000000000..6b693d9c1c --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/get.operation.ts @@ -0,0 +1,47 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { theHiveApiRequest } from '../../transport'; +import { taskRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [taskRLC]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const taskId = this.getNodeParameter('taskId', i, '', { extractValue: true }) as string; + + const qs: IDataObject = {}; + + const body = { + query: [ + { + _name: 'getTask', + idOrName: taskId, + }, + ], + }; + + qs.name = `get-task-${taskId}`; + + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/query', body, qs); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/index.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/index.ts new file mode 100644 index 0000000000..90cdec2126 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/index.ts @@ -0,0 +1,64 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteTask from './deleteTask.operation'; +import * as executeResponder from './executeResponder.operation'; +import * as get from './get.operation'; +import * as search from './search.operation'; +import * as update from './update.operation'; + +export { create, deleteTask, executeResponder, get, search, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + default: 'create', + type: 'options', + noDataExpression: true, + required: true, + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a task', + }, + { + name: 'Delete', + value: 'deleteTask', + action: 'Delete an task', + }, + { + name: 'Execute Responder', + value: 'executeResponder', + action: 'Execute responder on a task', + }, + { + name: 'Get', + value: 'get', + action: 'Get a task', + }, + { + name: 'Search', + value: 'search', + action: 'Search tasks', + }, + { + name: 'Update', + value: 'update', + action: 'Update a task', + }, + ], + displayOptions: { + show: { + resource: ['task'], + }, + }, + }, + ...create.description, + ...deleteTask.description, + ...executeResponder.description, + ...get.description, + ...search.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/search.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/search.operation.ts new file mode 100644 index 0000000000..d5ad99a21c --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/search.operation.ts @@ -0,0 +1,87 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; +import { + caseRLC, + genericFiltersCollection, + returnAllAndLimit, + searchOptions, + sortCollection, +} from '../../descriptions'; +import { theHiveApiQuery } from '../../transport'; +import type { QueryScope } from '../../helpers/interfaces'; + +const properties: INodeProperties[] = [ + { + displayName: 'Search in All Cases', + name: 'allCases', + type: 'boolean', + default: true, + description: 'Whether to search in all cases or only in a selected case', + }, + { + ...caseRLC, + displayOptions: { + show: { + allCases: [false], + }, + }, + }, + ...returnAllAndLimit, + genericFiltersCollection, + sortCollection, + searchOptions, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['search'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number): Promise { + let responseData: IDataObject | IDataObject[] = []; + + const allCases = this.getNodeParameter('allCases', i) as boolean; + const filtersValues = this.getNodeParameter('filters.values', i, []) as IDataObject[]; + const sortFields = this.getNodeParameter('sort.fields', i, []) as IDataObject[]; + const returnAll = this.getNodeParameter('returnAll', i); + const { returnCount, extraData } = this.getNodeParameter('options', i); + + let limit; + let scope: QueryScope; + + if (allCases) { + scope = { query: 'listTask' }; + } else { + const caseId = this.getNodeParameter('caseId', i, '', { extractValue: true }) as string; + scope = { query: 'getCase', id: caseId, restrictTo: 'tasks' }; + } + + if (!returnAll) { + limit = this.getNodeParameter('limit', i); + } + + responseData = await theHiveApiQuery.call( + this, + scope, + filtersValues, + sortFields, + limit, + returnCount as boolean, + extraData as string[], + ); + + const executionData = this.helpers.constructExecutionMetaData(wrapData(responseData), { + itemData: { item: i }, + }); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/task/update.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/task/update.operation.ts new file mode 100644 index 0000000000..824516c536 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/actions/task/update.operation.ts @@ -0,0 +1,133 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions, wrapData } from '@utils/utilities'; + +import { theHiveApiRequest } from '../../transport'; +import { fixFieldType, prepareInputItem } from '../../helpers/utils'; +import set from 'lodash/set'; + +const properties: INodeProperties[] = [ + { + displayName: 'Fields', + name: 'taskUpdateFields', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getTaskUpdateFields', + mode: 'update', + valuesLabel: 'Fields', + addAllFields: true, + multiKeyMatch: true, + }, + }, + }, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + item: INodeExecutionData, +): Promise { + let body: IDataObject = {}; + let updated = 1; + + const dataMode = this.getNodeParameter('taskUpdateFields.mappingMode', i) as string; + + if (dataMode === 'autoMapInputData') { + const schema = this.getNodeParameter('taskUpdateFields.schema', i) as IDataObject[]; + body = prepareInputItem(item.json, schema, i); + } + + if (dataMode === 'defineBelow') { + const taskUpdateFields = this.getNodeParameter('taskUpdateFields.value', i, []) as IDataObject; + body = taskUpdateFields; + } + + body = fixFieldType(body); + + const fieldsToMatchOn = this.getNodeParameter('taskUpdateFields.matchingColumns', i) as string[]; + + const updateBody: IDataObject = {}; + const matchFields: IDataObject = {}; + const { id } = body; // id would be used if matching on id, also we need to remove it from the body + + for (const field of Object.keys(body)) { + if (fieldsToMatchOn.includes(field)) { + // if field is in fieldsToMatchOn, we need to exclude it from the updateBody, as values used for matching should not be updated + matchFields[field] = body[field]; + } else { + // use set to construct the updateBody, as it allows to process customFields.fieldName + // if customFields provided under customFields property, it will be send as is + set(updateBody, field, body[field]); + } + } + + if (fieldsToMatchOn.includes('id')) { + await theHiveApiRequest.call(this, 'PATCH', `/v1/task/${id}`, body); + } else { + const filter = { + _name: 'filter', + _and: fieldsToMatchOn.map((field) => ({ + _eq: { + _field: field, + _value: matchFields[field], + }, + })), + }; + + const queryBody = { + query: [ + { + _name: 'listTask', + }, + filter, + ], + }; + + const matches = (await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + queryBody, + )) as IDataObject[]; + + if (!matches.length) { + throw new NodeOperationError(this.getNode(), 'No matching alerts found'); + } + const ids = matches.map((match) => match._id); + updated = ids.length; + + updateBody.ids = ids; + + await theHiveApiRequest.call(this, 'PATCH', '/v1/task/_bulk', updateBody); + } + + const executionData = this.helpers.constructExecutionMetaData( + wrapData({ success: true, updated }), + { + itemData: { item: i }, + }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/descriptions/common.description.ts b/packages/nodes-base/nodes/TheHiveProject/descriptions/common.description.ts new file mode 100644 index 0000000000..612f4dd446 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/descriptions/common.description.ts @@ -0,0 +1,542 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { TLP } from '../helpers/interfaces'; + +export const returnAllAndLimit: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + displayOptions: { + show: { + returnAll: [false], + }, + }, + }, +]; + +export const responderOptions: INodeProperties = { + displayName: 'Responder Name or ID', + name: 'responder', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + required: true, + default: '', + typeOptions: { + loadOptionsDependsOn: ['id', 'id.value'], + loadOptionsMethod: 'loadResponders', + }, + displayOptions: { + hide: { + id: [''], + }, + }, +}; + +export const tlpOptions: INodeProperties = { + displayName: 'Traffict Light Protocol (TLP)', + name: 'tlp', + type: 'options', + default: 2, + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], +}; + +export const severityOptions: INodeProperties = { + displayName: 'Severity', + name: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + { + name: 'Critical', + value: 4, + }, + ], + default: 2, + description: 'Severity of the alert. Default=Medium.', +}; + +export const observableTypeOptions: INodeProperties = { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Data Type', + name: 'dataType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'loadObservableTypes', + }, + description: + 'Type of the observable. Choose from the list, or specify an ID using an expression.', +}; + +export const alertStatusOptions: INodeProperties = { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'New', + value: 'New', + }, + { + name: 'Updated', + value: 'Updated', + }, + { + name: 'Ignored', + value: 'Ignored', + }, + { + name: 'Imported', + value: 'Imported', + }, + ], + default: 'New', + description: 'Status of the alert', +}; + +export const caseStatusOptions: INodeProperties = { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'Open', + }, + { + name: 'Resolved', + value: 'Resolved', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + default: 'Open', +}; + +export const observableStatusOptions: INodeProperties = { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'Ok', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + description: 'Status of the observable. Default=Ok.', +}; + +export const taskStatusOptions: INodeProperties = { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'Waiting', + options: [ + { + name: 'Cancel', + value: 'Cancel', + }, + { + name: 'Completed', + value: 'Completed', + }, + { + name: 'InProgress', + value: 'InProgress', + }, + { + name: 'Waiting', + value: 'Waiting', + }, + ], + description: 'Status of the task. Default=Waiting.', +}; + +export const searchOptions: INodeProperties = { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Return Count', + name: 'returnCount', + type: 'boolean', + description: 'Whether to return only the count of results', + default: false, + displayOptions: { + hide: { + '/returnAll': [false], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'multiOptions', + description: 'Additional data to include in the response', + options: [ + { + name: 'isOwner', + value: 'isOwner', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'links', + value: 'links', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'permissions', + value: 'permissions', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'seen', + value: 'seen', + }, + { + name: 'shareCount', + value: 'shareCount', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'shares', + value: 'shares', + }, + ], + default: [], + displayOptions: { + show: { + '/resource': ['observable'], + }, + hide: { + returnCount: [true], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'multiOptions', + description: 'Additional data to include in the response', + options: [ + { + name: 'actionRequired', + value: 'actionRequired', + }, + { + name: 'actionRequiredMap', + value: 'actionRequiredMap', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'case', + value: 'case', + }, + { + name: 'caseId', + value: 'caseId', + }, + { + name: 'caseTemplate', + value: 'caseTemplate', + }, + { + name: 'caseTemplateId', + value: 'caseTemplateId', + }, + { + name: 'shareCount', + value: 'shareCount', + }, + ], + default: [], + displayOptions: { + show: { + '/resource': ['task'], + }, + hide: { + returnCount: [true], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'multiOptions', + description: 'Additional data to include in the response', + options: [ + { + name: 'caseNumber', + value: 'caseNumber', + }, + { + name: 'importDate', + value: 'importDate', + }, + { + name: 'procedureCount', + value: 'procedureCount', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'status', + value: 'status', + }, + ], + default: [], + displayOptions: { + show: { + '/resource': ['alert'], + }, + hide: { + returnCount: [true], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'multiOptions', + description: 'Additional data to include in the response', + options: [ + { + name: 'actionRequired', + value: 'actionRequired', + }, + { + name: 'alertCount', + value: 'alertCount', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'alerts', + value: 'alerts', + }, + { + name: 'attachmentCount', + value: 'attachmentCount', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'contributors', + value: 'contributors', + }, + { + name: 'handlingDuration', + value: 'computed.handlingDuration', + }, + { + name: 'handlingDurationInDays', + value: 'computed.handlingDurationInDays', + }, + { + name: 'handlingDurationInHours', + value: 'computed.handlingDurationInHours', + }, + { + name: 'handlingDurationInMinutes', + value: 'computed.handlingDurationInMinutes', + }, + { + name: 'handlingDurationInSeconds', + value: 'computed.handlingDurationInSeconds', + }, + { + name: 'isOwner', + value: 'isOwner', + }, + { + name: 'observableStats', + value: 'observableStats', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'permissions', + value: 'permissions', + }, + { + name: 'procedureCount', + value: 'procedureCount', + }, + { + name: 'shareCount', + value: 'shareCount', + }, + { + name: 'similarAlerts', + value: 'similarAlerts', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'status', + value: 'status', + }, + { + name: 'taskStats', + value: 'taskStats', + }, + ], + default: [], + displayOptions: { + show: { + '/resource': ['case'], + }, + hide: { + returnCount: [true], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'multiOptions', + description: 'Additional data to include in the response', + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'links', + value: 'links', + }, + ], + default: [], + displayOptions: { + show: { + '/resource': ['comment'], + }, + hide: { + returnCount: [true], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'multiOptions', + description: 'Additional data to include in the response', + options: [ + { + name: 'actionCount', + value: 'actionCount', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'case', + value: 'case', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'task', + value: 'task', + }, + { + name: 'taskId', + value: 'taskId', + }, + ], + default: [], + displayOptions: { + show: { + '/resource': ['log'], + }, + hide: { + returnCount: [true], + }, + }, + }, + { + displayName: 'Extra Data', + name: 'extraData', + type: 'string', + description: 'Additional data to include in the response', + default: '', + requiresDataPath: 'multiple', + displayOptions: { + show: { + '/resource': ['query'], + }, + hide: { + returnCount: [true], + }, + }, + }, + ], +}; + +export const attachmentsUi: INodeProperties = { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachment', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Attachment Field Name', + name: 'field', + type: 'string', + default: 'data', + description: 'Add the field name from the input node', + hint: 'The name of the field with the attachment in the node input', + }, + ], + }, + ], + default: {}, + description: 'Array of supported attachments to add to the message', +}; diff --git a/packages/nodes-base/nodes/TheHiveProject/descriptions/filter.description.ts b/packages/nodes-base/nodes/TheHiveProject/descriptions/filter.description.ts new file mode 100644 index 0000000000..c1660f7a4a --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/descriptions/filter.description.ts @@ -0,0 +1,318 @@ +import type { INodeProperties } from 'n8n-workflow'; + +const field: INodeProperties[] = [ + { + displayName: 'Field', + name: 'field', + type: 'string', + default: '', + requiresDataPath: 'single', + description: 'Dot notation is also supported, e.g. customFields.field1', + displayOptions: { + hide: { + '/resource': ['alert', 'case', 'comment', 'task', 'observable', 'log', 'page'], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'loadAlertFields', + }, + description: + 'Choose from the list, or specify an ID using an expression', + displayOptions: { + show: { + '/resource': ['alert'], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'loadCaseFields', + }, + description: + 'Choose from the list, or specify an ID using an expression', + displayOptions: { + show: { + '/resource': ['case'], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'loadTaskFields', + }, + description: + 'Choose from the list, or specify an ID using an expression', + displayOptions: { + show: { + '/resource': ['task'], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'loadObservableFields', + }, + description: + 'Choose from the list, or specify an ID using an expression', + displayOptions: { + show: { + '/resource': ['observable'], + }, + }, + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + options: [ + { + name: 'Message', + value: 'message', + }, + { + name: 'Date', + value: 'date', + }, + ], + displayOptions: { + show: { + '/resource': ['log'], + }, + }, + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + options: [ + { + name: 'Message', + value: 'message', + }, + ], + displayOptions: { + show: { + '/resource': ['comment'], + }, + }, + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + options: [ + { + name: 'Category', + value: 'category', + }, + { + name: 'Content', + value: 'content', + }, + { + name: 'Title', + value: 'title', + }, + ], + displayOptions: { + show: { + '/resource': ['page'], + }, + }, + }, +]; + +export const genericFiltersCollection: INodeProperties = { + displayName: 'Filters', + name: 'filters', + type: 'fixedCollection', + placeholder: 'Add Filter', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + ...field, + { + displayName: 'Operator', + name: 'operator', + type: 'options', + options: [ + { + name: 'Between', + value: '_between', + description: "Field is between two values ('From' is inclusive, 'To' is exclusive)", + }, + { + name: 'Contains', + value: '_like', + description: 'Field contains the substring from value', + }, + { + name: 'Ends With', + value: '_endsWith', + description: 'Field ends with value', + }, + { + name: 'Equal', + value: '_eq', + description: 'Field is equal to value', + }, + { + name: 'Greater Than', + value: '_gt', + description: 'Field is greater than value', + }, + { + name: 'Greater Than Or Equal', + value: '_gte', + description: 'Field is greater than or equal to value', + }, + { + name: 'In', + value: '_in', + description: 'Field is one of the values', + }, + { + name: 'Less Than', + value: '_lt', + description: 'Field is less than value', + }, + { + name: 'Less Than Or Equal', + value: '_lte', + description: 'Field is less than or equal to value', + }, + { + name: 'Match Word', + value: '_match', + description: 'Field contains the value as a word', + }, + { + name: 'Not Equal', + value: '_ne', + description: 'Field is not equal to value', + }, + { + name: 'Starts With', + value: '_startsWith', + description: 'Field starts with value', + }, + ], + default: '_eq', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + displayOptions: { + hide: { + operator: ['_between', '_in'], + }, + }, + }, + { + displayName: 'Values', + name: 'values', + type: 'string', + default: '', + description: 'Comma-separated list of values', + placeholder: 'e.g. value1,value2', + displayOptions: { + show: { + operator: ['_in'], + }, + }, + }, + { + displayName: 'From', + name: 'from', + type: 'string', + default: '', + displayOptions: { + show: { + operator: ['_between'], + }, + }, + }, + { + displayName: 'To', + name: 'to', + type: 'string', + default: '', + displayOptions: { + show: { + operator: ['_between'], + }, + }, + }, + ], + }, + ], +}; + +export const sortCollection: INodeProperties = { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + placeholder: 'Add Sort Rule', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + values: [ + ...field, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'asc', + }, + { + name: 'Descending', + value: 'desc', + }, + ], + default: 'asc', + }, + ], + }, + ], +}; diff --git a/packages/nodes-base/nodes/TheHiveProject/descriptions/index.ts b/packages/nodes-base/nodes/TheHiveProject/descriptions/index.ts new file mode 100644 index 0000000000..53fb80b7bd --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/descriptions/index.ts @@ -0,0 +1,3 @@ +export * from './rlc.description'; +export * from './common.description'; +export * from './filter.description'; diff --git a/packages/nodes-base/nodes/TheHiveProject/descriptions/rlc.description.ts b/packages/nodes-base/nodes/TheHiveProject/descriptions/rlc.description.ts new file mode 100644 index 0000000000..529fe214ce --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/descriptions/rlc.description.ts @@ -0,0 +1,285 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const caseRLC: INodeProperties = { + displayName: 'Case', + name: 'caseId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a case...', + typeOptions: { + searchListMethod: 'caseSearch', + searchable: true, + }, + }, + { + displayName: 'Link', + name: 'url', + type: 'string', + extractValue: { + type: 'regex', + regex: 'https:\\/\\/.+\\/cases\\/(~[0-9]{1,})\\/details', + }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/.+\\/cases\\/(~[0-9]{1,})\\/details', + errorMessage: 'Not a valid Case URL', + }, + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid Case ID', + }, + }, + ], + }, + ], +}; + +export const alertRLC: INodeProperties = { + displayName: 'Alert', + name: 'alertId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a alert...', + typeOptions: { + searchListMethod: 'alertSearch', + searchable: true, + }, + }, + { + displayName: 'Link', + name: 'url', + type: 'string', + extractValue: { + type: 'regex', + regex: 'https:\\/\\/.+\\/alerts\\/(~[0-9]{1,})\\/details', + }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/.+\\/alerts\\/(~[0-9]{1,})\\/details', + errorMessage: 'Not a valid Alert URL', + }, + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid Alert ID', + }, + }, + ], + }, + ], +}; + +export const taskRLC: INodeProperties = { + displayName: 'Task', + name: 'taskId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a task...', + typeOptions: { + searchListMethod: 'taskSearch', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid Task ID', + }, + }, + ], + }, + ], +}; + +export const pageRLC: INodeProperties = { + displayName: 'Page', + name: 'pageId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['caseId'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a page...', + typeOptions: { + searchListMethod: 'pageSearch', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid Page ID', + }, + }, + ], + }, + ], +}; + +export const logRLC: INodeProperties = { + displayName: 'Task Log', + name: 'logId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a task log...', + typeOptions: { + searchListMethod: 'logSearch', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid task Log ID', + }, + }, + ], + }, + ], +}; + +export const commentRLC: INodeProperties = { + displayName: 'Comment', + name: 'commentId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a comment...', + typeOptions: { + searchListMethod: 'commentSearch', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid comment ID', + }, + }, + ], + }, + ], +}; + +export const observableRLC: INodeProperties = { + displayName: 'Observable', + name: 'observableId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select an observable...', + typeOptions: { + searchListMethod: 'observableSearch', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. ~123456789', + validation: [ + { + type: 'regex', + properties: { + regex: '(~[0-9]{1,})', + errorMessage: 'Not a valid Log ID', + }, + }, + ], + }, + ], +}; diff --git a/packages/nodes-base/nodes/TheHiveProject/helpers/constants.ts b/packages/nodes-base/nodes/TheHiveProject/helpers/constants.ts new file mode 100644 index 0000000000..33553fe127 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/helpers/constants.ts @@ -0,0 +1,543 @@ +import { TLP } from './interfaces'; + +export const alertCommonFields = [ + { + displayName: 'Title', + id: 'title', + type: 'string', + removed: false, + }, + { + displayName: 'Description', + id: 'description', + type: 'string', + removed: false, + }, + { + displayName: 'Type', + id: 'type', + type: 'string', + removed: false, + }, + { + displayName: 'Source', + id: 'source', + type: 'string', + removed: false, + }, + { + displayName: 'Source Reference', + id: 'sourceRef', + type: 'string', + removed: false, + }, + { + displayName: 'External Link', + id: 'externalLink', + type: 'string', + removed: true, + }, + { + displayName: 'Severity (Severity of information)', + id: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + { + name: 'Critical', + value: 4, + }, + ], + removed: true, + }, + { + displayName: 'Date', + id: 'date', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Last Sync Date', + id: 'lastSyncDate', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Tags', + id: 'tags', + type: 'string', + removed: true, + }, + { + displayName: 'Follow', + id: 'follow', + type: 'boolean', + removed: true, + }, + { + displayName: 'Flag', + id: 'flag', + type: 'boolean', + removed: true, + }, + { + displayName: 'TLP (Confidentiality of information)', + id: 'tlp', + type: 'options', + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + removed: true, + }, + { + displayName: 'PAP (Level of exposure of information)', + id: 'pap', + type: 'options', + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + removed: true, + }, + { + displayName: 'Summary', + id: 'summary', + type: 'string', + removed: true, + }, + { + displayName: 'Status', + id: 'status', + type: 'options', + removed: true, + }, + { + displayName: 'Case Template', + id: 'caseTemplate', + type: 'options', + removed: true, + }, + { + displayName: 'Add Tags', + id: 'addTags', + type: 'string', + canBeUsedToMatch: false, + removed: true, + }, + { + displayName: 'Remove Tags', + id: 'removeTags', + type: 'string', + canBeUsedToMatch: false, + removed: true, + }, +]; + +export const caseCommonFields = [ + { + displayName: 'Title', + id: 'title', + type: 'string', + removed: false, + }, + { + displayName: 'Description', + id: 'description', + type: 'string', + removed: false, + }, + { + displayName: 'Severity (Severity of information)', + id: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + { + name: 'Critical', + value: 4, + }, + ], + removed: false, + }, + { + displayName: 'Start Date', + id: 'startDate', + type: 'dateTime', + removed: false, + }, + { + displayName: 'End Date', + id: 'endDate', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Tags', + id: 'tags', + type: 'string', + removed: false, + }, + { + displayName: 'Flag', + id: 'flag', + type: 'boolean', + removed: true, + }, + { + displayName: 'TLP (Confidentiality of information)', + id: 'tlp', + type: 'options', + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + removed: false, + }, + { + displayName: 'PAP (Level of exposure of information)', + id: 'pap', + type: 'options', + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + removed: false, + }, + { + displayName: 'Summary', + id: 'summary', + type: 'string', + removed: true, + }, + { + displayName: 'Status', + id: 'status', + type: 'options', + removed: true, + }, + { + displayName: 'Assignee', + id: 'assignee', + type: 'options', + removed: true, + }, + { + displayName: 'Case Template', + id: 'caseTemplate', + type: 'options', + removed: true, + }, + { + displayName: 'Tasks', + id: 'tasks', + type: 'array', + removed: true, + }, + { + displayName: 'Sharing Parameters', + id: 'sharingParameters', + type: 'array', + removed: true, + }, + { + displayName: 'Impact Status', + id: 'impactStatus', + type: 'string', + removed: true, + }, + { + displayName: 'Task Rule', + id: 'taskRule', + type: 'string', + removed: true, + }, + { + displayName: 'Observable Rule', + id: 'observableRule', + type: 'string', + removed: true, + }, + { + displayName: 'Add Tags', + id: 'addTags', + type: 'string', + removed: true, + }, + { + displayName: 'Remove Tags', + id: 'removeTags', + type: 'string', + removed: true, + }, +]; + +export const taskCommonFields = [ + { + displayName: 'Title', + id: 'title', + type: 'string', + removed: false, + }, + { + displayName: 'Description', + id: 'description', + type: 'string', + removed: false, + }, + { + displayName: 'Group', + id: 'group', + type: 'string', + removed: false, + }, + { + displayName: 'Status', + id: 'status', + type: 'stirng', + removed: true, + }, + { + displayName: 'Flag', + id: 'flag', + type: 'boolean', + removed: false, + }, + { + displayName: 'Start Date', + id: 'startDate', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Due Date', + id: 'dueDate', + type: 'dateTime', + removed: false, + }, + { + displayName: 'End Date', + id: 'endDate', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Assignee', + id: 'assignee', + type: 'options', + removed: false, + }, + { + displayName: 'Mandatory', + id: 'mandatory', + type: 'boolean', + removed: false, + }, + { + displayName: 'Order', + id: 'order', + type: 'number', + removed: true, + }, +]; + +export const observableCommonFields = [ + { + displayName: 'Data Type', + id: 'dataType', + type: 'options', + removed: false, + }, + { + displayName: 'Start Date', + id: 'startDate', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Description', + id: 'message', + type: 'string', + removed: false, + }, + { + displayName: 'Tags', + id: 'tags', + type: 'string', + removed: false, + }, + { + displayName: 'TLP (Confidentiality of information)', + id: 'tlp', + type: 'options', + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + removed: false, + }, + { + displayName: 'PAP (Level of exposure of information)', + id: 'pap', + type: 'options', + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + removed: false, + }, + { + displayName: 'IOC', + id: 'ioc', + type: 'boolean', + removed: false, + }, + { + displayName: 'Sighted', + id: 'sighted', + type: 'boolean', + removed: false, + }, + { + displayName: 'Sighted At', + id: 'sightedAt', + type: 'dateTime', + removed: true, + }, + { + displayName: 'Ignore Similarity', + id: 'ignoreSimilarity', + type: 'boolean', + removed: false, + }, + { + displayName: 'Is Zip', + id: 'isZip', + type: 'boolean', + removed: true, + }, + { + displayName: 'Zip Password', + id: 'zipPassword', + type: 'string', + removed: true, + }, + { + displayName: 'Add Tags', + id: 'addTags', + type: 'string', + removed: true, + }, + { + displayName: 'Remove Tags', + id: 'removeTags', + type: 'string', + removed: true, + }, +]; diff --git a/packages/nodes-base/nodes/TheHiveProject/helpers/interfaces.ts b/packages/nodes-base/nodes/TheHiveProject/helpers/interfaces.ts new file mode 100644 index 0000000000..ebd508251f --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/helpers/interfaces.ts @@ -0,0 +1,8 @@ +export const enum TLP { + white, + green, + amber, + red, +} + +export type QueryScope = { query: string; id?: string; restrictTo?: string }; diff --git a/packages/nodes-base/nodes/TheHiveProject/helpers/utils.ts b/packages/nodes-base/nodes/TheHiveProject/helpers/utils.ts new file mode 100644 index 0000000000..2299e35a31 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/helpers/utils.ts @@ -0,0 +1,100 @@ +import type { IDataObject } from 'n8n-workflow'; + +import get from 'lodash/get'; +import set from 'lodash/set'; + +export function splitAndTrim(str: string | string[]) { + if (typeof str === 'string') { + return str + .split(',') + .map((tag) => tag.trim()) + .filter((tag) => tag); + } + return str; +} + +export function fixFieldType(fields: IDataObject) { + const returnData: IDataObject = {}; + + for (const key of Object.keys(fields)) { + if ( + [ + 'date', + 'lastSyncDate', + 'startDate', + 'endDate', + 'dueDate', + 'includeInTimeline', + 'sightedAt', + ].includes(key) + ) { + returnData[key] = Date.parse(fields[key] as string); + continue; + } + + if (['tags', 'addTags', 'removeTags'].includes(key)) { + returnData[key] = splitAndTrim(fields[key] as string); + continue; + } + + returnData[key] = fields[key]; + } + + return returnData; +} + +export function prepareInputItem(item: IDataObject, schema: IDataObject[], i: number) { + const returnData: IDataObject = {}; + + for (const entry of schema) { + const id = entry.id as string; + const value = get(item, id); + + if (value !== undefined) { + set(returnData, id, value); + } else { + if (entry.required) { + throw new Error(`Required field "${id}" is missing in item ${i}`); + } + } + } + + return returnData; +} + +export function constructFilter(entry: IDataObject) { + const { field, value } = entry; + let { operator } = entry; + + if (operator === undefined) { + operator = '_eq'; + } + + if (operator === '_between') { + const { from, to } = entry; + return { + _between: { + _field: field, + _from: from, + _to: to, + }, + }; + } + + if (operator === '_in') { + const { values } = entry; + return { + _in: { + _field: field, + _values: typeof values === 'string' ? splitAndTrim(values) : values, + }, + }; + } + + return { + [operator as string]: { + _field: field, + _value: value, + }, + }; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/methods/index.ts b/packages/nodes-base/nodes/TheHiveProject/methods/index.ts new file mode 100644 index 0000000000..b438352ebd --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/methods/index.ts @@ -0,0 +1,3 @@ +export * as loadOptions from './loadOptions'; +export * as listSearch from './listSearch'; +export * as resourceMapping from './resourceMapping'; diff --git a/packages/nodes-base/nodes/TheHiveProject/methods/listSearch.ts b/packages/nodes-base/nodes/TheHiveProject/methods/listSearch.ts new file mode 100644 index 0000000000..9849d3905a --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/methods/listSearch.ts @@ -0,0 +1,246 @@ +import type { IDataObject, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; +import { theHiveApiRequest } from '../transport'; + +async function listResource( + this: ILoadOptionsFunctions, + resource: string, + filterField: string, + nameField: string, + urlPlaceholder: string | undefined, + filter?: string, + paginationToken?: string, +): Promise { + const query: IDataObject[] = [ + { + _name: resource, + }, + ]; + + if (filter) { + query.push({ + _name: 'filter', + _like: { + _field: filterField, + _value: filter, + }, + }); + } + + const from = paginationToken !== undefined ? parseInt(paginationToken, 10) : 0; + const to = from + 100; + query.push({ + _name: 'page', + from, + to, + }); + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', { query }); + + if (response.length === 0) { + return { + results: [], + paginationToken: undefined, + }; + } + + const credentials = await this.getCredentials('theHiveProjectApi'); + const url = credentials?.url as string; + + return { + results: response.map((entry: IDataObject) => ({ + name: entry[nameField], + value: entry._id, + url: + urlPlaceholder !== undefined ? `${url}/${urlPlaceholder}/${entry._id}/details` : undefined, + })), + paginationToken: to, + }; +} + +export async function caseSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return listResource.call(this, 'listCase', 'title', 'title', 'cases', filter, paginationToken); +} + +export async function commentSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return listResource.call( + this, + 'listComment', + 'message', + 'message', + undefined, + filter, + paginationToken, + ); +} + +export async function alertSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return listResource.call(this, 'listAlert', 'title', 'title', 'alerts', filter, paginationToken); +} + +export async function taskSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return listResource.call(this, 'listTask', 'title', 'title', undefined, filter, paginationToken); +} + +export async function pageSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let caseId; + + try { + caseId = this.getNodeParameter('caseId', '', { extractValue: true }) as string; + } catch (error) { + caseId = undefined; + } + + let query: IDataObject[]; + + if (caseId) { + query = [ + { + _name: 'getCase', + idOrName: caseId, + }, + { + _name: 'pages', + }, + ]; + } else { + query = [ + { + _name: 'listOrganisationPage', + }, + ]; + } + + if (filter) { + query.push({ + _name: 'filter', + _like: { + _field: 'title', + _value: filter, + }, + }); + } + + const from = paginationToken !== undefined ? parseInt(paginationToken, 10) : 0; + const to = from + 100; + query.push({ + _name: 'page', + from, + to, + }); + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', { query }); + + if (response.length === 0) { + return { + results: [], + paginationToken: undefined, + }; + } + + return { + results: response.map((entry: IDataObject) => ({ + name: entry.title, + value: entry._id, + })), + paginationToken: to, + }; +} + +export async function logSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return listResource.call( + this, + 'listLog', + 'message', + 'message', + undefined, + filter, + paginationToken, + ); +} + +export async function observableSearch( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + const query: IDataObject[] = [ + { + _name: 'listObservable', + }, + ]; + + if (filter) { + query.push({ + _name: 'filter', + _or: [ + { + _like: { + _field: 'data', + _value: filter, + }, + }, + { + _like: { + _field: 'message', + _value: filter, + }, + }, + { + _like: { + _field: 'attachment.name', + _value: filter, + }, + }, + ], + }); + } + + const from = paginationToken !== undefined ? parseInt(paginationToken, 10) : 0; + const to = from + 100; + query.push({ + _name: 'page', + from, + to, + }); + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', { query }); + + if (response.length === 0) { + return { + results: [], + paginationToken: undefined, + }; + } + + return { + results: response.map((entry: IDataObject) => ({ + name: entry.data || (entry.attachment as IDataObject)?.name || entry.message || entry._id, + value: entry._id, + })), + paginationToken: to, + }; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/methods/loadOptions.ts b/packages/nodes-base/nodes/TheHiveProject/methods/loadOptions.ts new file mode 100644 index 0000000000..3e7c925257 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/methods/loadOptions.ts @@ -0,0 +1,348 @@ +import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; +import { theHiveApiRequest } from '../transport'; +import { + alertCommonFields, + caseCommonFields, + observableCommonFields, + taskCommonFields, +} from '../helpers/constants'; + +export async function loadResponders(this: ILoadOptionsFunctions): Promise { + let resource = this.getNodeParameter('resource') as string; + + let resourceId = ''; + + if (['case', 'alert', 'observable', 'log', 'task'].includes(resource)) { + resourceId = this.getNodeParameter('id', '', { extractValue: true }) as string; + } else { + resourceId = this.getNodeParameter('id') as string; + } + + switch (resource) { + case 'observable': + resource = 'case_artifact'; + break; + case 'task': + resource = 'case_task'; + break; + case 'log': + resource = 'case_task_log'; + break; + } + + const responders = await theHiveApiRequest.call( + this, + 'GET', + `/connector/cortex/responder/${resource}/${resourceId}`, + ); + + const returnData: INodePropertyOptions[] = []; + + for (const responder of responders) { + returnData.push({ + name: responder.name as string, + value: responder.id, + description: responder.description as string, + }); + } + + return returnData; +} + +export async function loadAnalyzers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const dataType = this.getNodeParameter('dataType') as string; + + const requestResult = await theHiveApiRequest.call( + this, + 'GET', + `/connector/cortex/analyzer/type/${dataType}`, + ); + + for (const analyzer of requestResult) { + for (const cortexId of analyzer.cortexIds) { + returnData.push({ + name: `[${cortexId}] ${analyzer.name}`, + value: `${analyzer.id as string}::${cortexId as string}`, + description: analyzer.description as string, + }); + } + } + + return returnData; +} + +export async function loadCustomFields( + this: ILoadOptionsFunctions, +): Promise { + const requestResult = await theHiveApiRequest.call(this, 'GET', '/customField'); + + const returnData: INodePropertyOptions[] = []; + + for (const field of requestResult) { + returnData.push({ + name: `Custom Field: ${(field.displayName || field.name) as string}`, + value: `customFields.${field.name}`, + // description: `${field.type}: ${field.description}`, + } as INodePropertyOptions); + } + + return returnData; +} + +export async function loadObservableTypes( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + + const body = { + query: [ + { + _name: 'listObservableType', + }, + ], + }; + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of response) { + returnData.push({ + name: `${entry.name as string}${entry.isAttachment ? ' (attachment)' : ''}`, + value: entry.name, + }); + } + return returnData; +} +export async function loadCaseAttachments( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const caseId = this.getNodeParameter('caseId', '', { extractValue: true }) as string; + + const body = { + query: [ + { + _name: 'getCase', + idOrName: caseId, + }, + { + _name: 'attachments', + }, + ], + }; + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of response) { + returnData.push({ + name: entry.name as string, + value: entry._id, + description: `Content-Type: ${entry.contentType}`, + }); + } + return returnData; +} + +export async function loadLogAttachments( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const logId = this.getNodeParameter('logId', '', { extractValue: true }) as string; + + const body = { + query: [ + { + _name: 'getLog', + idOrName: logId, + }, + ], + }; + + // extract log object from array + const [response] = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of (response.attachments as IDataObject[]) || []) { + returnData.push({ + name: entry.name as string, + value: entry._id as string, + description: `Content-Type: ${entry.contentType}`, + }); + } + return returnData; +} + +export async function loadAlertStatus( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + + const body = { + query: [ + { + _name: 'listAlertStatus', + }, + ], + }; + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of response) { + returnData.push({ + name: entry.value, + value: entry.value, + description: `Stage: ${entry.stage}`, + }); + } + return returnData.sort((a, b) => a.name.localeCompare(b.name)); +} + +export async function loadCaseStatus(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const body = { + query: [ + { + _name: 'listCaseStatus', + }, + ], + }; + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of response) { + returnData.push({ + name: entry.value, + value: entry.value, + description: `Stage: ${entry.stage}`, + }); + } + return returnData.sort((a, b) => a.name.localeCompare(b.name)); +} + +export async function loadCaseTemplate( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + + const body = { + query: [ + { + _name: 'listCaseTemplate', + }, + ], + }; + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of response) { + returnData.push({ + name: entry.displayName || entry.name, + value: entry.name, + }); + } + return returnData; +} + +export async function loadUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const body = { + query: [ + { + _name: 'listUser', + }, + ], + }; + + const response = await theHiveApiRequest.call(this, 'POST', '/v1/query', body); + + for (const entry of response) { + returnData.push({ + name: entry.name, + value: entry.login, + }); + } + return returnData; +} + +export async function loadAlertFields( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + + const excludeFields = ['addTags', 'removeTags']; + + const fields = alertCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const field: INodePropertyOptions = { + name: entry.displayName || entry.id, + value: entry.id, + }; + + return field; + }); + + const customFields = await loadCustomFields.call(this); + + returnData.push(...fields, ...customFields); + return returnData; +} + +export async function loadCaseFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const excludeFields = ['addTags', 'removeTags', 'taskRule', 'observableRule']; + + const fields = caseCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const field: INodePropertyOptions = { + name: entry.displayName || entry.id, + value: entry.id, + }; + + return field; + }); + + const customFields = await loadCustomFields.call(this); + + returnData.push(...fields, ...customFields); + return returnData; +} + +export async function loadObservableFields( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + + const excludeFields = ['addTags', 'removeTags', 'zipPassword']; + + const fields = observableCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const field: INodePropertyOptions = { + name: entry.displayName || entry.id, + value: entry.id, + }; + + return field; + }); + + returnData.push(...fields); + return returnData; +} + +export async function loadTaskFields(this: ILoadOptionsFunctions): Promise { + const fields = taskCommonFields.map((entry) => { + const field: INodePropertyOptions = { + name: entry.displayName || entry.id, + value: entry.id, + }; + + return field; + }); + + return fields; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/methods/resourceMapping.ts b/packages/nodes-base/nodes/TheHiveProject/methods/resourceMapping.ts new file mode 100644 index 0000000000..4157eacb2a --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/methods/resourceMapping.ts @@ -0,0 +1,442 @@ +import type { + FieldType, + IDataObject, + ILoadOptionsFunctions, + ResourceMapperField, + ResourceMapperFields, +} from 'n8n-workflow'; + +import { theHiveApiRequest } from '../transport'; + +import { + loadAlertStatus, + loadCaseStatus, + loadCaseTemplate, + loadObservableTypes, + loadUsers, +} from './loadOptions'; + +import { + alertCommonFields, + caseCommonFields, + observableCommonFields, + taskCommonFields, +} from '../helpers/constants'; + +async function getCustomFields(this: ILoadOptionsFunctions) { + const customFields = (await theHiveApiRequest.call(this, 'POST', '/v1/query', { + query: [ + { + _name: 'listCustomField', + }, + ], + })) as IDataObject[]; + + return customFields.map((field) => ({ + displayName: `Custom Field: ${(field.displayName || field.name) as string}`, + id: `customFields.${field.name}`, + required: false, + display: true, + type: (field.options as string[])?.length ? 'options' : (field.type as FieldType), + defaultMatch: false, + options: (field.options as string[])?.length + ? (field.options as string[]).map((option) => ({ name: option, value: option })) + : undefined, + removed: true, + })); +} + +export async function getAlertFields(this: ILoadOptionsFunctions): Promise { + const alertStatus = await loadAlertStatus.call(this); + const caseTemplates = await loadCaseTemplate.call(this); + + const requiredFields = ['title', 'description', 'type', 'source', 'sourceRef']; + const excludeFields = ['addTags', 'removeTags', 'lastSyncDate']; + + const fields: ResourceMapperField[] = alertCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + }; + + if (requiredFields.includes(entry.id)) { + field.required = true; + } + + if (field.id === 'status') { + field.options = alertStatus; + } + + if (field.id === 'caseTemplate') { + field.options = caseTemplates; + } + return field; + }); + + const customFields = (await getCustomFields.call(this)) || []; + fields.push(...customFields); + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getAlertUpdateFields( + this: ILoadOptionsFunctions, +): Promise { + const alertStatus = await loadAlertStatus.call(this); + const excludedFromMatching = ['addTags', 'removeTags']; + const excludeFields = ['flag', 'caseTemplate']; + + const alertUpdateFields = alertCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + canBeUsedToMatch: true, + }; + + if (excludedFromMatching.includes(field.id)) { + field.canBeUsedToMatch = false; + } + + if (field.id === 'status') { + field.options = alertStatus; + } + return field; + }); + + const fields: ResourceMapperField[] = [ + { + displayName: 'ID', + id: 'id', + required: false, + display: true, + type: 'string', + defaultMatch: true, + canBeUsedToMatch: true, + }, + ...alertUpdateFields, + ]; + + const customFields = (await getCustomFields.call(this)) || []; + fields.push(...customFields); + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getCaseFields(this: ILoadOptionsFunctions): Promise { + const caseStatus = await loadCaseStatus.call(this); + const caseTemplates = await loadCaseTemplate.call(this); + const users = await loadUsers.call(this); + + const requiredFields = ['title', 'description']; + const excludeCreateFields = ['impactStatus', 'taskRule', 'addTags', 'removeTags']; + + const fields: ResourceMapperField[] = caseCommonFields + .filter((entry) => !excludeCreateFields.includes(entry.id)) + .map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + }; + + if (requiredFields.includes(entry.id)) { + field.required = true; + } + + if (field.id === 'assignee') { + field.options = users; + } + + if (field.id === 'status') { + field.options = caseStatus; + } + + if (field.id === 'caseTemplate') { + field.options = caseTemplates; + } + return field; + }); + + const customFields = (await getCustomFields.call(this)) || []; + fields.push(...customFields); + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getCaseUpdateFields( + this: ILoadOptionsFunctions, +): Promise { + const caseStatus = await loadCaseStatus.call(this); + const users = await loadUsers.call(this); + + const excludedFromMatching = ['addTags', 'removeTags', 'taskRule', 'observableRule']; + const excludeUpdateFields = ['caseTemplate', 'tasks', 'sharingParameters']; + + const caseUpdateFields = caseCommonFields + .filter((entry) => !excludeUpdateFields.includes(entry.id)) + .map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + canBeUsedToMatch: true, + }; + + if (excludedFromMatching.includes(field.id)) { + field.canBeUsedToMatch = false; + } + + if (field.id === 'assignee') { + field.options = users; + } + + if (field.id === 'status') { + field.options = caseStatus; + } + return field; + }); + + const fields: ResourceMapperField[] = [ + { + displayName: 'ID', + id: 'id', + required: false, + display: true, + type: 'string', + defaultMatch: true, + canBeUsedToMatch: true, + }, + ...caseUpdateFields, + ]; + + const customFields = (await getCustomFields.call(this)) || []; + fields.push(...customFields); + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getTaskFields(this: ILoadOptionsFunctions): Promise { + const users = await loadUsers.call(this); + + const requiredFields = ['title']; + + const fields: ResourceMapperField[] = taskCommonFields.map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + }; + + if (requiredFields.includes(entry.id)) { + field.required = true; + } + + if (field.id === 'assignee') { + field.options = users; + } + + return field; + }); + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getTaskUpdateFields( + this: ILoadOptionsFunctions, +): Promise { + const users = await loadUsers.call(this); + + const caseUpdateFields = taskCommonFields.map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + canBeUsedToMatch: true, + }; + + if (field.id === 'assignee') { + field.options = users; + } + + return field; + }); + + const fields: ResourceMapperField[] = [ + { + displayName: 'ID', + id: 'id', + required: false, + display: true, + type: 'string', + defaultMatch: true, + canBeUsedToMatch: true, + }, + ...caseUpdateFields, + ]; + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getLogFields(this: ILoadOptionsFunctions): Promise { + const fields: ResourceMapperField[] = [ + { + displayName: 'Message', + id: 'message', + required: true, + display: true, + type: 'string', + defaultMatch: true, + }, + { + displayName: 'Start Date', + id: 'startDate', + required: false, + display: true, + type: 'dateTime', + defaultMatch: false, + removed: true, + }, + { + displayName: 'Include In Timeline', + id: 'includeInTimeline', + required: false, + display: true, + type: 'dateTime', + defaultMatch: false, + removed: true, + }, + ]; + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getObservableFields( + this: ILoadOptionsFunctions, +): Promise { + const excludeFields = ['addTags', 'removeTags', 'dataType']; + + const fields: ResourceMapperField[] = observableCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + }; + + return field; + }); + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} + +export async function getObservableUpdateFields( + this: ILoadOptionsFunctions, +): Promise { + const dataTypes = await loadObservableTypes.call(this); + + const excludedFromMatching = ['addTags', 'removeTags']; + const excludeFields: string[] = ['attachment', 'data', 'startDate', 'zipPassword', 'isZip']; + + const caseUpdateFields = observableCommonFields + .filter((entry) => !excludeFields.includes(entry.id)) + .map((entry) => { + const type = entry.type as FieldType; + const field: ResourceMapperField = { + ...entry, + type, + required: false, + display: true, + defaultMatch: false, + canBeUsedToMatch: true, + }; + + if (excludedFromMatching.includes(field.id)) { + field.canBeUsedToMatch = false; + } + + if (field.id === 'dataType') { + field.options = dataTypes; + } + + return field; + }); + + const fields: ResourceMapperField[] = [ + { + displayName: 'ID', + id: 'id', + required: false, + display: true, + type: 'string', + defaultMatch: true, + canBeUsedToMatch: true, + }, + ...caseUpdateFields, + ]; + + const columnData: ResourceMapperFields = { + fields, + }; + + return columnData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/test/transport.test.ts b/packages/nodes-base/nodes/TheHiveProject/test/transport.test.ts new file mode 100644 index 0000000000..4359af3b6f --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/test/transport.test.ts @@ -0,0 +1,164 @@ +import type { IExecuteFunctions } from 'n8n-workflow'; +import * as transport from '../transport/requestApi'; + +import { theHiveApiQuery } from '../transport/queryHelper'; + +import nock from 'nock'; + +jest.mock('../transport/requestApi', () => { + const originalModule = jest.requireActual('../transport/requestApi'); + return { + ...originalModule, + theHiveApiRequest: jest.fn(async function () { + return {}; + }), + }; +}); + +const fakeExecuteFunction = {} as unknown as IExecuteFunctions; + +describe('Test TheHiveProject, theHiveApiQuery', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../transport/requestApi'); + }); + + it('should make list query request', async () => { + const scope = { + query: 'listOrganisationPage', + }; + const filtersValues = [ + { + field: 'title', + operator: '_like', + value: 'Test', + }, + ]; + const sortFields = [ + { + field: 'title', + direction: 'asc', + }, + ]; + const limit = undefined; + const returnCount = false; + + await theHiveApiQuery.call( + fakeExecuteFunction, + scope, + filtersValues, + sortFields, + limit, + returnCount, + ); + + expect(transport.theHiveApiRequest).toHaveBeenCalledTimes(1); + expect(transport.theHiveApiRequest).toHaveBeenCalledWith('POST', '/v1/query', { + query: [ + { _name: 'listOrganisationPage' }, + { _and: [{ _like: { _field: 'title', _value: 'Test' } }], _name: 'filter' }, + { _fields: [{ title: 'asc' }], _name: 'sort' }, + { _name: 'page', extraData: undefined, from: 0, to: 500 }, + ], + }); + }); + + it('should make get query request', async () => { + const scope = { + query: 'getTask', + id: '~368644136', + restrictTo: 'logs', + }; + const filtersValues = [ + { + field: 'message', + operator: '_like', + value: 'Test', + }, + { + field: 'date', + operator: '_gt', + value: 1687263671915, + }, + ]; + const sortFields = [ + { + field: 'message', + direction: 'desc', + }, + ]; + const limit = undefined; + const returnCount = false; + const extraData = ['taskId', 'case']; + + await theHiveApiQuery.call( + fakeExecuteFunction, + scope, + filtersValues, + sortFields, + limit, + returnCount, + extraData, + ); + + expect(transport.theHiveApiRequest).toHaveBeenCalledTimes(2); + expect(transport.theHiveApiRequest).toHaveBeenCalledWith('POST', '/v1/query', { + query: [ + { _name: 'getTask', idOrName: '~368644136' }, + { _name: 'logs' }, + { + _and: [ + { _like: { _field: 'message', _value: 'Test' } }, + { _gt: { _field: 'date', _value: 1687263671915 } }, + ], + _name: 'filter', + }, + { _fields: [{ message: 'desc' }], _name: 'sort' }, + { _name: 'page', extraData: ['taskId', 'case'], from: 0, to: 500 }, + ], + }); + }); + + it('should make return count query request', async () => { + const scope = { + query: 'listOrganisationPage', + }; + const returnCount = true; + + await theHiveApiQuery.call( + fakeExecuteFunction, + scope, + undefined, + undefined, + undefined, + returnCount, + ); + + expect(transport.theHiveApiRequest).toHaveBeenCalledTimes(3); + expect(transport.theHiveApiRequest).toHaveBeenCalledWith('POST', '/v1/query', { + query: [{ _name: 'listOrganisationPage' }, { _name: 'count' }], + }); + }); + + it('should set limit to query request', async () => { + const scope = { + query: 'listOrganisationPage', + }; + + const limit = 15; + + await theHiveApiQuery.call(fakeExecuteFunction, scope, undefined, undefined, limit); + + expect(transport.theHiveApiRequest).toHaveBeenCalledTimes(4); + expect(transport.theHiveApiRequest).toHaveBeenCalledWith('POST', '/v1/query', { + query: [ + { _name: 'listOrganisationPage' }, + { _name: 'page', extraData: undefined, from: 0, to: 15 }, + ], + }); + }); +}); diff --git a/packages/nodes-base/nodes/TheHiveProject/test/utils.test.ts b/packages/nodes-base/nodes/TheHiveProject/test/utils.test.ts new file mode 100644 index 0000000000..af2e5855ff --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/test/utils.test.ts @@ -0,0 +1,179 @@ +import { splitAndTrim, fixFieldType, prepareInputItem, constructFilter } from '../helpers/utils'; + +describe('Test TheHiveProject, splitAndTrim', () => { + it('should split and trim string, removing empty entries', () => { + const data = 'a, b,, c, d, e, f,,'; + + const result = splitAndTrim(data); + + expect(result).toEqual(['a', 'b', 'c', 'd', 'e', 'f']); + }); + + it('should return unchanged array', () => { + const data = ['a', 'b', 'c', 'd', 'e', 'f']; + + const result = splitAndTrim(data); + + expect(result).toEqual(data); + }); +}); + +describe('Test TheHiveProject, fixFieldType', () => { + it('should split and trim tags', () => { + const data = { + tags: 'a, b,, c, d, e, f,,', + addTags: 'a, b,, c, d, e, f,,', + removeTags: 'a, b,, c, d, e, f,,', + notChanged: 'a, b,, c, d, e, f,,', + }; + + const result = fixFieldType(data); + + expect(result).toEqual({ + tags: ['a', 'b', 'c', 'd', 'e', 'f'], + addTags: ['a', 'b', 'c', 'd', 'e', 'f'], + removeTags: ['a', 'b', 'c', 'd', 'e', 'f'], + notChanged: 'a, b,, c, d, e, f,,', + }); + }); + + it('should convert date strings to milis', () => { + const data = { + date: '2020-01-01T00:00:00.000Z', + lastSyncDate: '2020-01-01T00:00:00.000Z', + startDate: '2020-01-01T00:00:00.000Z', + endDate: '2020-01-01T00:00:00.000Z', + dueDate: '2020-01-01T00:00:00.000Z', + includeInTimeline: '2020-01-01T00:00:00.000Z', + sightedAt: '2020-01-01T00:00:00.000Z', + notChanged: '2020-01-01T00:00:00.000Z', + }; + + const result = fixFieldType(data); + + expect(result).toEqual({ + date: 1577836800000, + lastSyncDate: 1577836800000, + startDate: 1577836800000, + endDate: 1577836800000, + dueDate: 1577836800000, + includeInTimeline: 1577836800000, + sightedAt: 1577836800000, + notChanged: '2020-01-01T00:00:00.000Z', + }); + }); +}); + +describe('Test TheHiveProject, prepareInputItem', () => { + it('should return object with fields present in schema', () => { + const data = { + a: 1, + b: 2, + c: 3, + d: 4, + f: 5, + g: 6, + }; + + const schema = [ + { + id: 'a', + required: true, + }, + { + id: 'b', + required: true, + }, + { + id: 'c', + }, + { + id: 'd', + required: true, + }, + { + id: 'e', + }, + ]; + + const result = prepareInputItem(data, schema, 0); + + expect(result).toEqual({ + a: 1, + b: 2, + c: 3, + d: 4, + }); + }); +}); + +describe('Test TheHiveProject, constructFilter', () => { + it('should add default operator _eq', () => { + const data = { + field: 'myField', + value: 'myValue', + }; + + const result = constructFilter(data); + + expect(result).toEqual({ + _eq: { + _field: 'myField', + _value: 'myValue', + }, + }); + }); + + it('should return filter _gte', () => { + const data = { + field: 'myField', + value: 'myValue', + operator: '_gte', + }; + + const result = constructFilter(data); + + expect(result).toEqual({ + _gte: { + _field: 'myField', + _value: 'myValue', + }, + }); + }); + + it('should return filter _in', () => { + const data = { + field: 'myField', + values: 'a, b,, c, d', + operator: '_in', + }; + + const result = constructFilter(data); + + expect(result).toEqual({ + _in: { + _field: 'myField', + _values: ['a', 'b', 'c', 'd'], + }, + }); + }); + + it('should return filter _between', () => { + const data = { + field: 'myField', + from: 'a', + to: 'b', + operator: '_between', + }; + + const result = constructFilter(data); + + expect(result).toEqual({ + _between: { + _field: 'myField', + _from: 'a', + _to: 'b', + }, + }); + }); +}); diff --git a/packages/nodes-base/nodes/TheHiveProject/thehiveproject.svg b/packages/nodes-base/nodes/TheHiveProject/thehiveproject.svg new file mode 100644 index 0000000000..fce8a64655 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/thehiveproject.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/TheHiveProject/transport/index.ts b/packages/nodes-base/nodes/TheHiveProject/transport/index.ts new file mode 100644 index 0000000000..7b846a2251 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/transport/index.ts @@ -0,0 +1,2 @@ +export * from './queryHelper'; +export * from './requestApi'; diff --git a/packages/nodes-base/nodes/TheHiveProject/transport/queryHelper.ts b/packages/nodes-base/nodes/TheHiveProject/transport/queryHelper.ts new file mode 100644 index 0000000000..69cd05f3e8 --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/transport/queryHelper.ts @@ -0,0 +1,101 @@ +import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; + +import type { QueryScope } from '../helpers/interfaces'; +import { constructFilter } from '../helpers/utils'; +import { theHiveApiRequest } from './requestApi'; + +export async function theHiveApiQuery( + this: IExecuteFunctions, + scope: QueryScope, + filters?: IDataObject[], + sortFields?: IDataObject[], + limit?: number, + returnCount = false, + extraData?: string[], +) { + const query: IDataObject[] = []; + + if (scope.id) { + query.push({ + _name: scope.query, + idOrName: scope.id, + }); + } else { + query.push({ + _name: scope.query, + }); + } + + if (scope.restrictTo) { + query.push({ + _name: scope.restrictTo, + }); + } + + if (filters && Array.isArray(filters) && filters.length) { + const filter = { + _name: 'filter', + _and: filters.filter((f) => f.field).map(constructFilter), + }; + + query.push(filter); + } + + if (sortFields?.length && !returnCount) { + const sort = { + _name: 'sort', + _fields: sortFields.map((field) => { + return { + [`${field.field as string}`]: field.direction as string, + }; + }), + }; + + query.push(sort); + } + + let responseData: IDataObject[] = []; + + if (returnCount) { + query.push({ + _name: 'count', + }); + + const count = await theHiveApiRequest.call(this, 'POST', '/v1/query', { query }); + + responseData.push({ count }); + } else if (limit) { + const pagination: IDataObject = { + _name: 'page', + from: 0, + to: limit, + extraData, + }; + + query.push(pagination); + responseData = await theHiveApiRequest.call(this, 'POST', '/v1/query', { query }); + } else { + let to = 500; + let from = 0; + let response: IDataObject[] = []; + + do { + const pagination: IDataObject = { + _name: 'page', + from, + to, + extraData, + }; + + response = await theHiveApiRequest.call(this, 'POST', '/v1/query', { + query: [...query, pagination], + }); + + responseData = responseData.concat(response || []); + from = to; + to += 500; + } while (response?.length); + } + + return responseData; +} diff --git a/packages/nodes-base/nodes/TheHiveProject/transport/requestApi.ts b/packages/nodes-base/nodes/TheHiveProject/transport/requestApi.ts new file mode 100644 index 0000000000..0b832f9cbe --- /dev/null +++ b/packages/nodes-base/nodes/TheHiveProject/transport/requestApi.ts @@ -0,0 +1,42 @@ +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IDataObject, + IHttpRequestOptions, + IHttpRequestMethods, +} from 'n8n-workflow'; + +export async function theHiveApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + resource: string, + body: IDataObject | FormData = {}, + query: IDataObject = {}, + uri?: string, + option: IDataObject = {}, +) { + const credentials = await this.getCredentials('theHiveProjectApi'); + + let options: IHttpRequestOptions = { + method, + qs: query, + url: uri || `${credentials.url}/api${resource}`, + body, + skipSslCertificateValidation: credentials.allowUnauthorizedCerts as boolean, + json: true, + }; + + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (Object.keys(query).length === 0) { + delete options.qs; + } + return this.helpers.requestWithAuthentication.call(this, 'theHiveProjectApi', options); +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 08db8673c1..5cf314d1b9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -329,6 +329,7 @@ "dist/credentials/TaigaApi.credentials.js", "dist/credentials/TapfiliateApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", + "dist/credentials/TheHiveProjectApi.credentials.js", "dist/credentials/TheHiveApi.credentials.js", "dist/credentials/TimescaleDb.credentials.js", "dist/credentials/TodoistApi.credentials.js", @@ -726,6 +727,8 @@ "dist/nodes/Tapfiliate/Tapfiliate.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", + "dist/nodes/TheHiveProject/TheHiveProject.node.js", + "dist/nodes/TheHiveProject/TheHiveProjectTrigger.node.js", "dist/nodes/TheHive/TheHive.node.js", "dist/nodes/TheHive/TheHiveTrigger.node.js", "dist/nodes/TimescaleDb/TimescaleDb.node.js", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 84f142fa05..e236d865b4 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1078,6 +1078,7 @@ export interface INodePropertyTypeOptions { export interface ResourceMapperTypeOptions { resourceMapperMethod: string; mode: 'add' | 'update' | 'upsert'; + valuesLabel?: string; fieldWords?: { singular: string; plural: string }; addAllFields?: boolean; noFieldsError?: string;