diff --git a/packages/nodes-base/credentials/CortexApi.credentials.ts b/packages/nodes-base/credentials/CortexApi.credentials.ts new file mode 100644 index 0000000000..eb07506bbf --- /dev/null +++ b/packages/nodes-base/credentials/CortexApi.credentials.ts @@ -0,0 +1,26 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class CortexApi implements ICredentialType { + name = 'cortexApi'; + displayName = 'Cortex API'; + properties = [ + { + displayName: 'API Key', + name: 'cortexApiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Cortex Instance', + name: 'host', + type: 'string' as NodePropertyTypes, + description: 'The URL of the Cortex instance', + default: '', + placeholder:'https://localhost:9001' + }, + ]; +} diff --git a/packages/nodes-base/credentials/TheHiveApi.credentials.ts b/packages/nodes-base/credentials/TheHiveApi.credentials.ts new file mode 100644 index 0000000000..3476ea38bc --- /dev/null +++ b/packages/nodes-base/credentials/TheHiveApi.credentials.ts @@ -0,0 +1,44 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TheHiveApi implements ICredentialType { + name = 'theHiveApi'; + displayName = 'The Hive API'; + properties = [ + { + displayName: 'API Key', + name: 'ApiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'URL', + name: 'url', + default: '', + type: 'string' as NodePropertyTypes, + description: 'The URL of TheHive instance', + placeholder: 'https://localhost:9000', + }, + { + displayName: 'API Version', + name: 'apiVersion', + default: '', + type: 'options' as NodePropertyTypes, + description: 'The version of api to be used', + options:[ + { + name:'Version 1', + value:'v1', + description:'API version supported by TheHive 4' + }, + { + name:'Version 0', + value:'', + description:'API version supported by TheHive 3' + }, + ], + }, + ]; +} diff --git a/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts b/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts new file mode 100644 index 0000000000..318e5b7fa0 --- /dev/null +++ b/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts @@ -0,0 +1,210 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + TLP, +}from './AnalyzerInterface'; + +export const analyzersOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + description: 'Choose an operation', + displayOptions: { + show: { + resource: [ + 'analyzer', + ], + }, + }, + default: 'execute', + options: [ + { + name: 'Execute', + value: 'execute', + description: 'Execute Analyzer', + }, + ], + }, +] as INodeProperties[]; + +export const analyzerFields: INodeProperties[] =[ + { + displayName: 'Analyzer Type', + name: 'analyzer', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'loadActiveAnalyzers', + }, + displayOptions:{ + show: { + resource: [ + 'analyzer', + ], + operation:[ + 'execute', + ], + }, + }, + description: 'Choose the analyzer', + default: '', + }, + { + displayName: 'Observable Type', + name: 'observableType', + type: 'options', + required: true, + displayOptions:{ + show: { + resource: [ + 'analyzer', + ], + operation:[ + 'execute', + ], + }, + hide:{ + analyzer:[ + '', + ], + }, + }, + typeOptions:{ + loadOptionsMethod: 'loadObservableOptions', + loadOptionsDependsOn: [ + 'analyzer', + ], + }, + default: '', + description: 'Choose the observable type', + }, + + // Observable type != file + { + displayName: 'Observable Value', + name: 'observableValue', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'analyzer', + ], + operation:[ + 'execute', + ], + }, + hide:{ + observableType: [ + 'file', + ], + analyzer:[ + '', + ], + }, + }, + default: '', + description: 'Enter the observable value', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + observableType: [ + 'file', + ], + resource: [ + 'analyzer', + ], + operation: [ + 'execute', + ], + }, + }, + description: 'Name of the binary property to which to
write the data of the read file.', + }, + { + displayName: 'TLP', + name: 'tlp', + type: 'options', + required: false, + displayOptions:{ + show: { + resource: [ + 'analyzer', + ], + operation: [ + 'execute', + ], + }, + hide:{ + observableType: [ + '', + ], + analyzer: [ + '', + ], + }, + }, + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + },{ + name: 'Red', + value: TLP.red, + } + ], + default: 2, + description: 'The TLP of the analyzed observable', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'analyzer', + ], + operation: [ + 'execute', + ], + }, + }, + options: [ + { + displayName: 'Force', + name: 'force', + type: 'boolean', + default: false, + description: 'To force bypassing the cache, set this parameter to true', + }, + { + displayName: 'Timeout (seconds)', + name: 'timeout', + type: 'number', + default: 3, + description: 'Timeout to wait for the report in case it is not available at the time the query was made', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts b/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts new file mode 100644 index 0000000000..608fef6ed6 --- /dev/null +++ b/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts @@ -0,0 +1,86 @@ +import { + IDataObject, +}from 'n8n-workflow'; + +export enum JobStatus { + WAITING = 'Waiting', + INPROGRESS = 'InProgress', + SUCCESS = 'Success', + FAILURE = 'Failure', + DELETED = 'Deleted' +} + +export enum TLP { + white, + green, + amber, + red +} + +export enum ObservableDataType { + 'domain'= 'domain', + 'file'= 'file', + 'filename'= 'filename', + 'fqdn'= 'fqdn', + 'hash'= 'hash', + 'ip'= 'ip', + 'mail'= 'mail', + 'mail_subject'= 'mail_subject', + 'other'= 'other', + 'regexp'= 'regexp', + 'registry'= 'registry', + 'uri_path'= 'uri_path', + 'url'= 'url', + 'user-agent'= 'user-agent' +} +export interface IJob{ + id?: string; + organization?: string; + analyzerDefinitionId?: string; + analyzerId?: string; + analyzerName?: string; + dataType?: ObservableDataType; + status?: JobStatus; + data?: string; + attachment?: IDataObject; + parameters?: IDataObject; + message? :string; + tlp?: TLP; + startDate?: Date; + endDate?: Date; + createdAt?: Date; + createdBy?: string; + updatedAt?: Date; + updatedBy?: Date; + report?: IDataObject | string; +} +export interface IAnalyzer{ + id?: string; + analyzerDefinitionId?: string; + name? :string; + version?: string; + description?: string; + author?: string; + url?: string; + license?: string; + dataTypeList?: ObservableDataType[]; + baseConfig?: string; + jobCache?: number; + rate?: number; + rateUnit?: string; + configuration?: IDataObject; + createdBy?: string; + updatedAt?: Date; + updatedBy?: Date; +} + +export interface IResponder{ + id?: string; + name?: string; + version?: string; + description?: string; + dataTypeList?: string[]; + maxTlp?: number; + maxPap?: number; + cortexIds?: string[] | undefined; +} diff --git a/packages/nodes-base/nodes/Cortex/Cortex.node.ts b/packages/nodes-base/nodes/Cortex/Cortex.node.ts new file mode 100644 index 0000000000..97eb108ab2 --- /dev/null +++ b/packages/nodes-base/nodes/Cortex/Cortex.node.ts @@ -0,0 +1,469 @@ +import { + IExecuteFunctions, + BINARY_ENCODING, +} from 'n8n-core'; + +import { + cortexApiRequest, + getEntityLabel, + prepareParameters, + splitTags, +} from './GenericFunctions'; + +import { + analyzersOperations, + analyzerFields, +} from './AnalyzerDescriptions'; + +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodePropertyOptions, + ILoadOptionsFunctions, + IDataObject, + IBinaryData, +} from 'n8n-workflow'; + +import { + respondersOperations, + responderFields, +} from './ResponderDescription'; + +import { + jobFields, + jobOperations, +} from './JobDescription'; + +import { + upperFirst, +} from 'lodash'; + +import { + IJob, +} from './AnalyzerInterface'; + +import { + createHash, +} from 'crypto'; + +import * as changeCase from 'change-case'; + +export class Cortex implements INodeType { + description: INodeTypeDescription = { + displayName: 'Cortex', + name: 'cortex', + icon: 'file:cortex.png', + group: ['transform'], + subtitle: '={{$parameter["resource"]+ ": " + $parameter["operation"]}}', + version: 1, + description: 'Apply the Cortex analyzer/responder on the given entity', + defaults: { + name: 'Cortex', + color: '#54c4c3', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'cortexApi', + required: true, + }, + ], + properties: [ + // Node properties which the user gets displayed and + // can change on the node. + { + displayName:'Resource', + name:'resource', + type:'options', + options:[ + { + name: 'Analyzer', + value:'analyzer', + }, + { + name: 'Responder', + value:'responder', + }, + { + name: 'Job', + value:'job', + }, + ], + default: 'analyzer', + description: 'Choose a resource', + required: true, + }, + ...analyzersOperations, + ...analyzerFields, + ...respondersOperations, + ...responderFields, + ...jobOperations, + ...jobFields + ], + }; + + methods = { + loadOptions: { + + async loadActiveAnalyzers(this: ILoadOptionsFunctions): Promise { + // request the enabled analyzers from instance + const requestResult = await cortexApiRequest.call( + this, + 'POST', + `/analyzer/_search`, + ); + + const returnData: INodePropertyOptions[] = []; + + for (const analyzer of requestResult) { + returnData.push({ + name: analyzer.name as string, + value: `${analyzer.id as string}::${analyzer.name as string}`, + description: analyzer.description as string, + }); + } + + return returnData; + }, + + async loadActiveResponders(this: ILoadOptionsFunctions): Promise { + // request the enabled responders from instance + const requestResult = await cortexApiRequest.call( + this, + 'GET', + `/responder`, + ); + + const returnData: INodePropertyOptions[] = []; + for (const responder of requestResult) { + returnData.push({ + name: responder.name as string, + value: `${responder.id as string}::${responder.name as string}`, + description: responder.description as string, + }); + } + return returnData; + }, + + async loadObservableOptions(this: ILoadOptionsFunctions): Promise { + const selectedAnalyzerId = (this.getNodeParameter('analyzer') as string).split('::')[0]; + // request the analyzers from instance + const requestResult = await cortexApiRequest.call( + this, + 'GET', + `/analyzer/${selectedAnalyzerId}`, + ); + + // parse supported observable types into options + const returnData: INodePropertyOptions[] = []; + for (const dataType of requestResult.dataTypeList) { + returnData.push( + { + name: upperFirst(dataType as string), + value: dataType as string, + }, + ); + } + return returnData; + }, + + async loadDataTypeOptions(this: ILoadOptionsFunctions): Promise { + const selectedResponderId = (this.getNodeParameter('responder') as string).split('::')[0]; + // request the responder from instance + const requestResult = await cortexApiRequest.call( + this, + 'GET', + `/responder/${selectedResponderId}`, + ); + // parse the accepted dataType into options + const returnData: INodePropertyOptions[] = []; + for (const dataType of requestResult.dataTypeList) { + returnData.push( + { + value: (dataType as string).split(':')[1], + name: changeCase.capitalCase((dataType as string).split(':')[1]) + }, + ); + } + return returnData; + }, + + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + if (resource === 'analyzer') { + //https://github.com/TheHive-Project/CortexDocs/blob/master/api/api-guide.md#run + if (operation === 'execute') { + + let force = false; + + const analyzer = this.getNodeParameter('analyzer', i) as string; + + const observableType = this.getNodeParameter('observableType', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const tlp = this.getNodeParameter('tlp', i) as string; + + const body: IDataObject = { + dataType: observableType, + tlp, + }; + + if (additionalFields.force === true) { + force = true; + } + + if (observableType === 'file') { + + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const fileBufferData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); + + const options = { + formData: { + data: { + value: fileBufferData, + options: { + contentType: item.binary[binaryPropertyName].mimeType, + filename: item.binary[binaryPropertyName].fileName, + } + }, + _json: JSON.stringify({ + dataType: observableType, + tlp, + }) + } + }; + + responseData = await cortexApiRequest.call( + this, + 'POST', + `/analyzer/${analyzer.split('::')[0]}/run`, + {}, + { force }, + '', + options, + ) as IJob; + + continue; + + } else { + const observableValue = this.getNodeParameter('observableValue', i) as string; + + body.data = observableValue; + + responseData = await cortexApiRequest.call( + this, + 'POST', + `/analyzer/${analyzer.split('::')[0]}/run`, + body, + { force }, + ) as IJob; + } + + if (additionalFields.timeout) { + responseData = await cortexApiRequest.call( + this, + 'GET', + `/job/${responseData.id}/waitreport`, + {}, + { atMost: `${additionalFields.timeout}second` }, + ); + } + } + } + + if (resource === 'job') { + //https://github.com/TheHive-Project/CortexDocs/blob/master/api/api-guide.md#get-details-1 + if (operation === 'get') { + + const jobId = this.getNodeParameter('jobId', i) as string; + + responseData = await cortexApiRequest.call( + this, + 'GET', + `/job/${jobId}`, + ); + } + //https://github.com/TheHive-Project/CortexDocs/blob/master/api/api-guide.md#get-details-and-report + if (operation === 'report') { + + const jobId = this.getNodeParameter('jobId', i) as string; + + responseData = await cortexApiRequest.call( + this, + 'GET', + `/job/${jobId}/report`, + ); + } + } + + if (resource === 'responder') { + if (operation === 'execute') { + const responderId = (this.getNodeParameter('responder', i) as string).split('::')[0]; + + const entityType = this.getNodeParameter('entityType', i) as string; + + const isJSON = this.getNodeParameter('jsonObject',i) as boolean; + let body:IDataObject; + + + if(isJSON){ + + + const entityJson = JSON.parse(this.getNodeParameter('objectData', i) as string); + + body = { + responderId, + label: getEntityLabel(entityJson), + dataType: `thehive:${entityType}`, + data: entityJson, + tlp: entityJson.tlp || 2, + pap: entityJson.pap || 2, + message: entityJson.message || '', + parameters:[], + }; + + }else{ + + const values = (this.getNodeParameter('parameters',i) as IDataObject).values as IDataObject; + + body= { + responderId, + dataType: `thehive:${entityType}`, + data: { + _type: entityType, + ...prepareParameters(values) + } + }; + if( entityType === 'alert'){ + // deal with alert artifacts + const artifacts = (body.data as IDataObject).artifacts as IDataObject; + + if (artifacts) { + + const artifactValues = (artifacts as IDataObject).artifactValues as IDataObject[]; + + if (artifactValues) { + + const artifactData = []; + + for (const artifactvalue of artifactValues) { + + const element: IDataObject = {}; + + element.message = artifactvalue.message as string; + + element.tags = splitTags(artifactvalue.tags as string) as string[]; + + element.dataType = artifactvalue.dataType as string; + + element.data = artifactvalue.data as string; + + if (artifactvalue.dataType === 'file') { + + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = artifactvalue.binaryProperty as string; + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property '${binaryPropertyName}' does not exists on item!`); + } + + const binaryData = item.binary[binaryPropertyName] as IBinaryData; + + element.data = `${binaryData.fileName};${binaryData.mimeType};${binaryData.data}`; + } + + artifactData.push(element); + } + + (body.data as IDataObject).artifacts = artifactData; + } + } + } + if(entityType ==='case_artifact'){ + // deal with file observable + + if ((body.data as IDataObject).dataType === 'file') { + + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = (body.data as IDataObject).binaryPropertyName as string; + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const fileBufferData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); + const sha256 = createHash('sha256').update(fileBufferData).digest('hex'); + + (body.data as IDataObject).attachment = { + name: item.binary[binaryPropertyName].fileName, + hashes: [ + sha256, + createHash('sha1').update(fileBufferData).digest('hex'), + createHash('md5').update(fileBufferData).digest('hex') + ], + size:fileBufferData.byteLength, + contentType: item.binary[binaryPropertyName].mimeType, + id:sha256, + }; + + delete (body.data as IDataObject).binaryPropertyName; + } + } + // add the job label after getting all entity attributes + body = { + label: getEntityLabel(body.data as IDataObject), + ...body + }; + + } + responseData = await cortexApiRequest.call( + this, + 'POST', + `/responder/${responderId}/run`, + body, + ) as IJob; + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Cortex/GenericFunctions.ts b/packages/nodes-base/nodes/Cortex/GenericFunctions.ts new file mode 100644 index 0000000000..0c301ff3dd --- /dev/null +++ b/packages/nodes-base/nodes/Cortex/GenericFunctions.ts @@ -0,0 +1,109 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IAnalyzer, + IJob, + IResponder, +} from './AnalyzerInterface'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import * as moment from 'moment'; + +export async function cortexApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('cortexApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const headerWithAuthentication = Object.assign({}, { Authorization: ` Bearer ${credentials.cortexApiKey}`}); + + let options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `${credentials.host}/api${resource}`, + body, + 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; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.error ) { + const errorMessage = `Cortex error response [${error.statusCode}]: ${error.error.message}`; + throw new Error(errorMessage); + } else throw error; + } +} + +export function getEntityLabel(entity: IDataObject): string{ + let label = ''; + switch (entity._type) { + case 'case': + label = `#${entity.caseId} ${entity.title}`; + break; + case 'case_artifact': + //@ts-ignore + label = `[${entity.dataType}] ${entity.data?entity.data:(entity.attachment.name)}`; + break; + case 'alert': + label = `[${entity.source}:${entity.sourceRef}] ${entity.title}`; + break; + case 'case_task_log': + label = `${entity.message} from ${entity.createdBy}`; + break; + case 'case_task': + label = `${entity.title} (${entity.status})`; + break; + case 'job': + label = `${entity.analyzerName} (${entity.status})`; + break; + default: + break; + } + return label; +} + +export function splitTags(tags: string): string[] { + return tags.split(',').filter(tag => tag !== ' ' && tag); +} + +export function prepareParameters(values: IDataObject): IDataObject { + const response: IDataObject = {}; + for (const key in values) { + if (values[key]!== undefined && values[key]!==null && values[key]!=='') { + if (moment(values[key] as string, moment.ISO_8601).isValid()) { + response[key] = Date.parse(values[key] as string); + } else if (key === 'tags') { + response[key] = splitTags(values[key] as string); + } else { + response[key] = values[key]; + } + } + } + return response; +} diff --git a/packages/nodes-base/nodes/Cortex/JobDescription.ts b/packages/nodes-base/nodes/Cortex/JobDescription.ts new file mode 100644 index 0000000000..1db9f41f69 --- /dev/null +++ b/packages/nodes-base/nodes/Cortex/JobDescription.ts @@ -0,0 +1,55 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const jobOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description:'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'job', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get job details', + }, + { + name: 'Report', + value: 'report', + description: 'Get job report', + }, + ], + default: 'get', + }, +] as INodeProperties[]; + +export const jobFields: INodeProperties[] =[ + { + displayName: 'Job ID', + name: 'jobId', + type: 'string', + required: true, + displayOptions:{ + show: { + resource: [ + 'job', + ], + operation: [ + 'get', + 'report', + ], + }, + }, + default:'', + description: 'ID of the job', + }, +]; diff --git a/packages/nodes-base/nodes/Cortex/ResponderDescription.ts b/packages/nodes-base/nodes/Cortex/ResponderDescription.ts new file mode 100644 index 0000000000..14362a61e4 --- /dev/null +++ b/packages/nodes-base/nodes/Cortex/ResponderDescription.ts @@ -0,0 +1,892 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + TLP, +} from './AnalyzerInterface'; + +export const respondersOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + description: 'Choose an operation', + displayOptions: { + show: { + resource: [ + 'responder', + ], + }, + }, + options: [ + { + name: 'Execute', + value: 'execute', + description: 'Execute Responder' + } + ], + default: 'execute' + } +] as INodeProperties[]; + +export const responderFields: INodeProperties[] = [ + { + displayName: 'Responder Type', + name: 'responder', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'loadActiveResponders' + }, + default: '', + displayOptions: { + show: { + resource: [ + 'responder', + ], + }, + }, + description: 'Choose the responder' + }, + { + displayName: 'Entity Type', + name: 'entityType', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'responder', + ] + }, + }, + typeOptions: { + loadOptionsMethod: 'loadDataTypeOptions', + loadOptionsDependsOn: [ + 'responder', + ], + }, + default: '', + description: 'Choose the Data type', + }, + { + displayName: 'JSON Parameters', + name: 'jsonObject', + type: 'boolean', + default: false, + description: 'Choose between providing JSON object or seperated attributes', + displayOptions: { + show: { + resource: [ + 'responder', + ], + }, + }, + }, + { + displayName: 'Entity Object (JSON)', + name: 'objectData', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'responder', + ], + jsonObject: [ + true, + ], + }, + }, + default: '' + }, + { + displayName: 'Parameters', + name: 'parameters', + type: 'fixedCollection', + placeholder: 'Add Parameter', + required: false, + options: [ + { + displayName: 'Case Attributes', + name: 'values', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the case', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the case', + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + default: 2, + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + description: 'Severity of the case. Default=Medium', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + description: `User who owns the case. This is automatically set to current user when status is set to InProgress`, + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + default: false, + description: 'Flag of the case default=false', + }, + { + displayName: '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, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + placeholder:'tag1,tag2,...', + }, + ], + }, + ], + typeOptions:{ + loadOptionsDependsOn:[ + 'entityType', + ], + }, + displayOptions: { + show: { + resource: [ + 'responder', + ], + jsonObject: [ + false, + ], + entityType: [ + 'case', + ], + }, + hide: { + entityType: [ + '', + 'alert', + 'case_artifact', + 'case_task', + 'case_task_log', + ], + }, + }, + default: {} + }, + { + displayName: 'Parameters', + name: 'parameters', + type: 'fixedCollection', + placeholder: 'Add Parameter', + required: false, + options: [ + { + displayName: 'Alert Attributes', + name: 'values', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the alert', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the alert', + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + default: 2, + options:[ + { + name: 'Low', + value: 1 + }, + { + name: 'Medium', + value: 2 + }, + { + name: 'High', + value: 3, + }, + ], + description: 'Severity of the case. Default=Medium', + }, + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + description: 'Date and time when the alert was raised default=now', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + placeholder:'tag1,tag2,...', + default: '' + }, + { + displayName: '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, + } + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'New', + options:[ + { + name: 'New', + value: 'New', + }, + { + name: 'Updated', + value: 'Updated', + }, + { + name: 'Ignored', + value: 'Ignored' + }, + { + name: 'Imported', + value: 'Imported', + }, + ], + description: 'Status of the alert. Default=New' + }, + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + description: 'Type of the alert', + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + description: 'Source of the alert', + }, + { + displayName: 'SourceRef', + name: 'sourceRef', + type: 'string', + default: '', + description: 'Source reference of the alert', + }, + { + displayName: 'Follow', + name: 'follow', + type: 'boolean', + default: false + }, + { + displayName: 'Artifacts', + name: 'artifacts', + type: 'fixedCollection', + placeholder:'Add an artifact', + required: false, + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add an Artifact', + }, + default: [], + options: [ + { + displayName: 'Artifact', + name: 'artifactValues', + values: [ + { + displayName: 'Data Type', + name: 'dataType', + type: 'options', + default: '', + options: [ + { + name: 'Domain', + value: 'domain', + }, + { + name: 'File', + value: 'file' + }, + { + name: 'Filename', + value: 'filename' + }, + { + name: 'Fqdn', + value: 'fqdn' + }, + { + name: 'Hash', + value: 'hash' + }, + { + name: 'IP', + value: 'ip' + }, + { + name: 'Mail', + value: 'mail' + }, + { + name: 'Mail Subject', + value: 'mail_subject' + }, + { + name: 'Other', + value: 'other' + }, + { + name: 'Regexp', + value: 'regexp' + }, + { + name: 'Registry', + value: 'registry' + }, + { + name: 'Uri Path', + value: 'uri_path' + }, + { + name: 'URL', + value: 'url' + }, + { + name: 'User Agent', + value: 'user-agent' + }, + ], + description: '', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + displayOptions: { + hide: { + dataType: [ + 'file', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + displayOptions: { + show: { + dataType: [ + 'file', + ], + }, + }, + default: 'data', + description: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: '', + }, + ], + } + ] + }, + ] + } + ], + typeOptions:{ + loadOptionsDependsOn:[ + 'entityType', + ], + }, + displayOptions: { + show: { + resource: [ + 'responder', + ], + jsonObject: [ + false, + ], + entityType: [ + 'alert', + ], + }, + hide: { + responder: [ + '', + ], + entityType: [ + '', + 'case', + 'case_artifact', + 'case_task', + 'case_task_log', + ], + }, + }, + default: {}, + }, + { + displayName: 'Parameters', + name: 'parameters', + type: 'fixedCollection', + placeholder: 'Add Parameter', + required: false, + options: [ + { + displayName: 'Observable Attributes', + name: 'values', + values: [ + { + displayName: 'DataType', + name: 'dataType', + type: 'options', + default: '', + options: [ + { + name: 'Domain', + value: 'domain', + }, + { + name: 'File', + value: 'file' + }, + { + name: 'Filename', + value: 'filename' + }, + { + name: 'Fqdn', + value: 'fqdn' + }, + { + name: 'Hash', + value: 'hash' + }, + { + name: 'IP', + value: 'ip' + }, + { + name: 'Mail', + value: 'mail' + }, + { + name: 'Mail Subject', + value: 'mail_subject' + }, + { + name: 'Other', + value: 'other' + }, + { + name: 'Regexp', + value: 'regexp' + }, + { + name: 'Registry', + value: 'registry' + }, + { + name: 'Uri Path', + value: 'uri_path' + }, + { + name: 'URL', + value: 'url' + }, + { + name: 'User Agent', + value: 'user-agent' + }, + ], + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + default: '', + displayOptions:{ + hide:{ + dataType:[ + 'file', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { + show: { + dataType:[ + 'file', + ], + }, + }, + description: 'Name of the binary property which contains the attachement data', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '' + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: '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, + } + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + { + displayName: 'IOC', + name: 'ioc', + type: 'boolean', + default: false, + description: 'Indicates if the observable is an IOC (Indicator of compromise)', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + description: 'Status of the observable (Ok or Deleted) default=Ok', + } + ], + }, + ], + typeOptions:{ + loadOptionsDependsOn:[ + 'entityType', + ], + }, + displayOptions: { + show: { + resource: [ + 'responder', + ], + jsonObject: [ + false, + ], + entityType: [ + 'case_artifact', + ], + }, + hide: { + responder: [ + '', + ], + entityType: [ + '', + 'case', + 'alert', + 'case_task', + 'case_task_log', + ], + }, + }, + default: {}, + }, + { + displayName: 'Parameters', + name: 'parameters', + type: 'fixedCollection', + placeholder: 'Add Parameter', + required: false, + options: [ + { + displayName: 'Task Attributes', + name: 'values', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + required: false, + default: '', + description: 'Title of the task', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'Waiting', + options: [ + { + name: 'Waiting', + value: 'Waiting', + }, + { + name: 'InProgress', + value: 'InProgress', + }, + { + name: 'Completed', + value: 'Completed', + }, + { + name: 'Cancel', + value: 'Cancel', + }, + ], + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + default: false + } + ] + } + ], + typeOptions:{ + loadOptionsDependsOn:[ + 'entityType', + ], + }, + displayOptions: { + show: { + resource: [ + 'responder', + ], + jsonObject: [ + false, + ], + entityType: [ + 'case_task', + ], + }, + hide: { + responder: [ + '', + ], + entityType: [ + '', + 'case', + 'alert', + 'case_artifact', + 'case_task_log', + ], + }, + }, + default: {}, + }, + { + displayName: 'Parameters', + name: 'parameters', + type: 'fixedCollection', + placeholder: 'Add Parameter', + required: false, + options: [ + { + displayName: 'Log Attributes', + name: 'values', + values: [ + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '' + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + }, + ], + }, + ], + typeOptions:{ + loadOptionsDependsOn:[ + 'entityType', + ], + }, + displayOptions: { + show: { + resource: [ + 'responder', + ], + jsonObject: [ + false, + ], + entityType: [ + 'case_task_log', + ], + }, + hide: { + responder: [ + '', + ], + entityType: [ + '', + 'case', + 'alert', + 'case_artifact', + 'case_task', + ], + }, + }, + default: {}, + }, +]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Cortex/cortex.png b/packages/nodes-base/nodes/Cortex/cortex.png new file mode 100644 index 0000000000..b750755749 Binary files /dev/null and b/packages/nodes-base/nodes/Cortex/cortex.png differ diff --git a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts new file mode 100644 index 0000000000..8c260b0587 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts @@ -0,0 +1,123 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import * as moment from 'moment'; + +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'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const headerWithAuthentication = Object.assign({}, { Authorization: `Bearer ${credentials.ApiKey}` }); + + let options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `${credentials.url}/api${resource}`, + body, + 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; + } + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.error ) { + const errorMessage = `TheHive error response [${error.statusCode}]: ${error.error.message || error.error.type}`; + throw new Error(errorMessage); + } else throw error; + } +} + +// Helpers functions +export function mapResource(resource: string): string { + switch (resource) { + case 'alert': + return 'alert'; + case 'case': + return 'case'; + case 'observable': + return 'case_artifact'; + case 'task': + return 'case_task'; + case 'log': + return 'case_task_log'; + default: + return ''; + } +} + +export function splitTags(tags: string): string[] { + return tags.split(',').filter(tag => tag !== ' ' && tag); +} + +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()) { + response[key] = Date.parse(optionals[key] as string); + } else if (key === 'artifacts') { + response[key] = JSON.parse(optionals[key] as string); + } else if (key === 'tags') { + response[key] = splitTags(optionals[key] as string); + } else { + response[key] = optionals[key]; + } + } + } + return response; +} + +export function prepareSortQuery(sort: string, body: { query: [IDataObject] }) { + if (sort) { + const field = sort.substring(1); + const value = sort.charAt(0) === '+' ? 'asc' : 'desc'; + const sortOption: IDataObject = {}; + sortOption[field] = value; + body.query.push( + { + '_name': 'sort', + '_fields': [ + sortOption, + ], + }, + ); + } +} + +export function prepareRangeQuery(range: string, body: { 'query': Array<{}> }) { + if (range && range !== 'all') { + body['query'].push( + { + '_name': 'page', + 'from': parseInt(range.split('-')[0], 10), + 'to': parseInt(range.split('-')[1], 10) + } + ); + } +} diff --git a/packages/nodes-base/nodes/TheHive/QueryFunctions.ts b/packages/nodes-base/nodes/TheHive/QueryFunctions.ts new file mode 100644 index 0000000000..cd5b984b8a --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/QueryFunctions.ts @@ -0,0 +1,82 @@ +// Query types +export declare type queryIndexSignature = '_field'|'_gt'|'_value'|'_gte'|'_lt'|'_lte'|'_and'|'_or'|'_not'|'_in'|'_contains'|'_id'|'_between'|'_parent'|'_parent'|'_child'|'_type'|'_string'|'_like'|'_wildcard'; +export type IQueryObject = { + [key in queryIndexSignature]?: IQueryObject|IQueryObject[]|string|number|object +}; + +// Query Functions +export function Eq(field: string, value: any):IQueryObject{ + return { '_field': field, '_value': value }; +} +export function Gt(field: string, value: any):IQueryObject{ + return { '_gt': { field: value } }; +} +export function Gte(field: string, value: any):IQueryObject{ + return { '_gte': { field: value } }; +} +export function Lt(field: string, value: any):IQueryObject{ + return { '_lt': { field: value } }; +} +export function Lte(field: string, value: any):IQueryObject{ + return { '_lte': { field: value } }; +} +export function And(...criteria: IQueryObject[]): IQueryObject{ + return { '_and': criteria }; +} +export function Or(...criteria: IQueryObject[]): IQueryObject{ + return { '_or': criteria }; +} +export function Not(criteria: IQueryObject[]): IQueryObject{ + return { '_not': criteria }; +} +export function In(field: string, values: any[]): IQueryObject{ + return { '_in': { '_field': field, '_values': values } }; +} +export function Contains(field: string): IQueryObject{ + return { '_contains': field }; +} +export function Id(id: string|number): IQueryObject{ + return {'_id': id }; +} +export function Between(field:string, from_value: any, to_value: any): IQueryObject{ + return {'_between': {'_field': field, '_from': from_value, '_to': to_value } }; +} +export function ParentId(tpe:string, id:string):IQueryObject{ + return { '_parent': {'_type': tpe, '_id': id } }; +} +export function Parent(tpe:string, criterion:IQueryObject):IQueryObject{ + return { '_parent': {'_type': tpe, '_query': criterion } }; +} +export function Child(tpe:string, criterion:IQueryObject):IQueryObject{ + return { '_child': {'_type': tpe, '_query': criterion } }; +} +export function Type(tpe:string):IQueryObject{ + return { '_type': tpe }; +} +export function queryString(query_string:string):IQueryObject{ + return { '_string': query_string }; +} +export function Like(field:string, value:string):IQueryObject{ + return { '_like': { '_field': field, '_value': value } }; +} +export function StartsWith(field:string, value:string){ + if (!value.startsWith('*')){ + value = value + '*'; + } + return { '_wildcard': { '_field': field, '_value': value } }; +} +export function EndsWith(field:string, value:string){ + if (!value.endsWith('*')){ + value = '*' + value; + } + return { '_wildcard': { '_field': field, '_value': value } }; +} +export function ContainsString(field:string, value:string){ + if (!value.endsWith('*')){ + value = value + '*'; + } + if (!value.startsWith('*')){ + value = '*' + value; + } + return { '_wildcard': { '_field': field, '_value': value } }; +} diff --git a/packages/nodes-base/nodes/TheHive/TheHive.node.ts b/packages/nodes-base/nodes/TheHive/TheHive.node.ts new file mode 100644 index 0000000000..2e25e23811 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/TheHive.node.ts @@ -0,0 +1,1971 @@ +import { + IExecuteFunctions, + BINARY_ENCODING +} from 'n8n-core'; + +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + IDataObject, + INodeParameters, + ILoadOptionsFunctions, + INodePropertyOptions, + IBinaryData +} from 'n8n-workflow'; + +import { + alertOperations, + alertFields, +} from './descriptions/AlertDescription'; + +import { + observableOperations, + observableFields, +} from './descriptions/ObservableDescription'; + +import { + caseOperations, + caseFields, +} from './descriptions/CaseDescription'; + +import { + taskOperations, + taskFields, +} from './descriptions/TaskDescription'; + +import { + logOperations, + logFields, +} from './descriptions/LogDescription'; + +import { + Buffer, +} from 'buffer'; + +import { + IQueryObject, + Parent, + Id, + Eq, + And, + Between, + In, + ContainsString, +} from './QueryFunctions'; + +import { + theHiveApiRequest, + mapResource, + splitTags, + prepareOptional, + prepareSortQuery, + prepareRangeQuery, +} from './GenericFunctions'; + +export class TheHive implements INodeType { + description: INodeTypeDescription = { + displayName: 'TheHive', + name: 'theHive', + icon: 'file:thehive.png', + group: ['transform'], + subtitle: '={{$parameter["operation"]}} : {{$parameter["resource"]}}', + version: 1, + description: 'Consume TheHive APIs', + defaults: { + name: 'TheHive', + color: '#f3d02f', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'theHiveApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + required: true, + options: [ + { + name: 'Alert', + value: 'alert', + }, + { + name: 'Case', + value: 'case', + }, + { + name: 'Log', + value: 'log', + }, + { + name: 'Observable', + value: 'observable', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'alert', + }, + // Alert + ...alertOperations, + ...alertFields, + // Observable + ...observableOperations, + ...observableFields, + // Case + ...caseOperations, + ...caseFields, + // Task + ...taskOperations, + ...taskFields, + // Log + ...logOperations, + ...logFields, + ], + }; + methods = { + loadOptions: { + async loadResponders(this: ILoadOptionsFunctions): Promise { + // request the analyzers from instance + const resource = mapResource(this.getNodeParameter('resource') as string); + const resourceId = this.getNodeParameter('id'); + const endpoint = `/connector/cortex/responder/${resource}/${resourceId}`; + + const responders = await theHiveApiRequest.call( + this, + 'GET', + endpoint as string, + ); + + 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; + }, + + async loadAnalyzers(this: ILoadOptionsFunctions): Promise { + // request the analyzers from instance + const dataType = this.getNodeParameter('dataType') as string; + const endpoint = `/connector/cortex/analyzer/type/${dataType}`; + const requestResult = await theHiveApiRequest.call( + this, + 'GET', + endpoint as string + ); + const returnData: INodePropertyOptions[] = []; + + 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; + }, + async loadObservableOptions(this: ILoadOptionsFunctions): Promise { + // if v1 is not used we remove 'count' option + const version = this.getCredentials('theHiveApi')?.apiVersion; + + const options= [ + ...(version==='v1')?[{name:'Count',value:'count',description:'Count observables'}]:[], + {name:'Create',value:'create',description:'Create observable'}, + {name:'Execute Analyzer',value:'executeAnalyzer',description:'Execute an responder on selected observable'}, + {name:'Execute Responder',value:'executeResponder',description:'Execute a responder on selected observable'}, + {name:'Get All',value:'getAll',description:'Get all observables of a specific case'}, + {name:'Get', value: 'get', description: 'Get a single observable' }, + {name:'Search',value:'search',description:'Search observables'}, + {name:'Update',value:'update',description:'Update observable'}, + ]; + return options; + }, + async loadTaskOptions(this:ILoadOptionsFunctions): Promise{ + const version = this.getCredentials('theHiveApi')?.apiVersion; + const options =[ + ...(version==='v1')?[{name:'Count',value:'count',description:'Count tasks'}]:[], + {name:'Create',value:'create',description:'Create a task'}, + {name:'Execute Responder', value: 'executeResponder', description: 'Execute a responder on the specified task' }, + {name:'Get All',value:'getAll',description:'Get all asks of a specific case'}, + {name:'Get', value: 'get', description: 'Get a single task' }, + {name:'Search',value:'search',description:'Search tasks'}, + {name:'Update',value:'update',description:'Update a task'}, + ]; + return options; + }, + async loadAlertOptions(this:ILoadOptionsFunctions):Promise{ + const version = this.getCredentials('theHiveApi')?.apiVersion; + const options = [ + ...(version ==='v1')?[{ name: 'Count', value: 'count', description: 'Count alerts' }]:[], + { name: 'Create', value: 'create', description: 'Create alert' }, + { name: 'Execute Responder', value: 'executeResponder', description: 'Execute a responder on the specified alert' }, + { name: 'Get', value: 'get', description: 'Get an alert' }, + { name: 'Get All', value: 'getAll', description: 'Get all alerts' }, + { name: 'Merge', value: 'merge', description: 'Merge alert into an existing case' }, + { name: 'Promote', value: 'promote', description: 'Promote an alert into a case' }, + { name: 'Update', value: 'update', description: 'Update alert' }, + ]; + return options; + }, + async loadCaseOptions(this:ILoadOptionsFunctions):Promise{ + const version = this.getCredentials('theHiveApi')?.apiVersion; + const options=[ + ...(version ==='v1')?[{ name: 'Count', value: 'count', description: 'Count a case' }]:[], + { name: 'Create', value: 'create', description: 'Create a case' }, + { name: 'Execute Responder', value: 'executeResponder', description: 'Execute a responder on the specified case' }, + { name: 'Get All', value: 'getAll', description: 'Get all cases' }, + { name: 'Get', value: 'get', description: 'Get a single case' }, + { name: 'Update', value: 'update', description: 'Update a case' }, + ]; + return options; + } + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + if (resource === 'alert') { + if (operation === 'count') { + const countQueryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _countSearchQuery: IQueryObject = And(); + + for (const key of Object.keys(countQueryAttributs)) { + if ( key === 'tags') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + In(key, countQueryAttributs[key] as string[]) + ); + } else if (key === 'description' || key === 'title' ) { + (_countSearchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, countQueryAttributs[key] as string) + ); + } else { + (_countSearchQuery['_and'] as IQueryObject[]).push( + Eq(key, countQueryAttributs[key] as string) + ); + } + } + + const body = { + 'query': [ + { + '_name': 'listAlert', + }, + { + '_name': 'filter', + '_and': _countSearchQuery['_and'] + }, + ] + }; + + body['query'].push( + { + '_name': 'count', + } + ); + + qs.name = 'count-Alert'; + + + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + body, + qs, + ); + + responseData = { count: responseData }; + + } + + if (operation === 'create') { + const body: IDataObject = { + title: this.getNodeParameter('title', i), + description: this.getNodeParameter('description', i), + severity: this.getNodeParameter('severity', i), + date: Date.parse(this.getNodeParameter('date', i) as string), + tags: splitTags(this.getNodeParameter('tags', i) as string), + tlp: this.getNodeParameter('tlp', i), + status: this.getNodeParameter('status', i), + type: this.getNodeParameter('type', i), + source: this.getNodeParameter('source', i), + sourceRef: this.getNodeParameter('sourceRef', i), + follow: this.getNodeParameter('follow', i, true), + ...prepareOptional(this.getNodeParameter('optionals', i, {}) as INodeParameters) + }; + + const artifactUi = this.getNodeParameter('artifactUi', i)as IDataObject; + + if (artifactUi) { + + const artifactValues = (artifactUi as IDataObject).artifactValues as IDataObject[]; + + if (artifactValues) { + + const artifactData = []; + + for (const artifactvalue of artifactValues) { + + const element: IDataObject = {}; + + element.message = artifactvalue.message as string; + + element.tags = (artifactvalue.tags as string).split(',') as string[]; + + element.dataType = artifactvalue.dataType as string; + + element.data = artifactvalue.data as string; + + if (artifactvalue.dataType === 'file') { + + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = artifactvalue.binaryProperty as string; + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property '${binaryPropertyName}' does not exists on item!`); + } + + const binaryData = item.binary[binaryPropertyName] as IBinaryData; + + element.data = `${binaryData.fileName};${binaryData.mimeType};${binaryData.data}`; + } + + artifactData.push(element); + } + body.artifacts = artifactData; + } + } + + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/alert' as string, + body, + ); + } + + /* + Execute responder feature differs from Cortex execute responder + if it doesn't interfere with n8n standards then we should keep it + */ + + if (operation === 'executeResponder'){ + const alertId = this.getNodeParameter('id', i); + 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 + } + + ] + } + ], + }; + qs.name = 'log-actions'; + do { + response = await theHiveApiRequest.call( + this, + 'POST', + `/v1/query`, + body, + qs + ); + } while (response.status === 'Waiting' || response.status === 'InProgress' ); + + responseData = response; + } + + if (operation === 'get') { + const alertId = this.getNodeParameter('id', i) as string; + + responseData = await theHiveApiRequest.call( + this, + 'GET', + `/alert/${alertId}`, + {}, + ); + } + + if (operation === 'getAll') { + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const queryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const options = this.getNodeParameter('options', i) as IDataObject; + + const _searchQuery: IQueryObject = And(); + + for (const key of Object.keys(queryAttributs)) { + if ( key === 'tags') { + (_searchQuery['_and'] as IQueryObject[]).push( + In(key, queryAttributs[key] as string[]) + ); + } else if (key === 'description' || key === 'title' ) { + (_searchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, queryAttributs[key] as string) + ); + } else { + (_searchQuery['_and'] as IQueryObject[]).push( + Eq(key, queryAttributs[key] as string) + ); + } + } + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'listAlert', + }, + { + '_name': 'filter', + '_and': _searchQuery['_and'] + }, + ], + }; + + //@ts-ignore + prepareSortQuery(options.sort, body); + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'alerts'; + + } else { + method = 'POST'; + + endpoint = '/alert/_search'; + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = _searchQuery; + + Object.assign(qs, prepareOptional(options)); + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if (operation === 'merge') { + const alertId = this.getNodeParameter('id', i) as string; + + const caseId = this.getNodeParameter('caseId', i) as string; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/alert/${alertId}/merge/${caseId}`, + {}, + ); + } + + if (operation === 'promote') { + const alertId = this.getNodeParameter('id', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, additionalFields); + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/alert/${alertId}/createCase`, + body, + ); + } + + if (operation === 'update') { + const alertId = this.getNodeParameter('id', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const artifactUi = updateFields.artifactUi as IDataObject; + + delete updateFields.artifactUi; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (artifactUi) { + const artifactValues = (artifactUi as IDataObject).artifactValues as IDataObject[]; + + if (artifactValues) { + const artifactData = []; + + for (const artifactvalue of artifactValues) { + + const element: IDataObject = {}; + + element.message = artifactvalue.message as string; + + element.tags = (artifactvalue.tags as string).split(',') as string[]; + + element.dataType = artifactvalue.dataType as string; + + element.data = artifactvalue.data as string; + + if (artifactvalue.dataType === 'file') { + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = artifactvalue.binaryProperty as string; + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property '${binaryPropertyName}' does not exists on item!`); + } + + const binaryData = item.binary[binaryPropertyName] as IBinaryData; + + element.data = `${binaryData.fileName};${binaryData.mimeType};${binaryData.data}`; + } + + artifactData.push(element); + } + body.artifacts = artifactData; + } + } + + responseData = await theHiveApiRequest.call( + this, + 'PATCH', + `/alert/${alertId}` as string, + body, + ); + } + } + + if(resource === 'observable'){ + if(operation === 'count'){ + const countQueryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _countSearchQuery: IQueryObject = And(); + + for (const key of Object.keys(countQueryAttributs)) { + if (key === 'dataType' || key === 'tags') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + In(key, countQueryAttributs[key] as string[]) + ); + } else if (key === 'description' || key === 'keywork' || key === 'message') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, countQueryAttributs[key] as string) + ); + } else if (key === 'range') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + Between( + 'startDate', + countQueryAttributs['range']['dateRange']['fromDate'], + countQueryAttributs['range']['dateRange']['toDate'] + ) + ); + } else { + (_countSearchQuery['_and'] as IQueryObject[]).push( + Eq(key, countQueryAttributs[key] as string) + ); + } + } + + const body = { + 'query': [ + { + '_name': 'listObservable' + }, + { + '_name': 'filter', + '_and': _countSearchQuery['_and'] + }, + ] + }; + + body['query'].push( + { + '_name': 'count' + } + ); + + qs.name = 'count-observables'; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + body, + qs, + ); + + responseData = { count: responseData }; + } + + if (operation === 'executeAnalyzer'){ + const observableId = this.getNodeParameter('id', i); + 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; + responseData = []; + 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' ); + } + + } + + if (operation === 'executeResponder'){ + 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 + } + + ] + } + ] + }; + qs.name = 'log-actions'; + do { + response = await theHiveApiRequest.call( + this, + 'POST', + `/v1/query`, + body, + qs + ); + } while (response.status === 'Waiting' || response.status === 'InProgress' ); + + responseData = response; + } + + if(operation === 'create'){ + const caseId = this.getNodeParameter('caseId', i); + + let body: IDataObject = { + dataType: this.getNodeParameter('dataType', i) as string, + message: this.getNodeParameter('message', i) as string, + startDate: Date.parse(this.getNodeParameter('startDate', i) as string), + tlp: this.getNodeParameter('tlp', i) as number, + ioc: this.getNodeParameter('ioc', i) as boolean, + sighted: this.getNodeParameter('sighted', i) as boolean, + status: this.getNodeParameter('status', i) as string, + ...prepareOptional(this.getNodeParameter('options', i, {}) as INodeParameters) + }; + + let options: IDataObject = {}; + + if (body.dataType === 'file') { + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = this.getNodeParameter('binaryProperty', i) as string; + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property '${binaryPropertyName}' does not exists on item!`); + } + + const binaryData = item.binary[binaryPropertyName] as IBinaryData; + + options = { + formData: { + attachment: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + contentType: binaryData.mimeType, + filename: binaryData.fileName, + } + }, + _json: JSON.stringify(body) + } + }; + body = {}; + }else{ + body.data = this.getNodeParameter('data', i) as string; + } + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/case/${caseId}/artifact` as string, + body, + qs, + '', + options + ); + } + + if(operation === 'get'){ + const observableId = this.getNodeParameter('id', i) as string; + + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const version = credentials.apiVersion; + + let endpoint; + + let method; + + let body: IDataObject = {}; + + if (version === 'v1') { + + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'getObservable', + 'idOrName': observableId + } + ] + }; + + qs.name = `get-observable-${observableId}`; + + } else { + + method = 'GET'; + + endpoint = `/case/artifact/${observableId}`; + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'getAll'){ + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const caseId = this.getNodeParameter('caseId', i); + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + + + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'getCase', + 'idOrName': caseId + }, + { + '_name': 'observables' + }, + ] + }; + + //@ts-ignore + prepareSortQuery(options.sort, body); + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'observables'; + + } else { + + method = 'POST'; + + endpoint = '/case/artifact/_search'; + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = Parent('case', Id(caseId as string)); + + Object.assign(qs, prepareOptional(options)); + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'search'){ + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const queryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _searchQuery: IQueryObject = And(); + + const options = this.getNodeParameter('options', i) as IDataObject; + + for (const key of Object.keys(queryAttributs)) { + if (key === 'dataType' || key === 'tags') { + (_searchQuery['_and'] as IQueryObject[]).push( + In(key, queryAttributs[key] as string[]) + ); + } else if (key === 'description' || key === 'keywork' || key === 'message') { + (_searchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, queryAttributs[key] as string) + ); + } else if (key === 'range') { + (_searchQuery['_and'] as IQueryObject[]).push( + Between( + 'startDate', + queryAttributs['range']['dateRange']['fromDate'], + queryAttributs['range']['dateRange']['toDate'] + ) + ); + } else { + (_searchQuery['_and'] as IQueryObject[]).push( + Eq(key, queryAttributs[key] as string) + ); + } + } + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'listObservable' + }, + { + '_name': 'filter', + '_and': _searchQuery['_and'] + }, + ] + }; + + //@ts-ignore + prepareSortQuery(options.sort, body); + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'observables'; + + } else { + + method = 'POST'; + + endpoint = '/case/artifact/_search'; + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = _searchQuery; + + Object.assign(qs, prepareOptional(options)); + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'update'){ + const id = this.getNodeParameter('id', i) as string; + + const body: IDataObject = { + ...prepareOptional(this.getNodeParameter('updateFields', i, {}) as INodeParameters) + }; + + responseData = await theHiveApiRequest.call( + this, + 'PATCH', + `/case/artifact/${id}` as string, + body, + qs, + ); + + responseData = { success: true }; + } + } + + if (resource === 'case'){ + if(operation === 'count'){ + const countQueryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _countSearchQuery: IQueryObject = And(); + + for (const key of Object.keys(countQueryAttributs)) { + if ( key === 'tags') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + In(key, countQueryAttributs[key] as string[]) + ); + } else if (key === 'description' || key === 'summary' || key === 'title') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, countQueryAttributs[key] as string) + ); + } else { + (_countSearchQuery['_and'] as IQueryObject[]).push( + Eq(key, countQueryAttributs[key] as string) + ); + } + } + + const body = { + 'query': [ + { + '_name': 'listCase', + }, + { + '_name': 'filter', + '_and': _countSearchQuery['_and'] + }, + ] + }; + + body['query'].push( + { + '_name': 'count', + } + ); + + qs.name = 'count-cases'; + + + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + body, + qs, + ); + + responseData = { count: responseData }; + } + + if (operation === 'executeResponder'){ + const caseId = this.getNodeParameter('id', i); + 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 + } + + ] + } + ] + }; + qs.name = 'log-actions'; + do { + response = await theHiveApiRequest.call( + this, + 'POST', + `/v1/query`, + body, + qs + ); + } while (response.status === 'Waiting' || response.status === 'InProgress' ); + + responseData = response; + } + + if(operation === 'create'){ + + const body: IDataObject = { + title: this.getNodeParameter('title', i), + description: this.getNodeParameter('description', i), + severity: this.getNodeParameter('severity', i), + startDate: Date.parse(this.getNodeParameter('startDate', i) as string), + owner: this.getNodeParameter('owner', i), + 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) + }; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/case' as string, + body, + ); + } + + if(operation === 'get'){ + const caseId = this.getNodeParameter('id', i) as string; + + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const version = credentials.apiVersion; + + let endpoint; + + let method; + + let body: IDataObject = {}; + + if (version === 'v1') { + + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'getCase', + 'idOrName': caseId + } + ] + }; + + qs.name = `get-case-${caseId}`; + + } else { + + method = 'GET'; + + endpoint = `/case/${caseId}`; + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'getAll'){ + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const queryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _searchQuery: IQueryObject = And(); + + const options = this.getNodeParameter('options', i) as IDataObject; + + for (const key of Object.keys(queryAttributs)) { + if ( key === 'tags') { + (_searchQuery['_and'] as IQueryObject[]).push( + In(key, queryAttributs[key] as string[]) + ); + } else if (key === 'description' || key === 'summary' || key === 'title') { + (_searchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, queryAttributs[key] as string) + ); + } else { + (_searchQuery['_and'] as IQueryObject[]).push( + Eq(key, queryAttributs[key] as string) + ); + } + } + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'listCase' + }, + { + '_name': 'filter', + '_and': _searchQuery['_and'] + }, + ] + }; + + //@ts-ignore + prepareSortQuery(options.sort, body); + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'cases'; + + } else { + method = 'POST'; + + endpoint = '/case/_search'; + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = _searchQuery; + + Object.assign(qs, prepareOptional(options)); + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'update'){ + const id = this.getNodeParameter('id', i) as string; + + const body: IDataObject = { + ...prepareOptional(this.getNodeParameter('updateFields', i, {}) as INodeParameters) + }; + + responseData = await theHiveApiRequest.call( + this, + 'PATCH', + `/case/${id}` as string, + body, + ); + } + } + + if (resource === 'task'){ + if (operation === 'count'){ + const countQueryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _countSearchQuery: IQueryObject = And(); + + for (const key of Object.keys(countQueryAttributs)) { + if (key === 'title' || key === 'description') { + (_countSearchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, countQueryAttributs[key] as string) + ); + } else { + (_countSearchQuery['_and'] as IQueryObject[]).push( + Eq(key, countQueryAttributs[key] as string) + ); + } + } + + const body = { + 'query': [ + { + '_name': 'listTask' + }, + { + '_name': 'filter', + '_and': _countSearchQuery['_and'] + }, + ] + }; + + body['query'].push( + { + '_name': 'count', + } + ); + + qs.name = 'count-tasks'; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + '/v1/query', + body, + qs, + ); + + responseData = { count: responseData }; + } + + if (operation === 'create'){ + const caseId = this.getNodeParameter('caseId', i) as string; + + const body: IDataObject = { + title: this.getNodeParameter('title', i) as string, + status: this.getNodeParameter('status', i) as string, + flag: this.getNodeParameter('flag', i), + ...prepareOptional(this.getNodeParameter('options', i, {}) as INodeParameters) + }; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/case/${caseId}/task` as string, + body, + ); + } + + if (operation === 'executeResponder'){ + const taskId = this.getNodeParameter('id', i); + const responderId = this.getNodeParameter('responder', i) as string; + let body:IDataObject; + let response; + responseData = []; + 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; + } + + if (operation === 'get'){ + const taskId = this.getNodeParameter('id', i) as string; + + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const version = credentials.apiVersion; + + let endpoint; + + let method; + + let body: IDataObject = {}; + + if (version === 'v1') { + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'getTask', + 'idOrName': taskId + } + ] + }; + + qs.name = `get-task-${taskId}`; + + } else { + method = 'GET'; + + endpoint = `/case/task/${taskId}`; + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'getAll'){ + // get all require a case id (it retursn all tasks for a specific case) + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const caseId = this.getNodeParameter('caseId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'getCase', + 'idOrName': caseId + }, + { + '_name': 'tasks' + }, + ] + }; + + //@ts-ignore + prepareSortQuery(options.sort, body); + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'case-tasks'; + + } else { + method = 'POST'; + + endpoint = '/case/task/_search'; + + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = And(Parent('case', Id(caseId))); + + Object.assign(qs, prepareOptional(options)); + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'search'){ + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const queryAttributs: any = prepareOptional(this.getNodeParameter('filters', i, {}) as INodeParameters); + + const _searchQuery: IQueryObject = And(); + + const options = this.getNodeParameter('options', i) as IDataObject; + + for (const key of Object.keys(queryAttributs)) { + if (key === 'title' || key === 'description') { + (_searchQuery['_and'] as IQueryObject[]).push( + ContainsString(key, queryAttributs[key] as string) + ); + } else { + (_searchQuery['_and'] as IQueryObject[]).push( + Eq(key, queryAttributs[key] as string) + ); + } + } + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'listTask' + }, + { + '_name': 'filter', + '_and': _searchQuery['_and'] + }, + ] + }; + + //@ts-ignore + prepareSortQuery(options.sort, body); + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'tasks'; + + } else { + method = 'POST'; + + endpoint = '/case/task/_search'; + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = _searchQuery; + + Object.assign(qs, prepareOptional(options)); + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if(operation === 'update'){ + const id = this.getNodeParameter('id', i) as string; + + const body: IDataObject = { + ...prepareOptional(this.getNodeParameter('updateFields', i, {}) as INodeParameters) + }; + + responseData = await theHiveApiRequest.call( + this, + 'PATCH', + `/case/task/${id}` as string, + body, + ); + + } + + } + + if (resource === 'log'){ + if (operation === 'create') { + + const taskId = this.getNodeParameter('taskId', i) as string; + + let body: IDataObject = { + message: this.getNodeParameter('message', i), + startDate: Date.parse(this.getNodeParameter('startDate', i) as string), + status: this.getNodeParameter('status', i), + }; + const optionals = this.getNodeParameter('options', i) as IDataObject; + + let options: IDataObject ={}; + + if (optionals.attachementUi) { + const attachmentValues = (optionals.attachementUi as IDataObject).attachmentValues as IDataObject; + + if (attachmentValues) { + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyName = attachmentValues.binaryProperty as string; + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property '${binaryPropertyName}' does not exists on item!`); + } + + const binaryData = item.binary[binaryPropertyName] as IBinaryData; + + options = { + formData: { + attachment: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + contentType: binaryData.mimeType, + filename: binaryData.fileName, + } + }, + _json: JSON.stringify(body) + } + }; + + body = {}; + } + } + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/case/task/${taskId}/log` as string, + body, + qs, + '', + options + ); + } + + if (operation === 'executeResponder'){ + const logId = this.getNodeParameter('id', i); + const responderId = this.getNodeParameter('responder', i) as string; + let body:IDataObject; + let response; + responseData = []; + 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; + } + + if (operation === 'get') { + const logId = this.getNodeParameter('id', i) as string; + + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const version = credentials.apiVersion; + + let endpoint; + + let method; + + let body: IDataObject = {}; + + if (version === 'v1') { + + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + query: [ + { + _name: 'getLog', + idOrName: logId + } + ] + }; + + qs.name = `get-log-${logId}`; + + } else { + + method = 'POST'; + + endpoint = '/case/task/log/_search'; + + body.query = { _id: logId }; + + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + + if (operation === 'getAll'){ + + const credentials = this.getCredentials('theHiveApi') as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const version = credentials.apiVersion; + + const taskId = this.getNodeParameter('taskId', i) as string; + + let endpoint; + + let method; + + let body: IDataObject = {}; + + let limit = undefined; + + if (returnAll === false) { + limit = this.getNodeParameter('limit', i) as number; + } + + if (version === 'v1') { + endpoint = '/v1/query'; + + method = 'POST'; + + body = { + 'query': [ + { + '_name': 'getTask', + 'idOrName': taskId + }, + { + '_name': 'logs' + }, + ] + }; + + if (limit !== undefined) { + //@ts-ignore + prepareRangeQuery(`0-${limit}`, body); + } + + qs.name = 'case-task-logs'; + + } else { + method = 'POST'; + + endpoint = '/case/task/log/_search'; + + if (limit !== undefined) { + qs.range = `0-${limit}`; + } + + body.query = And(Parent( + 'task', + Id(taskId) + )); + } + + responseData = await theHiveApiRequest.call( + this, + method, + endpoint as string, + body, + qs, + ); + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts new file mode 100644 index 0000000000..968e9ef2b8 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts @@ -0,0 +1,165 @@ +import { + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + IHookFunctions, +} from 'n8n-workflow'; + +export class TheHiveTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'TheHive Trigger', + name: 'theHiveTrigger', + icon: 'file:thehive.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when a TheHive event occurs.', + defaults: { + name: 'TheHive Trigger', + color: '#f3d02f', + }, + inputs: [], + outputs: ['main'], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + default: [], + required: true, + description: 'Events types', + 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 Updated', + value: 'alert_update', + description: 'Triggered when an alert is updated', + }, + { + name: 'Alert Deleted', + value: 'alert_delete', + description: 'Triggered when an alert is deleted', + }, + { + name: 'Observable Created', + value: 'case_artifact_create', + description: 'Triggered when an observable is created', + }, + { + name: 'Observable Updated', + value: 'case_artifact_update', + description: 'Triggered when an observable is updated', + }, + { + name: 'Observable Deleted', + value: 'case_artifact_delete', + description: 'Triggered when an observable is deleted', + }, + { + name: 'Case Created', + value: 'case_create', + description: 'Triggered when a case is created', + }, + { + name: 'Case Updated', + value: 'case_update', + description: 'Triggered when a case is updated', + }, + { + name: 'Case Deleted', + value: 'case_delete', + description: 'Triggered when a case is deleted', + }, + { + name: 'Task Created', + value: 'case_task_create', + description: 'Triggered when a task is created', + }, + { + name: 'Task Updated', + value: 'case_task_update', + description: 'Triggered when a task is updated', + }, + { + name: 'Task Deleted', + value: 'case_task_delete', + description: 'Triggered when a task is deleted', + }, + { + name: 'Log Created', + value: 'case_task_log_create', + description: 'Triggered when a task log is created', + }, + ] + } + ] + }; + // @ts-ignore (because of request) + 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.operation || !bodyData.objectType) { + // Don't start the workflow if mandatory fields are not specified + return {}; + } + + // Don't start the workflow if the event is not fired + const event = `${(bodyData.objectType as string).toLowerCase()}_${(bodyData.operation as string).toLowerCase()}`; + if(events.indexOf('*') === -1 && events.indexOf(event) === -1) { + return {}; + } + + // The data to return and so start the workflow with + const returnData: IDataObject[] = []; + 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/TheHive/descriptions/AlertDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts new file mode 100644 index 0000000000..21c50f893d --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts @@ -0,0 +1,839 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + TLP, +} from '../interfaces/AlertInterface'; + +export const alertOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'loadAlertOptions', + }, + displayOptions: { + show: { + resource: [ + 'alert', + ], + }, + }, + default: 'create', + }, +] as INodeProperties[]; + +export const alertFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'alert', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'alert', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + // required attributs + { + displayName: 'Alert ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert' + ], + operation: [ + 'promote', + 'merge', + 'update', + 'executeResponder', + 'get', + ], + }, + }, + description: 'Title of the alert' + }, + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'merge', + ], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Title of the alert', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Description of the alert', + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + options:[ + { + name: 'Low', + value: 1 + }, + { + name: 'Medium', + value: 2 + }, + { + name: 'High', + value: 3, + }, + ], + required: true, + default: 2, + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Severity of the alert. Default=Medium', + }, + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Date and time when the alert was raised default=now' + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + required: true, + default: '', + placeholder:'tag,tag2,tag3...', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Case Tags' + }, + { + displayName: 'TLP', + name: 'tlp', + type: 'options', + required: true, + default: 2, + options: [ + { + name:'White', + value:TLP.white, + }, + { + name:'Green', + value:TLP.green, + }, + { + name:'Amber', + value:TLP.amber, + },{ + name:'Red', + value:TLP.red, + } + ], + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Traffict Light Protocol (TLP). Default=Amber' + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + options:[ + { + name: 'New', + value: 'New', + }, + { + name: 'Updated', + value: 'Updated', + }, + { + name: 'Ignored', + value: 'Ignored' + }, + { + name: 'Imported', + value: 'Imported', + }, + ], + default: 'New', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Status of the alert', + }, + { + displayName: 'Type', + name: 'type', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Type of the alert' + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Source of the alert' + }, + { + displayName: 'SourceRef', + name: 'sourceRef', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Source reference of the alert' + }, + { + displayName: 'Follow', + name: 'follow', + type: 'boolean', + required: true, + default: true, + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + description: 'if true, the alert becomes active when updated default=true', + }, + { + displayName: 'Artifacts', + name: 'artifactUi', + type: 'fixedCollection', + placeholder: 'Add Artifact', + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Artifact', + name: 'artifactValues', + values: [ + { + displayName: 'Data Type', + name: 'dataType', + type: 'options', + default: '', + options: [ + { + name: 'IP', + value: 'ip', + }, + { + name: 'Domain', + value: 'domain', + }, + { + name: 'File', + value: 'file', + }, + ], + description: '', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + displayOptions: { + hide: { + dataType: [ + 'file', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + displayOptions: { + show: { + dataType: [ + 'file', + ], + }, + }, + default: 'data', + description: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Case Tags', + name: 'tags', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + description: 'Artifact attributes' + }, + // required for responder execution + { + displayName: 'Responder ID', + name: 'responder', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsDependsOn: [ + 'id', + ], + loadOptionsMethod: 'loadResponders', + }, + displayOptions:{ + show: { + resource: [ + 'alert', + ], + operation: [ + 'executeResponder', + ], + }, + hide: { + id: [ + '', + ], + }, + }, + }, + // optional attributs (Create, Promote operations) + { + displayName: 'Additional Fields', + name: 'additionalFields', + placeholder: 'Add Field', + type: 'collection', + required: false, + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'create', + 'promote', + ], + }, + }, + options:[ + { + displayName: 'Case Template', + name: 'caseTemplate', + type:'string', + default: '', + description: `Case template to use when a case is created from this alert`, + }, + ], + }, + // optional attributs (Update operation) + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: '', + displayOptions: { + show: { + resource: [ + 'alert', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Artifacts', + name: 'artifactUi', + type: 'fixedCollection', + placeholder: 'Add Artifact', + default: '', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Artifact', + name: 'artifactValues', + values: [ + { + displayName: 'Data Type', + name: 'dataType', + type: 'options', + default: '', + options: [ + { + name: 'IP', + value: 'ip', + }, + { + name: 'Domain', + value: 'domain', + }, + { + name: 'File', + value: 'file', + }, + ], + description: '', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + displayOptions: { + hide: { + dataType: [ + 'file', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + displayOptions: { + show: { + dataType: [ + 'file', + ], + }, + }, + default: 'data', + description: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Case Tags', + name: 'tags', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + }, + { + displayName: 'Case Template', + name: 'caseTemplate', + type: 'string', + required: false, + default: '', + description: `Case template to use when a case is created from this alert`, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + required: false, + default: '', + description: 'Description of the alert', + }, + { + displayName: 'Follow', + name: 'follow', + type: 'boolean', + default: true, + description: 'if true, the alert becomes active when updated default=true', + }, + { + displayName: 'Severity', + name: ' severity', + type: 'options', + options:[ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + default: 2, + description: 'Severity of the alert. Default=Medium', + }, + { + 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', + }, + { + displayName: 'Case Tags', + name: 'tags', + type: 'string', + default: '', + placeholder:'tag,tag2,tag3...', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: false, + default: '', + description: 'Title of the alert' + }, + { + displayName: 'TLP', + name: 'tlp', + type: 'options', + required: false, + default: 2, + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber' + }, + ], + }, + //Query attributs (Search operation) + { + displayName: 'Options', + name: 'options', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'alert', + ], + }, + }, + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'string', + placeholder: '±Attribut, exp +status', + default: '', + }, + ], + }, + { + displayName: 'Filters', + name: 'filters', + placeholder: 'Add Filter', + default: {}, + type: 'collection', + displayOptions: { + show: { + resource: [ + 'alert' + ], + operation: [ + 'getAll', + 'count', + ], + }, + }, + options:[ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the alert', + }, + { + displayName: 'Follow', + name: 'follow', + type: 'boolean', + default: false, + description: 'if true, the alert becomes active when updated default=true', + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1 + }, + { + name: 'Medium', + value: 2 + }, + { + name: 'High', + value: 3 + }, + ], + default: 2, + description: 'Severity of the alert. Default=Medium', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + placeholder: 'tag,tag2,tag3...', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: '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, + } + ], + description: 'Traffict Light Protocol (TLP). Default=Amber' + }, + ], + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts new file mode 100644 index 0000000000..6e78d07ef6 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts @@ -0,0 +1,757 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + TLP, +} from '../interfaces/AlertInterface'; + +export const caseOperations = [ + { + displayName: 'Operation', + name: 'operation', + default: 'getAll', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'case', + ], + }, + }, + typeOptions: { + loadOptionsDependsOn: [ + 'resource', + ], + loadOptionsMethod: 'loadCaseOptions', + }, + }, +] as INodeProperties[]; + +export const caseFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'case', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'case', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + // Required fields + { + displayName: 'Case ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'update', + 'executeResponder', + 'get', + ], + }, + }, + description: 'ID of the case', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Title of the case', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Description of the case', + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1, + }, + { + name: 'Medium', + value: 2, + }, + { + name: 'High', + value: 3, + }, + ], + required: true, + default: 2, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Severity of the alert. Default=Medium', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Flag of the case default=false', + }, + { + displayName: 'TLP', + name: 'tlp', + type: 'options', + required: true, + default: 2, + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Traffict Light Protocol (TLP). Default=Amber' + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + }, + // required for responder execution + { + displayName: 'Responder ID', + name: 'responder', + type: 'options', + default: '', + required: true, + typeOptions: { + loadOptionsDependsOn: [ + 'id', + ], + loadOptionsMethod: 'loadResponders', + }, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'executeResponder', + ], + }, + hide: { + id: [ + '', + ], + }, + }, + }, + // Optional fields (Create operation) + { + displayName: 'Options', + type: 'collection', + name: 'options', + placeholder: 'Add options', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + required: false, + default: '', + options: [ + { + displayName: 'End Date', + name: 'endDate', + default: '', + type: 'dateTime', + description: 'Resolution date', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Summary of the case, to be provided when closing a case', + }, + { + displayName: 'Metrics (JSON)', + name: 'metrics', + default: '[]', + type: 'json', + description: 'List of metrics', + }, + ], + }, + // Optional fields (Update operations) + { + displayName: 'Update Fields', + type: 'collection', + name: 'updateFields', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'update', + ], + }, + }, + required: false, + default: '', + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the case', + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + description: 'Resolution date', + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + default: false, + description: 'Flag of the case default=false', + }, + { + displayName: 'Impact Status', + name: 'impactStatus', + type: 'options', + default: '', + options: [ + { + name: 'No Impact', + value: 'NoImpact' + }, + { + name: 'With Impact', + value: 'WithImpact' + }, + { + name: 'Not Applicable', + value: 'NotApplicable' + }, + ], + description: 'Impact status of the case', + }, + { + displayName: 'Metrics (JSON)', + name: 'metrics', + type: 'json', + default: '[]', + description: 'List of metrics', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + }, + { + displayName: 'Resolution Status', + name: 'resolutionStatus', + type: 'options', + default: '', + options: [ + { + value: 'Indeterminate', + name: 'Indeterminate' + }, + { + value: 'False Positive', + name: 'FalsePositive' + }, + { + value: 'True Positive', + name: 'TruePositive' + }, + { + value: 'Other', + name: 'Other' + }, + { + value: 'Duplicated', + name: 'Duplicated' + }, + ], + description: 'Resolution status of the case', + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1 + }, + { + name: 'Medium', + value: 2 + }, + { + name: 'High', + value: 3 + }, + ], + default: 2, + description: 'Severity of the alert. Default=Medium', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'Open', + }, + { + name: 'Resolved', + value: 'Resolved', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + default: 'Open', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Summary of the case, to be provided when closing a case' + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the case', + }, + { + displayName: '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, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber' + }, + ], + }, + // query options + { + displayName: 'Options', + name: 'options', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'case', + ], + }, + }, + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'string', + placeholder: '±Attribut, exp +status', + description: 'Specify the sorting attribut, + for asc, - for desc', + default: '', + }, + ], + }, + // Query filters + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + required: false, + default: {}, + placeholder: 'Add a Filter', + displayOptions: { + show: { + resource: [ + 'case' + ], + operation: [ + 'getAll', + 'count', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the case', + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + description: 'Resolution date', + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + default: false, + description: 'Flag of the case default=false', + }, + { + displayName: 'Impact Status', + name: 'impactStatus', + type: 'options', + default: '', + options: [ + { + name: 'No Impact', + value: 'NoImpact', + }, + { + name: 'With Impact', + value: 'WithImpact', + }, + { + name: 'Not Applicable', + value: 'NotApplicable', + }, + ], + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + }, + { + displayName: 'Resolution Status', + name: 'resolutionStatus', + type: 'options', + default: '', + options: [ + { + value: 'Indeterminate', + name: 'Indeterminate', + }, + { + value: 'False Positive', + name: 'FalsePositive', + }, + { + value: 'True Positive', + name: 'TruePositive', + }, + { + value: 'Other', + name: 'Other', + }, + { + value: 'Duplicated', + name: 'Duplicated', + }, + ], + }, + { + displayName: 'Severity', + name: 'severity', + type: 'options', + options: [ + { + name: 'Low', + value: 1 + }, + { + name: 'Medium', + value: 2 + }, + { + name: 'High', + value: 3 + }, + ], + default: 2, + description: 'Severity of the alert. Default=Medium', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'Open', + }, + { + name: 'Resolved', + value: 'Resolved', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + default: 'Open', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Summary of the case, to be provided when closing a case', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the case', + }, + { + displayName: 'TLP', + name: 'tlp', + type: 'options', + required: false, + default: 2, + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts new file mode 100644 index 0000000000..73691719b8 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts @@ -0,0 +1,262 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const logOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + default: 'getAll', + displayOptions: { + show: { + resource: [ + 'log', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create task log', + }, + { + name: 'Execute Responder', + value: 'executeResponder', + description: 'Execute a responder on a selected log' + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all task logs' + }, + { + name: 'Get', + value: 'get', + description: 'Get a single log', + }, + ], + } +] as INodeProperties[]; + +export const logFields = [ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'create', + 'getAll', + ], + }, + }, + description: 'ID of the task', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'log', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'log', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + // required attributs + { + displayName: 'Log ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'executeResponder', + 'get', + ], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Content of the Log', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Date of the log submission default=now', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Status of the log (Ok or Deleted) default=Ok', + }, + // required for responder execution + { + displayName: 'Responder ID', + name: 'responder', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsDependsOn: [ + 'id', + ], + loadOptionsMethod: 'loadResponders' + }, + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'executeResponder', + ], + }, + hide: { + id: [ + '', + ], + }, + }, + }, + // Optional attributs + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'create', + ], + }, + }, + placeholder: 'Add Option', + options: [ + { + displayName: 'Attachment', + name: 'attachmentValues', + placeholder: 'Add Attachment', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + displayName: 'Attachment', + name: 'attachmentValues', + values: [ + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + default: 'data', + description: 'Object property name which holds binary data.', + }, + ], + }, + ], + description: 'File attached to the log', + }, + ], + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts new file mode 100644 index 0000000000..36e814db7b --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts @@ -0,0 +1,787 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + TLP, +} from '../interfaces/AlertInterface'; + +export const observableOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + default: 'getAll', + displayOptions: { + show: { + resource: [ + 'observable', + ], + }, + }, + typeOptions: { + loadOptionsDependsOn: [ + 'resource', + ], + loadOptionsMethod: 'loadObservableOptions', + }, + }, +] as INodeProperties[]; + +export const observableFields = [ + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + 'getAll', + ], + }, + }, + description: 'ID of the case', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + 'search', + ], + resource: [ + 'observable', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + 'search', + ], + resource: [ + 'observable', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + // required attributs + { + displayName: 'Observable ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'update', + 'executeResponder', + 'executeAnalyzer', + 'get', + ], + }, + }, + description: 'ID of the observable', + }, + { + displayName: 'Data Type', + name: 'dataType', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'domain', + value: 'domain', + }, + { + name: 'file', + value: 'file' + }, + { + name: 'filename', + value: 'filename' + }, + { + name: 'fqdn', + value: 'fqdn' + }, + { + name: 'hash', + value: 'hash' + }, + { + name: 'ip', + value: 'ip' + }, + { + name: 'mail', + value: 'mail' + }, + { + name: 'mail_subject', + value: 'mail_subject' + }, + { + name: 'other', + value: 'other' + }, + { + name: 'regexp', + value: 'regexp' + }, + { + name: 'registry', + value: 'registry' + }, + { + name: 'uri_path', + value: 'uri_path' + }, + { + name: 'url', + value: 'url' + }, + { + name: 'user-agent', + value: 'user-agent' + }, + ], + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + 'executeAnalyzer', + ], + }, + }, + description: 'Type of the observable', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + hide: { + dataType: [ + 'file', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Binary Property that represent the attachment file', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + dataType: [ + 'file', + ], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'observable' + ], + operation: [ + 'create', + ], + }, + }, + description: 'Description of the observable in the context of the case', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Date and time of the begin of the case default=now', + }, + { + displayName: 'TLP', + name: 'tlp', + type: 'options', + required: true, + default: 2, + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'White', + value: TLP.white, + }, + { + name: 'Green', + value: TLP.green, + }, + { + name: 'Amber', + value: TLP.amber, + }, + { + name: 'Red', + value: TLP.red, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + { + displayName: 'IOC', + name: 'ioc', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Indicates if the observable is an IOC (Indicator of compromise)', + }, + { + displayName: 'Sighted', + name: 'sighted', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Sighted previously', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Status of the observable. Default=Ok', + }, + // required for analyzer execution + { + displayName: 'Analyzer', + name: 'analyzers', + type: 'multiOptions', + required: true, + default: [], + typeOptions: { + loadOptionsDependsOn: [ + 'id', + 'dataType', + ], + loadOptionsMethod: 'loadAnalyzers', + }, + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'executeAnalyzer', + ], + }, + hide: { + id: [ + '', + ], + }, + }, + }, + + // required for responder execution + { + displayName: 'Responder ID', + name: 'responder', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsDependsOn: [ + 'id', + ], + loadOptionsMethod: 'loadResponders', + }, + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'executeResponder', + ], + }, + hide: { + id: [ + '', + ], + }, + }, + }, + // Optional attributes (Create operation) + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + required: false, + default: '', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Observable Tags', + name: 'tags', + type: 'string', + required: false, + default: '', + placeholder: 'tag1,tag2', + }, + ], + }, + // Optional attributes (Update operation) + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + required: false, + default: '', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: 'Description of the observable in the context of the case', + + }, + { + displayName: 'Observable Tags', + name: 'tags', + type: 'string', + default: '', + placeholder: 'tag1,tag2', + }, + { + displayName: '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, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + { + displayName: 'IOC', + name: 'ioc', + type: 'boolean', + default: false, + description: 'Indicates if the observable is an IOC (Indicator of compromise)', + }, + { + displayName: 'Sighted', + name: 'sighted', + description: 'sighted previously', + type: 'boolean', + default: false, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + description: 'Status of the observable. Default=Ok', + }, + ], + }, + // query options + { + displayName: 'Options', + name: 'options', + displayOptions: { + show: { + operation: [ + 'getAll', + 'search', + ], + resource: [ + 'observable', + ], + }, + }, + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'string', + placeholder: '±Attribut, exp +status', + description: 'Specify the sorting attribut, + for asc, - for desc', + default: '', + }, + ], + }, + // query attributes + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + required: false, + default: '', + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'observable', + ], + operation: [ + 'search', + 'count', + ], + }, + }, + options: [ + { + displayName: 'Data Type', + name: 'dataType', + type: 'multiOptions', + default: [], + options: [ + { + name: 'domain', + value: 'domain' + }, + { + name: 'file', + value: 'file' + }, + { + name: 'filename', + value: 'filename' + }, + { + name: 'fqdn', + value: 'fqdn' + }, + { + name: 'hash', + value: 'hash' + }, + { + name: 'ip', + value: 'ip' + }, + { + name: 'mail', + value: 'mail' + }, + { + name: 'mail_subject', + value: 'mail_subject' + }, + { + name: 'other', + value: 'other' + }, + { + name: 'regexp', + value: 'regexp' + }, + { + name: 'registry', + value: 'registry' + }, + { + name: 'uri_path', + value: 'uri_path' + }, + { + name: 'url', + value: 'url' + }, + { + name: 'user-agent', + value: 'user-agent' + }, + ], + description: 'Type of the observable', + }, + { + displayName: 'Date range', + type: 'fixedCollection', + name: 'range', + default: {}, + options: [ + { + displayName: 'Add date range inputs', + name: 'dateRange', + values: [ + { + displayName: 'From date', + name: 'fromDate', + type: 'dateTime', + required: false, + default: '', + }, + { + displayName: 'To date', + name: 'toDate', + type: 'dateTime', + required: false, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + placeholder: 'exp,freetext', + }, + { + displayName: 'IOC', + name: 'ioc', + type: 'boolean', + default: false, + description: 'Indicates if the observable is an IOC (Indicator of compromise)', + }, + { + displayName: 'Keyword', + name: 'keyword', + type: 'string', + default: '', + placeholder: 'exp,freetext', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: 'Description of the observable in the context of the case', + }, + { + displayName: 'Observable Tags', + name: 'tags', + type: 'string', + default: '', + placeholder: 'tag1,tag2', + }, + { + displayName: 'Sighted', + name: 'sighted', + type: 'boolean', + default: false, + }, + { + name: 'Status', + displayName: 'Status', + type: 'options', + default: '', + options: [ + { + name: 'Ok', + value: 'Ok', + }, + { + name: 'Deleted', + value: 'Deleted', + }, + ], + description: 'Status of the observable. Default=Ok', + }, + { + displayName: '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, + }, + ], + description: 'Traffict Light Protocol (TLP). Default=Amber', + }, + { + displayName: 'Value', + name: 'data', + type: 'string', + default: '', + placeholder: 'example.com; 8.8.8.8', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/TheHive/descriptions/TaskDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/TaskDescription.ts new file mode 100644 index 0000000000..fad2f04e13 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/descriptions/TaskDescription.ts @@ -0,0 +1,468 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const taskOperations = [ + { + displayName: 'Operation', + name: 'operation', + default: 'getAll', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + typeOptions: { + loadOptionsDependsOn: [ + 'operation', + ], + loadOptionsMethod: 'loadTaskOptions', + }, + }, +] as INodeProperties[]; + +export const taskFields = [ + { + displayName: 'Task ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + 'executeResponder', + 'get', + ], + }, + }, + description: 'ID of the taks', + }, + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'search', + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'search', + 'getAll', + ], + resource: [ + 'task', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Task details', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'Waiting', + options: [ + { + name: 'Waiting', + value: 'Waiting', + }, + { + name: 'InProgress', + value: 'InProgress', + }, + { + name: 'Completed', + value: 'Completed', + }, + { + name: 'Cancel', + value: 'Cancel', + }, + ], + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Status of the task. Default=Waiting', + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Flag of the task. Default=false', + }, + // required for responder execution + { + displayName: 'Responder ID', + name: 'responder', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsDependsOn: [ + 'id', + ], + loadOptionsMethod: 'loadResponders', + }, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'executeResponder', + ], + }, + hide: { + id: [ + '', + ], + }, + }, + }, + // optional attributes (Create operations) + { + displayName: 'Options', + type: 'collection', + name: 'options', + placeholder: 'Add Option', + required: false, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Task details', + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + description: 'Date of the end of the task. This is automatically set when status is set to Completed', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + description: `User who owns the task. This is automatically set to current user when status is set to InProgress`, + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date of the beginning of the task. This is automatically set when status is set to Open', + }, + ], + }, + // optional attributes (Update operation) + + { + displayName: 'Update Fields', + type: 'collection', + name: 'updateFields', + placeholder: 'Add Field', + default: '', + required: false, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Task details', + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + description: 'Date of the end of the task. This is automatically set when status is set to Completed', + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + default: false, + description: 'Flag of the task. Default=false', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + description: `User who owns the task. This is automatically set to current user when status is set to InProgress`, + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date of the beginning of the task. This is automatically set when status is set to Open', + }, + { + displayName: 'status', + name: 'status', + type: 'options', + default: 'Waiting', + options: [ + { + name: 'Waiting', + value: 'Waiting', + }, + { + name: 'In Progress', + value: 'InProgress', + }, + { + name: 'Completed', + value: 'Completed', + }, + { + name: 'Cancel', + value: 'Cancel', + }, + ], + description: 'Status of the task. Default=Waiting', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Task details', + }, + ], + }, + + // query options + { + displayName: 'Options', + name: 'options', + placeholder: 'Add Option', + type: 'collection', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + 'search', + ], + resource: [ + 'task', + ], + }, + }, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'string', + placeholder: '±Attribut, exp +status', + description: 'Specify the sorting attribut, + for asc, - for desc', + default: '', + }, + ], + }, + // query attributes + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + required: false, + default: {}, + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'search', + 'count', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Task details', + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + description: 'Date of the end of the task. This is automatically set when status is set to Completed', + }, + { + displayName: 'Flag', + name: 'flag', + type: 'boolean', + default: false, + description: 'Flag of the task. Default=false', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'string', + default: '', + description: `User who owns the task. This is automatically set to current user when status is set to InProgress`, + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Date of the beginning of the task. This is automatically set when status is set to Open', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'Waiting', + options: [ + { + name: 'Waiting', + value: 'Waiting', + }, + { + name: 'In Progress', + value: 'InProgress' + }, + { + name: 'Completed', + value: 'Completed' + }, + { + name: 'Cancel', + value: 'Cancel' + }, + ], + description: 'Status of the task. Default=Waiting', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Task details', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts new file mode 100644 index 0000000000..873d5bf927 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts @@ -0,0 +1,44 @@ +import { + IDataObject +}from 'n8n-workflow' +export enum AlertStatus{ + NEW="New", + UPDATED="Updated", + IGNORED="Ignored", + IMPORTED="Imported", +} +export enum TLP{ + white, + green, + amber, + red +} + +export interface IAlert{ + // Required attributes + id?:string; + title?:string; + description?:string; + severity?:number; + date?:Date; + tags?:string[]; + tlp?:TLP; + status?:AlertStatus; + type?:string; + source?:string; + sourceRef?:string; + artifacts?:IDataObject[]; + follow?:boolean; + + // Optional attributes + caseTemplate?:string; + + // Backend generated attributes + lastSyncDate?:Date; + case?:string; + + createdBy?:string; + createdAt?:Date; + updatedBy?:string; + upadtedAt?:Date; +} diff --git a/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts new file mode 100644 index 0000000000..88781526ff --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts @@ -0,0 +1,53 @@ +import { IDataObject } from "n8n-workflow"; +import { TLP } from './AlertInterface'; +export interface ICase{ + // Required attributes + id?:string; + title?:string; + description?:string; + severity?:number; + startDate?:Date; + owner?:string; + flag?:boolean; + tlp?:TLP; + tags?:string[]; + + // Optional attributes + resolutionStatus?:CaseResolutionStatus; + impactStatus?:CaseImpactStatus; + summary?:string; + endDate?:Date; + metrics?:IDataObject; + + // Backend generated attributes + status?:CaseStatus; + caseId?:number; // auto-generated attribute + mergeInto?:string; + mergeFrom?:string[]; + + createdBy?:string; + createdAt?:Date; + updatedBy?:string; + upadtedAt?:Date; +} + + +export enum CaseStatus{ + OPEN="Open", + RESOLVED="Resolved", + DELETED="Deleted", +} + +export enum CaseResolutionStatus{ + INDETERMINATE="Indeterminate", + FALSEPOSITIVE="FalsePositive", + TRUEPOSITIVE="TruePositive", + OTHER="Other", + DUPLICATED="Duplicated", +} + +export enum CaseImpactStatus{ + NOIMPACT="NoImpact", + WITHIMPACT="WithImpact", + NOTAPPLICABLE="NotApplicable", +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts new file mode 100644 index 0000000000..a2e957313f --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts @@ -0,0 +1,23 @@ +import { IDataObject } from "n8n-workflow"; +import {IAttachment} from "./ObservableInterface"; +export enum LogStatus{ + OK="Ok", + DELETED="Deleted" +} +export interface ILog{ + // Required attributes + id?:string; + message?:string; + startDate?:Date; + status?:LogStatus; + + // Optional attributes + attachment?:IAttachment; + + // Backend generated attributes + + createdBy?:string; + createdAt?:Date; + updatedBy?:string; + upadtedAt?:Date; +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts new file mode 100644 index 0000000000..ef4ca93101 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts @@ -0,0 +1,54 @@ +import { + TLP +}from './AlertInterface' +import { IDataObject } from 'n8n-workflow'; + +export enum ObservableStatus{ + OK="Ok", + DELETED="Deleted", +} +export enum ObservableDataType{ + "domain"= "domain", + "file"= "file", + "filename"= "filename", + "fqdn"= "fqdn", + "hash"= "hash", + "ip"= "ip", + "mail"= "mail", + "mail_subject"= "mail_subject", + "other"= "other", + "regexp"= "regexp", + "registry"= "registry", + "uri_path"= "uri_path", + "url"= "url", + "user-agent"= "user-agent" +} + +export interface IAttachment{ + name?:string; + size?:number; + id?:string; + contentType?:string; + hashes:string[]; +} +export interface IObservable{ + // Required attributes + id?:string; + data?:string; + attachment?:IAttachment; + dataType?:ObservableDataType; + message?:string; + startDate?:Date; + tlp?:TLP; + ioc?:boolean; + status?:ObservableStatus; + // Optional attributes + tags:string[]; + // Backend generated attributes + + createdBy?:string; + createdAt?:Date; + updatedBy?:string; + upadtedAt?:Date; + +} diff --git a/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts new file mode 100644 index 0000000000..4008f214c0 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts @@ -0,0 +1,25 @@ +export interface ITask{ + // Required attributes + id?:string; + title?:string; + status?:TaskStatus; + flag?:boolean; + // Optional attributes + owner?:string; + description?:string; + startDate?:Date; + endDate?:Date; + // Backend generated attributes + + createdBy?:string; + createdAt?:Date; + updatedBy?:string; + upadtedAt?:Date; +} + +export enum TaskStatus{ + WAITING="Waiting", + INPROGRESS="InProgress", + COMPLETED="Completed", + CANCEL="Cancel", +} diff --git a/packages/nodes-base/nodes/TheHive/thehive.png b/packages/nodes-base/nodes/TheHive/thehive.png new file mode 100644 index 0000000000..324e26df93 Binary files /dev/null and b/packages/nodes-base/nodes/TheHive/thehive.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d6a107c45c..c935133f0f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -54,6 +54,7 @@ "dist/credentials/ContentfulApi.credentials.js", "dist/credentials/ConvertKitApi.credentials.js", "dist/credentials/CopperApi.credentials.js", + "dist/credentials/CortexApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", @@ -195,6 +196,7 @@ "dist/credentials/TaigaCloudApi.credentials.js", "dist/credentials/TaigaServerApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", + "dist/credentials/TheHiveApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TodoistOAuth2Api.credentials.js", "dist/credentials/TravisCiApi.credentials.js", @@ -265,6 +267,7 @@ "dist/nodes/ConvertKit/ConvertKit.node.js", "dist/nodes/ConvertKit/ConvertKitTrigger.node.js", "dist/nodes/Copper/CopperTrigger.node.js", + "dist/nodes/Cortex/Cortex.node.js", "dist/nodes/CrateDb/CrateDb.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Crypto.node.js", @@ -420,6 +423,8 @@ "dist/nodes/Taiga/TaigaTrigger.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", + "dist/nodes/TheHive/TheHive.node.js", + "dist/nodes/TheHive/TheHiveTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js", "dist/nodes/Toggl/TogglTrigger.node.js", "dist/nodes/TravisCi/TravisCi.node.js",