diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts index 81761dd085df68..ed3664b6792b3b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts @@ -6,13 +6,13 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; export const querySignalsSchema = t.exact( t.partial({ query: t.object, aggs: t.object, - size: PositiveIntegerGreaterThanZero, + size: PositiveInteger, track_total_hits: t.boolean, _source: t.array(t.string), }) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 3b524bd252cdda..dda86d2717386c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details'; +import { ALERT_FLYOUT, CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details'; import { expandFirstAlert, @@ -41,12 +41,14 @@ describe('Alert details with unmapped fields', () => { openJsonView(); scrollJsonViewToBottom(); - cy.get(JSON_LINES).then((elements) => { - const length = elements.length; - cy.wrap(elements) - .eq(length - expectedUnmappedField.line) - .should('have.text', expectedUnmappedField.text); - }); + cy.get(ALERT_FLYOUT) + .find(JSON_LINES) + .then((elements) => { + const length = elements.length; + cy.wrap(elements) + .eq(length - expectedUnmappedField.line) + .should('have.text', expectedUnmappedField.text); + }); }); it('Displays the unmapped field on the table', () => { @@ -57,8 +59,8 @@ describe('Alert details with unmapped fields', () => { }; openTable(); - - cy.get(TABLE_ROWS) + cy.get(ALERT_FLYOUT) + .find(TABLE_ROWS) .eq(expectedUnmmappedField.row) .within(() => { cy.get(CELL_TEXT).eq(2).should('have.text', expectedUnmmappedField.field); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 6da09bc0da8cd0..2449a90f5328c4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const ALERT_FLYOUT = '[data-test-subj="timeline:details-panel:flyout"]'; + export const CELL_TEXT = '.euiText'; export const JSON_CONTENT = '[data-test-subj="jsonView"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index fe32b7addd25ef..f733331bcd691e 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -49,6 +49,7 @@ export interface HeaderSectionProps extends HeaderProps { tooltip?: string; growLeftSplit?: boolean; inspectMultiple?: boolean; + hideSubtitle?: boolean; } const HeaderSectionComponent: React.FC = ({ @@ -64,6 +65,7 @@ const HeaderSectionComponent: React.FC = ({ tooltip, growLeftSplit = true, inspectMultiple = false, + hideSubtitle = false, }) => (
@@ -82,7 +84,9 @@ const HeaderSectionComponent: React.FC = ({ - + {!hideSubtitle && ( + + )} {id && ( diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index f643c5690e284c..2286a530307843 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -26,6 +26,7 @@ import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { getOptions } from './helpers'; import { TopN } from './top_n'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; const EMPTY_FILTERS: Filter[] = []; const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; @@ -153,7 +154,7 @@ const StatefulTopNComponent: React.FC = ({ data-test-subj="top-n" defaultView={defaultView} deleteQuery={timelineId === TimelineId.active ? undefined : deleteQuery} - field={field} + field={field as AlertsStackByField} filters={timelineId === TimelineId.active ? EMPTY_FILTERS : globalFilters} from={timelineId === TimelineId.active ? activeTimelineFrom : from} indexPattern={indexPattern} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index cee4254fd7358b..8cb56d7581b369 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -35,7 +35,7 @@ jest.mock('uuid', () => { }; }); -const field = 'process.name'; +const field = 'host.name'; const value = 'nice'; const combinedQueries = { bool: { diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 9d38d6b4a59e35..0d4d52d338e56a 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -22,6 +22,7 @@ import { TopNOption } from './helpers'; import * as i18n from './translations'; import { getIndicesSelector, IndicesSelector } from './selectors'; import { State } from '../../store'; +import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; const TopNContainer = styled.div` width: 600px; @@ -50,7 +51,7 @@ const TopNContent = styled.div` export interface Props extends Pick { combinedQueries?: string; defaultView: TimelineEventsType; - field: string; + field: AlertsStackByField; filters: Filter[]; indexPattern: IIndexPattern; options: TopNOption[]; @@ -137,14 +138,11 @@ const TopNComponent: React.FC = ({ )} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts deleted file mode 100644 index 5515d07fc8040b..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const STACK_BY_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel', - { - defaultMessage: 'Stack by', - } -); - -export const STACK_BY_RISK_SCORES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown', - { - defaultMessage: 'Risk scores', - } -); - -export const STACK_BY_SEVERITIES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown', - { - defaultMessage: 'Severities', - } -); - -export const STACK_BY_DESTINATION_IPS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown', - { - defaultMessage: 'Top destination IPs', - } -); - -export const STACK_BY_SOURCE_IPS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown', - { - defaultMessage: 'Top source IPs', - } -); - -export const STACK_BY_ACTIONS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown', - { - defaultMessage: 'Top event actions', - } -); - -export const STACK_BY_CATEGORIES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown', - { - defaultMessage: 'Top event categories', - } -); - -export const STACK_BY_HOST_NAMES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown', - { - defaultMessage: 'Top host names', - } -); - -export const STACK_BY_RULE_TYPES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown', - { - defaultMessage: 'Top rule types', - } -); - -export const STACK_BY_RULE_NAMES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown', - { - defaultMessage: 'Top rules', - } -); - -export const STACK_BY_USERS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown', - { - defaultMessage: 'Top users', - } -); - -export const TOP = (fieldName: string) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel', { - values: { fieldName }, - defaultMessage: `Top {fieldName}`, - }); - -export const HISTOGRAM_HEADER = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle', - { - defaultMessage: 'Trend', - } -); - -export const ALL_OTHERS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', - { - defaultMessage: 'All others', - } -); - -export const VIEW_ALERTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel', - { - defaultMessage: 'View alerts', - } -); - -export const SHOWING_ALERTS = ( - totalAlertsFormatted: string, - totalAlerts: number, - modifier: string -) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle', { - values: { totalAlertsFormatted, totalAlerts, modifier }, - defaultMessage: - 'Showing: {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', - }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx new file mode 100644 index 00000000000000..561126f3264adb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { AlertsCount } from './alerts_count'; +import { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { TestProviders } from '../../../../common/mock'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { AlertsCountAggregation } from './types'; + +jest.mock('../../../../common/lib/kibana'); +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +describe('AlertsCount', () => { + it('renders correctly', () => { + const wrapper = shallow( + } + loading={false} + selectedStackByOption={'test_selected_field'} + /> + ); + + expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toBeTruthy(); + }); + + it('renders the given alert item', () => { + const alertFiedlKey = 'test_stack_by_test_key'; + const alertFiedlCount = 999; + const alertData = { + took: 0, + timeout: false, + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + aggregations: { + alertsByGroupingCount: { + buckets: [ + { + key: alertFiedlKey, + doc_count: alertFiedlCount, + }, + ], + }, + alertsByGrouping: { buckets: [] }, + }, + } as AlertSearchResponse; + + const wrapper = mount( + + + + + + ); + + expect(wrapper.text()).toContain(alertFiedlKey); + expect(wrapper.text()).toContain(alertFiedlCount); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx new file mode 100644 index 00000000000000..2c59868d8a6fe2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiProgress, EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; +import numeral from '@elastic/numeral'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import * as i18n from './translations'; +import { DefaultDraggable } from '../../../../common/components/draggables'; +import type { GenericBuckets } from '../../../../../common'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import type { AlertsCountAggregation } from './types'; +import { MISSING_IP } from '../common/helpers'; + +interface AlertsCountProps { + loading: boolean; + data: AlertSearchResponse | null; + selectedStackByOption: string; +} + +const Wrapper = styled.div` + overflow: scroll; + margin-top: -8px; +`; + +const StyledSpan = styled.span` + padding-left: 8px; +`; + +const getAlertsCountTableColumns = ( + selectedStackByOption: string, + defaultNumberFormat: string +): Array> => { + return [ + { + field: 'key', + name: selectedStackByOption, + truncateText: true, + render: function DraggableStackOptionField(value: string) { + return value === i18n.ALL_OTHERS || value === MISSING_IP ? ( + {value} + ) : ( + + ); + }, + }, + { + field: 'doc_count', + name: i18n.COUNT_TABLE_COLUMN_TITLE, + sortable: true, + textOnly: true, + dataType: 'number', + render: (item: string) => numeral(item).format(defaultNumberFormat), + }, + ]; +}; + +export const AlertsCount = memo(({ loading, selectedStackByOption, data }) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const listItems: GenericBuckets[] = data?.aggregations?.alertsByGroupingCount?.buckets ?? []; + const tableColumns = useMemo( + () => getAlertsCountTableColumns(selectedStackByOption, defaultNumberFormat), + [selectedStackByOption, defaultNumberFormat] + ); + + return ( + <> + {loading && } + + + + + + ); +}); + +AlertsCount.displayName = 'AlertsCount'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx new file mode 100644 index 00000000000000..e43381ce25530c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { getMissingFields } from '../common/helpers'; +import type { AlertsStackByField } from '../common/types'; + +export const getAlertsCountQuery = ( + stackByField: AlertsStackByField, + from: string, + to: string, + additionalFilters: Array<{ + bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; + }> = [] +) => { + const missing = getMissingFields(stackByField); + + return { + size: 0, + aggs: { + alertsByGroupingCount: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: DEFAULT_MAX_TABLE_QUERY_SIZE, + }, + }, + }, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx new file mode 100644 index 00000000000000..f1b7d8b06644d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { waitFor, act } from '@testing-library/react'; + +import { mount } from 'enzyme'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; + +import { TestProviders } from '../../../../common/mock'; + +import { AlertsCountPanel } from './index'; + +describe('AlertsCountPanel', () => { + const defaultProps = { + signalIndexName: 'signalIndexName', + }; + + it('renders correctly', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="alertsCountPanel"]').exists()).toBeTruthy(); + }); + }); + + describe('Query', () => { + it('it render with a illegal KQL', async () => { + const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); + spyOnBuildEsQuery.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="alertsCountPanel"]').exists()).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx new file mode 100644 index 00000000000000..001567d7d2cc85 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useState, useEffect } from 'react'; +import uuid from 'uuid'; + +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { HeaderSection } from '../../../../common/components/header_section'; + +import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; +import { InspectButtonContainer } from '../../../../common/components/inspect'; + +import { getAlertsCountQuery } from './helpers'; +import * as i18n from './translations'; +import { AlertsCount } from './alerts_count'; +import type { AlertsCountAggregation } from './types'; +import { DEFAULT_STACK_BY_FIELD } from '../common/config'; +import type { AlertsStackByField } from '../common/types'; +import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; +import { KpiPanel, StackBySelect } from '../common/components'; +import { useInspectButton } from '../common/hooks'; + +export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; + +interface AlertsCountPanelProps { + filters?: Filter[]; + query?: Query; + signalIndexName: string | null; +} + +export const AlertsCountPanel = memo( + ({ filters, query, signalIndexName }) => { + const { to, from, deleteQuery, setQuery } = useGlobalTime(); + + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_ALERTS_COUNT_ID}-${uuid.v4()}`, []); + const [selectedStackByOption, setSelectedStackByOption] = useState( + DEFAULT_STACK_BY_FIELD + ); + + const additionalFilters = useMemo(() => { + try { + return [ + esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter((f) => f.meta.disabled === false) ?? [] + ), + ]; + } catch (e) { + return []; + } + }, [query, filters]); + + const { + loading: isLoadingAlerts, + data: alertsData, + setQuery: setAlertsQuery, + response, + request, + refetch, + } = useQueryAlerts<{}, AlertsCountAggregation>({ + query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters), + indexName: signalIndexName, + }); + + useEffect(() => { + setAlertsQuery(getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters)); + }, [setAlertsQuery, selectedStackByOption, from, to, additionalFilters]); + + useInspectButton({ + setQuery, + response, + request, + refetch, + uniqueQueryId, + deleteQuery, + loading: isLoadingAlerts, + }); + + return ( + + + + + + + + + ); + } +); + +AlertsCountPanel.displayName = 'AlertsCountPanel'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/translations.ts new file mode 100644 index 00000000000000..6f2e428b6b519f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/translations.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COUNT_TABLE_COLUMN_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.count.countTableColumnTitle', + { + defaultMessage: 'Count', + } +); + +export const COUNT_TABLE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.count.countTableTitle', + { + defaultMessage: 'Count', + } +); + +export * from '../common/translations'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/types.ts new file mode 100644 index 00000000000000..06cdee581d3fd1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GenericBuckets } from '../../../../../common'; + +export interface AlertsCountAggregation { + alertsByGroupingCount: { + buckets: GenericBuckets[]; + }; +} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.test.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.test.tsx index 440d942bc117cf..11ab2c49a5dc0d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import '../../../common/mock/match_media'; +import '../../../../common/mock/match_media'; import { AlertsHistogram } from './alerts_histogram'; -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('AlertsHistogram', () => { it('renders correctly', () => { @@ -26,6 +26,6 @@ describe('AlertsHistogram', () => { /> ); - expect(wrapper.find('Chart')).toBeTruthy(); + expect(wrapper.find('Chart').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx similarity index 87% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx index ab5ad0557cc993..09d8d522716740 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx @@ -16,12 +16,12 @@ import { import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { useTheme, UpdateDateRange } from '../../../common/components/charts/common'; -import { histogramDateTimeFormatter } from '../../../common/components/utils'; -import { DraggableLegend } from '../../../common/components/charts/draggable_legend'; -import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; +import { useTheme, UpdateDateRange } from '../../../../common/components/charts/common'; +import { histogramDateTimeFormatter } from '../../../../common/components/utils'; +import { DraggableLegend } from '../../../../common/components/charts/draggable_legend'; +import { LegendItem } from '../../../../common/components/charts/draggable_legend_item'; -import { HistogramData } from './types'; +import type { HistogramData } from './types'; const DEFAULT_CHART_HEIGHT = 174; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx similarity index 77% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx index c6cf896937f482..298158440224fe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx @@ -7,10 +7,11 @@ import moment from 'moment'; -import { showAllOthersBucket } from '../../../../common/constants'; -import { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; -import { AlertSearchResponse } from '../../containers/detection_engine/alerts/types'; -import * as i18n from './translations'; +import { isEmpty } from 'lodash/fp'; +import type { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { getMissingFields } from '../common/helpers'; +import type { AlertsStackByField } from '../common/types'; const EMPTY_ALERTS_DATA: HistogramData[] = []; @@ -33,18 +34,14 @@ export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggre }; export const getAlertsHistogramQuery = ( - stackByField: string, + stackByField: AlertsStackByField, from: string, to: string, additionalFilters: Array<{ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> ) => { - const missing = showAllOthersBucket.includes(stackByField) - ? { - missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - } - : {}; + const missing = getMissingFields(stackByField); return { aggs: { @@ -103,3 +100,19 @@ export const showInitialLoadingSpinner = ({ isInitialLoading: boolean; isLoadingAlerts: boolean; }): boolean => isInitialLoading && isLoadingAlerts; + +export const parseCombinedQueries = (query?: string) => { + try { + return query != null && !isEmpty(query) ? JSON.parse(query) : {}; + } catch { + return {}; + } +}; + +export const buildCombinedQueries = (query?: string) => { + try { + return isEmpty(query) ? [] : [parseCombinedQueries(query)]; + } catch { + return []; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx similarity index 65% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 70101021bc4f0a..0d6793eb2b886b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -6,15 +6,14 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { shallow, mount } from 'enzyme'; +import { waitFor, act } from '@testing-library/react'; +import { mount } from 'enzyme'; -import '../../../common/mock/match_media'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; -import { TestProviders } from '../../../common/mock'; -import { SecurityPageName } from '../../../app/types'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { TestProviders } from '../../../../common/mock'; +import { SecurityPageName } from '../../../../app/types'; -import { AlertsHistogramPanel, buildCombinedQueries, parseCombinedQueries } from './index'; +import { AlertsHistogramPanel } from './index'; import * as helpers from './helpers'; jest.mock('react-router-dom', () => { @@ -27,8 +26,8 @@ jest.mock('react-router-dom', () => { }); const mockNavigateToApp = jest.fn(); -jest.mock('../../../common/lib/kibana/kibana_react', () => { - const original = jest.requireActual('../../../common/lib/kibana/kibana_react'); +jest.mock('../../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); return { ...original, @@ -46,8 +45,8 @@ jest.mock('../../../common/lib/kibana/kibana_react', () => { }; }); -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../../common/lib/kibana'); return { ...original, useUiSetting$: jest.fn().mockReturnValue([]), @@ -55,39 +54,65 @@ jest.mock('../../../common/lib/kibana', () => { }; }); -jest.mock('../../../common/components/navigation/use_get_url_search'); +jest.mock('../../../../common/components/navigation/use_get_url_search'); + +jest.mock('../../../containers/detection_engine/alerts/use_query', () => { + const original = jest.requireActual('../../../containers/detection_engine/alerts/use_query'); + return { + ...original, + useQueryAlerts: jest.fn().mockReturnValue({ + loading: true, + setQuery: () => undefined, + data: null, + response: '', + request: '', + refetch: null, + }), + }; +}); describe('AlertsHistogramPanel', () => { const defaultProps = { - from: '2020-07-07T08:20:18.966Z', signalIndexName: 'signalIndexName', setQuery: jest.fn(), - to: '2020-07-08T08:20:18.966Z', updateDateRange: jest.fn(), }; it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBeTruthy(); + wrapper.unmount(); }); describe('Button view alerts', () => { it('renders correctly', () => { const props = { ...defaultProps, showLinkToAlerts: true }; - const wrapper = shallow(); + const wrapper = mount( + + + + ); expect( - wrapper.find('[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]') + wrapper.find('[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]').exists() ).toBeTruthy(); + wrapper.unmount(); }); it('when click we call navigateToApp to make sure to navigate to right page', () => { const props = { ...defaultProps, showLinkToAlerts: true }; - const wrapper = shallow(); + const wrapper = mount( + + + + ); wrapper - .find('[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]') + .find('button[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]') .simulate('click', { preventDefault: jest.fn(), }); @@ -96,36 +121,36 @@ describe('AlertsHistogramPanel', () => { deepLinkId: SecurityPageName.alerts, path: '', }); + wrapper.unmount(); }); }); describe('Query', () => { it('it render with a illegal KQL', async () => { - const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); - spyOnBuildEsQuery.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; - const wrapper = mount( - - - - ); + await act(async () => { + const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); + spyOnBuildEsQuery.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; + const wrapper = mount( + + + + ); - await waitFor(() => { - expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBeTruthy(); + }); + wrapper.unmount(); }); }); }); describe('CombinedQueries', () => { - jest.mock('./helpers'); - const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery'); - beforeEach(() => { - mockGetAlertsHistogramQuery.mockReset(); - }); - it('combinedQueries props is valid, alerts query include combinedQueries', async () => { + const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery'); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' }, @@ -137,6 +162,7 @@ describe('AlertsHistogramPanel', () => { ); + await waitFor(() => { expect(mockGetAlertsHistogramQuery.mock.calls[0]).toEqual([ 'signal.rule.name', @@ -159,20 +185,20 @@ describe('AlertsHistogramPanel', () => { describe('parseCombinedQueries', () => { it('return empty object when variables is undefined', async () => { - expect(parseCombinedQueries(undefined)).toEqual({}); + expect(helpers.parseCombinedQueries(undefined)).toEqual({}); }); it('return empty object when variables is empty string', async () => { - expect(parseCombinedQueries('')).toEqual({}); + expect(helpers.parseCombinedQueries('')).toEqual({}); }); it('return empty object when variables is NOT a valid stringify json object', async () => { - expect(parseCombinedQueries('hello world')).toEqual({}); + expect(helpers.parseCombinedQueries('hello world')).toEqual({}); }); it('return a valid json object when variables is a valid json stringify', async () => { expect( - parseCombinedQueries( + helpers.parseCombinedQueries( '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}' ) ).toMatchInlineSnapshot(` @@ -199,20 +225,20 @@ describe('AlertsHistogramPanel', () => { describe('buildCombinedQueries', () => { it('return empty array when variables is undefined', async () => { - expect(buildCombinedQueries(undefined)).toEqual([]); + expect(helpers.buildCombinedQueries(undefined)).toEqual([]); }); it('return empty array when variables is empty string', async () => { - expect(buildCombinedQueries('')).toEqual([]); + expect(helpers.buildCombinedQueries('')).toEqual([]); }); it('return array with empty object when variables is NOT a valid stringify json object', async () => { - expect(buildCombinedQueries('hello world')).toEqual([{}]); + expect(helpers.buildCombinedQueries('hello world')).toEqual([{}]); }); it('return a valid json object when variables is a valid json stringify', async () => { expect( - buildCombinedQueries( + helpers.buildCombinedQueries( '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}' ) ).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 482032e6b4cbf7..2182ed7da0c4fa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -5,44 +5,44 @@ * 2.0. */ -import { Position } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; +import type { Position } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiTitleSize } from '@elastic/eui'; import numeral from '@elastic/numeral'; import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import uuid from 'uuid'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; -import { UpdateDateRange } from '../../../common/components/charts/common'; -import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; -import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; -import { HeaderSection } from '../../../common/components/header_section'; -import { Filter, esQuery, Query } from '../../../../../../../src/plugins/data/public'; -import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { getDetectionEngineUrl, useFormatUrl } from '../../../common/components/link_to'; -import { defaultLegendColors } from '../../../common/components/matrix_histogram/utils'; -import { InspectButtonContainer } from '../../../common/components/inspect'; -import { MatrixLoader } from '../../../common/components/matrix_histogram/matrix_loader'; -import { MatrixHistogramOption } from '../../../common/components/matrix_histogram/types'; -import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; -import { alertsHistogramOptions } from './config'; -import { formatAlertsData, getAlertsHistogramQuery, showInitialLoadingSpinner } from './helpers'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../../common/constants'; +import type { UpdateDateRange } from '../../../../common/components/charts/common'; +import type { LegendItem } from '../../../../common/components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; +import { HeaderSection } from '../../../../common/components/header_section'; +import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; +import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; +import { getDetectionEngineUrl, useFormatUrl } from '../../../../common/components/link_to'; +import { defaultLegendColors } from '../../../../common/components/matrix_histogram/utils'; +import { InspectButtonContainer } from '../../../../common/components/inspect'; +import { MatrixLoader } from '../../../../common/components/matrix_histogram/matrix_loader'; +import { useKibana, useUiSetting$ } from '../../../../common/lib/kibana'; +import { + parseCombinedQueries, + buildCombinedQueries, + formatAlertsData, + getAlertsHistogramQuery, + showInitialLoadingSpinner, +} from './helpers'; import { AlertsHistogram } from './alerts_histogram'; import * as i18n from './translations'; -import { AlertsHistogramOption, AlertsAggregation, AlertsTotal } from './types'; -import { LinkButton } from '../../../common/components/links'; -import { SecurityPageName } from '../../../app/types'; +import type { AlertsAggregation, AlertsTotal } from './types'; +import { LinkButton } from '../../../../common/components/links'; +import { SecurityPageName } from '../../../../app/types'; +import { DEFAULT_STACK_BY_FIELD, PANEL_HEIGHT } from '../common/config'; +import type { AlertsStackByField } from '../common/types'; +import { KpiPanel, StackBySelect } from '../common/components'; -const DEFAULT_PANEL_HEIGHT = 300; - -const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` - display: flex; - flex-direction: column; - ${({ height }) => (height != null ? `height: ${height}px;` : '')} - position: relative; -`; +import { useInspectButton } from '../common/hooks'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -52,89 +52,62 @@ const defaultTotalAlertsObj: AlertsTotal = { export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; const ViewAlertsFlexItem = styled(EuiFlexItem)` - margin-left: 24px; + margin-left: ${({ theme }) => theme.eui.euiSizeL}; `; -interface AlertsHistogramPanelProps - extends Pick { +interface AlertsHistogramPanelProps { chartHeight?: number; combinedQueries?: string; - defaultStackByOption?: AlertsHistogramOption; + defaultStackByOption?: AlertsStackByField; filters?: Filter[]; headerChildren?: React.ReactNode; /** Override all defaults, and only display this field */ - onlyField?: string; + onlyField?: AlertsStackByField; + titleSize?: EuiTitleSize; query?: Query; legendPosition?: Position; - panelHeight?: number; signalIndexName: string | null; showLinkToAlerts?: boolean; showTotalAlertsCount?: boolean; - stackByOptions?: AlertsHistogramOption[]; + showStackBy?: boolean; timelineId?: string; title?: string; updateDateRange: UpdateDateRange; } -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - const NO_LEGEND_DATA: LegendItem[] = []; -const DEFAULT_STACK_BY = 'signal.rule.name'; -const getDefaultStackByOption = (): AlertsHistogramOption => - alertsHistogramOptions.find(({ text }) => text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; - -export const parseCombinedQueries = (query?: string) => { - try { - return query != null && !isEmpty(query) ? JSON.parse(query) : {}; - } catch { - return {}; - } -}; - -export const buildCombinedQueries = (query?: string) => { - try { - return isEmpty(query) ? [] : [parseCombinedQueries(query)]; - } catch { - return []; - } -}; - export const AlertsHistogramPanel = memo( ({ chartHeight, combinedQueries, - defaultStackByOption = getDefaultStackByOption(), - deleteQuery, + defaultStackByOption = DEFAULT_STACK_BY_FIELD, filters, headerChildren, onlyField, query, - from, legendPosition = 'right', - panelHeight = DEFAULT_PANEL_HEIGHT, - setQuery, signalIndexName, showLinkToAlerts = false, showTotalAlertsCount = false, - stackByOptions, + showStackBy = true, timelineId, title = i18n.HISTOGRAM_HEADER, - to, updateDateRange, + titleSize = 'm', }) => { + const { to, from, deleteQuery, setQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); const [isInitialLoading, setIsInitialLoading] = useState(true); const [isInspectDisabled, setIsInspectDisabled] = useState(false); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const [totalAlertsObj, setTotalAlertsObj] = useState(defaultTotalAlertsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState( - onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) + const [selectedStackByOption, setSelectedStackByOption] = useState( + onlyField == null ? defaultStackByOption : onlyField ); + const { loading: isLoadingAlerts, data: alertsData, @@ -144,13 +117,14 @@ export const AlertsHistogramPanel = memo( refetch, } = useQueryAlerts<{}, AlertsAggregation>({ query: getAlertsHistogramQuery( - selectedStackByOption.value, + selectedStackByOption, from, to, buildCombinedQueries(combinedQueries) ), indexName: signalIndexName, }); + const kibana = useKibana(); const { navigateToApp } = kibana.services.application; const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts); @@ -166,13 +140,6 @@ export const AlertsHistogramPanel = memo( [totalAlertsObj] ); - const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => { - setSelectedStackByOption( - stackByOptions?.find((co) => co.value === event.target.value) ?? defaultStackByOption - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const goToDetectionEngine = useCallback( (ev) => { ev.preventDefault(); @@ -191,14 +158,14 @@ export const AlertsHistogramPanel = memo( ? alertsData.aggregations.alertsByGrouping.buckets.map((bucket, i) => ({ color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, dataProviderId: escapeDataProviderId( - `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption}-${bucket.key}` ), - field: selectedStackByOption.value, + field: selectedStackByOption, timelineId, value: bucket.key, })) : NO_LEGEND_DATA, - [alertsData, selectedStackByOption.value, timelineId] + [alertsData, selectedStackByOption, timelineId] ); useEffect(() => { @@ -213,29 +180,15 @@ export const AlertsHistogramPanel = memo( }; }, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]); - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (refetch != null && setQuery != null) { - setQuery({ - id: uniqueQueryId, - inspect: { - dsl: [request], - response: [response], - }, - loading: isLoadingAlerts, - refetch, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setQuery, isLoadingAlerts, alertsData, response, request, refetch]); + useInspectButton({ + setQuery, + response, + request, + refetch, + uniqueQueryId, + deleteQuery, + loading: isLoadingAlerts, + }); useEffect(() => { setTotalAlertsObj( @@ -265,7 +218,7 @@ export const AlertsHistogramPanel = memo( setIsInspectDisabled(false); setAlertsQuery( getAlertsHistogramQuery( - selectedStackByOption.value, + selectedStackByOption, from, to, !isEmpty(converted) ? [converted] : [] @@ -273,10 +226,10 @@ export const AlertsHistogramPanel = memo( ); } catch (e) { setIsInspectDisabled(true); - setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption.value, from, to, [])); + setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption, from, to, [])); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedStackByOption.value, from, to, query, filters, combinedQueries]); + }, [selectedStackByOption, from, to, query, filters, combinedQueries]); const linkButton = useMemo(() => { if (showLinkToAlerts) { @@ -301,22 +254,21 @@ export const AlertsHistogramPanel = memo( return ( - + - {stackByOptions && ( - )} {headerChildren != null && headerChildren} @@ -339,7 +291,7 @@ export const AlertsHistogramPanel = memo( updateDateRange={updateDateRange} /> )} - + ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/translations.ts new file mode 100644 index 00000000000000..67150926621ab5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/translations.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOP = (fieldName: string) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + +export const HISTOGRAM_HEADER = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle', + { + defaultMessage: 'Trend', + } +); + +export const VIEW_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel', + { + defaultMessage: 'View alerts', + } +); + +export const SHOWING_ALERTS = ( + totalAlertsFormatted: string, + totalAlerts: number, + modifier: string +) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts, modifier }, + defaultMessage: + 'Showing: {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts similarity index 86% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts index de4a770a439a5f..8c2a53dc23d43a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { inputsModel } from '../../../common/store'; - -export interface AlertsHistogramOption { - text: string; - value: string; -} +import type { inputsModel } from '../../../../common/store'; export interface HistogramData { x: number; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx new file mode 100644 index 00000000000000..53d41835d6bb92 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiPanel, EuiSelect } from '@elastic/eui'; +import styled from 'styled-components'; +import React, { useCallback } from 'react'; +import { PANEL_HEIGHT, MOBILE_PANEL_HEIGHT, alertsStackByOptions } from './config'; +import type { AlertsStackByField } from './types'; +import * as i18n from './translations'; + +export const KpiPanel = styled(EuiPanel)<{ height?: number }>` + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + + height: ${MOBILE_PANEL_HEIGHT}px; + + @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { + height: ${PANEL_HEIGHT}px; + } +`; +interface StackedBySelectProps { + selected: AlertsStackByField; + onSelect: (selected: AlertsStackByField) => void; +} + +export const StackBySelect: React.FC = ({ selected, onSelect }) => { + const setSelectedOptionCallback = useCallback( + (event: React.ChangeEvent) => { + onSelect(event.target.value as AlertsStackByField); + }, + [onSelect] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts similarity index 76% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts index 0b961cd8c7a13c..cb5a23e7119742 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { AlertsHistogramOption } from './types'; +import type { AlertsStackByOption } from './types'; -export const alertsHistogramOptions: AlertsHistogramOption[] = [ +export const alertsStackByOptions: AlertsStackByOption[] = [ { text: 'signal.rule.risk_score', value: 'signal.rule.risk_score' }, { text: 'signal.rule.severity', value: 'signal.rule.severity' }, { text: 'signal.rule.threat.tactic.name', value: 'signal.rule.threat.tactic.name' }, @@ -20,3 +20,9 @@ export const alertsHistogramOptions: AlertsHistogramOption[] = [ { text: 'source.ip', value: 'source.ip' }, { text: 'user.name', value: 'user.name' }, ]; + +export const DEFAULT_STACK_BY_FIELD = 'signal.rule.name'; + +export const PANEL_HEIGHT = 300; +export const MOBILE_PANEL_HEIGHT = 500; +export const CHART_HEIGHT = 200; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts new file mode 100644 index 00000000000000..ecc7cc01977781 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { showAllOthersBucket } from '../../../../../common/constants'; +import type { AlertsStackByField } from './types'; +import * as i18n from './translations'; + +export const MISSING_IP = '0.0.0.0'; + +export const getMissingFields = (stackByField: AlertsStackByField) => + showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField.endsWith('.ip') ? MISSING_IP : i18n.ALL_OTHERS, + } + : {}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx new file mode 100644 index 00000000000000..ad0fc1fa7ac61d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useInspectButton, UseInspectButtonParams } from './hooks'; + +describe('hooks', () => { + describe('useInspectButton', () => { + const defaultParams: UseInspectButtonParams = { + setQuery: jest.fn(), + response: '', + request: '', + refetch: jest.fn(), + uniqueQueryId: 'test-uniqueQueryId', + deleteQuery: jest.fn(), + loading: false, + }; + + it('calls setQuery when rendering', () => { + const mockSetQuery = jest.fn(); + + renderHook(() => useInspectButton({ ...defaultParams, setQuery: mockSetQuery })); + + expect(mockSetQuery).toHaveBeenCalledWith( + expect.objectContaining({ + id: defaultParams.uniqueQueryId, + }) + ); + }); + + it('calls deleteQuery when unmounting', () => { + const mockDeleteQuery = jest.fn(); + + const result = renderHook(() => + useInspectButton({ ...defaultParams, deleteQuery: mockDeleteQuery }) + ); + result.unmount(); + + expect(mockDeleteQuery).toHaveBeenCalledWith({ id: defaultParams.uniqueQueryId }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts new file mode 100644 index 00000000000000..6375e2b0c27fb3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import type { GlobalTimeArgs } from '../../../../common/containers/use_global_time'; + +export interface UseInspectButtonParams extends Pick { + response: string; + request: string; + refetch: (() => void) | null; + uniqueQueryId: string; + loading: boolean; +} +/** + * * Add query to inspect button utility. + * * Delete query from inspect button utility when component unmounts + */ +export const useInspectButton = ({ + setQuery, + response, + request, + refetch, + uniqueQueryId, + deleteQuery, + loading, +}: UseInspectButtonParams) => { + useEffect(() => { + if (refetch != null && setQuery != null) { + setQuery({ + id: uniqueQueryId, + inspect: { + dsl: [request], + response: [response], + }, + loading, + refetch, + }); + } + + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery]); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts new file mode 100644 index 00000000000000..ef540e088877c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const STACK_BY_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel', + { + defaultMessage: 'Stack by', + } +); + +export const ALL_OTHERS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts new file mode 100644 index 00000000000000..833c05bfc7a79b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface AlertsStackByOption { + text: AlertsStackByField; + value: AlertsStackByField; +} + +export type AlertsStackByField = + | 'signal.rule.risk_score' + | 'signal.rule.severity' + | 'signal.rule.threat.tactic.name' + | 'destination.ip' + | 'event.action' + | 'event.category' + | 'host.name' + | 'signal.rule.type' + | 'signal.rule.name' + | 'source.ip' + | 'user.name'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index f52b09e2d62b45..035784b2e27a49 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; @@ -26,8 +26,7 @@ import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions import { useAlertInfo } from '../../components/alerts_info'; import { AlertsTable } from '../../components/alerts_table'; import { NoApiIntegrationKeyCallOut } from '../../components/callouts/no_api_integration_callout'; -import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; -import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; +import { AlertsHistogramPanel } from '../../components/alerts_kpis/alerts_histogram_panel'; import { useUserData } from '../../components/user_info'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; @@ -57,6 +56,8 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { NeedAdminForUpdateRulesCallOut } from '../../components/callouts/need_admin_for_update_callout'; import { MissingPrivilegesCallOut } from '../../components/callouts/missing_privileges_callout'; import { useKibana } from '../../../common/lib/kibana'; +import { AlertsCountPanel } from '../../components/alerts_kpis/alerts_count_panel'; +import { CHART_HEIGHT } from '../../components/alerts_kpis/common/config'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -84,7 +85,7 @@ const DetectionEnginePageComponent = () => { // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); - const { to, from, deleteQuery, setQuery } = useGlobalTime(); + const { to, from } = useGlobalTime(); const { globalFullScreen } = useGlobalFullScreen(); const [ { @@ -250,18 +251,28 @@ const DetectionEnginePageComponent = () => { {i18n.BUTTON_MANAGE_RULES} - + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 8770e59e0c1788..233189a3e8be9a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -54,8 +54,7 @@ import { useListsConfig } from '../../../../containers/detection_engine/lists/us import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; -import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; -import { AlertsHistogramOption } from '../../../../components/alerts_histogram_panel/types'; +import { AlertsHistogramPanel } from '../../../../components/alerts_kpis/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; import { useUserData } from '../../../../components/user_info'; import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; @@ -72,7 +71,6 @@ import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { StepPanel } from '../../../../components/rules/step_panel'; import { getStepsData, redirectToDetections, userHasPermissions } from '../helpers'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; -import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_overflow'; @@ -119,6 +117,7 @@ import { getRuleStatusText } from '../../../../../../common/detection_engine/uti import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; import { BadgeOptions } from '../../../../../common/components/header_page/types'; +import { AlertsStackByField } from '../../../../components/alerts_kpis/common/types'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -173,7 +172,7 @@ const RuleDetailsPageComponent = () => { const query = useDeepEqualSelector(getGlobalQuerySelector); const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); - const { to, from, deleteQuery, setQuery } = useGlobalTime(); + const { to, from } = useGlobalTime(); const [ { loading: userInfoLoading, @@ -572,10 +571,7 @@ const RuleDetailsPageComponent = () => { return null; } - const defaultRuleStackByOption: AlertsHistogramOption = { - text: 'event.category', - value: 'event.category', - }; + const defaultRuleStackByOption: AlertsStackByField = 'event.category'; return ( <> @@ -711,15 +707,10 @@ const RuleDetailsPageComponent = () => { <> diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index a7823a1a6b98d2..76116f2261118c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -8,22 +8,21 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { AlertsHistogramPanel } from '../../../detections/components/alerts_histogram_panel'; -import { alertsHistogramOptions } from '../../../detections/components/alerts_histogram_panel/config'; +import { AlertsHistogramPanel } from '../../../detections/components/alerts_kpis/alerts_histogram_panel'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { Filter, Query } from '../../../../../../../src/plugins/data/public'; import { InputsModelId } from '../../../common/store/inputs/constants'; import * as i18n from '../../pages/translations'; import { UpdateDateRange } from '../../../common/components/charts/common'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; -interface Props extends Pick { +interface Props { combinedQueries?: string; filters?: Filter[]; headerChildren?: React.ReactNode; /** Override all defaults, and only display this field */ - onlyField?: string; + onlyField?: AlertsStackByField; query?: Query; setAbsoluteRangeDatePickerTarget?: InputsModelId; timelineId?: string; @@ -31,16 +30,12 @@ interface Props extends Pick = ({ combinedQueries, - deleteQuery, filters, - from, headerChildren, onlyField, query, setAbsoluteRangeDatePickerTarget = 'global', - setQuery, timelineId, - to, }) => { const dispatch = useDispatch(); const { signalIndexName } = useSignalIndex(); @@ -64,20 +59,17 @@ const SignalsByCategoryComponent: React.FC = ({ return ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index ed12dce6db482f..9c0b1ea87e1f97 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -96,13 +96,7 @@ const OverviewComponent = () => { - + diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e8a997e9361bf5..72b3e14bd55bee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20507,17 +20507,7 @@ "xpack.securitySolution.detectionEngine.alerts.documentTypeTitle": "アラート", "xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel": "その他すべて", "xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle": "傾向", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown": "上位のデスティネーションIP", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown": "上位のイベントアクション", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown": "上位のイベントカテゴリー", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown": "上位のホスト名", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown": "リスクスコア", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown": "上位のルール", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown": "上位のルールタイプ", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown": "重要度", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown": "上位のソースIP", "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel": "積み上げ", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown": "上位のユーザー", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "トップ{fieldName}", "xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel": "アラートを表示", "xpack.securitySolution.detectionEngine.alerts.inProgressAlertFailedToastMessage": "アラートを実行中に設定できませんでした", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2dae16efd76b00..aad31db65b19a4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7504,12 +7504,190 @@ "expressionRevealImage.functions.revealImageHelpText": "配置图像显示元素。", "expressionShape.renderer.shape.displayName": "形状", "expressionShape.renderer.shape.helpDescription": "呈现基本形状", + "expressionShape.functions.shape.args.borderHelpText": "形状轮廓边框的 {SVG} 颜色。", + "expressionShape.functions.shape.args.borderWidthHelpText": "边框的粗细。", + "expressionShape.functions.shape.args.fillHelpText": "填充形状的 {SVG} 颜色。", + "expressionShape.functions.shape.args.maintainAspectHelpText": "维持形状的原始纵横比?", + "expressionShape.functions.shape.args.shapeHelpText": "选取形状。", + "expressionShape.functions.shapeHelpText": "创建形状。", "expressionError.errorComponent.description": "表达式失败,并显示消息:", "expressionError.errorComponent.title": "哎哟!表达式失败", "expressionError.renderer.debug.displayName": "故障排查", "expressionError.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", "expressionError.renderer.error.displayName": "错误信息", "expressionError.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", + "xpack.cases.addConnector.title": "添加连接器", + "xpack.cases.allCases.actions": "操作", + "xpack.cases.allCases.comments": "注释", + "xpack.cases.allCases.noTagsAvailable": "没有可用标记", + "xpack.cases.caseTable.addNewCase": "添加新案例", + "xpack.cases.caseTable.bulkActions": "批处理操作", + "xpack.cases.caseTable.bulkActions.closeSelectedTitle": "关闭所选", + "xpack.cases.caseTable.bulkActions.deleteSelectedTitle": "删除所选", + "xpack.cases.caseTable.bulkActions.markInProgressTitle": "标记为进行中", + "xpack.cases.caseTable.bulkActions.openSelectedTitle": "打开所选", + "xpack.cases.caseTable.caseDetailsLinkAria": "单击以访问标题为 {detailName} 的案例", + "xpack.cases.caseTable.changeStatus": "更改状态", + "xpack.cases.caseTable.closed": "已关闭", + "xpack.cases.caseTable.closedCases": "已关闭案例", + "xpack.cases.caseTable.delete": "删除", + "xpack.cases.caseTable.incidentSystem": "事件管理系统", + "xpack.cases.caseTable.inProgressCases": "进行中的案例", + "xpack.cases.caseTable.noCases.body": "没有可显示的案例。请创建新案例或在上面更改您的筛选设置。", + "xpack.cases.caseTable.noCases.readonly.body": "没有可显示的案例。请在上面更改您的筛选设置。", + "xpack.cases.caseTable.noCases.title": "无案例", + "xpack.cases.caseTable.notPushed": "未推送", + "xpack.cases.caseTable.openCases": "未结案例", + "xpack.cases.caseTable.pushLinkAria": "单击可在 { thirdPartyName } 上查看该事件。", + "xpack.cases.caseTable.refreshTitle": "刷新", + "xpack.cases.caseTable.requiresUpdate": " 需要更新", + "xpack.cases.caseTable.searchAriaLabel": "搜索案例", + "xpack.cases.caseTable.searchPlaceholder": "例如案例名", + "xpack.cases.caseTable.selectedCasesTitle": "已选择 {totalRules} 个{totalRules, plural, other {案例}}", + "xpack.cases.caseTable.showingCasesTitle": "正在显示 {totalRules} 个{totalRules, plural, other {案例}}", + "xpack.cases.caseTable.snIncident": "外部事件", + "xpack.cases.caseTable.status": "状态", + "xpack.cases.caseTable.unit": "{totalCount, plural, other {案例}}", + "xpack.cases.caseTable.upToDate": " 是最新的", + "xpack.cases.caseView.actionLabel.addDescription": "添加了描述", + "xpack.cases.caseView.actionLabel.addedField": "添加了", + "xpack.cases.caseView.actionLabel.changededField": "更改了", + "xpack.cases.caseView.actionLabel.editedField": "编辑了", + "xpack.cases.caseView.actionLabel.on": "在", + "xpack.cases.caseView.actionLabel.pushedNewIncident": "已推送为新事件", + "xpack.cases.caseView.actionLabel.removedField": "移除了", + "xpack.cases.caseView.actionLabel.removedThirdParty": "已移除外部事件管理系统", + "xpack.cases.caseView.actionLabel.selectedThirdParty": "已选择 { thirdParty } 作为事件管理系统", + "xpack.cases.caseView.actionLabel.updateIncident": "更新了事件", + "xpack.cases.caseView.actionLabel.viewIncident": "查看 {incidentNumber}", + "xpack.cases.caseView.alertCommentLabelTitle": "添加了告警,从", + "xpack.cases.caseView.alreadyPushedToExternalService": "已推送到 { externalService } 事件", + "xpack.cases.caseView.backLabel": "返回到案例", + "xpack.cases.caseView.cancel": "取消", + "xpack.cases.caseView.case": "案例", + "xpack.cases.caseView.caseClosed": "案例已关闭", + "xpack.cases.caseView.caseInProgress": "案例进行中", + "xpack.cases.caseView.caseName": "案例名称", + "xpack.cases.caseView.caseOpened": "案例已打开", + "xpack.cases.caseView.caseRefresh": "刷新案例", + "xpack.cases.caseView.closeCase": "关闭案例", + "xpack.cases.caseView.closedOn": "关闭日期", + "xpack.cases.caseView.cloudDeploymentLink": "云部署", + "xpack.cases.caseView.comment": "注释", + "xpack.cases.caseView.comment.addComment": "添加注释", + "xpack.cases.caseView.comment.addCommentHelpText": "添加新注释......", + "xpack.cases.caseView.commentFieldRequiredError": "注释必填。", + "xpack.cases.caseView.connectors": "外部事件管理系统", + "xpack.cases.caseView.copyCommentLinkAria": "复制参考链接", + "xpack.cases.caseView.create": "创建新案例", + "xpack.cases.caseView.createCase": "创建案例", + "xpack.cases.caseView.description": "描述", + "xpack.cases.caseView.description.save": "保存", + "xpack.cases.caseView.doesNotExist.button": "返回到案例", + "xpack.cases.caseView.doesNotExist.description": "找不到 ID 为 {caseId} 的案例。这很可能意味着案例已删除或 ID 不正确。", + "xpack.cases.caseView.doesNotExist.title": "此案例不存在", + "xpack.cases.caseView.edit": "编辑", + "xpack.cases.caseView.edit.comment": "编辑注释", + "xpack.cases.caseView.edit.description": "编辑描述", + "xpack.cases.caseView.edit.quote": "引述", + "xpack.cases.caseView.editActionsLinkAria": "单击可查看所有操作", + "xpack.cases.caseView.editTagsLinkAria": "单击可编辑标签", + "xpack.cases.caseView.emailBody": "案例参考:{caseUrl}", + "xpack.cases.caseView.emailSubject": "Security 案例 - {caseTitle}", + "xpack.cases.caseView.errorsPushServiceCallOutTitle": "选择外部连接器", + "xpack.cases.caseView.fieldChanged": "已更改连接器字段", + "xpack.cases.caseView.fieldRequiredError": "必填字段", + "xpack.cases.caseView.generatedAlertCommentLabelTitle": "添加自", + "xpack.cases.caseView.generatedAlertCountCommentLabelTitle": "{totalCount} 个{totalCount, plural, other {告警}}", + "xpack.cases.caseView.isolatedHost": "已隔离主机", + "xpack.cases.caseView.lockedIncidentDesc": "不需要任何更新", + "xpack.cases.caseView.lockedIncidentTitle": "{ thirdParty } 事件是最新的", + "xpack.cases.caseView.lockedIncidentTitleNone": "外部事件是最新的", + "xpack.cases.caseView.markedCaseAs": "将案例标记为", + "xpack.cases.caseView.markInProgress": "标记为进行中", + "xpack.cases.caseView.moveToCommentAria": "高亮显示引用的注释", + "xpack.cases.caseView.name": "名称", + "xpack.cases.caseView.noReportersAvailable": "没有报告者。", + "xpack.cases.caseView.noTags": "当前没有为此案例分配标签。", + "xpack.cases.caseView.openCase": "创建案例", + "xpack.cases.caseView.openedOn": "打开时间", + "xpack.cases.caseView.optional": "可选", + "xpack.cases.caseView.otherEndpoints": " 以及{endpoints, plural, other {其他}} {endpoints} 个", + "xpack.cases.caseView.particpantsLabel": "参与者", + "xpack.cases.caseView.pushNamedIncident": "推送为 { thirdParty } 事件", + "xpack.cases.caseView.pushThirdPartyIncident": "推送为外部事件", + "xpack.cases.caseView.pushToService.configureConnector": "要在外部系统中打开和更新案例,必须为此案例选择外部事件管理系统。", + "xpack.cases.caseView.pushToServiceDisableBecauseCaseClosedDescription": "关闭的案例无法发送到外部系统。如果希望在外部系统中打开或更新案例,请重新打开案例。", + "xpack.cases.caseView.pushToServiceDisableBecauseCaseClosedTitle": "重新打开案例", + "xpack.cases.caseView.pushToServiceDisableByConfigDescription": "kibana.yml 文件已配置为仅允许特定连接器。要在外部系统中打开案例,请将 .[actionTypeId](例如:.servicenow | .jira)添加到 xpack.actions.enabledActiontypes 设置。有关更多信息,请参阅{link}。", + "xpack.cases.caseView.pushToServiceDisableByConfigTitle": "在 Kibana 配置文件中启用外部服务", + "xpack.cases.caseView.pushToServiceDisableByLicenseDescription": "有{appropriateLicense}、正使用{cloud}或正在免费试用时,可在外部系统中创建案例。", + "xpack.cases.caseView.pushToServiceDisableByLicenseTitle": "升级适当的许可", + "xpack.cases.caseView.releasedHost": "已释放主机", + "xpack.cases.caseView.reopenCase": "重新打开案例", + "xpack.cases.caseView.reporterLabel": "报告者", + "xpack.cases.caseView.requiredUpdateToExternalService": "需要更新 { externalService } 事件", + "xpack.cases.caseView.sendEmalLinkAria": "单击可向 {user} 发送电子邮件", + "xpack.cases.caseView.showAlertTooltip": "显示告警详情", + "xpack.cases.caseView.statusLabel": "状态", + "xpack.cases.caseView.syncAlertsLabel": "同步告警", + "xpack.cases.caseView.tags": "标签", + "xpack.cases.caseView.to": "到", + "xpack.cases.caseView.unknown": "未知", + "xpack.cases.caseView.unknownRule.label": "未知规则", + "xpack.cases.caseView.updateNamedIncident": "更新 { thirdParty } 事件", + "xpack.cases.caseView.updateThirdPartyIncident": "更新外部事件", + "xpack.cases.common.alertAddedToCase": "已添加到案例", + "xpack.cases.common.alertLabel": "告警", + "xpack.cases.common.alertsLabel": "告警", + "xpack.cases.common.allCases.caseModal.title": "选择案例", + "xpack.cases.common.allCases.table.selectableMessageCollections": "无法选择具有子案例的案例", + "xpack.cases.common.noConnector": "未选择任何连接器", + "xpack.cases.components.connectors.cases.actionTypeTitle": "案例", + "xpack.cases.components.connectors.cases.addNewCaseOption": "添加新案例", + "xpack.cases.components.connectors.cases.callOutMsg": "案例可以包含多个子案例以允许分组生成的告警。子案例将为这些已生成告警的状态提供更精细的控制,从而防止在一个案例上附加过多的告警。", + "xpack.cases.components.connectors.cases.callOutTitle": "已生成告警将附加到子案例", + "xpack.cases.components.connectors.cases.caseRequired": "必须选择策略。", + "xpack.cases.components.connectors.cases.casesDropdownRowLabel": "允许有子案例的案例", + "xpack.cases.components.connectors.cases.createCaseLabel": "创建案例", + "xpack.cases.components.connectors.cases.optionAddToExistingCase": "添加到现有案例", + "xpack.cases.components.connectors.cases.selectMessageText": "创建或更新案例。", + "xpack.cases.components.create.syncAlertHelpText": "启用此选项将使本案例中的告警状态与案例状态同步。", + "xpack.cases.configure.readPermissionsErrorDescription": "您无权查看连接器。如果要查看与此案例关联的连接器,请联系Kibana 管理员。", + "xpack.cases.configure.successSaveToast": "已保存外部连接设置", + "xpack.cases.configureCases.addNewConnector": "添加新连接器", + "xpack.cases.configureCases.cancelButton": "取消", + "xpack.cases.configureCases.caseClosureOptionsDesc": "定义如何关闭案例。要自动关闭,需要与外部事件管理系统建立连接。", + "xpack.cases.configureCases.caseClosureOptionsLabel": "案例关闭选项", + "xpack.cases.configureCases.caseClosureOptionsManual": "手动关闭案例", + "xpack.cases.configureCases.caseClosureOptionsNewIncident": "将新事件推送到外部系统时自动关闭案例", + "xpack.cases.configureCases.caseClosureOptionsSubCases": "不支持自动关闭子案例。", + "xpack.cases.configureCases.caseClosureOptionsTitle": "案例关闭", + "xpack.cases.configureCases.commentMapping": "注释", + "xpack.cases.configureCases.fieldMappingDesc": "将数据推送到 { thirdPartyName } 时,将案例字段映射到 { thirdPartyName } 字段。字段映射需要与 { thirdPartyName } 建立连接。", + "xpack.cases.configureCases.fieldMappingDescErr": "无法检索 { thirdPartyName } 的映射。", + "xpack.cases.configureCases.fieldMappingEditAppend": "追加", + "xpack.cases.configureCases.fieldMappingFirstCol": "Kibana 案例字段", + "xpack.cases.configureCases.fieldMappingSecondCol": "{ thirdPartyName } 字段", + "xpack.cases.configureCases.fieldMappingThirdCol": "编辑和更新时", + "xpack.cases.configureCases.fieldMappingTitle": "{ thirdPartyName } 字段映射", + "xpack.cases.configureCases.headerTitle": "配置案例", + "xpack.cases.configureCases.incidentManagementSystemDesc": "将您的案例连接到外部事件管理系统。然后,您便可以将案例数据推送为第三方系统中的事件。", + "xpack.cases.configureCases.incidentManagementSystemLabel": "事件管理系统", + "xpack.cases.configureCases.incidentManagementSystemTitle": "外部事件管理系统", + "xpack.cases.configureCases.requiredMappings": "至少有一个案例字段需要映射到以下所需的 { connectorName } 字段:{ fields }", + "xpack.cases.configureCases.saveAndCloseButton": "保存并关闭", + "xpack.cases.configureCases.saveButton": "保存", + "xpack.cases.configureCases.updateConnector": "更新字段映射", + "xpack.cases.configureCases.updateSelectedConnector": "更新 { connectorName }", + "xpack.cases.configureCases.warningTitle": "警告", + "xpack.cases.configureCasesButton": "编辑外部连接", + "xpack.cases.confirmDeleteCase.confirmQuestion": "删除{quantity, plural, =1 {此案例} other {这些案例}}即会永久移除所有相关案例数据,而且您将无法再将数据推送到外部事件管理系统。是否确定要继续?", + "xpack.cases.confirmDeleteCase.deleteCase": "删除{quantity, plural, other {案例}}", + "xpack.cases.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", + "xpack.cases.confirmDeleteCase.selectedCases": "删除“{quantity, plural, =1 {{title}} other {选定的 {quantity} 个案例}}”", + "xpack.cases.connecors.get.missingCaseConnectorErrorMessage": "对象类型“{id}”未注册。", + "xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage": "已注册对象类型“{id}”。", "xpack.cases.connectors.cases.externalIncidentAdded": " (由 {user} 于 {date}添加) ", "xpack.cases.connectors.cases.externalIncidentCreated": " (由 {user} 于 {date}创建) ", "xpack.cases.connectors.cases.externalIncidentDefault": " (由 {user} 于 {date}创建) ", @@ -20792,17 +20970,7 @@ "xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel": "所有其他", "xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle": "趋势", "xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle": "正在显示:{modifier}{totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown": "排名靠前的目标 IP", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown": "排名靠前的事件操作", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown": "排名靠前的事件类别", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown": "排名靠前的主机名", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown": "风险分数", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown": "排名靠前的规则", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown": "排名靠前的规则类型", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown": "严重性", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown": "排名靠前的源 IP", "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel": "堆叠依据", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown": "排名靠前的用户", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "排名靠前的{fieldName}", "xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel": "查看告警", "xpack.securitySolution.detectionEngine.alerts.inProgressAlertFailedToastMessage": "无法将告警标记为进行中",