Skip to content

Commit

Permalink
[Fleet][Integrations] Add callout to Microsoft Defender integrations …
Browse files Browse the repository at this point in the history
…showing support for bi-directional response actions (#207861)

## Summary

- Add dismissible callout indicating support for bi-directional response
actions to the following integration: Overview page:
    - Microsoft Defender for Endpoint
    - Microsoft M365 Defender
  • Loading branch information
paul-tavares authored Jan 23, 2025
1 parent a69236d commit cc38fbe
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import React from 'react';
import { type RenderResult } from '@testing-library/react';
import { type RenderResult, fireEvent, waitFor } from '@testing-library/react';

import type { FleetStartServices } from '../../../../../../..';

import { createFleetTestRendererMock } from '../../../../../../../mock';

Expand All @@ -18,33 +20,61 @@ import {
jest.mock('react-use/lib/useLocalStorage');

describe('BidirectionalIntegrationsBanner', () => {
let formProps: BidirectionalIntegrationsBannerProps;
let componentProps: BidirectionalIntegrationsBannerProps;
let renderResult: RenderResult;
let render: () => RenderResult;
let storageMock: jest.Mocked<FleetStartServices['storage']>;

beforeEach(() => {
formProps = {
onDismiss: jest.fn(),
};
componentProps = { integrationPackageName: 'sentinel_one' };

const testRunner = createFleetTestRendererMock();

const renderer = createFleetTestRendererMock();
storageMock = testRunner.startServices.storage;

renderResult = renderer.render(<BidirectionalIntegrationsBanner {...formProps} />);
render = () => {
renderResult = testRunner.render(<BidirectionalIntegrationsBanner {...componentProps} />);
return renderResult;
};
});

it('should render bidirectional integrations banner', () => {
render();
expect(renderResult.getByTestId('bidirectionalIntegrationsCallout')).toBeInTheDocument();
});

it('should contain a link to documentation', () => {
render();
const docLink = renderResult.getByTestId('bidirectionalIntegrationDocLink');

expect(docLink).toBeInTheDocument();
expect(docLink.getAttribute('href')).toContain('third-party-actions.html');
});

it('should call `onDismiss` callback when user clicks dismiss', () => {
renderResult.getByTestId('euiDismissCalloutButton').click();
it('should remove the callout when the dismiss button is clicked', async () => {
render();
fireEvent.click(renderResult.getByTestId('euiDismissCalloutButton'));

await waitFor(() => {
expect(storageMock.store.setItem).toHaveBeenCalledWith(
'fleet.showSOReponseSupportBanner',
'false'
);
expect(renderResult.queryByTestId('bidirectionalIntegrationsCallout')).toBeFalsy();
});
});

it('should render nothing if integration is not supported', () => {
componentProps.integrationPackageName = 'foo';
render();

expect(renderResult.queryByTestId('bidirectionalIntegrationsCallout')).toBeFalsy();
});

it('should render nothing if user had dismissed the callout in the past', () => {
(storageMock.store.getItem as jest.Mock).mockReturnValue('false');
render();

expect(formProps.onDismiss).toBeCalled();
expect(renderResult.queryByTestId('bidirectionalIntegrationsCallout')).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@
* 2.0.
*/

import React, { memo } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { EuiCallOut, EuiLink, EuiTextColor } from '@elastic/eui';
import { EuiCallOut, EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';

import type { FleetStartServices } from '../../../../../../..';

/**
* A list of Integration `package.name`'s that support security's bi-directional response actions
* along with its corresponding storage (local storage) key for persisting the user's dismissal of
* the callout
*/
const SUPPORTED_INTEGRATIONS_STORAGE_KEY: Readonly<Record<string, string>> = Object.freeze({
sentinel_one: 'fleet.showSOReponseSupportBanner',
crowdstrike: 'fleet.showCSResponseSupportBanner',
microsoft_defender_endpoint: 'fleet.showMSDefenderResponseSupportBanner',
m365_defender: 'fleet.showMSDefenderResponseSupportBanner', // Same key as the one above
});

const AccentCallout = styled(EuiCallOut)`
.euiCallOutHeader__title {
color: ${(props) => props.theme.eui.euiColorAccent};
Expand All @@ -19,47 +33,69 @@ const AccentCallout = styled(EuiCallOut)`
`;

export interface BidirectionalIntegrationsBannerProps {
onDismiss: () => void;
integrationPackageName: string;
}
export const BidirectionalIntegrationsBanner = memo<BidirectionalIntegrationsBannerProps>(
({ onDismiss }) => {
const { docLinks } = useKibana().services;
({ integrationPackageName }) => {
const { docLinks, storage } = useKibana<FleetStartServices>().services;
const storageKey = SUPPORTED_INTEGRATIONS_STORAGE_KEY[integrationPackageName];
const [showBanner, setShowBanner] = useState(
storageKey ? storage.get(storageKey) ?? true : false
);

const bannerTitle = (
<EuiTextColor color="accent">
<FormattedMessage
id="xpack.fleet.bidirectionalIntegrationsBanner.title"
defaultMessage={'NEW: Response enabled integration'}
/>
</EuiTextColor>
const onDismissHandler = useCallback(() => {
setShowBanner(false);

if (storageKey) {
storage.set(storageKey, false);
}
}, [storage, storageKey]);

const bannerTitle = useMemo(
() => (
<EuiTextColor color="accent">
<FormattedMessage
id="xpack.fleet.bidirectionalIntegrationsBanner.title"
defaultMessage={'NEW: Response enabled integration'}
/>
</EuiTextColor>
),
[]
);

if (!storageKey || !showBanner) {
return null;
}

return (
<AccentCallout
title={bannerTitle}
iconType="cheer"
onDismiss={onDismiss}
data-test-subj={'bidirectionalIntegrationsCallout'}
>
<FormattedMessage
id="xpack.fleet.bidirectionalIntegrationsBanner.body"
defaultMessage="Orchestrate response actions across endpoint vendors with bidirectional integrations. {learnmore}."
values={{
learnmore: (
<EuiLink
href={docLinks?.links.securitySolution.bidirectionalIntegrations}
target="_blank"
data-test-subj="bidirectionalIntegrationDocLink"
>
<FormattedMessage
id="xpack.fleet.bidirectionalIntegrations.doc.link"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</AccentCallout>
<>
<AccentCallout
title={bannerTitle}
iconType="cheer"
onDismiss={onDismissHandler}
data-test-subj={'bidirectionalIntegrationsCallout'}
>
<FormattedMessage
id="xpack.fleet.bidirectionalIntegrationsBanner.body"
defaultMessage="Orchestrate response actions across endpoint vendors with bidirectional integrations. {learnmore}."
values={{
learnmore: (
<EuiLink
href={docLinks?.links.securitySolution.bidirectionalIntegrations}
target="_blank"
data-test-subj="bidirectionalIntegrationDocLink"
>
<FormattedMessage
id="xpack.fleet.bidirectionalIntegrations.doc.link"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</AccentCallout>
<EuiSpacer size="s" />
</>
);
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,6 @@ export const OverviewPage: React.FC<Props> = memo(
const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId);
const isPrerelease = isPackagePrerelease(packageInfo.version);
const isElasticDefend = packageInfo.name === 'endpoint';
const isSentinelOne = packageInfo.name === 'sentinel_one';
const isCrowdStrike = packageInfo.name === 'crowdstrike';
const [markdown, setMarkdown] = useState<string | undefined>(undefined);
const [selectedItemId, setSelectedItem] = useState<string | undefined>(undefined);
const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false);
Expand Down Expand Up @@ -301,27 +299,11 @@ export const OverviewPage: React.FC<Props> = memo(
const [showAVCBanner, setShowAVCBanner] = useState(
storage.get('securitySolution.showAvcBanner') ?? true
);
const [showCSResponseSupportBanner, setShowCSResponseSupportBanner] = useState(
storage.get('fleet.showCSResponseSupportBanner') ?? true
);
const [showSOReponseSupportBanner, setShowSOResponseSupportBanner] = useState(
storage.get('fleet.showSOReponseSupportBanner') ?? true
);
const onAVCBannerDismiss = useCallback(() => {
setShowAVCBanner(false);
storage.set('securitySolution.showAvcBanner', false);
}, [storage]);

const onCSResponseSupportBannerDismiss = useCallback(() => {
setShowCSResponseSupportBanner(false);
storage.set('fleet.showCSResponseSupportBanner', false);
}, [storage]);

const onSOResponseSupportBannerDismiss = useCallback(() => {
setShowSOResponseSupportBanner(false);
storage.set('fleet.showSOReponseSupportBanner', false);
}, [storage]);

return (
<EuiFlexGroup alignItems="flexStart" data-test-subj="epm.OverviewPage">
<SideBar grow={2}>
Expand All @@ -342,18 +324,9 @@ export const OverviewPage: React.FC<Props> = memo(
<EuiSpacer size="s" />
</>
)}
{isCrowdStrike && showCSResponseSupportBanner && (
<>
<BidirectionalIntegrationsBanner onDismiss={onCSResponseSupportBannerDismiss} />
<EuiSpacer size="s" />
</>
)}
{isSentinelOne && showSOReponseSupportBanner && (
<>
<BidirectionalIntegrationsBanner onDismiss={onSOResponseSupportBannerDismiss} />
<EuiSpacer size="s" />
</>
)}

<BidirectionalIntegrationsBanner integrationPackageName={packageInfo.name} />

<CloudPostureThirdPartySupportCallout packageInfo={packageInfo} />
{isPrerelease && (
<PrereleaseCallout
Expand Down

0 comments on commit cc38fbe

Please sign in to comment.