mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[TIP] Investigate in timeline (#140496)
* [TIP] Investigate in timeline - 2 InvestigateInTimeline components (one for Button display the other for ButtonIcon) and 1 useInvestigateInTimeline hook - add new investigate in timeline hook in Security Solution plugin and pass via context to TI plugin - replace UrlOriginal by UrlFull in the threat.indicator.name mapping - bump kbn-optimizer limit for threatIntelligence - add EuiTooltip for all EuiButtonIcon - add missing translations - replace css with EuiFlexGroup where possible
This commit is contained in:
parent
6add682702
commit
3099159d02
44 changed files with 1330 additions and 230 deletions
|
@ -110,7 +110,7 @@ pageLoadAssetSize:
|
|||
synthetics: 40958
|
||||
telemetry: 51957
|
||||
telemetryManagementSection: 38586
|
||||
threatIntelligence: 29195
|
||||
threatIntelligence: 44299
|
||||
timelines: 327300
|
||||
transform: 41007
|
||||
triggersActionsUi: 119000
|
||||
|
|
|
@ -11,6 +11,9 @@ import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
|||
import type { SecuritySolutionPluginContext } from '@kbn/threat-intelligence-plugin/public';
|
||||
import { THREAT_INTELLIGENCE_BASE_PATH } from '@kbn/threat-intelligence-plugin/public';
|
||||
import type { SourcererDataView } from '@kbn/threat-intelligence-plugin/public/types';
|
||||
import type { Store } from 'redux';
|
||||
import { useInvestigateInTimeline } from './use_investigate_in_timeline';
|
||||
import { getStore } from '../common/store';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { FiltersGlobal } from '../common/components/filters_global';
|
||||
import { SpyRoute } from '../common/utils/route/spy_routes';
|
||||
|
@ -32,11 +35,15 @@ const ThreatIntelligence = memo(() => {
|
|||
return <Redirect to="/" />;
|
||||
}
|
||||
|
||||
const securitySolutionStore = getStore() as Store;
|
||||
|
||||
const securitySolutionContext: SecuritySolutionPluginContext = {
|
||||
getFiltersGlobalComponent: () => FiltersGlobal,
|
||||
getPageWrapper: () => SecuritySolutionPageWrapper,
|
||||
licenseService,
|
||||
sourcererDataView: sourcererDataView as unknown as SourcererDataView,
|
||||
getSecuritySolutionStore: securitySolutionStore,
|
||||
getUseInvestigateInTimeline: useInvestigateInTimeline,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.threatIntelligence.investigateInTimelineTitle',
|
||||
{
|
||||
defaultMessage: 'Investigate in timeline',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { timelineDefaults } from '../timelines/store/timeline/defaults';
|
||||
import { APP_UI_ID } from '../../common/constants';
|
||||
import type { DataProvider } from '../../common/types';
|
||||
import { TimelineId, TimelineType } from '../../common/types';
|
||||
import { useDeepEqualSelector } from '../common/hooks/use_selector';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { useStartTransaction } from '../common/lib/apm/use_start_transaction';
|
||||
import { timelineActions, timelineSelectors } from '../timelines/store/timeline';
|
||||
import { useCreateTimeline } from '../timelines/components/timeline/properties/use_create_timeline';
|
||||
import type { CreateTimelineProps } from '../detections/components/alerts_table/types';
|
||||
import { dispatchUpdateTimeline } from '../timelines/components/open_timeline/helpers';
|
||||
|
||||
interface UseInvestigateInTimelineActionProps {
|
||||
/**
|
||||
* Created when the user clicks on the Investigate in Timeline button.
|
||||
* DataProvider contain the field(s) and value(s) displayed in the timeline.
|
||||
*/
|
||||
dataProviders: DataProvider[];
|
||||
/**
|
||||
* Start date used in the createTimeline method.
|
||||
*/
|
||||
from: string;
|
||||
/**
|
||||
* End date used in the createTimeline method.
|
||||
*/
|
||||
to: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook passed down to the Threat Intelligence plugin, via context.
|
||||
* This code is closely duplicated from here: https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx,
|
||||
* the main changes being:
|
||||
* - no exceptions are handled at the moment
|
||||
* - we use dataProviders, from and to directly instead of consuming ecsData
|
||||
*/
|
||||
export const useInvestigateInTimeline = ({
|
||||
dataProviders,
|
||||
from,
|
||||
to,
|
||||
}: UseInvestigateInTimelineActionProps) => {
|
||||
const {
|
||||
data: { query },
|
||||
} = useKibana().services;
|
||||
const dispatch = useDispatch();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const filterManagerBackup = useMemo(() => query.filterManager, [query.filterManager]);
|
||||
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
|
||||
const { filterManager: activeFilterManager } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, TimelineId.active ?? '')
|
||||
);
|
||||
const filterManager = useMemo(
|
||||
() => activeFilterManager ?? filterManagerBackup,
|
||||
[activeFilterManager, filterManagerBackup]
|
||||
);
|
||||
|
||||
const updateTimelineIsLoading = useCallback(
|
||||
(payload) => dispatch(timelineActions.updateIsLoading(payload)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const clearActiveTimeline = useCreateTimeline({
|
||||
timelineId: TimelineId.active,
|
||||
timelineType: TimelineType.default,
|
||||
});
|
||||
|
||||
const createTimeline = useCallback(
|
||||
({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
|
||||
clearActiveTimeline();
|
||||
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
|
||||
dispatchUpdateTimeline(dispatch)({
|
||||
duplicate: true,
|
||||
from: fromTimeline,
|
||||
id: TimelineId.active,
|
||||
notes: [],
|
||||
timeline: {
|
||||
...timeline,
|
||||
filterManager,
|
||||
indexNames: timeline.indexNames ?? [],
|
||||
show: true,
|
||||
},
|
||||
to: toTimeline,
|
||||
ruleNote,
|
||||
})();
|
||||
},
|
||||
[dispatch, filterManager, updateTimelineIsLoading, clearActiveTimeline]
|
||||
);
|
||||
|
||||
const investigateInTimelineClick = useCallback(async () => {
|
||||
startTransaction({ name: `${APP_UI_ID} threat indicator investigateInTimeline` });
|
||||
await createTimeline({
|
||||
from,
|
||||
notes: null,
|
||||
timeline: {
|
||||
...timelineDefaults,
|
||||
dataProviders,
|
||||
id: TimelineId.active,
|
||||
indexNames: [],
|
||||
dateRange: {
|
||||
start: from,
|
||||
end: to,
|
||||
},
|
||||
eventType: 'all',
|
||||
filters: [],
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
kind: 'kuery',
|
||||
expression: '',
|
||||
},
|
||||
serializedQuery: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
to,
|
||||
ruleNote: '',
|
||||
});
|
||||
}, [startTransaction, createTimeline, dataProviders, from, to]);
|
||||
|
||||
return investigateInTimelineClick;
|
||||
};
|
|
@ -38,8 +38,10 @@ export enum RawIndicatorFieldId {
|
|||
FileImphash = 'threat.indicator.file.imphash',
|
||||
FilePehash = 'threat.indicator.file.pehash',
|
||||
FileVhash = 'threat.indicator.file.vhash',
|
||||
FileTelfhash = 'threat.indicator.file.elf.telfhash',
|
||||
X509Serial = 'threat.indicator.x509.serial_number',
|
||||
WindowsRegistryKey = 'threat.indicator.registry.key',
|
||||
WindowsRegistryPath = 'threat.indicator.registry.path',
|
||||
AutonomousSystemNumber = 'threat.indicator.as.number',
|
||||
MacAddress = 'threat.indicator.mac',
|
||||
TimeStamp = '@timestamp',
|
||||
|
@ -49,6 +51,22 @@ export enum RawIndicatorFieldId {
|
|||
NameOrigin = 'threat.indicator.name_origin',
|
||||
}
|
||||
|
||||
/**
|
||||
* Threat indicator field map to Enriched Event.
|
||||
* (reverse of https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/common/cti/constants.ts#L35)
|
||||
*/
|
||||
export const IndicatorFieldEventEnrichmentMap: { [id: string]: string[] } = {
|
||||
[RawIndicatorFieldId.FileMd5]: ['file.hash.md5'],
|
||||
[RawIndicatorFieldId.FileSha1]: ['file.hash.sha1'],
|
||||
[RawIndicatorFieldId.FileSha256]: ['file.hash.sha256'],
|
||||
[RawIndicatorFieldId.FileImphash]: ['file.pe.imphash'],
|
||||
[RawIndicatorFieldId.FileTelfhash]: ['file.elf.telfhash'],
|
||||
[RawIndicatorFieldId.FileSSDeep]: ['file.hash.ssdeep'],
|
||||
[RawIndicatorFieldId.Ip]: ['source.ip', 'destination.ip'],
|
||||
[RawIndicatorFieldId.UrlFull]: ['url.full'],
|
||||
[RawIndicatorFieldId.WindowsRegistryPath]: ['registry.path'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Threat Intelligence Indicator interface.
|
||||
*/
|
||||
|
@ -93,7 +111,7 @@ export const generateMockUrlIndicator = (): Indicator => {
|
|||
indicator.fields['threat.indicator.url.full'] = ['https://0.0.0.0/test'];
|
||||
indicator.fields['threat.indicator.url.original'] = ['https://0.0.0.0/test'];
|
||||
indicator.fields['threat.indicator.name'] = ['https://0.0.0.0/test'];
|
||||
indicator.fields['threat.indicator.name_origin'] = ['threat.indicator.url.original'];
|
||||
indicator.fields['threat.indicator.name_origin'] = ['threat.indicator.url.full'];
|
||||
|
||||
return indicator;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
UNTITLED_TIMELINE_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM,
|
||||
INDICATORS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_ICON,
|
||||
INDICATOR_FLYOUT_INVESTIGATE_IN_TIMELINE_BUTTON,
|
||||
} from '../screens/indicators';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||
import { login } from '../tasks/login';
|
||||
|
@ -88,5 +90,19 @@ describe('Indicators', () => {
|
|||
cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
|
||||
it('should investigate in timeline when clicking in an indicator table action row', () => {
|
||||
cy.get(INDICATORS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_ICON).should('exist').first().click();
|
||||
cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
|
||||
it('should investigate in timeline when clicking in an indicator flyout', () => {
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(INDICATOR_FLYOUT_INVESTIGATE_IN_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -101,3 +101,9 @@ export const UNTITLED_TIMELINE_BUTTON = '[data-test-subj="flyoutOverlay"]';
|
|||
export const TIMELINE_DRAGGABLE_ITEM = '[data-test-subj="providerContainer"]';
|
||||
|
||||
export const KQL_FILTER = '[id="popoverFor_filter0"]';
|
||||
|
||||
export const INDICATORS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_ICON =
|
||||
'[data-test-subj="tiIndicatorTableInvestigateInTimelineButtonIcon"]';
|
||||
|
||||
export const INDICATOR_FLYOUT_INVESTIGATE_IN_TIMELINE_BUTTON =
|
||||
'[data-test-subj="tiIndicatorFlyoutInvestigateInTimelineButton"]';
|
||||
|
|
|
@ -28,4 +28,12 @@ export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext
|
|||
indexPattern: { fields: [], title: '' },
|
||||
loading: false,
|
||||
},
|
||||
getSecuritySolutionStore: {
|
||||
// @ts-ignore
|
||||
dispatch: () => jest.fn(),
|
||||
},
|
||||
getUseInvestigateInTimeline:
|
||||
({ dataProviders, from, to }) =>
|
||||
() =>
|
||||
new Promise((resolve) => window.alert('investigate in timeline')),
|
||||
});
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
|
||||
import React, { useState, VFC } from 'react';
|
||||
import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { FilterIn } from '../../../query_bar/components/filter_in';
|
||||
import { FilterOut } from '../../../query_bar/components/filter_out';
|
||||
|
@ -17,6 +18,10 @@ export const TIMELINE_BUTTON_TEST_ID = 'tiBarchartTimelineButton';
|
|||
export const FILTER_IN_BUTTON_TEST_ID = 'tiBarchartFilterInButton';
|
||||
export const FILTER_OUT_BUTTON_TEST_ID = 'tiBarchartFilterOutButton';
|
||||
|
||||
const BUTTON_LABEL = i18n.translate('xpack.threatIntelligence.indicator.barChart.popover', {
|
||||
defaultMessage: 'More actions',
|
||||
});
|
||||
|
||||
export interface IndicatorBarchartLegendActionProps {
|
||||
/**
|
||||
* Indicator
|
||||
|
@ -59,12 +64,15 @@ export const IndicatorBarchartLegendAction: VFC<IndicatorBarchartLegendActionPro
|
|||
<EuiPopover
|
||||
data-test-subj={POPOVER_BUTTON_TEST_ID}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="boxesHorizontal"
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
onClick={() => setPopover(!isPopoverOpen)}
|
||||
/>
|
||||
<EuiToolTip content={BUTTON_LABEL}>
|
||||
<EuiButtonIcon
|
||||
aria-label={BUTTON_LABEL}
|
||||
iconType="boxesHorizontal"
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
onClick={() => setPopover(!isPopoverOpen)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setPopover(false)}
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
||||
import React, { VFC } from 'react';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { FilterIn } from '../../../query_bar/components/filter_in';
|
||||
import { FilterOut } from '../../../query_bar/components/filter_out';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { getIndicatorFieldAndValue } from '../../lib/field_value';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../lib/field_value';
|
||||
|
||||
export const TIMELINE_BUTTON_TEST_ID = 'TimelineButton';
|
||||
export const FILTER_IN_BUTTON_TEST_ID = 'FilterInButton';
|
||||
|
@ -44,8 +44,7 @@ export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
|
|||
...props
|
||||
}) => {
|
||||
const { key, value } = getIndicatorFieldAndValue(indicator, field);
|
||||
|
||||
if (!key || value === EMPTY_VALUE || !key) {
|
||||
if (!fieldAndValueValid(key, value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -54,7 +53,7 @@ export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
|
|||
const timelineTestId = `${props['data-test-subj']}${TIMELINE_BUTTON_TEST_ID}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="center" alignItems="center">
|
||||
<FilterIn as={Component} data={indicator} field={field} data-test-subj={filterInTestId} />
|
||||
<FilterOut as={Component} data={indicator} field={field} data-test-subj={filterOutTestId} />
|
||||
<AddToTimeline
|
||||
|
@ -63,6 +62,6 @@ export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
|
|||
field={field}
|
||||
data-test-subj={timelineTestId}
|
||||
/>
|
||||
</>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
import React, { useMemo, useState, VFC } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
|
@ -18,6 +21,7 @@ import {
|
|||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { InvestigateInTimelineButton } from '../../../timeline/components/investigate_in_timeline_button';
|
||||
import { DateFormatter } from '../../../../components/date_formatter/date_formatter';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyoutJson } from './tabs/indicators_flyout_json/indicators_flyout_json';
|
||||
|
@ -28,6 +32,7 @@ import { IndicatorsFlyoutOverview } from './tabs/indicators_flyout_overview';
|
|||
export const TITLE_TEST_ID = 'tiIndicatorFlyoutTitle';
|
||||
export const SUBTITLE_TEST_ID = 'tiIndicatorFlyoutSubtitle';
|
||||
export const TABS_TEST_ID = 'tiIndicatorFlyoutTabs';
|
||||
export const INVESTIGATE_IN_TIMELINE_BUTTON_ID = 'tiIndicatorFlyoutInvestigateInTimelineButton';
|
||||
|
||||
const enum TAB_IDS {
|
||||
overview,
|
||||
|
@ -142,6 +147,16 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ indicator, closeF
|
|||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<InvestigateInTimelineButton
|
||||
data={indicator}
|
||||
data-test-subj={INVESTIGATE_IN_TIMELINE_BUTTON_ID}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ const VisibleOnHover = euiStyled.div`
|
|||
& .actionsWrapper {
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
margin-inline-start: ${theme.eui.euiSizeXS};
|
||||
margin-inline-start: ${theme.eui.euiSizeS};
|
||||
}
|
||||
|
||||
&:hover .actionsWrapper {
|
||||
|
|
|
@ -6,10 +6,14 @@
|
|||
*/
|
||||
|
||||
import React, { useContext, VFC } from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { InvestigateInTimelineButtonIcon } from '../../../timeline/components/investigate_in_timeline_button_icon';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { OpenIndicatorFlyoutButton } from '../open_indicator_flyout_button/open_indicator_flyout_button';
|
||||
import { IndicatorsTableContext } from './context';
|
||||
|
||||
const INVESTIGATE_TEST_ID = 'tiIndicatorTableInvestigateInTimelineButtonIcon';
|
||||
|
||||
export const ActionsRowCell: VFC<{ indicator: Indicator }> = ({ indicator }) => {
|
||||
const indicatorTableContext = useContext(IndicatorsTableContext);
|
||||
|
||||
|
@ -20,10 +24,13 @@ export const ActionsRowCell: VFC<{ indicator: Indicator }> = ({ indicator }) =>
|
|||
const { setExpanded, expanded } = indicatorTableContext;
|
||||
|
||||
return (
|
||||
<OpenIndicatorFlyoutButton
|
||||
indicator={indicator}
|
||||
onOpen={setExpanded}
|
||||
isOpen={Boolean(expanded && expanded._id === indicator._id)}
|
||||
/>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<OpenIndicatorFlyoutButton
|
||||
indicator={indicator}
|
||||
onOpen={setExpanded}
|
||||
isOpen={Boolean(expanded && expanded._id === indicator._id)}
|
||||
/>
|
||||
<InvestigateInTimelineButtonIcon data={indicator} data-test-subj={INVESTIGATE_TEST_ID} />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,11 +8,10 @@
|
|||
import React, { VFC } from 'react';
|
||||
import { EuiDataGridColumnCellActionProps } from '@elastic/eui/src/components/datagrid/data_grid_types';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { Pagination } from '../../hooks/use_indicators';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { getIndicatorFieldAndValue } from '../../lib/field_value';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../lib/field_value';
|
||||
import { FilterIn } from '../../../query_bar/components/filter_in';
|
||||
import { FilterOut } from '../../../query_bar/components/filter_out';
|
||||
|
||||
|
@ -47,8 +46,7 @@ export const CellActions: VFC<CellActionsProps> = ({
|
|||
}) => {
|
||||
const indicator = indicators[rowIndex % pagination.pageSize];
|
||||
const { key, value } = getIndicatorFieldAndValue(indicator, columnId);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
if (!fieldAndValueValid(key, value)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,13 @@ import { Indicator } from '../../../../../common/types/indicator';
|
|||
|
||||
export const BUTTON_TEST_ID = 'tiToggleIndicatorFlyoutButton';
|
||||
|
||||
const BUTTON_LABEL: string = i18n.translate(
|
||||
'xpack.threatIntelligence.indicator.table.viewDetailsButton',
|
||||
{
|
||||
defaultMessage: 'View details',
|
||||
}
|
||||
);
|
||||
|
||||
export interface OpenIndicatorFlyoutButtonProps {
|
||||
/**
|
||||
* {@link Indicator} passed to the flyout component.
|
||||
|
@ -35,22 +42,15 @@ export const OpenIndicatorFlyoutButton: VFC<OpenIndicatorFlyoutButtonProps> = ({
|
|||
onOpen,
|
||||
isOpen,
|
||||
}) => {
|
||||
const buttonLabel: string = i18n.translate(
|
||||
'xpack.threatIntelligence.indicator.table.viewDetailsButton',
|
||||
{
|
||||
defaultMessage: 'View details',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={buttonLabel} delay="long">
|
||||
<EuiToolTip content={BUTTON_LABEL}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={BUTTON_TEST_ID}
|
||||
color={isOpen ? 'text' : 'primary'}
|
||||
iconType={isOpen ? 'minimize' : 'expand'}
|
||||
isSelected={isOpen}
|
||||
iconSize="s"
|
||||
aria-label={buttonLabel}
|
||||
aria-label={BUTTON_LABEL}
|
||||
onClick={() => onOpen(indicator)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('display name generation', () => {
|
|||
if (doc['threat.indicator.file.pehash'].value!=null) { return emit(doc['threat.indicator.file.pehash'].value) }
|
||||
if (doc['threat.indicator.file.vhash'].value!=null) { return emit(doc['threat.indicator.file.vhash'].value) } }
|
||||
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='url') { if (doc['threat.indicator.url.original'].value!=null) { return emit(doc['threat.indicator.url.original'].value) } }
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='url') { if (doc['threat.indicator.url.full'].value!=null) { return emit(doc['threat.indicator.url.full'].value) } }
|
||||
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='domain') { if (doc['threat.indicator.url.domain'].value!=null) { return emit(doc['threat.indicator.url.domain'].value) } }
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='domain-name') { if (doc['threat.indicator.url.domain'].value!=null) { return emit(doc['threat.indicator.url.domain'].value) } }
|
||||
|
@ -83,7 +83,7 @@ describe('display name generation', () => {
|
|||
if (doc['threat.indicator.file.pehash'].value!=null) { return emit('threat.indicator.file.pehash') }
|
||||
if (doc['threat.indicator.file.vhash'].value!=null) { return emit('threat.indicator.file.vhash') } }
|
||||
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='url') { if (doc['threat.indicator.url.original'].value!=null) { return emit('threat.indicator.url.original') } }
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='url') { if (doc['threat.indicator.url.full'].value!=null) { return emit('threat.indicator.url.full') } }
|
||||
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='domain') { if (doc['threat.indicator.url.domain'].value!=null) { return emit('threat.indicator.url.domain') } }
|
||||
if (doc['threat.indicator.type'].value != null && doc['threat.indicator.type'].value.toLowerCase()=='domain-name') { if (doc['threat.indicator.url.domain'].value!=null) { return emit('threat.indicator.url.domain') } }
|
||||
|
|
|
@ -42,7 +42,7 @@ const mappingsArray: Mappings = [
|
|||
RawIndicatorFieldId.FileVhash,
|
||||
],
|
||||
],
|
||||
[['url'], [RawIndicatorFieldId.UrlOriginal]],
|
||||
[['url'], [RawIndicatorFieldId.UrlFull]],
|
||||
[['domain', 'domain-name'], [RawIndicatorFieldId.UrlDomain]],
|
||||
[['x509-certificate', 'x509 serial'], [RawIndicatorFieldId.X509Serial]],
|
||||
[['email-addr'], [RawIndicatorFieldId.EmailAddress]],
|
||||
|
|
|
@ -5,39 +5,84 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getIndicatorFieldAndValue } from './field_value';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from './field_value';
|
||||
import {
|
||||
generateMockFileIndicator,
|
||||
generateMockUrlIndicator,
|
||||
} from '../../../../common/types/indicator';
|
||||
import { EMPTY_VALUE } from '../../../../common/constants';
|
||||
|
||||
describe('getIndicatorFieldAndValue()', () => {
|
||||
it('should return field/value pair for an indicator', () => {
|
||||
const mockData = generateMockUrlIndicator();
|
||||
const mockKey = 'threat.feed.name';
|
||||
describe('field_value', () => {
|
||||
describe('getIndicatorFieldAndValue()', () => {
|
||||
it('should return field/value pair for an indicator', () => {
|
||||
const mockData = generateMockUrlIndicator();
|
||||
const mockKey = 'threat.feed.name';
|
||||
|
||||
const result = getIndicatorFieldAndValue(mockData, mockKey);
|
||||
expect(result.key).toEqual(mockKey);
|
||||
expect(result.value).toEqual((mockData.fields[mockKey] as unknown as string[])[0]);
|
||||
const result = getIndicatorFieldAndValue(mockData, mockKey);
|
||||
expect(result.key).toEqual(mockKey);
|
||||
expect(result.value).toEqual((mockData.fields[mockKey] as unknown as string[])[0]);
|
||||
});
|
||||
|
||||
it('should return a null value for an incorrect field', () => {
|
||||
const mockData = generateMockUrlIndicator();
|
||||
const mockKey = 'abc';
|
||||
|
||||
const result = getIndicatorFieldAndValue(mockData, mockKey);
|
||||
expect(result.key).toEqual(mockKey);
|
||||
expect(result.value).toBeNull();
|
||||
});
|
||||
|
||||
it('should return field/value pair for an indicator and DisplayName field', () => {
|
||||
const mockData = generateMockFileIndicator();
|
||||
const mockKey = 'threat.indicator.name';
|
||||
|
||||
const result = getIndicatorFieldAndValue(mockData, mockKey);
|
||||
expect(result.key).toEqual(
|
||||
(mockData.fields['threat.indicator.name_origin'] as unknown as string[])[0]
|
||||
);
|
||||
expect(result.value).toEqual((mockData.fields[mockKey] as unknown as string[])[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a null value for an incorrect field', () => {
|
||||
const mockData = generateMockUrlIndicator();
|
||||
const mockKey = 'abc';
|
||||
describe('fieldAndValueValid()', () => {
|
||||
it('should return false for null value', () => {
|
||||
const mockField = 'abc';
|
||||
const mockValue = null;
|
||||
|
||||
const result = getIndicatorFieldAndValue(mockData, mockKey);
|
||||
expect(result.key).toEqual(mockKey);
|
||||
expect(result.value).toBeNull();
|
||||
});
|
||||
const result = fieldAndValueValid(mockField, mockValue);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return field/value pair for an indicator and DisplayName field', () => {
|
||||
const mockData = generateMockFileIndicator();
|
||||
const mockKey = 'threat.indicator.name';
|
||||
it('should return false for empty string value', () => {
|
||||
const mockField = 'abc';
|
||||
const mockValue = '';
|
||||
|
||||
const result = getIndicatorFieldAndValue(mockData, mockKey);
|
||||
expect(result.key).toEqual(
|
||||
(mockData.fields['threat.indicator.name_origin'] as unknown as string[])[0]
|
||||
);
|
||||
expect(result.value).toEqual((mockData.fields[mockKey] as unknown as string[])[0]);
|
||||
const result = fieldAndValueValid(mockField, mockValue);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it(`should return false for ${EMPTY_VALUE} value`, () => {
|
||||
const mockField = 'abc';
|
||||
const mockValue = EMPTY_VALUE;
|
||||
|
||||
const result = fieldAndValueValid(mockField, mockValue);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false for empty field', () => {
|
||||
const mockField = '';
|
||||
const mockValue = 'abc';
|
||||
|
||||
const result = fieldAndValueValid(mockField, mockValue);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true if field and value are correct', () => {
|
||||
const mockField = 'abc';
|
||||
const mockValue = 'abc';
|
||||
|
||||
const result = fieldAndValueValid(mockField, mockValue);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EMPTY_VALUE } from '../../../../common/constants';
|
||||
import { unwrapValue } from './unwrap_value';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../common/types/indicator';
|
||||
|
||||
|
@ -29,3 +30,12 @@ export const getIndicatorFieldAndValue = (
|
|||
value,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if field and value are correct
|
||||
* @param field Indicator string field
|
||||
* @param value Indicator string|null value for the field
|
||||
* @returns true if correct, false if not
|
||||
*/
|
||||
export const fieldAndValueValid = (field: string | null, value: string | null): boolean =>
|
||||
!!value && value !== EMPTY_VALUE && !!field;
|
||||
|
|
|
@ -127,6 +127,31 @@ Object {
|
|||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
|
@ -142,24 +167,7 @@ Object {
|
|||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
|
@ -220,6 +228,29 @@ Object {
|
|||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
aria-label="Filter In"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
aria-label="Filter In"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
|
@ -233,22 +264,7 @@ Object {
|
|||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-label="Filter In"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
|
|
|
@ -7,13 +7,12 @@
|
|||
|
||||
import React, { useCallback, VFC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem, EuiToolTip } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { FilterIn as FilterInConst, updateFiltersArray } from '../../lib/filter';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
|
@ -66,16 +65,18 @@ export const FilterIn: VFC<FilterInProps> = ({ data, field, type, as: Component,
|
|||
filterManager.setFilters(newFilters);
|
||||
}, [filterManager, key, value]);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
if (!fieldAndValueValid(key, value)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (type === ComponentType.EuiDataGrid) {
|
||||
return (
|
||||
<div {...props} css={styles.button}>
|
||||
{/* @ts-ignore*/}
|
||||
<Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterIn} />
|
||||
</div>
|
||||
<EuiToolTip content={ICON_TITLE}>
|
||||
<div {...props} css={styles.button}>
|
||||
{/* @ts-ignore*/}
|
||||
<Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterIn} />
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -94,14 +95,16 @@ export const FilterIn: VFC<FilterInProps> = ({ data, field, type, as: Component,
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={ICON_TITLE}
|
||||
iconType={ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterIn}
|
||||
{...props}
|
||||
/>
|
||||
<EuiToolTip content={ICON_TITLE}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ICON_TITLE}
|
||||
iconType={ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterIn}
|
||||
{...props}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -127,6 +127,31 @@ Object {
|
|||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
|
@ -142,24 +167,7 @@ Object {
|
|||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
|
@ -220,6 +228,29 @@ Object {
|
|||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
|
@ -233,22 +264,7 @@ Object {
|
|||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
|
|
|
@ -7,13 +7,12 @@
|
|||
|
||||
import React, { useCallback, VFC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem, EuiToolTip } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { FilterOut as FilterOutConst, updateFiltersArray } from '../../lib/filter';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
|
@ -66,16 +65,18 @@ export const FilterOut: VFC<FilterOutProps> = ({ data, field, type, as: Componen
|
|||
filterManager.setFilters(newFilters);
|
||||
}, [filterManager, key, value]);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
if (!fieldAndValueValid(key, value)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (type === ComponentType.EuiDataGrid) {
|
||||
return (
|
||||
<div {...props} css={styles.button}>
|
||||
{/* @ts-ignore*/}
|
||||
<Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterOut} />
|
||||
</div>
|
||||
<EuiToolTip content={ICON_TITLE}>
|
||||
<div {...props} css={styles.button}>
|
||||
{/* @ts-ignore*/}
|
||||
<Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterOut} />
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -94,14 +95,16 @@ export const FilterOut: VFC<FilterOutProps> = ({ data, field, type, as: Componen
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={ICON_TITLE}
|
||||
iconType={ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterOut}
|
||||
{...props}
|
||||
/>
|
||||
<EuiToolTip content={ICON_TITLE}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ICON_TITLE}
|
||||
iconType={ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterOut}
|
||||
{...props}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -249,8 +249,27 @@ Object {
|
|||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
css="[object Object]"
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
|
@ -258,18 +277,7 @@ Object {
|
|||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
|
@ -330,8 +338,27 @@ Object {
|
|||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<div
|
||||
css="[object Object]"
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
|
@ -339,18 +366,7 @@ Object {
|
|||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import React, { useRef, VFC } from 'react';
|
||||
import { DataProvider, QueryOperator } from '@kbn/timelines-plugin/common';
|
||||
import { DataProvider } from '@kbn/timelines-plugin/common';
|
||||
import { AddToTimelineButtonProps } from '@kbn/timelines-plugin/public';
|
||||
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui/src/components/button';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { EuiContextMenuItem, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { generateDataProvider } from '../../lib/data_provider';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
@ -61,27 +63,11 @@ export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, type, as,
|
|||
const { key, value } =
|
||||
typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
if (!fieldAndValueValid(key, value)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const operator = ':' as QueryOperator;
|
||||
|
||||
const dataProvider: DataProvider[] = [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: `timeline-indicator-${key}-${value}`,
|
||||
name: value,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: key,
|
||||
value,
|
||||
operator,
|
||||
},
|
||||
},
|
||||
];
|
||||
const dataProvider: DataProvider[] = [generateDataProvider(key, value as string)];
|
||||
|
||||
const addToTimelineProps: AddToTimelineButtonProps = {
|
||||
dataProvider,
|
||||
|
@ -105,7 +91,10 @@ export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, type, as,
|
|||
onClick={() => contextMenuRef.current?.click()}
|
||||
{...props}
|
||||
>
|
||||
Add to Timeline
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.addToTimelineContextMenu"
|
||||
defaultMessage="Add to Timeline"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
</>
|
||||
);
|
||||
|
@ -114,8 +103,12 @@ export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, type, as,
|
|||
if (as) addToTimelineProps.Component = as;
|
||||
|
||||
return (
|
||||
<div {...props} css={styles.inlineFlex}>
|
||||
{addToTimelineButton(addToTimelineProps)}
|
||||
</div>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.threatIntelligence.addToTimelineIconButton', {
|
||||
defaultMessage: 'Add to Timeline',
|
||||
})}
|
||||
>
|
||||
<EuiFlexItem {...props}>{addToTimelineButton(addToTimelineProps)}</EuiFlexItem>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,16 +8,11 @@
|
|||
import { CSSObject } from '@emotion/react';
|
||||
|
||||
export const useStyles = () => {
|
||||
const inlineFlex: CSSObject = {
|
||||
display: 'inline-flex',
|
||||
};
|
||||
|
||||
const displayNone: CSSObject = {
|
||||
display: 'none',
|
||||
};
|
||||
|
||||
return {
|
||||
inlineFlex,
|
||||
displayNone,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<InvestigateInTimelineButton /> should render button when Indicator data is correct 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--fill"
|
||||
data-test-subj="mockId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Investigate in Timeline
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--fill"
|
||||
data-test-subj="mockId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Investigate in Timeline
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<InvestigateInTimelineButton /> should render empty component when Indicator data is incorrect 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './investigate_in_timeline_button';
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { StoryProvidersComponent } from '../../../../common/mocks/story_providers';
|
||||
import { generateMockUrlIndicator } from '../../../../../common/types/indicator';
|
||||
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
|
||||
|
||||
export default {
|
||||
component: InvestigateInTimelineButton,
|
||||
title: 'InvestigateInTimelineButton',
|
||||
};
|
||||
|
||||
const mockIndicator = generateMockUrlIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return (
|
||||
<StoryProvidersComponent>
|
||||
<InvestigateInTimelineButton data={mockIndicator} />
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import {
|
||||
generateMockIndicator,
|
||||
generateMockUrlIndicator,
|
||||
Indicator,
|
||||
} from '../../../../../common/types/indicator';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
|
||||
|
||||
describe('<InvestigateInTimelineButton />', () => {
|
||||
it('should render button when Indicator data is correct', () => {
|
||||
const mockData: Indicator = generateMockUrlIndicator();
|
||||
const mockId = 'mockId';
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<InvestigateInTimelineButton data={mockData} data-test-subj={mockId} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component.getByTestId(mockId)).toBeInTheDocument();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render empty component when Indicator data is incorrect', () => {
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
mockData.fields['threat.indicator.first_seen'] = [''];
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<InvestigateInTimelineButton data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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 React, { VFC } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useInvestigateInTimeline } from '../../hooks/use_investigate_in_timeline';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
|
||||
export interface InvestigateInTimelineButtonProps {
|
||||
/**
|
||||
* Value passed to the timeline. Used in combination with field if is type of {@link Indicator}.
|
||||
*/
|
||||
data: Indicator;
|
||||
/**
|
||||
* Used for unit and e2e tests.
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Investigate in timeline button, supports being passed a {@link Indicator}.
|
||||
* This implementation uses the InvestigateInTimelineAction component (x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx)
|
||||
* retrieved from the SecuritySolutionContext.
|
||||
*
|
||||
* @returns add to timeline button or an empty component.
|
||||
*/
|
||||
export const InvestigateInTimelineButton: VFC<InvestigateInTimelineButtonProps> = ({
|
||||
data,
|
||||
...props
|
||||
}) => {
|
||||
const { onClick } = useInvestigateInTimeline({ indicator: data });
|
||||
|
||||
if (!onClick) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButton onClick={onClick} fill {...props}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Investigate in Timeline"
|
||||
id="xpack.threatIntelligence.investigateInTimelineButton"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,159 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<InvestigateInTimelineButtonIcon /> should render button icon when Indicator data is correct 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
aria-label="Investigate in Timeline"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="mockId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="timeline"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
aria-label="Investigate in Timeline"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="mockId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="timeline"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<InvestigateInTimelineButtonIcon /> should render empty component when calculated value is - 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './investigate_in_timeline_button_icon';
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { StoryProvidersComponent } from '../../../../common/mocks/story_providers';
|
||||
import { generateMockUrlIndicator } from '../../../../../common/types/indicator';
|
||||
import { InvestigateInTimelineButtonIcon } from './investigate_in_timeline_button_icon';
|
||||
|
||||
export default {
|
||||
component: InvestigateInTimelineButtonIcon,
|
||||
title: 'InvestigateInTimelineButtonIcon',
|
||||
};
|
||||
|
||||
const mockIndicator = generateMockUrlIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return (
|
||||
<StoryProvidersComponent>
|
||||
<InvestigateInTimelineButtonIcon data={mockIndicator} />
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import {
|
||||
generateMockIndicator,
|
||||
generateMockUrlIndicator,
|
||||
Indicator,
|
||||
} from '../../../../../common/types/indicator';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { InvestigateInTimelineButtonIcon } from './investigate_in_timeline_button_icon';
|
||||
|
||||
describe('<InvestigateInTimelineButtonIcon />', () => {
|
||||
it('should render button icon when Indicator data is correct', () => {
|
||||
const mockData: Indicator = generateMockUrlIndicator();
|
||||
const mockId = 'mockId';
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<InvestigateInTimelineButtonIcon data={mockData} data-test-subj={mockId} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component.getByTestId(mockId)).toBeInTheDocument();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`should render empty component when calculated value is ${EMPTY_VALUE}`, () => {
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
mockData.fields['threat.indicator.first_seen'] = [''];
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<InvestigateInTimelineButtonIcon data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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, { VFC } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useInvestigateInTimeline } from '../../hooks/use_investigate_in_timeline';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
|
||||
const BUTTON_LABEL: string = i18n.translate(
|
||||
'xpack.threatIntelligence.investigateInTimelineButtonIcon',
|
||||
{
|
||||
defaultMessage: 'Investigate in Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export interface InvestigateInTimelineButtonIconProps {
|
||||
/**
|
||||
* Value passed to the timeline. Used in combination with field if is type of {@link Indicator}.
|
||||
*/
|
||||
data: Indicator;
|
||||
/**
|
||||
* Used for unit and e2e tests.
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Investigate in timeline button, supports being passed a {@link Indicator}.
|
||||
* This implementation uses the InvestigateInTimelineAction component (x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx)
|
||||
* retrieved from the SecuritySolutionContext.
|
||||
*
|
||||
* @returns add to timeline button or an empty component.
|
||||
*/
|
||||
export const InvestigateInTimelineButtonIcon: VFC<InvestigateInTimelineButtonIconProps> = ({
|
||||
data,
|
||||
...props
|
||||
}) => {
|
||||
const { onClick } = useInvestigateInTimeline({ indicator: data });
|
||||
|
||||
if (!onClick) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip content={BUTTON_LABEL}>
|
||||
<EuiButtonIcon
|
||||
aria-label={BUTTON_LABEL}
|
||||
iconType="timeline"
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './use_investigate_in_timeline';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { renderHook, RenderHookResult, Renderer } from '@testing-library/react-hooks';
|
||||
import {
|
||||
useInvestigateInTimeline,
|
||||
UseInvestigateInTimelineValue,
|
||||
} from './use_investigate_in_timeline';
|
||||
import {
|
||||
generateMockIndicator,
|
||||
generateMockUrlIndicator,
|
||||
} from '../../../../common/types/indicator';
|
||||
import { TestProvidersComponent } from '../../../common/mocks/test_providers';
|
||||
|
||||
describe('useInvestigateInTimeline()', () => {
|
||||
let hookResult: RenderHookResult<{}, UseInvestigateInTimelineValue, Renderer<unknown>>;
|
||||
|
||||
it('should return empty object if Indicator is incorrect', () => {
|
||||
const indicator = generateMockIndicator();
|
||||
indicator.fields['threat.indicator.name'] = ['wrong'];
|
||||
|
||||
hookResult = renderHook(() => useInvestigateInTimeline({ indicator }), {
|
||||
wrapper: TestProvidersComponent,
|
||||
});
|
||||
expect(hookResult.result.current).toEqual({});
|
||||
});
|
||||
|
||||
it('should return ', () => {
|
||||
const indicator = generateMockUrlIndicator();
|
||||
|
||||
hookResult = renderHook(() => useInvestigateInTimeline({ indicator }), {
|
||||
wrapper: TestProvidersComponent,
|
||||
});
|
||||
|
||||
expect(hookResult.result.current).toHaveProperty('onClick');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { useContext } from 'react';
|
||||
import moment from 'moment';
|
||||
import { DataProvider } from '@kbn/timelines-plugin/common';
|
||||
import { generateDataProvider } from '../lib/data_provider';
|
||||
import { SecuritySolutionContext } from '../../../containers/security_solution_context';
|
||||
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../indicators/lib/field_value';
|
||||
import { unwrapValue } from '../../indicators/lib/unwrap_value';
|
||||
import {
|
||||
Indicator,
|
||||
IndicatorFieldEventEnrichmentMap,
|
||||
RawIndicatorFieldId,
|
||||
} from '../../../../common/types/indicator';
|
||||
|
||||
export interface UseInvestigateInTimelineParam {
|
||||
/**
|
||||
* Indicator used to retrieve the field and value then passed to the Investigate in Timeline logic
|
||||
*/
|
||||
indicator: Indicator;
|
||||
}
|
||||
|
||||
export interface UseInvestigateInTimelineValue {
|
||||
onClick: (() => Promise<void>) | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook that gets an {@link Indicator}, retrieves the field (from the RawIndicatorFieldId.Name)
|
||||
* and value, then creates DataProviders used to do the Investigate in Timeline logic
|
||||
* (see /kibana/x-pack/plugins/security_solution/public/threat_intelligence/use_investigate_in_timeline.ts)
|
||||
*/
|
||||
export const useInvestigateInTimeline = ({
|
||||
indicator,
|
||||
}: UseInvestigateInTimelineParam): UseInvestigateInTimelineValue => {
|
||||
const securitySolutionContext = useContext(SecuritySolutionContext);
|
||||
|
||||
const { key, value } = getIndicatorFieldAndValue(indicator, RawIndicatorFieldId.Name);
|
||||
if (!fieldAndValueValid(key, value)) {
|
||||
return {} as unknown as UseInvestigateInTimelineValue;
|
||||
}
|
||||
|
||||
const dataProviders: DataProvider[] = [...IndicatorFieldEventEnrichmentMap[key], key].map(
|
||||
(e: string) => generateDataProvider(e, value as string)
|
||||
);
|
||||
|
||||
const to = unwrapValue(indicator, RawIndicatorFieldId.TimeStamp) as string;
|
||||
const from = moment(to).subtract(10, 'm').toISOString();
|
||||
|
||||
const investigateInTimelineClick = securitySolutionContext?.getUseInvestigateInTimeline({
|
||||
dataProviders,
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
return {
|
||||
onClick: investigateInTimelineClick,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { generateDataProvider } from './data_provider';
|
||||
|
||||
describe('generateDataProvider', () => {
|
||||
it('should return DataProvider object', () => {
|
||||
const mockField: string = 'field';
|
||||
const mockValue: string = 'value';
|
||||
|
||||
const dataProvider = generateDataProvider(mockField, mockValue);
|
||||
expect(dataProvider.id).toContain(mockField);
|
||||
expect(dataProvider.id).toContain(mockValue);
|
||||
expect(dataProvider.name).toEqual(mockValue);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { DataProvider, QueryOperator } from '@kbn/timelines-plugin/common';
|
||||
|
||||
/**
|
||||
* Generate a DataProvider object to use when adding/investigating to/in a timeline.
|
||||
* @param field used to generate the DataProvider id as well as its queryMatch
|
||||
* @param value used to generate the DataProvider id as well as its name and queryMatch
|
||||
*/
|
||||
export const generateDataProvider = (field: string, value: string): DataProvider => {
|
||||
const operator = ':' as QueryOperator;
|
||||
const id: string = `timeline-indicator-${field}-${value}`;
|
||||
|
||||
return {
|
||||
and: [],
|
||||
enabled: true,
|
||||
id,
|
||||
name: value,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { Provider as ReduxStoreProvider } from 'react-redux';
|
||||
import React, { Suspense, VFC } from 'react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { KibanaContextProvider } from './hooks/use_kibana';
|
||||
|
@ -46,15 +47,17 @@ export const createApp =
|
|||
({ securitySolutionContext }: AppProps) =>
|
||||
(
|
||||
<IntlProvider>
|
||||
<SecuritySolutionContext.Provider value={securitySolutionContext}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EnterpriseGuard>
|
||||
<IntegrationsGuard>
|
||||
<IndicatorsPage />
|
||||
</IntegrationsGuard>
|
||||
</EnterpriseGuard>
|
||||
</KibanaContextProvider>
|
||||
</SecuritySolutionContext.Provider>
|
||||
<ReduxStoreProvider store={securitySolutionContext.getSecuritySolutionStore}>
|
||||
<SecuritySolutionContext.Provider value={securitySolutionContext}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EnterpriseGuard>
|
||||
<IntegrationsGuard>
|
||||
<IndicatorsPage />
|
||||
</IntegrationsGuard>
|
||||
</EnterpriseGuard>
|
||||
</KibanaContextProvider>
|
||||
</SecuritySolutionContext.Provider>
|
||||
</ReduxStoreProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
|
@ -77,7 +80,9 @@ export class ThreatIntelligencePlugin implements Plugin<void, void> {
|
|||
...plugins,
|
||||
} as Services;
|
||||
|
||||
return { getComponent: createApp(services) };
|
||||
return {
|
||||
getComponent: createApp(services),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
|
|
@ -18,6 +18,8 @@ import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
|||
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
|
||||
import { DataViewBase } from '@kbn/es-query';
|
||||
import { Store } from 'redux';
|
||||
import { DataProvider } from '@kbn/timelines-plugin/common';
|
||||
|
||||
export interface SecuritySolutionDataViewBase extends DataViewBase {
|
||||
fields: Array<FieldSpec & DataViewField>;
|
||||
|
@ -58,6 +60,12 @@ export interface SourcererDataView {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface UseInvestigateInTimelineProps {
|
||||
dataProviders: DataProvider[];
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods exposed from the security solution to the threat intelligence application.
|
||||
*/
|
||||
|
@ -77,7 +85,19 @@ export interface SecuritySolutionPluginContext {
|
|||
*/
|
||||
licenseService: LicenseAware;
|
||||
/**
|
||||
* Gets Security Solution shared information like browerFields, indexPattern and selectedPatterns in DataView
|
||||
* Gets Security Solution shared information like browerFields, indexPattern and selectedPatterns in DataView.
|
||||
*/
|
||||
sourcererDataView: SourcererDataView;
|
||||
/**
|
||||
* Security Solution store
|
||||
*/
|
||||
getSecuritySolutionStore: Store;
|
||||
/**
|
||||
* Pass UseInvestigateInTimeline functionality to TI plugin
|
||||
*/
|
||||
getUseInvestigateInTimeline: ({
|
||||
dataProviders,
|
||||
from,
|
||||
to,
|
||||
}: UseInvestigateInTimelineProps) => () => Promise<void>;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue