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

<img width="1685" alt="Zrzut ekranu 2023-08-3 o 11 18 54"
src="965ef8ef-497f-42a8-983e-38fd0370cba8">
 visual changes include a screenshot or gif.

<img width="1685" alt="image"
src="119d2255-ed9f-4923-886d-eb139223a47d">

<img width="1690" alt="image"
src="e2c569d2-b497-4641-a6a6-454494223ffc">
This commit is contained in:
Patryk Kopyciński 2023-08-09 20:02:11 +02:00 committed by GitHub
parent 6673e9dc59
commit 4637b744d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1883 additions and 2 deletions

5
.github/CODEOWNERS vendored
View file

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

View file

@ -129,7 +129,7 @@ pageLoadAssetSize:
snapshotRestore: 79032
spaces: 57868
stackAlerts: 58316
stackConnectors: 36314
stackConnectors: 52131
synthetics: 40958
telemetry: 51957
telemetryManagementSection: 38586

View file

@ -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<P> {
connectorId: string;
subAction: string;
subActionParams?: P;
disabled?: boolean;
}
export const useSubAction = <P, R>({
connectorId,
subAction,
subActionParams,
disabled = false,
...rest
}: UseSubActionParams<P>) => {
const { http } = useKibana().services;
return useQuery({
queryKey: ['useSubAction', connectorId, subAction, subActionParams],
queryFn: ({ signal }) =>
executeAction<R>({
id: connectorId,
params: {
subAction,
subActionParams,
},
http,
signal,
}),
enabled: !disabled && !!connectorId && !!subAction,
...rest,
});
};

View file

@ -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<P> {
connectorId: string;
subAction: string;
subActionParams?: P;
disabled?: boolean;
}
export const useSubActionMutation = <P, R>({
connectorId,
subAction,
subActionParams,
disabled = false,
}: UseSubActionParams<P>) => {
const { http } = useKibana().services;
return useMutation({
mutationFn: () =>
executeAction<R>({
id: connectorId,
params: {
subAction,
subActionParams,
},
http,
}),
});
};

View file

@ -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',
}

View file

@ -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,
]);

View file

@ -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<typeof SentinelOneConfigSchema>;
export type SentinelOneSecrets = TypeOf<typeof SentinelOneSecretsSchema>;
export type SentinelOneBaseApiResponse = TypeOf<typeof SentinelOneBaseApiResponseSchema>;
export type SentinelOneGetAgentsParams = TypeOf<typeof SentinelOneGetAgentsParamsSchema>;
export type SentinelOneGetAgentsResponse = TypeOf<typeof SentinelOneGetAgentsResponseSchema>;
export type SentinelOneKillProcessParams = TypeOf<typeof SentinelOneKillProcessParamsSchema>;
export type SentinelOneExecuteScriptParams = TypeOf<typeof SentinelOneExecuteScriptParamsSchema>;
export type SentinelOneGetRemoteScriptStatusParams = TypeOf<
typeof SentinelOneGetRemoteScriptsStatusParams
>;
export type SentinelOneGetRemoteScriptsParams = TypeOf<
typeof SentinelOneGetRemoteScriptsParamsSchema
>;
export type SentinelOneGetRemoteScriptsResponse = TypeOf<
typeof SentinelOneGetRemoteScriptsResponseSchema
>;
export type SentinelOneIsolateAgentParams = TypeOf<typeof SentinelOneIsolateAgentParamsSchema>;
export type SentinelOneActionParams = TypeOf<typeof SentinelOneActionParamsSchema>;

View file

@ -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';

View file

