mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
SentinelOne manual host isolation/release in Alert details (#168441)
## Summary Add possibility to Isolate/Release SentinelOne host from Alert details flyout. Add support for displaying S1 Agent status in UI. Add an experimental flag to S1 Connector. Rename S1 connector actions from `Agent` to `Host` Add a feature flag to security_solution to control enrollment of this feature. Update parallel script to support all FTR config options Add `cypress-data-session` plugin to allow better caching of test data (mostly for Dev experience) Testing instruction: 1. Ensure you have 2. From root Kibana folder run https://p.elstc.co/paste/URVrCEcR#aG1X9p3BMCRUDY+IzfIg5mGomcTGxwkYO6RGxSIAyWz 3. In Cypress run ```x-pack/plugins/security_solution/public/management/cypress/e2e/sentinelone/isolate.cy.ts``` 4. 💚 <img width="2375" alt="Zrzut ekranu 2023-11-15 o 12 38 27" src="c7ddc20e
-9944-452c-b739-fa2d9fbf072b"> <img width="2374" alt="Zrzut ekranu 2023-11-15 o 12 38 32" src="ab3ced14
-0a5c-4f40-a92e-844feb849bb4"> <img width="2370" alt="Zrzut ekranu 2023-11-15 o 12 38 38" src="96ccd237
-56a6-449e-979d-f4fe8ffbe048"> <img width="2373" alt="Zrzut ekranu 2023-11-15 o 12 38 46" src="924013aa
-79ef-405b-ae73-139cf0644ebf"> <img width="2374" alt="Zrzut ekranu 2023-11-15 o 12 39 17" src="e1ff5b05
-8b80-40a9-84b1-dd21bf9e059c"> <img width="2374" alt="Zrzut ekranu 2023-11-15 o 12 39 58" src="15fc5d36
-970f-47cb-ae2f-f8a19628e6f4"> <img width="2374" alt="Zrzut ekranu 2023-11-15 o 12 40 03" src="5860a0c9
-a6e5-43b9-b37d-aa68e4e71f26"> <img width="2373" alt="Zrzut ekranu 2023-11-15 o 12 40 09" src="5e2c5d41
-c96a-4c32-8d51-a8408efea8e3"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
edf4f35152
commit
2d0f99c59c
52 changed files with 1636 additions and 355 deletions
|
@ -902,6 +902,7 @@
|
|||
"css-box-model": "^1.2.1",
|
||||
"css.escape": "^1.5.1",
|
||||
"cuid": "^2.1.8",
|
||||
"cypress-data-session": "^2.7.0",
|
||||
"cytoscape": "^3.10.0",
|
||||
"cytoscape-dagre": "^2.2.2",
|
||||
"d3": "3.5.17",
|
||||
|
|
|
@ -223,20 +223,24 @@ export class ActionTypeRegistry {
|
|||
* Returns a list of registered action types [{ id, name, enabled }], filtered by featureId if provided.
|
||||
*/
|
||||
public list(featureId?: string): CommonActionType[] {
|
||||
return Array.from(this.actionTypes)
|
||||
.filter(([_, actionType]) =>
|
||||
featureId ? actionType.supportedFeatureIds.includes(featureId) : true
|
||||
)
|
||||
.map(([actionTypeId, actionType]) => ({
|
||||
id: actionTypeId,
|
||||
name: actionType.name,
|
||||
minimumLicenseRequired: actionType.minimumLicenseRequired,
|
||||
enabled: this.isActionTypeEnabled(actionTypeId),
|
||||
enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId),
|
||||
enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid,
|
||||
supportedFeatureIds: actionType.supportedFeatureIds,
|
||||
isSystemActionType: !!actionType.isSystemActionType,
|
||||
}));
|
||||
return (
|
||||
Array.from(this.actionTypes)
|
||||
.filter(([_, actionType]) =>
|
||||
featureId ? actionType.supportedFeatureIds.includes(featureId) : true
|
||||
)
|
||||
// Temporarily don't return SentinelOne connector for Security Solution Rule Actions
|
||||
.filter(([actionTypeId]) => (featureId ? actionTypeId !== '.sentinelone' : true))
|
||||
.map(([actionTypeId, actionType]) => ({
|
||||
id: actionTypeId,
|
||||
name: actionType.name,
|
||||
minimumLicenseRequired: actionType.minimumLicenseRequired,
|
||||
enabled: this.isActionTypeEnabled(actionTypeId),
|
||||
enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId),
|
||||
enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid,
|
||||
supportedFeatureIds: actionType.supportedFeatureIds,
|
||||
isSystemActionType: !!actionType.isSystemActionType,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -141,6 +141,11 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
* Enables experimental Entity Analytics Asset Criticality feature
|
||||
*/
|
||||
entityAnalyticsAssetCriticalityEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables SentinelOne manual host manipulation actions
|
||||
*/
|
||||
sentinelOneManualHostActionsEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -29,15 +29,15 @@ import { useGetEndpointDetails } from '../../../../management/hooks';
|
|||
import { getAgentStatusText } from '../agent_status_text';
|
||||
|
||||
const TOOLTIP_CONTENT_STYLES: React.CSSProperties = Object.freeze({ width: 150 });
|
||||
const ISOLATING_LABEL = i18n.translate(
|
||||
export const ISOLATING_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating',
|
||||
{ defaultMessage: 'Isolating' }
|
||||
);
|
||||
const RELEASING_LABEL = i18n.translate(
|
||||
export const RELEASING_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating',
|
||||
{ defaultMessage: 'Releasing' }
|
||||
);
|
||||
const ISOLATED_LABEL = i18n.translate(
|
||||
export const ISOLATED_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.agentAndActionsStatus.isolated',
|
||||
{ defaultMessage: 'Isolated' }
|
||||
);
|
||||
|
|
|
@ -31,10 +31,20 @@ export interface EndpointIsolatedFormProps {
|
|||
messageAppend?: ReactNode;
|
||||
/** If true, then `Confirm` and `Cancel` buttons will be disabled, and `Confirm` button will loading loading style */
|
||||
isLoading?: boolean;
|
||||
hideCommentField?: boolean;
|
||||
}
|
||||
|
||||
export const EndpointIsolateForm = memo<EndpointIsolatedFormProps>(
|
||||
({ hostName, onCancel, onConfirm, onChange, comment = '', messageAppend, isLoading = false }) => {
|
||||
({
|
||||
hostName,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
onChange,
|
||||
comment = '',
|
||||
messageAppend,
|
||||
isLoading = false,
|
||||
hideCommentField = false,
|
||||
}) => {
|
||||
const handleCommentChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(event) => {
|
||||
onChange({ comment: event.target.value });
|
||||
|
@ -66,15 +76,17 @@ export const EndpointIsolateForm = memo<EndpointIsolatedFormProps>(
|
|||
</EuiText>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label={COMMENT} fullWidth>
|
||||
<EuiTextArea
|
||||
data-test-subj="host_isolation_comment"
|
||||
fullWidth
|
||||
placeholder={COMMENT_PLACEHOLDER}
|
||||
value={comment}
|
||||
onChange={handleCommentChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{!hideCommentField && (
|
||||
<EuiFormRow label={COMMENT} fullWidth>
|
||||
<EuiTextArea
|
||||
data-test-subj="host_isolation_comment"
|
||||
fullWidth
|
||||
placeholder={COMMENT_PLACEHOLDER}
|
||||
value={comment}
|
||||
onChange={handleCommentChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
|
|
|
@ -23,7 +23,16 @@ import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM, UNISOLATE, ISOLATED } fr
|
|||
import type { EndpointIsolatedFormProps } from './isolate_form';
|
||||
|
||||
export const EndpointUnisolateForm = memo<EndpointIsolatedFormProps>(
|
||||
({ hostName, onCancel, onConfirm, onChange, comment = '', messageAppend, isLoading = false }) => {
|
||||
({
|
||||
hostName,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
onChange,
|
||||
comment = '',
|
||||
messageAppend,
|
||||
isLoading = false,
|
||||
hideCommentField = false,
|
||||
}) => {
|
||||
const handleCommentChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(event) => {
|
||||
onChange({ comment: event.target.value });
|
||||
|
@ -52,15 +61,17 @@ export const EndpointUnisolateForm = memo<EndpointIsolatedFormProps>(
|
|||
</EuiText>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label={COMMENT} fullWidth>
|
||||
<EuiTextArea
|
||||
data-test-subj="host_isolation_comment"
|
||||
fullWidth
|
||||
placeholder={COMMENT_PLACEHOLDER}
|
||||
value={comment}
|
||||
onChange={handleCommentChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{!hideCommentField && (
|
||||
<EuiFormRow label={COMMENT} fullWidth>
|
||||
<EuiTextArea
|
||||
data-test-subj="host_isolation_comment"
|
||||
fullWidth
|
||||
placeholder={COMMENT_PLACEHOLDER}
|
||||
value={comment}
|
||||
onChange={handleCommentChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { TimelineEventsDetailsItem } from '../../../../common/search_strate
|
|||
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import { getSummaryRows } from './get_alert_summary_rows';
|
||||
import { SummaryView } from './summary_view';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
|
||||
const AlertSummaryViewComponent: React.FC<{
|
||||
browserFields: BrowserFields;
|
||||
|
@ -33,6 +34,10 @@ const AlertSummaryViewComponent: React.FC<{
|
|||
isReadOnly,
|
||||
investigationFields,
|
||||
}) => {
|
||||
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
|
||||
'sentinelOneManualHostActionsEnabled'
|
||||
);
|
||||
|
||||
const summaryRows = useMemo(
|
||||
() =>
|
||||
getSummaryRows({
|
||||
|
@ -43,8 +48,18 @@ const AlertSummaryViewComponent: React.FC<{
|
|||
scopeId,
|
||||
isReadOnly,
|
||||
investigationFields,
|
||||
sentinelOneManualHostActionsEnabled,
|
||||
}),
|
||||
[browserFields, data, eventId, isDraggable, scopeId, isReadOnly, investigationFields]
|
||||
[
|
||||
browserFields,
|
||||
data,
|
||||
eventId,
|
||||
isDraggable,
|
||||
scopeId,
|
||||
isReadOnly,
|
||||
investigationFields,
|
||||
sentinelOneManualHostActionsEnabled,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -34,6 +34,10 @@ import type { EventSummaryField, EnrichedFieldInfo } from './types';
|
|||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
|
||||
import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check';
|
||||
import {
|
||||
SENTINEL_ONE_AGENT_ID_FIELD,
|
||||
isAlertFromSentinelOneEvent,
|
||||
} from '../../utils/sentinelone_alert_check';
|
||||
|
||||
const THRESHOLD_TERMS_FIELD = `${ALERT_THRESHOLD_RESULT}.terms.field`;
|
||||
const THRESHOLD_TERMS_VALUE = `${ALERT_THRESHOLD_RESULT}.terms.value`;
|
||||
|
@ -44,7 +48,13 @@ const THRESHOLD_COUNT = `${ALERT_THRESHOLD_RESULT}.count`;
|
|||
/** Always show these fields */
|
||||
const alwaysDisplayedFields: EventSummaryField[] = [
|
||||
{ id: 'host.name' },
|
||||
// ENDPOINT-related field //
|
||||
{ id: 'agent.id', overrideField: AGENT_STATUS_FIELD_NAME, label: i18n.AGENT_STATUS },
|
||||
{
|
||||
id: SENTINEL_ONE_AGENT_ID_FIELD,
|
||||
label: i18n.AGENT_STATUS,
|
||||
},
|
||||
// ** //
|
||||
{ id: 'user.name' },
|
||||
{ id: 'rule.name' },
|
||||
{ id: 'cloud.provider' },
|
||||
|
@ -294,6 +304,7 @@ export const getSummaryRows = ({
|
|||
isDraggable = false,
|
||||
isReadOnly = false,
|
||||
investigationFields,
|
||||
sentinelOneManualHostActionsEnabled,
|
||||
}: {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
browserFields: BrowserFields;
|
||||
|
@ -302,6 +313,7 @@ export const getSummaryRows = ({
|
|||
investigationFields?: string[];
|
||||
isDraggable?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
sentinelOneManualHostActionsEnabled?: boolean;
|
||||
}) => {
|
||||
const eventCategories = getEventCategoriesFromData(data);
|
||||
|
||||
|
@ -357,6 +369,14 @@ export const getSummaryRows = ({
|
|||
return acc;
|
||||
}
|
||||
|
||||
if (
|
||||
field.id === SENTINEL_ONE_AGENT_ID_FIELD &&
|
||||
sentinelOneManualHostActionsEnabled &&
|
||||
!isAlertFromSentinelOneEvent({ data })
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (field.id === THRESHOLD_TERMS_FIELD) {
|
||||
const enrichedInfo = enrichThresholdTerms(item, data, description);
|
||||
if (enrichedInfo) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
AGENT_STATUS_FIELD_NAME,
|
||||
QUARANTINED_PATH_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../utils/sentinelone_alert_check';
|
||||
|
||||
/**
|
||||
* Defines the behavior of the search input that appears above the table of data
|
||||
|
@ -181,6 +182,7 @@ export function getEnrichedFieldInfo({
|
|||
export const FIELDS_WITHOUT_ACTIONS: { [field: string]: boolean } = {
|
||||
[AGENT_STATUS_FIELD_NAME]: true,
|
||||
[QUARANTINED_PATH_FIELD_NAME]: true,
|
||||
[SENTINEL_ONE_AGENT_ID_FIELD]: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,8 +78,8 @@ export const ResponseActionsResults = React.memo(
|
|||
ResponseActionsResults.displayName = 'ResponseActionsResults';
|
||||
|
||||
const isOsquery = (item: LogsEndpointAction | LogsOsqueryAction): item is LogsOsqueryAction => {
|
||||
return item && 'input_type' in item && item?.input_type === 'osquery';
|
||||
return !!(item && 'input_type' in item && item?.input_type === 'osquery');
|
||||
};
|
||||
const isEndpoint = (item: LogsEndpointAction | LogsOsqueryAction): item is LogsEndpointAction => {
|
||||
return item && 'EndpointActions' in item && item?.EndpointActions.input_type === 'endpoint';
|
||||
return !!(item && 'EndpointActions' in item && item?.EndpointActions?.input_type === 'endpoint');
|
||||
};
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const LOAD_CONNECTORS_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.connectors.useLoadConnectors.errorMessage',
|
||||
{
|
||||
defaultMessage: 'Error loading connectors. Please check your configuration and try again.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ServerError } from '@kbn/cases-plugin/public/types';
|
||||
import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import * as i18n from '../translations';
|
||||
import { useHttp } from '../../../lib/kibana';
|
||||
|
||||
export interface Props {
|
||||
actionTypeId: string;
|
||||
toasts?: IToasts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to load all connectors for a given action type.
|
||||
* @param actionTypeId
|
||||
* @param toasts
|
||||
*/
|
||||
export const useLoadConnectors = ({
|
||||
actionTypeId,
|
||||
toasts,
|
||||
}: Props): UseQueryResult<ActionConnector[], IHttpFetchError> => {
|
||||
const http = useHttp();
|
||||
|
||||
return useQuery(
|
||||
['load-connectors', actionTypeId],
|
||||
async () => {
|
||||
const queryResult = await loadConnectors({ http });
|
||||
const filteredData = queryResult.filter(
|
||||
(connector) => !connector.isMissingSecrets && connector.actionTypeId === actionTypeId
|
||||
);
|
||||
|
||||
return filteredData;
|
||||
},
|
||||
{
|
||||
retry: false,
|
||||
keepPreviousData: true,
|
||||
onError: (error: ServerError) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts?.addError(
|
||||
error.body && error.body.message ? new Error(error.body.message) : error,
|
||||
{
|
||||
title: i18n.LOAD_CONNECTORS_ERROR_MESSAGE,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 { find, getOr, some } from 'lodash/fp';
|
||||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { getFieldValue } from '../../detections/components/host_isolation/helpers';
|
||||
|
||||
/**
|
||||
* Check to see if a timeline event item is an Alert (vs an event)
|
||||
* @param timelineEventItem
|
||||
*/
|
||||
export const isTimelineEventItemAnAlert = (
|
||||
timelineEventItem: TimelineEventsDetailsItem[]
|
||||
): boolean => {
|
||||
return some({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, timelineEventItem);
|
||||
};
|
||||
|
||||
export const SENTINEL_ONE_AGENT_ID_FIELD = 'observer.serial_number';
|
||||
|
||||
export const getSentinelOneAgentId = (data: TimelineEventsDetailsItem[] | null) =>
|
||||
getFieldValue({ category: 'observer', field: SENTINEL_ONE_AGENT_ID_FIELD }, data) || undefined;
|
||||
|
||||
/**
|
||||
* Checks to see if the given set of Timeline event detail items includes data that indicates its
|
||||
* an endpoint Alert. Note that it will NOT match on Events - only alerts
|
||||
* @param data
|
||||
*/
|
||||
export const isAlertFromSentinelOneEvent = ({
|
||||
data,
|
||||
}: {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
}): boolean => {
|
||||
if (!isTimelineEventItemAnAlert(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const findEndpointAlert = find({ field: 'event.module' }, data)?.values;
|
||||
return findEndpointAlert ? findEndpointAlert[0] === 'sentinel_one' : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if the given alert was generated out of the SentinelOne Alerts dataset, coming from
|
||||
* sentinel_one Fleet integration
|
||||
* @param ecsData
|
||||
*/
|
||||
export const isAlertFromSentinelOneAlert = ({
|
||||
ecsData,
|
||||
}: {
|
||||
ecsData: Ecs | null | undefined;
|
||||
}): boolean => {
|
||||
if (ecsData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventModules = getOr([], 'kibana.alert.original_event.module', ecsData);
|
||||
const kinds = getOr([], 'kibana.alert.original_event.dataset', ecsData);
|
||||
|
||||
return eventModules.includes('sentinel_one') && kinds.includes('alert');
|
||||
};
|
|
@ -1416,7 +1416,7 @@ describe('Exception helpers', () => {
|
|||
'execute',
|
||||
'upload_file',
|
||||
];
|
||||
const alertData = {
|
||||
const alertData: AlertData = {
|
||||
'kibana.alert.rule.category': 'Custom Query Rule',
|
||||
'kibana.alert.rule.consumer': 'siem',
|
||||
'kibana.alert.rule.execution.uuid': '28b687e3-8e16-48aa-91b8-bf044d366c2d',
|
||||
|
@ -1471,6 +1471,7 @@ describe('Exception helpers', () => {
|
|||
port: 443,
|
||||
},
|
||||
source: {
|
||||
// @ts-expect-error
|
||||
ports: [1, 2, 4],
|
||||
},
|
||||
flow: {
|
||||
|
@ -1774,6 +1775,10 @@ describe('Exception helpers', () => {
|
|||
overrideField: 'agent.status',
|
||||
label: 'Agent status',
|
||||
},
|
||||
{
|
||||
id: 'observer.serial_number',
|
||||
label: 'Agent status',
|
||||
},
|
||||
{
|
||||
id: 'cloud.provider',
|
||||
},
|
||||
|
@ -1794,6 +1799,10 @@ describe('Exception helpers', () => {
|
|||
label: 'Agent status',
|
||||
overrideField: 'agent.status',
|
||||
},
|
||||
{
|
||||
id: 'observer.serial_number',
|
||||
label: 'Agent status',
|
||||
},
|
||||
{
|
||||
id: 'user.name',
|
||||
},
|
||||
|
|
|
@ -871,7 +871,7 @@ export const buildRuleExceptionWithConditions = ({
|
|||
};
|
||||
|
||||
/**
|
||||
Generate exception conditions based on the highlighted fields of the alert that
|
||||
Generate exception conditions based on the highlighted fields of the alert that
|
||||
have corresponding values in the alert data.
|
||||
For the initial implementation the nested conditions are not considered
|
||||
Converting a singular value to a string or an array of strings
|
||||
|
@ -939,10 +939,10 @@ export const getPrepopulatedRuleExceptionWithHighlightFields = ({
|
|||
};
|
||||
|
||||
/**
|
||||
Filters out the irrelevant highlighted fields for Rule exceptions using
|
||||
Filters out the irrelevant highlighted fields for Rule exceptions using
|
||||
1. The "highlightedFieldsPrefixToExclude" array
|
||||
2. Agent.id field in case the alert was not generated from Endpoint
|
||||
3. Threshold Rule
|
||||
3. Threshold Rule
|
||||
*/
|
||||
export const filterHighlightedFields = (
|
||||
fields: EventSummaryField[],
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { find } from 'lodash/fp';
|
||||
import type { Maybe } from '@kbn/observability-plugin/common/typings';
|
||||
import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
|
||||
import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { IsolateSentinelOneHost } from './isolate_sentinelone';
|
||||
import { UnisolateSentinelOneHost } from './unisolate_sentinelone';
|
||||
import { getFieldValue } from './helpers';
|
||||
import { IsolateHost } from './isolate';
|
||||
import { UnisolateHost } from './unisolate';
|
||||
|
||||
|
@ -20,28 +22,48 @@ export const HostIsolationPanel = React.memo(
|
|||
successCallback,
|
||||
isolateAction,
|
||||
}: {
|
||||
details: Maybe<TimelineEventsDetailsItem[]>;
|
||||
details: TimelineEventsDetailsItem[] | null;
|
||||
cancelCallback: () => void;
|
||||
successCallback?: () => void;
|
||||
isolateAction: string;
|
||||
}) => {
|
||||
const endpointId = useMemo(() => {
|
||||
const findEndpointId = find({ category: 'agent', field: 'agent.id' }, details)?.values;
|
||||
return findEndpointId ? findEndpointId[0] : '';
|
||||
}, [details]);
|
||||
const endpointId = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.id' }, details),
|
||||
[details]
|
||||
);
|
||||
|
||||
const hostName = useMemo(() => {
|
||||
const findHostName = find({ category: 'host', field: 'host.name' }, details)?.values;
|
||||
return findHostName ? findHostName[0] : '';
|
||||
}, [details]);
|
||||
const sentinelOneAgentId = useMemo(() => getSentinelOneAgentId(details), [details]);
|
||||
|
||||
const alertId = useMemo(() => {
|
||||
const findAlertId = find({ category: '_id', field: '_id' }, details)?.values;
|
||||
return findAlertId ? findAlertId[0] : '';
|
||||
}, [details]);
|
||||
const hostName = useMemo(
|
||||
() => getFieldValue({ category: 'host', field: 'host.name' }, details),
|
||||
[details]
|
||||
);
|
||||
|
||||
const alertId = useMemo(
|
||||
() => getFieldValue({ category: '_id', field: '_id' }, details),
|
||||
[details]
|
||||
);
|
||||
|
||||
const { casesInfo } = useCasesFromAlerts({ alertId });
|
||||
|
||||
if (sentinelOneAgentId) {
|
||||
return isolateAction === 'isolateHost' ? (
|
||||
<IsolateSentinelOneHost
|
||||
sentinelOneAgentId={sentinelOneAgentId}
|
||||
hostName={hostName}
|
||||
cancelCallback={cancelCallback}
|
||||
successCallback={successCallback}
|
||||
/>
|
||||
) : (
|
||||
<UnisolateSentinelOneHost
|
||||
sentinelOneAgentId={sentinelOneAgentId}
|
||||
hostName={hostName}
|
||||
cancelCallback={cancelCallback}
|
||||
successCallback={successCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return isolateAction === 'isolateHost' ? (
|
||||
<IsolateHost
|
||||
endpointId={endpointId}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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, { useMemo, useState, useCallback } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { SENTINELONE_CONNECTOR_ID, SUB_ACTION } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { useLoadConnectors } from '../../../common/components/response_actions/use_load_connectors';
|
||||
import { useSubActionMutation } from '../../../timelines/components/side_panel/event_details/flyout/use_sub_action_mutation';
|
||||
import { RETURN_TO_ALERT_DETAILS } from './translations';
|
||||
import {
|
||||
EndpointIsolateForm,
|
||||
ActionCompletionReturnButton,
|
||||
} from '../../../common/components/endpoint/host_isolation';
|
||||
|
||||
export const IsolateSentinelOneHost = React.memo(
|
||||
({
|
||||
sentinelOneAgentId,
|
||||
hostName,
|
||||
cancelCallback,
|
||||
successCallback,
|
||||
}: {
|
||||
sentinelOneAgentId: string;
|
||||
hostName: string;
|
||||
cancelCallback: () => void;
|
||||
successCallback?: () => void;
|
||||
}) => {
|
||||
const { data: connectors } = useLoadConnectors({ actionTypeId: SENTINELONE_CONNECTOR_ID });
|
||||
const connector = useMemo(() => connectors?.[0], [connectors]);
|
||||
|
||||
const [isIsolated, setIsIsolated] = useState(false);
|
||||
|
||||
const { mutateAsync: isolateHost, isLoading } = useSubActionMutation({
|
||||
connectorId: connector?.id as string,
|
||||
subAction: SUB_ACTION.ISOLATE_HOST,
|
||||
subActionParams: {
|
||||
uuid: sentinelOneAgentId,
|
||||
},
|
||||
});
|
||||
|
||||
const onChange = useCallback(() => {}, []);
|
||||
|
||||
const confirmHostIsolation = useCallback(async () => {
|
||||
const response = await isolateHost();
|
||||
|
||||
if (response.status === 'ok') {
|
||||
setIsIsolated(true);
|
||||
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
}, [isolateHost, successCallback]);
|
||||
|
||||
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
||||
|
||||
const hostIsolatedSuccessButton = useMemo(
|
||||
() => (
|
||||
<ActionCompletionReturnButton
|
||||
onClick={backToAlertDetails}
|
||||
buttonText={RETURN_TO_ALERT_DETAILS}
|
||||
/>
|
||||
),
|
||||
[backToAlertDetails]
|
||||
);
|
||||
|
||||
const hostNotIsolated = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EndpointIsolateForm
|
||||
hostName={hostName}
|
||||
onCancel={backToAlertDetails}
|
||||
onConfirm={confirmHostIsolation}
|
||||
isLoading={isLoading}
|
||||
onChange={onChange}
|
||||
hideCommentField
|
||||
/>
|
||||
</>
|
||||
),
|
||||
[hostName, backToAlertDetails, confirmHostIsolation, isLoading, onChange]
|
||||
);
|
||||
|
||||
return isIsolated ? hostIsolatedSuccessButton : hostNotIsolated;
|
||||
}
|
||||
);
|
||||
|
||||
IsolateSentinelOneHost.displayName = 'IsolateSentinelOneHost';
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { SentinelOneAgent } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
|
||||
import { HostStatus } from '../../../../common/endpoint/types';
|
||||
import { getAgentStatusText } from '../../../common/components/endpoint/agent_status_text';
|
||||
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../management/pages/endpoint_hosts/view/host_constants';
|
||||
import { useSentinelOneAgentData } from './use_sentinelone_host_isolation';
|
||||
import {
|
||||
ISOLATING_LABEL,
|
||||
ISOLATED_LABEL,
|
||||
RELEASING_LABEL,
|
||||
} from '../../../common/components/endpoint/endpoint_agent_status';
|
||||
|
||||
const getSentinelOneAgentStatus = (data?: SentinelOneAgent) => {
|
||||
if (!data) {
|
||||
return HostStatus.UNENROLLED;
|
||||
}
|
||||
|
||||
if (!data?.isActive) {
|
||||
return HostStatus.OFFLINE;
|
||||
}
|
||||
|
||||
return HostStatus.HEALTHY;
|
||||
};
|
||||
|
||||
export enum SENTINEL_ONE_NETWORK_STATUS {
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
DISCONNECTING = 'disconnecting',
|
||||
DISCONNECTED = 'disconnected',
|
||||
}
|
||||
|
||||
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
|
||||
.isolation-status {
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeS};
|
||||
}
|
||||
`;
|
||||
|
||||
export const SentinelOneAgentStatus = React.memo(
|
||||
({ agentId, dataTestSubj }: { agentId: string; dataTestSubj?: string }) => {
|
||||
const { data, isFetched } = useSentinelOneAgentData({ agentId });
|
||||
|
||||
const label = useMemo(() => {
|
||||
const networkStatus = data?.data?.data?.[0]?.networkStatus;
|
||||
|
||||
if (networkStatus === SENTINEL_ONE_NETWORK_STATUS.DISCONNECTING) {
|
||||
return ISOLATING_LABEL;
|
||||
}
|
||||
|
||||
if (networkStatus === SENTINEL_ONE_NETWORK_STATUS.DISCONNECTED) {
|
||||
return ISOLATED_LABEL;
|
||||
}
|
||||
|
||||
if (networkStatus === SENTINEL_ONE_NETWORK_STATUS.CONNECTING) {
|
||||
return RELEASING_LABEL;
|
||||
}
|
||||
}, [data?.data?.data]);
|
||||
|
||||
const agentStatus = useMemo(() => getSentinelOneAgentStatus(data?.data?.data?.[0]), [data]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroupStyled
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isFetched ? (
|
||||
<EuiBadge color={HOST_STATUS_TO_BADGE_COLOR[agentStatus]} className="eui-textTruncate">
|
||||
{getAgentStatusText(agentStatus)}
|
||||
</EuiBadge>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{label && (
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
|
||||
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
|
||||
<>{label}</>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroupStyled>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SentinelOneAgentStatus.displayName = 'SentinelOneAgentStatus';
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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, { useMemo, useState, useCallback } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { SENTINELONE_CONNECTOR_ID, SUB_ACTION } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { useLoadConnectors } from '../../../common/components/response_actions/use_load_connectors';
|
||||
import { useSubActionMutation } from '../../../timelines/components/side_panel/event_details/flyout/use_sub_action_mutation';
|
||||
import { RETURN_TO_ALERT_DETAILS } from './translations';
|
||||
import {
|
||||
EndpointUnisolateForm,
|
||||
ActionCompletionReturnButton,
|
||||
} from '../../../common/components/endpoint/host_isolation';
|
||||
|
||||
export const UnisolateSentinelOneHost = React.memo(
|
||||
({
|
||||
sentinelOneAgentId,
|
||||
hostName,
|
||||
cancelCallback,
|
||||
successCallback,
|
||||
}: {
|
||||
sentinelOneAgentId: string;
|
||||
hostName: string;
|
||||
cancelCallback: () => void;
|
||||
successCallback?: () => void;
|
||||
}) => {
|
||||
const [isUnIsolated, setIsUnIsolated] = useState(false);
|
||||
const { data: connectors } = useLoadConnectors({ actionTypeId: SENTINELONE_CONNECTOR_ID });
|
||||
const connector = useMemo(() => connectors?.[0], [connectors]);
|
||||
|
||||
const { mutateAsync: unIsolateHost, isLoading } = useSubActionMutation({
|
||||
// @ts-expect-error update types
|
||||
connectorId: connector?.id,
|
||||
subAction: SUB_ACTION.RELEASE_HOST,
|
||||
subActionParams: {
|
||||
uuid: sentinelOneAgentId,
|
||||
},
|
||||
});
|
||||
|
||||
const confirmHostUnIsolation = useCallback(async () => {
|
||||
const response = await unIsolateHost();
|
||||
|
||||
if (response.status === 'ok') {
|
||||
setIsUnIsolated(true);
|
||||
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
}, [successCallback, unIsolateHost]);
|
||||
|
||||
const onChange = useCallback(() => {}, []);
|
||||
|
||||
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
||||
|
||||
const hostUnisolatedSuccessButton = useMemo(() => {
|
||||
return (
|
||||
<ActionCompletionReturnButton
|
||||
onClick={backToAlertDetails}
|
||||
buttonText={RETURN_TO_ALERT_DETAILS}
|
||||
/>
|
||||
);
|
||||
}, [backToAlertDetails]);
|
||||
|
||||
const hostNotUnisolated = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EndpointUnisolateForm
|
||||
hostName={hostName}
|
||||
onCancel={backToAlertDetails}
|
||||
onConfirm={confirmHostUnIsolation}
|
||||
onChange={onChange}
|
||||
isLoading={isLoading}
|
||||
hideCommentField
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [hostName, backToAlertDetails, confirmHostUnIsolation, onChange, isLoading]);
|
||||
|
||||
return isUnIsolated ? hostUnisolatedSuccessButton : hostNotUnisolated;
|
||||
}
|
||||
);
|
||||
|
||||
UnisolateSentinelOneHost.displayName = 'UnisolateSentinelOneHost';
|
|
@ -5,6 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useKibana } from '../../../common/lib/kibana/kibana_react';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import {
|
||||
getSentinelOneAgentId,
|
||||
isAlertFromSentinelOneEvent,
|
||||
} from '../../../common/utils/sentinelone_alert_check';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { isIsolationSupported } from '../../../../common/endpoint/service/host_isolation/utils';
|
||||
import { HostStatus } from '../../../../common/endpoint/types';
|
||||
|
@ -14,6 +20,7 @@ import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
|
|||
import { getFieldValue } from './helpers';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import type { AlertTableContextMenuItem } from '../alerts_table/types';
|
||||
import { useSentinelOneAgentData } from './use_sentinelone_host_isolation';
|
||||
|
||||
interface UseHostIsolationActionProps {
|
||||
closePopover: () => void;
|
||||
|
@ -28,17 +35,30 @@ export const useHostIsolationAction = ({
|
|||
isHostIsolationPanelOpen,
|
||||
onAddIsolationStatusClick,
|
||||
}: UseHostIsolationActionProps): AlertTableContextMenuItem[] => {
|
||||
const hasActionsAllPrivileges = useKibana().services.application?.capabilities?.actions?.save;
|
||||
|
||||
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
|
||||
'sentinelOneManualHostActionsEnabled'
|
||||
);
|
||||
const { canIsolateHost, canUnIsolateHost } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
const isEndpointAlert = useMemo(() => {
|
||||
return isAlertFromEndpointEvent({ data: detailsData || [] });
|
||||
}, [detailsData]);
|
||||
const isEndpointAlert = useMemo(
|
||||
() => isAlertFromEndpointEvent({ data: detailsData || [] }),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const isSentinelOneAlert = useMemo(
|
||||
() => isAlertFromSentinelOneEvent({ data: detailsData || [] }),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const agentId = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.id' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const sentinelOneAgentId = useMemo(() => getSentinelOneAgentId(detailsData), [detailsData]);
|
||||
|
||||
const hostOsFamily = useMemo(
|
||||
() => getFieldValue({ category: 'host', field: 'host.os.name' }, detailsData),
|
||||
[detailsData]
|
||||
|
@ -51,22 +71,55 @@ export const useHostIsolationAction = ({
|
|||
|
||||
const {
|
||||
loading: loadingHostIsolationStatus,
|
||||
isIsolated: isHostIsolated,
|
||||
isIsolated,
|
||||
agentStatus,
|
||||
capabilities,
|
||||
} = useHostIsolationStatus({
|
||||
agentId,
|
||||
});
|
||||
|
||||
const { data: sentinelOneResponse } = useSentinelOneAgentData({ agentId: sentinelOneAgentId });
|
||||
|
||||
const sentinelOneAgentData = useMemo(
|
||||
() => sentinelOneResponse?.data?.data?.[0],
|
||||
[sentinelOneResponse]
|
||||
);
|
||||
|
||||
const isHostIsolated = useMemo(() => {
|
||||
if (sentinelOneManualHostActionsEnabled && isSentinelOneAlert) {
|
||||
return sentinelOneAgentData?.networkStatus === 'disconnected';
|
||||
}
|
||||
|
||||
return isIsolated;
|
||||
}, [
|
||||
isIsolated,
|
||||
isSentinelOneAlert,
|
||||
sentinelOneAgentData?.networkStatus,
|
||||
sentinelOneManualHostActionsEnabled,
|
||||
]);
|
||||
|
||||
const doesHostSupportIsolation = useMemo(() => {
|
||||
return isEndpointAlert
|
||||
? isIsolationSupported({
|
||||
osName: hostOsFamily,
|
||||
version: agentVersion,
|
||||
capabilities,
|
||||
})
|
||||
: false;
|
||||
}, [agentVersion, capabilities, hostOsFamily, isEndpointAlert]);
|
||||
if (isEndpointAlert) {
|
||||
return isIsolationSupported({
|
||||
osName: hostOsFamily,
|
||||
version: agentVersion,
|
||||
capabilities,
|
||||
});
|
||||
}
|
||||
|
||||
if (sentinelOneManualHostActionsEnabled && isSentinelOneAlert && sentinelOneAgentData) {
|
||||
return sentinelOneAgentData.isActive;
|
||||
}
|
||||
return false;
|
||||
}, [
|
||||
agentVersion,
|
||||
capabilities,
|
||||
hostOsFamily,
|
||||
isEndpointAlert,
|
||||
isSentinelOneAlert,
|
||||
sentinelOneAgentData,
|
||||
sentinelOneManualHostActionsEnabled,
|
||||
]);
|
||||
|
||||
const isolateHostHandler = useCallback(() => {
|
||||
closePopover();
|
||||
|
@ -77,36 +130,69 @@ export const useHostIsolationAction = ({
|
|||
}
|
||||
}, [closePopover, isHostIsolated, onAddIsolationStatusClick]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
!isEndpointAlert ||
|
||||
!doesHostSupportIsolation ||
|
||||
loadingHostIsolationStatus ||
|
||||
isHostIsolationPanelOpen
|
||||
) {
|
||||
return [];
|
||||
const menuItemDisabled = useMemo(() => {
|
||||
if (sentinelOneManualHostActionsEnabled && isSentinelOneAlert) {
|
||||
return (
|
||||
!sentinelOneAgentData ||
|
||||
sentinelOneAgentData?.isUninstalled ||
|
||||
sentinelOneAgentData?.isPendingUninstall
|
||||
);
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
return agentStatus === HostStatus.UNENROLLED;
|
||||
}, [agentStatus, isSentinelOneAlert, sentinelOneAgentData, sentinelOneManualHostActionsEnabled]);
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'isolate-host-action-item',
|
||||
'data-test-subj': 'isolate-host-action-item',
|
||||
disabled: agentStatus === HostStatus.UNENROLLED,
|
||||
disabled: menuItemDisabled,
|
||||
onClick: isolateHostHandler,
|
||||
name: isHostIsolated ? UNISOLATE_HOST : ISOLATE_HOST,
|
||||
},
|
||||
];
|
||||
],
|
||||
[isHostIsolated, isolateHostHandler, menuItemDisabled]
|
||||
);
|
||||
|
||||
return canIsolateHost || (isHostIsolated && canUnIsolateHost) ? menuItems : [];
|
||||
return useMemo(() => {
|
||||
if (isHostIsolationPanelOpen) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
isSentinelOneAlert &&
|
||||
sentinelOneManualHostActionsEnabled &&
|
||||
sentinelOneAgentId &&
|
||||
sentinelOneAgentData &&
|
||||
hasActionsAllPrivileges
|
||||
) {
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
if (
|
||||
isEndpointAlert &&
|
||||
doesHostSupportIsolation &&
|
||||
!loadingHostIsolationStatus &&
|
||||
(canIsolateHost || (isHostIsolated && !canUnIsolateHost))
|
||||
) {
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [
|
||||
isEndpointAlert,
|
||||
doesHostSupportIsolation,
|
||||
loadingHostIsolationStatus,
|
||||
isHostIsolationPanelOpen,
|
||||
agentStatus,
|
||||
isolateHostHandler,
|
||||
canIsolateHost,
|
||||
isHostIsolated,
|
||||
canUnIsolateHost,
|
||||
doesHostSupportIsolation,
|
||||
hasActionsAllPrivileges,
|
||||
isEndpointAlert,
|
||||
isHostIsolated,
|
||||
isHostIsolationPanelOpen,
|
||||
isSentinelOneAlert,
|
||||
loadingHostIsolationStatus,
|
||||
menuItems,
|
||||
sentinelOneAgentData,
|
||||
sentinelOneAgentId,
|
||||
sentinelOneManualHostActionsEnabled,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -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 { isEmpty } from 'lodash';
|
||||
import type {
|
||||
SentinelOneGetAgentsParams,
|
||||
SentinelOneGetAgentsResponse,
|
||||
} from '@kbn/stack-connectors-plugin/common/sentinelone/types';
|
||||
import { SENTINELONE_CONNECTOR_ID, SUB_ACTION } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { useSubAction } from '../../../timelines/components/side_panel/event_details/flyout/use_sub_action';
|
||||
import { useLoadConnectors } from '../../../common/components/response_actions/use_load_connectors';
|
||||
import { SENTINEL_ONE_NETWORK_STATUS } from './sentinel_one_agent_status';
|
||||
|
||||
/**
|
||||
* Using SentinelOne connector to pull agent's data from the SentinelOne API. If the agentId is in the transition state
|
||||
* (isolating/releasing) it will keep pulling the state until it finalizes the action
|
||||
* @param agentId
|
||||
*/
|
||||
export const useSentinelOneAgentData = ({ agentId }: { agentId?: string }) => {
|
||||
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
|
||||
'sentinelOneManualHostActionsEnabled'
|
||||
);
|
||||
const { data: connector } = useLoadConnectors({ actionTypeId: SENTINELONE_CONNECTOR_ID });
|
||||
|
||||
return useSubAction<SentinelOneGetAgentsParams, SentinelOneGetAgentsResponse>({
|
||||
connectorId: connector?.[0]?.id,
|
||||
subAction: SUB_ACTION.GET_AGENTS,
|
||||
subActionParams: {
|
||||
uuid: agentId,
|
||||
},
|
||||
disabled: !sentinelOneManualHostActionsEnabled || isEmpty(agentId),
|
||||
// @ts-expect-error update types
|
||||
refetchInterval: (lastResponse: { data: SentinelOneGetAgentsResponse }) => {
|
||||
const networkStatus = lastResponse?.data?.data?.[0]
|
||||
.networkStatus as SENTINEL_ONE_NETWORK_STATUS;
|
||||
|
||||
return [
|
||||
SENTINEL_ONE_NETWORK_STATUS.CONNECTING,
|
||||
SENTINEL_ONE_NETWORK_STATUS.DISCONNECTING,
|
||||
].includes(networkStatus)
|
||||
? 5000
|
||||
: false;
|
||||
},
|
||||
});
|
||||
};
|
|
@ -9,6 +9,8 @@ import type { VFC } from 'react';
|
|||
import React, { useCallback } from 'react';
|
||||
import { EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { SentinelOneAgentStatus } from '../../../../detections/components/host_isolation/sentinel_one_agent_status';
|
||||
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../common/utils/sentinelone_alert_check';
|
||||
import { EndpointAgentStatusById } from '../../../../common/components/endpoint/endpoint_agent_status';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import {
|
||||
|
@ -90,6 +92,8 @@ export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({ values,
|
|||
endpointAgentId={String(value ?? '')}
|
||||
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
|
||||
/>
|
||||
) : field === SENTINEL_ONE_AGENT_ID_FIELD ? (
|
||||
<SentinelOneAgentStatus agentId={String(value ?? '')} />
|
||||
) : (
|
||||
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
|
||||
)}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// @ts-expect-error
|
||||
import registerDataSession from 'cypress-data-session/src/plugin';
|
||||
import { merge } from 'lodash';
|
||||
import { getVideosForFailedSpecs } from './support/filter_videos';
|
||||
import { setupToolingLogLevel } from './support/setup_tooling_log_level';
|
||||
|
@ -74,7 +76,8 @@ export const getCypressBaseConfig = (
|
|||
experimentalRunAllSpecs: true,
|
||||
experimentalMemoryManagement: true,
|
||||
experimentalInteractiveRunEvents: true,
|
||||
setupNodeEvents: async (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => {
|
||||
setupNodeEvents: (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => {
|
||||
registerDataSession(on, config);
|
||||
// IMPORTANT: setting the log level should happen before any tooling is called
|
||||
setupToolingLogLevel(config);
|
||||
|
||||
|
|
|
@ -27,7 +27,15 @@ describe(
|
|||
// `internal/kibana/settings` is not accessible in serverless
|
||||
'@brokenInServerless',
|
||||
],
|
||||
env: { ftrConfig: { enableExperimental: ['protectionUpdatesEnabled'] } },
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'sentinelOneManualHostActionsEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
describe('Protection updates', () => {
|
||||
|
|
|
@ -20,7 +20,15 @@ describe(
|
|||
// The `disableExpandableFlyoutAdvancedSettings()` fails because the API
|
||||
// `internal/kibana/settings` is not accessible in serverless
|
||||
tags: ['@ess', '@serverless', '@brokenInServerless'],
|
||||
env: { ftrConfig: { enableExperimental: ['protectionUpdatesEnabled'] } },
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'protectionUpdatesEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
// Today API wont let us create a policy with a manifest version before October 1st 2023
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 { recurse } from 'cypress-recurse';
|
||||
import { openAlertDetailsView } from '../../screens/alerts';
|
||||
import { closeAllToasts } from '../../tasks/toasts';
|
||||
import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate';
|
||||
import { cleanupRule, loadRule } from '../../tasks/api_fixtures';
|
||||
import { ROLE, login } from '../../tasks/login';
|
||||
import { disableExpandableFlyoutAdvancedSettings } from '../../tasks/common';
|
||||
import { waitForAlertsToPopulate } from '../../tasks/alerts';
|
||||
|
||||
describe.skip(
|
||||
'Isolate command',
|
||||
{
|
||||
tags: [
|
||||
'@ess',
|
||||
'@serverless',
|
||||
|
||||
// Not supported in serverless!
|
||||
// The `disableExpandableFlyoutAdvancedSettings()` fails because the API
|
||||
// `internal/kibana/settings` is not accessible in serverless
|
||||
'@brokenInServerless',
|
||||
],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'sentinelOneManualHostActionsEnabled',
|
||||
])}`,
|
||||
`--xpack.stack_connectors.enableExperimental=${JSON.stringify([
|
||||
'sentinelOneConnectorOn',
|
||||
])}`,
|
||||
`--xpack.actions.preconfigured=${JSON.stringify({
|
||||
'preconfigured-sentinelone': {
|
||||
name: 'preconfigured-sentinelone',
|
||||
actionTypeId: '.sentinelone',
|
||||
config: {
|
||||
url: process.env.CYPRESS_SENTINELONE_URL,
|
||||
},
|
||||
secrets: {
|
||||
token: process.env.CYPRESS_SENTINELONE_TOKEN,
|
||||
},
|
||||
},
|
||||
})}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
before(() => {
|
||||
cy.dataSession(
|
||||
'SENTINELONE_HOST', // data name
|
||||
() => cy.task('createSentinelOneHost', {}, { timeout: 1000000 }),
|
||||
() => true
|
||||
);
|
||||
cy.dataSession('SENTINELONE_POLICY', () => cy.task('createSentinelOneAgentPolicy'), true);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.dataSession('SENTINELONE_HOST').then((createdHost) => {
|
||||
if (createdHost && !Cypress.env('isInteractive')) {
|
||||
createdHost.destroy();
|
||||
}
|
||||
});
|
||||
cy.dataSession('SENTINELONE_POLICY').then((agentPolicy) => {
|
||||
if (agentPolicy) {
|
||||
agentPolicy.stop();
|
||||
cy.task('deleteFleetServerPolicy', agentPolicy.policyId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLE.soc_manager);
|
||||
disableExpandableFlyoutAdvancedSettings();
|
||||
});
|
||||
|
||||
describe('From alerts', () => {
|
||||
let ruleId: string;
|
||||
let ruleName: string;
|
||||
|
||||
before(() => {
|
||||
cy.dataSession('SENTINELONE_HOST').then((createdHost) =>
|
||||
loadRule(
|
||||
{
|
||||
index: ['logs-sentinel_one.alert*'],
|
||||
query: `host.name: ${createdHost.name} and observer.serial_number:*`,
|
||||
from: 'now-3660s',
|
||||
},
|
||||
false
|
||||
).then((data) => {
|
||||
ruleId = data.id;
|
||||
ruleName = data.name;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (ruleId) {
|
||||
cleanupRule(ruleId);
|
||||
}
|
||||
});
|
||||
|
||||
it('should isolate and release host', () => {
|
||||
toggleRuleOffAndOn(ruleName);
|
||||
visitRuleAlerts(ruleName);
|
||||
waitForAlertsToPopulate(1, 10000, 600000);
|
||||
|
||||
closeAllToasts();
|
||||
openAlertDetailsView();
|
||||
|
||||
cy.getByTestSubj('isolate-host-action-item').click();
|
||||
cy.getByTestSubj('hostIsolateConfirmButton').click();
|
||||
cy.dataSession('SENTINELONE_HOST').then((createdHost) => {
|
||||
cy.log('create', createdHost);
|
||||
cy.getByTestSubj('hostIsolateSuccessMessage').should(
|
||||
'contain.text',
|
||||
`Isolation on host ${createdHost.name} successfully submitted`
|
||||
);
|
||||
});
|
||||
|
||||
cy.getByTestSubj('euiFlyoutCloseButton').click();
|
||||
|
||||
recurse<string>(
|
||||
() => {
|
||||
openAlertDetailsView();
|
||||
return cy.getByTestSubj('isolate-host-action-item').invoke('text');
|
||||
},
|
||||
(text) => text === 'Release host',
|
||||
{
|
||||
log: true,
|
||||
delay: 30000,
|
||||
timeout: 600000,
|
||||
post: () => cy.getByTestSubj('euiFlyoutCloseButton').click(),
|
||||
}
|
||||
);
|
||||
|
||||
cy.getByTestSubj('isolate-host-action-item').should('contain.text', 'Release host', {
|
||||
timeout: 120000,
|
||||
});
|
||||
cy.getByTestSubj('isolate-host-action-item').click();
|
||||
|
||||
cy.contains('Confirm').click();
|
||||
cy.dataSession('SENTINELONE_HOST').then((createdHost) => {
|
||||
cy.getByTestSubj('hostUnisolateSuccessMessage').should(
|
||||
'contain.text',
|
||||
`Release on host ${createdHost.name} successfully submitted`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -11,7 +11,11 @@ import type { CasePostRequest } from '@kbn/cases-plugin/common';
|
|||
import execa from 'execa';
|
||||
import type { KbnClient } from '@kbn/test';
|
||||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import { getHostVmClient } from '../../../../scripts/endpoint/common/vm_services';
|
||||
import {
|
||||
getHostVmClient,
|
||||
createVm,
|
||||
generateVmName,
|
||||
} from '../../../../scripts/endpoint/common/vm_services';
|
||||
import { setupStackServicesUsingCypressConfig } from './common';
|
||||
import type { KibanaKnownUserAccounts } from '../common/constants';
|
||||
import { KIBANA_KNOWN_DEFAULT_ACCOUNTS } from '../common/constants';
|
||||
|
@ -57,6 +61,17 @@ import type { IndexedHostsAndAlertsResponse } from '../../../../common/endpoint/
|
|||
import { deleteIndexedHostsAndAlerts } from '../../../../common/endpoint/index_data';
|
||||
import type { IndexedCase } from '../../../../common/endpoint/data_loaders/index_case';
|
||||
import { deleteIndexedCase, indexCase } from '../../../../common/endpoint/data_loaders/index_case';
|
||||
import {
|
||||
installSentinelOneAgent,
|
||||
S1Client,
|
||||
} from '../../../../scripts/endpoint/sentinelone_host/common';
|
||||
import {
|
||||
addSentinelOneIntegrationToAgentPolicy,
|
||||
deleteAgentPolicy,
|
||||
fetchAgentPolicyEnrollmentKey,
|
||||
getOrCreateDefaultAgentPolicy,
|
||||
} from '../../../../scripts/endpoint/common/fleet_services';
|
||||
import { startElasticAgentWithDocker } from '../../../../scripts/endpoint/common/elastic_agent_service';
|
||||
import type { IndexedFleetEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
|
||||
import {
|
||||
deleteIndexedFleetEndpointPolicies,
|
||||
|
@ -291,6 +306,87 @@ export const dataLoadersForRealEndpoints = (
|
|||
const stackServicesPromise = setupStackServicesUsingCypressConfig(config);
|
||||
|
||||
on('task', {
|
||||
createSentinelOneHost: async () => {
|
||||
if (!process.env.CYPRESS_SENTINELONE_URL || !process.env.CYPRESS_SENTINELONE_TOKEN) {
|
||||
throw new Error('CYPRESS_SENTINELONE_URL and CYPRESS_SENTINELONE_TOKEN must be set');
|
||||
}
|
||||
|
||||
const { log } = await stackServicesPromise;
|
||||
const s1Client = new S1Client({
|
||||
url: process.env.CYPRESS_SENTINELONE_URL,
|
||||
apiToken: process.env.CYPRESS_SENTINELONE_TOKEN,
|
||||
log,
|
||||
});
|
||||
|
||||
const vmName = generateVmName('sentinelone');
|
||||
|
||||
const hostVm = await createVm({
|
||||
type: 'multipass',
|
||||
name: vmName,
|
||||
log,
|
||||
memory: '2G',
|
||||
disk: '10G',
|
||||
});
|
||||
|
||||
const s1Info = await installSentinelOneAgent({
|
||||
hostVm,
|
||||
log,
|
||||
s1Client,
|
||||
});
|
||||
|
||||
// wait 30s before running malicious action
|
||||
await new Promise((resolve) => setTimeout(resolve, 30000));
|
||||
|
||||
// nslookup triggers an alert on S1
|
||||
await getHostVmClient(vmName).exec('nslookup amazon.com');
|
||||
|
||||
log.info(`Done!
|
||||
|
||||
${hostVm.info()}
|
||||
|
||||
SentinelOne Agent Status:
|
||||
${s1Info.status}
|
||||
`);
|
||||
|
||||
return hostVm;
|
||||
},
|
||||
|
||||
createSentinelOneAgentPolicy: async () => {
|
||||
if (!process.env.CYPRESS_SENTINELONE_URL || !process.env.CYPRESS_SENTINELONE_TOKEN) {
|
||||
throw new Error('CYPRESS_SENTINELONE_URL and CYPRESS_SENTINELONE_TOKEN must be set');
|
||||
}
|
||||
|
||||
const { log, kbnClient } = await stackServicesPromise;
|
||||
const agentPolicyId = (await getOrCreateDefaultAgentPolicy({ kbnClient, log })).id;
|
||||
|
||||
await addSentinelOneIntegrationToAgentPolicy({
|
||||
kbnClient,
|
||||
log,
|
||||
agentPolicyId,
|
||||
consoleUrl: process.env.CYPRESS_SENTINELONE_URL,
|
||||
apiToken: process.env.CYPRESS_SENTINELONE_TOKEN,
|
||||
});
|
||||
|
||||
const enrollmentToken = await fetchAgentPolicyEnrollmentKey(kbnClient, agentPolicyId);
|
||||
|
||||
const elasticAgent = await startElasticAgentWithDocker({
|
||||
kbnClient,
|
||||
logger: log,
|
||||
enrollmentToken,
|
||||
});
|
||||
|
||||
return {
|
||||
...elasticAgent,
|
||||
policyId: agentPolicyId,
|
||||
};
|
||||
},
|
||||
|
||||
deleteAgentPolicy: async (agentPolicyId: string) => {
|
||||
const { kbnClient } = await stackServicesPromise;
|
||||
|
||||
await deleteAgentPolicy(kbnClient, agentPolicyId);
|
||||
},
|
||||
|
||||
createEndpointHost: async (
|
||||
options: Omit<CreateAndEnrollEndpointHostCIOptions, 'log' | 'kbnClient'>
|
||||
): Promise<CreateAndEnrollEndpointHostCIResponse> => {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// ***********************************************************
|
||||
|
||||
import { subj as testSubjSelector } from '@kbn/test-subj-selector';
|
||||
import 'cypress-data-session';
|
||||
// @ts-ignore
|
||||
import registerCypressGrep from '@cypress/grep';
|
||||
|
||||
|
|
|
@ -185,3 +185,75 @@ export const changeAlertsFilter = (text: string) => {
|
|||
cy.getByTestSubj('querySubmitButton').click();
|
||||
});
|
||||
};
|
||||
|
||||
/* copied from test/security_solution_cypress/cypress/tasks/create_new_rule */
|
||||
export const DETECTION_PAGE_FILTER_GROUP_WRAPPER = '.filter-group__wrapper';
|
||||
export const DETECTION_PAGE_FILTERS_LOADING = '.securityPageWrapper .controlFrame--controlLoading';
|
||||
export const DETECTION_PAGE_FILTER_GROUP_LOADING = '[data-test-subj="filter-group__loading"]';
|
||||
export const OPTION_LISTS_LOADING = '.optionsList--filterBtnWrapper .euiLoadingSpinner';
|
||||
export const DATAGRID_CHANGES_IN_PROGRESS = '[data-test-subj="body-data-grid"] .euiProgress';
|
||||
export const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="internalAlertsPageLoading"]';
|
||||
export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]';
|
||||
export const ALERTS_URL = '/app/security/alerts';
|
||||
export const GLOBAL_KQL_WRAPPER = '[data-test-subj="filters-global-container"]';
|
||||
export const REFRESH_BUTTON = `${GLOBAL_KQL_WRAPPER} [data-test-subj="querySubmitButton"]`;
|
||||
export const EMPTY_ALERT_TABLE = '[data-test-subj="alertsStateTableEmptyState"]';
|
||||
export const ALERTS_TABLE_COUNT = `[data-test-subj="toolbar-alerts-count"]`;
|
||||
|
||||
export const waitForPageFilters = () => {
|
||||
cy.log('Waiting for Page Filters');
|
||||
cy.url().then((urlString) => {
|
||||
const url = new URL(urlString);
|
||||
if (url.pathname.endsWith(ALERTS_URL)) {
|
||||
// since these are only valid on the alert page
|
||||
cy.get(DETECTION_PAGE_FILTER_GROUP_WRAPPER).should('exist');
|
||||
cy.get(DETECTION_PAGE_FILTER_GROUP_LOADING).should('not.exist');
|
||||
cy.get(DETECTION_PAGE_FILTERS_LOADING).should('not.exist');
|
||||
cy.get(OPTION_LISTS_LOADING).should('have.lengthOf', 0);
|
||||
} else {
|
||||
cy.log('Skipping Page Filters Wait');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const waitForAlerts = () => {
|
||||
/*
|
||||
* below line commented because alertpagefiltersenabled feature flag
|
||||
* is disabled by default
|
||||
* target: enable by default in v8.8
|
||||
*
|
||||
* waitforpagefilters();
|
||||
*
|
||||
* */
|
||||
waitForPageFilters();
|
||||
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
|
||||
cy.get(DATAGRID_CHANGES_IN_PROGRESS).should('not.be.true');
|
||||
cy.get(EVENT_CONTAINER_TABLE_LOADING).should('not.exist');
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
};
|
||||
|
||||
export const waitForAlertsToPopulate = (
|
||||
alertCountThreshold = 1,
|
||||
interval = 500,
|
||||
timeout = 12000
|
||||
) => {
|
||||
cy.waitUntil(
|
||||
() => {
|
||||
cy.log('Waiting for alerts to appear');
|
||||
cy.get(REFRESH_BUTTON).click({ force: true });
|
||||
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
|
||||
return cy.root().then(($el) => {
|
||||
const emptyTableState = $el.find(EMPTY_ALERT_TABLE);
|
||||
if (emptyTableState.length > 0) {
|
||||
cy.log('Table is empty', emptyTableState.length);
|
||||
return false;
|
||||
}
|
||||
const countEl = $el.find(ALERTS_TABLE_COUNT);
|
||||
const alertCount = parseInt(countEl.text(), 10) || 0;
|
||||
return alertCount >= alertCountThreshold;
|
||||
});
|
||||
},
|
||||
{ interval, timeout }
|
||||
);
|
||||
waitForAlerts();
|
||||
};
|
||||
|
|
|
@ -9,8 +9,8 @@ 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;
|
||||
export interface UseSubActionParams<P, R> {
|
||||
connectorId?: string;
|
||||
subAction: string;
|
||||
subActionParams?: P;
|
||||
disabled?: boolean;
|
||||
|
@ -22,14 +22,14 @@ export const useSubAction = <P, R>({
|
|||
subActionParams,
|
||||
disabled = false,
|
||||
...rest
|
||||
}: UseSubActionParams<P>) => {
|
||||
}: UseSubActionParams<P, R>) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['useSubAction', connectorId, subAction, subActionParams],
|
||||
queryFn: ({ signal }) =>
|
||||
executeAction<R>({
|
||||
id: connectorId,
|
||||
id: connectorId as string,
|
||||
params: {
|
||||
subAction,
|
||||
subActionParams,
|
||||
|
|
|
@ -13,18 +13,17 @@ 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({
|
||||
mutationKey: ['executeSubAction', connectorId, subAction, subActionParams],
|
||||
mutationFn: () =>
|
||||
executeAction<R>({
|
||||
id: connectorId,
|
||||
|
|
|
@ -12,6 +12,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
|||
import { isNumber, isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../../common/utils/sentinelone_alert_check';
|
||||
import { SentinelOneAgentStatus } from '../../../../../detections/components/host_isolation/sentinel_one_agent_status';
|
||||
import { EndpointAgentStatusById } from '../../../../../common/components/endpoint/endpoint_agent_status';
|
||||
import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants';
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
|
@ -245,6 +247,8 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
data-test-subj="endpointHostAgentStatus"
|
||||
/>
|
||||
);
|
||||
} else if (fieldName === SENTINEL_ONE_AGENT_ID_FIELD) {
|
||||
return <SentinelOneAgentStatus agentId={String(value ?? '')} />;
|
||||
} else if (
|
||||
[
|
||||
RULE_REFERENCE_FIELD_NAME,
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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 { verifyDockerInstalled } from '@kbn/es';
|
||||
import assert from 'assert';
|
||||
import type { KbnClient } from '@kbn/test';
|
||||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import chalk from 'chalk';
|
||||
import execa from 'execa';
|
||||
import { dump } from 'js-yaml';
|
||||
import {
|
||||
fetchFleetServerUrl,
|
||||
getAgentVersionMatchingCurrentStack,
|
||||
waitForHostToEnroll,
|
||||
} from './fleet_services';
|
||||
|
||||
interface StartedElasticAgent {
|
||||
/** The type of virtualization used to start the agent */
|
||||
type: 'docker';
|
||||
/** The ID of the agent */
|
||||
id: string;
|
||||
/** The name of the agent */
|
||||
name: string;
|
||||
/** Stop agent */
|
||||
stop: () => Promise<void>;
|
||||
/** Any information about the agent */
|
||||
info?: string;
|
||||
}
|
||||
|
||||
interface StartElasticAgentWithDockerOptions {
|
||||
kbnClient: KbnClient;
|
||||
logger: ToolingLog;
|
||||
containerName?: string;
|
||||
/** The enrollment token for Elastic Agent */
|
||||
enrollmentToken?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const startElasticAgentWithDocker = async ({
|
||||
kbnClient,
|
||||
logger: log,
|
||||
enrollmentToken = '',
|
||||
version,
|
||||
containerName = 'elastic-agent',
|
||||
}: StartElasticAgentWithDockerOptions): Promise<StartedElasticAgent> => {
|
||||
await verifyDockerInstalled(log);
|
||||
|
||||
const agentVersion = version || (await getAgentVersionMatchingCurrentStack(kbnClient));
|
||||
|
||||
log.info(`Starting a new Elastic agent using Docker (version: ${agentVersion})`);
|
||||
|
||||
const response: StartedElasticAgent = await log.indent(4, async () => {
|
||||
const fleetServerUrl = await fetchFleetServerUrl(kbnClient);
|
||||
const hostname = `${containerName}.${Math.random().toString(32).substring(2, 6)}`;
|
||||
let containerId = '';
|
||||
let elasticAgentVersionInfo = '';
|
||||
|
||||
assert.ok(!!fleetServerUrl, '`fleetServerUrl` is required');
|
||||
assert.ok(!!enrollmentToken, '`enrollmentToken` is required');
|
||||
|
||||
try {
|
||||
const dockerArgs = getElasticAgentDockerArgs({
|
||||
containerName: hostname,
|
||||
hostname,
|
||||
enrollmentToken,
|
||||
agentVersion,
|
||||
fleetServerUrl: fleetServerUrl as string,
|
||||
});
|
||||
|
||||
await execa('docker', ['kill', containerName])
|
||||
.then(() => {
|
||||
log.verbose(
|
||||
`Killed an existing container with name [${containerName}]. New one will be started.`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!/no such container/i.test(error.message)) {
|
||||
log.verbose(`Attempt to kill currently running elastic-agent container with name [${containerName}] was unsuccessful:
|
||||
${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
log.verbose(`docker arguments:\n${dockerArgs.join(' ')}`);
|
||||
|
||||
containerId = (await execa('docker', dockerArgs)).stdout;
|
||||
|
||||
log.info(`Elastic agent container started`);
|
||||
|
||||
{
|
||||
log.info('Waiting for Elastic Agent to show up in Kibana Fleet');
|
||||
|
||||
const elasticAgent = await waitForHostToEnroll(kbnClient, log, hostname, 120000);
|
||||
|
||||
log.verbose(`Enrolled agent:\n${JSON.stringify(elasticAgent, null, 2)}`);
|
||||
}
|
||||
|
||||
elasticAgentVersionInfo = (
|
||||
await execa('docker', [
|
||||
'exec',
|
||||
containerName,
|
||||
'/bin/bash',
|
||||
'-c',
|
||||
'./elastic-agent version',
|
||||
]).catch((err) => {
|
||||
log.verbose(`Failed to retrieve agent version information from running instance.`, err);
|
||||
return { stdout: 'Unable to retrieve version information' };
|
||||
})
|
||||
).stdout;
|
||||
} catch (error) {
|
||||
log.error(dump(error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
const info = `Container Name: ${containerName}
|
||||
Container Id: ${containerId}
|
||||
Elastic Agent version:
|
||||
${elasticAgentVersionInfo.replace(/\n/g, '\n ')}
|
||||
|
||||
View running output: ${chalk.cyan(`docker attach ---sig-proxy=false ${containerName}`)}
|
||||
Shell access: ${chalk.cyan(`docker exec -it ${containerName} /bin/bash`)}
|
||||
Kill container: ${chalk.cyan(`docker kill ${containerId}`)}
|
||||
`;
|
||||
|
||||
return {
|
||||
type: 'docker',
|
||||
name: containerName,
|
||||
id: containerId,
|
||||
info,
|
||||
stop: async () => {
|
||||
await execa('docker', ['kill', containerId]);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
log.info(`Done. Elastic agent up and running`);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
interface GetElasticAgentDockerArgsOptions {
|
||||
containerName: string;
|
||||
fleetServerUrl: string;
|
||||
enrollmentToken: string;
|
||||
agentVersion: string;
|
||||
/** The hostname. Defaults to `containerName` */
|
||||
hostname?: string;
|
||||
}
|
||||
|
||||
const getElasticAgentDockerArgs = ({
|
||||
hostname,
|
||||
enrollmentToken,
|
||||
fleetServerUrl,
|
||||
containerName,
|
||||
agentVersion,
|
||||
}: GetElasticAgentDockerArgsOptions): string[] => {
|
||||
return [
|
||||
'run',
|
||||
'--net',
|
||||
'elastic',
|
||||
'--detach',
|
||||
'--add-host',
|
||||
'host.docker.internal:host-gateway',
|
||||
'--name',
|
||||
containerName,
|
||||
// The container's hostname will appear in Fleet when the agent enrolls
|
||||
'--hostname',
|
||||
hostname || containerName,
|
||||
'--env',
|
||||
'FLEET_ENROLL=1',
|
||||
'--env',
|
||||
`FLEET_URL=${fleetServerUrl}`,
|
||||
'--env',
|
||||
`FLEET_ENROLLMENT_TOKEN=${enrollmentToken}`,
|
||||
'--env',
|
||||
'FLEET_INSECURE=true',
|
||||
'--rm',
|
||||
`docker.elastic.co/beats/elastic-agent:${agentVersion}`,
|
||||
];
|
||||
};
|
|
@ -53,6 +53,7 @@ import type {
|
|||
PostAgentUnenrollResponse,
|
||||
GenerateServiceTokenResponse,
|
||||
GetOutputsResponse,
|
||||
DeleteAgentPolicyResponse,
|
||||
} from '@kbn/fleet-plugin/common/types';
|
||||
import nodeFetch from 'node-fetch';
|
||||
import semver from 'semver';
|
||||
|
@ -322,6 +323,28 @@ export const fetchAgentPolicy = async (
|
|||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a single Fleet Agent Policy
|
||||
* @param kbnClient
|
||||
* @param agentPolicyId
|
||||
*/
|
||||
export const deleteAgentPolicy = async (
|
||||
kbnClient: KbnClient,
|
||||
agentPolicyId: string
|
||||
): Promise<DeleteAgentPolicyResponse> => {
|
||||
return kbnClient
|
||||
.request<DeleteAgentPolicyResponse>({
|
||||
method: 'POST',
|
||||
path: agentPolicyRouteService.getDeletePath(),
|
||||
body: {
|
||||
agentPolicyId,
|
||||
},
|
||||
headers: { 'elastic-api-version': '2023-10-31' },
|
||||
})
|
||||
.then((response) => response.data)
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a list of Fleet Integration policies
|
||||
* @param kbnClient
|
||||
|
@ -533,6 +556,33 @@ export const unEnrollFleetAgent = async (
|
|||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Un-enrolls a Fleet agent
|
||||
*
|
||||
* @param kbnClient
|
||||
* @param agentId
|
||||
* @param force
|
||||
*/
|
||||
export const getAgentPolicyEnrollmentKey = async (
|
||||
kbnClient: KbnClient,
|
||||
policyId: string
|
||||
): Promise<string> => {
|
||||
const { data } = await kbnClient
|
||||
.request<GetEnrollmentAPIKeysResponse>({
|
||||
method: 'GET',
|
||||
path: enrollmentAPIKeyRouteService.getListPath(),
|
||||
query: {
|
||||
policy_id: policyId,
|
||||
},
|
||||
headers: {
|
||||
'elastic-api-version': API_VERSIONS.public.v1,
|
||||
},
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
|
||||
return data.items?.[0]?.api_key;
|
||||
};
|
||||
|
||||
export const generateFleetServiceToken = async (
|
||||
kbnClient: KbnClient,
|
||||
logger: ToolingLog
|
||||
|
|
|
@ -105,21 +105,8 @@ export const getFTRConfig = ({
|
|||
return value;
|
||||
});
|
||||
|
||||
if (
|
||||
specFileFTRConfig?.enableExperimental?.length &&
|
||||
_.some(vars.kbnTestServer.serverArgs, (value) =>
|
||||
value.includes('--xpack.securitySolution.enableExperimental')
|
||||
)
|
||||
) {
|
||||
vars.kbnTestServer.serverArgs = _.filter(
|
||||
vars.kbnTestServer.serverArgs,
|
||||
(value) => !value.includes('--xpack.securitySolution.enableExperimental')
|
||||
);
|
||||
vars.kbnTestServer.serverArgs.push(
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(
|
||||
specFileFTRConfig?.enableExperimental
|
||||
)}`
|
||||
);
|
||||
if (specFileFTRConfig?.kbnServerArgs?.length) {
|
||||
vars.kbnTestServer.serverArgs.push(...specFileFTRConfig?.kbnServerArgs);
|
||||
}
|
||||
|
||||
if (specFileFTRConfig?.license) {
|
||||
|
|
|
@ -11,7 +11,6 @@ import * as parser from '@babel/parser';
|
|||
import generate from '@babel/generator';
|
||||
import type { ExpressionStatement, ObjectExpression, ObjectProperty } from '@babel/types';
|
||||
import { schema, type TypeOf } from '@kbn/config-schema';
|
||||
import { getExperimentalAllowedValues } from '../../common/experimental_features';
|
||||
|
||||
/**
|
||||
* Retrieve test files using a glob pattern.
|
||||
|
@ -68,9 +67,10 @@ export const parseTestFileConfig = (filePath: string): SecuritySolutionDescribeB
|
|||
plugins: ['typescript'],
|
||||
});
|
||||
|
||||
const expressionStatement = _.find(ast.program.body, ['type', 'ExpressionStatement']) as
|
||||
| ExpressionStatement
|
||||
| undefined;
|
||||
const expressionStatement = _.find(ast.program.body, {
|
||||
type: 'ExpressionStatement',
|
||||
expression: { callee: { name: 'describe' } },
|
||||
}) as ExpressionStatement | undefined;
|
||||
|
||||
const callExpression = expressionStatement?.expression;
|
||||
// @ts-expect-error
|
||||
|
@ -114,21 +114,7 @@ export const parseTestFileConfig = (filePath: string): SecuritySolutionDescribeB
|
|||
const TestFileFtrConfigSchema = schema.object(
|
||||
{
|
||||
license: schema.maybe(schema.string()),
|
||||
enableExperimental: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.string({
|
||||
validate: (value) => {
|
||||
const allowedValues = getExperimentalAllowedValues();
|
||||
|
||||
if (!allowedValues.includes(value)) {
|
||||
return `Invalid [enableExperimental] value {${value}.\nValid values are: [${allowedValues.join(
|
||||
', '
|
||||
)}]`;
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
kbnServerArgs: schema.maybe(schema.arrayOf(schema.string())),
|
||||
productTypes: schema.maybe(
|
||||
// TODO:PT write validate function to ensure that only the correct combinations are used
|
||||
schema.arrayOf(
|
||||
|
|
|
@ -103,7 +103,6 @@
|
|||
"@kbn/expect",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/analytics",
|
||||
"@kbn/observability-plugin",
|
||||
"@kbn/core-notifications-browser",
|
||||
"@kbn/doc-links",
|
||||
"@kbn/react-field",
|
||||
|
@ -182,6 +181,7 @@
|
|||
"@kbn/unified-doc-viewer-plugin",
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
"@kbn/zod-helpers",
|
||||
"@kbn/core-http-common"
|
||||
"@kbn/core-http-common",
|
||||
"@kbn/stack-connectors-plugin"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ export enum SUB_ACTION {
|
|||
KILL_PROCESS = 'killProcess',
|
||||
EXECUTE_SCRIPT = 'executeScript',
|
||||
GET_AGENTS = 'getAgents',
|
||||
ISOLATE_AGENT = 'isolateAgent',
|
||||
RELEASE_AGENT = 'releaseAgent',
|
||||
ISOLATE_HOST = 'isolateHost',
|
||||
RELEASE_HOST = 'releaseHost',
|
||||
GET_REMOTE_SCRIPTS = 'getRemoteScripts',
|
||||
GET_REMOTE_SCRIPT_STATUS = 'getRemoteScriptStatus',
|
||||
GET_REMOTE_SCRIPT_RESULTS = 'getRemoteScriptResults',
|
||||
|
|
|
@ -25,140 +25,148 @@ export const SentinelOneGetAgentsResponseSchema = schema.object({
|
|||
}),
|
||||
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(
|
||||
{
|
||||
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({
|
||||
assignedBy: schema.string(),
|
||||
assignedAt: schema.string(),
|
||||
assignedById: schema.string(),
|
||||
key: schema.string(),
|
||||
value: schema.string(),
|
||||
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(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
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(),
|
||||
})
|
||||
fullDiskScanLastUpdatedAt: schema.string(),
|
||||
appsVulnerabilityStatus: schema.string(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const SentinelOneIsolateAgentResponseSchema = schema.object({
|
||||
export const SentinelOneIsolateHostResponseSchema = schema.object({
|
||||
errors: schema.nullable(schema.arrayOf(schema.string())),
|
||||
data: schema.object({
|
||||
affected: schema.number(),
|
||||
}),
|
||||
data: schema.object(
|
||||
{
|
||||
affected: schema.number(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
});
|
||||
|
||||
export const SentinelOneGetRemoteScriptsParamsSchema = schema.object({
|
||||
query: schema.nullable(schema.string()),
|
||||
osTypes: schema.nullable(schema.arrayOf(schema.string())),
|
||||
osTypes: schema.nullable(schema.string()),
|
||||
});
|
||||
|
||||
export const AlertIds = schema.maybe(schema.arrayOf(schema.string()));
|
||||
|
||||
export const SentinelOneGetRemoteScriptsResponseSchema = schema.object({
|
||||
errors: schema.nullable(schema.arrayOf(schema.string())),
|
||||
pagination: schema.object({
|
||||
|
@ -238,6 +246,7 @@ export const SentinelOneExecuteScriptParamsSchema = schema.object({
|
|||
),
|
||||
password: schema.maybe(schema.string()),
|
||||
}),
|
||||
alertIds: AlertIds,
|
||||
});
|
||||
|
||||
export const SentinelOneGetRemoteScriptStatusParamsSchema = schema.object(
|
||||
|
@ -252,40 +261,39 @@ export const SentinelOneGetRemoteScriptStatusResponseSchema = schema.object({
|
|||
totalItems: schema.number(),
|
||||
nextCursor: schema.nullable(schema.string()),
|
||||
}),
|
||||
errors: schema.arrayOf(schema.object({ type: schema.string() })),
|
||||
errors: schema.nullable(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(),
|
||||
})
|
||||
schema.object(
|
||||
{
|
||||
agentIsDecommissioned: schema.nullable(schema.boolean()),
|
||||
agentComputerName: schema.nullable(schema.string()),
|
||||
status: schema.nullable(schema.string()),
|
||||
groupName: schema.nullable(schema.string()),
|
||||
initiatedById: schema.nullable(schema.string()),
|
||||
parentTaskId: schema.nullable(schema.string()),
|
||||
updatedAt: schema.nullable(schema.string()),
|
||||
createdAt: schema.nullable(schema.string()),
|
||||
agentIsActive: schema.nullable(schema.boolean()),
|
||||
agentOsType: schema.nullable(schema.string()),
|
||||
agentMachineType: schema.nullable(schema.string()),
|
||||
id: schema.nullable(schema.string()),
|
||||
siteName: schema.nullable(schema.string()),
|
||||
detailedStatus: schema.nullable(schema.string()),
|
||||
siteId: schema.nullable(schema.string()),
|
||||
scriptResultsSignature: schema.nullable(schema.nullable(schema.string())),
|
||||
initiatedBy: schema.nullable(schema.string()),
|
||||
accountName: schema.nullable(schema.string()),
|
||||
groupId: schema.nullable(schema.string()),
|
||||
agentUuid: schema.nullable(schema.string()),
|
||||
accountId: schema.nullable(schema.string()),
|
||||
type: schema.nullable(schema.string()),
|
||||
scriptResultsPath: schema.nullable(schema.string()),
|
||||
scriptResultsBucket: schema.nullable(schema.string()),
|
||||
description: schema.nullable(schema.string()),
|
||||
agentId: schema.nullable(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
|
@ -439,13 +447,14 @@ export const SentinelOneBaseFilterSchema = schema.object({
|
|||
lastLoggedInUserName__contains: schema.nullable(schema.string()),
|
||||
awsRole__contains: schema.nullable(schema.string()),
|
||||
K8SVersion__contains: schema.nullable(schema.string()),
|
||||
alertIds: AlertIds,
|
||||
});
|
||||
|
||||
export const SentinelOneKillProcessParamsSchema = SentinelOneBaseFilterSchema.extends({
|
||||
processName: schema.string(),
|
||||
});
|
||||
|
||||
export const SentinelOneIsolateAgentParamsSchema = SentinelOneBaseFilterSchema;
|
||||
export const SentinelOneIsolateHostParamsSchema = SentinelOneBaseFilterSchema;
|
||||
|
||||
export const SentinelOneGetAgentsParamsSchema = SentinelOneBaseFilterSchema;
|
||||
|
||||
|
@ -472,14 +481,14 @@ export const SentinelOneKillProcessSchema = schema.object({
|
|||
subActionParams: SentinelOneKillProcessParamsSchema,
|
||||
});
|
||||
|
||||
export const SentinelOneIsolateAgentSchema = schema.object({
|
||||
subAction: schema.literal(SUB_ACTION.ISOLATE_AGENT),
|
||||
subActionParams: SentinelOneIsolateAgentParamsSchema,
|
||||
export const SentinelOneIsolateHostSchema = schema.object({
|
||||
subAction: schema.literal(SUB_ACTION.ISOLATE_HOST),
|
||||
subActionParams: SentinelOneIsolateHostParamsSchema,
|
||||
});
|
||||
|
||||
export const SentinelOneReleaseAgentSchema = schema.object({
|
||||
subAction: schema.literal(SUB_ACTION.RELEASE_AGENT),
|
||||
subActionParams: SentinelOneIsolateAgentParamsSchema,
|
||||
export const SentinelOneReleaseHostSchema = schema.object({
|
||||
subAction: schema.literal(SUB_ACTION.RELEASE_HOST),
|
||||
subActionParams: SentinelOneIsolateHostParamsSchema,
|
||||
});
|
||||
|
||||
export const SentinelOneExecuteScriptSchema = schema.object({
|
||||
|
@ -489,7 +498,7 @@ export const SentinelOneExecuteScriptSchema = schema.object({
|
|||
|
||||
export const SentinelOneActionParamsSchema = schema.oneOf([
|
||||
SentinelOneKillProcessSchema,
|
||||
SentinelOneIsolateAgentSchema,
|
||||
SentinelOneReleaseAgentSchema,
|
||||
SentinelOneIsolateHostSchema,
|
||||
SentinelOneReleaseHostSchema,
|
||||
SentinelOneExecuteScriptSchema,
|
||||
]);
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
SentinelOneGetRemoteScriptsParamsSchema,
|
||||
SentinelOneGetRemoteScriptsResponseSchema,
|
||||
SentinelOneGetRemoteScriptsStatusParams,
|
||||
SentinelOneIsolateAgentParamsSchema,
|
||||
SentinelOneIsolateHostParamsSchema,
|
||||
SentinelOneKillProcessParamsSchema,
|
||||
SentinelOneSecretsSchema,
|
||||
SentinelOneActionParamsSchema,
|
||||
|
@ -26,9 +26,11 @@ export type SentinelOneSecrets = TypeOf<typeof SentinelOneSecretsSchema>;
|
|||
|
||||
export type SentinelOneBaseApiResponse = TypeOf<typeof SentinelOneBaseApiResponseSchema>;
|
||||
|
||||
export type SentinelOneGetAgentsParams = TypeOf<typeof SentinelOneGetAgentsParamsSchema>;
|
||||
export type SentinelOneGetAgentsParams = Partial<TypeOf<typeof SentinelOneGetAgentsParamsSchema>>;
|
||||
export type SentinelOneGetAgentsResponse = TypeOf<typeof SentinelOneGetAgentsResponseSchema>;
|
||||
|
||||
export type SentinelOneAgent = SentinelOneGetAgentsResponse['data'][0];
|
||||
|
||||
export type SentinelOneKillProcessParams = TypeOf<typeof SentinelOneKillProcessParamsSchema>;
|
||||
|
||||
export type SentinelOneExecuteScriptParams = TypeOf<typeof SentinelOneExecuteScriptParamsSchema>;
|
||||
|
@ -45,6 +47,6 @@ export type SentinelOneGetRemoteScriptsResponse = TypeOf<
|
|||
typeof SentinelOneGetRemoteScriptsResponseSchema
|
||||
>;
|
||||
|
||||
export type SentinelOneIsolateAgentParams = TypeOf<typeof SentinelOneIsolateAgentParamsSchema>;
|
||||
export type SentinelOneIsolateHostParams = TypeOf<typeof SentinelOneIsolateHostParamsSchema>;
|
||||
|
||||
export type SentinelOneActionParams = TypeOf<typeof SentinelOneActionParamsSchema>;
|
||||
|
|
|
@ -9,3 +9,8 @@ import OpenAILogo from '../connector_types/openai/logo';
|
|||
|
||||
export { OPENAI_CONNECTOR_ID, OpenAiProviderType } from '../../common/openai/constants';
|
||||
export { OpenAILogo };
|
||||
|
||||
import SentinelOneLogo from '../connector_types/sentinelone/logo';
|
||||
|
||||
export { SENTINELONE_CONNECTOR_ID, SUB_ACTION } from '../../common/sentinelone/constants';
|
||||
export { SentinelOneLogo };
|
||||
|
|
|
@ -35,6 +35,7 @@ export function getConnectorType(): ConnectorTypeModel<
|
|||
id: SENTINELONE_CONNECTOR_ID,
|
||||
actionTypeTitle: SENTINELONE_TITLE,
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
isExperimental: true,
|
||||
selectMessage: i18n.translate(
|
||||
'xpack.stackConnectors.security.sentinelone.config.selectMessageText',
|
||||
{
|
||||
|
@ -53,7 +54,7 @@ export function getConnectorType(): ConnectorTypeModel<
|
|||
// 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)) {
|
||||
} else if (!Object.values(SUB_ACTION).includes(subAction)) {
|
||||
errors.subAction.push(translations.INVALID_ACTION);
|
||||
}
|
||||
return { errors };
|
||||
|
|
|
@ -68,7 +68,7 @@ const SentinelOneParamsFields: React.FunctionComponent<
|
|||
} = useSubAction<SentinelOneGetAgentsParams, SentinelOneGetAgentsResponse>({
|
||||
connectorId,
|
||||
subAction: SUB_ACTION.GET_AGENTS,
|
||||
disabled: isTest,
|
||||
disabled: !isTest,
|
||||
});
|
||||
|
||||
const agentOptions = useMemo(
|
||||
|
@ -221,11 +221,11 @@ const SentinelOneParamsFields: React.FunctionComponent<
|
|||
inputDisplay: i18n.KILL_PROCESS_ACTION_LABEL,
|
||||
},
|
||||
{
|
||||
value: SUB_ACTION.ISOLATE_AGENT,
|
||||
value: SUB_ACTION.ISOLATE_HOST,
|
||||
inputDisplay: i18n.ISOLATE_AGENT_ACTION_LABEL,
|
||||
},
|
||||
{
|
||||
value: SUB_ACTION.RELEASE_AGENT,
|
||||
value: SUB_ACTION.RELEASE_HOST,
|
||||
inputDisplay: i18n.RELEASE_AGENT_ACTION_LABEL,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
import type {
|
||||
SentinelOneKillProcessParams,
|
||||
SentinelOneExecuteScriptParams,
|
||||
SentinelOneIsolateAgentParams,
|
||||
SentinelOneIsolateHostParams,
|
||||
} from '../../../common/sentinelone/types';
|
||||
import type { SUB_ACTION } from '../../../common/sentinelone/constants';
|
||||
|
||||
export type SentinelOneExecuteSubActionParams =
|
||||
| SentinelOneKillProcessParams
|
||||
| SentinelOneExecuteScriptParams
|
||||
| SentinelOneIsolateAgentParams;
|
||||
| SentinelOneIsolateHostParams;
|
||||
|
||||
export interface SentinelOneExecuteActionParams {
|
||||
subAction: SUB_ACTION;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map } from 'lodash';
|
||||
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';
|
||||
|
@ -19,30 +20,40 @@ export const renderParameterTemplates = (
|
|||
variables: Record<string, unknown>
|
||||
) => {
|
||||
const context = variables?.context as Context;
|
||||
const alertIds = map(context.alerts, '_id');
|
||||
|
||||
if (params?.subAction === SUB_ACTION.KILL_PROCESS) {
|
||||
const processNames = ((params.subActionParams.processName && [
|
||||
params.subActionParams.processName,
|
||||
]) ||
|
||||
map(context.alerts, 'process.name').filter((elm) => elm)) as string[];
|
||||
|
||||
return {
|
||||
subAction: SUB_ACTION.KILL_PROCESS,
|
||||
subActionParams: {
|
||||
processName: context.alerts[0].process?.name,
|
||||
processName: processNames.join(','),
|
||||
computerName: context.alerts[0].host?.name,
|
||||
alertIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (params?.subAction === SUB_ACTION.ISOLATE_AGENT) {
|
||||
if (params?.subAction === SUB_ACTION.ISOLATE_HOST) {
|
||||
return {
|
||||
subAction: SUB_ACTION.ISOLATE_AGENT,
|
||||
subAction: SUB_ACTION.ISOLATE_HOST,
|
||||
subActionParams: {
|
||||
computerName: context.alerts[0].host?.name,
|
||||
alertIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (params?.subAction === SUB_ACTION.RELEASE_AGENT) {
|
||||
if (params?.subAction === SUB_ACTION.RELEASE_HOST) {
|
||||
return {
|
||||
subAction: SUB_ACTION.RELEASE_AGENT,
|
||||
subAction: SUB_ACTION.RELEASE_HOST,
|
||||
subActionParams: {
|
||||
computerName: context.alerts[0].host?.name,
|
||||
alertIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -53,6 +64,7 @@ export const renderParameterTemplates = (
|
|||
subActionParams: {
|
||||
computerName: context.alerts[0].host?.name,
|
||||
...params.subActionParams,
|
||||
alertIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
|||
SentinelOneBaseApiResponse,
|
||||
SentinelOneGetRemoteScriptsParams,
|
||||
SentinelOneGetRemoteScriptsResponse,
|
||||
SentinelOneIsolateAgentParams,
|
||||
SentinelOneIsolateHostParams,
|
||||
SentinelOneKillProcessParams,
|
||||
SentinelOneExecuteScriptParams,
|
||||
} from '../../../common/sentinelone/types';
|
||||
|
@ -27,12 +27,13 @@ import {
|
|||
SentinelOneGetRemoteScriptsParamsSchema,
|
||||
SentinelOneGetRemoteScriptsResponseSchema,
|
||||
SentinelOneGetAgentsResponseSchema,
|
||||
SentinelOneIsolateAgentResponseSchema,
|
||||
SentinelOneIsolateAgentParamsSchema,
|
||||
SentinelOneIsolateHostResponseSchema,
|
||||
SentinelOneIsolateHostParamsSchema,
|
||||
SentinelOneGetRemoteScriptStatusParamsSchema,
|
||||
SentinelOneGetRemoteScriptStatusResponseSchema,
|
||||
SentinelOneGetAgentsParamsSchema,
|
||||
SentinelOneExecuteScriptResponseSchema,
|
||||
SentinelOneKillProcessParamsSchema,
|
||||
} from '../../../common/sentinelone/schema';
|
||||
import { SUB_ACTION } from '../../../common/sentinelone/constants';
|
||||
|
||||
|
@ -45,8 +46,8 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
> {
|
||||
private urls: {
|
||||
agents: string;
|
||||
isolateAgent: string;
|
||||
releaseAgent: string;
|
||||
isolateHost: string;
|
||||
releaseHost: string;
|
||||
remoteScripts: string;
|
||||
remoteScriptStatus: string;
|
||||
remoteScriptsExecute: string;
|
||||
|
@ -56,8 +57,8 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
super(params);
|
||||
|
||||
this.urls = {
|
||||
isolateAgent: `${this.config.url}${API_PATH}/agents/actions/disconnect`,
|
||||
releaseAgent: `${this.config.url}${API_PATH}/agents/actions/connect`,
|
||||
isolateHost: `${this.config.url}${API_PATH}/agents/actions/disconnect`,
|
||||
releaseHost: `${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`,
|
||||
|
@ -87,21 +88,21 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.ISOLATE_AGENT,
|
||||
method: 'isolateAgent',
|
||||
schema: SentinelOneIsolateAgentParamsSchema,
|
||||
name: SUB_ACTION.ISOLATE_HOST,
|
||||
method: 'isolateHost',
|
||||
schema: SentinelOneIsolateHostParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.RELEASE_AGENT,
|
||||
method: 'releaseAgent',
|
||||
schema: SentinelOneIsolateAgentParamsSchema,
|
||||
name: SUB_ACTION.RELEASE_HOST,
|
||||
method: 'releaseHost',
|
||||
schema: SentinelOneIsolateHostParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.KILL_PROCESS,
|
||||
method: 'killProcess',
|
||||
schema: SentinelOneKillProcessResponseSchema,
|
||||
schema: SentinelOneKillProcessParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
|
@ -112,7 +113,7 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
}
|
||||
|
||||
public async executeScript(payload: SentinelOneExecuteScriptParams) {
|
||||
return this.sentinelOneApiRequest({
|
||||
await this.sentinelOneApiRequest({
|
||||
url: this.urls.remoteScriptsExecute,
|
||||
method: 'post',
|
||||
data: {
|
||||
|
@ -128,7 +129,7 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
});
|
||||
}
|
||||
|
||||
public async killProcess({ processName, ...payload }: SentinelOneKillProcessParams) {
|
||||
public async killProcess({ alertIds, processName, ...payload }: SentinelOneKillProcessParams) {
|
||||
const agentData = await this.getAgents(payload);
|
||||
|
||||
const agentId = agentData.data[0]?.id;
|
||||
|
@ -139,14 +140,14 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
|
||||
const terminateScriptResponse = await this.getRemoteScripts({
|
||||
query: 'terminate',
|
||||
osTypes: [agentData?.data[0]?.osType],
|
||||
osTypes: agentData?.data[0]?.osType,
|
||||
});
|
||||
|
||||
if (!processName) {
|
||||
throw new Error('No process name provided');
|
||||
}
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
await this.sentinelOneApiRequest({
|
||||
url: this.urls.remoteScriptsExecute,
|
||||
method: 'post',
|
||||
data: {
|
||||
|
@ -165,32 +166,36 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
});
|
||||
}
|
||||
|
||||
public async isolateAgent(payload: SentinelOneIsolateAgentParams) {
|
||||
public async isolateHost({ alertIds, ...payload }: SentinelOneIsolateHostParams) {
|
||||
const response = await this.getAgents(payload);
|
||||
|
||||
if (response.data.length === 0) {
|
||||
throw new Error('No agents found');
|
||||
const errorMessage = 'No agents found';
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (response.data[0].networkStatus === 'disconnected') {
|
||||
throw new Error('Agent already isolated');
|
||||
const errorMessage = 'Agent already isolated';
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const agentId = response.data[0].id;
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.isolateAgent,
|
||||
url: this.urls.isolateHost,
|
||||
method: 'post',
|
||||
data: {
|
||||
filter: {
|
||||
ids: agentId,
|
||||
},
|
||||
},
|
||||
responseSchema: SentinelOneIsolateAgentResponseSchema,
|
||||
responseSchema: SentinelOneIsolateHostResponseSchema,
|
||||
});
|
||||
}
|
||||
|
||||
public async releaseAgent(payload: SentinelOneIsolateAgentParams) {
|
||||
public async releaseHost({ alertIds, ...payload }: SentinelOneIsolateHostParams) {
|
||||
const response = await this.getAgents(payload);
|
||||
|
||||
if (response.data.length === 0) {
|
||||
|
@ -204,14 +209,14 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
const agentId = response.data[0].id;
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.releaseAgent,
|
||||
url: this.urls.releaseHost,
|
||||
method: 'post',
|
||||
data: {
|
||||
filter: {
|
||||
ids: agentId,
|
||||
},
|
||||
},
|
||||
responseSchema: SentinelOneIsolateAgentResponseSchema,
|
||||
responseSchema: SentinelOneIsolateHostResponseSchema,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ type Capabilities = Record<string, any>;
|
|||
|
||||
export const hasShowActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.show;
|
||||
export const hasSaveActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.save;
|
||||
export const hasExecuteActionsCapability = (capabilities: Capabilities) =>
|
||||
capabilities?.actions?.execute;
|
||||
export const hasExecuteActionsCapability = (capabilities: Capabilities, actionTypeId?: string) =>
|
||||
actionTypeId === '.sentinelone' ? capabilities?.actions?.save : capabilities?.actions?.execute;
|
||||
export const hasDeleteActionsCapability = (capabilities: Capabilities) =>
|
||||
capabilities?.actions?.delete;
|
||||
|
||||
|
|
|
@ -24,11 +24,14 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { betaBadgeProps } from '../beta_badge_props';
|
||||
import { EditConnectorTabs } from '../../../../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { hasExecuteActionsCapability } from '../../../lib/capabilities';
|
||||
|
||||
const FlyoutHeaderComponent: React.FC<{
|
||||
isExperimental?: boolean;
|
||||
isPreconfigured: boolean;
|
||||
connectorName: string;
|
||||
connectorTypeId: string;
|
||||
connectorTypeDesc: string;
|
||||
selectedTab: EditConnectorTabs;
|
||||
setTab: () => void;
|
||||
|
@ -38,11 +41,17 @@ const FlyoutHeaderComponent: React.FC<{
|
|||
isExperimental = false,
|
||||
isPreconfigured,
|
||||
connectorName,
|
||||
connectorTypeId,
|
||||
connectorTypeDesc,
|
||||
selectedTab,
|
||||
setTab,
|
||||
}) => {
|
||||
const {
|
||||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const canExecute = hasExecuteActionsCapability(capabilities, connectorTypeId);
|
||||
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder data-test-subj="edit-connector-flyout-header">
|
||||
|
@ -136,15 +145,17 @@ const FlyoutHeaderComponent: React.FC<{
|
|||
defaultMessage: 'Configuration',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={setTab}
|
||||
data-test-subj="testConnectorTab"
|
||||
isSelected={EditConnectorTabs.Test === selectedTab}
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', {
|
||||
defaultMessage: 'Test',
|
||||
})}
|
||||
</EuiTab>
|
||||
{canExecute && (
|
||||
<EuiTab
|
||||
onClick={setTab}
|
||||
data-test-subj="testConnectorTab"
|
||||
isSelected={EditConnectorTabs.Test === selectedTab}
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', {
|
||||
defaultMessage: 'Test',
|
||||
})}
|
||||
</EuiTab>
|
||||
)}
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
);
|
||||
|
|
|
@ -64,7 +64,7 @@ describe('EditConnectorFlyout', () => {
|
|||
appMockRenderer = createAppMockRenderer();
|
||||
appMockRenderer.coreStart.application.capabilities = {
|
||||
...appMockRenderer.coreStart.application.capabilities,
|
||||
actions: { save: true, show: true },
|
||||
actions: { save: true, show: true, execute: true },
|
||||
};
|
||||
appMockRenderer.coreStart.http.put = jest.fn().mockResolvedValue(updateConnectorResponse);
|
||||
appMockRenderer.coreStart.http.post = jest.fn().mockResolvedValue(executeConnectorResponse);
|
||||
|
|
|
@ -228,6 +228,7 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
|
|||
<FlyoutHeader
|
||||
isPreconfigured={connector.isPreconfigured}
|
||||
connectorName={connector.name}
|
||||
connectorTypeId={connector.actionTypeId}
|
||||
connectorTypeDesc={actionTypeModel?.selectMessage}
|
||||
setTab={handleSetTab}
|
||||
selectedTab={selectedTab}
|
||||
|
|
|
@ -90,7 +90,6 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
docLinks,
|
||||
} = useKibana().services;
|
||||
const canDelete = hasDeleteActionsCapability(capabilities);
|
||||
const canExecute = hasExecuteActionsCapability(capabilities);
|
||||
const canSave = hasSaveActionsCapability(capabilities);
|
||||
|
||||
const [actionTypesIndex, setActionTypesIndex] = useState<ActionTypeIndex | undefined>(undefined);
|
||||
|
@ -317,7 +316,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
name: '',
|
||||
render: (item: ActionConnectorTableItem) => {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="flexEnd">
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<DeleteOperation canDelete={canDelete} item={item} onDelete={() => onDelete([item])} />
|
||||
{item.isMissingSecrets ? (
|
||||
<>
|
||||
|
@ -347,7 +346,13 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
</>
|
||||
) : (
|
||||
<RunOperation
|
||||
canExecute={canExecute && actionTypesIndex && actionTypesIndex[item.actionTypeId]}
|
||||
canExecute={
|
||||
!!(
|
||||
hasExecuteActionsCapability(capabilities, item.actionTypeId) &&
|
||||
actionTypesIndex &&
|
||||
actionTypesIndex[item.actionTypeId]
|
||||
)
|
||||
}
|
||||
item={item}
|
||||
onRun={() => editItem(item, EditConnectorTabs.Test)}
|
||||
/>
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -13995,6 +13995,15 @@ cypress-axe@^1.5.0:
|
|||
resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.5.0.tgz#95082734583da77b51ce9b7784e14a442016c7a1"
|
||||
integrity sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==
|
||||
|
||||
cypress-data-session@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress-data-session/-/cypress-data-session-2.7.0.tgz#1dbae30798a7f7351c67b40cc20fdf28af512eac"
|
||||
integrity sha512-Rj7WZ/Vn/givI6ja+y/c3QlT3jiwpVDReuBuYm66A31/Vi9PfCoI0SSY1pL/dUUrFEcqCLEFZiTiAWhgTtjXwQ==
|
||||
dependencies:
|
||||
cypress-plugin-config "^1.2.0"
|
||||
debug "^4.3.2"
|
||||
simple-sha256 "^1.1.0"
|
||||
|
||||
cypress-file-upload@^5.0.8:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
|
||||
|
@ -14008,6 +14017,11 @@ cypress-multi-reporters@^1.6.3:
|
|||
debug "^4.3.4"
|
||||
lodash "^4.17.21"
|
||||
|
||||
cypress-plugin-config@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress-plugin-config/-/cypress-plugin-config-1.2.1.tgz#aa7eaa55ab5ce5e186ab7d0e37cc7e42bfb609b4"
|
||||
integrity sha512-z+bQ7oyfDKun51HiCVNBOR+g38/nYRJ7zVdCZT2/9UozzE8P4iA1zF/yc85ePZLy5NOj/0atutoUPBBR5SqjSQ==
|
||||
|
||||
cypress-real-events@^1.10.3:
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.10.3.tgz#e2e949ea509cc4306df6c238de1a9982d67360e5"
|
||||
|
@ -27314,6 +27328,11 @@ simple-git@^3.16.0:
|
|||
"@kwsites/promise-deferred" "^1.1.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
simple-sha256@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-sha256/-/simple-sha256-1.1.0.tgz#e6d245a932e70ba45fc1699321e3cc3f82a1d605"
|
||||
integrity sha512-jMdj+5MR1dRiFbrQ8idmkPv+HCoJEmJVBBwc0vQQz4Gtg3PEkjdpVxUM4BSwK898zhNP/ub3vcP9edw47EHuLQ==
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue