From 77a05976ec6159a8335c71907eee1cf1f38c42b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Sat, 8 Jan 2022 10:53:10 +0100 Subject: [PATCH] :sparkles: Add MS Graph Security node (#2307) * :sparkles: Create MS Graph Security node * :zap: General update * :package: Update package-lock.json * :shirt: Fix lint * :fire: Remove Reviewed field * :zap: Set max limit to 1000 * :zap: Add limit to 1000 to second resource --- ...osoftGraphSecurityOAuth2Api.credentials.ts | 21 ++ .../GraphSecurity/GenericFunctions.ts | 88 +++++++ .../MicrosoftGraphSecurity.node.ts | 241 ++++++++++++++++++ .../SecureScoreControlProfileDescription.ts | 230 +++++++++++++++++ .../descriptions/SecureScoreDescription.ts | 132 ++++++++++ .../GraphSecurity/descriptions/index.ts | 2 + .../GraphSecurity/microsoftGraph.svg | 45 ++++ packages/nodes-base/package.json | 4 +- 8 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GraphSecurity/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreControlProfileDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GraphSecurity/microsoftGraph.svg diff --git a/packages/nodes-base/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.ts new file mode 100644 index 000000000..e356cf404 --- /dev/null +++ b/packages/nodes-base/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class MicrosoftGraphSecurityOAuth2Api implements ICredentialType { + name = 'microsoftGraphSecurityOAuth2Api'; + displayName = 'Microsoft Graph Security OAuth2 API'; + extends = [ + 'microsoftOAuth2Api', + ]; + documentationUrl = 'microsoft'; + properties: INodeProperties[] = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden', + default: 'SecurityEvents.ReadWrite.All', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/GraphSecurity/GenericFunctions.ts new file mode 100644 index 000000000..28bcb312a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/GenericFunctions.ts @@ -0,0 +1,88 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +export async function msGraphSecurityApiRequest( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + headers: IDataObject = {}, +) { + const { + oauthTokenData: { + access_token, // tslint:disable-line variable-name + }, + } = await this.getCredentials('microsoftGraphSecurityOAuth2Api') as { + oauthTokenData: { + access_token: string; + } + }; + + const options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${access_token}`, + }, + method, + body, + qs, + uri: `https://graph.microsoft.com/v1.0/security${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(headers).length) { + options.headers = { ...options.headers, ...headers }; + } + + try { + return await this.helpers.request(options); + } catch (error) { + const nestedMessage = error?.error?.error?.message; + + if (nestedMessage.startsWith('{"')) { + error = JSON.parse(nestedMessage); + } + + if (nestedMessage.startsWith('Http request failed with statusCode=BadRequest')) { + error.error.error.message = 'Request failed with bad request'; + } else if (nestedMessage.startsWith('Http request failed with')) { + const stringified = nestedMessage.split(': ').pop(); + if (stringified) { + error = JSON.parse(stringified); + } + } + + if (['Invalid filter clause', 'Invalid ODATA query filter'].includes(nestedMessage)) { + error.error.error.message += ' - Please check that your query parameter syntax is correct: https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter'; + } + + throw new NodeApiError(this.getNode(), error); + } +} + +export function tolerateDoubleQuotes(filterQueryParameter: string) { + return filterQueryParameter.replace(/"/g, `'`); +} + +export function throwOnEmptyUpdate(this: IExecuteFunctions) { + throw new NodeOperationError(this.getNode(), 'Please enter at least one field to update'); +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.ts b/packages/nodes-base/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.ts new file mode 100644 index 000000000..8238bdb51 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.ts @@ -0,0 +1,241 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + msGraphSecurityApiRequest, + throwOnEmptyUpdate, + tolerateDoubleQuotes, +} from './GenericFunctions'; + +import { + secureScoreControlProfileFields, + secureScoreControlProfileOperations, + secureScoreFields, + secureScoreOperations, +} from './descriptions'; + +export class MicrosoftGraphSecurity implements INodeType { + description: INodeTypeDescription = { + displayName: 'Microsoft Graph Security', + name: 'microsoftGraphSecurity', + icon: 'file:microsoftGraph.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Microsoft Graph Security API', + defaults: { + name: 'Microsoft Graph Security', + color: '#0078d4', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftGraphSecurityOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Secure Score', + value: 'secureScore', + }, + { + name: 'Secure Score Control Profile', + value: 'secureScoreControlProfile', + }, + ], + default: 'secureScore', + }, + ...secureScoreOperations, + ...secureScoreFields, + ...secureScoreControlProfileOperations, + ...secureScoreControlProfileFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as 'secureScore'| 'secureScoreControlProfile'; + const operation = this.getNodeParameter('operation', 0) as 'get' | 'getAll' | 'update'; + + let responseData; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'secureScore') { + + // ********************************************************************** + // secureScore + // ********************************************************************** + + if (operation === 'get') { + + // ---------------------------------------- + // secureScore: get + // ---------------------------------------- + + // https://docs.microsoft.com/en-us/graph/api/securescore-get + + const secureScoreId = this.getNodeParameter('secureScoreId', i); + + responseData = await msGraphSecurityApiRequest.call(this, 'GET', `/secureScores/${secureScoreId}`); + delete responseData['@odata.context']; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // secureScore: getAll + // ---------------------------------------- + + // https://docs.microsoft.com/en-us/graph/api/security-list-securescores + + const qs: IDataObject = {}; + + const { + filter, + includeControlScores, + } = this.getNodeParameter('filters', i) as { + filter?: string; + includeControlScores?: boolean; + }; + + if (filter) { + qs.$filter = tolerateDoubleQuotes(filter); + } + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (!returnAll) { + qs.$count = true; + qs.$top = this.getNodeParameter('limit', 0); + } + + responseData = await msGraphSecurityApiRequest + .call(this, 'GET', '/secureScores', {}, qs) + .then(response => response.value) as Array<{ controlScores: object[] }>; + + if (!includeControlScores) { + responseData = responseData.map(({ controlScores, ...rest }) => rest); + } + + } + + } else if (resource === 'secureScoreControlProfile') { + + // ********************************************************************** + // secureScoreControlProfile + // ********************************************************************** + + if (operation === 'get') { + + // ---------------------------------------- + // secureScoreControlProfile: get + // ---------------------------------------- + + // https://docs.microsoft.com/en-us/graph/api/securescorecontrolprofile-get + + const secureScoreControlProfileId = this.getNodeParameter('secureScoreControlProfileId', i); + const endpoint = `/secureScoreControlProfiles/${secureScoreControlProfileId}`; + + responseData = await msGraphSecurityApiRequest.call(this, 'GET', endpoint); + delete responseData['@odata.context']; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // secureScoreControlProfile: getAll + // ---------------------------------------- + + // https://docs.microsoft.com/en-us/graph/api/security-list-securescorecontrolprofiles + + const qs: IDataObject = {}; + + const { filter } = this.getNodeParameter('filters', i) as { filter?: string }; + + if (filter) { + qs.$filter = tolerateDoubleQuotes(filter); + } + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (!returnAll) { + qs.$count = true; + qs.$top = this.getNodeParameter('limit', 0); + } + + responseData = await msGraphSecurityApiRequest + .call(this, 'GET', '/secureScoreControlProfiles', {}, qs) + .then(response => response.value); + + } else if (operation === 'update') { + + // ---------------------------------------- + // secureScoreControlProfile: update + // ---------------------------------------- + + // https://docs.microsoft.com/en-us/graph/api/securescorecontrolprofile-update + + const body: IDataObject = { + vendorInformation: { + provider: this.getNodeParameter('provider', i), + vendor: this.getNodeParameter('vendor', i), + }, + }; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (!Object.keys(updateFields).length) { + throwOnEmptyUpdate.call(this); + } + + if (Object.keys(updateFields).length) { + Object.assign(body, updateFields); + } + + const id = this.getNodeParameter('secureScoreControlProfileId', i); + const endpoint = `/secureScoreControlProfiles/${id}`; + const headers = { Prefer: 'return=representation' }; + + responseData = await msGraphSecurityApiRequest.call(this, 'PATCH', endpoint, body, {}, headers); + delete responseData['@odata.context']; + + } + + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreControlProfileDescription.ts b/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreControlProfileDescription.ts new file mode 100644 index 000000000..4f3448d23 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreControlProfileDescription.ts @@ -0,0 +1,230 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const secureScoreControlProfileOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'get', + }, +]; + +export const secureScoreControlProfileFields: INodeProperties[] = [ + // ---------------------------------------- + // secureScore: get + // ---------------------------------------- + { + displayName: 'Secure Score Control Profile ID', + name: 'secureScoreControlProfileId', + description: 'ID of the secure score control profile to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // secureScoreControlProfile: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + default: {}, + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Filter Query Parameter', + name: 'filter', + description: 'Query parameter to filter results by', + type: 'string', + default: '', + placeholder: 'startsWith(id, \'AATP\')', + }, + ], + }, + + // ---------------------------------------- + // secureScoreControlProfile: update + // ---------------------------------------- + { + displayName: 'Secure Score Control Profile ID', + name: 'secureScoreControlProfileId', + description: 'ID of the secure score control profile to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Provider', + name: 'provider', + type: 'string', + description: 'Name of the provider of the security product or service', + default: '', + placeholder: 'SecureScore', + required: true, + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Vendor', + name: 'vendor', + type: 'string', + description: 'Name of the vendor of the security product or service', + default: '', + placeholder: 'Microsoft', + required: true, + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'secureScoreControlProfile', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'State', + name: 'state', + type: 'options', + default: 'Default', + description: 'Analyst driven setting on the control', + options: [ + { + name: 'Default', + value: 'Default', + }, + { + name: 'Ignored', + value: 'Ignored', + }, + { + name: 'Third Party', + value: 'ThirdParty', + }, + ], + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreDescription.ts b/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreDescription.ts new file mode 100644 index 000000000..6c09bee12 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/SecureScoreDescription.ts @@ -0,0 +1,132 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const secureScoreOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'secureScore', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'get', + }, +]; + +export const secureScoreFields: INodeProperties[] = [ + // ---------------------------------------- + // secureScore: get + // ---------------------------------------- + { + displayName: 'Secure Score ID', + name: 'secureScoreId', + description: 'ID of the secure score to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'secureScore', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // secureScore: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'secureScore', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'secureScore', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + default: {}, + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'secureScore', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Filter Query Parameter', + name: 'filter', + description: 'Query parameter to filter results by', + type: 'string', + default: '', + placeholder: 'currentScore eq 13', + }, + { + displayName: 'Include Control Scores', + name: 'includeControlScores', + type: 'boolean', + default: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/index.ts b/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/index.ts new file mode 100644 index 000000000..99fbf69d1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/descriptions/index.ts @@ -0,0 +1,2 @@ +export * from './SecureScoreDescription'; +export * from './SecureScoreControlProfileDescription'; diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/microsoftGraph.svg b/packages/nodes-base/nodes/Microsoft/GraphSecurity/microsoftGraph.svg new file mode 100644 index 000000000..a490f454d --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/microsoftGraph.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e2d4f9df7..25bb7deec 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -178,6 +178,7 @@ "dist/credentials/MessageBirdApi.credentials.js", "dist/credentials/MicrosoftDynamicsOAuth2Api.credentials.js", "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", + "dist/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.js", "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", "dist/credentials/MicrosoftOutlookOAuth2Api.credentials.js", @@ -505,6 +506,7 @@ "dist/nodes/MessageBird/MessageBird.node.js", "dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", + "dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", "dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js", "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js", @@ -765,4 +767,4 @@ "json" ] } -} +} \ No newline at end of file