2021-09-22 08:48:50 -07:00
import {
IExecuteFunctions ,
} from 'n8n-core' ;
import {
ICredentialsDecrypted ,
ICredentialTestFunctions ,
IDataObject ,
ILoadOptionsFunctions ,
2022-02-05 13:55:43 -08:00
INodeCredentialTestResult ,
2021-09-22 08:48:50 -07:00
INodeExecutionData ,
INodePropertyOptions ,
INodeType ,
INodeTypeDescription ,
NodeOperationError ,
} from 'n8n-workflow' ;
import {
elasticSecurityApiRequest ,
getConnector ,
getVersion ,
handleListing ,
throwOnEmptyUpdate ,
tolerateTrailingSlash ,
} from './GenericFunctions' ;
import {
caseCommentFields ,
caseCommentOperations ,
caseFields ,
caseOperations ,
caseTagFields ,
caseTagOperations ,
connectorFields ,
connectorOperations ,
} from './descriptions' ;
import {
Connector ,
ConnectorCreatePayload ,
ConnectorType ,
ElasticSecurityApiCredentials ,
} from './types' ;
import {
OptionsWithUri ,
} from 'request' ;
export class ElasticSecurity implements INodeType {
description : INodeTypeDescription = {
displayName : 'Elastic Security' ,
name : 'elasticSecurity' ,
icon : 'file:elasticSecurity.svg' ,
group : [ 'transform' ] ,
version : 1 ,
subtitle : '={{$parameter["operation"] + ": " + $parameter["resource"]}}' ,
description : 'Consume the Elastic Security API' ,
defaults : {
name : 'Elastic Security' ,
} ,
inputs : [ 'main' ] ,
outputs : [ 'main' ] ,
credentials : [
{
name : 'elasticSecurityApi' ,
required : true ,
testedBy : 'elasticSecurityApiTest' ,
} ,
] ,
properties : [
{
displayName : 'Resource' ,
name : 'resource' ,
noDataExpression : true ,
type : 'options' ,
options : [
{
name : 'Case' ,
value : 'case' ,
} ,
{
name : 'Case Comment' ,
value : 'caseComment' ,
} ,
{
name : 'Case Tag' ,
value : 'caseTag' ,
} ,
{
name : 'Connector' ,
value : 'connector' ,
} ,
] ,
default : 'case' ,
} ,
. . . caseOperations ,
. . . caseFields ,
. . . caseCommentOperations ,
. . . caseCommentFields ,
. . . caseTagOperations ,
. . . caseTagFields ,
. . . connectorOperations ,
. . . connectorFields ,
] ,
} ;
methods = {
loadOptions : {
async getTags ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const tags = await elasticSecurityApiRequest . call ( this , 'GET' , '/cases/tags' ) as string [ ] ;
return tags . map ( tag = > ( { name : tag , value : tag } ) ) ;
} ,
async getConnectors ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const endpoint = '/cases/configure/connectors/_find' ;
const connectors = await elasticSecurityApiRequest . call ( this , 'GET' , endpoint ) as Connector [ ] ;
return connectors . map ( ( { name , id } ) = > ( { name , value : id } ) ) ;
} ,
} ,
credentialTest : {
async elasticSecurityApiTest (
this : ICredentialTestFunctions ,
credential : ICredentialsDecrypted ,
2022-02-05 13:55:43 -08:00
) : Promise < INodeCredentialTestResult > {
2021-09-22 08:48:50 -07:00
const {
username ,
password ,
baseUrl : rawBaseUrl ,
} = credential . data as ElasticSecurityApiCredentials ;
const baseUrl = tolerateTrailingSlash ( rawBaseUrl ) ;
const token = Buffer . from ( ` ${ username } : ${ password } ` ) . toString ( 'base64' ) ;
const endpoint = '/cases/status' ;
const options : OptionsWithUri = {
headers : {
Authorization : ` Basic ${ token } ` ,
'kbn-xsrf' : true ,
} ,
method : 'GET' ,
body : { } ,
qs : { } ,
uri : ` ${ baseUrl } /api ${ endpoint } ` ,
json : true ,
} ;
try {
await this . helpers . request ( options ) ;
return {
status : 'OK' ,
message : 'Authentication successful' ,
} ;
} catch ( error ) {
return {
status : 'Error' ,
message : error.message ,
} ;
}
} ,
} ,
} ;
async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
const items = this . getInputData ( ) ;
const returnData : IDataObject [ ] = [ ] ;
const resource = this . getNodeParameter ( 'resource' , 0 ) as string ;
const operation = this . getNodeParameter ( 'operation' , 0 ) as string ;
let responseData ;
for ( let i = 0 ; i < items . length ; i ++ ) {
try {
if ( resource === 'case' ) {
// **********************************************************************
// case
// **********************************************************************
if ( operation === 'create' ) {
// ----------------------------------------
// case: create
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-create.html
const body = {
title : this.getNodeParameter ( 'title' , i ) ,
connector : { } ,
owner : 'securitySolution' ,
description : '' ,
tags : [ ] , // set via `caseTag: add` but must be present
settings : {
syncAlerts : this.getNodeParameter ( 'additionalFields.syncAlerts' , i , false ) ,
} ,
} as IDataObject ;
const connectorId = this . getNodeParameter ( 'connectorId' , i ) as ConnectorType ;
const {
id : fetchedId ,
name : fetchedName ,
type : fetchedType ,
} = await getConnector . call ( this , connectorId ) ;
const selectedConnectorType = this . getNodeParameter ( 'connectorType' , i ) as ConnectorType ;
if ( fetchedType !== selectedConnectorType ) {
throw new NodeOperationError (
this . getNode ( ) ,
'Connector Type does not match the type of the connector in Connector Name' ,
2022-07-12 08:51:01 -07:00
{ itemIndex : i } ,
2021-09-22 08:48:50 -07:00
) ;
}
const connector = {
id : fetchedId ,
name : fetchedName ,
type : fetchedType ,
fields : { } ,
} ;
if ( selectedConnectorType === '.jira' ) {
connector . fields = {
issueType : this.getNodeParameter ( 'issueType' , i ) ,
priority : this.getNodeParameter ( 'priority' , i ) ,
parent : null , // required but unimplemented
} ;
} else if ( selectedConnectorType === '.servicenow' ) {
connector . fields = {
urgency : this.getNodeParameter ( 'urgency' , i ) ,
severity : this.getNodeParameter ( 'severity' , i ) ,
impact : this.getNodeParameter ( 'impact' , i ) ,
category : this.getNodeParameter ( 'category' , i ) ,
subcategory : null , // required but unimplemented
} ;
} else if ( selectedConnectorType === '.resilient' ) {
const rawIssueTypes = this . getNodeParameter ( 'issueTypes' , i ) as string ;
connector . fields = {
issueTypes : rawIssueTypes.split ( ',' ) . map ( Number ) ,
severityCode : this.getNodeParameter ( 'severityCode' , i ) as number ,
incidentTypes : null , // required but undocumented
} ;
}
body . connector = connector ;
const {
syncAlerts , // ignored because already set
. . . rest
} = this . getNodeParameter ( 'additionalFields' , i ) as IDataObject ;
if ( Object . keys ( rest ) . length ) {
Object . assign ( body , rest ) ;
}
responseData = await elasticSecurityApiRequest . call ( this , 'POST' , '/cases' , body ) ;
} else if ( operation === 'delete' ) {
// ----------------------------------------
// case: delete
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-delete-case.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
await elasticSecurityApiRequest . call ( this , 'DELETE' , ` /cases?ids=[" ${ caseId } "] ` ) ;
responseData = { success : true } ;
} else if ( operation === 'get' ) {
// ----------------------------------------
// case: get
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-get-case.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
responseData = await elasticSecurityApiRequest . call ( this , 'GET' , ` /cases/ ${ caseId } ` ) ;
} else if ( operation === 'getAll' ) {
// ----------------------------------------
// case: getAll
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-find-cases.html
const qs = { } as IDataObject ;
const {
tags ,
status ,
} = this . getNodeParameter ( 'filters' , i ) as IDataObject & { tags : string [ ] , status : string } ;
const sortOptions = this . getNodeParameter ( 'sortOptions' , i ) as IDataObject ;
qs . sortField = sortOptions . sortField ? ? 'createdAt' ;
qs . sortOrder = sortOptions . sortOrder ? ? 'asc' ;
if ( status ) {
qs . status = status ;
}
if ( tags ? . length ) {
qs . tags = tags . join ( ',' ) ;
}
responseData = await handleListing . call ( this , 'GET' , '/cases/_find' , { } , qs ) ;
} else if ( operation === 'getStatus' ) {
// ----------------------------------------
// case: getStatus
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-get-status.html
responseData = await elasticSecurityApiRequest . call ( this , 'GET' , '/cases/status' ) ;
} else if ( operation === 'update' ) {
// ----------------------------------------
// case: update
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-update.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const body = { } as IDataObject ;
const updateFields = this . getNodeParameter ( 'updateFields' , i ) as IDataObject ;
if ( ! Object . keys ( updateFields ) . length ) {
throwOnEmptyUpdate . call ( this , resource ) ;
}
const { syncAlerts , . . . rest } = updateFields ;
Object . assign ( body , {
cases : [
{
id : caseId ,
version : await getVersion . call ( this , ` /cases/ ${ caseId } ` ) ,
. . . ( syncAlerts && { settings : { syncAlerts } } ) ,
. . . rest ,
} ,
] ,
} ) ;
responseData = await elasticSecurityApiRequest . call ( this , 'PATCH' , '/cases' , body ) ;
}
} else if ( resource === 'caseTag' ) {
// **********************************************************************
// caseTag
// **********************************************************************
if ( operation === 'add' ) {
// ----------------------------------------
// caseTag: add
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-create.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const {
title ,
connector ,
owner ,
description ,
settings ,
tags ,
} = await elasticSecurityApiRequest . call ( this , 'GET' , ` /cases/ ${ caseId } ` ) ;
const tagToAdd = this . getNodeParameter ( 'tag' , i ) ;
if ( tags . includes ( tagToAdd ) ) {
throw new NodeOperationError (
this . getNode ( ) ,
` Cannot add tag " ${ tagToAdd } " to case ID ${ caseId } because this case already has this tag. ` ,
2022-07-12 08:51:01 -07:00
{ itemIndex : i } ,
2021-09-22 08:48:50 -07:00
) ;
}
const body = { } ;
Object . assign ( body , {
cases : [
{
id : caseId ,
title ,
connector ,
owner ,
description ,
settings ,
version : await getVersion . call ( this , ` /cases/ ${ caseId } ` ) ,
tags : [ . . . tags , tagToAdd ] ,
} ,
] ,
} ) ;
responseData = await elasticSecurityApiRequest . call ( this , 'PATCH' , '/cases' , body ) ;
} else if ( operation === 'remove' ) {
// https://www.elastic.co/guide/en/security/current/cases-api-update.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const tagToRemove = this . getNodeParameter ( 'tag' , i ) as string ;
const {
title ,
connector ,
owner ,
description ,
settings ,
tags ,
} = await elasticSecurityApiRequest . call ( this , 'GET' , ` /cases/ ${ caseId } ` ) as IDataObject & { tags : string [ ] } ;
if ( ! tags . includes ( tagToRemove ) ) {
2022-07-12 08:51:01 -07:00
throw new NodeOperationError ( this . getNode ( ) , ` Cannot remove tag " ${ tagToRemove } " from case ID ${ caseId } because this case does not have this tag. ` , { itemIndex : i } ) ;
2021-09-22 08:48:50 -07:00
}
const body = { } ;
Object . assign ( body , {
cases : [
{
id : caseId ,
title ,
connector ,
owner ,
description ,
settings ,
version : await getVersion . call ( this , ` /cases/ ${ caseId } ` ) ,
tags : tags.filter ( ( tag ) = > tag !== tagToRemove ) ,
} ,
] ,
} ) ;
responseData = await elasticSecurityApiRequest . call ( this , 'PATCH' , '/cases' , body ) ;
}
} else if ( resource === 'caseComment' ) {
// **********************************************************************
// caseComment
// **********************************************************************
if ( operation === 'add' ) {
// ----------------------------------------
// caseComment: add
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-add-comment.html
const simple = this . getNodeParameter ( 'simple' , i ) as boolean ;
const additionalFields = this . getNodeParameter ( 'additionalFields' , i ) as IDataObject ;
const body = {
comment : this.getNodeParameter ( 'comment' , i ) ,
type : 'user' ,
owner : additionalFields.owner || 'securitySolution' ,
} as IDataObject ;
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const endpoint = ` /cases/ ${ caseId } /comments ` ;
responseData = await elasticSecurityApiRequest . call ( this , 'POST' , endpoint , body ) ;
if ( simple === true ) {
const { comments } = responseData ;
responseData = comments [ comments . length - 1 ] ;
}
} else if ( operation === 'get' ) {
// ----------------------------------------
// caseComment: get
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-get-comment.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const commentId = this . getNodeParameter ( 'commentId' , i ) ;
const endpoint = ` /cases/ ${ caseId } /comments/ ${ commentId } ` ;
responseData = await elasticSecurityApiRequest . call ( this , 'GET' , endpoint ) ;
} else if ( operation === 'getAll' ) {
// ----------------------------------------
// caseComment: getAll
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-get-all-case-comments.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const endpoint = ` /cases/ ${ caseId } /comments ` ;
responseData = await handleListing . call ( this , 'GET' , endpoint ) ;
} else if ( operation === 'remove' ) {
// ----------------------------------------
// caseComment: remove
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-delete-comment.html
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const commentId = this . getNodeParameter ( 'commentId' , i ) ;
const endpoint = ` /cases/ ${ caseId } /comments/ ${ commentId } ` ;
await elasticSecurityApiRequest . call ( this , 'DELETE' , endpoint ) ;
responseData = { success : true } ;
} else if ( operation === 'update' ) {
// ----------------------------------------
// caseComment: update
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/cases-api-update-comment.html
const simple = this . getNodeParameter ( 'simple' , i ) as boolean ;
const caseId = this . getNodeParameter ( 'caseId' , i ) ;
const commentId = this . getNodeParameter ( 'commentId' , i ) ;
const body = {
comment : this.getNodeParameter ( 'comment' , i ) ,
id : commentId ,
type : 'user' ,
owner : 'securitySolution' ,
version : await getVersion . call ( this , ` /cases/ ${ caseId } /comments/ ${ commentId } ` ) ,
} as IDataObject ;
const patchEndpoint = ` /cases/ ${ caseId } /comments ` ;
responseData = await elasticSecurityApiRequest . call ( this , 'PATCH' , patchEndpoint , body ) ;
if ( simple === true ) {
const { comments } = responseData ;
responseData = comments [ comments . length - 1 ] ;
}
}
} else if ( resource === 'connector' ) {
if ( operation === 'create' ) {
// ----------------------------------------
// connector: create
// ----------------------------------------
// https://www.elastic.co/guide/en/security/current/register-connector.html
const connectorType = this . getNodeParameter ( 'connectorType' , i ) as ConnectorType ;
const body : ConnectorCreatePayload = {
connector_type_id : connectorType ,
name : this.getNodeParameter ( 'name' , i ) as string ,
} ;
if ( connectorType === '.jira' ) {
body . config = {
apiUrl : this.getNodeParameter ( 'apiUrl' , i ) as string ,
projectKey : this.getNodeParameter ( 'projectKey' , i ) as string ,
} ;
body . secrets = {
email : this.getNodeParameter ( 'email' , i ) as string ,
apiToken : this.getNodeParameter ( 'apiToken' , i ) as string ,
} ;
} else if ( connectorType === '.resilient' ) {
body . config = {
apiUrl : this.getNodeParameter ( 'apiUrl' , i ) as string ,
orgId : this.getNodeParameter ( 'orgId' , i ) as string ,
} ;
body . secrets = {
apiKeyId : this.getNodeParameter ( 'apiKeyId' , i ) as string ,
apiKeySecret : this.getNodeParameter ( 'apiKeySecret' , i ) as string ,
} ;
} else if ( connectorType === '.servicenow' ) {
body . config = {
apiUrl : this.getNodeParameter ( 'apiUrl' , i ) as string ,
} ;
body . secrets = {
username : this.getNodeParameter ( 'username' , i ) as string ,
password : this.getNodeParameter ( 'password' , i ) as string ,
} ;
}
responseData = await elasticSecurityApiRequest . call ( this , 'POST' , '/actions/connector' , body ) ;
}
}
Array . isArray ( responseData )
? returnData . push ( . . . responseData )
: returnData . push ( responseData ) ;
} catch ( error ) {
if ( this . continueOnFail ( ) ) {
returnData . push ( { error : error.message } ) ;
continue ;
}
throw error ;
}
}
return [ this . helpers . returnJsonArray ( returnData ) ] ;
}
}