diff --git a/x-pack/plugins/threat_intelligence/README.md b/x-pack/plugins/threat_intelligence/README.md index 4be0e18819075..8c9c690924218 100755 --- a/x-pack/plugins/threat_intelligence/README.md +++ b/x-pack/plugins/threat_intelligence/README.md @@ -18,6 +18,10 @@ Verify your node version [here](https://github.com/elastic/kibana/blob/main/.nod **Run Kibana:** +> **Important:** +> +> See here to get your `kibana.yaml` to enable the Threat Intelligence plugin. + ``` yarn kbn reset && yarn kbn bootstrap yarn start --no-base-path diff --git a/x-pack/plugins/threat_intelligence/common/types/indicator.ts b/x-pack/plugins/threat_intelligence/common/types/indicator.ts index 4a219794009bf..e131063d344cb 100644 --- a/x-pack/plugins/threat_intelligence/common/types/indicator.ts +++ b/x-pack/plugins/threat_intelligence/common/types/indicator.ts @@ -5,6 +5,9 @@ * 2.0. */ +/** + * Enum of indicator fields supported by the Threat Intelligence plugin. + */ export enum RawIndicatorFieldId { Type = 'threat.indicator.type', FirstSeen = 'threat.indicator.first_seen', @@ -21,11 +24,17 @@ export enum RawIndicatorFieldId { TimeStamp = '@timestamp', } +/** + * Threat Intelligence Indicator interface. + */ export interface Indicator { _id?: unknown; fields: Partial>; } +/** + * Used to create new Indicators, used mainly in jest unit tests and Storybook stories. + */ export const generateMockIndicator = (): Indicator => ({ fields: { '@timestamp': ['2022-01-01T01:01:01.000Z'], @@ -37,6 +46,9 @@ export const generateMockIndicator = (): Indicator => ({ _id: Math.random(), }); +/** + * Used to create new url-type Indicators, used mainly in jest unit tests and Storybook stories. + */ export const generateMockUrlIndicator = (): Indicator => { const indicator = generateMockIndicator(); @@ -46,6 +58,9 @@ export const generateMockUrlIndicator = (): Indicator => { return indicator; }; +/** + * Used to create new file-type Indicators, used mainly in jest unit tests and Storybook stories. + */ export const generateMockFileIndicator = (): Indicator => { const indicator = generateMockIndicator(); diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/empty_page/empty_page.spec.ts b/x-pack/plugins/threat_intelligence/cypress/integration/empty_page.spec.ts similarity index 92% rename from x-pack/plugins/threat_intelligence/cypress/integration/empty_page/empty_page.spec.ts rename to x-pack/plugins/threat_intelligence/cypress/integration/empty_page.spec.ts index edcb1a472547a..6e31db8e12edc 100644 --- a/x-pack/plugins/threat_intelligence/cypress/integration/empty_page/empty_page.spec.ts +++ b/x-pack/plugins/threat_intelligence/cypress/integration/empty_page.spec.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { login } from '../../tasks/login'; +import { login } from '../tasks/login'; import { EMPTY_PAGE_BODY, EMPTY_PAGE_DOCS_LINK, EMPTY_PAGE_INTEGRATIONS_LINK, -} from '../../screens/empty_page'; +} from '../screens/empty_page'; const THREAT_INTEL_PATH = '/app/security/threat_intelligence/'; diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/indicators/indicators.spec.ts b/x-pack/plugins/threat_intelligence/cypress/integration/indicators.spec.ts similarity index 96% rename from x-pack/plugins/threat_intelligence/cypress/integration/indicators/indicators.spec.ts rename to x-pack/plugins/threat_intelligence/cypress/integration/indicators.spec.ts index 70dbd9e9fbf61..d331f2937e41d 100644 --- a/x-pack/plugins/threat_intelligence/cypress/integration/indicators/indicators.spec.ts +++ b/x-pack/plugins/threat_intelligence/cypress/integration/indicators.spec.ts @@ -25,10 +25,10 @@ import { ENDING_BREADCRUMB, FIELD_BROWSER, FIELD_BROWSER_MODAL, -} from '../../screens/indicators'; -import { login } from '../../tasks/login'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; -import { selectRange } from '../../tasks/select_range'; +} from '../screens/indicators'; +import { login } from '../tasks/login'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { selectRange } from '../tasks/select_range'; before(() => { login(); diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/timeline.spec.ts b/x-pack/plugins/threat_intelligence/cypress/integration/timeline.spec.ts new file mode 100644 index 0000000000000..8235a9ac513ae --- /dev/null +++ b/x-pack/plugins/threat_intelligence/cypress/integration/timeline.spec.ts @@ -0,0 +1,64 @@ +/* + * 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 { + BARCHART_TIMELINE_BUTTON, + FLYOUT_CLOSE_BUTTON, + FLYOUT_TABLE_ROW_TIMELINE_BUTTON, + INDICATOR_TYPE_CELL, + INDICATORS_TABLE_CELL_TIMELINE_BUTTON, + TIMELINE_DRAGGABLE_ITEM, + TOGGLE_FLYOUT_BUTTON, + UNTITLED_TIMELINE_BUTTON, +} from '../screens/indicators'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { login } from '../tasks/login'; +import { selectRange } from '../tasks/select_range'; + +const THREAT_INTELLIGENCE = '/app/security/threat_intelligence/indicators'; + +before(() => { + login(); +}); + +describe('Indicators', () => { + before(() => { + esArchiverLoad('threat_intelligence'); + }); + after(() => { + esArchiverUnload('threat_intelligence'); + }); + + describe('Indicators timeline interactions', () => { + before(() => { + cy.visit(THREAT_INTELLIGENCE); + + selectRange(); + }); + + it('should add entry in timeline when clicking in the barchart legend', () => { + cy.get(BARCHART_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); + }); + + it('should add entry in timeline when clicking in an indicator table cell', () => { + cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover'); + cy.get(INDICATORS_TABLE_CELL_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); + }); + + it('should add entry in timeline when clicking in an indicators flyout row', () => { + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_TABLE_ROW_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'); + }); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts index f5ab5f46f687d..e903abb729023 100644 --- a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts +++ b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts @@ -17,6 +17,8 @@ export const INDICATORS_TABLE = `[data-test-subj="tiIndicatorsTable"]`; export const TOGGLE_FLYOUT_BUTTON = `[data-test-subj="tiToggleIndicatorFlyoutButton"]`; +export const FLYOUT_CLOSE_BUTTON = `[data-test-subj="euiFlyoutCloseButton"]`; + export const FLYOUT_TITLE = `[data-test-subj="tiIndicatorFlyoutTitle"]`; export const FLYOUT_TABS = `[data-test-subj="tiIndicatorFlyoutTabs"]`; @@ -46,3 +48,14 @@ export const FIELD_BROWSER_MODAL = `[data-test-subj="fields-browser-container"]` export const FIELD_BROWSER_MODAL_SOURCE_CHECKBOX = `[data-test-subj="field-_source-checkbox"]`; export const FIELD_BROWSER_CLOSE = `[data-test-subj="close"]`; + +export const BARCHART_TIMELINE_BUTTON = '[data-test-subj="tiTimelineButton"]'; + +export const INDICATORS_TABLE_CELL_TIMELINE_BUTTON = + '[data-test-subj="tiIndicatorsTableCellTimelineButton"]'; + +export const FLYOUT_TABLE_ROW_TIMELINE_BUTTON = '[data-test-subj="tiFlyoutTableRowTimelineButton"]'; + +export const UNTITLED_TIMELINE_BUTTON = '[data-test-subj="flyoutOverlay"]'; + +export const TIMELINE_DRAGGABLE_ITEM = '[data-test-subj="providerContainer"]'; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts new file mode 100644 index 0000000000000..39a03139f30a4 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +/** + * Mock to map an indicator field to its type. + */ +export const generateFieldTypeMap = (): { [id: string]: string } => ({ + '@timestamp': 'date', + 'threat.indicator.ip': 'ip', + 'threat.indicator.first_seen': 'date', + 'threat.feed.name': 'string', +}); diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_timelines_service.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_timelines_service.tsx new file mode 100644 index 0000000000000..2c7cd529a4207 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_timelines_service.tsx @@ -0,0 +1,29 @@ +/* + * 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 { HoverActionsConfig } from '@kbn/timelines-plugin/public/components/hover_actions'; +import { EuiButtonIcon } from '@elastic/eui'; +import { TimelinesUIStart } from '@kbn/timelines-plugin/public'; + +/** + * Returns a default object to mock the timelines plugin for our unit tests and storybook stories. + * The button mocks a window.alert onClick event. + */ +export const mockKibanaTimelinesService: TimelinesUIStart = { + getHoverActions(): HoverActionsConfig { + return { + getAddToTimelineButton: () => ( + window.alert('Add to Timeline button clicked')} + /> + ), + } as unknown as HoverActionsConfig; + }, +} as unknown as TimelinesUIStart; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_triggers_actions_ui_service.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_triggers_actions_ui_service.tsx new file mode 100644 index 0000000000000..9cd15144c50e8 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_triggers_actions_ui_service.tsx @@ -0,0 +1,21 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public'; + +/** + * Returns a default object to mock the triggers actions ui plugin for our unit tests and storybook stories. + */ +export const mockTriggersActionsUiService: TriggersActionsStart = { + getFieldBrowser: () => ( + + Fields + + ), +} as unknown as TriggersActionsStart; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_ui_settings_service.ts b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_ui_settings_service.ts new file mode 100644 index 0000000000000..7341dc78c9808 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_ui_settings_service.ts @@ -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 { IUiSettingsClient } from '@kbn/core/public'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; + +/** + * Creates an object to pass to the uiSettings property when creating a KibanaReacrContext (see src/plugins/kibana_react/public/context/context.tsx). + * @param dateFormat defaults to '' + * @param timezone defaults to 'UTC + * @returns the object {@link IUiSettingsClient} + */ +export const mockUiSettingsService = (dateFormat: string = '', timezone: string = 'UTC') => + ({ + get: (key: string) => { + const settings = { + [DEFAULT_DATE_FORMAT]: dateFormat, + [DEFAULT_DATE_FORMAT_TZ]: timezone, + }; + // @ts-expect-error + return settings[key]; + }, + } as unknown as IUiSettingsClient); + +/** + * Mocks date format or timezone for testing. + * @param key dateFormat | dateFormat:tz + * @returns string + */ +export function mockUiSetting(key: string): string | undefined { + if (key === 'dateFormat') { + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + } + if (key === 'dateFormat:tz') { + return 'America/New_York'; + } +} diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx index 752e93b756ddb..6da13f89d0887 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx @@ -8,7 +8,8 @@ import React, { ReactNode, VFC } from 'react'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { CoreStart, IUiSettingsClient } from '@kbn/core/public'; +import { IUiSettingsClient } from '@kbn/core/public'; +import { TimelinesUIStart } from '@kbn/timelines-plugin/public'; import { SecuritySolutionContext } from '../../containers/security_solution_context'; import { getSecuritySolutionContextMock } from './mock_security_context'; @@ -21,6 +22,10 @@ export interface KibanaContextMock { * For the core ui-settings package (see {@link IUiSettingsClient}) */ uiSettings?: IUiSettingsClient; + /** + * For the timelines plugin + */ + timelines: TimelinesUIStart; } export interface StoryProvidersComponentProps { @@ -42,7 +47,7 @@ export const StoryProvidersComponent: VFC = ({ children, kibana, }) => { - const KibanaReactContext = createKibanaReactContext(kibana as CoreStart); + const KibanaReactContext = createKibanaReactContext(kibana); const securitySolutionContextMock = getSecuritySolutionContextMock(); return ( diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx index a2c0318c482aa..055e405cdb695 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx @@ -7,17 +7,18 @@ import moment from 'moment/moment'; import React, { FC } from 'react'; +import { BehaviorSubject } from 'rxjs'; import { I18nProvider } from '@kbn/i18n-react'; import { coreMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import type { IStorage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import { BehaviorSubject } from 'rxjs'; -import { getSecuritySolutionContextMock } from './mock_security_context'; -import { mockUiSetting } from './mock_kibana_ui_setting'; +import { createTGridMocks } from '@kbn/timelines-plugin/public/mock'; import { KibanaContext } from '../../hooks/use_kibana'; import { SecuritySolutionPluginContext } from '../../types'; +import { getSecuritySolutionContextMock } from './mock_security_context'; +import { mockUiSetting } from './mock_kibana_ui_settings_service'; import { SecuritySolutionContext } from '../../containers/security_solution_context'; export const localStorageMock = (): IStorage => { @@ -90,6 +91,8 @@ const dataServiceMock = { }, }; +const timelinesServiceMock = createTGridMocks(); + const core = coreMock.createStart(); const coreServiceMock = { ...core, @@ -106,6 +109,7 @@ export const mockedServices = { triggersActionsUi: { getFieldBrowser: jest.fn().mockReturnValue(null), }, + timelines: timelinesServiceMock, }; export const TestProvidersComponent: FC = ({ children }) => ( diff --git a/x-pack/plugins/threat_intelligence/public/components/date_formatter/date_formatter.stories.tsx b/x-pack/plugins/threat_intelligence/public/components/date_formatter/date_formatter.stories.tsx index dc0f6f6cdb1c1..73da72d24abb5 100644 --- a/x-pack/plugins/threat_intelligence/public/components/date_formatter/date_formatter.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/components/date_formatter/date_formatter.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { CoreStart } from '@kbn/core/public'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; +import { mockUiSettingsService } from '../../common/mocks/mock_kibana_ui_settings_service'; import { DateFormatter } from './date_formatter'; const mockValidStringDate = '1 Jan 2022 00:00:00 GMT'; @@ -21,19 +21,9 @@ export default { }; export function Default() { - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, - } as unknown as CoreStart; - const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService(), + } as unknown as CoreStart); return ( @@ -43,19 +33,9 @@ export function Default() { } export function UserTimeZone() { - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'America/New_York', - }; - // @ts-expect-error - return settings[key]; - }, - }, - } as unknown as CoreStart; - const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService('', 'America/New York'), + }); return ( @@ -65,19 +45,9 @@ export function UserTimeZone() { } export function UserDateFormat() { - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: 'MMM Do YY', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, - } as unknown as CoreStart; - const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService('MMM Do YY', 'UTC'), + }); return ( @@ -87,18 +57,9 @@ export function UserDateFormat() { } export function CustomDateFormat() { - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, - } as unknown as CoreStart; - const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService('', 'UTC'), + }); return ( @@ -108,19 +69,9 @@ export function CustomDateFormat() { } export function InvalidStringDate() { - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, - } as unknown as CoreStart; - const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService(), + }); return ( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx index 8bf5bf33a7736..467ff2ea16cf3 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants'; +import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; +import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; import { generateMockIndicator } from '../../../../../common/types/indicator'; import { IndicatorField } from './indicator_field'; @@ -18,13 +18,12 @@ export default { }; const mockIndicator = generateMockIndicator(); -const mockFieldTypesMap: { [id: string]: string } = { - 'threat.indicator.ip': 'ip', - 'threat.indicator.first_seen': 'date', -}; + +const mockFieldTypesMap = generateFieldTypeMap(); export function Default() { const mockField = 'threat.indicator.ip'; + return ( ); @@ -32,27 +31,16 @@ export function Default() { export function IncorrectField() { const mockField = 'abc'; + return ( ); } export function HandlesDates() { - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, - } as unknown as CoreStart; - const KibanaReactContext = createKibanaReactContext(coreMock); - + const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService() }); const mockField = 'threat.indicator.first_seen'; + return ( ', () => { beforeEach(() => {}); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx index 5889d8f8d59bd..6ea779c28be29 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx @@ -12,17 +12,23 @@ import { DateFormatter } from '../../../../components/date_formatter'; import { unwrapValue } from '../../lib/unwrap_value'; export interface IndicatorFieldProps { + /** + * Indicator to display the field value from (see {@link Indicator}). + */ indicator: Indicator; + /** + * The field to get the indicator's value for. + */ field: string; + /** + * An object to know what type the field is ('file', 'date', ...). + */ fieldTypesMap: { [id: string]: string }; } /** - * Takes an indicator object, a field and a field => type object to returns the correct value to display - * @param indicator see {@link Indicator} - * @param field the field to get the indicator's value for - * @param fieldTypesMap an object to know what type ('file', 'date', ...) the field is - * @returns If the type is a 'date', returns the {@link DateFormatter} component, else returns the value or {@link EMPTY_VALUE} + * Takes an indicator object, a field and a field => type object to returns the correct value to display. + * @returns If the type is a 'date', returns the {@link DateFormatter} component, else returns the value or {@link EMPTY_VALUE}. */ export const IndicatorField: VFC = ({ indicator, field, fieldTypesMap }) => { const value = unwrapValue(indicator, field as RawIndicatorFieldId); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx index 1b127ad56b11b..d3884b7994726 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx @@ -9,6 +9,9 @@ import moment from 'moment'; import React from 'react'; import { Story } from '@storybook/react'; import { TimeRangeBounds } from '@kbn/data-plugin/common'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { CoreStart } from '@kbn/core/public'; +import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; import { ChartSeries } from '../../hooks/use_aggregated_indicators'; import { IndicatorsBarChart } from './indicators_barchart'; @@ -45,12 +48,19 @@ const mockIndicators: ChartSeries[] = [ }, ]; const validDate: string = '1 Jan 2022 00:00:00 GMT'; + const numberOfDays = 1; + const mockDateRange: TimeRangeBounds = { min: moment(validDate), max: moment(validDate).add(numberOfDays, 'days'), }; -const mockHeight = '500px'; + +const mockField: string = 'threat.indicator.ip'; + +const KibanaReactContext = createKibanaReactContext({ + timelines: mockKibanaTimelinesService, +} as unknown as CoreStart); export default { component: IndicatorsBarChart, @@ -58,13 +68,28 @@ export default { }; export const Default: Story = () => ( - + + + ); export const NoData: Story = () => ( - + + + ); -export const CustomHeight: Story = () => ( - -); +export const CustomHeight: Story = () => { + const mockHeight = '500px'; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.test.tsx index 5872cc5c83fde..38de7df2b3d4e 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.test.tsx @@ -16,39 +16,44 @@ import { IndicatorsBarChart } from './indicators_barchart'; moment.suppressDeprecationWarnings = true; moment.tz.setDefault('UTC'); -const mockIndicators: ChartSeries[] = [ - { - x: '1 Jan 2022 00:00:00 GMT', - y: 0, - g: '[Filebeat] AbuseCH Malware', - }, - { - x: '1 Jan 2022 00:00:00 GMT', - y: 10, - g: '[Filebeat] AbuseCH MalwareBazaar', - }, - { - x: '1 Jan 2022 12:00:00 GMT', - y: 25, - g: '[Filebeat] AbuseCH Malware', - }, - { - x: '1 Jan 2022 18:00:00 GMT', - y: 15, - g: '[Filebeat] AbuseCH MalwareBazaar', - }, -]; -const validDate: string = '1 Jan 2022 00:00:00 GMT'; -const mockDateRange: TimeRangeBounds = { - min: moment(validDate), - max: moment(validDate).add(1, 'days'), -}; - describe('', () => { it('should render barchart', () => { + const mockIndicators: ChartSeries[] = [ + { + x: '1 Jan 2022 00:00:00 GMT', + y: 0, + g: '[Filebeat] AbuseCH Malware', + }, + { + x: '1 Jan 2022 00:00:00 GMT', + y: 10, + g: '[Filebeat] AbuseCH MalwareBazaar', + }, + { + x: '1 Jan 2022 12:00:00 GMT', + y: 25, + g: '[Filebeat] AbuseCH Malware', + }, + { + x: '1 Jan 2022 18:00:00 GMT', + y: 15, + g: '[Filebeat] AbuseCH MalwareBazaar', + }, + ]; + const validDate: string = '1 Jan 2022 00:00:00 GMT'; + const mockDateRange: TimeRangeBounds = { + min: moment(validDate), + max: moment(validDate).add(1, 'days'), + }; + const mockField: string = 'threat.indicator.ip'; + const component = render( - + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx index 466a262f753e7..f3c352c6296d0 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx @@ -9,28 +9,55 @@ import React, { VFC } from 'react'; import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '@elastic/charts'; import { EuiThemeProvider } from '@elastic/eui'; import { TimeRangeBounds } from '@kbn/data-plugin/common'; +import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; import { barChartTimeAxisLabelFormatter } from '../../../../common/utils/dates'; import { ChartSeries } from '../../hooks/use_aggregated_indicators'; +export const TIMELINE_BUTTON_TEST_ID = 'tiTimelineButton'; + const ID = 'tiIndicator'; const DEFAULT_CHART_HEIGHT = '200px'; const DEFAULT_CHART_WIDTH = '100%'; export interface IndicatorsBarChartProps { + /** + * Array of indicators already processed to be consumed by the BarSeries component from the @elastic/charts library. + */ indicators: ChartSeries[]; + /** + * Min and max dates to nicely format the label in the @elastic/charts Axis component. + */ dateRange: TimeRangeBounds; + /** + * Indicator field selected in the IndicatorFieldSelector component, passed to the {@link AddToTimeline} to populate the timeline. + */ + field: string; + /** + * Option height value to override the default {@link DEFAULT_CHART_HEIGHT} default barchart height. + */ height?: string; } +/** + * Displays a barchart of aggregated indicators using the @elastic/charts library. + */ export const IndicatorsBarChart: VFC = ({ indicators, dateRange, + field, height = DEFAULT_CHART_HEIGHT, }) => { return ( - + ( + + )} + /> = () => { + const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE; -const validDate: string = '1 Jan 2022 00:00:00 GMT'; -const numberOfDays: number = 1; -const aggregation1: Aggregation = { - events: { - buckets: [ - { - doc_count: 0, - key: 1641016800000, - key_as_string: '1 Jan 2022 06:00:00 GMT', - }, - { - doc_count: 10, - key: 1641038400000, - key_as_string: '1 Jan 2022 12:00:00 GMT', - }, - ], - }, - doc_count: 0, - key: '[Filebeat] AbuseCH Malware', -}; -const aggregation2: Aggregation = { - events: { - buckets: [ + const mockIndexPattern: DataView = { + fields: [ { - doc_count: 20, - key: 1641016800000, - key_as_string: '1 Jan 2022 06:00:00 GMT', - }, + name: '@timestamp', + type: 'date', + } as DataViewField, { - doc_count: 8, - key: 1641038400000, - key_as_string: '1 Jan 2022 12:00:00 GMT', - }, + name: 'threat.feed.name', + type: 'string', + } as DataViewField, ], - }, - doc_count: 0, - key: '[Filebeat] AbuseCH MalwareBazaar', -}; -const mockData = { - search: { - search: () => - of({ - rawResponse: { - aggregations: { - [AGGREGATION_NAME]: { - buckets: [aggregation1, aggregation2], + } as DataView; + + const validDate: string = '1 Jan 2022 00:00:00 GMT'; + const numberOfDays: number = 1; + const aggregation1: Aggregation = { + events: { + buckets: [ + { + doc_count: 0, + key: 1641016800000, + key_as_string: '1 Jan 2022 06:00:00 GMT', + }, + { + doc_count: 10, + key: 1641038400000, + key_as_string: '1 Jan 2022 12:00:00 GMT', + }, + ], + }, + doc_count: 0, + key: '[Filebeat] AbuseCH Malware', + }; + const aggregation2: Aggregation = { + events: { + buckets: [ + { + doc_count: 20, + key: 1641016800000, + key_as_string: '1 Jan 2022 06:00:00 GMT', + }, + { + doc_count: 8, + key: 1641038400000, + key_as_string: '1 Jan 2022 12:00:00 GMT', + }, + ], + }, + doc_count: 0, + key: '[Filebeat] AbuseCH MalwareBazaar', + }; + + const dataServiceMock = { + search: { + search: () => + of({ + rawResponse: { + aggregations: { + [AGGREGATION_NAME]: { + buckets: [aggregation1, aggregation2], + }, }, }, - }, - }), - }, - query: { - timefilter: { - timefilter: { - calculateBounds: () => ({ - min: moment(validDate), - max: moment(validDate).add(numberOfDays, 'days'), }), - }, }, - filterManager: { - getFilters: () => {}, - setFilters: () => {}, - getUpdates$: () => of(), + query: { + timefilter: { + timefilter: { + calculateBounds: () => ({ + min: moment(validDate), + max: moment(validDate).add(numberOfDays, 'days'), + }), + }, + }, + filterManager: { + getFilters: () => {}, + setFilters: () => {}, + getUpdates$: () => of(), + }, }, - }, -} as unknown as DataPublicPluginStart; + } as unknown as DataPublicPluginStart; -const mockUiSettings = { get: () => {} } as unknown as IUiSettingsClient; + const uiSettingsMock = { + get: () => {}, + } as unknown as IUiSettingsClient; + + const timelinesMock = mockKibanaTimelinesService; -export const Default: Story = () => { return ( - + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx index a30bdffe0e23c..fab1cfc5473d1 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx @@ -18,13 +18,25 @@ import { IndicatorsBarChart } from '../indicators_barchart/indicators_barchart'; const DEFAULT_FIELD = RawIndicatorFieldId.Feed; export interface IndicatorsBarChartWrapperProps { + /** + * From and to values received from the KQL bar and passed down to the hook to query data. + */ timeRange?: TimeRange; + /** + * List of fields coming from the Security Solution sourcerer data view, passed down to the {@link IndicatorFieldSelector} to populate the dropdown. + */ indexPattern: SecuritySolutionDataViewBase; } +/** + * Displays the {@link IndicatorsBarChart} and {@link IndicatorsFieldSelector} components, + * and handles retrieving aggregated indicator data. + */ export const IndicatorsBarChartWrapper = memo( ({ timeRange, indexPattern }) => { - const { dateRange, indicators, onFieldChange } = useAggregatedIndicators({ timeRange }); + const { dateRange, indicators, selectedField, onFieldChange } = useAggregatedIndicators({ + timeRange, + }); return ( <> @@ -47,7 +59,11 @@ export const IndicatorsBarChartWrapper = memo( /> - {timeRange ? : <>} + {timeRange ? ( + + ) : ( + <> + )} ); } diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_field_selector/indicators_field_selector.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_field_selector/indicators_field_selector.stories.tsx index 94ca9f903aec6..4c7b9d67ea92f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_field_selector/indicators_field_selector.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_field_selector/indicators_field_selector.stories.tsx @@ -33,8 +33,7 @@ export const Default: Story = () => { return ( console.log(value)} + valueChange={(value: string) => window.alert(`${value} selected`)} /> ); }; @@ -43,19 +42,12 @@ export const WithDefaultValue: Story = () => { return ( console.log(value)} + valueChange={(value: string) => window.alert(`${value} selected`)} defaultStackByValue={RawIndicatorFieldId.LastSeen} /> ); }; export const NoData: Story = () => { - return ( - console.log(value)} - /> - ); + return {}} />; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx index a66a0d18d2e60..6f65bab2157a9 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx @@ -7,9 +7,11 @@ import React from 'react'; import { Story } from '@storybook/react'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { CoreStart } from '@kbn/core/public'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; +import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; +import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { IndicatorsFlyout } from './indicators_flyout'; @@ -18,47 +20,34 @@ export default { title: 'IndicatorsFlyout', }; -const mockIndicator: Indicator = generateMockIndicator(); -const mockFieldTypesMap: { [id: string]: string } = { - 'threat.indicator.ip': 'ip', - 'threat.indicator.first_seen': 'date', -}; - const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, + uiSettings: mockUiSettingsService(), + timelines: mockKibanaTimelinesService, } as unknown as CoreStart; const KibanaReactContext = createKibanaReactContext(coreMock); export const Default: Story = () => { + const mockIndicator: Indicator = generateMockIndicator(); + const mockFieldTypesMap = generateFieldTypeMap(); + return ( console.log('closing')} + closeFlyout={() => window.alert('Closing flyout')} /> ); }; -export const EmtpyIndicator: Story = () => { +export const EmptyIndicator: Story = () => { return ( console.log('closing')} + closeFlyout={() => window.alert('Closing flyout')} /> ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx index 58c50d4a4424e..4e22bd0d11ccf 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx @@ -11,16 +11,14 @@ import { IndicatorsFlyout, SUBTITLE_TEST_ID, TITLE_TEST_ID } from './indicators_ import { generateMockIndicator, RawIndicatorFieldId } from '../../../../../common/types/indicator'; import { EMPTY_VALUE } from '../../../../../common/constants'; import { dateFormatter } from '../../../../common/utils/dates'; -import { mockUiSetting } from '../../../../common/mocks/mock_kibana_ui_setting'; +import { mockUiSetting } from '../../../../common/mocks/mock_kibana_ui_settings_service'; import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; +import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; import { unwrapValue } from '../../lib/unwrap_value'; import { displayValue } from '../../lib/display_value'; const mockIndicator = generateMockIndicator(); -const mockFieldTypesMap: { [id: string]: string } = { - '@timestamp': 'date', - 'threat.feed.name': 'string', -}; +const mockFieldTypesMap = generateFieldTypeMap(); describe('', () => { it('should render ioc id in title and first_seen in subtitle', () => { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx index 46028a22ebdac..84bb92d0b5724 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx @@ -36,11 +36,23 @@ const enum TAB_IDS { } export interface IndicatorsFlyoutProps { + /** + * Indicator passed down to the different tabs (table and json views). + */ indicator: Indicator; + /** + * Object mapping each field with their type to ease display in the {@link IndicatorsFlyoutTable} component. + */ fieldTypesMap: { [id: string]: string }; + /** + * Event to close flyout (used by {@link EuiFlyout}). + */ closeFlyout: () => void; } +/** + * Leverages the {@link EuiFlyout} from the @elastic/eui library to dhow the details of a specific {@link Indicator}. + */ export const IndicatorsFlyout: VFC = ({ indicator, fieldTypesMap, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx index a5b2ca8e8ea16..447dcfcdda6ee 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx @@ -15,9 +15,9 @@ export default { title: 'IndicatorsFlyoutJson', }; -const mockIndicator: Indicator = generateMockIndicator(); - export const Default: Story = () => { + const mockIndicator: Indicator = generateMockIndicator(); + return ; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx index 72c3aed376828..6bc42b1980c20 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx @@ -14,9 +14,16 @@ export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutJsonEmptyPrompt'; export const CODE_BLOCK_TEST_ID = 'tiFlyoutJsonCodeBlock'; export interface IndicatorsFlyoutJsonProps { + /** + * Indicator to display in json format. + */ indicator: Indicator; } +/** + * Displays all the properties and values of an {@link Indicator} in json view, + * using the {@link EuiCodeBlock} from the @elastic/eui library. + */ export const IndicatorsFlyoutJson: VFC = ({ indicator }) => { return Object.keys(indicator).length === 0 ? ( = () => { + const mockIndicator: Indicator = generateMockIndicator(); + const mockFieldTypesMap = generateFieldTypeMap(); -const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, -} as unknown as CoreStart; -const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService(), + timelines: mockKibanaTimelinesService, + } as unknown as CoreStart); -export const Default: Story = () => { return ( @@ -48,11 +38,6 @@ export const Default: Story = () => { export const EmptyIndicator: Story = () => { return ( - - - + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx index 3da21ff9ef8e1..e89a84e8b85ab 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx @@ -13,6 +13,7 @@ import { Indicator, RawIndicatorFieldId, } from '../../../../../common/types/indicator'; +import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; import { EMPTY_PROMPT_TEST_ID, IndicatorsFlyoutTable, @@ -22,10 +23,7 @@ import { unwrapValue } from '../../lib/unwrap_value'; import { displayValue } from '../../lib/display_value'; const mockIndicator: Indicator = generateMockIndicator(); -const mockFieldTypesMap: { [id: string]: string } = { - '@timestamp': 'date', - 'threat.feed.name': 'string', -}; +const mockFieldTypesMap = generateFieldTypeMap(); describe('', () => { it('should render fields and values in table', () => { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx index e6fa116d45524..5c56dfe91dc3b 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx @@ -9,10 +9,12 @@ import { EuiEmptyPrompt, EuiInMemoryTable } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { VFC } from 'react'; import { Indicator } from '../../../../../common/types/indicator'; +import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; import { IndicatorField } from '../indicator_field/indicator_field'; export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutTableEmptyPrompt'; export const TABLE_TEST_ID = 'tiFlyoutTableMemoryTable'; +export const TIMELINE_BUTTON_TEST_ID = 'tiFlyoutTableRowTimelineButton'; const search = { box: { @@ -22,16 +24,42 @@ const search = { }; export interface IndicatorsFlyoutTableProps { + /** + * Indicator to display in table view. + */ indicator: Indicator; + /** + * Object mapping each field with their type to ease display in the {@link IndicatorField} component. + */ fieldTypesMap: { [id: string]: string }; } +/** + * Displays all the properties and values of an {@link Indicator} in table view, + * using the {@link EuiInMemoryTable} from the @elastic/eui library. + */ export const IndicatorsFlyoutTable: VFC = ({ indicator, fieldTypesMap, }) => { const items: string[] = Object.keys(indicator.fields); const columns = [ + { + name: ( + + ), + actions: [ + { + render: (field: string) => ( + + ), + }, + ], + width: '72px', + }, { name: ( void; onToggleColumn: (columnId: string) => void; }) => diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx index 0e94e87372820..a327cec411f90 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants'; +import { mockTriggersActionsUiService } from '../../../../common/mocks/mock_kibana_triggers_actions_ui_service'; +import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; +import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { IndicatorsTable } from './indicators_table'; @@ -19,34 +20,19 @@ export default { title: 'IndicatorsTable', }; -const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator()); const mockIndexPattern: DataView = undefined as unknown as DataView; const stub = () => void 0; -const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, - triggersActionsUi: { - getFieldBrowser: () => ( - - Fields - - ), - }, -} as unknown as CoreStart; +export function WithIndicators() { + const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator()); -const KibanaReactContext = createKibanaReactContext(coreMock); + const KibanaReactContext = createKibanaReactContext({ + uiSettings: mockUiSettingsService(), + timelines: mockKibanaTimelinesService, + triggersActionsUi: mockTriggersActionsUiService, + } as unknown as CoreStart); -export function WithIndicators() { return ( = ({ indexPattern, browserFields, }) => { - const [columns, setColumns] = useState(defaultColumns); + const [columns, setColumns] = useState(defaultColumns); - const [visibleColumns, setVisibleColumns] = useState>( + const [visibleColumns, setVisibleColumns] = useState>( columns.map((column) => column.id) ); @@ -152,8 +162,8 @@ export const IndicatorsTable: VFC = ({ const handleToggleColumn = useCallback((columnId: string) => { setColumns((currentColumns) => { - const columnsMatchingId = ({ id }: Column) => id === columnId; - const columnsNotMatchingId = (column: Column) => !columnsMatchingId(column); + const columnsMatchingId = ({ id }: EuiDataGridColumn) => id === columnId; + const columnsNotMatchingId = (column: EuiDataGridColumn) => !columnsMatchingId(column); const enabled = Boolean(currentColumns.find(columnsMatchingId)); @@ -174,6 +184,22 @@ export const IndicatorsTable: VFC = ({ setVisibleColumns(columns.map(({ id }) => id)); }, [columns]); + useMemo(() => { + columns.map( + (col: EuiDataGridColumn) => + (col.cellActions = [ + ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => ( + + ), + ]) + ); + }, [columns, indicators, pagination]); + const toolbarOptions = useToolbarOptions({ browserFields, start, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.stories.tsx index 45fbf494835f5..c5289adf51c21 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.stories.tsx @@ -7,9 +7,8 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; -import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants'; +import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { OpenIndicatorFlyoutButton } from './open_indicator_flyout_button'; @@ -23,20 +22,7 @@ export default { const mockIndicator: Indicator = generateMockIndicator(); -const coreMock = { - uiSettings: { - get: (key: string) => { - const settings = { - [DEFAULT_DATE_FORMAT]: '', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - }; - // @ts-expect-error - return settings[key]; - }, - }, -} as unknown as CoreStart; - -const KibanaReactContext = createKibanaReactContext(coreMock); +const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService() }); const Template: ComponentStory = (args) => { return ( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.tsx index dcd46b2d67df9..869fc9c4c5b49 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button.tsx @@ -13,11 +13,23 @@ import { Indicator } from '../../../../../common/types/indicator'; export const BUTTON_TEST_ID = 'tiToggleIndicatorFlyoutButton'; export interface OpenIndicatorFlyoutButtonProps { + /** + * {@link Indicator} passed to the flyout component. + */ indicator: Indicator; + /** + * Method called by the onClick event to open/close the flyout. + */ onOpen: (indicator: Indicator) => void; + /** + * Boolean used to change the color and type of icon depending on the state of the flyout. + */ isOpen: boolean; } +/** + * Button added to the actions column of the indicators table to open/close the IndicatorFlyout component. + */ export const OpenIndicatorFlyoutButton: VFC = ({ indicator, onOpen, @@ -31,18 +43,16 @@ export const OpenIndicatorFlyoutButton: VFC = ({ ); return ( - <> - - onOpen(indicator)} - /> - - + + onOpen(indicator)} + /> + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts index 9322e84d78c4f..63da10697f11a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts @@ -32,6 +32,7 @@ export interface UseAggregatedIndicatorsValue { indicators: ChartSeries[]; onFieldChange: (field: string) => void; dateRange: TimeRangeBounds; + selectedField: string; } export interface Aggregation { @@ -209,5 +210,6 @@ export const useAggregatedIndicators = ({ dateRange, indicators, onFieldChange, + selectedField: field, }; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx index 455abdfcf5b51..c4dfad36764d9 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx @@ -40,6 +40,7 @@ describe('', () => { ).mockReturnValue({ dateRange: { min: moment(), max: moment() }, indicators: [], + selectedField: '', onFieldChange: () => {}, }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.test.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.test.ts index 4f4211c235c41..1b1e6f3085d23 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.test.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.test.ts @@ -10,8 +10,9 @@ import { generateMockIndicator, generateMockUrlIndicator, Indicator, + RawIndicatorFieldId, } from '../../../../common/types/indicator'; -import { displayValue } from './display_value'; +import { displayField, displayValue } from './display_value'; type ExpectedIndicatorValue = string | null; @@ -40,3 +41,18 @@ describe('displayValue()', () => { } ); }); + +describe('displayValueField()', () => { + it('should return correct RawIndicatorFieldId for valid field', () => { + const mockIndicator = generateMockIndicator(); + const result = displayField(mockIndicator); + expect(result).toEqual(RawIndicatorFieldId.Ip); + }); + + it('should return null for invalid field', () => { + const mockIndicator = generateMockIndicator(); + mockIndicator.fields['threat.indicator.type'] = ['abc']; + const result = displayField(mockIndicator); + expect(result).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.ts index c0be52ccc2bc7..f661fd4603111 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/lib/display_value.ts @@ -14,7 +14,7 @@ export type IndicatorTypePredicate = (indicatorType: string | null) => boolean; type MapperRule = [predicate: IndicatorTypePredicate, extract: IndicatorValueExtractor]; /** - * Predicates to help identify identicator by type + * Predicates to help identify indicator by type */ const isIpIndicator: IndicatorTypePredicate = (indicatorType) => !!indicatorType && ['ipv4-addr', 'ipv6-addr'].includes(indicatorType); @@ -56,6 +56,25 @@ const findMappingRule = (indicatorType: string | null): IndicatorValueExtractor */ const rules: Record = {}; +/** + * Mapping between the indicator type and the {@link RawIndicatorFieldId}. + */ +const indicatorTypeToField: { [id: string]: RawIndicatorFieldId } = { + file: RawIndicatorFieldId.FileSha256, + 'ipv4-addr': RawIndicatorFieldId.Ip, + 'ipv6-addr': RawIndicatorFieldId.Ip, + url: RawIndicatorFieldId.UrlFull, +}; + +/** + * Find and return indicator display value field + */ +export const displayField = (indicator: Indicator): string | null => { + const indicatorType = (unwrapValue(indicator, RawIndicatorFieldId.Type) || '').toLowerCase(); + + return indicatorTypeToField[indicatorType]; +}; + /** * Find and return indicator display value, if possible */ diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/__snapshots__/add_to_timeline.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/__snapshots__/add_to_timeline.test.tsx.snap new file mode 100644 index 0000000000000..c56f657d1d905 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/__snapshots__/add_to_timeline.test.tsx.snap @@ -0,0 +1,407 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render empty component when calculated value is - 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+ , + "container":
, + "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[` should render empty component when data is - 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+ , + "container":
, + "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[` should render empty component when field doesn't exist in data 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+ , + "container":
, + "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[` should render empty component when field exist in data but isn't supported 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+ , + "container":
, + "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[` should render timeline button when Indicator data 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+ + Add To Timeline + +
+
+ , + "container":
+
+ + Add To Timeline + +
+
, + "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[` should render timeline button when string data 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+ + Add To Timeline + +
+
+ , + "container":
+
+ + Add To Timeline + +
+
, + "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], +} +`; diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.stories.tsx new file mode 100644 index 0000000000000..ee3f2d67ed91c --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.stories.tsx @@ -0,0 +1,52 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; +import { EMPTY_VALUE } from '../../../../../common/constants'; +import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; +import { AddToTimeline } from './add_to_timeline'; + +export default { + component: AddToTimeline, + title: 'AddToTimeline', +}; + +const mockField: string = 'threat.indicator.ip'; + +const KibanaReactContext = createKibanaReactContext({ + timelines: mockKibanaTimelinesService, +} as unknown as CoreStart); + +export const Default: Story = () => { + const mockData: Indicator = generateMockIndicator(); + + return ( + + + + ); +}; + +export const WithIndicator: Story = () => { + const mockData: string = 'ip'; + + return ( + + + + ); +}; + +export const EmptyValue: Story = () => ( + + + +); diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.test.tsx new file mode 100644 index 0000000000000..b8ca854613b08 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.test.tsx @@ -0,0 +1,89 @@ +/* + * 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, Indicator } from '../../../../../common/types/indicator'; +import { EMPTY_VALUE } from '../../../../../common/constants'; +import { AddToTimeline } from './add_to_timeline'; +import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; + +describe('', () => { + it('should render timeline button when Indicator data', () => { + const mockField: string = 'threat.indicator.ip'; + const mockData: Indicator = generateMockIndicator(); + + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render timeline button when string data', () => { + const mockField: string = 'threat.indicator.ip'; + const mockString: string = 'ip'; + + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it(`should render empty component when field doesn't exist in data`, () => { + const mockField: string = 'abc'; + const mockData: Indicator = generateMockIndicator(); + + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it(`should render empty component when field exist in data but isn't supported`, () => { + const mockField: string = 'abc'; + const mockData: Indicator = generateMockIndicator(); + mockData.fields['threat.indicator.type'] = ['abc']; + + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it(`should render empty component when calculated value is ${EMPTY_VALUE}`, () => { + const mockField: string = 'threat.indicator.first_seen'; + const mockData: Indicator = generateMockIndicator(); + mockData.fields['threat.indicator.first_seen'] = ['']; + + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it(`should render empty component when data is ${EMPTY_VALUE}`, () => { + const mockField: string = 'threat.indicator.ip'; + const mockData = EMPTY_VALUE; + + const component = render( + + + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx new file mode 100644 index 0000000000000..21f4c35819cc1 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx @@ -0,0 +1,97 @@ +/* + * 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 { DataProvider, QueryOperator } from '@kbn/timelines-plugin/common'; +import { AddToTimelineButtonProps } from '@kbn/timelines-plugin/public'; +import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui/src/components/button'; +import { EMPTY_VALUE } from '../../../../../common/constants'; +import { displayField, displayValue } from '../../../indicators/lib/display_value'; +import { ComputedIndicatorFieldId } from '../../../indicators/components/indicators_table/cell_renderer'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { unwrapValue } from '../../../indicators/lib/unwrap_value'; +import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator'; +import { useStyles } from './styles'; + +export interface AddToTimelineProps { + /** + * Value passed to the timeline. Used in combination with field if is type of {@link Indicator}. + */ + data: Indicator | string; + /** + * Value passed to the timeline. + */ + field: string; + /** + * Only used with `EuiDataGrid` (see {@link AddToTimelineButtonProps}). + */ + component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; + /** + * Used as `data-test-subj` value for e2e tests. + */ + testId?: string; +} + +/** + * Add to timeline button, used in many places throughout the TI plugin. + * Support being passed a {@link Indicator} or a string, can be used in a `EuiDataGrid` or as a normal button. + * Leverages the built-in functionality retrieves from the timeLineService (see ThreatIntelligenceSecuritySolutionContext in x-pack/plugins/threat_intelligence/public/types.ts) + * Clicking on the button will add a key-value pair to an Untitled timeline. + * + * @returns add to timeline button or an empty component. + */ +export const AddToTimeline: VFC = ({ data, field, component, testId }) => { + const styles = useStyles(); + + const addToTimelineButton = + useKibana().services.timelines.getHoverActions().getAddToTimelineButton; + + let value: string | null; + if (typeof data === 'string') { + value = data; + } else if (field === ComputedIndicatorFieldId.DisplayValue) { + field = displayField(data) || ''; + value = displayValue(data); + } else { + value = unwrapValue(data, field as RawIndicatorFieldId); + } + + if (!value || value === EMPTY_VALUE || !field) { + return <>; + } + + const operator = ':' as QueryOperator; + + const dataProvider: DataProvider[] = [ + { + and: [], + enabled: true, + id: `timeline-indicator-${field}-${value}`, + name: value, + excluded: false, + kqlQuery: '', + queryMatch: { + field, + value, + operator, + }, + }, + ]; + + const addToTimelineProps: AddToTimelineButtonProps = { + dataProvider, + field, + ownFocus: false, + }; + if (component) addToTimelineProps.Component = component; + + return ( +
+ {addToTimelineButton(addToTimelineProps)} +
+ ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/index.tsx new file mode 100644 index 0000000000000..29ae1a24a0f68 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/index.tsx @@ -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 './add_to_timeline'; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_ui_setting.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts similarity index 54% rename from x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_ui_setting.tsx rename to x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts index 6366a68c38240..4f7814eb793cc 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_kibana_ui_setting.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts @@ -5,11 +5,14 @@ * 2.0. */ -export function mockUiSetting(key: string): string | undefined { - if (key === 'dateFormat') { - return 'MMM D, YYYY @ HH:mm:ss.SSS'; - } - if (key === 'dateFormat:tz') { - return 'America/New_York'; - } -} +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const button: CSSObject = { + display: 'inline-flex', + }; + + return { + button, + }; +}; diff --git a/x-pack/plugins/threat_intelligence/public/types.ts b/x-pack/plugins/threat_intelligence/public/types.ts index a197eea7ae4e5..f1aad2017f5ec 100644 --- a/x-pack/plugins/threat_intelligence/public/types.ts +++ b/x-pack/plugins/threat_intelligence/public/types.ts @@ -14,6 +14,7 @@ import { FieldSpec, } from '@kbn/data-views-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; +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'; @@ -40,6 +41,7 @@ export type Services = { storage: Storage; dataViews: DataViewsPublicPluginStart; triggersActionsUi: TriggersActionsStart; + timelines: TimelinesUIStart; } & CoreStart; export interface LicenseAware { @@ -66,5 +68,8 @@ export interface SecuritySolutionPluginContext { * Get the user's license to drive the Threat Intelligence plugin's visibility. */ licenseService: LicenseAware; + /** + * Gets Security Solution shared information like browerFields, indexPattern and selectedPatterns in DataView + */ sourcererDataView: SourcererDataView; } diff --git a/x-pack/plugins/threat_intelligence/tsconfig.json b/x-pack/plugins/threat_intelligence/tsconfig.json index ad720811a9947..8d19be714a8de 100644 --- a/x-pack/plugins/threat_intelligence/tsconfig.json +++ b/x-pack/plugins/threat_intelligence/tsconfig.json @@ -14,6 +14,7 @@ "../../../typings/**/*" ], "references": [ + { "path": "../timelines/tsconfig.json" }, { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/unified_search/tsconfig.json" }, diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index f6dd148c8dce3..b31f01c0663c4 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -93,3 +93,5 @@ export function plugin() { export { StatefulEventContext } from './components/stateful_event_context'; export { TimelineContext } from './components/t_grid/shared'; + +export type { AddToTimelineButtonProps } from './components/hover_actions/actions/add_to_timeline';