Skip to content

Commit

Permalink
[TIP] Investigate in timeline (#140496)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
PhilippeOberti authored Sep 19, 2022
1 parent 6add682 commit 3099159
Show file tree
Hide file tree
Showing 44 changed files with 1,330 additions and 230 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pageLoadAssetSize:
synthetics: 40958
telemetry: 51957
telemetryManagementSection: 38586
threatIntelligence: 29195
threatIntelligence: 44299
timelines: 327300
transform: 41007
triggersActionsUi: 119000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
}
);
Original file line number Diff line number Diff line change
@@ -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;
};
20 changes: 19 additions & 1 deletion x-pack/plugins/threat_intelligence/common/types/indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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;
};
Expand Down
16 changes: 16 additions & 0 deletions x-pack/plugins/threat_intelligence/cypress/e2e/timeline.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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"]';
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand All @@ -63,6 +62,6 @@ export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
field={field}
data-test-subj={timelineTestId}
/>
</>
</EuiFlexGroup>
);
};
Loading

0 comments on commit 3099159

Please sign in to comment.