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} >