[Security Solution][Endpoint][Sentinel One] Gate calls to /agent_status on endpoint agent (#180600)

## Summary

Gates call to internal `agent_status` API call that supports only
`sentinel_one` to be called on `endpoint` agent responder.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
This commit is contained in:
Ash 2024-04-11 21:36:02 +02:00 committed by GitHub
parent 8162230b1c
commit 3ad523d2a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 123 additions and 14 deletions

View file

@ -8,6 +8,7 @@
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { getAgentStatusText } from '../../../common/components/endpoint/agent_status_text';
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../management/pages/endpoint_hosts/view/host_constants';
import { useGetSentinelOneAgentStatus } from './use_sentinelone_host_isolation';
@ -32,7 +33,13 @@ const EuiFlexGroupStyled = styled(EuiFlexGroup)`
export const SentinelOneAgentStatus = React.memo(
({ agentId, 'data-test-subj': dataTestSubj }: { agentId: string; 'data-test-subj'?: string }) => {
const { data, isLoading, isFetched } = useGetSentinelOneAgentStatus([agentId]);
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
'sentinelOneManualHostActionsEnabled'
);
const { data, isLoading, isFetched } = useGetSentinelOneAgentStatus([agentId], {
enabled: sentinelOneManualHostActionsEnabled,
});
const agentStatus = data?.[`${agentId}`];
const label = useMemo(() => {

View file

@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { useHostIsolationAction } from './use_host_isolation_action';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useGetSentinelOneAgentStatus } from './use_sentinelone_host_isolation';
jest.mock('./use_sentinelone_host_isolation');
jest.mock('../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
const useGetSentinelOneAgentStatusMock = useGetSentinelOneAgentStatus as jest.Mock;
describe('useHostIsolationAction', () => {
const createReactQueryWrapper = () => {
const queryClient = new QueryClient();
const wrapper: React.FC = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return wrapper;
};
const render = (isSentinelAlert: boolean = true) =>
renderHook(
() =>
useHostIsolationAction({
closePopover: jest.fn(),
detailsData: isSentinelAlert
? [
{
category: 'event',
field: 'event.module',
values: ['sentinel_one'],
originalValue: ['sentinel_one'],
isObjectArray: false,
},
{
category: 'observer',
field: 'observer.serial_number',
values: ['some-agent-id'],
originalValue: ['some-agent-id'],
isObjectArray: false,
},
]
: [
{
category: 'agent',
field: 'agent.id',
values: ['some-agent-id'],
originalValue: ['some-agent-id'],
isObjectArray: false,
},
],
isHostIsolationPanelOpen: false,
onAddIsolationStatusClick: jest.fn(),
}),
{
wrapper: createReactQueryWrapper(),
}
);
beforeEach(() => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
});
afterEach(() => {
jest.clearAllMocks();
});
it('`useGetSentinelOneAgentStatusMock` is invoked as `enabled` when SentinelOne alert and FF enabled', () => {
render();
expect(useGetSentinelOneAgentStatusMock).toHaveBeenCalledWith(['some-agent-id'], {
enabled: true,
});
});
it('`useGetSentinelOneAgentStatusMock` is invoked as `disabled` when SentinelOne alert and FF disabled', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
render();
expect(useGetSentinelOneAgentStatusMock).toHaveBeenCalledWith(['some-agent-id'], {
enabled: false,
});
});
it('`useGetSentinelOneAgentStatusMock` is invoked as `disabled` when non-SentinelOne alert', () => {
render(false);
expect(useGetSentinelOneAgentStatusMock).toHaveBeenCalledWith([''], {
enabled: false,
});
});
});

View file

@ -79,7 +79,9 @@ export const useHostIsolationAction = ({
agentType: sentinelOneAgentId ? 'sentinel_one' : 'endpoint',
});
const { data: sentinelOneAgentData } = useGetSentinelOneAgentStatus([sentinelOneAgentId || '']);
const { data: sentinelOneAgentData } = useGetSentinelOneAgentStatus([sentinelOneAgentId || ''], {
enabled: !!sentinelOneAgentId && sentinelOneManualHostActionsEnabled,
});
const sentinelOneAgentStatus = sentinelOneAgentData?.[`${sentinelOneAgentId}`];
const isHostIsolated = useMemo(() => {

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { isEmpty } from 'lodash';
import type { SentinelOneGetAgentsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
@ -14,7 +13,6 @@ import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
import { ENDPOINT_AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants';
import type { AgentStatusApiResponse } from '../../../../common/endpoint/types';
import { useHttp } from '../../../common/lib/kibana';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
interface ErrorType {
statusCode: number;
@ -26,19 +24,11 @@ export const useGetSentinelOneAgentStatus = (
agentIds: string[],
options: UseQueryOptions<AgentStatusApiResponse['data'], IHttpFetchError<ErrorType>> = {}
): UseQueryResult<AgentStatusApiResponse['data'], IHttpFetchError<ErrorType>> => {
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
'sentinelOneManualHostActionsEnabled'
);
const http = useHttp();
return useQuery<AgentStatusApiResponse['data'], IHttpFetchError<ErrorType>>({
queryKey: ['get-agent-status', agentIds],
...options,
enabled: !(
sentinelOneManualHostActionsEnabled &&
isEmpty(agentIds.filter((agentId) => agentId.trim().length))
),
// TODO: update this to use a function instead of a number
refetchInterval: 2000,
queryFn: () =>

View file

@ -6,6 +6,7 @@
*/
import React, { memo } from 'react';
import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features';
import { useGetSentinelOneAgentStatus } from '../../../../../../detections/components/host_isolation/use_sentinelone_host_isolation';
import { SentinelOneAgentStatus } from '../../../../../../detections/components/host_isolation/sentinel_one_agent_status';
import type { ThirdPartyAgentInfo } from '../../../../../../../common/types';
@ -20,7 +21,10 @@ interface HeaderSentinelOneInfoProps {
export const HeaderSentinelOneInfo = memo<HeaderSentinelOneInfoProps>(
({ agentId, platform, hostName }) => {
const { data } = useGetSentinelOneAgentStatus([agentId]);
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
'sentinelOneManualHostActionsEnabled'
);
const { data } = useGetSentinelOneAgentStatus([agentId], { enabled: isSentinelOneV1Enabled });
const agentStatus = data?.[agentId];
const lastCheckin = agentStatus ? agentStatus.lastSeen : '';

View file

@ -28,12 +28,18 @@ export const OfflineCallout = memo<OfflineCalloutProps>(({ agentType, endpointId
'responseActionsSentinelOneV1Enabled'
);
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
'sentinelOneManualHostActionsEnabled'
);
const { data: endpointDetails } = useGetEndpointDetails(endpointId, {
refetchInterval: 10000,
enabled: isEndpointAgent,
});
const { data } = useGetSentinelOneAgentStatus([endpointId]);
const { data } = useGetSentinelOneAgentStatus([endpointId], {
enabled: sentinelOneManualHostActionsEnabled && isSentinelOneAgent,
});
// TODO: simplify this to use the yet to be implemented agentStatus API hook
const showOfflineCallout = useMemo(