Skip to content

Commit

Permalink
[Security Solution][Endpoint][Sentinel One] Gate calls to `/agent_sta…
Browse files Browse the repository at this point in the history
…tus` 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
  • Loading branch information
ashokaditya committed Apr 11, 2024
1 parent 8162230 commit 3ad523d
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 : '';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 3ad523d

Please sign in to comment.