[TIP] display indicator name within cases flyout (#146744)

elastic/security-team#5475
This commit is contained in:
Philippe Oberti 2022-12-06 10:10:06 -06:00 committed by GitHub
parent 00df93eb1d
commit 7022e56b2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 99 additions and 35 deletions

View file

@ -48,9 +48,10 @@ export const CommentChildren: VFC<CommentChildrenProps> = ({ id, metadata }) =>
indicator={indicator as Indicator}
closeFlyout={() => setExpanded(false)}
kqlBarIntegration={true}
indicatorName={indicatorName}
/>
) : null,
[expanded, indicator]
[expanded, indicator, indicatorName]
);
if (isLoading) {

View file

@ -9,6 +9,7 @@ import { createContext } from 'react';
export interface IndicatorsFlyoutContextValue {
kqlBarIntegration: boolean;
indicatorName?: string | undefined;
}
export const IndicatorsFlyoutContext = createContext<IndicatorsFlyoutContextValue | undefined>(

View file

@ -20,14 +20,14 @@ export default {
export function WithIndicators() {
const indicator = generateMockIndicator();
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorFieldsTable
fields={['threat.indicator.type']}
indicator={indicator}
@ -41,14 +41,14 @@ export function WithIndicators() {
export function NoFilterButtons() {
const indicator = generateMockIndicator();
const kqlBarIntegration = {
const context = {
kqlBarIntegration: true,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorFieldsTable
fields={['threat.indicator.type']}
indicator={indicator}

View file

@ -55,6 +55,13 @@ export interface IndicatorsFlyoutProps {
* We should be showing the filter in and out buttons when the flyout is used in the cases view.
*/
kqlBarIntegration?: boolean;
/**
* Name of the indicator, used only when the flyout is rendered in the Cases view.
* Because the indicator name is a runtime field, when querying for the indicator from within
* the Cases view, this logic is not ran. Therefore, passing the name to the flyout is an
* easy (hopefully temporary) solution to display it within the flyout.
*/
indicatorName?: string;
}
/**
@ -64,6 +71,7 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({
indicator,
closeFlyout,
kqlBarIntegration = false,
indicatorName,
}) => {
const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.overview);
@ -157,7 +165,7 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<IndicatorsFlyoutContext.Provider value={{ kqlBarIntegration }}>
<IndicatorsFlyoutContext.Provider value={{ kqlBarIntegration, indicatorName }}>
{selectedTabContent}
</IndicatorsFlyoutContext.Provider>
</EuiFlyoutBody>

View file

@ -20,12 +20,12 @@ const indicator: Indicator = generateMockFileIndicator();
const field: string = 'threat.indicator.name';
export const Default: Story<void> = () => {
const kqlBarIntegration = {
const context = {
kqlBarIntegration: true,
};
return (
<StoryProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</StoryProvidersComponent>
@ -33,12 +33,12 @@ export const Default: Story<void> = () => {
};
export const WithoutFilterInOut: Story<void> = () => {
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</StoryProvidersComponent>

View file

@ -17,11 +17,11 @@ describe('IndicatorValueActions', () => {
it('should return null if field and value are invalid', () => {
const field: string = 'invalid';
const kqlBarIntegration = {
const context = {
kqlBarIntegration: true,
};
const component = render(
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
);
@ -30,12 +30,12 @@ describe('IndicatorValueActions', () => {
it('should only render add to timeline and copy to clipboard', () => {
const field: string = 'threat.indicator.name';
const kqlBarIntegration = {
const context = {
kqlBarIntegration: true,
};
const component = render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
@ -45,12 +45,12 @@ describe('IndicatorValueActions', () => {
it('should render filter in/out and dropdown for add to timeline and copy to clipboard', () => {
const field: string = 'threat.indicator.name';
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
const component = render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>

View file

@ -21,14 +21,14 @@ const mockIndicator = generateMockIndicator();
export function Default() {
const mockField = 'threat.indicator.ip';
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={{} as any}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorBlock indicator={mockIndicator} field={mockField} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
@ -38,14 +38,14 @@ export function Default() {
export function NoFilterButtons() {
const mockField = 'threat.indicator.ip';
const kqlBarIntegration = {
const context = {
kqlBarIntegration: true,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={{} as any}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorBlock indicator={mockIndicator} field={mockField} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>

View file

@ -26,14 +26,14 @@ export default {
export const Default: Story<void> = () => {
const mockIndicator: Indicator = generateMockIndicator();
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={{} as any}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutOverview onViewAllFieldsInTable={() => {}} indicator={mockIndicator} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>

View file

@ -13,6 +13,7 @@ import {
IndicatorsFlyoutOverview,
TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS,
TI_FLYOUT_OVERVIEW_TABLE,
TI_FLYOUT_OVERVIEW_TITLE,
} from '.';
import { EMPTY_PROMPT_TEST_ID } from '../empty_prompt';
import { IndicatorsFlyoutContext } from '../context';
@ -20,12 +21,18 @@ import { IndicatorsFlyoutContext } from '../context';
describe('<IndicatorsFlyoutOverview />', () => {
describe('invalid indicator', () => {
it('should render error message on invalid indicator', () => {
const context = {
kqlBarIntegration: false,
};
render(
<TestProvidersComponent>
<IndicatorsFlyoutOverview
onViewAllFieldsInTable={() => {}}
indicator={{ fields: {} } as unknown as Indicator}
/>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutOverview
onViewAllFieldsInTable={() => {}}
indicator={{ fields: {} } as unknown as Indicator}
/>
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);
@ -34,13 +41,13 @@ describe('<IndicatorsFlyoutOverview />', () => {
});
it('should render the highlighted blocks and table when valid indicator is passed', () => {
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutOverview
onViewAllFieldsInTable={() => {}}
indicator={generateMockIndicator()}
@ -52,4 +59,44 @@ describe('<IndicatorsFlyoutOverview />', () => {
expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TABLE)).toBeInTheDocument();
expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS)).toBeInTheDocument();
});
it('should render the indicator name value in the title', () => {
const context = {
kqlBarIntegration: false,
};
const indicator: Indicator = generateMockIndicator();
const indicatorName: string = (indicator.fields['threat.indicator.name'] as string[])[0];
render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutOverview onViewAllFieldsInTable={() => {}} indicator={indicator} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);
expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TITLE)?.innerHTML).toContain(indicatorName);
});
it('should render the indicator name passed via context in the title', () => {
const context = {
kqlBarIntegration: false,
indicatorName: '123',
};
render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutOverview
onViewAllFieldsInTable={() => {}}
indicator={generateMockIndicator()}
/>
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);
expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TITLE)?.innerHTML).toContain(
context.indicatorName
);
});
});

View file

@ -16,6 +16,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useMemo, VFC } from 'react';
import { useIndicatorsFlyoutContext } from '../use_context';
import { EMPTY_VALUE } from '../../../../../common/constants';
import { Indicator, RawIndicatorFieldId } from '../../../../../../common/types/indicator';
import { unwrapValue } from '../../../utils';
@ -30,6 +31,7 @@ const highLevelFields = [
RawIndicatorFieldId.Confidence,
];
export const TI_FLYOUT_OVERVIEW_TITLE = 'tiFlyoutOverviewTitle';
export const TI_FLYOUT_OVERVIEW_TABLE = 'tiFlyoutOverviewTableRow';
export const TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS = 'tiFlyoutOverviewHighLevelBlocks';
@ -42,6 +44,8 @@ export const IndicatorsFlyoutOverview: VFC<IndicatorsFlyoutOverviewProps> = ({
indicator,
onViewAllFieldsInTable,
}) => {
const { indicatorName } = useIndicatorsFlyoutContext();
const indicatorType = unwrapValue(indicator, RawIndicatorFieldId.Type);
const highLevelBlocks = useMemo(
@ -67,7 +71,10 @@ export const IndicatorsFlyoutOverview: VFC<IndicatorsFlyoutOverviewProps> = ({
return unwrappedDescription ? <EuiText>{unwrappedDescription}</EuiText> : null;
}, [indicator]);
const indicatorName = unwrapValue(indicator, RawIndicatorFieldId.Name) || EMPTY_VALUE;
const title =
indicatorName != null
? indicatorName
: unwrapValue(indicator, RawIndicatorFieldId.Name) || EMPTY_VALUE;
if (!indicatorType) {
return <IndicatorEmptyPrompt />;
@ -76,7 +83,7 @@ export const IndicatorsFlyoutOverview: VFC<IndicatorsFlyoutOverviewProps> = ({
return (
<>
<EuiTitle>
<h2>{indicatorName}</h2>
<h2 data-test-subj={TI_FLYOUT_OVERVIEW_TITLE}>{title}</h2>
</EuiTitle>
{indicatorDescription}

View file

@ -21,7 +21,7 @@ export default {
component: IndicatorsFlyoutTable,
title: 'IndicatorsFlyoutTable',
};
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
@ -35,7 +35,7 @@ export const Default: Story<void> = () => {
return (
<KibanaReactContext.Provider>
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutTable indicator={mockIndicator} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
@ -45,7 +45,7 @@ export const Default: Story<void> = () => {
export const EmptyIndicator: Story<void> = () => {
return (
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutTable indicator={{ fields: {} } as unknown as Indicator} />
</IndicatorsFlyoutContext.Provider>
);

View file

@ -22,13 +22,13 @@ const mockIndicator: Indicator = generateMockIndicator();
describe('<IndicatorsFlyoutTable />', () => {
it('should render fields and values in table', () => {
const kqlBarIntegration = {
const context = {
kqlBarIntegration: false,
};
const { getByTestId, getByText, getAllByText } = render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutContext.Provider value={context}>
<IndicatorsFlyoutTable indicator={mockIndicator} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>