From d52026bebec3f7671951816177b9819621677ca5 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Tue, 17 Dec 2019 11:17:27 -0700 Subject: [PATCH] ## [SIEM] Overview Page "1.5" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A redesigned SIEM Overview page that includes `Recent timelines`, a `Security news` feed, visualizations, and rolled-up event counts ![overview-day](https://user-images.githubusercontent.com/4459398/72360664-806b9e00-36ad-11ea-9c6e-8efcf6973734.png) ![overview-night](https://user-images.githubusercontent.com/4459398/72360667-83ff2500-36ad-11ea-84fb-047c1d4c54e3.png) ### Overview enhancements - Added the global Search bar and Date picker to the Overview page - New `Recent timelines` widget affords quick access to favorite and recently modified timelines - New `Security news` widget - New Kibana advanced settings (toggle switch) for enabling or disabling the news widget and configuring the news URL ![news-settings](https://user-images.githubusercontent.com/4459398/72362776-fd4c4700-36b0-11ea-805b-3c7353f2c1cd.png) - New `Events count by dataset` widget - Updated the `Host Events` and `Network Events` widgets to integrate with the Search bar and date picker input - Enhanced the `Host Events` and `Network Events` widgets to use an accordion paradigm that summarizes stats by source (e.g. `Auditbeat`, `Endgame`) - Enhanced the `Host Events` and `Network Events` widgets to visualize relative percentages of events collected as progress bars - New `Alerts count by category` widget - New `Signals count by MITRE ATT&CK™ category` widget - New `View events`, `View alerts`, and `View signals` navigation buttons for their respective visualizations ### FTUE enhancements - FTUE "no data" view design refresh ![ftue](https://user-images.githubusercontent.com/4459398/72361771-43a0a680-36af-11ea-969f-5872ac4a01a1.png) - When the FTUE "no data" page is displayed, hide all global navigation links (i.e. `Hosts`, `Network`, `Detection engine`), such that only `Overview` appears in the global nav - App Help popover design refresh ![help](https://user-images.githubusercontent.com/4459398/72362132-d80b0900-36af-11ea-9b58-1fd3b923b7c8.png) - Removed the `Beta` badge and `Security Information & Event Management with the Elastic Stack` from the Overview header - Tested in Chrome `79.0.3945.117`, Firefox `72.0.1`, and Safari `13.0.4` ## Known issues - The `siem:newsFeedUrl` advanced setting is defaulted to `https://feeds.elastic.co/kibana` - The `Signals count by MITRE ATT&CK™ category` visualization does not display all categories - The `Signals count by MITRE ATT&CK™ category` visualization may require a different index pattern - The Hosts page `Alerts` tab is not always selected after navigating to it via the Overview’s `View alerts` button - `EuiButtonGroup` throwing a `Can't perform a React state update on an unmounted component` warning when switching from the Overview tab https://github.com/elastic/siem-team/issues/484 --- docs/management/advanced-options.asciidoc | 2 + .../public/doc_links/doc_links_service.ts | 10 +- .../legacy/plugins/siem/common/constants.ts | 9 + .../integration/lib/overview/selectors.ts | 4 + .../smoke_tests/overview/overview.spec.ts | 16 +- x-pack/legacy/plugins/siem/index.ts | 28 +- .../__snapshots__/index.test.tsx.snap | 1 + .../public/components/empty_page/index.tsx | 1 + .../components/formatted_date/index.tsx | 53 +- .../__snapshots__/index.test.tsx.snap | 47 +- .../public/components/header_global/index.tsx | 26 +- .../public/components/help_menu/index.tsx | 9 + .../siem/public/components/link_to/index.ts | 2 +- .../components/matrix_histogram/index.tsx | 7 +- .../components/matrix_histogram/types.ts | 4 +- .../components/matrix_histogram/utils.ts | 4 +- .../public/components/news_feed/helpers.ts | 89 ++ .../public/components/news_feed/index.tsx | 57 ++ .../public/components/news_feed/news_feed.tsx | 39 + .../components/news_feed/news_link/index.tsx | 24 + .../components/news_feed/no_news/index.tsx | 24 + .../components/news_feed/post/index.tsx | 62 ++ .../components/news_feed/translations.ts | 19 + .../siem/public/components/news_feed/types.ts | 42 + .../public/components/page/manage_query.tsx | 3 + .../page/overview/overview_host/index.tsx | 119 ++- .../__snapshots__/index.test.tsx.snap | 852 +++++++++++++----- .../overview_host_stats/index.test.tsx | 59 +- .../overview/overview_host_stats/index.tsx | 296 +++--- .../page/overview/overview_network/index.tsx | 120 ++- .../__snapshots__/index.test.tsx.snap | 534 ++++++++--- .../overview_network_stats/index.test.tsx | 59 +- .../overview/overview_network_stats/index.tsx | 227 ++--- .../components/page/overview/stat_value.tsx | 54 ++ .../public/components/page/overview/types.ts | 43 + .../recent_timelines/counts/index.tsx | 59 ++ .../recent_timelines/filters/index.tsx | 39 + .../recent_timelines/header/index.tsx | 66 ++ .../components/recent_timelines/helpers.ts | 26 + .../components/recent_timelines/index.tsx | 141 +++ .../recent_timelines/recent_timelines.tsx | 55 ++ .../recent_timelines/translations.ts | 55 ++ .../components/recent_timelines/types.ts | 7 + .../components/sidebar_header/index.tsx | 27 + .../components/url_state/index.test.tsx | 2 +- .../siem/public/components/url_state/types.ts | 8 +- .../siem/public/pages/common/translations.ts | 27 + .../detection_engine_empty_page.tsx | 5 +- .../detection_engine_no_signal_index.tsx | 2 +- .../detection_engine_user_unauthenticated.tsx | 2 +- .../public/pages/hosts/hosts_empty_page.tsx | 5 +- .../siem/public/pages/hosts/translations.ts | 13 - .../pages/network/network_empty_page.tsx | 5 +- .../siem/public/pages/network/translations.ts | 13 - .../overview/alerts_by_category/index.tsx | 102 +++ .../pages/overview/event_counts/index.tsx | 80 ++ .../overview/events_by_dataset/index.tsx | 103 +++ .../siem/public/pages/overview/index.tsx | 4 +- .../public/pages/overview/overview.test.tsx | 25 + .../siem/public/pages/overview/overview.tsx | 182 ++-- .../pages/overview/overview_empty/index.tsx | 35 + .../public/pages/overview/sidebar/index.tsx | 18 + .../public/pages/overview/sidebar/sidebar.tsx | 58 ++ .../overview/signals_by_category/index.tsx | 63 ++ .../siem/public/pages/overview/summary.tsx | 2 +- .../public/pages/overview/translations.ts | 20 +- .../translations/translations/ja-JP.json | 13 +- .../translations/translations/zh-CN.json | 13 +- 68 files changed, 3310 insertions(+), 910 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/news_feed/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/overview/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/common/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 757c6f10f2a999c..695a4d4f45b021f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -217,6 +217,8 @@ might increase the search time. This setting is off by default. Users must opt-i [horizontal] `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:enableNewsFeed`:: Enables the News feed +`siem:newsFeedUrl`:: News feed content will be retrieved from this URL `siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds. `siem:timeDefaults`:: The default period of time in the SIEM time filter. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 36b220f16f39507..1046f7a17dc518c 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -106,7 +106,10 @@ export class DocLinksService { introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, }, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, - siem: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`, + siem: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`, + gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/install-siem.html`, + }, query: { luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`, queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`, @@ -199,7 +202,10 @@ export interface DocLinksStart { readonly introduction: string; }; readonly kibana: string; - readonly siem: string; + readonly siem: { + readonly guide: string; + readonly gettingStarted: string; + }; readonly query: { readonly luceneQuerySyntax: string; readonly queryDsl: string; diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 5116416b527a5e7..7dc0298a01bc512 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -34,6 +34,15 @@ export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; +/** This Kibana Advanced Setting enables the `Security news` feed widget */ +export const ENABLE_NEWS_FEED_SETTING = 'siem:enableNewsFeed'; + +/** This Kibana Advanced Setting specifies the URL of the News feed widget */ +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/kibana'; // TODO: replace this with the real feed URL + /** * Id for the signals alerting type */ diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts index cf48ba716830d6e..a8af321ff9832db 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts @@ -133,3 +133,7 @@ export const NETWORK_STATS = [ STAT_FLOW, STAT_TLS, ]; + +export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; + +export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts index 4ef3eb67cafc9c6..be66fdc86be36d8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts @@ -6,7 +6,13 @@ import { OVERVIEW_PAGE } from '../../lib/urls'; import { clearFetch, stubApi } from '../../lib/fixtures/helpers'; -import { HOST_STATS, NETWORK_STATS, STAT_AUDITD } from '../../lib/overview/selectors'; +import { + HOST_STATS, + NETWORK_STATS, + OVERVIEW_HOST_STATS, + OVERVIEW_NETWORK_STATS, + STAT_AUDITD, +} from '../../lib/overview/selectors'; import { loginAndWaitForPage } from '../../lib/util/helpers'; describe('Overview Page', () => { @@ -17,6 +23,14 @@ describe('Overview Page', () => { }); it('Host and Network stats render with correct values', () => { + cy.get(OVERVIEW_HOST_STATS) + .find('button') + .invoke('click'); + + cy.get(OVERVIEW_NETWORK_STATS) + .find('button') + .invoke('click'); + cy.get(STAT_AUDITD.domId); HOST_STATS.forEach(stat => { diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index fe8b16471a7ce5e..05721f9b24b646a 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -25,8 +25,11 @@ import { DEFAULT_FROM, DEFAULT_TO, DEFAULT_SIGNALS_INDEX, - SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY, + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, + NEWS_FEED_URL_SETTING_DEFAULT, + SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; import { initServerWithKibana } from './server/kibana.index'; @@ -133,6 +136,29 @@ export const siem = (kibana: any) => { category: ['siem'], requiresPageReload: true, }, + [ENABLE_NEWS_FEED_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { + defaultMessage: 'News feed', + }), + value: true, + description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { + defaultMessage: '

