diff --git a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts index 18d22ec1df..d37cf9f770 100644 --- a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts @@ -13,6 +13,7 @@ import { } from 'n8n-workflow'; import * as moment from 'moment'; +import { Eq } from './QueryFunctions'; export async function theHiveApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('theHiveApi'); @@ -77,7 +78,10 @@ export function prepareOptional(optionals: IDataObject): IDataObject { const response: IDataObject = {}; for (const key in optionals) { if (optionals[key] !== undefined && optionals[key] !== null && optionals[key] !== '') { - if (moment(optionals[key] as string, moment.ISO_8601).isValid()) { + if (['customFieldsJson', 'customFieldsUi'].indexOf(key) > -1) { + continue; // Ignore customFields, they need special treatment + } + else if (moment(optionals[key] as string, moment.ISO_8601).isValid()) { response[key] = Date.parse(optionals[key] as string); } else if (key === 'artifacts') { response[key] = JSON.parse(optionals[key] as string); @@ -91,6 +95,73 @@ export function prepareOptional(optionals: IDataObject): IDataObject { return response; } +export async function prepareCustomFields(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, additionalFields: IDataObject, jsonParameters = false): Promise { + // Check if the additionalFields object contains customFields + if (jsonParameters === true) { + const customFieldsJson = additionalFields.customFieldsJson; + // Delete from additionalFields as some operations (e.g. alert:update) do not run prepareOptional + // which would remove the extra fields + delete additionalFields.customFieldsJson; + + if (typeof customFieldsJson === 'string') { + return JSON.parse(customFieldsJson); + } else if (typeof customFieldsJson === 'object') { + return customFieldsJson as IDataObject; + } else if (customFieldsJson) { + throw Error('customFieldsJson value is invalid'); + } + } else if (additionalFields.customFieldsUi) { + // Get Custom Field Types from TheHive + const version = this.getCredentials('theHiveApi')?.apiVersion; + const endpoint = version === 'v1' ? '/customField' : '/list/custom_fields'; + + const requestResult = await theHiveApiRequest.call( + this, + 'GET', + endpoint as string, + ); + + // Convert TheHive3 response to the same format as TheHive 4 + // [{name, reference, type}] + const hiveCustomFields = version === 'v1' ? requestResult : Object.keys(requestResult).map(key => requestResult[key]); + // Build reference to type mapping object + const referenceTypeMapping = hiveCustomFields.reduce((acc: IDataObject, curr: IDataObject) => (acc[curr.reference as string] = curr.type, acc), {}); + + // Build "fieldName": {"type": "value"} objects + const customFieldsUi = (additionalFields.customFieldsUi as IDataObject); + const customFields : IDataObject = (customFieldsUi?.customFields as IDataObject[]).reduce((acc: IDataObject, curr: IDataObject) => { + const fieldName = curr.field as string; + + // Might be able to do some type conversions here if needed, TODO + + acc[fieldName] = { + [referenceTypeMapping[fieldName]]: curr.value, + }; + return acc; + }, {} as IDataObject); + + delete additionalFields.customFieldsUi; + return customFields; + } + return undefined; +} + +export function buildCustomFieldSearch(customFields: IDataObject): IDataObject[] { + const customFieldTypes = ['boolean', 'date', 'float', 'integer', 'number', 'string']; + const searchQueries: IDataObject[] = []; + Object.keys(customFields).forEach(customFieldName => { + const customField = customFields[customFieldName] as IDataObject; + + // Figure out the field type from the object's keys + const fieldType = Object.keys(customField) + .filter(key => customFieldTypes.indexOf(key) > -1)[0]; + const fieldValue = customField[fieldType]; + + searchQueries.push(Eq(`customFields.${customFieldName}.${fieldType}`, fieldValue)); + }); + return searchQueries; +} + export function prepareSortQuery(sort: string, body: { query: [IDataObject] }) { if (sort) { const field = sort.substring(1); diff --git a/packages/nodes-base/nodes/TheHive/TheHive.node.ts b/packages/nodes-base/nodes/TheHive/TheHive.node.ts index 5b3a93c7f0..0db28ce56c 100644 --- a/packages/nodes-base/nodes/TheHive/TheHive.node.ts +++ b/packages/nodes-base/nodes/TheHive/TheHive.node.ts @@ -56,7 +56,9 @@ import { } from './QueryFunctions'; import { + buildCustomFieldSearch, mapResource, + prepareCustomFields, prepareOptional, prepareRangeQuery, prepareSortQuery, @@ -180,6 +182,31 @@ export class TheHive implements INodeType { } return returnData; }, + async loadCustomFields(this: ILoadOptionsFunctions): Promise { + const version = this.getCredentials('theHiveApi')?.apiVersion; + const endpoint = version === 'v1' ? '/customField' : '/list/custom_fields'; + + const requestResult = await theHiveApiRequest.call( + this, + 'GET', + endpoint as string, + ); + + const returnData: INodePropertyOptions[] = []; + + // Convert TheHive3 response to the same format as TheHive 4 + const customFields = version === 'v1' ? requestResult : Object.keys(requestResult).map(key => requestResult[key]); + + for (const field of customFields) { + returnData.push({ + name: `${field.name}: ${field.reference}`, + value: field.reference, + description: `${field.type}: ${field.description}`, + } as INodePropertyOptions); + } + + return returnData; + }, async loadObservableOptions(this: ILoadOptionsFunctions): Promise { // if v1 is not used we remove 'count' option const version = this.getCredentials('theHiveApi')?.apiVersion; @@ -296,10 +323,17 @@ export class TheHive implements INodeType { for (let i = 0; i < length; i++) { if (resource === 'alert') { if (operation === 'count') { - const countQueryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); // tslint:disable-line:no-any - + const filters = this.getNodeParameter('filters', i, {}) as INodeParameters; + const countQueryAttributs: any = prepareOptional(filters); // tslint:disable-line:no-any + const _countSearchQuery: IQueryObject = And(); + if ('customFieldsUi' in filters) { + const customFields = await prepareCustomFields.call(this, filters) as IDataObject; + const searchQueries = buildCustomFieldSearch(customFields); + (_countSearchQuery['_and'] as IQueryObject[]).push(...searchQueries); + } + for (const key of Object.keys(countQueryAttributs)) { if (key === 'tags') { (_countSearchQuery['_and'] as IQueryObject[]).push( @@ -348,6 +382,10 @@ export class TheHive implements INodeType { } if (operation === 'create') { + const additionalFields = this.getNodeParameter('additionalFields', i) as INodeParameters; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + const customFields = await prepareCustomFields.call(this, additionalFields, jsonParameters); const body: IDataObject = { title: this.getNodeParameter('title', i), description: this.getNodeParameter('description', i), @@ -360,7 +398,8 @@ export class TheHive implements INodeType { source: this.getNodeParameter('source', i), sourceRef: this.getNodeParameter('sourceRef', i), follow: this.getNodeParameter('follow', i, true), - ...prepareOptional(this.getNodeParameter('optionals', i, {}) as INodeParameters), + customFields, + ...prepareOptional(additionalFields), }; const artifactUi = this.getNodeParameter('artifactUi', i) as IDataObject; @@ -497,12 +536,18 @@ export class TheHive implements INodeType { const version = credentials.apiVersion; - const queryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); // tslint:disable-line:no-any - + const filters = this.getNodeParameter('filters', i, {}) as INodeParameters; + const queryAttributs: any = prepareOptional(filters); // tslint:disable-line:no-any const options = this.getNodeParameter('options', i) as IDataObject; const _searchQuery: IQueryObject = And(); + if ('customFieldsUi' in filters) { + const customFields = await prepareCustomFields.call(this, filters) as IDataObject; + const searchQueries = buildCustomFieldSearch(customFields); + (_searchQuery['_and'] as IQueryObject[]).push(...searchQueries); + } + for (const key of Object.keys(queryAttributs)) { if (key === 'tags') { (_searchQuery['_and'] as IQueryObject[]).push( @@ -634,14 +679,18 @@ export class TheHive implements INodeType { if (operation === 'update') { const alertId = this.getNodeParameter('id', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const customFields = await prepareCustomFields.call(this, updateFields, jsonParameters); const artifactUi = updateFields.artifactUi as IDataObject; delete updateFields.artifactUi; - const body: IDataObject = {}; + const body: IDataObject = { + customFields, + }; Object.assign(body, updateFields); @@ -1149,10 +1198,17 @@ export class TheHive implements INodeType { if (resource === 'case') { if (operation === 'count') { - const countQueryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); // tslint:disable-line:no-any + const filters = this.getNodeParameter('filters', i, {}) as INodeParameters; + const countQueryAttributs: any = prepareOptional(filters); // tslint:disable-line:no-any const _countSearchQuery: IQueryObject = And(); + if ('customFieldsUi' in filters) { + const customFields = await prepareCustomFields.call(this, filters) as IDataObject; + const searchQueries = buildCustomFieldSearch(customFields); + (_countSearchQuery['_and'] as IQueryObject[]).push(...searchQueries); + } + for (const key of Object.keys(countQueryAttributs)) { if (key === 'tags') { (_countSearchQuery['_and'] as IQueryObject[]).push( @@ -1258,6 +1314,9 @@ export class TheHive implements INodeType { } if (operation === 'create') { + const options = this.getNodeParameter('options', i, {}) as INodeParameters; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + const customFields = await prepareCustomFields.call(this, options, jsonParameters); const body: IDataObject = { title: this.getNodeParameter('title', i), @@ -1268,7 +1327,8 @@ export class TheHive implements INodeType { flag: this.getNodeParameter('flag', i), tlp: this.getNodeParameter('tlp', i), tags: splitTags(this.getNodeParameter('tags', i) as string), - ...prepareOptional(this.getNodeParameter('options', i, {}) as INodeParameters), + customFields, + ...prepareOptional(options), }; responseData = await theHiveApiRequest.call( @@ -1333,12 +1393,19 @@ export class TheHive implements INodeType { const version = credentials.apiVersion; - const queryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); // tslint:disable-line:no-any - + const filters = this.getNodeParameter('filters', i, {}) as INodeParameters; + const queryAttributs: any = prepareOptional(filters); // tslint:disable-line:no-any + const _searchQuery: IQueryObject = And(); const options = this.getNodeParameter('options', i) as IDataObject; + if ('customFieldsUi' in filters) { + const customFields = await prepareCustomFields.call(this, filters) as IDataObject; + const searchQueries = buildCustomFieldSearch(customFields); + (_searchQuery['_and'] as IQueryObject[]).push(...searchQueries); + } + for (const key of Object.keys(queryAttributs)) { if (key === 'tags') { (_searchQuery['_and'] as IQueryObject[]).push( @@ -1419,9 +1486,14 @@ export class TheHive implements INodeType { if (operation === 'update') { const id = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i, {}) as INodeParameters; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + const customFields = await prepareCustomFields.call(this, updateFields, jsonParameters); const body: IDataObject = { - ...prepareOptional(this.getNodeParameter('updateFields', i, {}) as INodeParameters), + customFields, + ...prepareOptional(updateFields), }; responseData = await theHiveApiRequest.call( diff --git a/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts index 1c607305eb..d6d72e2361 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts @@ -468,6 +468,24 @@ export const alertFields = [ }, }, }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: true, + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + 'update', + ], + }, + }, + }, + // optional attributs (Create, Promote operations) { displayName: 'Additional Fields', @@ -483,6 +501,89 @@ export const alertFields = [ ], operation: [ 'create', + ], + }, + }, + options: [ + { + displayName: 'Case Template', + name: 'caseTemplate', + type: 'string', + default: '', + description: `Case template to use when a case is created from this alert.`, + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: {}, + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Custom Field', + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'loadCustomFields', + }, + default: 'Custom Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Custom Field value. Use an expression if the type is not a string.', + }, + ], + }, + ], + }, + { + displayName: 'Custom Fields (JSON)', + name: 'customFieldsJson', + type: 'string', + default: '', + displayOptions: { + show: { + '/jsonParameters': [ + true, + ], + }, + }, + description: 'Custom fields in JSON format. Overrides Custom Fields UI if set.', + }, + ], + }, + // optional attributs (Promote operation) + + { + displayName: 'Additional Fields', + name: 'additionalFields', + placeholder: 'Add Field', + type: 'collection', + required: false, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ 'promote', ], }, @@ -581,6 +682,61 @@ export const alertFields = [ }, ], }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + placeholder: 'Add Custom Field', + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'loadCustomFields', + }, + default: 'Custom Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Custom Field value. Use an expression if the type is not a string.', + }, + ], + }, + ], + }, + { + displayName: 'Custom Fields (JSON)', + name: 'customFieldsJson', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + true, + ], + }, + }, + default: '', + description: 'Custom fields in JSON format. Overrides Custom Fields UI if set.', + }, { displayName: 'Case Template', name: 'caseTemplate', @@ -737,6 +893,40 @@ export const alertFields = [ }, }, options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Custom Field', + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'loadCustomFields', + }, + default: 'Custom Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Custom Field value. Use an expression if the type is not a string.', + }, + ], + }, + ], + }, { displayName: 'Description', name: 'description', diff --git a/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts index 1ff1aba4a6..60f7f8f8ca 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts @@ -295,6 +295,23 @@ export const caseFields = [ }, }, }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: true, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + 'update', + ], + }, + }, + }, // Optional fields (Create operation) { displayName: 'Options', @@ -314,6 +331,61 @@ export const caseFields = [ required: false, default: '', options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + placeholder: 'Add Custom Field', + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'loadCustomFields', + }, + default: 'Custom Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Custom Field value. Use an expression if the type is not a string.', + }, + ], + }, + ], + }, + { + displayName: 'Custom Fields (JSON)', + name: 'customFieldsJson', + type: 'string', + default: '', + displayOptions: { + show: { + '/jsonParameters': [ + true, + ], + }, + }, + description: 'Custom fields in JSON format. Overrides Custom Fields UI if set.', + }, { displayName: 'End Date', name: 'endDate', @@ -333,6 +405,13 @@ export const caseFields = [ name: 'metrics', default: '[]', type: 'json', + displayOptions: { + show: { + '/jsonParameters': [ + true, + ], + }, + }, description: 'List of metrics', }, ], @@ -356,6 +435,61 @@ export const caseFields = [ required: false, default: '', options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + placeholder: 'Add Custom Field', + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'loadCustomFields', + }, + default: 'Custom Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Custom Field value. Use an expression if the type is not a string.', + }, + ], + }, + ], + }, + { + displayName: 'Custom Fields (JSON)', + name: 'customFieldsJson', + type: 'string', + default: '', + displayOptions: { + show: { + '/jsonParameters': [ + true, + ], + }, + }, + description: 'Custom fields in JSON format. Overrides Custom Fields UI if set.', + }, { displayName: 'Description', name: 'description', @@ -403,6 +537,13 @@ export const caseFields = [ name: 'metrics', type: 'json', default: '[]', + displayOptions: { + show: { + '/jsonParameters': [ + true, + ], + }, + }, description: 'List of metrics', }, { @@ -583,6 +724,40 @@ export const caseFields = [ }, }, options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Custom Field', + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'loadCustomFields', + }, + default: 'Custom Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Custom Field value. Use an expression if the type is not a string.', + }, + ], + }, + ], + }, { displayName: 'Description', name: 'description',