From 4637b744d80686a2f9cbdfaf2f8b24a0c92f79c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 9 Aug 2023 20:02:11 +0200 Subject: [PATCH] Add SentinelOne connector (#159157) ## Summary Adds new connector type to support https://www.sentinelone.com/ The scope of this PR was limited to the Connector logic, schemas, and types to make PR more digestible. In the current PR, the connector is NOT registered, so it's not going to be available to the users. In the follow-up PR I'm going to improve the UX of Param's form and then enable the connector Zrzut ekranu 2023-08-3 o 11 18 54 visual changes include a screenshot or gif. image image --- .github/CODEOWNERS | 5 + packages/kbn-optimizer/limits.yml | 2 +- .../event_details/flyout/use_sub_action.tsx | 43 ++ .../flyout/use_sub_action_mutation.tsx | 38 ++ .../common/sentinelone/constants.ts | 21 + .../common/sentinelone/schema.ts | 495 ++++++++++++++++++ .../common/sentinelone/types.ts | 50 ++ .../connector_types/sentinelone/index.ts | 8 + .../connector_types/sentinelone/logo.tsx | 84 +++ .../sentinelone/sentinelone.ts | 64 +++ .../sentinelone/sentinelone_connector.tsx | 47 ++ .../sentinelone/sentinelone_params.tsx | 333 ++++++++++++ .../sentinelone/translations.ts | 270 ++++++++++ .../connector_types/sentinelone/types.ts | 23 + .../connector_types/sentinelone/index.ts | 38 ++ .../connector_types/sentinelone/render.ts | 79 +++ .../sentinelone/sentinelone.ts | 276 ++++++++++ x-pack/plugins/stack_connectors/tsconfig.json | 1 + .../server/queries/oldest_idle_action_task.ts | 1 + .../server/saved_objects/index.ts | 1 + .../text_area_with_message_variables.tsx | 3 + .../public/application/lib/index.ts | 2 +- .../triggers_actions_ui/public/index.ts | 1 + 23 files changed, 1883 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx create mode 100644 x-pack/plugins/stack_connectors/common/sentinelone/constants.ts create mode 100644 x-pack/plugins/stack_connectors/common/sentinelone/schema.ts create mode 100644 x-pack/plugins/stack_connectors/common/sentinelone/types.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/index.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts create mode 100644 x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts create mode 100644 x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts create mode 100644 x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 06e0d2478429f..0ba3f7fdcc32a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1146,6 +1146,11 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/plugins/stack_connectors/server/connector_types/gen_ai @elastic/security-threat-hunting-explore /x-pack/plugins/stack_connectors/common/gen_ai @elastic/security-threat-hunting-explore +## Defend Workflows owner connectors +/x-pack/plugins/stack_connectors/public/connector_types/sentinelone @elastic/security-defend-workflows +/x-pack/plugins/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows +/x-pack/plugins/stack_connectors/common/sentinelone @elastic/security-defend-workflows + ## Security Solution sub teams - Detection Rule Management /x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine /x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations @elastic/security-detection-rule-management diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index b08ff89b5dd09..d63b19f67f783 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -129,7 +129,7 @@ pageLoadAssetSize: snapshotRestore: 79032 spaces: 57868 stackAlerts: 58316 - stackConnectors: 36314 + stackConnectors: 52131 synthetics: 40958 telemetry: 51957 telemetryManagementSection: 38586 diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.tsx new file mode 100644 index 0000000000000..44f9099a5b816 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { executeAction } from '@kbn/triggers-actions-ui-plugin/public'; +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../../../../../common/lib/kibana/kibana_react'; + +export interface UseSubActionParams

{ + connectorId: string; + subAction: string; + subActionParams?: P; + disabled?: boolean; +} + +export const useSubAction = ({ + connectorId, + subAction, + subActionParams, + disabled = false, + ...rest +}: UseSubActionParams

) => { + const { http } = useKibana().services; + + return useQuery({ + queryKey: ['useSubAction', connectorId, subAction, subActionParams], + queryFn: ({ signal }) => + executeAction({ + id: connectorId, + params: { + subAction, + subActionParams, + }, + http, + signal, + }), + enabled: !disabled && !!connectorId && !!subAction, + ...rest, + }); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx new file mode 100644 index 0000000000000..78c48ccca1491 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { executeAction } from '@kbn/triggers-actions-ui-plugin/public'; +import { useMutation } from '@tanstack/react-query'; +import { useKibana } from '../../../../../common/lib/kibana/kibana_react'; + +export interface UseSubActionParams

{ + connectorId: string; + subAction: string; + subActionParams?: P; + disabled?: boolean; +} + +export const useSubActionMutation = ({ + connectorId, + subAction, + subActionParams, + disabled = false, +}: UseSubActionParams

) => { + const { http } = useKibana().services; + + return useMutation({ + mutationFn: () => + executeAction({ + id: connectorId, + params: { + subAction, + subActionParams, + }, + http, + }), + }); +}; diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/constants.ts b/x-pack/plugins/stack_connectors/common/sentinelone/constants.ts new file mode 100644 index 0000000000000..a77e070a71056 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/sentinelone/constants.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SENTINELONE_TITLE = 'Sentinel One'; +export const SENTINELONE_CONNECTOR_ID = '.sentinelone'; +export const API_MAX_RESULTS = 1000; + +export enum SUB_ACTION { + KILL_PROCESS = 'killProcess', + EXECUTE_SCRIPT = 'executeScript', + GET_AGENTS = 'getAgents', + ISOLATE_AGENT = 'isolateAgent', + RELEASE_AGENT = 'releaseAgent', + GET_REMOTE_SCRIPTS = 'getRemoteScripts', + GET_REMOTE_SCRIPT_STATUS = 'getRemoteScriptStatus', + GET_REMOTE_SCRIPT_RESULTS = 'getRemoteScriptResults', +} diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts new file mode 100644 index 0000000000000..f475a9e6a83f6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts @@ -0,0 +1,495 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { schema } from '@kbn/config-schema'; +import { SUB_ACTION } from './constants'; + +// Connector schema +export const SentinelOneConfigSchema = schema.object({ url: schema.string() }); +export const SentinelOneSecretsSchema = schema.object({ + token: schema.string(), +}); + +export const SentinelOneBaseApiResponseSchema = schema.object({}, { unknowns: 'allow' }); + +export const SentinelOneGetAgentsResponseSchema = schema.object({ + pagination: schema.object({ + totalItems: schema.number(), + nextCursor: schema.nullable(schema.string()), + }), + errors: schema.nullable(schema.arrayOf(schema.string())), + data: schema.arrayOf( + schema.object({ + modelName: schema.string(), + firewallEnabled: schema.boolean(), + totalMemory: schema.number(), + osName: schema.string(), + cloudProviders: schema.recordOf(schema.string(), schema.any()), + siteName: schema.string(), + cpuId: schema.string(), + isPendingUninstall: schema.boolean(), + isUpToDate: schema.boolean(), + osArch: schema.string(), + accountId: schema.string(), + locationEnabled: schema.boolean(), + consoleMigrationStatus: schema.string(), + scanFinishedAt: schema.nullable(schema.string()), + operationalStateExpiration: schema.nullable(schema.string()), + agentVersion: schema.string(), + isActive: schema.boolean(), + locationType: schema.string(), + activeThreats: schema.number(), + inRemoteShellSession: schema.boolean(), + allowRemoteShell: schema.boolean(), + serialNumber: schema.nullable(schema.string()), + updatedAt: schema.string(), + lastActiveDate: schema.string(), + firstFullModeTime: schema.nullable(schema.string()), + operationalState: schema.string(), + externalId: schema.string(), + mitigationModeSuspicious: schema.string(), + licenseKey: schema.string(), + cpuCount: schema.number(), + mitigationMode: schema.string(), + networkStatus: schema.string(), + installerType: schema.string(), + uuid: schema.string(), + detectionState: schema.nullable(schema.string()), + infected: schema.boolean(), + registeredAt: schema.string(), + lastIpToMgmt: schema.string(), + storageName: schema.nullable(schema.string()), + osUsername: schema.string(), + groupIp: schema.string(), + createdAt: schema.string(), + remoteProfilingState: schema.string(), + groupUpdatedAt: schema.nullable(schema.string()), + scanAbortedAt: schema.nullable(schema.string()), + isUninstalled: schema.boolean(), + networkQuarantineEnabled: schema.boolean(), + tags: schema.object({ + sentinelone: schema.arrayOf( + schema.object({ + assignedBy: schema.string(), + assignedAt: schema.string(), + assignedById: schema.string(), + key: schema.string(), + value: schema.string(), + id: schema.string(), + }) + ), + }), + externalIp: schema.string(), + siteId: schema.string(), + machineType: schema.string(), + domain: schema.string(), + scanStatus: schema.string(), + osStartTime: schema.string(), + accountName: schema.string(), + lastLoggedInUserName: schema.string(), + showAlertIcon: schema.boolean(), + rangerStatus: schema.string(), + groupName: schema.string(), + threatRebootRequired: schema.boolean(), + remoteProfilingStateExpiration: schema.nullable(schema.string()), + policyUpdatedAt: schema.nullable(schema.string()), + activeDirectory: schema.object({ + userPrincipalName: schema.nullable(schema.string()), + lastUserDistinguishedName: schema.nullable(schema.string()), + computerMemberOf: schema.arrayOf(schema.object({ type: schema.string() })), + lastUserMemberOf: schema.arrayOf(schema.object({ type: schema.string() })), + mail: schema.nullable(schema.string()), + computerDistinguishedName: schema.nullable(schema.string()), + }), + isDecommissioned: schema.boolean(), + rangerVersion: schema.string(), + userActionsNeeded: schema.arrayOf( + schema.object({ + type: schema.string(), + example: schema.string(), + enum: schema.arrayOf(schema.string()), + }) + ), + locations: schema.nullable( + schema.arrayOf( + schema.object({ name: schema.string(), scope: schema.string(), id: schema.string() }) + ) + ), + id: schema.string(), + coreCount: schema.number(), + osRevision: schema.string(), + osType: schema.string(), + groupId: schema.string(), + computerName: schema.string(), + scanStartedAt: schema.string(), + encryptedApplications: schema.boolean(), + storageType: schema.nullable(schema.string()), + networkInterfaces: schema.arrayOf( + schema.object({ + gatewayMacAddress: schema.nullable(schema.string()), + inet6: schema.arrayOf(schema.string()), + name: schema.string(), + inet: schema.arrayOf(schema.string()), + physical: schema.string(), + gatewayIp: schema.nullable(schema.string()), + id: schema.string(), + }) + ), + fullDiskScanLastUpdatedAt: schema.string(), + appsVulnerabilityStatus: schema.string(), + }) + ), +}); + +export const SentinelOneIsolateAgentResponseSchema = schema.object({ + errors: schema.nullable(schema.arrayOf(schema.string())), + data: schema.object({ + affected: schema.number(), + }), +}); + +export const SentinelOneGetRemoteScriptsParamsSchema = schema.object({ + query: schema.nullable(schema.string()), + osTypes: schema.nullable(schema.arrayOf(schema.string())), +}); + +export const SentinelOneGetRemoteScriptsResponseSchema = schema.object({ + errors: schema.nullable(schema.arrayOf(schema.string())), + pagination: schema.object({ + nextCursor: schema.nullable(schema.string()), + totalItems: schema.number(), + }), + data: schema.arrayOf( + schema.object({ + id: schema.string(), + updater: schema.nullable(schema.string()), + isAvailableForLite: schema.boolean(), + isAvailableForArs: schema.boolean(), + fileSize: schema.number(), + mgmtId: schema.number(), + scopeLevel: schema.string(), + shortFileName: schema.string(), + scriptName: schema.string(), + creator: schema.string(), + package: schema.nullable( + schema.object({ + id: schema.string(), + bucketName: schema.string(), + endpointExpiration: schema.string(), + fileName: schema.string(), + endpointExpirationSeconds: schema.nullable(schema.number()), + fileSize: schema.number(), + signatureType: schema.string(), + signature: schema.string(), + }) + ), + bucketName: schema.string(), + inputRequired: schema.boolean(), + fileName: schema.string(), + supportedDestinations: schema.nullable(schema.arrayOf(schema.string())), + scopeName: schema.nullable(schema.string()), + signatureType: schema.string(), + outputFilePaths: schema.nullable(schema.arrayOf(schema.string())), + scriptDescription: schema.nullable(schema.string()), + createdByUserId: schema.string(), + scopeId: schema.string(), + updatedAt: schema.string(), + scriptType: schema.string(), + scopePath: schema.string(), + creatorId: schema.string(), + osTypes: schema.arrayOf(schema.string()), + scriptRuntimeTimeoutSeconds: schema.number(), + version: schema.string(), + updaterId: schema.nullable(schema.string()), + createdAt: schema.string(), + inputExample: schema.nullable(schema.string()), + inputInstructions: schema.nullable(schema.string()), + signature: schema.string(), + createdByUser: schema.string(), + requiresApproval: schema.maybe(schema.boolean()), + }) + ), +}); + +export const SentinelOneExecuteScriptParamsSchema = schema.object({ + computerName: schema.maybe(schema.string()), + script: schema.object({ + scriptId: schema.string(), + scriptName: schema.maybe(schema.string()), + apiKey: schema.maybe(schema.string()), + outputDirectory: schema.maybe(schema.string()), + requiresApproval: schema.maybe(schema.boolean()), + taskDescription: schema.maybe(schema.string()), + singularityxdrUrl: schema.maybe(schema.string()), + inputParams: schema.maybe(schema.string()), + singularityxdrKeyword: schema.maybe(schema.string()), + scriptRuntimeTimeoutSeconds: schema.maybe(schema.number()), + passwordFromScope: schema.maybe( + schema.object({ + scopeLevel: schema.maybe(schema.string()), + scopeId: schema.maybe(schema.string()), + }) + ), + password: schema.maybe(schema.string()), + }), +}); + +export const SentinelOneGetRemoteScriptStatusParamsSchema = schema.object( + { + parentTaskId: schema.string(), + }, + { unknowns: 'allow' } +); + +export const SentinelOneGetRemoteScriptStatusResponseSchema = schema.object({ + pagination: schema.object({ + totalItems: schema.number(), + nextCursor: schema.nullable(schema.string()), + }), + errors: schema.arrayOf(schema.object({ type: schema.string() })), + data: schema.arrayOf( + schema.object({ + agentIsDecommissioned: schema.boolean(), + agentComputerName: schema.string(), + status: schema.string(), + groupName: schema.string(), + initiatedById: schema.string(), + parentTaskId: schema.string(), + updatedAt: schema.string(), + createdAt: schema.string(), + agentIsActive: schema.boolean(), + agentOsType: schema.string(), + agentMachineType: schema.string(), + id: schema.string(), + siteName: schema.string(), + detailedStatus: schema.string(), + siteId: schema.string(), + scriptResultsSignature: schema.nullable(schema.string()), + initiatedBy: schema.string(), + accountName: schema.string(), + groupId: schema.string(), + statusDescription: schema.object({ + readOnly: schema.boolean(), + description: schema.string(), + }), + agentUuid: schema.string(), + accountId: schema.string(), + type: schema.string(), + scriptResultsPath: schema.string(), + scriptResultsBucket: schema.string(), + description: schema.string(), + agentId: schema.string(), + }) + ), +}); + +export const SentinelOneBaseFilterSchema = schema.object({ + K8SNodeName__contains: schema.nullable(schema.string()), + coreCount__lt: schema.nullable(schema.string()), + rangerStatuses: schema.nullable(schema.string()), + adUserQuery__contains: schema.nullable(schema.string()), + rangerVersionsNin: schema.nullable(schema.string()), + rangerStatusesNin: schema.nullable(schema.string()), + coreCount__gte: schema.nullable(schema.string()), + threatCreatedAt__gte: schema.nullable(schema.string()), + decommissionedAt__lte: schema.nullable(schema.string()), + operationalStatesNin: schema.nullable(schema.string()), + appsVulnerabilityStatusesNin: schema.nullable(schema.string()), + mitigationMode: schema.nullable(schema.string()), + createdAt__gte: schema.nullable(schema.string()), + gatewayIp: schema.nullable(schema.string()), + cloudImage__contains: schema.nullable(schema.string()), + registeredAt__between: schema.nullable(schema.string()), + threatMitigationStatus: schema.nullable(schema.string()), + installerTypesNin: schema.nullable(schema.string()), + appsVulnerabilityStatuses: schema.nullable(schema.string()), + threatResolved: schema.nullable(schema.string()), + mitigationModeSuspicious: schema.nullable(schema.string()), + isUpToDate: schema.nullable(schema.string()), + adComputerQuery__contains: schema.nullable(schema.string()), + updatedAt__gte: schema.nullable(schema.string()), + azureResourceGroup__contains: schema.nullable(schema.string()), + scanStatus: schema.nullable(schema.string()), + threatContentHash: schema.nullable(schema.string()), + osTypesNin: schema.nullable(schema.string()), + threatRebootRequired: schema.nullable(schema.string()), + totalMemory__between: schema.nullable(schema.string()), + firewallEnabled: schema.nullable(schema.string()), + gcpServiceAccount__contains: schema.nullable(schema.string()), + updatedAt__gt: schema.nullable(schema.string()), + remoteProfilingStates: schema.nullable(schema.string()), + filteredGroupIds: schema.nullable(schema.string()), + agentVersions: schema.nullable(schema.string()), + activeThreats: schema.nullable(schema.string()), + machineTypesNin: schema.nullable(schema.string()), + lastActiveDate__gt: schema.nullable(schema.string()), + awsSubnetIds__contains: schema.nullable(schema.string()), + installerTypes: schema.nullable(schema.string()), + registeredAt__gte: schema.nullable(schema.string()), + migrationStatus: schema.nullable(schema.string()), + cloudTags__contains: schema.nullable(schema.string()), + totalMemory__gte: schema.nullable(schema.string()), + decommissionedAt__lt: schema.nullable(schema.string()), + threatCreatedAt__lt: schema.nullable(schema.string()), + updatedAt__lte: schema.nullable(schema.string()), + osArch: schema.nullable(schema.string()), + registeredAt__gt: schema.nullable(schema.string()), + registeredAt__lt: schema.nullable(schema.string()), + siteIds: schema.nullable(schema.string()), + networkInterfaceInet__contains: schema.nullable(schema.string()), + groupIds: schema.nullable(schema.string()), + uuids: schema.nullable(schema.string()), + accountIds: schema.nullable(schema.string()), + scanStatusesNin: schema.nullable(schema.string()), + cpuCount__lte: schema.nullable(schema.string()), + locationIds: schema.nullable(schema.string()), + awsSecurityGroups__contains: schema.nullable(schema.string()), + networkStatusesNin: schema.nullable(schema.string()), + activeThreats__gt: schema.nullable(schema.string()), + infected: schema.nullable(schema.string()), + osVersion__contains: schema.nullable(schema.string()), + machineTypes: schema.nullable(schema.string()), + agentPodName__contains: schema.nullable(schema.string()), + computerName__like: schema.nullable(schema.string()), + threatCreatedAt__gt: schema.nullable(schema.string()), + consoleMigrationStatusesNin: schema.nullable(schema.string()), + computerName: schema.nullable(schema.string()), + decommissionedAt__between: schema.nullable(schema.string()), + cloudInstanceId__contains: schema.nullable(schema.string()), + createdAt__lte: schema.nullable(schema.string()), + coreCount__between: schema.nullable(schema.string()), + totalMemory__lte: schema.nullable(schema.string()), + remoteProfilingStatesNin: schema.nullable(schema.string()), + adComputerMember__contains: schema.nullable(schema.string()), + threatCreatedAt__between: schema.nullable(schema.string()), + totalMemory__gt: schema.nullable(schema.string()), + ids: schema.nullable(schema.string()), + agentVersionsNin: schema.nullable(schema.string()), + updatedAt__between: schema.nullable(schema.string()), + locationEnabled: schema.nullable(schema.string()), + locationIdsNin: schema.nullable(schema.string()), + osTypes: schema.nullable(schema.string()), + encryptedApplications: schema.nullable(schema.string()), + filterId: schema.nullable(schema.string()), + decommissionedAt__gt: schema.nullable(schema.string()), + adUserMember__contains: schema.nullable(schema.string()), + uuid: schema.nullable(schema.string()), + coreCount__lte: schema.nullable(schema.string()), + coreCount__gt: schema.nullable(schema.string()), + cloudNetwork__contains: schema.nullable(schema.string()), + clusterName__contains: schema.nullable(schema.string()), + cpuCount__gte: schema.nullable(schema.string()), + query: schema.nullable(schema.string()), + lastActiveDate__between: schema.nullable(schema.string()), + rangerStatus: schema.nullable(schema.string()), + domains: schema.nullable(schema.string()), + cloudProvider: schema.nullable(schema.string()), + lastActiveDate__lt: schema.nullable(schema.string()), + scanStatuses: schema.nullable(schema.string()), + hasLocalConfiguration: schema.nullable(schema.string()), + networkStatuses: schema.nullable(schema.string()), + isPendingUninstall: schema.nullable(schema.string()), + createdAt__gt: schema.nullable(schema.string()), + cpuCount__lt: schema.nullable(schema.string()), + consoleMigrationStatuses: schema.nullable(schema.string()), + adQuery: schema.nullable(schema.string()), + updatedAt__lt: schema.nullable(schema.string()), + createdAt__lt: schema.nullable(schema.string()), + adComputerName__contains: schema.nullable(schema.string()), + cloudInstanceSize__contains: schema.nullable(schema.string()), + registeredAt__lte: schema.nullable(schema.string()), + networkQuarantineEnabled: schema.nullable(schema.string()), + cloudAccount__contains: schema.nullable(schema.string()), + cloudLocation__contains: schema.nullable(schema.string()), + rangerVersions: schema.nullable(schema.string()), + networkInterfaceGatewayMacAddress__contains: schema.nullable(schema.string()), + uuid__contains: schema.nullable(schema.string()), + agentNamespace__contains: schema.nullable(schema.string()), + K8SNodeLabels__contains: schema.nullable(schema.string()), + adQuery__contains: schema.nullable(schema.string()), + K8SType__contains: schema.nullable(schema.string()), + countsFor: schema.nullable(schema.string()), + totalMemory__lt: schema.nullable(schema.string()), + externalId__contains: schema.nullable(schema.string()), + filteredSiteIds: schema.nullable(schema.string()), + decommissionedAt__gte: schema.nullable(schema.string()), + cpuCount__gt: schema.nullable(schema.string()), + threatHidden: schema.nullable(schema.string()), + isUninstalled: schema.nullable(schema.string()), + computerName__contains: schema.nullable(schema.string()), + lastActiveDate__lte: schema.nullable(schema.string()), + adUserName__contains: schema.nullable(schema.string()), + isActive: schema.nullable(schema.string()), + userActionsNeeded: schema.nullable(schema.string()), + threatCreatedAt__lte: schema.nullable(schema.string()), + domainsNin: schema.nullable(schema.string()), + operationalStates: schema.nullable(schema.string()), + externalIp__contains: schema.nullable(schema.string()), + isDecommissioned: schema.nullable(schema.string()), + networkInterfacePhysical__contains: schema.nullable(schema.string()), + lastActiveDate__gte: schema.nullable(schema.string()), + createdAt__between: schema.nullable(schema.string()), + cpuCount__between: schema.nullable(schema.string()), + lastLoggedInUserName__contains: schema.nullable(schema.string()), + awsRole__contains: schema.nullable(schema.string()), + K8SVersion__contains: schema.nullable(schema.string()), +}); + +export const SentinelOneKillProcessParamsSchema = SentinelOneBaseFilterSchema.extends({ + processName: schema.string(), +}); + +export const SentinelOneIsolateAgentParamsSchema = SentinelOneBaseFilterSchema; + +export const SentinelOneGetAgentsParamsSchema = SentinelOneBaseFilterSchema; + +export const SentinelOneGetRemoteScriptsStatusParams = schema.object({ + parentTaskId: schema.string(), +}); + +export const SentinelOneExecuteScriptResponseSchema = schema.object({ + errors: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + data: schema.nullable( + schema.object({ + pendingExecutionId: schema.nullable(schema.string()), + affected: schema.nullable(schema.number()), + parentTaskId: schema.nullable(schema.string()), + pending: schema.nullable(schema.boolean()), + }) + ), +}); + +export const SentinelOneKillProcessResponseSchema = SentinelOneExecuteScriptResponseSchema; + +export const SentinelOneKillProcessSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.KILL_PROCESS), + subActionParams: SentinelOneKillProcessParamsSchema, +}); + +export const SentinelOneIsolateAgentSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.ISOLATE_AGENT), + subActionParams: SentinelOneIsolateAgentParamsSchema, +}); + +export const SentinelOneReleaseAgentSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.RELEASE_AGENT), + subActionParams: SentinelOneIsolateAgentParamsSchema, +}); + +export const SentinelOneExecuteScriptSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.EXECUTE_SCRIPT), + subActionParams: SentinelOneExecuteScriptParamsSchema, +}); + +export const SentinelOneActionParamsSchema = schema.oneOf([ + SentinelOneKillProcessSchema, + SentinelOneIsolateAgentSchema, + SentinelOneReleaseAgentSchema, + SentinelOneExecuteScriptSchema, +]); diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/types.ts b/x-pack/plugins/stack_connectors/common/sentinelone/types.ts new file mode 100644 index 0000000000000..ab50e316d03f7 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/sentinelone/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { + SentinelOneBaseApiResponseSchema, + SentinelOneConfigSchema, + SentinelOneExecuteScriptParamsSchema, + SentinelOneGetAgentsParamsSchema, + SentinelOneGetAgentsResponseSchema, + SentinelOneGetRemoteScriptsParamsSchema, + SentinelOneGetRemoteScriptsResponseSchema, + SentinelOneGetRemoteScriptsStatusParams, + SentinelOneIsolateAgentParamsSchema, + SentinelOneKillProcessParamsSchema, + SentinelOneSecretsSchema, + SentinelOneActionParamsSchema, +} from './schema'; + +export type SentinelOneConfig = TypeOf; +export type SentinelOneSecrets = TypeOf; + +export type SentinelOneBaseApiResponse = TypeOf; + +export type SentinelOneGetAgentsParams = TypeOf; +export type SentinelOneGetAgentsResponse = TypeOf; + +export type SentinelOneKillProcessParams = TypeOf; + +export type SentinelOneExecuteScriptParams = TypeOf; + +export type SentinelOneGetRemoteScriptStatusParams = TypeOf< + typeof SentinelOneGetRemoteScriptsStatusParams +>; + +export type SentinelOneGetRemoteScriptsParams = TypeOf< + typeof SentinelOneGetRemoteScriptsParamsSchema +>; + +export type SentinelOneGetRemoteScriptsResponse = TypeOf< + typeof SentinelOneGetRemoteScriptsResponseSchema +>; + +export type SentinelOneIsolateAgentParams = TypeOf; + +export type SentinelOneActionParams = TypeOf; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/index.ts new file mode 100644 index 0000000000000..7bb5159f87525 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getConnectorType as getSentinelOneConnectorType } from './sentinelone'; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx new file mode 100644 index 0000000000000..656e75d07d67c --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +const Logo = () => ( + + + + + + + + + + + + + + + + + + + + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts new file mode 100644 index 0000000000000..469613621bf05 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { + ActionTypeModel as ConnectorTypeModel, + GenericValidationResult, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { + SENTINELONE_CONNECTOR_ID, + SENTINELONE_TITLE, + SUB_ACTION, +} from '../../../common/sentinelone/constants'; +import type { + SentinelOneConfig, + SentinelOneSecrets, + SentinelOneActionParams, +} from '../../../common/sentinelone/types'; + +interface ValidationErrors { + subAction: string[]; +} + +export function getConnectorType(): ConnectorTypeModel< + SentinelOneConfig, + SentinelOneSecrets, + SentinelOneActionParams +> { + return { + id: SENTINELONE_CONNECTOR_ID, + actionTypeTitle: SENTINELONE_TITLE, + iconClass: lazy(() => import('./logo')), + selectMessage: i18n.translate( + 'xpack.stackConnectors.security.sentinelone.config.selectMessageText', + { + defaultMessage: 'Execute SentinelOne scripts', + } + ), + validateParams: async ( + actionParams: SentinelOneActionParams + ): Promise> => { + const translations = await import('./translations'); + const errors: ValidationErrors = { + subAction: [], + }; + const { subAction } = actionParams; + + // The internal "subAction" param should always be valid, ensure it is only if "subActionParams" are valid + if (!subAction) { + errors.subAction.push(translations.ACTION_REQUIRED); + } else if (!(subAction in SUB_ACTION)) { + errors.subAction.push(translations.INVALID_ACTION); + } + return { errors }; + }, + actionConnectorFields: lazy(() => import('./sentinelone_connector')), + actionParamsFields: lazy(() => import('./sentinelone_params')), + }; +} diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.tsx new file mode 100644 index 0000000000000..785dc5d05832d --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + ActionConnectorFieldsProps, + ConfigFieldSchema, + SecretsFieldSchema, + SimpleConnectorForm, +} from '@kbn/triggers-actions-ui-plugin/public'; +import * as i18n from './translations'; + +const configFormSchema: ConfigFieldSchema[] = [ + { + id: 'url', + label: i18n.URL_LABEL, + isUrlField: true, + }, +]; + +const secretsFormSchema: SecretsFieldSchema[] = [ + { + id: 'token', + label: i18n.TOKEN_LABEL, + isPasswordField: true, + }, +]; + +const SentinelOneActionConnectorFields: React.FunctionComponent = ({ + readOnly, + isEdit, +}) => ( + +); + +// eslint-disable-next-line import/no-default-export +export { SentinelOneActionConnectorFields as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx new file mode 100644 index 0000000000000..f74e1c897b97b --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx @@ -0,0 +1,333 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState, ReactNode } from 'react'; +import { reduce } from 'lodash'; +import { + EuiButtonIcon, + EuiComboBox, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiInMemoryTable, + EuiSuperSelect, +} from '@elastic/eui'; +import { + ActionConnectorMode, + ActionParamsProps, + TextAreaWithMessageVariables, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { useSubAction, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import { EuiBasicTableColumn, EuiSearchBarProps, EuiLink } from '@elastic/eui'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; +import type { + SentinelOneGetAgentsParams, + SentinelOneGetAgentsResponse, + SentinelOneGetRemoteScriptsParams, + SentinelOneGetRemoteScriptsResponse, + SentinelOneActionParams, +} from '../../../common/sentinelone/types'; +import type { SentinelOneExecuteSubActionParams } from './types'; +import * as i18n from './translations'; + +type ScriptOption = SentinelOneGetRemoteScriptsResponse['data'][0]; + +const SentinelOneParamsFields: React.FunctionComponent< + ActionParamsProps +> = ({ actionConnector, actionParams, editAction, index, executionMode, errors, ...rest }) => { + const { toasts } = useKibana().notifications; + const { subAction, subActionParams } = actionParams; + const [selectedScript, setSelectedScript] = useState(); + + const [selectedAgent, setSelectedAgent] = useState>(() => { + if (subActionParams?.computerName) { + return [{ label: subActionParams?.computerName }]; + } + return []; + }); + const [connectorId] = useState(actionConnector?.id); + + const isTest = useMemo(() => executionMode === ActionConnectorMode.Test, [executionMode]); + + const editSubActionParams = useCallback( + (params: Partial) => { + editAction('subActionParams', { ...subActionParams, ...params }, index); + }, + [editAction, index, subActionParams] + ); + + const { + response: { data: agents } = {}, + isLoading: isLoadingAgents, + error: agentsError, + } = useSubAction({ + connectorId, + subAction: SUB_ACTION.GET_AGENTS, + disabled: isTest, + }); + + const agentOptions = useMemo( + () => + reduce( + agents, + (acc, item) => { + acc.push({ + label: item.computerName, + }); + return acc; + }, + [] as Array<{ label: string }> + ), + [agents] + ); + + const { + response: { data: remoteScripts } = {}, + isLoading: isLoadingScripts, + error: scriptsError, + } = useSubAction({ + connectorId, + subAction: SUB_ACTION.GET_REMOTE_SCRIPTS, + }); + + useEffect(() => { + if (agentsError) { + toasts.danger({ title: i18n.AGENTS_ERROR, body: agentsError.message }); + } + if (scriptsError) { + toasts.danger({ title: i18n.REMOTE_SCRIPTS_ERROR, body: scriptsError.message }); + } + }, [toasts, scriptsError, agentsError]); + + const pagination = { + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }; + + const search: EuiSearchBarProps = { + defaultQuery: 'scriptType:action', + box: { + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'scriptType', + name: i18n.SCRIPT_TYPE_FILTER_LABEL, + multiSelect: true, + options: [ + { + value: 'action', + }, + { value: 'dataCollection' }, + ], + }, + { + type: 'field_value_selection', + field: 'osTypes', + name: i18n.OS_TYPES_FILTER_LABEL, + multiSelect: true, + options: [ + { + value: 'Windows', + }, + { + value: 'macos', + }, + { + value: 'linux', + }, + ], + }, + ], + }; + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( + {} + ); + + const toggleDetails = (script: ScriptOption) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + + if (script.id) { + if (itemIdToExpandedRowMapValues[script.id]) { + delete itemIdToExpandedRowMapValues[script.id]; + } else { + itemIdToExpandedRowMapValues[script.id] = <>More details true; + } + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; + + const columns: Array> = [ + { + field: 'scriptName', + name: 'Script name', + }, + { + field: 'scriptType', + name: 'Script type', + }, + { + field: 'osTypes', + name: 'OS types', + }, + { + actions: [ + { + name: 'Choose', + description: 'Choose this script', + isPrimary: true, + onClick: (item) => { + setSelectedScript(item); + editSubActionParams({ + script: { + scriptId: item.id, + scriptRuntimeTimeoutSeconds: 3600, + taskDescription: item.scriptName, + requiresApproval: item.requiresApproval ?? false, + }, + }); + }, + }, + ], + }, + { + align: 'right', + width: '40px', + isExpander: true, + render: (script: ScriptOption) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + + return ( + toggleDetails(script)} + aria-label={itemIdToExpandedRowMapValues[script.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMapValues[script.id] ? 'arrowDown' : 'arrowRight'} + /> + ); + }, + }, + ]; + + const actionTypeOptions = [ + { + value: SUB_ACTION.KILL_PROCESS, + inputDisplay: i18n.KILL_PROCESS_ACTION_LABEL, + }, + { + value: SUB_ACTION.ISOLATE_AGENT, + inputDisplay: i18n.ISOLATE_AGENT_ACTION_LABEL, + }, + { + value: SUB_ACTION.RELEASE_AGENT, + inputDisplay: i18n.RELEASE_AGENT_ACTION_LABEL, + }, + ]; + + const handleEditSubAction = useCallback( + (payload) => { + if (subAction !== payload) { + editSubActionParams({}); + editAction('subAction', payload, index); + } + }, + [editAction, editSubActionParams, index, subAction] + ); + + return ( + + {isTest && ( + + + { + setSelectedAgent(item); + editSubActionParams({ computerName: item[0].label }); + }} + isDisabled={isLoadingAgents} + /> + + + )} + + + + + + {subAction === SUB_ACTION.EXECUTE_SCRIPT && ( + <> + + setSelectedScript(undefined)}> + {i18n.CHANGE_ACTION_LABEL} + + ) : null + } + > + {selectedScript?.scriptName ? ( + + ) : ( + + items={remoteScripts ?? []} + itemId="scriptId" + loading={isLoadingScripts} + columns={columns} + search={search} + pagination={pagination} + sorting + hasActions + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + /> + )} + + + + <> + {selectedScript && ( + + + + )} + + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SentinelOneParamsFields as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts new file mode 100644 index 0000000000000..a5b9a274857c3 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { API_MAX_RESULTS } from '../../../common/sentinelone/constants'; + +// config form +export const URL_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.config.urlTextFieldLabel', + { + defaultMessage: 'SentinelOne tenant URL', + } +); + +export const TOKEN_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.config.tokenTextFieldLabel', + { + defaultMessage: 'API token', + } +); + +// params form +export const ASC = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.storyFieldLabel', + { + defaultMessage: 'SentinelOne Script', + } +); + +export const SCRIPT_TYPE_FILTER_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.scriptTypeFilterLabel', + { + defaultMessage: 'Script type', + } +); + +export const OS_TYPES_FILTER_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.osTypesFilterLabel', + { + defaultMessage: 'OS', + } +); + +export const STORY_ARIA_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.storyFieldAriaLabel', + { + defaultMessage: 'Select a SentinelOne script', + } +); + +export const KILL_PROCESS_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.killProcessActionLabel', + { + defaultMessage: 'Kill process', + } +); + +export const ISOLATE_AGENT_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.isolateAgentActionLabel', + { + defaultMessage: 'Isolate agent', + } +); + +export const RELEASE_AGENT_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.releaseAgentActionLabel', + { + defaultMessage: 'Release agent', + } +); + +export const AGENTS_FIELD_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.agentsFieldLabel', + { + defaultMessage: 'SentinelOne agent', + } +); + +export const AGENTS_FIELD_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.agentsFieldPlaceholder', + { + defaultMessage: 'Select a single agent', + } +); + +export const ACTION_TYPE_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.actionTypeFieldLabel', + { + defaultMessage: 'Action Type', + } +); + +export const COMMAND_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.commandFieldLabel', + { + defaultMessage: 'Command', + } +); + +export const CHANGE_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.changeActionButton', + { + defaultMessage: 'Change action', + } +); + +export const WEBHOOK_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookFieldLabel', + { + defaultMessage: 'SentinelOne Webhook action', + } +); +export const WEBHOOK_HELP = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookHelp', + { + defaultMessage: 'The data entry action in the story', + } +); +export const WEBHOOK_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookPlaceholder', + { + defaultMessage: 'Select a webhook action', + } +); +export const WEBHOOK_DISABLED_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookDisabledPlaceholder', + { + defaultMessage: 'Select a story first', + } +); +export const WEBHOOK_ARIA_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookFieldAriaLabel', + { + defaultMessage: 'Select a SentinelOne webhook action', + } +); + +export const WEBHOOK_URL_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlFieldLabel', + { + defaultMessage: 'Webhook URL', + } +); +export const WEBHOOK_URL_FALLBACK_TITLE = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlFallbackTitle', + { + defaultMessage: 'SentinelOne API results limit reached', + } +); +export const WEBHOOK_URL_FALLBACK_TEXT = (entity: 'Story' | 'Webhook') => + i18n.translate('xpack.stackConnectors.security.sentinelone.params.webhookUrlFallbackText', { + values: { entity, limit: API_MAX_RESULTS }, + defaultMessage: `Not possible to retrieve more than {limit} results from the SentinelOne {entity} API. If your {entity} does not appear in the list, please fill the Webhook URL below`, + }); +export const WEBHOOK_URL_HELP = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlHelp', + { + defaultMessage: 'The Story and Webhook selectors will be ignored if the Webhook URL is defined', + } +); +export const WEBHOOK_URL_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlPlaceholder', + { + defaultMessage: 'Paste the Webhook URL here', + } +); +export const DISABLED_BY_WEBHOOK_URL_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.disabledByWebhookUrlPlaceholder', + { + defaultMessage: 'Remove the Webhook URL to use this selector', + } +); + +export const BODY_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.bodyFieldLabel', + { + defaultMessage: 'Body', + } +); +export const AGENTS_ERROR = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentError.storiesRequestFailed', + { + defaultMessage: 'Error retrieving agent from SentinelOne', + } +); + +export const REMOTE_SCRIPTS_ERROR = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentError.remoteScriptsRequestFailed', + { + defaultMessage: 'Error retrieving remote scripts from SentinelOne', + } +); + +export const WEBHOOKS_ERROR = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentError.webhooksRequestFailed', + { + defaultMessage: 'Error retrieving webhook actions from SentinelOne', + } +); + +export const STORY_NOT_FOUND_WARNING = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentWarning.storyNotFound', + { + defaultMessage: 'Cannot find the saved story. Please select a valid story from the selector', + } +); +export const WEBHOOK_NOT_FOUND_WARNING = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentWarning.webhookNotFound', + { + defaultMessage: + 'Cannot find the saved webhook. Please select a valid webhook from the selector', + } +); + +export const ACTION_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredActionText', + { + defaultMessage: 'Action is required.', + } +); + +export const INVALID_ACTION = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.invalidActionText', + { + defaultMessage: 'Invalid action name.', + } +); + +export const BODY_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredBodyText', + { + defaultMessage: 'Body is required.', + } +); + +export const BODY_INVALID = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.invalidBodyText', + { + defaultMessage: 'Body does not have a valid JSON format.', + } +); + +export const STORY_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredStoryText', + { + defaultMessage: 'Story is required.', + } +); +export const WEBHOOK_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredWebhookText', + { + defaultMessage: 'Webhook is required.', + } +); +export const WEBHOOK_PATH_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredWebhookPathText', + { + defaultMessage: 'Webhook action path is missing.', + } +); +export const WEBHOOK_SECRET_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredWebhookSecretText', + { + defaultMessage: 'Webhook action secret is missing.', + } +); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts new file mode 100644 index 0000000000000..ac97bdaf224f4 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SentinelOneKillProcessParams, + SentinelOneExecuteScriptParams, + SentinelOneIsolateAgentParams, +} from '../../../common/sentinelone/types'; +import type { SUB_ACTION } from '../../../common/sentinelone/constants'; + +export type SentinelOneExecuteSubActionParams = + | SentinelOneKillProcessParams + | SentinelOneExecuteScriptParams + | SentinelOneIsolateAgentParams; + +export interface SentinelOneExecuteActionParams { + subAction: SUB_ACTION; + subActionParams: SentinelOneExecuteSubActionParams; +} diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts new file mode 100644 index 0000000000000..1ce534079e829 --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SubActionConnectorType, + ValidatorType, +} from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { urlAllowListValidator } from '@kbn/actions-plugin/server'; +import { SENTINELONE_CONNECTOR_ID, SENTINELONE_TITLE } from '../../../common/sentinelone/constants'; +import { + SentinelOneConfigSchema, + SentinelOneSecretsSchema, +} from '../../../common/sentinelone/schema'; +import { SentinelOneConfig, SentinelOneSecrets } from '../../../common/sentinelone/types'; +import { SentinelOneConnector } from './sentinelone'; +import { renderParameterTemplates } from './render'; + +export const getSentinelOneConnectorType = (): SubActionConnectorType< + SentinelOneConfig, + SentinelOneSecrets +> => ({ + id: SENTINELONE_CONNECTOR_ID, + name: SENTINELONE_TITLE, + Service: SentinelOneConnector, + schema: { + config: SentinelOneConfigSchema, + secrets: SentinelOneSecretsSchema, + }, + validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }], + supportedFeatureIds: [SecurityConnectorFeatureId], + minimumLicenseRequired: 'enterprise' as const, + renderParameterTemplates, +}); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts new file mode 100644 index 0000000000000..9d852510ea7b7 --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { set } from '@kbn/safer-lodash-set/fp'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; + +interface Context { + alerts: Ecs[]; +} + +export const renderParameterTemplates = ( + params: ExecutorParams, + variables: Record +) => { + const context = variables?.context as Context; + if (params?.subAction === SUB_ACTION.KILL_PROCESS) { + return { + subAction: SUB_ACTION.KILL_PROCESS, + subActionParams: { + processName: context.alerts[0].process?.name, + computerName: context.alerts[0].host?.name, + }, + }; + } + + if (params?.subAction === SUB_ACTION.ISOLATE_AGENT) { + return { + subAction: SUB_ACTION.ISOLATE_AGENT, + subActionParams: { + computerName: context.alerts[0].host?.name, + }, + }; + } + + if (params?.subAction === SUB_ACTION.RELEASE_AGENT) { + return { + subAction: SUB_ACTION.RELEASE_AGENT, + subActionParams: { + computerName: context.alerts[0].host?.name, + }, + }; + } + + if (params?.subAction === SUB_ACTION.EXECUTE_SCRIPT) { + return { + subAction: SUB_ACTION.EXECUTE_SCRIPT, + subActionParams: { + computerName: context.alerts[0].host?.name, + ...params.subActionParams, + }, + }; + } + + let body: string; + try { + let bodyObject; + const alerts = context.alerts; + if (alerts) { + // Remove the "kibana" entry from all alerts to reduce weight, the same data can be found in other parts of the alert object. + bodyObject = set( + 'context.alerts', + alerts.map(({ kibana, ...alert }) => alert), + variables + ); + } else { + bodyObject = variables; + } + body = JSON.stringify(bodyObject); + } catch (err) { + body = JSON.stringify({ error: { message: err.message } }); + } + return set('subActionParams.body', body, params); +}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts new file mode 100644 index 0000000000000..d27f7dcc5588b --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server'; +import type { AxiosError } from 'axios'; +import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import type { + SentinelOneConfig, + SentinelOneSecrets, + SentinelOneGetAgentsResponse, + SentinelOneGetAgentsParams, + SentinelOneGetRemoteScriptStatusParams, + SentinelOneBaseApiResponse, + SentinelOneGetRemoteScriptsParams, + SentinelOneGetRemoteScriptsResponse, + SentinelOneIsolateAgentParams, + SentinelOneKillProcessParams, + SentinelOneExecuteScriptParams, +} from '../../../common/sentinelone/types'; +import { + SentinelOneKillProcessResponseSchema, + SentinelOneExecuteScriptParamsSchema, + SentinelOneGetRemoteScriptsParamsSchema, + SentinelOneGetRemoteScriptsResponseSchema, + SentinelOneGetAgentsResponseSchema, + SentinelOneIsolateAgentResponseSchema, + SentinelOneIsolateAgentParamsSchema, + SentinelOneGetRemoteScriptStatusParamsSchema, + SentinelOneGetRemoteScriptStatusResponseSchema, + SentinelOneGetAgentsParamsSchema, + SentinelOneExecuteScriptResponseSchema, +} from '../../../common/sentinelone/schema'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; + +export const API_MAX_RESULTS = 1000; +export const API_PATH = '/web/api/v2.1'; + +export class SentinelOneConnector extends SubActionConnector< + SentinelOneConfig, + SentinelOneSecrets +> { + private urls: { + agents: string; + isolateAgent: string; + releaseAgent: string; + remoteScripts: string; + remoteScriptStatus: string; + remoteScriptsExecute: string; + }; + + constructor(params: ServiceParams) { + super(params); + + this.urls = { + isolateAgent: `${this.config.url}${API_PATH}/agents/actions/disconnect`, + releaseAgent: `${this.config.url}${API_PATH}/agents/actions/connect`, + remoteScripts: `${this.config.url}${API_PATH}/remote-scripts`, + remoteScriptStatus: `${this.config.url}${API_PATH}/remote-scripts/status`, + remoteScriptsExecute: `${this.config.url}${API_PATH}/remote-scripts/execute`, + agents: `${this.config.url}${API_PATH}/agents`, + }; + + this.registerSubActions(); + } + + private registerSubActions() { + this.registerSubAction({ + name: SUB_ACTION.GET_REMOTE_SCRIPTS, + method: 'getRemoteScripts', + schema: SentinelOneGetRemoteScriptsParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.GET_REMOTE_SCRIPT_STATUS, + method: 'getRemoteScriptStatus', + schema: SentinelOneGetRemoteScriptStatusParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.GET_AGENTS, + method: 'getAgents', + schema: SentinelOneGetAgentsParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.ISOLATE_AGENT, + method: 'isolateAgent', + schema: SentinelOneIsolateAgentParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.RELEASE_AGENT, + method: 'releaseAgent', + schema: SentinelOneIsolateAgentParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.KILL_PROCESS, + method: 'killProcess', + schema: SentinelOneKillProcessResponseSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.EXECUTE_SCRIPT, + method: 'executeScript', + schema: SentinelOneExecuteScriptParamsSchema, + }); + } + + public async executeScript(payload: SentinelOneExecuteScriptParams) { + return this.sentinelOneApiRequest({ + url: this.urls.remoteScriptsExecute, + method: 'post', + data: { + data: { + outputDestination: 'SentinelCloud', + ...payload.script, + }, + filter: { + computerName: payload.computerName, + }, + }, + responseSchema: SentinelOneExecuteScriptResponseSchema, + }); + } + + public async killProcess({ processName, ...payload }: SentinelOneKillProcessParams) { + const agentData = await this.getAgents(payload); + + const agentId = agentData.data[0]?.id; + + if (!agentId) { + throw new Error(`No agent found for filter ${JSON.stringify(payload)}`); + } + + const terminateScriptResponse = await this.getRemoteScripts({ + query: 'terminate', + osTypes: [agentData?.data[0]?.osType], + }); + + if (!processName) { + throw new Error('No process name provided'); + } + + return this.sentinelOneApiRequest({ + url: this.urls.remoteScriptsExecute, + method: 'post', + data: { + data: { + outputDestination: 'SentinelCloud', + scriptId: terminateScriptResponse.data[0].id, + scriptRuntimeTimeoutSeconds: terminateScriptResponse.data[0].scriptRuntimeTimeoutSeconds, + taskDescription: terminateScriptResponse.data[0].scriptName, + inputParams: `--terminate --processes ${processName}`, + }, + filter: { + ids: agentId, + }, + }, + responseSchema: SentinelOneKillProcessResponseSchema, + }); + } + + public async isolateAgent(payload: SentinelOneIsolateAgentParams) { + const response = await this.getAgents(payload); + + if (response.data.length === 0) { + throw new Error('No agents found'); + } + + if (response.data[0].networkStatus === 'disconnected') { + throw new Error('Agent already isolated'); + } + + const agentId = response.data[0].id; + + return this.sentinelOneApiRequest({ + url: this.urls.isolateAgent, + method: 'post', + data: { + filter: { + ids: agentId, + }, + }, + responseSchema: SentinelOneIsolateAgentResponseSchema, + }); + } + + public async releaseAgent(payload: SentinelOneIsolateAgentParams) { + const response = await this.getAgents(payload); + + if (response.data.length === 0) { + throw new Error('No agents found'); + } + + if (response.data[0].networkStatus !== 'disconnected') { + throw new Error('Agent not isolated'); + } + + const agentId = response.data[0].id; + + return this.sentinelOneApiRequest({ + url: this.urls.releaseAgent, + method: 'post', + data: { + filter: { + ids: agentId, + }, + }, + responseSchema: SentinelOneIsolateAgentResponseSchema, + }); + } + + public async getAgents( + payload: SentinelOneGetAgentsParams + ): Promise { + return this.sentinelOneApiRequest({ + url: this.urls.agents, + params: { + ...payload, + }, + responseSchema: SentinelOneGetAgentsResponseSchema, + }); + } + + public async getRemoteScriptStatus(payload: SentinelOneGetRemoteScriptStatusParams) { + return this.sentinelOneApiRequest({ + url: this.urls.remoteScriptStatus, + params: { + parent_task_id: payload.parentTaskId, + }, + responseSchema: SentinelOneGetRemoteScriptStatusResponseSchema, + }); + } + + private async sentinelOneApiRequest( + req: SubActionRequestParams + ): Promise { + const response = await this.request({ + ...req, + params: { + ...req.params, + APIToken: this.secrets.token, + }, + }); + + return response.data; + } + + protected getResponseErrorMessage(error: AxiosError): string { + if (!error.response?.status) { + return 'Unknown API Error'; + } + if (error.response.status === 401) { + return 'Unauthorized API Error'; + } + return `API Error: ${error.response?.statusText}`; + } + + public async getRemoteScripts( + payload: SentinelOneGetRemoteScriptsParams + ): Promise { + return this.sentinelOneApiRequest({ + url: this.urls.remoteScripts, + params: { + limit: API_MAX_RESULTS, + ...payload, + }, + responseSchema: SentinelOneGetRemoteScriptsResponseSchema, + }); + } +} diff --git a/x-pack/plugins/stack_connectors/tsconfig.json b/x-pack/plugins/stack_connectors/tsconfig.json index 7cc6696f04368..f18dfaea77cca 100644 --- a/x-pack/plugins/stack_connectors/tsconfig.json +++ b/x-pack/plugins/stack_connectors/tsconfig.json @@ -33,6 +33,7 @@ "@kbn/core-saved-objects-common", "@kbn/core-http-browser-mocks", "@kbn/core-saved-objects-api-server-mocks", + "@kbn/securitysolution-ecs", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts b/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts index 49e7bc1e8d9e3..69947cb08fc8d 100644 --- a/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts +++ b/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts @@ -44,6 +44,7 @@ export const getOldestIdleActionTask = async ( 'actions:.jira', 'actions:.resilient', 'actions:.teams', + 'actions:.sentinelone', ], }, }, diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index 53d09d4baf131..0bb12906708de 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -50,6 +50,7 @@ export function setupSavedObjects( 'actions:.jira', 'actions:.resilient', 'actions:.teams', + 'actions:.sentinelone', ], }, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 3bb65037d5c93..346dce44af60b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -20,6 +20,7 @@ interface Props { isDisabled?: boolean; editAction: (property: string, value: any, index: number) => void; label: string; + helpText?: string; errors?: string[]; } @@ -32,6 +33,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ editAction, label, errors, + helpText, }) => { const [currentTextElement, setCurrentTextElement] = useState(null); @@ -64,6 +66,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ paramsProperty={paramsProperty} /> } + helpText={helpText} >