From b24632da541dddbe659e23e7d43b8d626bc002cc Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 10 Jul 2020 15:05:07 +0200 Subject: [PATCH 01/10] [Security Solution] Allow to configure Event Renderers settings (#69693) --- .../common/types/timeline/index.ts | 23 +- .../security_solution/common/utility_types.ts | 11 + .../cypress/screens/hosts/events.ts | 2 +- .../alerts_viewer/default_headers.ts | 3 +- .../drag_and_drop/draggable_wrapper.tsx | 334 ++++--- .../common/components/draggables/index.tsx | 130 +-- .../events_viewer/events_viewer.test.tsx | 2 +- .../common/components/events_viewer/index.tsx | 19 +- .../public/common/mock/global_state.ts | 2 +- .../public/common/mock/mock_timeline_data.ts | 28 +- .../public/common/mock/timeline_results.ts | 4 +- .../components/alerts_table/actions.test.tsx | 2 +- .../alerts_table/alerts_utility_bar/index.tsx | 8 +- .../alerts_table/default_config.tsx | 3 +- .../public/detections/index.ts | 4 +- .../public/graphql/introspection.json | 99 ++ .../security_solution/public/graphql/types.ts | 24 + .../source_destination_arrows.tsx | 4 + .../components/fields_browser/index.test.tsx | 58 +- .../components/fields_browser/index.tsx | 331 +++---- .../components/flyout/pane/index.tsx | 1 + .../components/open_timeline/helpers.test.ts | 8 +- .../components/open_timeline/types.ts | 2 + .../row_renderers_browser/catalog/index.tsx | 199 ++++ .../catalog/translations.ts | 215 +++++ .../row_renderers_browser/constants.ts | 7 + .../row_renderers_browser/examples/auditd.tsx | 30 + .../examples/auditd_file.tsx | 30 + .../row_renderers_browser/examples/index.tsx | 18 + .../examples/netflow.tsx | 22 + .../examples/suricata.tsx | 22 + .../row_renderers_browser/examples/system.tsx | 30 + .../examples/system_dns.tsx | 26 + .../examples/system_endgame_process.tsx | 30 + .../examples/system_file.tsx | 30 + .../examples/system_fim.tsx | 30 + .../examples/system_security_event.tsx | 28 + .../examples/system_socket.tsx | 29 + .../row_renderers_browser/examples/zeek.tsx | 22 + .../row_renderers_browser/index.tsx | 182 ++++ .../row_renderers_browser.tsx | 179 ++++ .../row_renderers_browser/translations.ts | 43 + .../timeline/body/actions/index.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 906 +++++++++--------- .../body/column_headers/helpers.test.ts | 6 +- .../timeline/body/column_headers/helpers.ts | 16 +- .../timeline/body/column_headers/index.tsx | 39 +- .../components/timeline/body/constants.ts | 3 + .../body/events/event_column_view.tsx | 70 +- .../generic_file_details.test.tsx.snap | 4 +- .../generic_row_renderer.test.tsx.snap | 10 +- .../auditd/generic_row_renderer.test.tsx | 8 +- .../renderers/auditd/generic_row_renderer.tsx | 4 + .../netflow/netflow_row_renderer.tsx | 2 + .../body/renderers/plain_row_renderer.tsx | 11 +- .../timeline/body/renderers/row_renderer.tsx | 2 + .../suricata_signature.test.tsx.snap | 1 - .../suricata/suricata_row_renderer.tsx | 3 + .../renderers/suricata/suricata_signature.tsx | 2 - .../renderers/system/generic_row_renderer.tsx | 9 + .../body/renderers/zeek/zeek_row_renderer.tsx | 3 + .../body/renderers/zeek/zeek_signature.tsx | 3 - .../timeline/body/stateful_body.tsx | 59 +- .../data_providers/provider_item_badge.tsx | 2 +- .../timelines/components/timeline/styles.tsx | 24 +- .../containers/all/index.gql_query.ts | 1 + .../public/timelines/containers/all/index.tsx | 1 + .../containers/one/index.gql_query.ts | 1 + .../timelines/store/timeline/actions.ts | 9 +- .../timelines/store/timeline/defaults.ts | 2 +- .../timelines/store/timeline/epic.test.ts | 3 +- .../public/timelines/store/timeline/epic.ts | 1 + .../store/timeline/epic_local_storage.ts | 2 + .../timelines/store/timeline/helpers.ts | 34 +- .../public/timelines/store/timeline/model.ts | 7 +- .../timelines/store/timeline/reducer.test.ts | 20 +- .../timelines/store/timeline/reducer.ts | 14 +- .../server/graphql/timeline/schema.gql.ts | 18 + .../security_solution/server/graphql/types.ts | 31 + .../lib/timeline/pick_saved_timeline.ts | 2 + .../timeline/routes/utils/export_timelines.ts | 2 +- .../lib/timeline/saved_object_mappings.ts | 3 + 82 files changed, 2544 insertions(+), 1074 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 90d254b15e8b3..9e7a6f46bbcec 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/camelcase, @typescript-eslint/no-empty-interface */ import * as runtimeTypes from 'io-ts'; import { SavedObjectsClient } from 'kibana/server'; -import { unionWithNullType } from '../../utility_types'; +import { stringEnum, unionWithNullType } from '../../utility_types'; import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event'; @@ -164,6 +164,24 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< typeof TimelineStatusLiteralWithNullRt >; +export enum RowRendererId { + auditd = 'auditd', + auditd_file = 'auditd_file', + netflow = 'netflow', + plain = 'plain', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fim = 'system_fim', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + zeek = 'zeek', +} + +export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId'); + /** * Timeline template type */ @@ -211,6 +229,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), eventType: unionWithNullType(runtimeTypes.string), + excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), kqlMode: unionWithNullType(runtimeTypes.string), diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index a12dd926a9181..43271dc40ba12 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -15,3 +15,14 @@ export interface DescriptionList { export const unionWithNullType = (type: T) => runtimeTypes.union([type, runtimeTypes.null]); + +export const stringEnum = (enumObj: T, enumName = 'enum') => + new runtimeTypes.Type( + enumName, + (u): u is T[keyof T] => Object.values(enumObj).includes(u), + (u, c) => + Object.values(enumObj).includes(u) + ? runtimeTypes.success(u as T[keyof T]) + : runtimeTypes.failure(u, c), + (a) => (a as unknown) as string + ); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts index a946fefe273e1..4b1ca19bd96fe 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts @@ -7,7 +7,7 @@ export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]'; export const EVENTS_VIEWER_FIELDS_BUTTON = - '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser-gear"]'; + '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]'; export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts index cf5b565b99f67..ba4ecf9a33eee 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RowRendererId } from '../../../../common/types/timeline'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, @@ -69,5 +70,5 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ export const alertsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, columns: alertsHeaders, - showRowRenderers: false, + excludedRowRendererIds: Object.values(RowRendererId), }; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index e7594365e8103..64f6699d21dac 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -15,13 +15,13 @@ import { } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants'; + import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; - import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content'; import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; @@ -49,13 +49,27 @@ class DragDropErrorBoundary extends React.PureComponent { } } -const Wrapper = styled.div` +interface WrapperProps { + disabled: boolean; +} + +const Wrapper = styled.div` display: inline-block; max-width: 100%; [data-rbd-placeholder-context-id] { display: none !important; } + + ${({ disabled }) => + disabled && + ` + [data-rbd-draggable-id]:hover, + .euiBadge:hover, + .euiBadge__text:hover { + cursor: default; + } + `} `; Wrapper.displayName = 'Wrapper'; @@ -74,6 +88,7 @@ type RenderFunctionProp = ( interface Props { dataProvider: DataProvider; + disabled?: boolean; inline?: boolean; render: RenderFunctionProp; timelineId?: string; @@ -100,162 +115,169 @@ export const getStyle = ( }; }; -export const DraggableWrapper = React.memo( - ({ dataProvider, onFilterAdded, render, timelineId, truncate }) => { - const draggableRef = useRef(null); - const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); - const [showTopN, setShowTopN] = useState(false); - const [goGetTimelineId, setGoGetTimelineId] = useState(false); - const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); - const [providerRegistered, setProviderRegistered] = useState(false); - - const dispatch = useDispatch(); - - const handleClosePopOverTrigger = useCallback( - () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger), - [] - ); - - const toggleTopN = useCallback(() => { - setShowTopN((prevShowTopN) => { - const newShowTopN = !prevShowTopN; - if (newShowTopN === false) { - handleClosePopOverTrigger(); - } - return newShowTopN; - }); - }, [handleClosePopOverTrigger]); - - const registerProvider = useCallback(() => { - if (!providerRegistered) { - dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); - setProviderRegistered(true); +const DraggableWrapperComponent: React.FC = ({ + dataProvider, + onFilterAdded, + render, + timelineId, + truncate, +}) => { + const draggableRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); + const [showTopN, setShowTopN] = useState(false); + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); + const [providerRegistered, setProviderRegistered] = useState(false); + const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`); + const dispatch = useDispatch(); + + const handleClosePopOverTrigger = useCallback( + () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger), + [] + ); + + const toggleTopN = useCallback(() => { + setShowTopN((prevShowTopN) => { + const newShowTopN = !prevShowTopN; + if (newShowTopN === false) { + handleClosePopOverTrigger(); } - }, [dispatch, providerRegistered, dataProvider]); - - const unRegisterProvider = useCallback( - () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), - [dispatch, dataProvider] - ); - - useEffect( - () => () => { - unRegisterProvider(); - }, - [unRegisterProvider] - ); - - const hoverContent = useMemo( - () => ( - - ), - [ - dataProvider, - handleClosePopOverTrigger, - onFilterAdded, - showTopN, - timelineId, - timelineIdFind, - toggleTopN, - ] - ); - - const renderContent = useCallback( - () => ( - - - ( - -
- - {render(dataProvider, provided, snapshot)} - -
-
- )} - > - {(droppableProvided) => ( -
- { + if (!isDisabled) { + dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); + setProviderRegistered(true); + } + }, [isDisabled, dispatch, dataProvider]); + + const unRegisterProvider = useCallback( + () => + providerRegistered && + dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), + [providerRegistered, dispatch, dataProvider.id] + ); + + useEffect( + () => () => { + unRegisterProvider(); + }, + [unRegisterProvider] + ); + + const hoverContent = useMemo( + () => ( + + ), + [ + dataProvider, + handleClosePopOverTrigger, + onFilterAdded, + showTopN, + timelineId, + timelineIdFind, + toggleTopN, + ] + ); + + const renderContent = useCallback( + () => ( + + + ( + +
+ - {(provided, snapshot) => ( - { - provided.innerRef(e); - draggableRef.current = e; - }} - data-test-subj="providerContainer" - isDragging={snapshot.isDragging} - registerProvider={registerProvider} - > - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - )} - - {droppableProvided.placeholder} + {render(dataProvider, provided, snapshot)} +
- )} -
-
-
- ), - [dataProvider, render, registerProvider, truncate] - ); - - return ( - - ); - }, - (prevProps, nextProps) => - deepEqual(prevProps.dataProvider, nextProps.dataProvider) && - prevProps.render !== nextProps.render && - prevProps.truncate === nextProps.truncate -); + + )} + > + {(droppableProvided) => ( +
+ + {(provided, snapshot) => ( + { + provided.innerRef(e); + draggableRef.current = e; + }} + data-test-subj="providerContainer" + isDragging={snapshot.isDragging} + registerProvider={registerProvider} + > + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + + )} + + {droppableProvided.placeholder} +
+ )} + + + + ), + [dataProvider, registerProvider, render, isDisabled, truncate] + ); + + if (isDisabled) return <>{renderContent()}; + + return ( + + ); +}; + +export const DraggableWrapper = React.memo(DraggableWrapperComponent); DraggableWrapper.displayName = 'DraggableWrapper'; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index 62a07550650aa..4dc3c6fcbe440 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -5,13 +5,16 @@ */ import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { getEmptyStringTag } from '../empty_value'; -import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { + DataProvider, + IS_OPERATOR, +} from '../../../timelines/components/timeline/data_providers/data_provider'; import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; export interface DefaultDraggableType { @@ -84,36 +87,48 @@ Content.displayName = 'Content'; * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ export const DefaultDraggable = React.memo( - ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => - value != null ? ( + ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => { + const dataProviderProp: DataProvider = useMemo( + () => ({ + and: [], + enabled: true, + id: escapeDataProviderId(id), + name: name ? name : value ?? '', + excluded: false, + kqlQuery: '', + queryMatch: { + field, + value: queryValue ? queryValue : value ?? '', + operator: IS_OPERATOR, + }, + }), + [field, id, name, queryValue, value] + ); + + const renderCallback = useCallback( + (dataProvider, _, snapshot) => + snapshot.isDragging ? ( + + + + ) : ( + + {children} + + ), + [children, field, tooltipContent, value] + ); + + if (value == null) return null; + + return ( - snapshot.isDragging ? ( - - - - ) : ( - - {children} - - ) - } + dataProvider={dataProviderProp} + render={renderCallback} timelineId={timelineId} /> - ) : null + ); + } ); DefaultDraggable.displayName = 'DefaultDraggable'; @@ -146,33 +161,34 @@ export type BadgeDraggableType = Omit & { * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DraggableBadge = React.memo( - ({ - contextId, - eventId, - field, - value, - iconType, - name, - color = 'hollow', - children, - tooltipContent, - queryValue, - }) => - value != null ? ( - - - {children ? children : value !== '' ? value : getEmptyStringTag()} - - - ) : null -); +const DraggableBadgeComponent: React.FC = ({ + contextId, + eventId, + field, + value, + iconType, + name, + color = 'hollow', + children, + tooltipContent, + queryValue, +}) => + value != null ? ( + + + {children ? children : value !== '' ? value : getEmptyStringTag()} + + + ) : null; + +DraggableBadgeComponent.displayName = 'DraggableBadgeComponent'; +export const DraggableBadge = React.memo(DraggableBadgeComponent); DraggableBadge.displayName = 'DraggableBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 2a079ce015f0d..38ca1176d1700 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -77,7 +77,7 @@ describe('EventsViewer', () => { await wait(); wrapper.update(); - expect(wrapper.find(`[data-test-subj="show-field-browser-gear"]`).first().exists()).toBe(true); + expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); }); test('it renders the footer containing the Load More button', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 02b3571421f67..b89d2b8c08625 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -45,6 +45,7 @@ const StatefulEventsViewerComponent: React.FC = ({ defaultIndices, deleteEventQuery, end, + excludedRowRendererIds, filters, headerFilterGroup, id, @@ -57,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC = ({ removeColumn, start, showCheckboxes, - showRowRenderers, sort, updateItemsPerPage, upsertColumn, @@ -69,7 +69,14 @@ const StatefulEventsViewerComponent: React.FC = ({ useEffect(() => { if (createTimeline != null) { - createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); + createTimeline({ + id, + columns, + excludedRowRendererIds, + sort, + itemsPerPage, + showCheckboxes, + }); } return () => { deleteEventQuery({ id, inputId: 'global' }); @@ -125,7 +132,7 @@ const StatefulEventsViewerComponent: React.FC = ({ onChangeItemsPerPage={onChangeItemsPerPage} query={query} start={start} - sort={sort!} + sort={sort} toggleColumn={toggleColumn} utilityBar={utilityBar} /> @@ -145,18 +152,19 @@ const makeMapStateToProps = () => { columns, dataProviders, deletedEventIds, + excludedRowRendererIds, itemsPerPage, itemsPerPageOptions, kqlMode, sort, showCheckboxes, - showRowRenderers, } = events; return { columns, dataProviders, deletedEventIds, + excludedRowRendererIds, filters: getGlobalFiltersQuerySelector(state), id, isLive: input.policy.kind === 'interval', @@ -166,7 +174,6 @@ const makeMapStateToProps = () => { query: getGlobalQuerySelector(state), sort, showCheckboxes, - showRowRenderers, }; }; return mapStateToProps; @@ -192,6 +199,7 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.columns, nextProps.columns) && deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && @@ -204,7 +212,6 @@ export const StatefulEventsViewer = connector( prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar ) diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 3d76416855e9e..89f100992e1b9 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -195,6 +195,7 @@ export const mockGlobalState: State = { dataProviders: [], description: '', eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -215,7 +216,6 @@ export const mockGlobalState: State = { }, selectedEventIds: {}, show: false, - showRowRenderers: true, showCheckboxes: false, pinnedEventIds: {}, pinnedEventsSaveObject: {}, diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index 7503062300d2d..9974842bff474 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -418,8 +418,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-03-07T05:06:51.000Z'] }, { field: 'host.name', value: ['zeek-franfurt'] }, - { field: 'source.ip', value: ['185.176.26.101'] }, - { field: 'destination.ip', value: ['207.154.238.205'] }, + { field: 'source.ip', value: ['192.168.26.101'] }, + { field: 'destination.ip', value: ['192.168.238.205'] }, ], ecs: { _id: '14', @@ -466,8 +466,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-03-07T00:51:28.000Z'] }, { field: 'host.name', value: ['suricata-zeek-singapore'] }, - { field: 'source.ip', value: ['206.189.35.240'] }, - { field: 'destination.ip', value: ['67.207.67.3'] }, + { field: 'source.ip', value: ['192.168.35.240'] }, + { field: 'destination.ip', value: ['192.168.67.3'] }, ], ecs: { _id: '15', @@ -520,8 +520,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-03-05T07:00:20.000Z'] }, { field: 'host.name', value: ['suricata-zeek-singapore'] }, - { field: 'source.ip', value: ['206.189.35.240'] }, - { field: 'destination.ip', value: ['192.241.164.26'] }, + { field: 'source.ip', value: ['192.168.35.240'] }, + { field: 'destination.ip', value: ['192.168.164.26'] }, ], ecs: { _id: '16', @@ -572,7 +572,7 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-02-28T22:36:28.000Z'] }, { field: 'host.name', value: ['zeek-franfurt'] }, - { field: 'source.ip', value: ['8.42.77.171'] }, + { field: 'source.ip', value: ['192.168.77.171'] }, ], ecs: { _id: '17', @@ -621,8 +621,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-02-22T21:12:13.000Z'] }, { field: 'host.name', value: ['zeek-sensor-amsterdam'] }, - { field: 'source.ip', value: ['188.166.66.184'] }, - { field: 'destination.ip', value: ['91.189.95.15'] }, + { field: 'source.ip', value: ['192.168.66.184'] }, + { field: 'destination.ip', value: ['192.168.95.15'] }, ], ecs: { _id: '18', @@ -767,7 +767,7 @@ export const mockTimelineData: TimelineItem[] = [ { field: '@timestamp', value: ['2019-03-14T22:30:25.527Z'] }, { field: 'event.category', value: ['user-login'] }, { field: 'host.name', value: ['zeek-london'] }, - { field: 'source.ip', value: ['8.42.77.171'] }, + { field: 'source.ip', value: ['192.168.77.171'] }, { field: 'user.name', value: ['root'] }, ], ecs: { @@ -1101,7 +1101,7 @@ export const mockTimelineData: TimelineItem[] = [ { field: 'event.action', value: ['connected-to'] }, { field: 'event.category', value: ['audit-rule'] }, { field: 'host.name', value: ['zeek-london'] }, - { field: 'destination.ip', value: ['93.184.216.34'] }, + { field: 'destination.ip', value: ['192.168.216.34'] }, { field: 'user.name', value: ['alice'] }, ], ecs: { @@ -1121,7 +1121,7 @@ export const mockTimelineData: TimelineItem[] = [ data: null, summary: { actor: { primary: ['alice'], secondary: ['alice'] }, - object: { primary: ['93.184.216.34'], secondary: ['80'], type: ['socket'] }, + object: { primary: ['192.168.216.34'], secondary: ['80'], type: ['socket'] }, how: ['/usr/bin/wget'], message_type: null, sequence: null, @@ -1133,7 +1133,7 @@ export const mockTimelineData: TimelineItem[] = [ ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], }, source: null, - destination: { ip: ['93.184.216.34'], port: [80] }, + destination: { ip: ['192.168.216.34'], port: [80] }, geo: null, suricata: null, network: null, @@ -1174,7 +1174,7 @@ export const mockTimelineData: TimelineItem[] = [ }, auditd: { result: ['success'], - session: ['unset'], + session: ['242'], data: null, summary: { actor: { primary: ['unset'], secondary: ['root'] }, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 5248136437d7d..b1df41a19aebe 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2098,6 +2098,7 @@ export const mockTimelineModel: TimelineModel = { description: 'This is a sample rule description', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [ { $state: { @@ -2137,7 +2138,6 @@ export const mockTimelineModel: TimelineModel = { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc, @@ -2217,6 +2217,7 @@ export const defaultTimelineProps: CreateTimelineProps = { description: '', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -2241,7 +2242,6 @@ export const defaultTimelineProps: CreateTimelineProps = { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, status: TimelineStatus.draft, title: '', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 2fa7cfeedcd15..1213312e2a22c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -158,6 +158,7 @@ describe('alert actions', () => { description: 'This is a sample rule description', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [ { $state: { @@ -210,7 +211,6 @@ describe('alert actions', () => { selectedEventIds: {}, show: true, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 0ceb2c87dd5ea..6533be1a9b09c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -39,6 +39,10 @@ interface AlertsUtilityBarProps { updateAlertsStatus: UpdateAlertsStatus; } +const UtilityBarFlexGroup = styled(EuiFlexGroup)` + min-width: 175px; +`; + const AlertsUtilityBarComponent: React.FC = ({ canUserCRUD, hasIndexWrite, @@ -69,10 +73,6 @@ const AlertsUtilityBarComponent: React.FC = ({ defaultNumberFormat ); - const UtilityBarFlexGroup = styled(EuiFlexGroup)` - min-width: 175px; - `; - const UtilityBarPopoverContent = (closePopover: () => void) => ( {currentFilter !== FILTER_OPEN && ( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index e95ea4531d9ad..319575c9c307f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -11,6 +11,7 @@ import ApolloClient from 'apollo-client'; import { Dispatch } from 'redux'; import { EuiText } from '@elastic/eui'; +import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { @@ -162,7 +163,7 @@ export const alertsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, columns: alertsHeaders, showCheckboxes: true, - showRowRenderers: false, + excludedRowRendererIds: Object.values(RowRendererId), }; export const requiredFieldsForActions = [ diff --git a/x-pack/plugins/security_solution/public/detections/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts index d043127a3098b..30d1e30417583 100644 --- a/x-pack/plugins/security_solution/public/detections/index.ts +++ b/x-pack/plugins/security_solution/public/detections/index.ts @@ -10,7 +10,7 @@ import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; import { AlertsRoutes } from './routes'; import { SecuritySubPlugin } from '../app/types'; -const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [ +const DETECTIONS_TIMELINE_IDS: TimelineIdLiteral[] = [ TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsPage, ]; @@ -22,7 +22,7 @@ export class Detections { return { SubPluginRoutes: AlertsRoutes, storageTimelines: { - timelineById: getTimelinesInStorageByIds(storage, ALERTS_TIMELINE_IDS), + timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS), }, }; } diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 2b8b07cb6a24b..20978fa3b063c 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -9641,6 +9641,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "excludedRowRendererIds", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "favorite", "description": "", @@ -10146,6 +10162,75 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "RowRendererId", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "auditd", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "auditd_file", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "netflow", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "plain", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "suricata", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "system", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "system_dns", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_endgame_process", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_file", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_fim", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_security_event", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_socket", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "zeek", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "FavoriteTimelineResult", @@ -11061,6 +11146,20 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "excludedRowRendererIds", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null } + } + }, + "defaultValue": null + }, { "name": "filters", "description": "", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 2c8f2e63356e6..27aa02038097e 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -124,6 +124,8 @@ export interface TimelineInput { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + filters?: Maybe; kqlMode?: Maybe; @@ -349,6 +351,22 @@ export enum DataProviderType { template = 'template', } +export enum RowRendererId { + auditd = 'auditd', + auditd_file = 'auditd_file', + netflow = 'netflow', + plain = 'plain', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fim = 'system_fim', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + zeek = 'zeek', +} + export enum TimelineStatus { active = 'active', draft = 'draft', @@ -1961,6 +1979,8 @@ export interface TimelineResult { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + favorite?: Maybe; filters?: Maybe; @@ -4385,6 +4405,8 @@ export namespace GetAllTimeline { eventIdToNoteIds: Maybe; + excludedRowRendererIds: Maybe; + notes: Maybe; noteIds: Maybe; @@ -5454,6 +5476,8 @@ export namespace GetOneTimeline { eventIdToNoteIds: Maybe; + excludedRowRendererIds: Maybe; + favorite: Maybe; filters: Maybe; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx index 95cc76a349c17..73c5c1e37da0f 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx @@ -35,6 +35,10 @@ Percent.displayName = 'Percent'; const SourceDestinationArrowsContainer = styled(EuiFlexGroup)` margin: 0 2px; + + .euiToolTipAnchor { + white-space: nowrap; + } `; SourceDestinationArrowsContainer.displayName = 'SourceDestinationArrowsContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 92ef5c41f3b4c..ed3f957ad11a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -6,11 +6,9 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; @@ -29,17 +27,6 @@ afterAll(() => { console.warn = originalWarn; }); -const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{ - id: string; - columnId: string; -}>; - -const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>; - describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; @@ -54,13 +41,11 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().text()).toEqual('Columns'); + expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true); }); describe('toggleShow', () => { @@ -75,8 +60,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -95,8 +78,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -122,8 +103,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -149,8 +128,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -186,39 +163,14 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} - /> - - ); - - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true); - }); - - test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { - const isEventViewer = false; - - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); }); - test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { + test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { const isEventViewer = true; const wrapper = mount( @@ -232,12 +184,10 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index a3937107936b6..7b843b4f69447 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; -import { timelineActions } from '../../store/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; import { FieldsBrowser } from './field_browser'; @@ -34,181 +32,156 @@ FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer'; /** * Manages the state of the field browser */ -export const StatefulFieldsBrowserComponent = React.memo( - ({ - columnHeaders, - browserFields, - height, - isEventViewer = false, - onFieldSelected, - onUpdateColumns, - timelineId, - toggleColumn, - width, - }) => { - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - - /** all field names shown in the field browser must contain this string (when specified) */ - const [filterInput, setFilterInput] = useState(''); - /** all fields in this collection have field names that match the filterInput */ - const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); - /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ - const [isSearching, setIsSearching] = useState(false); - /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); - /** show the field browser */ - const [show, setShow] = useState(false); - useEffect(() => { - return () => { - if (inputTimeoutId.current !== 0) { - // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: - clearTimeout(inputTimeoutId.current); - inputTimeoutId.current = 0; - } - }; - }, []); - - /** Shows / hides the field browser */ - const toggleShow = useCallback(() => { - setShow(!show); - }, [show]); - - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback( - (newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: newFilterInput, - }); - setFilteredBrowserFields(newFilteredBrowserFields); - setIsSearching(false); - - const newSelectedCategoryId = - newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(newFilteredBrowserFields) - .sort() - .reduce( - (selected, category) => - newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(newFilteredBrowserFields[category].fields!).length > - Object.keys(newFilteredBrowserFields[selected].fields!).length - ? category - : selected, - Object.keys(newFilteredBrowserFields)[0] - ); - setSelectedCategoryId(newSelectedCategoryId); - }, INPUT_TIMEOUT); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [browserFields, filterInput, inputTimeoutId.current] - ); - - /** - * Invoked when the user clicks a category name in the left-hand side of - * the field browser - */ - const updateSelectedCategoryId = useCallback((categoryId: string) => { - setSelectedCategoryId(categoryId); - }, []); - - /** - * Invoked when the user clicks on the context menu to view a category's - * columns in the timeline, this function dispatches the action that - * causes the timeline display those columns. - */ - const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { - onUpdateColumns(columns); // show the category columns in the timeline - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - /** Invoked when the field browser should be hidden */ - const hideFieldBrowser = useCallback(() => { - setFilterInput(''); - setFilterInput(''); - setFilteredBrowserFields(null); - setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); - setShow(false); - }, []); - // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = useMemo( - () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), - [show, browserFields] - ); - - return ( - <> - - - {isEventViewer ? ( - - ) : ( - - {i18n.FIELDS} - - )} - - - {show && ( - - )} - - - ); - } -); - -const mapDispatchToProps = { - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, +export const StatefulFieldsBrowserComponent: React.FC = ({ + columnHeaders, + browserFields, + height, + isEventViewer = false, + onFieldSelected, + onUpdateColumns, + timelineId, + toggleColumn, + width, +}) => { + /** tracks the latest timeout id from `setTimeout`*/ + const inputTimeoutId = useRef(0); + + /** all field names shown in the field browser must contain this string (when specified) */ + const [filterInput, setFilterInput] = useState(''); + /** all fields in this collection have field names that match the filterInput */ + const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); + /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ + const [isSearching, setIsSearching] = useState(false); + /** this category will be displayed in the right-hand pane of the field browser */ + const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + /** show the field browser */ + const [show, setShow] = useState(false); + useEffect(() => { + return () => { + if (inputTimeoutId.current !== 0) { + // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: + clearTimeout(inputTimeoutId.current); + inputTimeoutId.current = 0; + } + }; + }, []); + + /** Shows / hides the field browser */ + const toggleShow = useCallback(() => { + setShow(!show); + }, [show]); + + /** Invoked when the user types in the filter input */ + const updateFilter = useCallback( + (newFilterInput: string) => { + setFilterInput(newFilterInput); + setIsSearching(true); + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: newFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + + const newSelectedCategoryId = + newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(newFilteredBrowserFields) + .sort() + .reduce( + (selected, category) => + newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + Object.keys(newFilteredBrowserFields[category].fields!).length > + Object.keys(newFilteredBrowserFields[selected].fields!).length + ? category + : selected, + Object.keys(newFilteredBrowserFields)[0] + ); + setSelectedCategoryId(newSelectedCategoryId); + }, INPUT_TIMEOUT); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [browserFields, filterInput, inputTimeoutId.current] + ); + + /** + * Invoked when the user clicks a category name in the left-hand side of + * the field browser + */ + const updateSelectedCategoryId = useCallback((categoryId: string) => { + setSelectedCategoryId(categoryId); + }, []); + + /** + * Invoked when the user clicks on the context menu to view a category's + * columns in the timeline, this function dispatches the action that + * causes the timeline display those columns. + */ + const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { + onUpdateColumns(columns); // show the category columns in the timeline + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** Invoked when the field browser should be hidden */ + const hideFieldBrowser = useCallback(() => { + setFilterInput(''); + setFilterInput(''); + setFilteredBrowserFields(null); + setIsSearching(false); + setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setShow(false); + }, []); + // only merge in the default category if the field browser is visible + const browserFieldsWithDefaultCategory = useMemo( + () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), + [show, browserFields] + ); + + return ( + + + + {i18n.FIELDS} + + + + {show && ( + + )} + + ); }; -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulFieldsBrowser = connector(React.memo(StatefulFieldsBrowserComponent)); +export const StatefulFieldsBrowser = React.memo(StatefulFieldsBrowserComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index fbe3c475c9fe6..8c03d82aafafb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -28,6 +28,7 @@ interface FlyoutPaneComponentProps { const EuiFlyoutContainer = styled.div` .timeline-flyout { + z-index: 4001; min-width: 150px; width: auto; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 31ac3240afb72..89a35fb838a96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -270,6 +270,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -294,7 +295,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', @@ -368,6 +368,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -392,7 +393,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', @@ -502,6 +502,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -532,7 +533,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', @@ -628,6 +628,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [ { $state: { @@ -701,7 +702,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index c21edaa916588..a8485328e8393 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -13,6 +13,7 @@ import { TimelineTypeLiteralWithNull, TimelineStatus, TemplateTimelineTypeLiteral, + RowRendererId, } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -46,6 +47,7 @@ export interface OpenTimelineResult { created?: number | null; description?: string | null; eventIdToNoteIds?: Readonly> | null; + excludedRowRendererIds?: RowRendererId[] | null; favorite?: FavoriteTimelineResult[] | null; noteIds?: string[] | null; notes?: TimelineResultNote[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx new file mode 100644 index 0000000000000..55d1694297e2e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -0,0 +1,199 @@ +/* + * 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'; +import { ExternalLinkIcon } from '../../../../common/components/external_link_icon'; + +import { RowRendererId } from '../../../../../common/types/timeline'; +import { + AuditdExample, + AuditdFileExample, + NetflowExample, + SuricataExample, + SystemExample, + SystemDnsExample, + SystemEndgameProcessExample, + SystemFileExample, + SystemFimExample, + SystemSecurityEventExample, + SystemSocketExample, + ZeekExample, +} from '../examples'; +import * as i18n from './translations'; + +const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ( + + {children} + + +); + +export interface RowRendererOption { + id: RowRendererId; + name: string; + description: React.ReactNode; + searchableDescription: string; + example: React.ReactNode; +} + +export const renderers: RowRendererOption[] = [ + { + id: RowRendererId.auditd, + name: i18n.AUDITD_NAME, + description: ( + + + {i18n.AUDITD_NAME} + {' '} + {i18n.AUDITD_DESCRIPTION_PART1} + + ), + example: AuditdExample, + searchableDescription: `${i18n.AUDITD_NAME} ${i18n.AUDITD_DESCRIPTION_PART1}`, + }, + { + id: RowRendererId.auditd_file, + name: i18n.AUDITD_FILE_NAME, + description: ( + + + {i18n.AUDITD_NAME} + {' '} + {i18n.AUDITD_FILE_DESCRIPTION_PART1} + + ), + example: AuditdFileExample, + searchableDescription: `${i18n.AUDITD_FILE_NAME} ${i18n.AUDITD_FILE_DESCRIPTION_PART1}`, + }, + { + id: RowRendererId.system_security_event, + name: i18n.AUTHENTICATION_NAME, + description: ( +
+

{i18n.AUTHENTICATION_DESCRIPTION_PART1}

+
+

{i18n.AUTHENTICATION_DESCRIPTION_PART2}

+
+ ), + example: SystemSecurityEventExample, + searchableDescription: `${i18n.AUTHENTICATION_DESCRIPTION_PART1} ${i18n.AUTHENTICATION_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.system_dns, + name: i18n.DNS_NAME, + description: i18n.DNS_DESCRIPTION_PART1, + example: SystemDnsExample, + searchableDescription: i18n.DNS_DESCRIPTION_PART1, + }, + { + id: RowRendererId.netflow, + name: i18n.FLOW_NAME, + description: ( +
+

{i18n.FLOW_DESCRIPTION_PART1}

+
+

{i18n.FLOW_DESCRIPTION_PART2}

+
+ ), + example: NetflowExample, + searchableDescription: `${i18n.FLOW_DESCRIPTION_PART1} ${i18n.FLOW_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.system, + name: i18n.SYSTEM_NAME, + description: ( +
+

+ {i18n.SYSTEM_DESCRIPTION_PART1}{' '} + + {i18n.SYSTEM_NAME} + {' '} + {i18n.SYSTEM_DESCRIPTION_PART2} +

+
+

{i18n.SYSTEM_DESCRIPTION_PART3}

+
+ ), + example: SystemExample, + searchableDescription: `${i18n.SYSTEM_DESCRIPTION_PART1} ${i18n.SYSTEM_NAME} ${i18n.SYSTEM_DESCRIPTION_PART2} ${i18n.SYSTEM_DESCRIPTION_PART3}`, + }, + { + id: RowRendererId.system_endgame_process, + name: i18n.PROCESS, + description: ( +
+

{i18n.PROCESS_DESCRIPTION_PART1}

+
+

{i18n.PROCESS_DESCRIPTION_PART2}

+
+ ), + example: SystemEndgameProcessExample, + searchableDescription: `${i18n.PROCESS_DESCRIPTION_PART1} ${i18n.PROCESS_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.system_fim, + name: i18n.FIM_NAME, + description: i18n.FIM_DESCRIPTION_PART1, + example: SystemFimExample, + searchableDescription: i18n.FIM_DESCRIPTION_PART1, + }, + { + id: RowRendererId.system_file, + name: i18n.FILE_NAME, + description: i18n.FILE_DESCRIPTION_PART1, + example: SystemFileExample, + searchableDescription: i18n.FILE_DESCRIPTION_PART1, + }, + { + id: RowRendererId.system_socket, + name: i18n.SOCKET_NAME, + description: ( +
+

{i18n.SOCKET_DESCRIPTION_PART1}

+
+

{i18n.SOCKET_DESCRIPTION_PART2}

+
+ ), + example: SystemSocketExample, + searchableDescription: `${i18n.SOCKET_DESCRIPTION_PART1} ${i18n.SOCKET_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.suricata, + name: 'Suricata', + description: ( +

+ {i18n.SURICATA_DESCRIPTION_PART1}{' '} + + {i18n.SURICATA_NAME} + {' '} + {i18n.SURICATA_DESCRIPTION_PART2} +

+ ), + example: SuricataExample, + searchableDescription: `${i18n.SURICATA_DESCRIPTION_PART1} ${i18n.SURICATA_NAME} ${i18n.SURICATA_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.zeek, + name: i18n.ZEEK_NAME, + description: ( +

+ {i18n.ZEEK_DESCRIPTION_PART1}{' '} + + {i18n.ZEEK_NAME} + {' '} + {i18n.ZEEK_DESCRIPTION_PART2} +

+ ), + example: ZeekExample, + searchableDescription: `${i18n.ZEEK_DESCRIPTION_PART1} ${i18n.ZEEK_NAME} ${i18n.ZEEK_DESCRIPTION_PART2}`, + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts new file mode 100644 index 0000000000000..f4d473cdfd3d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts @@ -0,0 +1,215 @@ +/* + * 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 AUDITD_NAME = i18n.translate('xpack.securitySolution.eventRenderers.auditdName', { + defaultMessage: 'Auditd', +}); + +export const AUDITD_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.auditdDescriptionPart1', + { + defaultMessage: 'audit events convey security-relevant logs from the Linux Audit Framework.', + } +); + +export const AUDITD_FILE_NAME = i18n.translate( + 'xpack.securitySolution.eventRenderers.auditdFileName', + { + defaultMessage: 'Auditd File', + } +); + +export const AUDITD_FILE_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.auditdFileDescriptionPart1', + { + defaultMessage: + 'File events show users (and system accounts) performing CRUD operations on files via specific processes.', + } +); + +export const AUTHENTICATION_NAME = i18n.translate( + 'xpack.securitySolution.eventRenderers.authenticationName', + { + defaultMessage: 'Authentication', + } +); + +export const AUTHENTICATION_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.authenticationDescriptionPart1', + { + defaultMessage: + 'Authentication events show users (and system accounts) successfully or unsuccessfully logging into hosts.', + } +); + +export const AUTHENTICATION_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.authenticationDescriptionPart2', + { + defaultMessage: + 'Some authentication events may include additional details when users authenticate on behalf of other users.', + } +); + +export const DNS_NAME = i18n.translate('xpack.securitySolution.eventRenderers.dnsName', { + defaultMessage: 'Domain Name System (DNS)', +}); + +export const DNS_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.dnsDescriptionPart1', + { + defaultMessage: + 'Domain Name System (DNS) events show users (and system accounts) making requests via specific processes to translate from host names to IP addresses.', + } +); + +export const FILE_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fileName', { + defaultMessage: 'File', +}); + +export const FILE_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.fileDescriptionPart1', + { + defaultMessage: + 'File events show users (and system accounts) performing CRUD operations on files via specific processes.', + } +); + +export const FIM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fimName', { + defaultMessage: 'File Integrity Module (FIM)', +}); + +export const FIM_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.fimDescriptionPart1', + { + defaultMessage: + 'File Integrity Module (FIM) events show users (and system accounts) performing CRUD operations on files via specific processes.', + } +); + +export const FLOW_NAME = i18n.translate('xpack.securitySolution.eventRenderers.flowName', { + defaultMessage: 'Flow', +}); + +export const FLOW_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.flowDescriptionPart1', + { + defaultMessage: + "The Flow renderer visualizes the flow of data between a source and destination. It's applicable to many types of events.", + } +); + +export const FLOW_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.flowDescriptionPart2', + { + defaultMessage: + 'The hosts, ports, protocol, direction, duration, amount transferred, process, geographic location, and other details are visualized when available.', + } +); + +export const PROCESS = i18n.translate('xpack.securitySolution.eventRenderers.processName', { + defaultMessage: 'Process', +}); + +export const PROCESS_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.processDescriptionPart1', + { + defaultMessage: + 'Process events show users (and system accounts) starting and stopping processes.', + } +); + +export const PROCESS_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.processDescriptionPart2', + { + defaultMessage: + 'Details including the command line arguments, parent process, and if applicable, file hashes are displayed when available.', + } +); + +export const SOCKET_NAME = i18n.translate('xpack.securitySolution.eventRenderers.socketName', { + defaultMessage: 'Socket (Network)', +}); + +export const SOCKET_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.socketDescriptionPart1', + { + defaultMessage: + 'Socket (Network) events show processes listening, accepting, and closing connections.', + } +); + +export const SOCKET_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.socketDescriptionPart2', + { + defaultMessage: + 'Details including the protocol, ports, and a community ID for correlating all network events related to a single flow are displayed when available.', + } +); + +export const SURICATA_NAME = i18n.translate('xpack.securitySolution.eventRenderers.suricataName', { + defaultMessage: 'Suricata', +}); + +export const SURICATA_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.suricataDescriptionPart1', + { + defaultMessage: 'Summarizes', + } +); + +export const SURICATA_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.suricataDescriptionPart2', + { + defaultMessage: + 'intrusion detection (IDS), inline intrusion prevention (IPS), and network security monitoring (NSM) events', + } +); + +export const SYSTEM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.systemName', { + defaultMessage: 'System', +}); + +export const SYSTEM_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.systemDescriptionPart1', + { + defaultMessage: 'The Auditbeat', + } +); + +export const SYSTEM_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.systemDescriptionPart2', + { + defaultMessage: 'module collects various security related information about a system.', + } +); + +export const SYSTEM_DESCRIPTION_PART3 = i18n.translate( + 'xpack.securitySolution.eventRenderers.systemDescriptionPart3', + { + defaultMessage: + 'All datasets send both periodic state information (e.g. all currently running processes) and real-time changes (e.g. when a new process starts or stops).', + } +); + +export const ZEEK_NAME = i18n.translate('xpack.securitySolution.eventRenderers.zeekName', { + defaultMessage: 'Zeek (formerly Bro)', +}); + +export const ZEEK_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.zeekDescriptionPart1', + { + defaultMessage: 'Summarizes events from the', + } +); + +export const ZEEK_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.zeekDescriptionPart2', + { + defaultMessage: 'Network Security Monitoring (NSM) tool', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts new file mode 100644 index 0000000000000..4749afda9570a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.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 const ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID = 'row-renderer-example'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx new file mode 100644 index 0000000000000..d90d0fdfa558b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx @@ -0,0 +1,30 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { createGenericAuditRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; +import { CONNECTED_USING } from '../../timeline/body/renderers/auditd/translations'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const AuditdExampleComponent: React.FC = () => { + const auditdRowRenderer = createGenericAuditRowRenderer({ + actionName: 'connected-to', + text: CONNECTED_USING, + }); + + return ( + <> + {auditdRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[26].ecs, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const AuditdExample = React.memo(AuditdExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx new file mode 100644 index 0000000000000..fc8e51864f50a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx @@ -0,0 +1,30 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { createGenericFileRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; +import { OPENED_FILE, USING } from '../../timeline/body/renderers/auditd/translations'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const AuditdFileExampleComponent: React.FC = () => { + const auditdFileRowRenderer = createGenericFileRowRenderer({ + actionName: 'opened-file', + text: `${OPENED_FILE} ${USING}`, + }); + + return ( + <> + {auditdFileRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[27].ecs, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const AuditdFileExample = React.memo(AuditdFileExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx new file mode 100644 index 0000000000000..3cc39a3bf7050 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/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. + */ + +export * from './auditd'; +export * from './auditd_file'; +export * from './netflow'; +export * from './suricata'; +export * from './system'; +export * from './system_dns'; +export * from './system_endgame_process'; +export * from './system_file'; +export * from './system_fim'; +export * from './system_security_event'; +export * from './system_socket'; +export * from './zeek'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx new file mode 100644 index 0000000000000..a276bafb65c60 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { getMockNetflowData } from '../../../../common/mock/netflow'; +import { netflowRowRenderer } from '../../timeline/body/renderers/netflow/netflow_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const NetflowExampleComponent: React.FC = () => ( + <> + {netflowRowRenderer.renderRow({ + browserFields: {}, + data: getMockNetflowData(), + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + +); +export const NetflowExample = React.memo(NetflowExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx new file mode 100644 index 0000000000000..318f427b81f28 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { suricataRowRenderer } from '../../timeline/body/renderers/suricata/suricata_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SuricataExampleComponent: React.FC = () => ( + <> + {suricataRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[2].ecs, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + +); +export const SuricataExample = React.memo(SuricataExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx new file mode 100644 index 0000000000000..c8c3b48ac366a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx @@ -0,0 +1,30 @@ +/* + * 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 { TERMINATED_PROCESS } from '../../timeline/body/renderers/system/translations'; +import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameTerminationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemExampleComponent: React.FC = () => { + const systemRowRenderer = createGenericSystemRowRenderer({ + actionName: 'termination_event', + text: TERMINATED_PROCESS, + }); + + return ( + <> + {systemRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameTerminationEvent, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemExample = React.memo(SystemExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx new file mode 100644 index 0000000000000..4937b0f05ce7c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx @@ -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 React from 'react'; + +import { createDnsRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameDnsRequest } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemDnsExampleComponent: React.FC = () => { + const systemDnsRowRenderer = createDnsRowRenderer(); + + return ( + <> + {systemDnsRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameDnsRequest, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemDnsExample = React.memo(SystemDnsExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx new file mode 100644 index 0000000000000..675bc172ab6f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx @@ -0,0 +1,30 @@ +/* + * 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 { createEndgameProcessRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameCreationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { PROCESS_STARTED } from '../../timeline/body/renderers/system/translations'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemEndgameProcessExampleComponent: React.FC = () => { + const systemEndgameProcessRowRenderer = createEndgameProcessRowRenderer({ + actionName: 'creation_event', + text: PROCESS_STARTED, + }); + + return ( + <> + {systemEndgameProcessRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameCreationEvent, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemEndgameProcessExample = React.memo(SystemEndgameProcessExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx new file mode 100644 index 0000000000000..62c243a7e8502 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx @@ -0,0 +1,30 @@ +/* + * 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 { mockEndgameFileDeleteEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { DELETED_FILE } from '../../timeline/body/renderers/system/translations'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemFileExampleComponent: React.FC = () => { + const systemFileRowRenderer = createGenericFileRowRenderer({ + actionName: 'file_delete_event', + text: DELETED_FILE, + }); + + return ( + <> + {systemFileRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameFileDeleteEvent, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemFileExample = React.memo(SystemFileExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx new file mode 100644 index 0000000000000..ad3eeb7f797ff --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx @@ -0,0 +1,30 @@ +/* + * 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 { mockEndgameFileCreateEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { createFimRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { CREATED_FILE } from '../../timeline/body/renderers/system/translations'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemFimExampleComponent: React.FC = () => { + const systemFimRowRenderer = createFimRowRenderer({ + actionName: 'file_create_event', + text: CREATED_FILE, + }); + + return ( + <> + {systemFimRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameFileCreateEvent, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemFimExample = React.memo(SystemFimExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx new file mode 100644 index 0000000000000..bc577771cc90c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx @@ -0,0 +1,28 @@ +/* + * 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 { createSecurityEventRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameUserLogon } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemSecurityEventExampleComponent: React.FC = () => { + const systemSecurityEventRowRenderer = createSecurityEventRowRenderer({ + actionName: 'user_logon', + }); + + return ( + <> + {systemSecurityEventRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameUserLogon, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemSecurityEventExample = React.memo(SystemSecurityEventExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx new file mode 100644 index 0000000000000..dd119d1b60f39 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { ACCEPTED_A_CONNECTION_VIA } from '../../timeline/body/renderers/system/translations'; +import { createSocketRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameIpv4ConnectionAcceptEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const SystemSocketExampleComponent: React.FC = () => { + const systemSocketRowRenderer = createSocketRowRenderer({ + actionName: 'ipv4_connection_accept_event', + text: ACCEPTED_A_CONNECTION_VIA, + }); + return ( + <> + {systemSocketRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameIpv4ConnectionAcceptEvent, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + + ); +}; +export const SystemSocketExample = React.memo(SystemSocketExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx new file mode 100644 index 0000000000000..56f0d207fbc6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { zeekRowRenderer } from '../../timeline/body/renderers/zeek/zeek_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const ZeekExampleComponent: React.FC = () => ( + <> + {zeekRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[13].ecs, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + +); +export const ZeekExample = React.memo(ZeekExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx new file mode 100644 index 0000000000000..2792b264ba7e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -0,0 +1,182 @@ +/* + * 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 { + EuiButtonEmpty, + EuiButtonIcon, + EuiText, + EuiToolTip, + EuiOverlayMask, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, +} from '@elastic/eui'; +import React, { useState, useCallback, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { State } from '../../../common/store'; + +import { renderers } from './catalog'; +import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; +import { RowRenderersBrowser } from './row_renderers_browser'; +import * as i18n from './translations'; + +const StyledEuiModal = styled(EuiModal)` + margin: 0 auto; + max-width: 95vw; + min-height: 95vh; + + > .euiModal__flex { + max-height: 95vh; + } +`; + +const StyledEuiModalBody = styled(EuiModalBody)` + .euiModalBody__overflow { + display: flex; + align-items: stretch; + overflow: hidden; + + > div { + display: flex; + flex-direction: column; + flex: 1; + + > div:first-child { + flex: 0; + } + + .euiBasicTable { + flex: 1; + overflow: auto; + } + } + } +`; + +const StyledEuiOverlayMask = styled(EuiOverlayMask)` + z-index: 8001; + padding-bottom: 0; + + > div { + width: 100%; + } +`; + +interface StatefulRowRenderersBrowserProps { + timelineId: string; +} + +const StatefulRowRenderersBrowserComponent: React.FC = ({ + timelineId, +}) => { + const tableRef = useRef>(); + const dispatch = useDispatch(); + const excludedRowRendererIds = useSelector( + (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || [] + ); + const [show, setShow] = useState(false); + + const setExcludedRowRendererIds = useCallback( + (payload) => + dispatch( + dispatchSetExcludedRowRendererIds({ + id: timelineId, + excludedRowRendererIds: payload, + }) + ), + [dispatch, timelineId] + ); + + const toggleShow = useCallback(() => setShow(!show), [show]); + + const hideFieldBrowser = useCallback(() => setShow(false), []); + + const handleDisableAll = useCallback(() => { + // eslint-disable-next-line no-unused-expressions + tableRef?.current?.setSelection([]); + }, [tableRef]); + + const handleEnableAll = useCallback(() => { + // eslint-disable-next-line no-unused-expressions + tableRef?.current?.setSelection(renderers); + }, [tableRef]); + + return ( + <> + + + {i18n.EVENT_RENDERERS_TITLE} + + + + {show && ( + + + + + + {i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE} + {i18n.CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION} + + + + + + {i18n.DISABLE_ALL} + + + + + + {i18n.ENABLE_ALL} + + + + + + + + + + + + + )} + + ); +}; + +export const StatefulRowRenderersBrowser = React.memo(StatefulRowRenderersBrowserComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx new file mode 100644 index 0000000000000..d2b0ad788fdb5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -0,0 +1,179 @@ +/* + * 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 { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; +import React, { useMemo, useCallback } from 'react'; +import { xor, xorBy } from 'lodash/fp'; +import styled from 'styled-components'; + +import { RowRendererId } from '../../../../common/types/timeline'; +import { renderers, RowRendererOption } from './catalog'; + +interface RowRenderersBrowserProps { + excludedRowRendererIds: RowRendererId[]; + setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` + .euiTable { + tr > *:last-child { + display: none; + } + + .euiTableHeaderCellCheckbox > .euiTableCellContent { + display: none; // we don't want to display checkbox in the table + } + } +`; + +const StyledEuiFlexItem = styled(EuiFlexItem)` + overflow: auto; + + > div { + padding: 0; + + > div { + margin: 0; + } + } +`; + +const ExampleWrapperComponent = (Example?: React.ElementType) => { + if (!Example) return; + + return ( + + + + ); +}; + +const search = { + box: { + incremental: true, + schema: true, + }, +}; + +/** + * Since `searchableDescription` contains raw text to power the Search bar, + * this "noop" function ensures it's not actually rendered + */ +const renderSearchableDescriptionNoop = () => <>; + +const initialSorting = { + sort: { + field: 'name', + direction: 'asc', + }, +}; + +const StyledNameButton = styled.button` + text-align: left; +`; + +const RowRenderersBrowserComponent = React.forwardRef( + ({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => { + const notExcludedRowRenderers = useMemo(() => { + if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return []; + + return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); + }, [excludedRowRendererIds]); + + const handleNameClick = useCallback( + (item: RowRendererOption) => () => { + const newSelection = xor([item], notExcludedRowRenderers); + // @ts-ignore + ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions + }, + [notExcludedRowRenderers, ref] + ); + + const nameColumnRenderCallback = useCallback( + (value, item) => ( + + {value} + + ), + [handleNameClick] + ); + + const columns = useMemo( + () => [ + { + field: 'name', + name: 'Name', + sortable: true, + width: '10%', + render: nameColumnRenderCallback, + }, + { + field: 'description', + name: 'Description', + width: '25%', + render: (description: React.ReactNode) => description, + }, + { + field: 'example', + name: 'Example', + width: '65%', + render: ExampleWrapperComponent, + }, + { + field: 'searchableDescription', + name: 'Searchable Description', + sortable: false, + width: '0px', + render: renderSearchableDescriptionNoop, + }, + ], + [nameColumnRenderCallback] + ); + + const handleSelectable = useCallback(() => true, []); + + const handleSelectionChange = useCallback( + (selection: RowRendererOption[]) => { + if (!selection || !selection.length) + return setExcludedRowRendererIds(Object.values(RowRendererId)); + + const excludedRowRenderers = xorBy('id', renderers, selection); + + setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id)); + }, + [setExcludedRowRendererIds] + ); + + const selectionValue = useMemo( + () => ({ + selectable: handleSelectable, + onSelectionChange: handleSelectionChange, + initialSelected: notExcludedRowRenderers, + }), + [handleSelectable, handleSelectionChange, notExcludedRowRenderers] + ); + + return ( + + ); + } +); + +RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent'; + +export const RowRenderersBrowser = React.memo(RowRenderersBrowserComponent); + +RowRenderersBrowser.displayName = 'RowRenderersBrowser'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts new file mode 100644 index 0000000000000..93874ff3240bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const EVENT_RENDERERS_TITLE = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.eventRenderersTitle', + { + defaultMessage: 'Event Renderers', + } +); + +export const CUSTOMIZE_EVENT_RENDERERS_TITLE = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersTitle', + { + defaultMessage: 'Customize Event Renderers', + } +); + +export const CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersDescription', + { + defaultMessage: + 'Event Renderers automatically convey the most relevant details in an event to reveal its story', + } +); + +export const ENABLE_ALL = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.enableAllRenderersButtonLabel', + { + defaultMessage: 'Enable all', + } +); + +export const DISABLE_ALL = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.disableAllRenderersButtonLabel', + { + defaultMessage: 'Disable all', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 2facb2b3d0ede..125ba23a5c5a5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -119,9 +119,9 @@ export const Actions = React.memo( - {loading && } - - {!loading && ( + {loading ? ( + + ) : ( - - - + } + columnHeaders={ + Array [ + Object { + "columnHeaderType": "not-filtered", + "id": "@timestamp", + "width": 190, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "message", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.category", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.action", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "host.name", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "source.ip", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "destination.ip", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "user.name", + "width": 180, + }, + ] + } + data-test-subj="field-browser" + height={300} + isEventViewer={false} + onUpdateColumns={[MockFunction]} + timelineId="test" + toggleColumn={[MockFunction]} + width={900} + /> + + + { @@ -36,12 +36,12 @@ describe('helpers', () => { }); test('returns the events viewer actions column width when isEventViewer is true', () => { - expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); + expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH); }); test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { expect(getActionsColumnWidth(true, true)).toEqual( - EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index c538457431fef..903b17c4e8f15 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -14,6 +14,7 @@ import { SHOW_CHECK_BOXES_COLUMN_WIDTH, EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, DEFAULT_ACTIONS_COLUMN_WIDTH, + MINIMUM_ACTIONS_COLUMN_WIDTH, } from '../constants'; /** Enriches the column headers with field details from the specified browserFields */ @@ -42,7 +43,14 @@ export const getActionsColumnWidth = ( isEventViewer: boolean, showCheckboxes = false, additionalActionWidth = 0 -): number => - (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + - (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + - additionalActionWidth; +): number => { + const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0; + const actionsColumnWidth = + checkboxesWidth + + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + + additionalActionWidth; + + return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth + ? actionsColumnWidth + : MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index aa0b8d770f60c..b139aa1a7a9a6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -18,8 +18,6 @@ import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix, } from '../../../../../common/components/drag_and_drop/helpers'; -import { StatefulFieldsBrowser } from '../../../fields_browser'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; import { OnColumnRemoved, OnColumnResized, @@ -29,6 +27,9 @@ import { OnUpdateColumns, } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { StatefulFieldsBrowser } from '../../../fields_browser'; +import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; import { EventsTh, EventsThContent, @@ -170,6 +171,7 @@ export const ColumnHeadersComponent = ({ {showSelectAllCheckbox && ( @@ -185,22 +187,23 @@ export const ColumnHeadersComponent = ({ )} - - - + + + + {showEventsSelect && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 5f3fb4fa5113c..6b6ae3c3467b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +/** The minimum (fixed) width of the Actions column */ +export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; + /** The (fixed) width of the Actions column */ export const DEFAULT_ACTIONS_COLUMN_WIDTH = 76; // px; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 63de117aeaf3d..23f7aad049215 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -21,7 +21,7 @@ import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { EventsTdContent, EventsTrData } from '../../styles'; +import { EventsTd, EventsTdContent, EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { DataDrivenColumns } from '../data_driven_columns'; import { eventHasNotes, getPinOnClick } from '../helpers'; @@ -133,22 +133,24 @@ export const EventColumnView = React.memo( ...acc, icon: [ ...acc.icon, - - - action.onClick({ eventId: id, ecsData, data })} - /> - - , + + + + action.onClick({ eventId: id, ecsData, data })} + /> + + + , ], }; } @@ -176,23 +178,25 @@ export const EventColumnView = React.memo( return grouped.contextMenu.length > 0 ? [ ...grouped.icon, - - + - - - , + + + + + , ] : grouped.icon; }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap index 8e806fadb7bf8..f6fbc771c954a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap @@ -16,7 +16,7 @@ exports[`GenericFileDetails rendering it renders the default GenericFileDetails processTitle="/lib/systemd/systemd-journald" result="success" secondary="root" - session="unset" + session="242" text="generic-text-123" userName="root" workingDirectory="/" @@ -34,7 +34,7 @@ exports[`GenericFileDetails rendering it renders the default GenericFileDetails "success", ], "session": Array [ - "unset", + "242", ], "summary": Object { "actor": Object { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap index b24a90589ce65..784924e896673 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap @@ -32,7 +32,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga "message_type": null, "object": Object { "primary": Array [ - "93.184.216.34", + "192.168.216.34", ], "secondary": Array [ "80", @@ -46,7 +46,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga }, "destination": Object { "ip": Array [ - "93.184.216.34", + "192.168.216.34", ], "port": Array [ 80, @@ -113,7 +113,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga "zeek": null, } } - text="some text" + text="connected using" timelineId="test" /> @@ -135,7 +135,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai "success", ], "session": Array [ - "unset", + "242", ], "summary": Object { "actor": Object { @@ -259,7 +259,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai } } fileIcon="document" - text="some text" + text="opened file using" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index aec463f531448..1e314c0ebd281 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -34,7 +34,7 @@ describe('GenericRowRenderer', () => { auditd = cloneDeep(mockTimelineData[26].ecs); connectedToRenderer = createGenericAuditRowRenderer({ actionName: 'connected-to', - text: 'some text', + text: 'connected using', }); }); test('renders correctly against snapshot', () => { @@ -80,7 +80,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + 'Session246alice@zeek-londonconnected usingwget(1490)wget www.example.comwith resultsuccessDestination192.168.216.34:80' ); }); }); @@ -95,7 +95,7 @@ describe('GenericRowRenderer', () => { auditdFile = cloneDeep(mockTimelineData[27].ecs); fileToRenderer = createGenericFileRowRenderer({ actionName: 'opened-file', - text: 'some text', + text: 'opened file using', }); }); @@ -142,7 +142,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' + 'Session242root@zeek-londonin/opened file using/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index e9d0bdfa3a323..3e7520f641f4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -10,6 +10,8 @@ import { IconType } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { AuditdGenericDetails } from './generic_details'; import { AuditdGenericFileDetails } from './generic_file_details'; @@ -22,6 +24,7 @@ export const createGenericAuditRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: RowRendererId.auditd, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -54,6 +57,7 @@ export const createGenericFileRowRenderer = ({ text: string; fileIcon?: IconType; }): RowRenderer => ({ + id: RowRendererId.auditd_file, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 91499fd9c30f5..795c914c3c9a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -10,6 +10,7 @@ import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; import { asArrayIfExists } from '../../../../../../common/lib/helpers'; import { TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, @@ -84,6 +85,7 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu }; export const netflowRowRenderer: RowRenderer = { + id: RowRendererId.netflow, isInstance: (ecs) => eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx index e63f60226c707..0b860491918df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - import React from 'react'; +import { RowRendererId } from '../../../../../../common/types/timeline'; + import { RowRenderer } from './row_renderer'; +const PlainRowRenderer = () => <>; + +PlainRowRenderer.displayName = 'PlainRowRenderer'; + export const plainRowRenderer: RowRenderer = { + id: RowRendererId.plain, isInstance: (_) => true, - renderRow: () => <>, + renderRow: PlainRowRenderer, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx index 5cee0a0118dd2..609e9dba1a46e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { BrowserFields } from '../../../../../common/containers/source'; +import type { RowRendererId } from '../../../../../../common/types/timeline'; import { Ecs } from '../../../../../graphql/types'; import { EventsTrSupplement } from '../../styles'; @@ -22,6 +23,7 @@ export const RowRendererContainer = React.memo(({ chi RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { + id: RowRendererId; isInstance: (data: Ecs) => boolean; renderRow: ({ browserFields, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap index f766befaf47e4..e55465cfd8895 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap @@ -34,7 +34,6 @@ exports[`SuricataSignature rendering it renders the default SuricataSignature 1` > Hello -
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index 5012f321188d6..242f63611f2ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -9,10 +9,13 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { SuricataDetails } from './suricata_details'; export const suricataRowRenderer: RowRenderer = { + id: RowRendererId.suricata, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx index db0ddd857238f..1cd78178d017f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -13,7 +13,6 @@ import { DraggableWrapper, } from '../../../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers'; -import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { GoogleLink } from '../../../../../../common/components/links'; import { Provider } from '../../../data_providers/provider'; @@ -122,7 +121,6 @@ export const SuricataSignature = React.memo<{ {signature.split(' ').splice(tokens.length).join(' ')} - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index e31fc26e4ae52..67e050160805e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -9,6 +9,8 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { DnsRequestEventDetails } from '../dns/dns_request_event_details'; import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details'; import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers'; @@ -25,6 +27,7 @@ export const createGenericSystemRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: RowRendererId.system, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -55,6 +58,7 @@ export const createEndgameProcessRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: RowRendererId.system_file, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); @@ -86,6 +90,7 @@ export const createFimRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: RowRendererId.system_fim, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); @@ -117,6 +122,7 @@ export const createGenericFileRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: RowRendererId.system_file, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -147,6 +153,7 @@ export const createSocketRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: RowRendererId.system_socket, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; @@ -169,6 +176,7 @@ export const createSecurityEventRowRenderer = ({ }: { actionName: string; }): RowRenderer => ({ + id: RowRendererId.system_security_event, isInstance: (ecs) => { const category: string | null | undefined = get('event.category[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -192,6 +200,7 @@ export const createSecurityEventRowRenderer = ({ }); export const createDnsRowRenderer = (): RowRenderer => ({ + id: RowRendererId.system_dns, isInstance: (ecs) => { const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', ecs); const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index 25228b04bb50b..9bbb7a4090dea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -9,10 +9,13 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { ZeekDetails } from './zeek_details'; export const zeekRowRenderer: RowRenderer = { + id: RowRendererId.zeek, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx index cdf4a8cba68ab..74f75a0a73386 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -15,7 +15,6 @@ import { DraggableWrapper, } from '../../../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers'; -import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { GoogleLink, ReputationLink } from '../../../../../../common/components/links'; import { Provider } from '../../../data_providers/provider'; import { IS_OPERATOR } from '../../../data_providers/data_provider'; @@ -120,7 +119,6 @@ export const Link = React.memo(({ value, link }) => {
{value} -
); @@ -129,7 +127,6 @@ export const Link = React.memo(({ value, link }) => {
-
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index c9660182a4050..141534f1dcb6f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; @@ -60,6 +60,7 @@ const StatefulBodyComponent = React.memo( columnHeaders, data, eventIdToNoteIds, + excludedRowRendererIds, height, id, isEventViewer = false, @@ -74,7 +75,6 @@ const StatefulBodyComponent = React.memo( clearSelected, show, showCheckboxes, - showRowRenderers, graphEventId, sort, toggleColumn, @@ -97,8 +97,7 @@ const StatefulBodyComponent = React.memo( const onAddNoteToEvent: AddNoteToEvent = useCallback( ({ eventId, noteId }: { eventId: string; noteId: string }) => addNoteToEvent!({ id, eventId, noteId }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, addNoteToEvent] ); const onRowSelected: OnRowSelected = useCallback( @@ -135,35 +134,36 @@ const StatefulBodyComponent = React.memo( (sorted) => { updateSort!({ id, sort: sorted }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateSort] ); const onColumnRemoved: OnColumnRemoved = useCallback( (columnId) => removeColumn!({ id, columnId }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, removeColumn] ); const onColumnResized: OnColumnResized = useCallback( ({ columnId, delta }) => applyDeltaToColumnWidth!({ id, columnId, delta }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [applyDeltaToColumnWidth, id] ); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [id]); + const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [ + id, + pinEvent, + ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [id]); + const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [ + id, + unPinEvent, + ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), []); + const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), [ + updateNote, + ]); const onUpdateColumns: OnUpdateColumns = useCallback( (columns) => updateColumns!({ id, columns }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateColumns] ); // Sync to selectAll so parent components can select all events @@ -171,8 +171,19 @@ const StatefulBodyComponent = React.memo( if (selectAll) { onSelectAll({ isSelected: true }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectAll]); // onSelectAll dependency not necessary + }, [onSelectAll, selectAll]); + + const enabledRowRenderers = useMemo(() => { + if ( + excludedRowRendererIds && + excludedRowRendererIds.length === Object.keys(RowRendererId).length + ) + return [plainRowRenderer]; + + if (!excludedRowRendererIds) return rowRenderers; + + return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); + }, [excludedRowRendererIds]); return ( ( onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} - rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]} + rowRenderers={enabledRowRenderers} selectedEventIds={selectedEventIds} show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} @@ -213,6 +224,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.data, nextProps.data) && + deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && @@ -225,7 +237,6 @@ const StatefulBodyComponent = React.memo( prevProps.show === nextProps.show && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.sort === nextProps.sort ); @@ -245,6 +256,7 @@ const makeMapStateToProps = () => { columns, eventIdToNoteIds, eventType, + excludedRowRendererIds, graphEventId, isSelectAllChecked, loadingEventIds, @@ -252,13 +264,13 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - showRowRenderers, } = timeline; return { columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, eventType, + excludedRowRendererIds, graphEventId, isSelectAllChecked, loadingEventIds, @@ -268,7 +280,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - showRowRenderers, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index bc7c313553f1e..ece23d7a10886 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -97,7 +97,7 @@ export const ProviderItemBadge = React.memo( useEffect(() => { // optionally register the provider if provided - if (!providerRegistered && register != null) { + if (register != null) { dispatch(dragAndDropActions.registerProvider({ provider: { ...register, and: [] } })); setProviderRegistered(true); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 47d848021ba43..eb103d8e7e861 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -91,10 +91,14 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({ export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__thGroupActions ${className}`, -}))<{ actionsColumnWidth: number }>` +}))<{ actionsColumnWidth: number; isEventViewer: boolean }>` display: flex; - flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`}; + flex: 0 0 + ${({ actionsColumnWidth, isEventViewer }) => + `${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`}; min-width: 0; + padding-left: ${({ isEventViewer }) => + !isEventViewer ? '4px;' : '0;'}; // match timeline event border `; export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ @@ -151,6 +155,11 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({ width != null ? `${width}px` : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + > button.euiButtonIcon, + > .euiToolTipAnchor > button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } `; /* EVENTS BODY */ @@ -198,8 +207,7 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ }))<{ className: string }>` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; line-height: ${({ theme }) => theme.eui.euiLineHeight}; - padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 - ${({ theme }) => theme.eui.paddingSizes.xl}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 52px; `; export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ @@ -249,6 +257,11 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({ width != null ? `${width}px` : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + > button.euiButtonIcon, + > .euiToolTipAnchor > button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } `; /** @@ -334,6 +347,5 @@ export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({ */ export const EventsLoading = styled(EuiLoadingSpinner)` - margin: ${({ theme }) => theme.eui.euiSizeXS}; - vertical-align: top; + vertical-align: middle; `; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts index 5cbc922f09c9a..cd03e43938b44 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts @@ -51,6 +51,7 @@ export const allTimelinesQuery = gql` updatedBy version } + excludedRowRendererIds notes { eventId note diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index 4ecabeef16dff..3cf33048007e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -75,6 +75,7 @@ export const getAllTimeline = memoizeOne( return acc; }, {}) : null, + excludedRowRendererIds: timeline.excludedRowRendererIds, favorite: timeline.favorite, noteIds: timeline.noteIds, notes: diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index 24beed0801aa6..0aaeb22d72afc 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -69,6 +69,7 @@ export const oneTimelineQuery = gql` updatedBy version } + excludedRowRendererIds favorite { fullName userName diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 80be2aee80b68..618de48091ce8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -17,7 +17,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../../graphql/types'; -import { TimelineTypeLiteral } from '../../../../common/types/timeline'; +import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline'; import { InsertTimeline } from './types'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline'); @@ -59,6 +59,7 @@ export const createTimeline = actionCreator<{ start: number; end: number; }; + excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; columns: ColumnHeaderOptions[]; itemsPerPage?: number; @@ -69,7 +70,6 @@ export const createTimeline = actionCreator<{ show?: boolean; sort?: Sort; showCheckboxes?: boolean; - showRowRenderers?: boolean; timelineType?: TimelineTypeLiteral; templateTimelineId?: string; templateTimelineVersion?: number; @@ -266,3 +266,8 @@ export const clearEventsDeleted = actionCreator<{ export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( 'UPDATE_EVENT_TYPE' ); + +export const setExcludedRowRendererIds = actionCreator<{ + id: string; + excludedRowRendererIds: RowRendererId[]; +}>('SET_TIMELINE_EXCLUDED_ROW_RENDERER_IDS'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 5290178092f3e..f4c4085715af9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -18,6 +18,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick { description: '', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], filters: [ @@ -146,7 +147,6 @@ describe('Epic Timeline', () => { selectedEventIds: {}, show: true, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, status: TimelineStatus.active, width: 1100, @@ -233,6 +233,7 @@ describe('Epic Timeline', () => { }, description: '', eventType: 'all', + excludedRowRendererIds: [], filters: [ { exists: null, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 605700cb71a2a..2f9331ec9db8e 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -331,6 +331,7 @@ const timelineInput: TimelineInput = { dataProviders: null, description: null, eventType: null, + excludedRowRendererIds: null, filters: null, kqlMode: null, kqlQuery: null, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts index b3d1db23ffae8..632525750c8d8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts @@ -16,6 +16,7 @@ import { removeColumn, upsertColumn, applyDeltaToColumnWidth, + setExcludedRowRendererIds, updateColumns, updateItemsPerPage, updateSort, @@ -30,6 +31,7 @@ const timelineActionTypes = [ updateColumns.type, updateItemsPerPage.type, updateSort.type, + setExcludedRowRendererIds.type, ]; export const isPageTimeline = (timelineId: string | undefined): boolean => diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index a347d3e41e206..59f47297b1f65 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -21,7 +21,11 @@ import { } from '../../../timelines/components/timeline/data_providers/data_provider'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; import { TimelineNonEcsData } from '../../../graphql/types'; -import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline'; +import { + TimelineTypeLiteral, + TimelineType, + RowRendererId, +} from '../../../../common/types/timeline'; import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; @@ -130,6 +134,7 @@ interface AddNewTimelineParams { start: number; end: number; }; + excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; id: string; itemsPerPage?: number; @@ -140,7 +145,6 @@ interface AddNewTimelineParams { show?: boolean; sort?: Sort; showCheckboxes?: boolean; - showRowRenderers?: boolean; timelineById: TimelineById; timelineType: TimelineTypeLiteral; } @@ -150,6 +154,7 @@ export const addNewTimeline = ({ columns, dataProviders = [], dateRange = { start: 0, end: 0 }, + excludedRowRendererIds = [], filters = timelineDefaults.filters, id, itemsPerPage = timelineDefaults.itemsPerPage, @@ -157,7 +162,6 @@ export const addNewTimeline = ({ sort = timelineDefaults.sort, show = false, showCheckboxes = false, - showRowRenderers = true, timelineById, timelineType, }: AddNewTimelineParams): TimelineById => { @@ -176,6 +180,7 @@ export const addNewTimeline = ({ columns, dataProviders, dateRange, + excludedRowRendererIds, filters, itemsPerPage, kqlQuery, @@ -186,7 +191,6 @@ export const addNewTimeline = ({ isSaving: false, isLoading: false, showCheckboxes, - showRowRenderers, timelineType, ...templateTimelineInfo, }, @@ -1436,3 +1440,25 @@ export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams }, }; }; + +interface UpdateExcludedRowRenderersIds { + id: string; + excludedRowRendererIds: RowRendererId[]; + timelineById: TimelineById; +} + +export const updateExcludedRowRenderersIds = ({ + id, + excludedRowRendererIds, + timelineById, +}: UpdateExcludedRowRenderersIds): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + excludedRowRendererIds, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index a78fbc41ac430..95d525c7eb59f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -15,6 +15,7 @@ import { TimelineStatus, } from '../../../graphql/types'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; +import type { RowRendererId } from '../../../../common/types/timeline'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; @@ -54,6 +55,8 @@ export interface TimelineModel { eventType?: EventType; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; + /** A list of Ids of excluded Row Renderers */ + excludedRowRendererIds: RowRendererId[]; filters?: Filter[]; /** When non-empty, display a graph view for this event */ graphEventId?: string; @@ -108,8 +111,6 @@ export interface TimelineModel { show: boolean; /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ showCheckboxes: boolean; - /** When true, shows additional rowRenderers below the PlainRowRenderer **/ - showRowRenderers: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ sort: Sort; /** status: active | draft */ @@ -131,6 +132,7 @@ export type SubsetTimelineModel = Readonly< | 'description' | 'eventType' | 'eventIdToNoteIds' + | 'excludedRowRendererIds' | 'graphEventId' | 'highlightedDropAndProviderId' | 'historyIds' @@ -153,7 +155,6 @@ export type SubsetTimelineModel = Readonly< | 'selectedEventIds' | 'show' | 'showCheckboxes' - | 'showRowRenderers' | 'sort' | 'width' | 'isSaving' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index b8bdb4f2ad7f0..4cfc20eb81705 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -70,6 +70,7 @@ const timelineByIdMock: TimelineById = { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], id: 'foo', @@ -97,7 +98,6 @@ const timelineByIdMock: TimelineById = { selectedEventIds: {}, show: true, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc, @@ -1119,6 +1119,7 @@ describe('Timeline', () => { deletedEventIds: [], description: '', eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1139,7 +1140,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1215,6 +1215,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1235,7 +1236,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1421,6 +1421,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1441,7 +1442,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1517,6 +1517,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1537,7 +1538,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1619,6 +1619,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1639,7 +1640,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1722,6 +1722,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1742,7 +1743,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1917,6 +1917,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1937,7 +1938,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1995,6 +1995,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -2003,7 +2004,6 @@ describe('Timeline', () => { isLoading: false, id: 'foo', savedObjectId: null, - showRowRenderers: true, kqlMode: 'filter', kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], @@ -2099,6 +2099,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], id: 'foo', @@ -2121,7 +2122,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 6bb546c16b617..d15bce5e217fa 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -25,6 +25,7 @@ import { removeProvider, setEventsDeleted, setEventsLoading, + setExcludedRowRendererIds, setFilters, setInsertTimeline, setKqlFilterQueryDraft, @@ -75,6 +76,7 @@ import { setLoadingTimelineEvents, setSelectedTimelineEvents, unPinTimelineEvent, + updateExcludedRowRenderersIds, updateHighlightedDropAndProvider, updateKqlFilterQueryDraft, updateTimelineColumns, @@ -129,13 +131,13 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) id, dataProviders, dateRange, + excludedRowRendererIds, show, columns, itemsPerPage, kqlQuery, sort, showCheckboxes, - showRowRenderers, timelineType = TimelineType.default, filters, } @@ -146,6 +148,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) columns, dataProviders, dateRange, + excludedRowRendererIds, filters, id, itemsPerPage, @@ -153,7 +156,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) sort, show, showCheckboxes, - showRowRenderers, timelineById: state.timelineById, timelineType, }), @@ -306,6 +308,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) + .case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({ + ...state, + timelineById: updateExcludedRowRenderersIds({ + id, + excludedRowRendererIds, + timelineById: state.timelineById, + }), + })) .case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({ ...state, timelineById: setSelectedTimelineEvents({ diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index e46d3be44dbd1..15e188e281d10 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -147,11 +147,28 @@ export const timelineSchema = gql` custom } + enum RowRendererId { + auditd + auditd_file + netflow + plain + suricata + system + system_dns + system_endgame_process + system_file + system_fim + system_security_event + system_socket + zeek + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] description: String eventType: String + excludedRowRendererIds: [RowRendererId!] filters: [FilterTimelineInput!] kqlMode: String kqlQuery: SerializedFilterQueryInput @@ -252,6 +269,7 @@ export const timelineSchema = gql` description: String eventIdToNoteIds: [NoteResult!] eventType: String + excludedRowRendererIds: [RowRendererId!] favorite: [FavoriteTimelineResult!] filters: [FilterTimelineResult!] kqlMode: String diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 52bb4a9862160..6553f709a7fa7 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -126,6 +126,8 @@ export interface TimelineInput { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + filters?: Maybe; kqlMode?: Maybe; @@ -351,6 +353,22 @@ export enum DataProviderType { template = 'template', } +export enum RowRendererId { + auditd = 'auditd', + auditd_file = 'auditd_file', + netflow = 'netflow', + plain = 'plain', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fim = 'system_fim', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + zeek = 'zeek', +} + export enum TimelineStatus { active = 'active', draft = 'draft', @@ -1963,6 +1981,8 @@ export interface TimelineResult { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + favorite?: Maybe; filters?: Maybe; @@ -8101,6 +8121,12 @@ export namespace TimelineResultResolvers { eventType?: EventTypeResolver, TypeParent, TContext>; + excludedRowRendererIds?: ExcludedRowRendererIdsResolver< + Maybe, + TypeParent, + TContext + >; + favorite?: FavoriteResolver, TypeParent, TContext>; filters?: FiltersResolver, TypeParent, TContext>; @@ -8184,6 +8210,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type ExcludedRowRendererIdsResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type FavoriteResolver< R = Maybe, Parent = TimelineResult, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts index eb8f6f5022985..d3d7783dc9385 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts @@ -44,5 +44,7 @@ export const pickSavedTimeline = ( savedTimeline.status = TimelineStatus.active; } + savedTimeline.excludedRowRendererIds = savedTimeline.excludedRowRendererIds ?? []; + return savedTimeline; }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index 23090bfc6f0bd..f4b97ac3510cc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -181,7 +181,7 @@ const getTimelinesFromObjects = async ( if (myTimeline != null) { const timelineNotes = myNotes.filter((n) => n.timelineId === timelineId); const timelinePinnedEventIds = myPinnedEventIds.filter((p) => p.timelineId === timelineId); - const exportedTimeline = omit('status', myTimeline); + const exportedTimeline = omit(['status', 'excludedRowRendererIds'], myTimeline); return [ ...acc, { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts index 22b98930f3181..c5ee611dfa27f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts @@ -135,6 +135,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { eventType: { type: 'keyword', }, + excludedRowRendererIds: { + type: 'text', + }, favorite: { properties: { keySearch: { From 3fc54e7c552998367171d76e0119eab6d4f2ff1c Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Fri, 10 Jul 2020 15:53:34 -0400 Subject: [PATCH 02/10] [Security Solution][Endpoint] Policy creation callback fixes + Improved error handling in user manifest loop (#71269) * Clean up matcher types * Rework promise and error-handling in ManifestManager * Write tests for ingest callback and ensure policy is returned when errors occur * More tests for ingest callback * Update tests * Fix tests Co-authored-by: Elastic Machine --- x-pack/plugins/ingest_manager/common/mocks.ts | 43 ++++ .../endpoint/ingest_integration.test.ts | 91 ++++++++ .../server/endpoint/ingest_integration.ts | 105 +++++---- .../server/endpoint/lib/artifacts/common.ts | 7 + .../server/endpoint/lib/artifacts/task.ts | 26 ++- .../server/endpoint/mocks.ts | 4 +- .../endpoint/schemas/artifacts/lists.ts | 26 +-- .../manifest_manager/manifest_manager.mock.ts | 61 ++---- .../manifest_manager/manifest_manager.test.ts | 11 +- .../manifest_manager/manifest_manager.ts | 205 +++++++++++------- .../apps/endpoint/policy_details.ts | 42 ++++ 11 files changed, 430 insertions(+), 191 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/mocks.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts diff --git a/x-pack/plugins/ingest_manager/common/mocks.ts b/x-pack/plugins/ingest_manager/common/mocks.ts new file mode 100644 index 0000000000000..131917af44595 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/mocks.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. + */ + +import { NewPackageConfig, PackageConfig } from './types/models/package_config'; + +export const createNewPackageConfigMock = () => { + return { + name: 'endpoint-1', + description: '', + namespace: 'default', + enabled: true, + config_id: '93c46720-c217-11ea-9906-b5b8a21b268e', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.9.0', + }, + inputs: [], + } as NewPackageConfig; +}; + +export const createPackageConfigMock = () => { + const newPackageConfig = createNewPackageConfigMock(); + return { + ...newPackageConfig, + id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + version: 'abcd', + revision: 1, + updated_at: '2020-06-25T16:03:38.159292', + updated_by: 'kibana', + created_at: '2020-06-25T16:03:38.159292', + created_by: 'kibana', + inputs: [ + { + config: {}, + }, + ], + } as PackageConfig; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts new file mode 100644 index 0000000000000..bb035a19f33d6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { createNewPackageConfigMock } from '../../../ingest_manager/common/mocks'; +import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; +import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { getPackageConfigCreateCallback } from './ingest_integration'; + +describe('ingest_integration tests ', () => { + describe('ingest_integration sanity checks', () => { + test('policy is updated with manifest', async () => { + const logger = loggerMock.create(); + const manifestManager = getManifestManagerMock(); + const callback = getPackageConfigCreateCallback(logger, manifestManager); + const policyConfig = createNewPackageConfigMock(); + const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({ + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + }, + manifest_version: 'WzAsMF0=', + schema_version: 'v1', + }); + }); + + test('policy is returned even if error is encountered during artifact sync', async () => { + const logger = loggerMock.create(); + const manifestManager = getManifestManagerMock(); + manifestManager.syncArtifacts = jest.fn().mockRejectedValue([new Error('error updating')]); + const lastDispatched = await manifestManager.getLastDispatchedManifest(); + const callback = getPackageConfigCreateCallback(logger, manifestManager); + const policyConfig = createNewPackageConfigMock(); + const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( + lastDispatched.toEndpointFormat() + ); + }); + + test('initial policy creation succeeds if snapshot retrieval fails', async () => { + const logger = loggerMock.create(); + const manifestManager = getManifestManagerMock(); + const lastDispatched = await manifestManager.getLastDispatchedManifest(); + manifestManager.getSnapshot = jest.fn().mockResolvedValue(null); + const callback = getPackageConfigCreateCallback(logger, manifestManager); + const policyConfig = createNewPackageConfigMock(); + const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( + lastDispatched.toEndpointFormat() + ); + }); + + test('subsequent policy creations succeed', async () => { + const logger = loggerMock.create(); + const manifestManager = getManifestManagerMock(); + const snapshot = await manifestManager.getSnapshot(); + manifestManager.getLastDispatchedManifest = jest.fn().mockResolvedValue(snapshot!.manifest); + manifestManager.getSnapshot = jest.fn().mockResolvedValue({ + manifest: snapshot!.manifest, + diffs: [], + }); + const callback = getPackageConfigCreateCallback(logger, manifestManager); + const policyConfig = createNewPackageConfigMock(); + const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( + snapshot!.manifest.toEndpointFormat() + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index 1acec1e7c53ac..e2522ac4af778 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -8,7 +8,9 @@ import { Logger } from '../../../../../src/core/server'; import { NewPackageConfig } from '../../../ingest_manager/common/types/models'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; -import { ManifestManager } from './services/artifacts'; +import { ManifestManager, ManifestSnapshot } from './services/artifacts'; +import { reportErrors, ManifestConstants } from './lib/artifacts/common'; +import { ManifestSchemaVersion } from '../../common/endpoint/schema/common'; /** * Callback to handle creation of PackageConfigs in Ingest Manager @@ -29,58 +31,83 @@ export const getPackageConfigCreateCallback = ( // follow the types/schema expected let updatedPackageConfig = newPackageConfig as NewPolicyData; - // get snapshot based on exception-list-agnostic SOs - // with diffs from last dispatched manifest, if it exists - const snapshot = await manifestManager.getSnapshot({ initialize: true }); + // get current manifest from SO (last dispatched) + const manifest = ( + await manifestManager.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION) + )?.toEndpointFormat() ?? { + manifest_version: 'default', + schema_version: ManifestConstants.SCHEMA_VERSION as ManifestSchemaVersion, + artifacts: {}, + }; - if (snapshot === null) { - logger.warn('No manifest snapshot available.'); - return updatedPackageConfig; + // Until we get the Default Policy Configuration in the Endpoint package, + // we will add it here manually at creation time. + if (newPackageConfig.inputs.length === 0) { + updatedPackageConfig = { + ...newPackageConfig, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + artifact_manifest: { + value: manifest, + }, + policy: { + value: policyConfigFactory(), + }, + }, + }, + ], + }; } - if (snapshot.diffs.length > 0) { - // create new artifacts - await manifestManager.syncArtifacts(snapshot, 'add'); + let snapshot: ManifestSnapshot | null = null; + let success = true; + try { + // Try to get most up-to-date manifest data. - // Until we get the Default Policy Configuration in the Endpoint package, - // we will add it here manually at creation time. - // @ts-ignore - if (newPackageConfig.inputs.length === 0) { - updatedPackageConfig = { - ...newPackageConfig, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - artifact_manifest: { - value: snapshot.manifest.toEndpointFormat(), - }, - policy: { - value: policyConfigFactory(), - }, - }, - }, - ], + // get snapshot based on exception-list-agnostic SOs + // with diffs from last dispatched manifest, if it exists + snapshot = await manifestManager.getSnapshot({ initialize: true }); + + if (snapshot && snapshot.diffs.length) { + // create new artifacts + const errors = await manifestManager.syncArtifacts(snapshot, 'add'); + if (errors.length) { + reportErrors(logger, errors); + throw new Error('Error writing new artifacts.'); + } + } + + if (snapshot) { + updatedPackageConfig.inputs[0].config.artifact_manifest = { + value: snapshot.manifest.toEndpointFormat(), }; } - } - try { + return updatedPackageConfig; + } catch (err) { + success = false; + logger.error(err); return updatedPackageConfig; } finally { - if (snapshot.diffs.length > 0) { - // TODO: let's revisit the way this callback happens... use promises? - // only commit when we know the package config was created + if (success && snapshot !== null) { try { - await manifestManager.commit(snapshot.manifest); + if (snapshot.diffs.length > 0) { + // TODO: let's revisit the way this callback happens... use promises? + // only commit when we know the package config was created + await manifestManager.commit(snapshot.manifest); - // clean up old artifacts - await manifestManager.syncArtifacts(snapshot, 'delete'); + // clean up old artifacts + await manifestManager.syncArtifacts(snapshot, 'delete'); + } } catch (err) { logger.error(err); } + } else if (snapshot === null) { + logger.error('No manifest snapshot available.'); } } }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 9ad4554b30203..71d14eb1226d5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from 'src/core/server'; export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', @@ -16,3 +17,9 @@ export const ManifestConstants = { SCHEMA_VERSION: 'v1', INITIAL_VERSION: 'WzAsMF0=', }; + +export const reportErrors = (logger: Logger, errors: Error[]) => { + errors.forEach((err) => { + logger.error(err); + }); +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index aa7f56e815d58..583f4499f591b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -11,6 +11,7 @@ import { TaskManagerStartContract, } from '../../../../../task_manager/server'; import { EndpointAppContext } from '../../types'; +import { reportErrors } from './common'; export const ManifestTaskConstants = { TIMEOUT: '1m', @@ -88,19 +89,36 @@ export class ManifestTask { return; } + let errors: Error[] = []; try { // get snapshot based on exception-list-agnostic SOs // with diffs from last dispatched manifest const snapshot = await manifestManager.getSnapshot(); if (snapshot && snapshot.diffs.length > 0) { // create new artifacts - await manifestManager.syncArtifacts(snapshot, 'add'); + errors = await manifestManager.syncArtifacts(snapshot, 'add'); + if (errors.length) { + reportErrors(this.logger, errors); + throw new Error('Error writing new artifacts.'); + } // write to ingest-manager package config - await manifestManager.dispatch(snapshot.manifest); + errors = await manifestManager.dispatch(snapshot.manifest); + if (errors.length) { + reportErrors(this.logger, errors); + throw new Error('Error dispatching manifest.'); + } // commit latest manifest state to user-artifact-manifest SO - await manifestManager.commit(snapshot.manifest); + const error = await manifestManager.commit(snapshot.manifest); + if (error) { + reportErrors(this.logger, [error]); + throw new Error('Error committing manifest.'); + } // clean up old artifacts - await manifestManager.syncArtifacts(snapshot, 'delete'); + errors = await manifestManager.syncArtifacts(snapshot, 'delete'); + if (errors.length) { + reportErrors(this.logger, errors); + throw new Error('Error cleaning up outdated artifacts.'); + } } } catch (err) { this.logger.error(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 55d7baec36dc6..6a8c26e08d9dd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,6 +6,8 @@ import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loggerMock } from 'src/core/server/logging/logger.mock'; import { xpackMocks } from '../../../../mocks'; import { AgentService, @@ -63,8 +65,8 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< > => { return { agentService: createMockAgentService(), + logger: loggerMock.create(), savedObjectsStart: savedObjectsServiceMock.createStartContract(), - // @ts-ignore manifestManager: getManifestManagerMock(), registerIngestCallback: jest.fn< ReturnType, diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts index b7f99fe6fe297..ed97d04eecee6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts @@ -7,38 +7,38 @@ import * as t from 'io-ts'; import { operator } from '../../../../../lists/common/schemas'; +export const translatedEntryMatchAnyMatcher = t.keyof({ + exact_cased_any: null, + exact_caseless_any: null, +}); +export type TranslatedEntryMatchAnyMatcher = t.TypeOf; + export const translatedEntryMatchAny = t.exact( t.type({ field: t.string, operator, - type: t.keyof({ - exact_cased_any: null, - exact_caseless_any: null, - }), + type: translatedEntryMatchAnyMatcher, value: t.array(t.string), }) ); export type TranslatedEntryMatchAny = t.TypeOf; -export const translatedEntryMatchAnyMatcher = translatedEntryMatchAny.type.props.type; -export type TranslatedEntryMatchAnyMatcher = t.TypeOf; +export const translatedEntryMatchMatcher = t.keyof({ + exact_cased: null, + exact_caseless: null, +}); +export type TranslatedEntryMatchMatcher = t.TypeOf; export const translatedEntryMatch = t.exact( t.type({ field: t.string, operator, - type: t.keyof({ - exact_cased: null, - exact_caseless: null, - }), + type: translatedEntryMatchMatcher, value: t.string, }) ); export type TranslatedEntryMatch = t.TypeOf; -export const translatedEntryMatchMatcher = translatedEntryMatch.type.props.type; -export type TranslatedEntryMatchMatcher = t.TypeOf; - export const translatedEntryMatcher = t.union([ translatedEntryMatchMatcher, translatedEntryMatchAnyMatcher, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index dfbe2572076d0..3bdc5dfbcbd45 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line max-classes-per-file import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; import { Logger } from 'src/core/server'; +import { createPackageConfigMock } from '../../../../../../ingest_manager/common/mocks'; +import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; +import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { listMock } from '../../../../../../lists/server/mocks'; import { @@ -21,40 +23,6 @@ import { getArtifactClientMock } from '../artifact_client.mock'; import { getManifestClientMock } from '../manifest_client.mock'; import { ManifestManager } from './manifest_manager'; -function getMockPackageConfig() { - return { - id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', - inputs: [ - { - config: {}, - }, - ], - revision: 1, - version: 'abcd', // TODO: not yet implemented in ingest_manager (https://github.com/elastic/kibana/issues/69992) - updated_at: '2020-06-25T16:03:38.159292', - updated_by: 'kibana', - created_at: '2020-06-25T16:03:38.159292', - created_by: 'kibana', - }; -} - -class PackageConfigServiceMock { - public create = jest.fn().mockResolvedValue(getMockPackageConfig()); - public get = jest.fn().mockResolvedValue(getMockPackageConfig()); - public getByIds = jest.fn().mockResolvedValue([getMockPackageConfig()]); - public list = jest.fn().mockResolvedValue({ - items: [getMockPackageConfig()], - total: 1, - page: 1, - perPage: 20, - }); - public update = jest.fn().mockResolvedValue(getMockPackageConfig()); -} - -export function getPackageConfigServiceMock() { - return new PackageConfigServiceMock(); -} - async function mockBuildExceptionListArtifacts( os: string, schemaVersion: string @@ -66,27 +34,23 @@ async function mockBuildExceptionListArtifacts( return [await buildArtifact(exceptions, os, schemaVersion)]; } -// @ts-ignore export class ManifestManagerMock extends ManifestManager { - // @ts-ignore - private buildExceptionListArtifacts = async () => { - return mockBuildExceptionListArtifacts('linux', 'v1'); - }; + protected buildExceptionListArtifacts = jest + .fn() + .mockResolvedValue(mockBuildExceptionListArtifacts('linux', 'v1')); - // @ts-ignore - private getLastDispatchedManifest = jest + public getLastDispatchedManifest = jest .fn() .mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION)); - // @ts-ignore - private getManifestClient = jest + protected getManifestClient = jest .fn() .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); } export const getManifestManagerMock = (opts?: { cache?: ExceptionsCache; - packageConfigService?: PackageConfigServiceMock; + packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManagerMock => { let cache = new ExceptionsCache(5); @@ -94,10 +58,14 @@ export const getManifestManagerMock = (opts?: { cache = opts.cache; } - let packageConfigService = getPackageConfigServiceMock(); + let packageConfigService = createPackageConfigServiceMock(); if (opts?.packageConfigService !== undefined) { packageConfigService = opts.packageConfigService; } + packageConfigService.list = jest.fn().mockResolvedValue({ + total: 1, + items: [createPackageConfigMock()], + }); let savedObjectsClient = savedObjectsClientMock.create(); if (opts?.savedObjectsClient !== undefined) { @@ -107,7 +75,6 @@ export const getManifestManagerMock = (opts?: { const manifestManager = new ManifestManagerMock({ artifactClient: getArtifactClientMock(savedObjectsClient), cache, - // @ts-ignore packageConfigService, exceptionListClient: listMock.getExceptionListClient(), logger: loggingSystemMock.create().get() as jest.Mocked, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index b1cbc41459f15..d092e7060f8aa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -6,13 +6,14 @@ import { inflateSync } from 'zlib'; import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { ArtifactConstants, ManifestConstants, Manifest, ExceptionsCache, } from '../../../lib/artifacts'; -import { getPackageConfigServiceMock, getManifestManagerMock } from './manifest_manager.mock'; +import { getManifestManagerMock } from './manifest_manager.mock'; describe('manifest_manager', () => { describe('ManifestManager sanity checks', () => { @@ -73,15 +74,15 @@ describe('manifest_manager', () => { }); test('ManifestManager can dispatch manifest', async () => { - const packageConfigService = getPackageConfigServiceMock(); + const packageConfigService = createPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); const snapshot = await manifestManager.getSnapshot(); const dispatched = await manifestManager.dispatch(snapshot!.manifest); - expect(dispatched).toEqual(true); + expect(dispatched).toEqual([]); const entries = snapshot!.manifest.getEntries(); const artifact = Object.values(entries)[0].getArtifact(); expect( - packageConfigService.update.mock.calls[0][2].inputs[0].config.artifact_manifest.value + packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value ).toEqual({ manifest_version: ManifestConstants.INITIAL_VERSION, schema_version: 'v1', @@ -115,7 +116,7 @@ describe('manifest_manager', () => { snapshot!.diffs.push(diff); const dispatched = await manifestManager.dispatch(snapshot!.manifest); - expect(dispatched).toEqual(true); + expect(dispatched).toEqual([]); await manifestManager.commit(snapshot!.manifest); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index b9e289cee62af..c8cad32ab746e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -61,19 +61,25 @@ export class ManifestManager { /** * Gets a ManifestClient for the provided schemaVersion. * - * @param schemaVersion + * @param schemaVersion The schema version of the manifest. + * @returns {ManifestClient} A ManifestClient scoped to the provided schemaVersion. */ - private getManifestClient(schemaVersion: string) { + protected getManifestClient(schemaVersion: string): ManifestClient { return new ManifestClient(this.savedObjectsClient, schemaVersion as ManifestSchemaVersion); } /** * Builds an array of artifacts (one per supported OS) based on the current - * state of exception-list-agnostic SO's. + * state of exception-list-agnostic SOs. * - * @param schemaVersion + * @param schemaVersion The schema version of the artifact + * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. + * @throws Throws/rejects if there are errors building the list. */ - private async buildExceptionListArtifacts(schemaVersion: string) { + protected async buildExceptionListArtifacts( + schemaVersion: string + ): Promise { + // TODO: should wrap in try/catch? return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce( async (acc: Promise, os) => { const exceptionList = await getFullEndpointExceptionList( @@ -90,13 +96,75 @@ export class ManifestManager { ); } + /** + * Writes new artifact SOs based on provided snapshot. + * + * @param snapshot A ManifestSnapshot to use for writing the artifacts. + * @returns {Promise} Any errors encountered. + */ + private async writeArtifacts(snapshot: ManifestSnapshot): Promise { + const errors: Error[] = []; + for (const diff of snapshot.diffs) { + const artifact = snapshot.manifest.getArtifact(diff.id); + if (artifact === undefined) { + throw new Error( + `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.` + ); + } + + const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64')); + artifact.body = compressedArtifact.toString('base64'); + artifact.encodedSize = compressedArtifact.byteLength; + artifact.compressionAlgorithm = 'zlib'; + artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex'); + + try { + // Write the artifact SO + await this.artifactClient.createArtifact(artifact); + // Cache the compressed body of the artifact + this.cache.set(diff.id, Buffer.from(artifact.body, 'base64')); + } catch (err) { + if (err.status === 409) { + this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); + } else { + // TODO: log error here? + errors.push(err); + } + } + } + return errors; + } + + /** + * Deletes old artifact SOs based on provided snapshot. + * + * @param snapshot A ManifestSnapshot to use for deleting the artifacts. + * @returns {Promise} Any errors encountered. + */ + private async deleteArtifacts(snapshot: ManifestSnapshot): Promise { + const errors: Error[] = []; + for (const diff of snapshot.diffs) { + try { + // Delete the artifact SO + await this.artifactClient.deleteArtifact(diff.id); + // TODO: should we delete the cache entry here? + this.logger.info(`Cleaned up artifact ${diff.id}`); + } catch (err) { + errors.push(err); + } + } + return errors; + } + /** * Returns the last dispatched manifest based on the current state of the * user-artifact-manifest SO. * - * @param schemaVersion + * @param schemaVersion The schema version of the manifest. + * @returns {Promise} The last dispatched manifest, or null if does not exist. + * @throws Throws/rejects if there is an unexpected error retrieving the manifest. */ - private async getLastDispatchedManifest(schemaVersion: string) { + public async getLastDispatchedManifest(schemaVersion: string): Promise { try { const manifestClient = this.getManifestClient(schemaVersion); const manifestSo = await manifestClient.getManifest(); @@ -127,9 +195,11 @@ export class ManifestManager { /** * Snapshots a manifest based on current state of exception-list-agnostic SOs. * - * @param opts TODO + * @param opts Optional parameters for snapshot retrieval. + * @param opts.initialize Initialize a new Manifest when no manifest SO can be retrieved. + * @returns {Promise} A snapshot of the manifest, or null if not initialized. */ - public async getSnapshot(opts?: ManifestSnapshotOpts) { + public async getSnapshot(opts?: ManifestSnapshotOpts): Promise { try { let oldManifest: Manifest | null; @@ -176,71 +246,39 @@ export class ManifestManager { * Creates artifacts that do not yet exist and cleans up old artifacts that have been * superceded by this snapshot. * - * Can be filtered to apply one or both operations. - * - * @param snapshot - * @param diffType + * @param snapshot A ManifestSnapshot to use for sync. + * @returns {Promise} Any errors encountered. */ - public async syncArtifacts(snapshot: ManifestSnapshot, diffType?: 'add' | 'delete') { - const filteredDiffs = snapshot.diffs.reduce((diffs: ManifestDiff[], diff) => { - if (diff.type === diffType || diffType === undefined) { - diffs.push(diff); - } else if (!['add', 'delete'].includes(diff.type)) { - // TODO: replace with io-ts schema - throw new Error(`Unsupported diff type: ${diff.type}`); - } - return diffs; - }, []); - - const adds = filteredDiffs.filter((diff) => { - return diff.type === 'add'; + public async syncArtifacts( + snapshot: ManifestSnapshot, + diffType: 'add' | 'delete' + ): Promise { + const filteredDiffs = snapshot.diffs.filter((diff) => { + return diff.type === diffType; }); - const deletes = filteredDiffs.filter((diff) => { - return diff.type === 'delete'; - }); + const tmpSnapshot = { ...snapshot }; + tmpSnapshot.diffs = filteredDiffs; - for (const diff of adds) { - const artifact = snapshot.manifest.getArtifact(diff.id); - if (artifact === undefined) { - throw new Error( - `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.` - ); - } - const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64')); - artifact.body = compressedArtifact.toString('base64'); - artifact.encodedSize = compressedArtifact.byteLength; - artifact.compressionAlgorithm = 'zlib'; - artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex'); - - try { - await this.artifactClient.createArtifact(artifact); - } catch (err) { - if (err.status === 409) { - this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); - } else { - throw err; - } - } - // Cache the body of the artifact - this.cache.set(diff.id, Buffer.from(artifact.body, 'base64')); + if (diffType === 'add') { + return this.writeArtifacts(tmpSnapshot); + } else if (diffType === 'delete') { + return this.deleteArtifacts(tmpSnapshot); } - for (const diff of deletes) { - await this.artifactClient.deleteArtifact(diff.id); - // TODO: should we delete the cache entry here? - this.logger.info(`Cleaned up artifact ${diff.id}`); - } + return [new Error(`Unsupported diff type: ${diffType}`)]; } /** * Dispatches the manifest by writing it to the endpoint package config. * + * @param manifest The Manifest to dispatch. + * @returns {Promise} Any errors encountered. */ - public async dispatch(manifest: Manifest) { + public async dispatch(manifest: Manifest): Promise { let paging = true; let page = 1; - let success = true; + const errors: Error[] = []; while (paging) { const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, { @@ -264,13 +302,10 @@ export class ManifestManager { `Updated package config ${id} with manifest version ${manifest.getVersion()}` ); } catch (err) { - success = false; - this.logger.debug(`Error updating package config ${id}`); - this.logger.error(err); + errors.push(err); } } else { - success = false; - this.logger.debug(`Package config ${id} has no config.`); + errors.push(new Error(`Package config ${id} has no config.`)); } } @@ -278,32 +313,38 @@ export class ManifestManager { page++; } - // TODO: revisit success logic - return success; + return errors; } /** * Commits a manifest to indicate that it has been dispatched. * - * @param manifest + * @param manifest The Manifest to commit. + * @returns {Promise} An error if encountered, or null if successful. */ - public async commit(manifest: Manifest) { - const manifestClient = this.getManifestClient(manifest.getSchemaVersion()); - - // Commit the new manifest - if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) { - await manifestClient.createManifest(manifest.toSavedObject()); - } else { - const version = manifest.getVersion(); - if (version === ManifestConstants.INITIAL_VERSION) { - throw new Error('Updating existing manifest with baseline version. Bad state.'); + public async commit(manifest: Manifest): Promise { + try { + const manifestClient = this.getManifestClient(manifest.getSchemaVersion()); + + // Commit the new manifest + if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) { + await manifestClient.createManifest(manifest.toSavedObject()); + } else { + const version = manifest.getVersion(); + if (version === ManifestConstants.INITIAL_VERSION) { + throw new Error('Updating existing manifest with baseline version. Bad state.'); + } + await manifestClient.updateManifest(manifest.toSavedObject(), { + version, + }); } - await manifestClient.updateManifest(manifest.toSavedObject(), { - version, - }); + + this.logger.info(`Committed manifest ${manifest.getVersion()}`); + } catch (err) { + return err; } - this.logger.info(`Committed manifest ${manifest.getVersion()}`); + return null; } /** diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index db33775abeb0a..7207bb3fc37b3 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -116,6 +116,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { version: policyInfo.packageInfo.version, }, }, + artifact_manifest: { + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: 'WzEwNSwxXQ==', + schema_version: 'v1', + }, policy: { linux: { events: { file: false, network: true, process: true }, From 66c531d90366cfd08fc671c9bf2208dc102b87e2 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 10 Jul 2020 16:26:57 -0500 Subject: [PATCH 03/10] [Metrics UI] Round metric threshold time buckets to nearest unit (#71172) Co-authored-by: Elastic Machine --- .../metric_threshold/lib/evaluate_alert.ts | 25 +++++++++++-------- .../metric_threshold/lib/metric_query.test.ts | 22 ++++++++++++++++ .../metric_threshold/lib/metric_query.ts | 9 +++++-- .../metric_threshold_executor.test.ts | 2 ++ .../metric_threshold_executor.ts | 4 ++- .../alerting/metric_threshold/test_mocks.ts | 1 + .../infra/server/utils/round_timestamp.ts | 15 +++++++++++ 7 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/infra/server/utils/round_timestamp.ts diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index de5eda4a1f2c3..7f6bf9551e2c1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -23,6 +23,7 @@ interface Aggregation { buckets: Array<{ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; doc_count: number; + key_as_string: string; }>; }; } @@ -57,17 +58,18 @@ export const evaluateAlert = ( ); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; - return mapValues(currentValues, (values: number | number[] | null) => { - if (isTooManyBucketsPreviewException(values)) throw values; + return mapValues(currentValues, (points: any[] | typeof NaN | null) => { + if (isTooManyBucketsPreviewException(points)) throw points; return { ...criterion, metric: criterion.metric ?? DOCUMENT_COUNT_I18N, - currentValue: Array.isArray(values) ? last(values) : NaN, - shouldFire: Array.isArray(values) - ? values.map((value) => comparisonFunction(value, threshold)) + currentValue: Array.isArray(points) ? last(points)?.value : NaN, + timestamp: Array.isArray(points) ? last(points)?.key : NaN, + shouldFire: Array.isArray(points) + ? points.map((point) => comparisonFunction(point.value, threshold)) : [false], - isNoData: values === null, - isError: isNaN(values), + isNoData: points === null, + isError: isNaN(points), }; }); }) @@ -157,17 +159,20 @@ const getValuesFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state if (aggType === Aggregators.COUNT) { - return buckets.map((bucket) => bucket.doc_count); + return buckets.map((bucket) => ({ key: bucket.key_as_string, value: bucket.doc_count })); } if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { return buckets.map((bucket) => { const values = bucket.aggregatedValue?.values || []; const firstValue = first(values); if (!firstValue) return null; - return firstValue.value; + return { key: bucket.key_as_string, value: firstValue.value }; }); } - return buckets.map((bucket) => bucket.aggregatedValue.value); + return buckets.map((bucket) => ({ + key: bucket.key_as_string, + value: bucket.aggregatedValue.value, + })); } catch (e) { return NaN; // Error state } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts index 3ad1031f574e2..b4fe8f053a44a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts @@ -56,4 +56,26 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { ); }); }); + + describe('handles time', () => { + const end = new Date('2020-07-08T22:07:27.235Z').valueOf(); + const timerange = { + end, + start: end - 5 * 60 * 1000, + }; + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timefield, + undefined, + undefined, + timerange + ); + test('by rounding timestamps to the nearest timeUnit', () => { + const rangeFilter = searchBody.query.bool.filter.find((filter) => + filter.hasOwnProperty('range') + )?.range[timefield]; + expect(rangeFilter?.lte).toBe(1594246020000); + expect(rangeFilter?.gte).toBe(1594245720000); + }); + }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 15506a30529c4..078ca46d42e60 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Aggregators } from '../types'; import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds'; +import { roundTimestamp } from '../../../../utils/round_timestamp'; import { getDateHistogramOffset } from '../../../snapshot/query_helpers'; import { createPercentileAggregation } from './create_percentile_aggregation'; @@ -34,12 +36,15 @@ export const getElasticsearchMetricQuery = ( const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); - const to = timeframe ? timeframe.end : Date.now(); + const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit); // We need enough data for 5 buckets worth of data. We also need // to convert the intervalAsSeconds to milliseconds. const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS; - const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom; + const from = roundTimestamp( + timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, + timeUnit + ); const offset = getDateHistogramOffset(from, interval); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 24f4bc2c678b4..003a6c3c20e98 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -94,12 +94,14 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('reports expected values to the action context', async () => { + const now = 1577858400000; await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); expect(action.reason).toContain('current value is 1'); expect(action.reason).toContain('threshold of 0.75'); expect(action.reason).toContain('test.metric.1'); + expect(action.timestamp).toBe(new Date(now).toISOString()); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4c02593dd0095..bc1cc24f65eeb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -76,11 +76,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s } } if (reason) { + const firstResult = first(alertResults); + const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, alertState: stateToAlertMessage[nextState], reason, - timestamp: moment().toISOString(), + timestamp, value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue), threshold: mapToConditionsLookup(criteria, (c) => c.threshold), metric: mapToConditionsLookup(criteria, (c) => c.metric), diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index ee2cf94a2fd62..c7e53eb2008f5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -12,6 +12,7 @@ const bucketsA = [ { doc_count: 3, aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, + key_as_string: new Date(1577858400000).toISOString(), }, ]; diff --git a/x-pack/plugins/infra/server/utils/round_timestamp.ts b/x-pack/plugins/infra/server/utils/round_timestamp.ts new file mode 100644 index 0000000000000..9b5ae2ac40197 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/round_timestamp.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Unit } from '@elastic/datemath'; +import moment from 'moment'; + +export const roundTimestamp = (timestamp: number, unit: Unit) => { + const floor = moment(timestamp).startOf(unit).valueOf(); + const ceil = moment(timestamp).add(1, unit).startOf(unit).valueOf(); + if (Math.abs(timestamp - floor) <= Math.abs(timestamp - ceil)) return floor; + return ceil; +}; From e1253ed0478b5c0276d5520d9f54a167a325fe0e Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Fri, 10 Jul 2020 18:23:51 -0400 Subject: [PATCH 04/10] [Security Solution][Ingest Manager][Endpoint] Optional ingest manager (#71198) --- x-pack/plugins/security_solution/kibana.json | 5 +++-- .../plugins/security_solution/public/plugin.tsx | 10 ++++++---- x-pack/plugins/security_solution/public/types.ts | 2 +- .../endpoint_app_context_services.test.ts | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 40d3402378895..29d0ab58e8b55 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -11,7 +11,6 @@ "embeddable", "features", "home", - "ingestManager", "taskManager", "inspector", "licensing", @@ -21,6 +20,7 @@ ], "optionalPlugins": [ "encryptedSavedObjects", + "ingestManager", "ml", "newsfeed", "security", @@ -33,6 +33,7 @@ "requiredBundles": [ "kibanaUtils", "esUiShared", - "kibanaReact" + "kibanaReact", + "ingestManager" ] } diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 7bb4be6b50879..62328bd767748 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -324,10 +324,12 @@ export class Plugin implements IPlugin { - // it('should return undefined on getAgentService if dependencies are not enabled', async () => { - // const endpointAppContextService = new EndpointAppContextService(); - // expect(endpointAppContextService.getAgentService()).toEqual(undefined); - // }); - // it('should return undefined on getManifestManager if dependencies are not enabled', async () => { - // const endpointAppContextService = new EndpointAppContextService(); - // expect(endpointAppContextService.getManifestManager()).toEqual(undefined); - // }); + it('should return undefined on getAgentService if dependencies are not enabled', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getAgentService()).toEqual(undefined); + }); + it('should return undefined on getManifestManager if dependencies are not enabled', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getManifestManager()).toEqual(undefined); + }); it('should throw error on getScopedSavedObjectsClient if start is not called', async () => { const endpointAppContextService = new EndpointAppContextService(); expect(() => From 79df8e01ad78d0b4ecb24edad14203c116990128 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 13 Jul 2020 09:26:34 +0300 Subject: [PATCH 05/10] improve bugfix 7198 test stability (#71250) * improve test stability * reenabled test Co-authored-by: Elastic Machine --- .../apps/discover/_doc_navigation.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 9bcf7fd2d73b5..5ae799f8756c0 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); // Flaky: https://github.com/elastic/kibana/issues/71216 - describe.skip('doc link in discover', function contextSize() { + describe('doc link in discover', function contextSize() { beforeEach(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.loadIfNeeded('discover'); @@ -63,20 +63,28 @@ export default function ({ getService, getPageObjects }) { await filterBar.addFilter('agent', 'is', 'Missing/Fields'); await PageObjects.discover.waitUntilSearchingHasFinished(); - // navigate to the doc view - await docTable.clickRowToggle({ rowIndex: 0 }); + await retry.try(async () => { + // navigate to the doc view + await docTable.clickRowToggle({ rowIndex: 0 }); - const details = await docTable.getDetailsRow(); - await docTable.addInclusiveFilter(details, 'referer'); - await PageObjects.discover.waitUntilSearchingHasFinished(); + const details = await docTable.getDetailsRow(); + await docTable.addInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); - const hasInclusiveFilter = await filterBar.hasFilter('referer', 'exists', true, false, true); - expect(hasInclusiveFilter).to.be(true); + const hasInclusiveFilter = await filterBar.hasFilter( + 'referer', + 'exists', + true, + false, + true + ); + expect(hasInclusiveFilter).to.be(true); - await docTable.removeInclusiveFilter(details, 'referer'); - await PageObjects.discover.waitUntilSearchingHasFinished(); - const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); - expect(hasExcludeFilter).to.be(true); + await docTable.removeInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); + expect(hasExcludeFilter).to.be(true); + }); }); }); } From 0c04d982065749e670295ec819da8796604e1a15 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 13 Jul 2020 01:53:31 -0700 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20use=20allow-li?= =?UTF-8?q?st=20in=20AppArch=20codebase=20(#71400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/data/common/field_formats/converters/url.ts | 4 ++-- .../public/state_management/url/hash_unhash_url.test.ts | 4 ++-- .../public/state_management/url/hash_unhash_url.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/common/field_formats/converters/url.ts b/src/plugins/data/common/field_formats/converters/url.ts index a0a498b6cab34..b797159b53486 100644 --- a/src/plugins/data/common/field_formats/converters/url.ts +++ b/src/plugins/data/common/field_formats/converters/url.ts @@ -30,7 +30,7 @@ import { } from '../types'; const templateMatchRE = /{{([\s\S]+?)}}/g; -const whitelistUrlSchemes = ['http://', 'https://']; +const allowedUrlSchemes = ['http://', 'https://']; const URL_TYPES = [ { @@ -161,7 +161,7 @@ export class UrlFormat extends FieldFormat { return this.generateImgHtml(url, imageLabel); default: - const inWhitelist = whitelistUrlSchemes.some((scheme) => url.indexOf(scheme) === 0); + const inWhitelist = allowedUrlSchemes.some((scheme) => url.indexOf(scheme) === 0); if (!inWhitelist && !parsedUrl) { return url; } diff --git a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts index ce8cd4acb24ab..8114c2d910cb2 100644 --- a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts @@ -116,7 +116,7 @@ describe('hash unhash url', () => { expect(mockStorage.length).toBe(3); }); - it('hashes only whitelisted properties', () => { + it('hashes only allow-listed properties', () => { const stateParamKey1 = '_g'; const stateParamValue1 = '(yes:!t)'; const stateParamKey2 = '_a'; @@ -227,7 +227,7 @@ describe('hash unhash url', () => { ); }); - it('unhashes only whitelisted properties', () => { + it('un-hashes only allow-listed properties', () => { const stateParamKey1 = '_g'; const stateParamValueHashed1 = 'h@4e60e02'; const state1 = { yes: true }; diff --git a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts index ec82bdeadedd5..aaeae65f094cd 100644 --- a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts @@ -35,7 +35,7 @@ export const hashUrl = createQueryReplacer(hashQuery); // naive hack, but this allows to decouple these utils from AppState, GlobalState for now // when removing AppState, GlobalState and migrating to IState containers, -// need to make sure that apps explicitly passing this whitelist to hash +// need to make sure that apps explicitly passing this allow-list to hash const __HACK_HARDCODED_LEGACY_HASHABLE_PARAMS = ['_g', '_a', '_s']; function createQueryMapper(queryParamMapper: (q: string) => string | null) { return ( From 60032b81ca698ac18daef5c7fcb210453e1377a2 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 13 Jul 2020 05:03:25 -0400 Subject: [PATCH 07/10] Bump lodash package version (#71392) --- package.json | 1 + yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a45f240ce13af..d58da61047d28 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "**/@types/hoist-non-react-statics": "^3.3.1", "**/@types/chai": "^4.2.11", "**/cypress/@types/lodash": "^4.14.155", + "**/cypress/lodash": "^4.15.19", "**/typescript": "3.9.5", "**/graphql-toolkit/lodash": "^4.17.15", "**/hoist-non-react-statics": "^3.3.2", diff --git a/yarn.lock b/yarn.lock index 390b89ea5ce7d..153f4e89fe969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20903,10 +20903,10 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.15.19, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== lodash@^3.10.1: version "3.10.1" From 639f8b7ca2ae74ebd4c2775e956a9c58742dcf9d Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 13 Jul 2020 13:28:42 +0300 Subject: [PATCH 08/10] Migrated agg table karma tests to jest (#71224) * Migrated karma tests to jest * Add comment Co-authored-by: Elastic Machine --- .../public/__tests__/vis_type_table/legacy.ts | 39 ----- .../public/agg_table/agg_table.test.js} | 153 ++++++++++-------- .../public/agg_table/agg_table_group.test.js} | 50 +++--- .../public/agg_table}/tabified_data.js | 0 .../public/paginated_table/rows.js | 5 +- 5 files changed, 125 insertions(+), 122 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts rename src/{legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js => plugins/vis_type_table/public/agg_table/agg_table.test.js} (75%) rename src/{legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js => plugins/vis_type_table/public/agg_table/agg_table_group.test.js} (74%) rename src/{legacy/core_plugins/kibana/public/__tests__/vis_type_table => plugins/vis_type_table/public/agg_table}/tabified_data.js (100%) diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts deleted file mode 100644 index 216afe5920408..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { PluginInitializerContext } from 'kibana/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart, npSetup } from 'ui/new_platform'; -import { - TableVisPlugin, - TablePluginSetupDependencies, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/vis_type_table/public/plugin'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = new TableVisPlugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { - data: npStart.plugins.data, - kibanaLegacy: npStart.plugins.kibanaLegacy, -}); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.test.js similarity index 75% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js rename to src/plugins/vis_type_table/public/agg_table/agg_table.test.js index 88eb299e3c3a8..0362bd55963d9 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.test.js @@ -19,44 +19,71 @@ import $ from 'jquery'; import moment from 'moment'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; +import angular from 'angular'; +import 'angular-mocks'; import sinon from 'sinon'; -import './legacy'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart } from 'ui/new_platform'; import { round } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInnerAngular } from '../../../../../../plugins/vis_type_table/public/get_inner_angular'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { initTableVisLegacyModule } from '../../../../../../plugins/vis_type_table/public/table_vis_legacy_module'; +import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; +import { coreMock } from '../../../../core/public/mocks'; +import { initAngularBootstrap } from '../../../kibana_legacy/public'; +import { setUiSettings } from '../../../data/public/services'; +import { UI_SETTINGS } from '../../../data/public/'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; + +import { setFormatService } from '../services'; +import { getInnerAngular } from '../get_inner_angular'; +import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { configureAppAngularModule } from '../../../../../../plugins/kibana_legacy/public/angular'; + +const uiSettings = new Map(); describe('Table Vis - AggTable Directive', function () { + const core = coreMock.createStart(); + + core.uiSettings.set = jest.fn((key, value) => { + uiSettings.set(key, value); + }); + + core.uiSettings.get = jest.fn((key) => { + const defaultValues = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + 'dateFormat:tz': 'UTC', + [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, + [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, + [CSV_SEPARATOR_SETTING]: ',', + [CSV_QUOTE_VALUES_SETTING]: true, + }; + + return defaultValues[key] || uiSettings.get(key); + }); + let $rootScope; let $compile; let settings; const initLocalAngular = () => { - const tableVisModule = getInnerAngular('kibana/table_vis', npStart.core); - configureAppAngularModule(tableVisModule, npStart.core, true); + const tableVisModule = getInnerAngular('kibana/table_vis', core); initTableVisLegacyModule(tableVisModule); }; - beforeEach(initLocalAngular); - - beforeEach(ngMock.module('kibana/table_vis')); - beforeEach( - ngMock.inject(function ($injector, config) { + beforeEach(() => { + setUiSettings(core.uiSettings); + setFormatService(getFieldFormatsRegistry(core)); + initAngularBootstrap(); + initLocalAngular(); + angular.mock.module('kibana/table_vis'); + angular.mock.inject(($injector, config) => { settings = config; $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); - }) - ); + }); + }); let $scope; beforeEach(function () { @@ -66,7 +93,7 @@ describe('Table Vis - AggTable Directive', function () { $scope.$destroy(); }); - it('renders a simple response properly', function () { + test('renders a simple response properly', function () { $scope.dimensions = { metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], buckets: [], @@ -78,12 +105,12 @@ describe('Table Vis - AggTable Directive', function () { ); $scope.$digest(); - expect($el.find('tbody').length).to.be(1); - expect($el.find('td').length).to.be(1); - expect($el.find('td').text()).to.eql('1,000'); + expect($el.find('tbody').length).toBe(1); + expect($el.find('td').length).toBe(1); + expect($el.find('td').text()).toEqual('1,000'); }); - it('renders nothing if the table is empty', function () { + test('renders nothing if the table is empty', function () { $scope.dimensions = {}; $scope.table = null; const $el = $compile('')( @@ -91,10 +118,10 @@ describe('Table Vis - AggTable Directive', function () { ); $scope.$digest(); - expect($el.find('tbody').length).to.be(0); + expect($el.find('tbody').length).toBe(0); }); - it('renders a complex response properly', async function () { + test('renders a complex response properly', async function () { $scope.dimensions = { buckets: [ { accessor: 0, params: {} }, @@ -112,37 +139,37 @@ describe('Table Vis - AggTable Directive', function () { $compile($el)($scope); $scope.$digest(); - expect($el.find('tbody').length).to.be(1); + expect($el.find('tbody').length).toBe(1); const $rows = $el.find('tbody tr'); - expect($rows.length).to.be.greaterThan(0); + expect($rows.length).toBeGreaterThan(0); function validBytes(str) { const num = str.replace(/,/g, ''); if (num !== '-') { - expect(num).to.match(/^\d+$/); + expect(num).toMatch(/^\d+$/); } } $rows.each(function () { // 6 cells in every row const $cells = $(this).find('td'); - expect($cells.length).to.be(6); + expect($cells.length).toBe(6); const txts = $cells.map(function () { return $(this).text().trim(); }); // two character country code - expect(txts[0]).to.match(/^(png|jpg|gif|html|css)$/); + expect(txts[0]).toMatch(/^(png|jpg|gif|html|css)$/); validBytes(txts[1]); // country - expect(txts[2]).to.match(/^\w\w$/); + expect(txts[2]).toMatch(/^\w\w$/); validBytes(txts[3]); // os - expect(txts[4]).to.match(/^(win|mac|linux)$/); + expect(txts[4]).toMatch(/^(win|mac|linux)$/); validBytes(txts[5]); }); }); @@ -153,9 +180,9 @@ describe('Table Vis - AggTable Directive', function () { moment.tz.setDefault(settings.get('dateFormat:tz')); } - const off = $scope.$on('change:config.dateFormat:tz', setDefaultTimezone); const oldTimezoneSetting = settings.get('dateFormat:tz'); settings.set('dateFormat:tz', 'UTC'); + setDefaultTimezone(); $scope.dimensions = { buckets: [ @@ -181,24 +208,24 @@ describe('Table Vis - AggTable Directive', function () { $compile($el)($scope); $scope.$digest(); - expect($el.find('tfoot').length).to.be(1); + expect($el.find('tfoot').length).toBe(1); const $rows = $el.find('tfoot tr'); - expect($rows.length).to.be(1); + expect($rows.length).toBe(1); const $cells = $($rows[0]).find('th'); - expect($cells.length).to.be(6); + expect($cells.length).toBe(6); for (let i = 0; i < 6; i++) { - expect($($cells[i]).text().trim()).to.be(expected[i]); + expect($($cells[i]).text().trim()).toBe(expected[i]); } settings.set('dateFormat:tz', oldTimezoneSetting); - off(); + setDefaultTimezone(); } - it('as count', async function () { + test('as count', async function () { await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); }); - it('as min', async function () { + test('as min', async function () { await totalsRowTest('min', [ '', '2014-09-28', @@ -208,7 +235,7 @@ describe('Table Vis - AggTable Directive', function () { '11', ]); }); - it('as max', async function () { + test('as max', async function () { await totalsRowTest('max', [ '', '2014-10-03', @@ -218,16 +245,16 @@ describe('Table Vis - AggTable Directive', function () { '837', ]); }); - it('as avg', async function () { + test('as avg', async function () { await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); }); - it('as sum', async function () { + test('as sum', async function () { await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); }); }); describe('aggTable.toCsv()', function () { - it('escapes rows and columns properly', function () { + test('escapes rows and columns properly', function () { const $el = $compile('')( $scope ); @@ -244,12 +271,12 @@ describe('Table Vis - AggTable Directive', function () { rows: [{ a: 1, b: 2, c: '"foobar"' }], }; - expect(aggTable.toCsv()).to.be( + expect(aggTable.toCsv()).toBe( 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' ); }); - it('exports rows and columns properly', async function () { + test('exports rows and columns properly', async function () { $scope.dimensions = { buckets: [ { accessor: 0, params: {} }, @@ -274,7 +301,7 @@ describe('Table Vis - AggTable Directive', function () { $tableScope.table = $scope.table; const raw = aggTable.toCsv(false); - expect(raw).to.be( + expect(raw).toBe( '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + '\r\n' + 'png,412032,IT,9299,win,0' + @@ -304,7 +331,7 @@ describe('Table Vis - AggTable Directive', function () { ); }); - it('exports formatted rows and columns properly', async function () { + test('exports formatted rows and columns properly', async function () { $scope.dimensions = { buckets: [ { accessor: 0, params: {} }, @@ -332,7 +359,7 @@ describe('Table Vis - AggTable Directive', function () { $tableScope.formattedColumns[0].formatter.convert = (v) => `${v}_formatted`; const formatted = aggTable.toCsv(true); - expect(formatted).to.be( + expect(formatted).toBe( '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + '\r\n' + '"png_formatted",412032,IT,9299,win,0' + @@ -363,7 +390,7 @@ describe('Table Vis - AggTable Directive', function () { }); }); - it('renders percentage columns', async function () { + test('renders percentage columns', async function () { $scope.dimensions = { buckets: [ { accessor: 0, params: {} }, @@ -390,8 +417,8 @@ describe('Table Vis - AggTable Directive', function () { $scope.$digest(); const $headings = $el.find('th'); - expect($headings.length).to.be(7); - expect($headings.eq(3).text().trim()).to.be('Average bytes percentages'); + expect($headings.length).toBe(7); + expect($headings.eq(3).text().trim()).toBe('Average bytes percentages'); const countColId = $scope.table.columns.find((col) => col.name === $scope.percentageCol).id; const counts = $scope.table.rows.map((row) => row[countColId]); @@ -400,7 +427,7 @@ describe('Table Vis - AggTable Directive', function () { $percentageColValues.each((i, value) => { const percentage = `${round((counts[i] / total) * 100, 3)}%`; - expect(value).to.be(percentage); + expect(value).toBe(percentage); }); }); @@ -420,7 +447,7 @@ describe('Table Vis - AggTable Directive', function () { window.Blob = origBlob; }); - it('calls _saveAs properly', function () { + test('calls _saveAs properly', function () { const $el = $compile('')($scope); $scope.$digest(); @@ -440,19 +467,19 @@ describe('Table Vis - AggTable Directive', function () { aggTable.csv.filename = 'somefilename.csv'; aggTable.exportAsCsv(); - expect(saveAs.callCount).to.be(1); + expect(saveAs.callCount).toBe(1); const call = saveAs.getCall(0); - expect(call.args[0]).to.be.a(FakeBlob); - expect(call.args[0].slices).to.eql([ + expect(call.args[0]).toBeInstanceOf(FakeBlob); + expect(call.args[0].slices).toEqual([ 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', ]); - expect(call.args[0].opts).to.eql({ + expect(call.args[0].opts).toEqual({ type: 'text/plain;charset=utf-8', }); - expect(call.args[1]).to.be('somefilename.csv'); + expect(call.args[1]).toBe('somefilename.csv'); }); - it('should use the export-title attribute', function () { + test('should use the export-title attribute', function () { const expected = 'export file name'; const $el = $compile( `` @@ -468,7 +495,7 @@ describe('Table Vis - AggTable Directive', function () { $tableScope.exportTitle = expected; $scope.$digest(); - expect(aggTable.csv.filename).to.equal(`${expected}.csv`); + expect(aggTable.csv.filename).toEqual(`${expected}.csv`); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js similarity index 74% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js rename to src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js index 99b397167009d..43913eed32f90 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js @@ -18,38 +18,50 @@ */ import $ from 'jquery'; -import ngMock from 'ng_mock'; +import angular from 'angular'; +import 'angular-mocks'; import expect from '@kbn/expect'; -import './legacy'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInnerAngular } from '../../../../../../plugins/vis_type_table/public/get_inner_angular'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { initTableVisLegacyModule } from '../../../../../../plugins/vis_type_table/public/table_vis_legacy_module'; + +import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; +import { coreMock } from '../../../../core/public/mocks'; +import { initAngularBootstrap } from '../../../kibana_legacy/public'; +import { setUiSettings } from '../../../data/public/services'; +import { setFormatService } from '../services'; +import { getInnerAngular } from '../get_inner_angular'; +import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { configureAppAngularModule } from '../../../../../../plugins/kibana_legacy/public/angular'; + +const uiSettings = new Map(); describe('Table Vis - AggTableGroup Directive', function () { + const core = coreMock.createStart(); let $rootScope; let $compile; + core.uiSettings.set = jest.fn((key, value) => { + uiSettings.set(key, value); + }); + + core.uiSettings.get = jest.fn((key) => { + return uiSettings.get(key); + }); + const initLocalAngular = () => { - const tableVisModule = getInnerAngular('kibana/table_vis', npStart.core); - configureAppAngularModule(tableVisModule, npStart.core, true); + const tableVisModule = getInnerAngular('kibana/table_vis', core); initTableVisLegacyModule(tableVisModule); }; - beforeEach(initLocalAngular); - - beforeEach(ngMock.module('kibana/table_vis')); - beforeEach( - ngMock.inject(function ($injector) { + beforeEach(() => { + setUiSettings(core.uiSettings); + setFormatService(getFieldFormatsRegistry(core)); + initAngularBootstrap(); + initLocalAngular(); + angular.mock.module('kibana/table_vis'); + angular.mock.inject(($injector) => { $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); - }) - ); + }); + }); let $scope; beforeEach(function () { diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/tabified_data.js b/src/plugins/vis_type_table/public/agg_table/tabified_data.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/tabified_data.js rename to src/plugins/vis_type_table/public/agg_table/tabified_data.js diff --git a/src/plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/paginated_table/rows.js index d2192a5843644..d8f01a10c63fa 100644 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ b/src/plugins/vis_type_table/public/paginated_table/rows.js @@ -19,6 +19,7 @@ import $ from 'jquery'; import _ from 'lodash'; +import angular from 'angular'; import tableCellFilterHtml from './table_cell_filter.html'; export function KbnRows($compile) { @@ -65,7 +66,9 @@ export function KbnRows($compile) { if (column.filterable && contentsIsDefined) { $cell = createFilterableCell(contents); - $cellContent = $cell.find('[data-cell-content]'); + // in jest tests 'angular' is using jqLite. In jqLite the method find lookups only by tags. + // Because of this, we should change a way how we get cell content so that tests will pass. + $cellContent = angular.element($cell[0].querySelector('[data-cell-content]')); } else { $cell = $cellContent = createCell(); } From 3fc8c7af258293e9c8045ac2fed046582aa84f9f Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 13 Jul 2020 13:47:12 +0300 Subject: [PATCH 09/10] Validate incoming url timerange (#70948) * validate incoming url timerange * adjust discover test * fix tests * stabilize tests * oops Co-authored-by: Elastic Machine --- src/plugins/data/public/public.api.md | 2 +- .../state_sync/connect_to_query_state.ts | 5 +- .../data/public/query/timefilter/index.ts | 1 + .../timefilter/lib/validate_timerange.test.ts | 52 +++++++++++++++++++ .../timefilter/lib/validate_timerange.ts | 28 ++++++++++ test/functional/apps/discover/_discover.js | 24 +++++---- 6 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 src/plugins/data/public/query/timefilter/lib/validate_timerange.test.ts create mode 100644 src/plugins/data/public/query/timefilter/lib/validate_timerange.ts diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 01fcefe27df3e..b532bacf5df25 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1991,7 +1991,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:40:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:41:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:61:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index e74497a5053b4..2e62dac87f6ef 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -24,6 +24,7 @@ import { BaseStateContainer } from '../../../../kibana_utils/public'; import { QuerySetup, QueryStart } from '../query_service'; import { QueryState, QueryStateChange } from './types'; import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common'; +import { validateTimeRange } from '../timefilter'; /** * Helper to setup two-way syncing of global data and a state container @@ -159,9 +160,9 @@ export const connectToQueryState = ( // cloneDeep is required because services are mutating passed objects // and state in state container is frozen if (syncConfig.time) { - const time = state.time || timefilter.getTimeDefaults(); + const time = validateTimeRange(state.time) ? state.time : timefilter.getTimeDefaults(); if (!_.isEqual(time, timefilter.getTime())) { - timefilter.setTime(_.cloneDeep(time)); + timefilter.setTime(_.cloneDeep(time!)); } } diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index f71061677ceb7..19386c10ab59f 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -24,3 +24,4 @@ export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; +export { validateTimeRange } from './lib/validate_timerange'; diff --git a/src/plugins/data/public/query/timefilter/lib/validate_timerange.test.ts b/src/plugins/data/public/query/timefilter/lib/validate_timerange.test.ts new file mode 100644 index 0000000000000..e20849c21a717 --- /dev/null +++ b/src/plugins/data/public/query/timefilter/lib/validate_timerange.test.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { validateTimeRange } from './validate_timerange'; + +describe('Validate timerange', () => { + test('Validate no range', () => { + const ok = validateTimeRange(); + + expect(ok).toBe(false); + }); + test('normal range', () => { + const ok = validateTimeRange({ + to: 'now', + from: 'now-7d', + }); + + expect(ok).toBe(true); + }); + test('bad from time', () => { + const ok = validateTimeRange({ + to: 'nowa', + from: 'now-7d', + }); + + expect(ok).toBe(false); + }); + test('bad to time', () => { + const ok = validateTimeRange({ + to: 'now', + from: 'nowa-7d', + }); + + expect(ok).toBe(false); + }); +}); diff --git a/src/plugins/data/public/query/timefilter/lib/validate_timerange.ts b/src/plugins/data/public/query/timefilter/lib/validate_timerange.ts new file mode 100644 index 0000000000000..f9e4aa0ae1cab --- /dev/null +++ b/src/plugins/data/public/query/timefilter/lib/validate_timerange.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import dateMath from '@elastic/datemath'; +import { TimeRange } from '../../../../common'; + +export function validateTimeRange(time?: TimeRange): boolean { + if (!time) return false; + const momentDateFrom = dateMath.parse(time.from); + const momentDateTo = dateMath.parse(time.to); + return !!(momentDateFrom && momentDateFrom.isValid() && momentDateTo && momentDateTo.isValid()); +} diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 47741c1ab8a0d..94a271987ecdf 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -254,6 +254,19 @@ export default function ({ getService, getPageObjects }) { }); }); + describe('invalid time range in URL', function () { + it('should get the default timerange', async function () { + const prevTime = await PageObjects.timePicker.getTimeConfig(); + await PageObjects.common.navigateToUrl('discover', '#/?_g=(time:(from:now-15m,to:null))', { + useActualUrl: true, + }); + await PageObjects.header.awaitKibanaChrome(); + const time = await PageObjects.timePicker.getTimeConfig(); + expect(time.start).to.be(prevTime.start); + expect(time.end).to.be(prevTime.end); + }); + }); + describe('empty query', function () { it('should update the histogram timerange when the query is resubmitted', async function () { await kibanaServer.uiSettings.update({ @@ -268,17 +281,6 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('invalid time range in URL', function () { - it('should display a "Invalid time range toast"', async function () { - await PageObjects.common.navigateToUrl('discover', '#/?_g=(time:(from:now-15m,to:null))', { - useActualUrl: true, - }); - await PageObjects.header.awaitKibanaChrome(); - const toastMessage = await PageObjects.common.closeToast(); - expect(toastMessage).to.be('Invalid time range'); - }); - }); - describe('managing fields', function () { it('should add a field, sort by it, remove it and also sorting by it', async function () { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); From cd2d1b8053da9ca37b44fc3edc9c83bd72eecc46 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 13 Jul 2020 13:00:44 +0200 Subject: [PATCH 10/10] [SIEM][Detections] Fixes text layout in Schedule step (#71306) * fixes text layout in schedule step * Removes unused import Co-authored-by: Garrett Spong Co-authored-by: Elastic Machine --- .../rules/step_schedule_rule/index.tsx | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index 60855bc5fa25f..fa0f4dbd3668c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -6,7 +6,6 @@ import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import styled from 'styled-components'; import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { @@ -25,10 +24,6 @@ interface StepScheduleRuleProps extends RuleStepProps { defaultValues?: ScheduleStepRule | null; } -const RestrictedWidthContainer = styled.div` - max-width: 300px; -`; - const stepScheduleDefaultValue = { interval: '5m', isNew: true, @@ -93,29 +88,25 @@ const StepScheduleRuleComponent: FC = ({ <>
- - - - - - + +