@ -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 = () => (
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" version="1.1">
<g id="Layer_1">
<g stroke="null" id="svg_1" transform="matrix(0.028688 0 0 0.028688 -136.328 247.841)" />
<g stroke="null">
<g stroke="null" />
</g>
<g stroke="null" transform="matrix(0.332422 0 0 0.332422 -135.68 245.363)">
<rect
stroke="none"
fillRule="nonzero"
fill="rgb(107,10,234)"
strokeMiterlimit="4"
strokeLinejoin="miter"
strokeDashoffset="0"
strokeLinecap="butt"
strokeDasharray="none"
height="77.25"
width="12.92"
y="-737.83562"
x="448.36749"
vectorEffect="non-scaling-stroke"
/>
</g>
<g stroke="null" transform="matrix(0.332422 0 0 0.332422 -135.686 251.011)">
<path
stroke="none"
fillRule="nonzero"
fill="rgb(107,10,234)"
strokeMiterlimit="4"
strokeLinejoin="miter"
strokeDashoffset="0"
strokeDasharray="none"
strokeLinecap="butt"
d="m502.95749,-603.33062l12.91,-8l0,-66.88c-3.82729,-2.83807 -8.23656,-4.79167 -12.91,-5.72l0,80.6zm-32.13,-7.96l12.92,8l0,-80.6c-4.67381,0.91429 -9.08653,2.85424 -12.92,5.68l0,66.92z"
transform="translate(-38.52 -55.6)"
vectorEffect="non-scaling-stroke"
/>
</g>
<g stroke="null" transform="matrix(0.332422 0 0 0.332422 -125.01 247.509)">
<path
stroke="none"
fillRule="nonzero"
fill="rgb(107,10,234)"
strokeMiterlimit="4"
strokeLinejoin="miter"
strokeDashoffset="0"
strokeDasharray="none"
strokeLinecap="butt"
d="m518.99749,-695.54062l0,82.81l6,-3.72c4.36622,-2.87218 6.96435,-7.77432 6.89,-13l0,-39.11c0.03,-11.28 -12.89,-26.98 -12.89,-26.98z"
transform="translate(-70.62 -45.08)"
vectorEffect="non-scaling-stroke"
/>
</g>
<g stroke="null" transform="matrix(0.332422 0 0 0.332422 -146.356 247.503)">
<path
stroke="none"
fillRule="nonzero"
fill="rgb(107,10,234)"
strokeMiterlimit="4"
strokeLinejoin="miter"
strokeDashoffset="0"
strokeDasharray="none"
strokeLinecap="butt"
d="m454.82749,-629.48062c-0.08001,5.22704 2.51948,10.13174 6.89,13l6,3.72l0,-82.78c0,0 -12.89,15.7 -12.89,26.98l0,39.08z"
transform="translate(-6.44 -45.06)"
vectorEffect="non-scaling-stroke"
/>
</g>
</g>
</svg>
);
// eslint-disable-next-line import/no-default-export
export { Logo as default };

View file

@ -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<GenericValidationResult<ValidationErrors>> => {
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')),
};
}

View file

@ -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<ActionConnectorFieldsProps> = ({
readOnly,
isEdit,
}) => (
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={configFormSchema}
secretsFormSchema={secretsFormSchema}
/>
);
// eslint-disable-next-line import/no-default-export
export { SentinelOneActionConnectorFields as default };

View file

