From e97b451cc186e47a4621b85cf2bd814a8529c0b6 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 26 Feb 2020 16:42:15 +0000 Subject: [PATCH] [SIEM] add custom reputation link (#57814) * add custom reputation link * fix unit test * add number of limitation to reputation links * fix dependency * apply defaultFieldRendererOverflow to reputation link * fix unit test * fix url template * fix display links * fix types * fix for review * update test case * update snapshot * add icons and tooltips * fix style * update test * add external link component * update test * fix types * fix style * update snapshot * remove useMemo * update description * code review * review II * code review * update description * fix unit test * fix unit test * fix unit test * fix unit test * fix types * fix styles * fix style * fix style * fix review Co-authored-by: Elastic Machine --- .../legacy/plugins/siem/common/constants.ts | 9 + x-pack/legacy/plugins/siem/index.ts | 15 + .../field_renderers.test.tsx.snap | 20 +- .../field_renderers/field_renderers.tsx | 111 +- .../public/components/links/index.test.tsx | 365 +- .../siem/public/components/links/index.tsx | 203 +- .../__snapshots__/zeek_details.test.tsx.snap | 3261 +++++++++++++++++ .../body/renderers/zeek/zeek_details.test.tsx | 5 +- .../renderers/zeek/zeek_signature.test.tsx | 11 +- .../body/renderers/zeek/zeek_signature.tsx | 10 +- .../step_about_rule/helpers.test.ts | 18 + 11 files changed, 3876 insertions(+), 152 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 0d9e092e21d82..2a30293c244af 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -38,6 +38,15 @@ export const NEWS_FEED_URL_SETTING = 'siem:newsFeedUrl'; /** The default value for News feed widget */ export const NEWS_FEED_URL_SETTING_DEFAULT = 'https://feeds.elastic.co/security-solution'; +/** This Kibana Advanced Setting specifies the URLs of `IP Reputation Links`*/ +export const IP_REPUTATION_LINKS_SETTING = 'siem:ipReputationLinks'; + +/** The default value for `IP Reputation Links` */ +export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ + { "name": "virustotal.com", "url_template": "https://www.virustotal.com/gui/search/{{ip}}" }, + { "name": "talosIntelligence.com", "url_template": "https://talosintelligence.com/reputation_center/lookup?search={{ip}}" } +]`; + /** * Id for the signals alerting type */ diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 731ef10aa225e..db398821aecfd 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -28,6 +28,8 @@ import { NEWS_FEED_URL_SETTING, NEWS_FEED_URL_SETTING_DEFAULT, SIGNALS_INDEX_KEY, + IP_REPUTATION_LINKS_SETTING, + IP_REPUTATION_LINKS_SETTING_DEFAULT, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; @@ -144,6 +146,19 @@ export const siem = (kibana: any) => { category: ['siem'], requiresPageReload: true, }, + [IP_REPUTATION_LINKS_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { + defaultMessage: 'IP Reputation Links', + }), + value: IP_REPUTATION_LINKS_SETTING_DEFAULT, + type: 'json', + description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { + defaultMessage: + 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', + }), + category: ['siem'], + requiresPageReload: true, + }, }, mappings: savedObjectMappings, }, diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap index 2ff93b2ecada4..59010ab80af63 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap @@ -122,25 +122,17 @@ exports[`Field Renderers #reputationRenderer it renders correctly against snapsh - - virustotal.com - - , - - talosIntelligence.com - + /> `; exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1`] = ` - iana.org - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx index 80d68dfe1b731..222eef515958c 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx @@ -15,7 +15,7 @@ import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { DefaultDraggable } from '../draggables'; import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; -import { HostDetailsLink, ReputationLink, VirusTotalLink, WhoIsLink } from '../links'; +import { HostDetailsLink, ReputationLink, WhoIsLink, ReputationLinkSetting } from '../links'; import { Spacer } from '../page'; import * as i18n from '../page/network/ip_overview/translations'; @@ -132,11 +132,7 @@ export const hostNameRenderer = (host: HostEcsFields, ipFilter?: string): React. export const whoisRenderer = (ip: string) => {i18n.VIEW_WHOIS}; export const reputationRenderer = (ip: string): React.ReactElement => ( - <> - {i18n.VIEW_VIRUS_TOTAL} - {', '} - {i18n.VIEW_TALOS_INTELLIGENCE} - + ); interface DefaultFieldRendererProps { @@ -148,73 +144,78 @@ interface DefaultFieldRendererProps { moreMaxHeight?: string; } +type OverflowRenderer = (item: string | ReputationLinkSetting) => JSX.Element; + // TODO: This causes breaks between elements until the ticket below is fixed // https://github.com/elastic/ingest-dev/issues/474 -export const DefaultFieldRenderer = React.memo( - ({ - attrName, - displayCount = 1, - idPrefix, - moreMaxHeight = DEFAULT_MORE_MAX_HEIGHT, - render, - rowItems, - }) => { - if (rowItems != null && rowItems.length > 0) { - const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => { - const id = escapeDataProviderId( - `default-field-renderer-default-draggable-${idPrefix}-${attrName}-${rowItem}` - ); - return ( - - {index !== 0 && ( - <> - {','} - - - )} +export const DefaultFieldRendererComponent: React.FC = ({ + attrName, + displayCount = 1, + idPrefix, + moreMaxHeight = DEFAULT_MORE_MAX_HEIGHT, + render, + rowItems, +}) => { + if (rowItems != null && rowItems.length > 0) { + const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => { + const id = escapeDataProviderId( + `default-field-renderer-default-draggable-${idPrefix}-${attrName}-${rowItem}` + ); + return ( + + {index !== 0 && ( + <> + {','} + + + )} + {typeof rowItem === 'string' && ( {render ? render(rowItem) : rowItem} - - ); - }); - - return draggables.length > 0 ? ( - - {draggables}{' '} - { - - } - - ) : ( - getEmptyTagValue() + )} + ); - } else { - return getEmptyTagValue(); - } + }); + + return draggables.length > 0 ? ( + + {draggables} + + + + + ) : ( + getEmptyTagValue() + ); + } else { + return getEmptyTagValue(); } -); +}; + +export const DefaultFieldRenderer = React.memo(DefaultFieldRendererComponent); DefaultFieldRenderer.displayName = 'DefaultFieldRenderer'; +type RowItemTypes = string | ReputationLinkSetting; interface DefaultFieldRendererOverflowProps { - rowItems: string[]; + rowItems: string[] | ReputationLinkSetting[]; idPrefix: string; - render?: (item: string) => React.ReactNode; + render?: (item: RowItemTypes) => React.ReactNode; overflowIndexStart?: number; moreMaxHeight: string; } interface MoreContainerProps { idPrefix: string; - render?: (item: string) => React.ReactNode; - rowItems: string[]; + render?: (item: RowItemTypes) => React.ReactNode; + rowItems: RowItemTypes[]; moreMaxHeight: string; overflowIndexStart: number; } diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx index ceef7e353b521..d2d1d6569854d 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx @@ -4,24 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; +import { mount, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { encodeIpv6 } from '../../lib/helpers'; +import { useUiSetting$ } from '../../lib/kibana'; import { GoogleLink, HostDetailsLink, IPDetailsLink, ReputationLink, - VirusTotalLink, WhoIsLink, CertificateFingerprintLink, Ja3FingerprintLink, PortOrServiceNameLink, + DEFAULT_NUMBER_OF_LINK, + ExternalLink, } from '.'; +jest.mock('../../lib/kibana', () => { + return { + useUiSetting$: jest.fn(), + }; +}); + describe('Custom Links', () => { const hostName = 'Host Name'; const ipv4 = '192.0.2.255'; @@ -101,53 +109,332 @@ describe('Custom Links', () => { }); }); - describe('ReputationLink', () => { - test('it renders link text', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.text()).toEqual('Example Link'); - }); + describe('External Link', () => { + const mockLink = 'https://www.virustotal.com/gui/search/'; + const mockLinkName = 'Link'; + let wrapper: ShallowWrapper; - test('it renders correct href', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - 'https://www.talosintelligence.com/reputation_center/lookup?search=192.0.2.0' - ); + describe('render', () => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test('it renders tooltip', () => { + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeTruthy(); + }); + + test('it renders ExternalLinkIcon', () => { + expect(wrapper.find('[data-test-subj="externalLinkIcon"]').exists()).toBeTruthy(); + }); + + test('it renders correct url', () => { + expect(wrapper.find('[data-test-subj="externalLink"]').prop('href')).toEqual(mockLink); + }); + + test('it renders comma if id is given', () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeTruthy(); + }); }); - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}>{'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://www.talosintelligence.com/reputation_center/lookup?search=%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); + describe('not render', () => { + test('it should not render if childen prop is not given', () => { + wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); + }); + + test('it should not render if url prop is not given', () => { + wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); + }); + + test('it should not render if url prop is invalid', () => { + wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); + }); + + test('it should not render comma if id is not given', () => { + wrapper = shallow( + + {mockLinkName} + + ); + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeFalsy(); + }); + + test('it should not render comma for the last item', () => { + wrapper = shallow( + + {mockLinkName} + + ); + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeFalsy(); + }); }); + + describe.each<[number, number, number, boolean]>([ + [0, 2, 5, true], + [1, 2, 5, false], + [2, 2, 5, false], + [3, 2, 5, false], + [4, 2, 5, false], + [5, 2, 5, false], + ])( + 'renders Comma when overflowIndex is smaller than allItems limit', + (idx, overflowIndexStart, allItemsLimit, showComma) => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test(`should render Comma if current id (${idx}) is smaller than the index of last visible item`, () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); + }); + } + ); + + describe.each<[number, number, number, boolean]>([ + [0, 5, 4, true], + [1, 5, 4, true], + [2, 5, 4, true], + [3, 5, 4, false], + [4, 5, 4, false], + [5, 5, 4, false], + ])( + 'When overflowIndex is grater than allItems limit', + (idx, overflowIndexStart, allItemsLimit, showComma) => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test(`Current item (${idx}) should render Comma execpt the last item`, () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); + }); + } + ); + + describe.each<[number, number, number, boolean]>([ + [0, 5, 5, true], + [1, 5, 5, true], + [2, 5, 5, true], + [3, 5, 5, true], + [4, 5, 5, false], + [5, 5, 5, false], + ])( + 'when overflowIndex equals to allItems limit', + (idx, overflowIndexStart, allItemsLimit, showComma) => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test(`Current item (${idx}) should render Comma correctly`, () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); + }); + } + ); }); - describe('VirusTotalLink', () => { - test('it renders sha passed in as value', () => { - const wrapper = mountWithIntl({'Example Link'}); - expect(wrapper.text()).toEqual('Example Link'); + describe('ReputationLink', () => { + const mockCustomizedReputationLinks = [ + { name: 'Link 1', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, + { + name: 'Link 2', + url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', + }, + { name: 'Link 3', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, + { + name: 'Link 4', + url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', + }, + { name: 'Link 5', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, + { + name: 'Link 6', + url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', + }, + ]; + const mockDefaultReputationLinks = mockCustomizedReputationLinks.slice(0, 2); + + describe('links property', () => { + beforeEach(() => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockDefaultReputationLinks]); + }); + + test('it renders default link text', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { + expect(node.at(idx).text()).toEqual(mockDefaultReputationLinks[idx].name); + }); + }); + + test('it renders customized link text', () => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + const wrapper = shallow(); + wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { + expect(node.at(idx).text()).toEqual(mockCustomizedReputationLinks[idx].name); + }); + }); + + test('it renders correct href', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { + expect(node.prop('href')).toEqual( + mockDefaultReputationLinks[idx].url_template.replace('{{ip}}', '192.0.2.0') + ); + }); + }); }); - test('it renders sha passed in as link', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual('https://www.virustotal.com/#/search/abc'); + describe('number of links', () => { + beforeAll(() => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + }); + + afterEach(() => { + (useUiSetting$ as jest.Mock).mockClear(); + }); + + test('it renders correct number of links by default', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="externalLinkComponent"]')).toHaveLength( + DEFAULT_NUMBER_OF_LINK + ); + }); + + test('it renders correct number of tooltips by default', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]')).toHaveLength( + DEFAULT_NUMBER_OF_LINK + ); + }); + + test('it renders correct number of visible link', () => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLinkComponent"]')).toHaveLength(1); + }); + + test('it renders correct number of tooltips for visible links', () => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]')).toHaveLength(1); + }); }); - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}>{'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://www.virustotal.com/#/search/%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); + describe('invalid customized links', () => { + const mockInvalidLinksEmptyObj = [{}]; + const mockInvalidLinksNoName = [ + { url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}' }, + ]; + const mockInvalidLinksNoUrl = [{ name: 'Link 1' }]; + const mockInvalidUrl = [{ name: 'Link 1', url_template: "" }]; + afterEach(() => { + (useUiSetting$ as jest.Mock).mockReset(); + }); + + test('it filters empty object', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksEmptyObj]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + + test('it filters object without name property', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoName]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + + test('it filters object without url_template property', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoUrl]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + + test('it filters object with invalid url', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidUrl]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + }); + + describe('external icon', () => { + beforeAll(() => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + }); + + afterEach(() => { + (useUiSetting$ as jest.Mock).mockClear(); + }); + + test('it renders correct number of external icons by default', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(5); + }); + + test('it renders correct number of external icons', () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(1); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index b6548e3e950ba..04de0b1d5d3bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink } from '@elastic/eui'; -import React from 'react'; +import { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { isNil } from 'lodash/fp'; +import styled from 'styled-components'; +import { + DefaultFieldRendererOverflow, + DEFAULT_MORE_MAX_HEIGHT, +} from '../field_renderers/field_renderers'; import { encodeIpv6 } from '../../lib/helpers'; import { getCaseDetailsUrl, @@ -15,6 +21,13 @@ import { getCreateCaseUrl, } from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { useUiSetting$ } from '../../lib/kibana'; +import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; +import * as i18n from '../page/network/ip_overview/translations'; +import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; +import { ExternalLinkIcon } from '../external_link_icon'; + +export const DEFAULT_NUMBER_OF_LINK = 5; // Internal Links const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ @@ -26,6 +39,39 @@ const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: ); +const whitelistUrlSchemes = ['http://', 'https://']; +export const ExternalLink = React.memo<{ + url: string; + children?: React.ReactNode; + idx?: number; + overflowIndexStart?: number; + allItemsLimit?: number; +}>( + ({ + url, + children, + idx, + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + }) => { + const lastVisibleItemIndex = overflowIndexStart - 1; + const lastItemIndex = allItemsLimit - 1; + const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); + const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); + return url && inWhitelist && !isUrlInvalid(url) && children ? ( + + + {children} + + {!isNil(idx) && idx < lastIndexToShow && } + + + ) : null; + } +); + +ExternalLink.displayName = 'ExternalLink'; + export const HostDetailsLink = React.memo(HostDetailsLinkComponent); const IPDetailsLinkComponent: React.FC<{ @@ -63,9 +109,9 @@ CreateCaseLink.displayName = 'CreateCaseLink'; // External Links export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( - + {children ? children : link} - + ) ); @@ -120,39 +166,136 @@ export const CertificateFingerprintLink = React.memo<{ CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; -export const ReputationLink = React.memo<{ children?: React.ReactNode; domain: string }>( - ({ children, domain }) => ( - - {children ? children : domain} - - ) -); +enum DefaultReputationLink { + 'virustotal.com' = 'virustotal.com', + 'talosIntelligence.com' = 'talosIntelligence.com', +} -ReputationLink.displayName = 'ReputationLink'; +export interface ReputationLinkSetting { + name: string; + url_template: string; +} -export const VirusTotalLink = React.memo<{ children?: React.ReactNode; link: string }>( - ({ children, link }) => ( - - {children ? children : link} - - ) -); +function isDefaultReputationLink(name: string): name is DefaultReputationLink { + return ( + name === DefaultReputationLink['virustotal.com'] || + name === DefaultReputationLink['talosIntelligence.com'] + ); +} +const isReputationLink = ( + rowItem: string | ReputationLinkSetting +): rowItem is ReputationLinkSetting => + (rowItem as ReputationLinkSetting).url_template !== undefined && + (rowItem as ReputationLinkSetting).name !== undefined; + +export const Comma = styled('span')` + margin-right: 5px; + margin-left: 5px; + &::after { + content: ' ,'; + } +`; + +Comma.displayName = 'Comma'; + +const defaultNameMapping: Record = { + [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, + [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, +}; + +const ReputationLinkComponent: React.FC<{ + overflowIndexStart?: number; + allItemsLimit?: number; + showDomain?: boolean; + domain: string; + direction?: 'row' | 'column'; +}> = ({ + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + showDomain = false, + domain, + direction = 'row', +}) => { + const [ipReputationLinksSetting] = useUiSetting$( + IP_REPUTATION_LINKS_SETTING + ); + + const ipReputationLinks: ReputationLinkSetting[] = useMemo( + () => + ipReputationLinksSetting + ?.slice(0, allItemsLimit) + .filter( + ({ url_template, name }) => + !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) + ) + .map(({ name, url_template }: { name: string; url_template: string }) => ({ + name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, + url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), + })), + [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] + ); + + return ipReputationLinks?.length > 0 ? ( +
+ + + {ipReputationLinks + ?.slice(0, overflowIndexStart) + .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( + + <>{showDomain ? domain : name ?? domain} + + ))} + + + + { + return ( + isReputationLink(rowItem) && ( + + <>{rowItem.name ?? domain} + + ) + ); + }} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={overflowIndexStart} + /> + + +
+ ) : null; +}; + +ReputationLinkComponent.displayName = 'ReputationLinkComponent'; -VirusTotalLink.displayName = 'VirusTotalLink'; +export const ReputationLink = React.memo(ReputationLinkComponent); export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( - + {children ? children : domain} - +
) ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index 0a60c8facff9c..6b866aeecc831 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -498,3 +498,3264 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` timelineId="test" /> `; + +exports[`ZeekDetails rendering it returns zeek.files if the data does contain zeek.files data 1`] = ` +.c3, +.c3::before, +.c3::after { + -webkit-transition: background 150ms ease, color 150ms ease; + transition: background 150ms ease, color 150ms ease; +} + +.c3 { + border-radius: 2px; + padding: 0 4px 0 8px; + position: relative; + z-index: 0 !important; +} + +.c3::before { + background-image: linear-gradient( 135deg, #535966 25%, transparent 25% ), linear-gradient( -135deg, #535966 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #535966 75% ), linear-gradient( -135deg, transparent 75%, #535966 75% ); + background-position: 0 0,1px 0,1px -1px,0px 1px; + background-size: 2px 2px; + bottom: 2px; + content: ''; + display: block; + left: 2px; + position: absolute; + top: 2px; + width: 4px; +} + +.c3:hover, +.c3:hover .euiBadge, +.c3:hover .euiBadge__text { + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} + +.event-column-view:hover .c3, +tr:hover .c3 { + background-color: #343741; +} + +.event-column-view:hover .c3::before, +tr:hover .c3::before { + background-image: linear-gradient( 135deg, #98a2b3 25%, transparent 25% ), linear-gradient( -135deg, #98a2b3 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #98a2b3 75% ), linear-gradient( -135deg, transparent 75%, #98a2b3 75% ); +} + +.c3:hover, +.c3:focus, +.event-column-view:hover .c3:hover, +.event-column-view:focus .c3:focus, +tr:hover .c3:hover, +tr:hover .c3:focus { + background-color: #1ba9f5; +} + +.c3:hover, +.c3:focus, +.event-column-view:hover .c3:hover, +.event-column-view:focus .c3:focus, +tr:hover .c3:hover, +tr:hover .c3:focus, +.c3:hover a, +.c3:focus a, +.event-column-view:hover .c3:hover a, +.event-column-view:focus .c3:focus a, +tr:hover .c3:hover a, +tr:hover .c3:focus a, +.c3:hover a:hover, +.c3:focus a:hover, +.event-column-view:hover .c3:hover a:hover, +.event-column-view:focus .c3:focus a:hover, +tr:hover .c3:hover a:hover, +tr:hover .c3:focus a:hover { + color: #1d1e24; +} + +.c3:hover::before, +.c3:focus::before, +.event-column-view:hover .c3:hover::before, +.event-column-view:focus .c3:focus::before, +tr:hover .c3:hover::before, +tr:hover .c3:focus::before { + background-image: linear-gradient( 135deg, #1d1e24 25%, transparent 25% ), linear-gradient( -135deg, #1d1e24 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #1d1e24 75% ), linear-gradient( -135deg, transparent 75%, #1d1e24 75% ); +} + +.c2 { + display: inline-block; + max-width: 100%; +} + +.c2 [data-rbd-placeholder-context-id] { + display: none !important; +} + +.c4 > span.euiToolTipAnchor { + display: block; +} + +.c8 { + margin: 0 2px; +} + +.c7 { + margin-top: 3px; +} + +.c6 { + margin-right: 10px; +} + +.c1 { + margin-left: 3px; +} + +.c5 { + margin-left: 6px; +} + +.c0 { + margin: 5px 0; +} + + + + + + + + + + + + + + + +
+
+ + +
+ + + +
+ + + +
+ + +
+ + + + + + +
+ + + + + + + + + + + Cu0n232QMyvNtzb75j + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ + +
+ + + + + + +
+ + + + + + + + + + + files + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ + +
+ + + + + + +
+ + + + + + + + + + + sha1: fa5195a... + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ + +
+ + + + + + +
+ + + + + + + + + + + md5: f7653f1... + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + +
+
+ +
+ + + + + + + + +
+
+ +
+
+
+
+
+
+
+
+ +
+ + + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + + + +
+ + +
+ + +
+ + + +
+ + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ +
+ +
+
+ + + +
+ + + + +
+ +
+
+
+
+ +
+ + +
+
+ +
+
+
+
+ +
+
+ +
+ + +
+ + +
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 7617a01acf1d9..db51ade6df4c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -113,9 +113,8 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.text()).toEqual( - 'Cu0n232QMyvNtzb75jfilessha1: fa5195a...md5: f7653f1...fa5195a5dfacc9d1c68d43600f0e0262cad14dde' - ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.text()).toEqual('Cu0n232QMyvNtzb75jfilessha1: fa5195a...md5: f7653f1...'); }); test('it returns null for text if the data contains no zeek data', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index c09bd6b7a356d..f199b537f1be0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -79,14 +79,9 @@ describe('ZeekSignature', () => { ).toBeFalsy(); }); - test('should render value', () => { - const wrapper = mount(); - expect(wrapper.text()).toEqual('abc'); - }); - - test('should render link with sha', () => { - const wrapper = mount(); - expect(wrapper.find('a').prop('href')).toEqual('https://www.virustotal.com/#/search/abcdefg'); + test('should render', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="reputationLinkSha"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 72f58df5677e4..57e5ff19eb815 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -13,7 +13,7 @@ import { Ecs } from '../../../../../graphql/types'; import { DragEffects, DraggableWrapper } from '../../../../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../drag_and_drop/helpers'; import { ExternalLinkIcon } from '../../../../external_link_icon'; -import { GoogleLink, VirusTotalLink } from '../../../../links'; +import { GoogleLink, ReputationLink } from '../../../../links'; import { Provider } from '../../../../timeline/data_providers/provider'; import { IS_OPERATOR } from '../../../data_providers/data_provider'; @@ -148,8 +148,12 @@ export const TotalVirusLinkSha = React.memo(({ value }) value != null ? (
- {value} - +
) : null diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts new file mode 100644 index 0000000000000..5eb503f8ba074 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isUrlInvalid } from './helpers'; + +describe('helpers', () => { + describe('isUrlInvalid', () => { + test('verifies invalid url', () => { + expect(isUrlInvalid('this is not a url')).toBeTruthy(); + }); + + test('verifies valid url', () => { + expect(isUrlInvalid('https://www.elastic.co/')).toBeFalsy(); + }); + }); +});