Enables the News feed

', + }), + type: 'boolean', + category: ['siem'], + requiresPageReload: true, + }, + [NEWS_FEED_URL_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { + defaultMessage: 'News feed URL', + }), + value: NEWS_FEED_URL_SETTING_DEFAULT, + description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { + defaultMessage: '

News feed content will be retrieved from this URL

', + }), + category: ['siem'], + requiresPageReload: true, + }, }, mappings: savedObjectMappings, }, diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap index 87409c5fdebe02f..f9ee342967b8a26 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap @@ -18,6 +18,7 @@ exports[`renders correctly 1`] = ` } + iconType="securityAnalyticsApp" title={

My Super Title diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx index f2b0ec1ab5e6089..a067c1d28f87fa9 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx @@ -43,6 +43,7 @@ export const EmptyPage = React.memo( ...rest }) => ( {title}

} body={message &&

{message}

} actions={ diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index 4e5903c02abf7ad..fb579a14b045753 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -10,17 +10,60 @@ import { FormattedRelative } from '@kbn/i18n/react'; import { useDateFormat, useTimeZone } from '../../hooks'; import { getOrEmptyTagFromValue } from '../empty_value'; +import { useUiSetting$ } from '../../lib/kibana'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; import { getMaybeDate } from './maybe_date'; -export const PreferenceFormattedDate = React.memo<{ value: Date }>(({ value }) => { - const dateFormat = useDateFormat(); - const timeZone = useTimeZone(); +export const PreferenceFormattedDate = React.memo<{ dateFormat?: string; value: Date }>( + ({ value, dateFormat = useDateFormat() }) => ( + <>{moment.tz(value, useTimeZone()).format(dateFormat)} + ) +); + +PreferenceFormattedDate.displayName = 'PreferenceFormattedDate'; - return <>{moment.tz(value, timeZone).format(dateFormat)}; +/** + * This function may be passed to `Array.find()` to locate the `P1DT` + * configuration (sub) setting, a string array that contains two entries + * like the following example: `['P1DT', 'YYYY-MM-DD']`. + */ +export const isP1DTFormatterSetting = (formatNameFormatterPair?: string[]) => + Array.isArray(formatNameFormatterPair) && + formatNameFormatterPair[0] === 'P1DT' && + formatNameFormatterPair.length === 2; + +/** + * Renders a date in `P1DT` format, e.g. `YYYY-MM-DD`, as specified by + * the `P1DT1` entry in the `dateFormat:scaled` Kibana Advanced setting. + * + * If the `P1DT` format is not specified in the `dateFormat:scaled` setting, + * the fallback format `YYYY-MM-DD` will be applied + */ +export const PreferenceFormattedP1DTDate = React.memo<{ value: Date }>(({ value }) => { + /** + * A fallback "format name / formatter" 2-tuple for the `P1DT` formatter, which is + * one of many such pairs expected to be contained in the `dateFormat:scaled` + * Kibana advanced setting. + */ + const FALLBACK_DATE_FORMAT_SCALED_P1DT = ['P1DT', 'YYYY-MM-DD']; + + // Read the 'dateFormat:scaled' Kibana Advanced setting, which contains 2-tuple sub-settings: + const [scaledDateFormatPreference] = useUiSetting$('dateFormat:scaled'); + + // attempt to find the nested `['P1DT', 'formatString']` setting + const maybeP1DTFormatter = Array.isArray(scaledDateFormatPreference) + ? scaledDateFormatPreference.find(isP1DTFormatterSetting) + : null; + + const p1dtFormat = + Array.isArray(maybeP1DTFormatter) && maybeP1DTFormatter.length === 2 + ? maybeP1DTFormatter[1] + : FALLBACK_DATE_FORMAT_SCALED_P1DT[1]; + + return ; }); -PreferenceFormattedDate.displayName = 'PreferenceFormattedDate'; +PreferenceFormattedP1DTDate.displayName = 'PreferenceFormattedP1DTDate'; /** * Renders the specified date value in a format determined by the user's preferences, diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap index 849f3616524cc57..aaefb4a83ded4fd 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap @@ -30,48 +30,11 @@ exports[`HeaderGlobal it renders 1`] = ` - + + + diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx index 53365a4daa34a41..db6ff7cf55f920f 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx @@ -16,6 +16,7 @@ import { getOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; const Wrapper = styled.header` ${({ theme }) => css` @@ -47,14 +48,25 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - key !== SiemPageName.detectionEngine, navTabs) - : navTabs + + {({ indicesExist }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + key !== SiemPageName.detectionEngine, navTabs) + : navTabs + } + /> + ) : ( + key === SiemPageName.overview, navTabs)} + /> + ) } - /> + diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx index d42ee08e864079d..732d83ac6e7361d 100644 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx @@ -7,6 +7,7 @@ import React, { useEffect } from 'react'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; +import { documentationLinks } from 'ui/documentation_links'; export const HelpMenu = React.memo(() => { useEffect(() => { @@ -15,6 +16,14 @@ export const HelpMenu = React.memo(() => { defaultMessage: 'SIEM', }), links: [ + { + content: i18n.translate('xpack.siem.chrome.helpMenu.documentation', { + defaultMessage: 'SIEM documentation', + }), + href: documentationLinks.siem.guide, + iconType: 'documents', + linkType: 'custom', + }, { linkType: 'discuss', href: 'https://discuss.elastic.co/c/siem', diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index 10198345755c37c..ad6147e5aad769c 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -10,6 +10,6 @@ export { RedirectToDetectionEnginePage, } from './redirect_to_detection_engine'; export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; -export { getHostsUrl, getHostDetailsUrl } from './redirect_to_hosts'; +export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 94c05d00d5462dd..d11645ab9019cbf 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -25,7 +25,9 @@ export const MatrixHistogramComponent: React.FC { const barchartConfigs = getBarchartConfigs({ from: startDate, + legendPosition, to: endDate, onBrushEnd: updateDateRange, scaleType, @@ -59,7 +62,9 @@ export const MatrixHistogramComponent: React.FC - + + {headerChildren} + {loadingInitial ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts index edcd8e3cb9d5cfe..d12eb71b85b9253 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ScaleType } from '@elastic/charts'; +import { Position, ScaleType } from '@elastic/charts'; import { MatrixOverTimeHistogramData, MatrixOverOrdinalHistogramData } from '../../graphql/types'; import { AuthMatrixDataFields } from '../page/hosts/authentications_over_time/utils'; import { UpdateDateRange } from '../charts/common'; @@ -24,6 +24,8 @@ export interface MatrixHistogramBasicProps { export interface MatrixHistogramProps extends MatrixHistogramBasicProps { dataKey?: string; + headerChildren?: React.ReactNode; + legendPosition?: Position; scaleType?: ScaleType; subtitle?: string; title?: string; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts index a7ef71f7a6a0d3a..3ab82f988a40d52 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -12,6 +12,7 @@ import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types'; export const getBarchartConfigs = ({ from, + legendPosition, to, scaleType, onBrushEnd, @@ -19,6 +20,7 @@ export const getBarchartConfigs = ({ showLegend, }: { from: number; + legendPosition?: Position; to: number; scaleType: ScaleType; onBrushEnd: UpdateDateRange; @@ -39,7 +41,7 @@ export const getBarchartConfigs = ({ tickSize: 8, }, settings: { - legendPosition: Position.Bottom, + legendPosition: legendPosition ?? Position.Bottom, onBrushEnd, showLegend: showLegend || true, theme: { diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts new file mode 100644 index 000000000000000..497127cdfba3d21 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; +import moment from 'moment'; +import uuid from 'uuid'; + +import { NewsItem, RawNewsApiItem, RawNewsApiResponse } from './types'; +import { throwIfNotOk } from '../../hooks/api/api'; + +/** + * Combines the URL specified with the `newsFeedUrlSetting` with the Kibana + * version returned from `getKibanaVersion` to form a complete path to the + * news (specific to the current version of Kibana) + */ +export const getNewsFeedUrl = ({ + newsFeedUrlSetting, + getKibanaVersion, +}: { + newsFeedUrlSetting: string; + getKibanaVersion: () => string; +}) => [newsFeedUrlSetting, `v${getKibanaVersion()}.json`].join('/'); + +export const NEWS_FEED_FALLBACK_LANGUAGE = 'en'; + +/** + * Returns the current locale of the browser as specified in the `document`, + * or the value of `fallback` if the locale could not be retrieved + */ +export const getLocale = (fallback: string): string => + document.documentElement.lang?.toLowerCase() ?? fallback; // use the `lang` attribute of the `html` tag + +const NO_NEWS_ITEMS: NewsItem[] = []; + +/** + * Transforms a `RawNewsApiResponse` from the news feed API to a collection of + * `NewsItem`s + */ +export const getNewsItemsFromApiResponse = (response?: RawNewsApiResponse): NewsItem[] => { + const locale = getLocale(NEWS_FEED_FALLBACK_LANGUAGE); + + if (response == null || response.items == null) { + return NO_NEWS_ITEMS; + } + + return response.items + .filter((x: RawNewsApiItem | null) => x != null) + .map(x => ({ + description: + get(locale, x.description) ?? get(NEWS_FEED_FALLBACK_LANGUAGE, x.description) ?? '', + expireOn: new Date(x.expire_on ?? ''), + hash: x.hash ?? uuid.v4(), + imageUrl: x.image_url ?? null, + linkUrl: get(locale, x.link_url) ?? get(NEWS_FEED_FALLBACK_LANGUAGE, x.link_url) ?? '', + publishOn: new Date(x.publish_on ?? ''), + title: get(locale, x.title) ?? get(NEWS_FEED_FALLBACK_LANGUAGE, x.title) ?? '', + })); +}; + +/** + * Fetches `RawNewsApiResponse` from the specified `newsFeedUrl`, via a + * cross-origin (CORS) request. This function throws an error if the request + * fails + */ +export const fetchNews = async ({ + newsFeedUrl, +}: { + newsFeedUrl: string; +}): Promise => { + const response = await fetch(newsFeedUrl, { + credentials: 'omit', + method: 'GET', + mode: 'cors', + }); + + await throwIfNotOk(response); + + return response.json(); +}; + +/** + * Returns false if `now` is before the `NewsItem` `publishOn` date, or + * after the `expireOn` date + */ +export const showNewsItem = ({ publishOn, expireOn }: NewsItem): boolean => + !moment().isBefore(publishOn) && !moment().isAfter(expireOn); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx new file mode 100644 index 000000000000000..95f12758d5e6359 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx @@ -0,0 +1,57 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import chrome from 'ui/chrome'; + +import { fetchNews, getNewsFeedUrl, getNewsItemsFromApiResponse } from './helpers'; +import { useUiSetting$ } from '../../lib/kibana'; +import { NewsFeed } from './news_feed'; +import { NewsItem } from './types'; + +export const StatefulNewsFeed = React.memo<{ + enableNewsFeedSetting: string; + newsFeedSetting: string; +}>(({ enableNewsFeedSetting, newsFeedSetting }) => { + const [enableNewsFeed] = useUiSetting$(enableNewsFeedSetting); + const [newsFeedUrlSetting] = useUiSetting$(newsFeedSetting); + const [news, setNews] = useState(null); + + const newsFeedUrl = getNewsFeedUrl({ + newsFeedUrlSetting, + getKibanaVersion: chrome.getKibanaVersion, + }); + + useEffect(() => { + let canceled = false; + + const fetchData = async () => { + try { + const apiResponse = await fetchNews({ newsFeedUrl }); + + if (!canceled) { + setNews(getNewsItemsFromApiResponse(apiResponse)); + } + } catch { + if (!canceled) { + setNews([]); + } + } + }; + + if (enableNewsFeed) { + fetchData(); + } + + return () => { + canceled = true; + }; + }, [enableNewsFeed, newsFeedUrl]); + + return <>{enableNewsFeed ? : null}; +}); + +StatefulNewsFeed.displayName = 'StatefulNewsFeed'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx new file mode 100644 index 000000000000000..dcc3f79716775f5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import React from 'react'; + +import { NoNews } from './no_news'; +import { Post } from './post'; +import { NewsItem } from './types'; + +interface Props { + news: NewsItem[] | null | undefined; +} + +export const NewsFeed = React.memo(({ news }) => { + if (news == null) { + return ; + } + + if (news.length === 0) { + return ; + } + + return ( + <> + {news.map((n: NewsItem) => ( + + + + + ))} + + ); +}); + +NewsFeed.displayName = 'NewsFeed'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx new file mode 100644 index 000000000000000..b50ed578fb0bc34 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx @@ -0,0 +1,24 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import React from 'react'; + +/** prevents links to the new pages from accessing `window.opener` */ +const REL_NOOPENER = 'noopener'; + +/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */ +const REL_NOFOLLOW = 'nofollow'; + +/** prevents the browser from sending the current address as referrer via the Referer HTTP header */ +const REL_NOREFERRER = 'noreferrer'; + +/** A hyperlink to a (presumed to be external) news site */ +export const NewsLink = ({ href, children }: { href: string; children: React.ReactNode }) => ( + + {children} + +); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx new file mode 100644 index 000000000000000..b645fd7da7be3f2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx @@ -0,0 +1,24 @@ +/* + * 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 { EuiLink, EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../translations'; + +export const NoNews = React.memo(() => ( + <> + + {`${i18n.NO_NEWS_MESSAGE} `} + + {i18n.ADVANCED_SETTINGS_LINK_TITLE} + + {'.'} + + +)); + +NoNews.displayName = 'NoNews'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx new file mode 100644 index 000000000000000..cb2542a497f088a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { PreferenceFormattedP1DTDate } from '../../formatted_date'; +import { showNewsItem } from '../helpers'; +import { NewsLink } from '../news_link'; +import { NewsItem } from '../types'; + +const NewsItemPreviewImage = styled.img` + height: 56px; + margin-left: 16px; + min-width: 56px; + padding: 4px; + width: 56px; +`; + +export const Post = React.memo<{ newsItem: NewsItem }>(({ newsItem }) => { + const { linkUrl, title, publishOn, description, imageUrl } = newsItem; + + if (!showNewsItem(newsItem)) { + return null; + } + + return ( + + + + {title} + + + + + +
{description}
+
+
+ + + {imageUrl && ( + + + + )} + +
+ ); +}); + +Post.displayName = 'Post'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts new file mode 100644 index 000000000000000..71981723cc93792 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts @@ -0,0 +1,19 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const NO_NEWS_MESSAGE = i18n.translate('xpack.siem.newsFeed.noNewsMessage', { + defaultMessage: + 'Your current News feed URL returned no recent news. You may update the URL or disable security news via', +}); + +export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate( + 'xpack.siem.newsFeed.advancedSettingsLinkTitle', + { + defaultMessage: 'SIEM advanced settings', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/types.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/types.ts new file mode 100644 index 000000000000000..2ee5a4c3c02aabf --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/types.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +/** + * For rendering, `RawNewsApiItem`s are transformed to this + * representation of a news item + */ +export interface NewsItem { + description: string; + expireOn: Date; + hash: string; + imageUrl: string | null; + linkUrl: string; + publishOn: Date; + title: string; +} + +/** + * The raw (wire format) representation of a News API item + */ +export interface RawNewsApiItem { + badge?: { [lang: string]: string | null } | null; + description?: { [lang: string]: string | null } | null; + expire_on?: Date | null; + hash?: string | null; + image_url?: string | null; + languages?: string[] | null; + link_text?: { [lang: string]: string | null } | null; + link_url?: { [lang: string]: string | null } | null; + publish_on?: Date | null; + title?: { [lang: string]: string } | null; +} + +/** + * Defines the shape of a raw response from the News API + */ +export interface RawNewsApiResponse { + items?: RawNewsApiItem[]; +} diff --git a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx b/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx index fd38850bad5da5f..3e786ff91f9fd62 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Position } from '@elastic/charts'; import { omit } from 'lodash/fp'; import React from 'react'; @@ -11,7 +12,9 @@ import { inputsModel } from '../../store'; interface OwnProps { deleteQuery?: ({ id }: { id: string }) => void; + headerChildren?: React.ReactNode; id: string; + legendPosition?: Position; loading: boolean; refetch: inputsModel.Refetch; setQuery: (params: { diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx index a70d9d00802718c..e069e64c66a9263 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -5,23 +5,28 @@ */ import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { HeaderSection } from '../../../header_section'; -import { manageQuery } from '../../../page/manage_query'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { ESQuery } from '../../../../../common/typed_json'; import { ID as OverviewHostQueryId, OverviewHostQuery, } from '../../../../containers/overview/overview_host'; -import { inputsModel } from '../../../../store/inputs'; -import { OverviewHostStats } from '../overview_host_stats'; +import { HeaderSection } from '../../../header_section'; +import { useUiSetting$ } from '../../../../lib/kibana'; import { getHostsUrl } from '../../../link_to'; +import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; +import { manageQuery } from '../../../page/manage_query'; +import { inputsModel } from '../../../../store/inputs'; import { InspectButtonContainer } from '../../../inspect'; export interface OwnProps { startDate: number; endDate: number; + filterQuery?: ESQuery | string; setQuery: ({ id, inspect, @@ -37,44 +42,76 @@ export interface OwnProps { const OverviewHostStatsManage = manageQuery(OverviewHostStats); type OverviewHostProps = OwnProps; +export const OverviewHost = React.memo( + ({ endDate, filterQuery, startDate, setQuery }) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + + return ( + + + + + {({ overviewHost, loading, id, inspect, refetch }) => { + const hostEventsCount = getOverviewHostStats(overviewHost).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedHostEventsCount = numeral(hostEventsCount).format( + defaultNumberFormat + ); -const OverviewHostComponent: React.FC = ({ endDate, startDate, setQuery }) => ( - - - - - } - title={ - - } - > - - - - + return ( + <> + + } + title={ + + } + > + + + + - - {({ overviewHost, loading, id, inspect, refetch }) => ( - - )} - - - - + + + ); + }} + + + + + ); + } ); -export const OverviewHost = React.memo(OverviewHostComponent); +OverviewHost.displayName = 'OverviewHost'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap index 142a9a03b78443c..21a4568e2413351 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap @@ -1,233 +1,637 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Overview Host Stat Data rendering it renders the default OverviewHostStats 1`] = ` - - - - - + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="host-stat-accordion-groupauditbeat" + initialIsOpen={false} + paddingSize="none" > - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="host-stat-accordion-groupendgame" + initialIsOpen={false} + paddingSize="none" > - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="host-stat-accordion-groupfilebeat" + initialIsOpen={false} + paddingSize="none" > - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="host-stat-accordion-groupwinlogbeat" + initialIsOpen={false} + paddingSize="none" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx index f99b2687d70728c..4240ea441284cfc 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import React from 'react'; import { OverviewHostStats } from '.'; import { mockData } from './mock'; +import { TestProviders } from '../../../../mock/test_providers'; describe('Overview Host Stat Data', () => { describe('rendering', () => { @@ -18,23 +19,51 @@ describe('Overview Host Stat Data', () => { }); }); describe('loading', () => { - test('it does not show loading indicator when not loading', () => { - const wrapper = shallow(); - const loadingWrapper = wrapper - .dive() - .find('[data-test-subj="host-stat-auditbeatAuditd"]') + test('it does NOT show loading indicator when loading is false', () => { + const wrapper = mount( + + + + ); + + // click the accordion to expand it + wrapper + .find('button') .first() - .childAt(0); - expect(loadingWrapper.prop('isLoading')).toBe(false); + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="host-stat-auditbeatAuditd"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(false); }); - test('it does show loading indicator when loading', () => { - const wrapper = shallow(); - const loadingWrapper = wrapper - .dive() - .find('[data-test-subj="host-stat-auditbeatAuditd"]') + test('it shows loading indicator when loading is true', () => { + const wrapper = mount( + + + + ); + + // click the accordion to expand it + wrapper + .find('button') .first() - .childAt(0); - expect(loadingWrapper.prop('isLoading')).toBe(true); + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="host-stat-auditbeatAuditd"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(true); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx index aa2c6d61451bc6c..4134cc5ef4e4e3b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx @@ -4,209 +4,120 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiLoadingSpinner, -} from '@elastic/eui'; -import numeral from '@elastic/numeral'; +import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { has } from 'lodash/fp'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { OverviewHostData } from '../../../../graphql/types'; -import { getEmptyTagValue } from '../../../empty_value'; +import { FormattedStat, StatGroup } from '../types'; +import { StatValue } from '../stat_value'; interface OverviewHostProps { data: OverviewHostData; loading: boolean; } -// eslint-disable-next-line complexity -const overviewHostStats = (data: OverviewHostData) => [ +export const getOverviewHostStats = (data: OverviewHostData): FormattedStat[] => [ { - description: - has('auditbeatAuditd', data) && data.auditbeatAuditd !== null - ? numeral(data.auditbeatAuditd).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.auditbeatAuditd ?? 0, + title: , id: 'auditbeatAuditd', }, { - description: - has('auditbeatFIM', data) && data.auditbeatFIM !== null - ? numeral(data.auditbeatFIM).format('0,0') - : getEmptyTagValue(), + count: data.auditbeatFIM ?? 0, title: ( ), id: 'auditbeatFIM', }, { - description: - has('auditbeatLogin', data) && data.auditbeatLogin !== null - ? numeral(data.auditbeatLogin).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.auditbeatLogin ?? 0, + title: , id: 'auditbeatLogin', }, { - description: - has('auditbeatPackage', data) && data.auditbeatPackage !== null - ? numeral(data.auditbeatPackage).format('0,0') - : getEmptyTagValue(), + count: data.auditbeatPackage ?? 0, title: ( - + ), id: 'auditbeatPackage', }, { - description: - has('auditbeatProcess', data) && data.auditbeatProcess !== null - ? numeral(data.auditbeatProcess).format('0,0') - : getEmptyTagValue(), + count: data.auditbeatProcess ?? 0, title: ( - + ), id: 'auditbeatProcess', }, { - description: - has('auditbeatUser', data) && data.auditbeatUser !== null - ? numeral(data.auditbeatUser).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.auditbeatUser ?? 0, + title: , id: 'auditbeatUser', }, { - description: - has('endgameDns', data) && data.endgameDns !== null - ? numeral(data.endgameDns).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.endgameDns ?? 0, + title: , id: 'endgameDns', }, { - description: - has('endgameFile', data) && data.endgameFile !== null - ? numeral(data.endgameFile).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.endgameFile ?? 0, + title: , id: 'endgameFile', }, { - description: - has('endgameImageLoad', data) && data.endgameImageLoad !== null - ? numeral(data.endgameImageLoad).format('0,0') - : getEmptyTagValue(), + count: data.endgameImageLoad ?? 0, title: ( ), id: 'endgameImageLoad', }, { - description: - has('endgameNetwork', data) && data.endgameNetwork !== null - ? numeral(data.endgameNetwork).format('0,0') - : getEmptyTagValue(), + count: data.endgameNetwork ?? 0, title: ( - + ), id: 'endgameNetwork', }, { - description: - has('endgameProcess', data) && data.endgameProcess !== null - ? numeral(data.endgameProcess).format('0,0') - : getEmptyTagValue(), + count: data.endgameProcess ?? 0, title: ( - + ), id: 'endgameProcess', }, { - description: - has('endgameRegistry', data) && data.endgameRegistry !== null - ? numeral(data.endgameRegistry).format('0,0') - : getEmptyTagValue(), + count: data.endgameRegistry ?? 0, title: ( - + ), id: 'endgameRegistry', }, { - description: - has('endgameSecurity', data) && data.endgameSecurity !== null - ? numeral(data.endgameSecurity).format('0,0') - : getEmptyTagValue(), + count: data.endgameSecurity ?? 0, title: ( - + ), id: 'endgameSecurity', }, { - description: - has('filebeatSystemModule', data) && data.filebeatSystemModule !== null - ? numeral(data.filebeatSystemModule).format('0,0') - : getEmptyTagValue(), + count: data.filebeatSystemModule ?? 0, title: ( ), id: 'filebeatSystemModule', }, { - description: - has('winlogbeat', data) && data.winlogbeat !== null - ? numeral(data.winlogbeat).format('0,0') - : getEmptyTagValue(), + count: data.winlogbeat ?? 0, title: ( ), @@ -214,31 +125,128 @@ const overviewHostStats = (data: OverviewHostData) => [ }, ]; -export const DescriptionListDescription = styled(EuiDescriptionListDescription)` - text-align: right; +const HostStatsContainer = styled.div` + .accordion-button { + width: 100%; + } +`; + +const hostStatGroups: StatGroup[] = [ + { + groupId: 'auditbeat', + name: ( + + ), + statIds: [ + 'auditbeatAuditd', + 'auditbeatFIM', + 'auditbeatLogin', + 'auditbeatPackage', + 'auditbeatProcess', + 'auditbeatUser', + ], + }, + { + groupId: 'endgame', + name: ( + + ), + statIds: [ + 'endgameDns', + 'endgameFile', + 'endgameImageLoad', + 'endgameNetwork', + 'endgameProcess', + 'endgameRegistry', + 'endgameSecurity', + ], + }, + { + groupId: 'filebeat', + name: ( + + ), + statIds: ['filebeatSystemModule'], + }, + { + groupId: 'winlogbeat', + name: ( + + ), + statIds: ['winlogbeat'], + }, +]; + +const Title = styled.div` + margin-left: 24px; `; -DescriptionListDescription.displayName = 'DescriptionListDescription'; +export const OverviewHostStats = React.memo(({ data, loading }) => { + const allHostStats = getOverviewHostStats(data); + const allHostStatsCount = allHostStats.reduce((total, stat) => total + stat.count, 0); -const StatValue = React.memo<{ isLoading: boolean; value: React.ReactNode | null | undefined }>( - ({ isLoading, value }) => ( - <>{isLoading ? : value != null ? value : getEmptyTagValue()} - ) -); + return ( + + {hostStatGroups.map((statGroup, i) => { + const statsForGroup = allHostStats.filter(s => statGroup.statIds.includes(s.id)); + const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0); -StatValue.displayName = 'StatValue'; + const accordionButton = useMemo( + () => ( + + + {statGroup.name} + + + + + + ), + [statGroup, statsForGroupCount, loading, allHostStatsCount] + ); -export const OverviewHostStats = React.memo(({ data, loading }) => ( - - {overviewHostStats(data).map((item, index) => ( - - {item.title} - - - - - ))} - -)); + return ( + + + {statsForGroup.map(stat => ( + + + + {stat.title} + + + + + + + ))} + + {i !== hostStatGroups.length - 1 && } + + ); + })} + + ); +}); OverviewHostStats.displayName = 'OverviewHostStats'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx index af8c87ff385968f..36af58c4879a78d 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx @@ -5,23 +5,28 @@ */ import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { ESQuery } from '../../../../../common/typed_json'; import { HeaderSection } from '../../../header_section'; +import { useUiSetting$ } from '../../../../lib/kibana'; import { manageQuery } from '../../../page/manage_query'; import { ID as OverviewNetworkQueryId, OverviewNetworkQuery, } from '../../../../containers/overview/overview_network'; import { inputsModel } from '../../../../store/inputs'; -import { OverviewNetworkStats } from '../overview_network_stats'; +import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; import { getNetworkUrl } from '../../../link_to'; import { InspectButtonContainer } from '../../../inspect'; export interface OwnProps { startDate: number; endDate: number; + filterQuery?: ESQuery | string; setQuery: ({ id, inspect, @@ -37,49 +42,76 @@ export interface OwnProps { const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); -const OverviewNetworkComponent: React.FC = ({ endDate, startDate, setQuery }) => ( - - - - - } - title={ - - } - > - - - - +export const OverviewNetwork = React.memo( + ({ endDate, filterQuery, startDate, setQuery }) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - - {({ overviewNetwork, loading, id, inspect, refetch }) => ( - - )} - - - - + return ( + + + + + {({ overviewNetwork, loading, id, inspect, refetch }) => { + const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedNetworkEventsCount = numeral(networkEventsCount).format( + defaultNumberFormat + ); + + return ( + <> + + } + title={ + + } + > + + + + + + + + ); + }} + + + + + ); + } ); -export const OverviewNetwork = React.memo(OverviewNetworkComponent); +OverviewNetwork.displayName = 'OverviewNetwork'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap index 9db61c474e22003..4544c05f7b180d7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap @@ -1,143 +1,407 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Overview Network Stat Data rendering it renders the default OverviewNetworkStats 1`] = ` - - - - - + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="network-stat-accordion-groupauditbeat" + initialIsOpen={false} + paddingSize="none" > - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="network-stat-accordion-groupfilebeat" + initialIsOpen={false} + paddingSize="none" > - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + buttonContentClassName="accordion-button" + id="network-stat-accordion-grouppacketbeat" + initialIsOpen={false} + paddingSize="none" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx index 08093c5d38c1515..cf1a7d20b73eca7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import React from 'react'; import { OverviewNetworkStats } from '.'; import { mockData } from './mock'; +import { TestProviders } from '../../../../mock/test_providers'; describe('Overview Network Stat Data', () => { describe('rendering', () => { @@ -20,28 +21,52 @@ describe('Overview Network Stat Data', () => { }); }); describe('loading', () => { - test('it does not show loading indicator when not loading', () => { - const wrapper = shallow( - + test('it does NOT show loading indicator when loading is false', () => { + const wrapper = mount( + + + ); - const loadingWrapper = wrapper - .dive() - .find('[data-test-subj="network-stat-auditbeatSocket"]') + // click the accordion to expand it + wrapper + .find('button') .first() - .childAt(0); - expect(loadingWrapper.prop('isLoading')).toBe(false); + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="network-stat-auditbeatSocket"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(false); }); - test('it does show loading indicator when not loading', () => { - const wrapper = shallow( - + + test('it shows the loading indicator when loading is true', () => { + const wrapper = mount( + + + ); - const loadingWrapper = wrapper - .dive() - .find('[data-test-subj="network-stat-auditbeatSocket"]') + + // click the accordion to expand it + wrapper + .find('button') .first() - .childAt(0); - expect(loadingWrapper.prop('isLoading')).toBe(true); + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="network-stat-auditbeatSocket"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(true); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx index 81d374bc0286bc1..123f7f21a75fd96 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx @@ -4,168 +4,191 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiLoadingSpinner, -} from '@elastic/eui'; -import numeral from '@elastic/numeral'; +import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { has } from 'lodash/fp'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { OverviewNetworkData } from '../../../../graphql/types'; -import { getEmptyTagValue } from '../../../empty_value'; +import { FormattedStat, StatGroup } from '../types'; +import { StatValue } from '../stat_value'; interface OverviewNetworkProps { data: OverviewNetworkData; loading: boolean; } -const overviewNetworkStats = (data: OverviewNetworkData) => [ +export const getOverviewNetworkStats = (data: OverviewNetworkData): FormattedStat[] => [ { - description: - has('auditbeatSocket', data) && data.auditbeatSocket !== null - ? numeral(data.auditbeatSocket).format('0,0') - : getEmptyTagValue(), + count: data.auditbeatSocket ?? 0, title: ( - + ), id: 'auditbeatSocket', }, { - description: - has('filebeatCisco', data) && data.filebeatCisco !== null - ? numeral(data.filebeatCisco).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.filebeatCisco ?? 0, + title: , id: 'filebeatCisco', }, { - description: - has('filebeatNetflow', data) && data.filebeatNetflow !== null - ? numeral(data.filebeatNetflow).format('0,0') - : getEmptyTagValue(), + count: data.filebeatNetflow ?? 0, title: ( - + ), id: 'filebeatNetflow', }, { - description: - has('filebeatPanw', data) && data.filebeatPanw !== null - ? numeral(data.filebeatPanw).format('0,0') - : getEmptyTagValue(), + count: data.filebeatPanw ?? 0, title: ( ), id: 'filebeatPanw', }, { - description: - has('filebeatSuricata', data) && data.filebeatSuricata !== null - ? numeral(data.filebeatSuricata).format('0,0') - : getEmptyTagValue(), + count: data.filebeatSuricata ?? 0, title: ( - + ), id: 'filebeatSuricata', }, { - description: - has('filebeatZeek', data) && data.filebeatZeek !== null - ? numeral(data.filebeatZeek).format('0,0') - : getEmptyTagValue(), - title: ( - - ), + count: data.filebeatZeek ?? 0, + title: , id: 'filebeatZeek', }, { - description: - has('packetbeatDNS', data) && data.packetbeatDNS !== null - ? numeral(data.packetbeatDNS).format('0,0') - : getEmptyTagValue(), - title: ( + count: data.packetbeatDNS ?? 0, + title: , + id: 'packetbeatDNS', + }, + { + count: data.packetbeatFlow ?? 0, + title: , + id: 'packetbeatFlow', + }, + { + count: data.packetbeatTLS ?? 0, + title: , + id: 'packetbeatTLS', + }, +]; + +const networkStatGroups: StatGroup[] = [ + { + groupId: 'auditbeat', + name: ( ), - id: 'packetbeatDNS', + statIds: ['auditbeatSocket'], }, { - description: - has('packetbeatFlow', data) && data.packetbeatFlow !== null - ? numeral(data.packetbeatFlow).format('0,0') - : getEmptyTagValue(), - title: ( + groupId: 'filebeat', + name: ( ), - id: 'packetbeatFlow', + statIds: [ + 'filebeatCisco', + 'filebeatNetflow', + 'filebeatPanw', + 'filebeatSuricata', + 'filebeatZeek', + ], }, { - description: - has('packetbeatTLS', data) && data.packetbeatTLS !== null - ? numeral(data.packetbeatTLS).format('0,0') - : getEmptyTagValue(), - title: ( + groupId: 'packetbeat', + name: ( ), - id: 'packetbeatTLS', + statIds: ['packetbeatDNS', 'packetbeatFlow', 'packetbeatTLS'], }, ]; -export const DescriptionListDescription = styled(EuiDescriptionListDescription)` - text-align: right; +const NetworkStatsContainer = styled.div` + .accordion-button { + width: 100%; + } +`; + +const Title = styled.div` + margin-left: 24px; `; -DescriptionListDescription.displayName = 'DescriptionListDescription'; +export const OverviewNetworkStats = React.memo(({ data, loading }) => { + const allNetworkStats = getOverviewNetworkStats(data); + const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0); -const StatValue = React.memo<{ isLoading: boolean; value: React.ReactNode | null | undefined }>( - ({ isLoading, value }) => ( - <>{isLoading ? : value != null ? value : getEmptyTagValue()} - ) -); + return ( + + {networkStatGroups.map((statGroup, i) => { + const statsForGroup = allNetworkStats.filter(s => statGroup.statIds.includes(s.id)); + const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0); -StatValue.displayName = 'StatValue'; + const accordionButton = useMemo( + () => ( + + + {statGroup.name} + + + + + + ), + [statGroup, statsForGroupCount, loading, allNetworkStatsCount] + ); -export const OverviewNetworkStats = React.memo(({ data, loading }) => ( - - {overviewNetworkStats(data).map((item, index) => ( - - {item.title} - - - - - ))} - -)); + return ( + + + {statsForGroup.map(stat => ( + + + + {stat.title} + + + + + + + ))} + + {i !== networkStatGroups.length - 1 && } + + ); + })} + + ); +}); OverviewNetworkStats.displayName = 'OverviewNetworkStats'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx new file mode 100644 index 000000000000000..5a496ba78eb6c69 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiProgress, EuiText } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React from 'react'; +import styled from 'styled-components'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { useUiSetting$ } from '../../../lib/kibana'; + +const ProgressContainer = styled.div` + width: 100px; +`; + +export const StatValue = React.memo<{ + count: number; + isLoading: boolean; + isGroupStat: boolean; + max: number; +}>(({ count, isGroupStat, isLoading, max }) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + + return ( + <> + {isLoading ? ( + + ) : ( + + + + {numeral(count).format(defaultNumberFormat)} + + + + + + + + + )} + + ); +}); + +StatValue.displayName = 'StatValue'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/types.ts b/x-pack/legacy/plugins/siem/public/components/page/overview/types.ts new file mode 100644 index 000000000000000..9333aa386dbc067 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/types.ts @@ -0,0 +1,43 @@ +/* + * 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. + */ + +export type OverviewStatId = + | 'auditbeatAuditd' + | 'auditbeatFIM' + | 'auditbeatLogin' + | 'auditbeatPackage' + | 'auditbeatProcess' + | 'auditbeatSocket' + | 'auditbeatUser' + | 'endgameDns' + | 'endgameFile' + | 'endgameImageLoad' + | 'endgameNetwork' + | 'endgameProcess' + | 'endgameRegistry' + | 'endgameSecurity' + | 'filebeatCisco' + | 'filebeatNetflow' + | 'filebeatPanw' + | 'filebeatSuricata' + | 'filebeatSystemModule' + | 'filebeatZeek' + | 'packetbeatDNS' + | 'packetbeatFlow' + | 'packetbeatTLS' + | 'winlogbeat'; + +export interface FormattedStat { + count: number; + id: OverviewStatId; + title: React.ReactNode; +} + +export interface StatGroup { + name: string | React.ReactNode; + groupId: string; + statIds: OverviewStatId[]; +} diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx new file mode 100644 index 000000000000000..42ac3c19ff792cd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { getPinnedEventCount, getNotesCount } from '../../open_timeline/helpers'; +import { OpenTimelineResult } from '../../open_timeline/types'; + +import * as i18n from '../translations'; + +const Icon = styled(EuiIcon)` + margin-right: 8px; +`; + +const FlexGroup = styled(EuiFlexGroup)` + margin-right: 16px; +`; + +const IconWithCount = React.memo<{ count: number; icon: string; tooltip: string }>( + ({ count, icon, tooltip }) => ( + + + + + + + + + {count} + + + + + ) +); + +IconWithCount.displayName = 'IconWithCount'; + +export const RecentTimelineCounts = React.memo<{ + timeline: OpenTimelineResult; +}>(({ timeline }) => { + return ( + <> + + + + ); +}); + +RecentTimelineCounts.displayName = 'RecentTimelineCounts'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx new file mode 100644 index 000000000000000..de8a3de8094d043 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui'; +import React from 'react'; + +import { FilterMode } from '../types'; + +const toggleButtonIcons: EuiButtonGroupOption[] = [ + { + id: 'favorites', + label: 'Favorites', + iconType: 'starFilled', + }, + { + id: `recently-updated`, + label: 'Last updated', + iconType: 'documentEdit', + }, +]; + +export const Filters = React.memo<{ + filterBy: FilterMode; + setFilterBy: (filterBy: FilterMode) => void; +}>(({ filterBy, setFilterBy }) => ( + { + setFilterBy(f as FilterMode); + }} + isIconOnly + /> +)); + +Filters.displayName = 'Filters'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx new file mode 100644 index 000000000000000..886a2345248a2ac --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx @@ -0,0 +1,66 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiLink, + EuiToolTip, + EuiButtonIcon, +} from '@elastic/eui'; +import React from 'react'; + +import { isUntitled } from '../../open_timeline/helpers'; +import { OnOpenTimeline, OpenTimelineResult } from '../../open_timeline/types'; + +import * as i18n from '../translations'; + +export interface MeApiResponse { + username: string; +} + +export const RecentTimelineHeader = React.memo<{ + onOpenTimeline: OnOpenTimeline; + timeline: OpenTimelineResult; +}>(({ onOpenTimeline, timeline }) => { + const { title, savedObjectId } = timeline; + + return ( + + + + onOpenTimeline({ duplicate: false, timelineId: `${savedObjectId}` })} + > + {isUntitled(timeline) ? i18n.UNTITLED_TIMELINE : title} + + + + + + + + onOpenTimeline({ + duplicate: true, + timelineId: `${savedObjectId}`, + }) + } + size="s" + /> + + + + ); +}); + +RecentTimelineHeader.displayName = 'RecentTimelineHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts b/x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts new file mode 100644 index 000000000000000..61b49da01dc3aaa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts @@ -0,0 +1,26 @@ +/* + * 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 { throwIfNotOk } from '../../hooks/api/api'; +import { MeApiResponse } from './recent_timelines'; + +export const getMeApiUrl = (getBasePath: () => string): string => + `${getBasePath()}/internal/security/me`; + +export const fetchUsername = async (meApiUrl: string) => { + const response = await fetch(meApiUrl, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + }, + }); + + await throwIfNotOk(response); + const apiResponse: MeApiResponse = await response.json(); + + return apiResponse.username; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx new file mode 100644 index 000000000000000..f1e22d1901d47b3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx @@ -0,0 +1,141 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import { EuiHorizontalRule, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { ActionCreator } from 'typescript-fsa'; +import chrome from 'ui/chrome'; + +import { AllTimelinesQuery } from '../../containers/timeline/all'; +import { SortFieldTimeline, Direction } from '../../graphql/types'; +import { fetchUsername, getMeApiUrl } from './helpers'; +import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; +import { DispatchUpdateTimeline, OnOpenTimeline } from '../open_timeline/types'; +import { RecentTimelines } from './recent_timelines'; +import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions'; +import { FilterMode } from './types'; + +import * as i18n from './translations'; + +export interface MeApiResponse { + username: string; +} + +interface OwnProps { + apolloClient: ApolloClient<{}>; + filterBy: FilterMode; +} + +interface DispatchProps { + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + updateTimeline: DispatchUpdateTimeline; +} + +export type Props = OwnProps & DispatchProps; + +const StatefulRecentTimelinesComponent = React.memo( + ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { + const actionDispatcher = updateIsLoading as ActionCreator<{ id: string; isLoading: boolean }>; + const [username, setUsername] = useState(undefined); + const LoadingSpinner = useMemo(() => , []); + const onOpenTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading: actionDispatcher, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + useEffect(() => { + let canceled = false; + + const fetchData = async () => { + try { + const loggedInUser = await fetchUsername(getMeApiUrl(chrome.getBasePath)); + + if (!canceled) { + setUsername(loggedInUser); + } + } catch (e) { + if (!canceled) { + setUsername(null); + } + } + }; + + fetchData(); + + return () => { + canceled = true; + }; + }, []); + + if (username === undefined) { + return LoadingSpinner; + } else if (username == null) { + return null; + } + + // TODO: why does `createdBy: ` specified as a `search` query does not match results? + + const noTimelinesMessage = + filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + + return ( + + {({ timelines, loading }) => ( + <> + {loading ? ( + <>{LoadingSpinner} + ) : ( + + )} + + + {i18n.VIEW_ALL_TIMELINES} + + + )} + + ); + } +); + +StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +export const StatefulRecentTimelines = connect( + null, + mapDispatchToProps +)(StatefulRecentTimelinesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx new file mode 100644 index 000000000000000..a310d0613d49c5b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; + +import { RecentTimelineHeader } from './header'; +import { OnOpenTimeline, OpenTimelineResult } from '../open_timeline/types'; + +import { RecentTimelineCounts } from './counts'; + +export interface MeApiResponse { + username: string; +} + +export const RecentTimelines = React.memo<{ + noTimelinesMessage: string; + onOpenTimeline: OnOpenTimeline; + timelines: OpenTimelineResult[]; +}>(({ noTimelinesMessage, onOpenTimeline, timelines }) => { + if (timelines.length === 0) { + return ( + <> + + {noTimelinesMessage} + + + ); + } + + return ( + <> + {timelines.map((t, i) => ( +
+ + + {t.description && t.description.length && ( + <> + + + {t.description} + + + )} + {i !== timelines.length - 1 && } +
+ ))} + + ); +}); + +RecentTimelines.displayName = 'RecentTimelines'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts b/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts new file mode 100644 index 000000000000000..e547272fde6e169 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts @@ -0,0 +1,55 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ERROR_RETRIEVING_USER_DETAILS = i18n.translate( + 'xpack.siem.recentTimelines.errorRetrievingUserDetailsMessage', + { + defaultMessage: 'Recent Timelines: An error occurred while retrieving user details', + } +); + +export const NO_FAVORITE_TIMELINES = i18n.translate( + 'xpack.siem.recentTimelines.noFavoriteTimelinesMessage', + { + defaultMessage: + "You haven't favorited any timelines yet. Get out there and start threat hunting!", + } +); + +export const NO_TIMELINES = i18n.translate('xpack.siem.recentTimelines.noTimelinesMessage', { + defaultMessage: "You haven't created any timelines yet. Get out there and start threat hunting!", +}); + +export const NOTES = i18n.translate('xpack.siem.recentTimelines.notesTooltip', { + defaultMessage: 'Notes', +}); + +export const OPEN_AS_DUPLICATE = i18n.translate( + 'xpack.siem.recentTimelines.openAsDuplicateTooltip', + { + defaultMessage: 'Open as a duplicate timeline', + } +); + +export const PINNED_EVENTS = i18n.translate('xpack.siem.recentTimelines.pinnedEventsTooltip', { + defaultMessage: 'Pinned events', +}); + +export const UNTITLED_TIMELINE = i18n.translate( + 'xpack.siem.recentTimelines.untitledTimelineLabel', + { + defaultMessage: 'Untitled timeline', + } +); + +export const VIEW_ALL_TIMELINES = i18n.translate( + 'xpack.siem.recentTimelines.viewAllTimelinesLink', + { + defaultMessage: 'View all timelines', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts b/x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts new file mode 100644 index 000000000000000..d99209dfb1267fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export type FilterMode = 'favorites' | 'recently-updated'; diff --git a/x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx b/x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx new file mode 100644 index 000000000000000..90949f1bdd14732 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; +import React from 'react'; + +export const SidebarHeader = React.memo<{ children?: React.ReactNode; title: string }>( + ({ children, title }) => ( + <> + + + +

{title}

+
+
+ + {children} +
+ + + ) +); + +SidebarHeader.displayName = 'SidebarHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx index 67823bea9e170cd..ab290c2f2fd67bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx @@ -145,7 +145,7 @@ describe('UrlStateContainer', () => { ).toEqual({ hash: '', pathname: examplePath, - search: [CONSTANTS.overviewPage, CONSTANTS.timelinePage].includes(page) + search: [CONSTANTS.timelinePage].includes(page) ? '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' : `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, state: '', diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index 4eb6398cc7773db..24b3270e8944288 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -39,7 +39,13 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timerange, CONSTANTS.timeline, ], - overview: [CONSTANTS.timeline, CONSTANTS.timerange], + overview: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], timeline: [CONSTANTS.timeline, CONSTANTS.timerange], }; diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts new file mode 100644 index 000000000000000..3e2033837561634 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', { + defaultMessage: 'Welcome to SIEM. Let’s get you started.', +}); + +export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', { + defaultMessage: + 'To begin using security information and event management, you’ll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Let’s do that now!', +}); + +export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', { + defaultMessage: 'Add data with Beats', +}); + +export const EMPTY_ACTION_SECONDARY = i18n.translate( + 'xpack.siem.pages.common.emptyActionSecondary', + { + defaultMessage: 'View getting started guide', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx index a217fd6a737e70a..bf7a2109fd3b5d4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx @@ -9,7 +9,7 @@ import chrome from 'ui/chrome'; import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; -import * as i18n from './translations'; +import * as i18n from '../common/translations'; const basePath = chrome.getBasePath(); @@ -21,8 +21,9 @@ export const DetectionEngineEmptyPage = React.memo(() => ( actionSecondaryIcon="popout" actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY} actionSecondaryTarget="_blank" - actionSecondaryUrl={useKibana().services.docLinks.links.siem} + actionSecondaryUrl={useKibana().services.docLinks.links.siem.gettingStarted} data-test-subj="empty-page" + message={i18n.EMPTY_MESSAGE} title={i18n.EMPTY_TITLE} /> )); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx index 713bd6239d80e99..c3a0ceab5e25eb2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx @@ -14,7 +14,7 @@ export const DetectionEngineNoIndex = React.memo(() => ( ( { actionSecondaryIcon="popout" actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY} actionSecondaryTarget="_blank" - actionSecondaryUrl={docLinks.links.siem} + actionSecondaryUrl={docLinks.links.siem.gettingStarted} data-test-subj="empty-page" + message={i18n.EMPTY_MESSAGE} title={i18n.EMPTY_TITLE} /> ); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts index 87617f6bc5f7fd5..2d7030663579e64 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts @@ -49,16 +49,3 @@ export const NAVIGATION_EVENTS_TITLE = i18n.translate('xpack.siem.hosts.navigati export const NAVIGATION_ALERTS_TITLE = i18n.translate('xpack.siem.hosts.navigation.alertsTitle', { defaultMessage: 'Alerts', }); - -export const EMPTY_TITLE = i18n.translate('xpack.siem.hosts.emptyTitle', { - defaultMessage: - 'It looks like you don’t have any indices relevant to hosts in the SIEM application', -}); - -export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.hosts.emptyActionPrimary', { - defaultMessage: 'View setup instructions', -}); - -export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.hosts.emptyActionSecondary', { - defaultMessage: 'Go to documentation', -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx index e22802fd29d49ef..78a3ae147fd0fcc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx @@ -9,7 +9,7 @@ import chrome from 'ui/chrome'; import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; -import * as i18n from './translations'; +import * as i18n from '../common/translations'; const basePath = chrome.getBasePath(); @@ -24,9 +24,10 @@ export const NetworkEmptyPage = React.memo(() => { actionSecondaryIcon="popout" actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY} actionSecondaryTarget="_blank" - actionSecondaryUrl={docLinks.links.siem} + actionSecondaryUrl={docLinks.links.siem.gettingStarted} data-test-subj="empty-page" title={i18n.EMPTY_TITLE} + message={i18n.EMPTY_MESSAGE} /> ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts index 91c3338ff790308..a087d80f68cc0b2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts @@ -14,19 +14,6 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.network.pageTitle', { defaultMessage: 'Network', }); -export const EMPTY_TITLE = i18n.translate('xpack.siem.network.emptyTitle', { - defaultMessage: - 'It looks like you don’t have any indices relevant to network in the SIEM application', -}); - -export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.network.emptyActionPrimary', { - defaultMessage: 'View setup instructions', -}); - -export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.network.emptyActionSecondary', { - defaultMessage: 'Go to documentation', -}); - export const NAVIGATION_FLOWS_TITLE = i18n.translate('xpack.siem.network.navigation.flowsTitle', { defaultMessage: 'Flows', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx new file mode 100644 index 000000000000000..1b935faca0e7ce9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -0,0 +1,102 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public'; + +import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; +import { AlertsOverTimeHistogram } from '../../../components/page/hosts/alerts_over_time'; +import { manageQuery } from '../../../components/page/manage_query'; +import { AlertsOverTimeQuery } from '../../../containers/alerts/alerts_over_time'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { SetAbsoluteRangeDatePicker } from '../../network/types'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { hostsModel, inputsModel } from '../../../store'; +import { HostsTableType } from '../../../store/hosts/model'; + +import * as i18n from '../translations'; + +const NO_FILTERS: esFilters.Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: esFilters.Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +export const AlertsByCategory = React.memo( + ({ + filters = NO_FILTERS, + from, + indexPattern, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setQuery, + to, + }) => { + const kibana = useKibana(); + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + const alertsCountViewAlertsButton = useMemo( + () => ( + {i18n.VIEW_ALERTS} + ), + [] + ); + + const AlertsOverTimeManage = manageQuery(AlertsOverTimeHistogram); + + return ( + + {({ alertsOverTime, loading, id, inspect, refetch, totalCount }) => ( + + )} + + ); + } +); + +AlertsByCategory.displayName = 'AlertsByCategory'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx new file mode 100644 index 000000000000000..d0bb46a1965dc62 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx @@ -0,0 +1,80 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public'; +import styled from 'styled-components'; + +import { OverviewHost } from '../../../components/page/overview/overview_host'; +import { OverviewNetwork } from '../../../components/page/overview/overview_network'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; + +const HorizontalSpacer = styled(EuiFlexItem)` + width: 24px; +`; + +const NO_FILTERS: esFilters.Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: esFilters.Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +export const EventCounts = React.memo( + ({ filters = NO_FILTERS, from, indexPattern, query = DEFAULT_QUERY, setQuery, to }) => { + const kibana = useKibana(); + + return ( + + + + + + + + + + + + ); + } +); + +EventCounts.displayName = 'EventCounts'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx new file mode 100644 index 000000000000000..c7f303ddbafb944 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public'; + +import { EventsOverTimeHistogram } from '../../../components/page/hosts/events_over_time'; +import { manageQuery } from '../../../components/page/manage_query'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { SetAbsoluteRangeDatePicker } from '../../network/types'; +import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { hostsModel, inputsModel } from '../../../store'; +import { HostsTableType } from '../../../store/hosts/model'; +import { EventsOverTimeQuery } from '../../../containers/events/events_over_time'; + +import * as i18n from '../translations'; + +const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram); + +const NO_FILTERS: esFilters.Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: esFilters.Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +export const EventsByDataset = React.memo( + ({ + filters = NO_FILTERS, + from, + indexPattern, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setQuery, + to, + }) => { + const kibana = useKibana(); + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + const eventsCountViewEventsButton = useMemo( + () => ( + {i18n.VIEW_EVENTS} + ), + [] + ); + + return ( + + {({ eventsOverTime, loading, id, inspect, refetch, totalCount }) => ( + + )} + + ); + } +); + +EventsByDataset.displayName = 'EventsByDataset'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx index e0af54acde31051..65b401f00a86e0c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx @@ -6,8 +6,8 @@ import React, { memo } from 'react'; -import { OverviewComponent } from './overview'; +import { StatefulOverview } from './overview'; -export const Overview = memo(() => ); +export const Overview = memo(() => ); Overview.displayName = 'Overview'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx index eff61bf6a9710a0..be43ae8f5ed64b9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx @@ -10,10 +10,35 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { MemoryRouter } from 'react-router-dom'; +import '../../mock/match_media'; import { TestProviders } from '../../mock'; import { mocksSource } from '../../containers/source/mock'; import { Overview } from './index'; +jest.mock('ui/chrome', () => ({ + getBasePath: () => { + return ''; + }, + getKibanaVersion: () => { + return 'v8.0.0'; + }, + breadcrumbs: { + set: jest.fn(), + }, + getUiSettingsClient: () => ({ + get: jest.fn(), + }), +})); + +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar and QueryBar +jest.mock('../../components/search_bar', () => ({ + SiemSearchBar: () => null, +})); +jest.mock('../../components/query_bar', () => ({ + QueryBar: () => null, +})); + let localSource: Array<{ request: {}; result: { diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index a0e94431054ccad..a9b465102bddeea 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -4,76 +4,144 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; -import moment from 'moment'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; -import chrome from 'ui/chrome'; +import { connect } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; +import { compose } from 'redux'; +import { Query, esFilters } from 'src/plugins/data/public'; +import styled from 'styled-components'; -import { useKibana } from '../../lib/kibana'; -import { EmptyPage } from '../../components/empty_page'; +import { AlertsByCategory } from './alerts_by_category'; +import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; -import { OverviewHost } from '../../components/page/overview/overview_host'; -import { OverviewNetwork } from '../../components/page/overview/overview_network'; +import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; import { GlobalTime } from '../../containers/global_time'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { EventsByDataset } from './events_by_dataset'; +import { EventCounts } from './event_counts'; +import { SetAbsoluteRangeDatePicker } from '../network/types'; +import { OverviewEmpty } from './overview_empty'; +import { StatefulSidebar } from './sidebar'; +import { SignalsByCategory } from './signals_by_category'; +import { inputsSelectors, State } from '../../store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { Summary } from './summary'; + import * as i18n from './translations'; -const basePath = chrome.getBasePath(); +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const NO_FILTERS: esFilters.Filter[] = []; +const SidebarFlexItem = styled(EuiFlexItem)` + margin-right: 24px; +`; -export const OverviewComponent = React.memo(() => { - const docLinks = useKibana().services.docLinks; - const dateEnd = Date.now(); - const dateRange = moment.duration(24, 'hours').asMilliseconds(); - const dateStart = dateEnd - dateRange; +interface OverviewComponentReduxProps { + query?: Query; + filters?: esFilters.Filter[]; + setAbsoluteRangeDatePicker?: SetAbsoluteRangeDatePicker; +} - return ( +const OverviewComponent = React.memo( + ({ filters = NO_FILTERS, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker }) => ( <> - - - - - {({ indicesExist }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ setQuery }) => ( - - - - - - )} - - ) : ( - - ) - } - - + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + + + + + + + + + + {({ from, setQuery, to }) => ( + <> + + + + + + + + + + + + + + + )} + + + + + + ) : ( + + ) + } + - ); -}); + ) +); + OverviewComponent.displayName = 'OverviewComponent'; + +const makeMapStateToProps = () => { + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + + const mapStateToProps = (state: State): OverviewComponentReduxProps => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); + + return mapStateToProps; +}; + +export const StatefulOverview = compose>( + connect(makeMapStateToProps, { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }) +)(OverviewComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx new file mode 100644 index 000000000000000..43883515574acc0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import chrome from 'ui/chrome'; + +import * as i18nCommon from '../../common/translations'; +import { EmptyPage } from '../../../components/empty_page'; +import { useKibana } from '../../../lib/kibana'; + +const basePath = chrome.getBasePath(); + +export const OverviewEmpty = React.memo(() => { + const docLinks = useKibana().services.docLinks; + + return ( + + ); +}); + +OverviewEmpty.displayName = 'OverviewEmpty'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx new file mode 100644 index 000000000000000..ad2821edde411c0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx @@ -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 React, { useState } from 'react'; + +import { FilterMode } from '../../../components/recent_timelines/types'; +import { Sidebar } from './sidebar'; + +export const StatefulSidebar = React.memo(() => { + const [filterBy, setFilterBy] = useState('favorites'); + + return ; +}); + +StatefulSidebar.displayName = 'StatefulSidebar'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx new file mode 100644 index 000000000000000..29ab7b42e0ae3c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx @@ -0,0 +1,58 @@ +/* + * 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 { ApolloConsumer } from 'react-apollo'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { Filters } from '../../../components/recent_timelines/filters'; +import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; +import { StatefulRecentTimelines } from '../../../components/recent_timelines'; +import { StatefulNewsFeed } from '../../../components/news_feed'; +import { FilterMode } from '../../../components/recent_timelines/types'; +import { SidebarHeader } from '../../../components/sidebar_header'; + +import * as i18n from '../translations'; + +const SidebarFlexGroup = styled(EuiFlexGroup)` + width: 305px; +`; + +export const Sidebar = React.memo<{ + filterBy: FilterMode; + setFilterBy: (filterBy: FilterMode) => void; +}>(({ filterBy, setFilterBy }) => { + const RecentTimelinesFilters = useMemo( + () => , + [filterBy, setFilterBy] + ); + + return ( + + + {RecentTimelinesFilters} + + {client => } + + + + + + + + + + + + + ); +}); + +Sidebar.displayName = 'Sidebar'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx new file mode 100644 index 000000000000000..ce6e6ccf4e9e30f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public'; + +import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel'; +import { SetAbsoluteRangeDatePicker } from '../../network/types'; +import { inputsModel } from '../../../store'; + +import * as i18n from '../translations'; + +const NO_FILTERS: esFilters.Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: esFilters.Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +export const SignalsByCategory = React.memo( + ({ filters = NO_FILTERS, from, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, to }) => { + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + return ( + + ); + } +); + +SignalsByCategory.displayName = 'SignalsByCategory'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx index 51cfcbe9374ab96..da16cb28c61711f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx @@ -29,7 +29,7 @@ export const Summary = React.memo(() => { defaultMessage="Welcome to Security Information & Event Management (SIEM). Get started by reviewing our {docs} or {data}. For information about upcoming features and tutorials, be sure to check out our {siemSolution} page." values={{ docs: ( - +