@ -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<SentinelOneActionParams>
> = ({ actionConnector, actionParams, editAction, index, executionMode, errors, ...rest }) => {
const { toasts } = useKibana().notifications;
const { subAction, subActionParams } = actionParams;
const [selectedScript, setSelectedScript] = useState<ScriptOption | undefined>();
const [selectedAgent, setSelectedAgent] = useState<Array<{ label: string }>>(() => {
if (subActionParams?.computerName) {
return [{ label: subActionParams?.computerName }];
}
return [];
});
const [connectorId] = useState<string | undefined>(actionConnector?.id);
const isTest = useMemo(() => executionMode === ActionConnectorMode.Test, [executionMode]);
const editSubActionParams = useCallback(
(params: Partial<SentinelOneExecuteSubActionParams>) => {
editAction('subActionParams', { ...subActionParams, ...params }, index);
},
[editAction, index, subActionParams]
);
const {
response: { data: agents } = {},
isLoading: isLoadingAgents,
error: agentsError,
} = useSubAction<SentinelOneGetAgentsParams, SentinelOneGetAgentsResponse>({
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<SentinelOneGetRemoteScriptsParams, SentinelOneGetRemoteScriptsResponse>({
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<Record<string, ReactNode>>(
{}
);
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<EuiBasicTableColumn<SentinelOneGetRemoteScriptsResponse['data'][0]>> = [
{
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 (
<EuiButtonIcon
onClick={() => 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 (
<EuiFlexGroup direction="column">
{isTest && (
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.AGENTS_FIELD_LABEL}>
<EuiComboBox
fullWidth
placeholder={i18n.AGENTS_FIELD_PLACEHOLDER}
singleSelection={{ asPlainText: true }}
options={agentOptions}
selectedOptions={selectedAgent}
onChange={(item) => {
setSelectedAgent(item);
editSubActionParams({ computerName: item[0].label });
}}
isDisabled={isLoadingAgents}
/>
</EuiFormRow>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.ACTION_TYPE_LABEL}>
<EuiSuperSelect
fullWidth
options={actionTypeOptions}
valueOfSelected={subAction}
onChange={handleEditSubAction}
/>
</EuiFormRow>
</EuiFlexItem>
{subAction === SUB_ACTION.EXECUTE_SCRIPT && (
<>
<EuiFlexItem>
<EuiFormRow
fullWidth
error={errors.script}
isInvalid={!!errors.script?.length}
label={'Script'}
labelAppend={
selectedScript ? (
<EuiLink onClick={() => setSelectedScript(undefined)}>
{i18n.CHANGE_ACTION_LABEL}
</EuiLink>
) : null
}
>
{selectedScript?.scriptName ? (
<EuiFieldText fullWidth value={selectedScript.scriptName} />
) : (
<EuiInMemoryTable<ScriptOption>
items={remoteScripts ?? []}
itemId="scriptId"
loading={isLoadingScripts}
columns={columns}
search={search}
pagination={pagination}
sorting
hasActions
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
/>
)}
</EuiFormRow>
</EuiFlexItem>
<>
{selectedScript && (
<EuiFlexItem>
<TextAreaWithMessageVariables
index={index}
editAction={editAction}
messageVariables={[]}
paramsProperty={'subActionParams.script.inputParams'}
label={i18n.COMMAND_LABEL}
inputTargetValue={subActionParams?.script?.inputParams ?? undefined}
helpText={
selectedScript?.inputExample
? `Example: ${selectedScript?.inputExample}`
: undefined
}
/>
</EuiFlexItem>
)}
</>
</>
)}
</EuiFlexGroup>
);
};
// eslint-disable-next-line import/no-default-export
export { SentinelOneParamsFields as default };

View file

@ -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.',
}
);

View file

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

View file

@ -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,
});

View file

@ -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<string, unknown>
) => {
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);
};

View file

@ -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<SentinelOneConfig, SentinelOneSecrets>) {
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<SentinelOneGetAgentsResponse> {
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<R extends SentinelOneBaseApiResponse>(
req: SubActionRequestParams<R>
): Promise<R> {
const response = await this.request<R>({
...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<SentinelOneGetRemoteScriptsResponse> {
return this.sentinelOneApiRequest({
url: this.urls.remoteScripts,
params: {
limit: API_MAX_RESULTS,
...payload,
},
responseSchema: SentinelOneGetRemoteScriptsResponseSchema,
});
}
}

View file

@ -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/**/*",

View file

@ -44,6 +44,7 @@ export const getOldestIdleActionTask = async (
'actions:.jira',
'actions:.resilient',
'actions:.teams',
'actions:.sentinelone',
],
},
},

View file

@ -50,6 +50,7 @@ export function setupSavedObjects(
'actions:.jira',
'actions:.resilient',
'actions:.teams',
'actions:.sentinelone',
],
},
},

View file

@ -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<Props> = ({
editAction,
label,
errors,
helpText,
}) => {
const [currentTextElement, setCurrentTextElement] = useState<HTMLTextAreaElement | null>(null);
@ -64,6 +66,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent<Props> = ({
paramsProperty={paramsProperty}
/>
}
helpText={helpText}
>
<EuiTextArea
disabled={isDisabled}

View file

@ -8,5 +8,5 @@
export { templateActionVariable } from './template_action_variable';
export { hasMustacheTokens } from './has_mustache_tokens';
export { AlertProvidedActionVariables } from './action_variables';
export { updateActionConnector } from './action_connector_api';
export { updateActionConnector, executeAction } from './action_connector_api';
export { isRuleSnoozed } from './is_rule_snoozed';

View file

@ -89,6 +89,7 @@ export {
hasMustacheTokens,
templateActionVariable,
updateActionConnector,
executeAction,
} from './application/lib';
export type { ActionGroupWithCondition } from './application/sections';