diff --git a/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx b/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx index fc4d050d71d59..5bf94109d3b9f 100644 --- a/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx +++ b/src/plugins/controls/public/controls/timeslider_control/components/time_slider_popover_content.tsx @@ -8,6 +8,8 @@ */ import React from 'react'; +import { useMemo, useEffect, useState } from 'react'; +import { debounce } from 'lodash'; import { EuiButtonIcon, EuiRangeTick, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { TimeSliderStrings } from './time_slider_strings'; @@ -27,29 +29,63 @@ interface Props { compressed: boolean; } -export function TimeSliderPopoverContent(props: Props) { - const rangeInput = props.isAnchored ? ( +export function TimeSliderPopoverContent({ + isAnchored, + setIsAnchored, + value, + onChange, + stepSize, + ticks, + timeRangeMin, + timeRangeMax, + compressed, +}: Props) { + const [displayedValue, setDisplayedValue] = useState(value); + + const debouncedOnChange = useMemo( + () => + debounce((updateTimeslice: Timeslice | undefined) => { + onChange(updateTimeslice); + }, 750), + [onChange] + ); + + /** + * The following `useEffect` ensures that the changes to the value that come from the embeddable (for example, + * from the `clear` button on the dashboard) are reflected in the displayed value + */ + useEffect(() => { + setDisplayedValue(value); + }, [value]); + + const rangeInput = isAnchored ? ( { + setDisplayedValue(newValue as Timeslice); + debouncedOnChange(newValue); + }} + stepSize={stepSize} + ticks={ticks} + timeRangeMin={timeRangeMin} + timeRangeMax={timeRangeMax} + compressed={compressed} /> ) : ( { + setDisplayedValue(newValue as Timeslice); + debouncedOnChange(newValue); + }} + stepSize={stepSize} + ticks={ticks} + timeRangeMin={timeRangeMin} + timeRangeMax={timeRangeMax} + compressed={compressed} /> ); - const anchorStartToggleButtonLabel = props.isAnchored + const anchorStartToggleButtonLabel = isAnchored ? TimeSliderStrings.control.getUnpinStart() : TimeSliderStrings.control.getPinStart(); @@ -59,17 +95,24 @@ export function TimeSliderPopoverContent(props: Props) { gutterSize="none" data-test-subj="timeSlider-popoverContents" responsive={false} + onMouseUp={() => { + // when the pin is dropped (on mouse up), cancel any pending debounced changes and force the change + // in value to happen instantly (which, in turn, will re-calculate the from/to for the slider due to + // the `useEffect` above. + debouncedOnChange.cancel(); + onChange(displayedValue); + }} > { - const nextIsAnchored = !props.isAnchored; + const nextIsAnchored = !isAnchored; if (nextIsAnchored) { - props.onChange([props.timeRangeMin, props.value[1]]); + onChange([timeRangeMin, value[1]]); } - props.setIsAnchored(nextIsAnchored); + setIsAnchored(nextIsAnchored); }} aria-label={anchorStartToggleButtonLabel} data-test-subj="timeSlider__anchorStartToggleButton" diff --git a/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx b/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx index 59ad0a2a5076c..7e81fa075334e 100644 --- a/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/src/plugins/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx @@ -270,7 +270,6 @@ export const getTimesliderControlFactory = (): ControlFactory< Component: (controlPanelClassNames) => { const [isAnchored, isPopoverOpen, timeRangeMeta, timeslice] = useBatchedPublishingSubjects(isAnchored$, isPopoverOpen$, timeRangeMeta$, timeslice$); - useEffect(() => { return () => { cleanupTimeRangeSubscription(); @@ -284,6 +283,9 @@ export const getTimesliderControlFactory = (): ControlFactory< const to = useMemo(() => { return timeslice ? timeslice[TO_INDEX] : timeRangeMeta.timeRangeMax; }, [timeslice, timeRangeMeta.timeRangeMax]); + const value: Timeslice = useMemo(() => { + return [from, to]; + }, [from, to]); return ( { diff --git a/src/plugins/kibana_overview/public/components/_index.scss b/src/plugins/kibana_overview/public/components/_index.scss deleted file mode 100644 index b8857d171728f..0000000000000 --- a/src/plugins/kibana_overview/public/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'overview'; diff --git a/src/plugins/kibana_overview/public/components/app.tsx b/src/plugins/kibana_overview/public/components/app.tsx index 581356f83d358..50233396f3c48 100644 --- a/src/plugins/kibana_overview/public/components/app.tsx +++ b/src/plugins/kibana_overview/public/components/app.tsx @@ -13,11 +13,11 @@ import { I18nProvider } from '@kbn/i18n-react'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { FetchResult } from '@kbn/newsfeed-plugin/public'; import { Route, Routes } from '@kbn/shared-ux-router'; +import { withSuspense } from '@kbn/shared-ux-utility'; import React, { useEffect, useState } from 'react'; import { HashRouter as Router } from 'react-router-dom'; import useObservable from 'react-use/lib/useObservable'; import type { Observable } from 'rxjs'; -import { Overview } from './overview'; interface KibanaOverviewAppDeps { basename: string; @@ -48,6 +48,14 @@ export const KibanaOverviewApp = ({ } }, [newsfeed$]); + const Overview = withSuspense( + React.lazy(() => + import('./overview').then(({ Overview: OverviewComponent }) => { + return { default: OverviewComponent }; + }) + ) + ); + return ( diff --git a/src/plugins/kibana_overview/public/components/_overview.scss b/src/plugins/kibana_overview/public/components/overview/overview.scss similarity index 100% rename from src/plugins/kibana_overview/public/components/_overview.scss rename to src/plugins/kibana_overview/public/components/overview/overview.scss diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 4d21adeecd377..c4a36e966142b 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import './overview.scss'; + import { snakeCase } from 'lodash'; import React, { FC, useState, useEffect } from 'react'; import useObservable from 'react-use/lib/useObservable'; diff --git a/src/plugins/kibana_overview/public/index.scss b/src/plugins/kibana_overview/public/index.scss deleted file mode 100644 index 841415620d691..0000000000000 --- a/src/plugins/kibana_overview/public/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'components/index'; diff --git a/src/plugins/kibana_overview/public/index.ts b/src/plugins/kibana_overview/public/index.ts index 5091cd83ae67c..6fb3f0cc4a6e3 100644 --- a/src/plugins/kibana_overview/public/index.ts +++ b/src/plugins/kibana_overview/public/index.ts @@ -7,8 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import './index.scss'; - import { KibanaOverviewPlugin } from './plugin'; // This exports static code and TypeScript types, diff --git a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts index 5b1a48cf940b7..114ff1aec9568 100644 --- a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -99,4 +99,5 @@ export const edgeDataSchema = schema.object({ source: schema.string(), target: schema.string(), color: colorSchema, + type: schema.maybe(schema.oneOf([schema.literal('solid'), schema.literal('dashed')])), }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx index 4b17004865502..6b7569dcddd8d 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiFlexItem, EUI_MODAL_CONFIRM_BUTTON, + EuiSpacer, } from '@elastic/eui'; import type { DeleteAction } from './use_delete_action'; @@ -79,16 +80,22 @@ export const DeleteActionModal: FC = ({ {userCanDeleteIndex && dataViewExists && ( - + <> + + + )} diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx index 9fe4da68aa6f8..08360eb4f2cca 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx @@ -281,7 +281,18 @@ export function AnalyticsIdSelector({ {renderTabs()} - + + + + {i18n.translate('xpack.ml.analyticsSelector.closeFlyoutButton', { + defaultMessage: 'Close', + })} + + - - - {i18n.translate('xpack.ml.analyticsSelector.closeFlyoutButton', { - defaultMessage: 'Close', - })} - - diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 5920e6a05eb29..3d5b5169d9c14 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { JOB_STATUS } from '@kbn/reporting-common'; import { ReportDocument } from '@kbn/reporting-common/types'; @@ -212,7 +212,7 @@ describe('ReportingStore', () => { const [[updateCall]] = mockEsClient.update.mock.calls; - const response = (updateCall as estypes.UpdateRequest).body?.doc as Report; + const response = (updateCall as estypes.UpdateRequest)?.doc as Report; expect(response.migration_version).toBe(`7.14.0`); expect(response.status).toBe(`processing`); expect(updateCall.if_seq_no).toBe(42); @@ -242,7 +242,7 @@ describe('ReportingStore', () => { await store.setReportFailed(report, { errors: 'yes' } as any); const [[updateCall]] = mockEsClient.update.mock.calls; - const response = (updateCall as estypes.UpdateRequest).body?.doc as Report; + const response = (updateCall as estypes.UpdateRequest)?.doc as Report; expect(response.migration_version).toBe(`7.14.0`); expect(response.status).toBe(`failed`); expect(updateCall.if_seq_no).toBe(43); @@ -272,7 +272,7 @@ describe('ReportingStore', () => { await store.setReportError(report, { errors: 'yes' } as any); const [[updateCall]] = mockEsClient.update.mock.calls; - const response = (updateCall as estypes.UpdateRequest).body?.doc as Report; + const response = (updateCall as estypes.UpdateRequest)?.doc as Report; expect(response.migration_version).toBe(`7.14.0`); expect(updateCall.if_seq_no).toBe(43); expect(updateCall.if_primary_term).toBe(10002); @@ -301,7 +301,7 @@ describe('ReportingStore', () => { await store.setReportCompleted(report, { certainly_completed: 'yes' } as any); const [[updateCall]] = mockEsClient.update.mock.calls; - const response = (updateCall as estypes.UpdateRequest).body?.doc as Report; + const response = (updateCall as estypes.UpdateRequest)?.doc as Report; expect(response.migration_version).toBe(`7.14.0`); expect(response.status).toBe(`completed`); expect(updateCall.if_seq_no).toBe(44); @@ -336,7 +336,7 @@ describe('ReportingStore', () => { } as any); const [[updateCall]] = mockEsClient.update.mock.calls; - const response = (updateCall as estypes.UpdateRequest).body?.doc as Report; + const response = (updateCall as estypes.UpdateRequest)?.doc as Report; expect(response.migration_version).toBe(`7.14.0`); expect(response.status).toBe(`completed_with_warnings`); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 05b34f2c04533..85da045996279 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -86,7 +86,7 @@ const esDocForUpdate = ( if_seq_no: report._seq_no, if_primary_term: report._primary_term, refresh: 'wait_for' as estypes.Refresh, - body: { doc }, + doc, }; }; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 4ca908310c78c..80cbb1e71a5d3 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -12,7 +12,7 @@ import { Writable } from 'stream'; import { finished } from 'stream/promises'; import { setTimeout } from 'timers/promises'; -import { UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { UpdateResponse } from '@elastic/elasticsearch/lib/api/types'; import type { Logger } from '@kbn/core/server'; import { CancellationToken, diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx index dd1c956a55e55..b4f35af2054f4 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { ThemeProvider } from '@emotion/react'; import { ReactFlow, @@ -17,14 +17,18 @@ import { useEdgesState, type BuiltInNode, type NodeProps, + type Node as xyNode, + type Edge as xyEdge, } from '@xyflow/react'; +import { isEmpty, isEqual, pick, size, xorWith } from 'lodash'; import { Story } from '@storybook/react'; -import { SvgDefsMarker } from './styles'; import { DefaultEdge } from '.'; +import { LabelNode } from '../node'; +import type { EdgeViewModel } from '../types'; +import { SvgDefsMarker } from './markers'; import '@xyflow/react/dist/style.css'; -import { LabelNode } from '../node'; -import type { NodeViewModel } from '../types'; +import { HandleStyleOverride } from '../node/styles'; export default { title: 'Components/Graph Components/Default Edge', @@ -34,22 +38,32 @@ export default { options: ['primary', 'danger', 'warning'], control: { type: 'radio' }, }, + type: { + options: ['solid', 'dashed'], + control: { type: 'radio' }, + }, }, }; const nodeTypes = { - default: ((props: NodeProps) => { - const handleStyle = { - width: 0, - height: 0, - 'min-width': 0, - 'min-height': 0, - border: 'none', - }; + // eslint-disable-next-line react/display-name + default: React.memo((props: NodeProps) => { return (
- - + + {props.data.label}
); @@ -61,66 +75,87 @@ const edgeTypes = { default: DefaultEdge, }; -const Template: Story = (args: NodeViewModel) => { - const initialNodes = [ - { - id: 'source', - type: 'default', - data: { label: 'source' }, - position: { x: 0, y: 0 }, - draggable: true, - }, - { - id: 'target', - type: 'default', - data: { label: 'target' }, - position: { x: 320, y: 100 }, - draggable: true, - }, - { - id: args.id, - type: 'label', - data: args, - position: { x: 160, y: 50 }, - draggable: true, - }, - ]; +const Template: Story = (args: EdgeViewModel) => { + const nodes = useMemo( + () => [ + { + id: 'source', + type: 'default', + data: { + label: 'source', + }, + position: { x: 0, y: 0 }, + }, + { + id: 'target', + type: 'default', + data: { + label: 'target', + }, + position: { x: 420, y: 0 }, + }, + { + id: args.id, + type: 'label', + data: pick(args, ['id', 'label', 'interactive', 'source', 'target', 'color', 'type']), + position: { x: 230, y: 6 }, + }, + ], + [args] + ); - const initialEdges = [ - { - id: `source-${args.id}`, - source: 'source', - target: args.id, - data: { + const edges = useMemo( + () => [ + { id: `source-${args.id}`, source: 'source', - sourceShape: 'rectangle', target: args.id, - targetShape: 'label', - color: args.color, - interactive: true, + data: { + id: `source-${args.id}`, + source: 'source', + sourceShape: 'custom', + target: args.id, + targetShape: 'label', + color: args.color, + type: args.type, + }, + type: 'default', }, - type: 'default', - }, - { - id: `${args.id}-target`, - source: args.id, - target: 'target', - data: { + { id: `${args.id}-target`, source: args.id, - sourceShape: 'label', target: 'target', - targetShape: 'rectangle', - color: args.color, - interactive: true, + data: { + id: `${args.id}-target`, + source: args.id, + sourceShape: 'label', + target: 'target', + targetShape: 'custom', + color: args.color, + type: args.type, + }, + type: 'default', }, - type: 'default', - }, - ]; + ], + [args] + ); - const [nodes, _setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, _setEdges, onEdgesChange] = useEdgesState(initialEdges); + const [nodesState, setNodes, onNodesChange] = useNodesState(nodes); + const [edgesState, setEdges, onEdgesChange] = useEdgesState>(edges); + const currNodesRef = useRef(nodes); + const currEdgesRef = useRef(edges); + + useEffect(() => { + if ( + !isArrayOfObjectsEqual(nodes, currNodesRef.current) || + !isArrayOfObjectsEqual(edges, currEdgesRef.current) + ) { + setNodes(nodes); + setEdges(edges); + currNodesRef.current = nodes; + currEdgesRef.current = edges; + } + }, [setNodes, setEdges, nodes, edges]); return ( @@ -128,12 +163,13 @@ const Template: Story = (args: NodeViewModel) => { @@ -148,6 +184,9 @@ Edge.args = { id: 'siem-windows', label: 'User login to OKTA', color: 'primary', - icon: 'okta', interactive: true, + type: 'solid', }; + +const isArrayOfObjectsEqual = (x: object[], y: object[]) => + size(x) === size(y) && isEmpty(xorWith(x, y, isEqual)); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx index 6826c47b270ce..370c7b3909973 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; -import { BaseEdge, getBezierPath } from '@xyflow/react'; +import { BaseEdge, getSmoothStepPath } from '@xyflow/react'; import { useEuiTheme } from '@elastic/eui'; -import type { Color } from '@kbn/cloud-security-posture-common/types/graph/latest'; -import type { EdgeProps } from '../types'; -import { getMarker } from './styles'; +import type { EdgeProps, EdgeViewModel } from '../types'; import { getShapeHandlePosition } from './utils'; +import { getMarkerStart, getMarkerEnd } from './markers'; + +type EdgeColor = EdgeViewModel['color']; export function DefaultEdge({ id, @@ -25,9 +26,9 @@ export function DefaultEdge({ data, }: EdgeProps) { const { euiTheme } = useEuiTheme(); - const color: Color = data?.color ?? 'primary'; + const color: EdgeColor = data?.color ?? 'primary'; - const [edgePath] = getBezierPath({ + const [edgePath] = getSmoothStepPath({ // sourceX and targetX are adjusted to account for the shape handle position sourceX: sourceX - getShapeHandlePosition(data?.sourceShape), sourceY, @@ -35,12 +36,8 @@ export function DefaultEdge({ targetX: targetX + getShapeHandlePosition(data?.targetShape), targetY, targetPosition, - curvature: - 0.1 * - (data?.sourceShape === 'group' || - (data?.sourceShape === 'label' && data?.targetShape === 'group') - ? -1 // We flip direction when the edge is between parent node to child nodes (groups always contain children in our graph) - : 1), + borderRadius: 15, + offset: 0, }); return ( @@ -50,12 +47,19 @@ export function DefaultEdge({ style={{ stroke: euiTheme.colors[color], }} - css={{ - strokeDasharray: '2,2', - }} + css={ + (!data?.type || data?.type === 'dashed') && { + strokeDasharray: '2,2', + } + } + markerStart={ + data?.sourceShape !== 'label' && data?.sourceShape !== 'group' + ? getMarkerStart(color) + : undefined + } markerEnd={ data?.targetShape !== 'label' && data?.targetShape !== 'group' - ? getMarker(color) + ? getMarkerEnd(color) : undefined } /> diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx new file mode 100644 index 0000000000000..06dcaf29c63d6 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useEuiTheme } from '@elastic/eui'; + +const getArrowPoints = (width: number, height: number): string => { + return `${-width},${-height} 0,0 ${-width},${height} ${-width},${-height}`; +}; + +const ArrowMarker = ({ + id, + color, + width = 5, + height = 4, +}: { + id: string; + color: string; + width?: number; + height?: number; +}) => { + const points = getArrowPoints(width, height); + + return ( + + + + ); +}; + +const DotMarker = ({ id, color }: { id: string; color: string }) => { + return ( + + + + ); +}; + +const MarkerStartType = { + primary: 'url(#dotPrimary)', + danger: 'url(#dotDanger)', + warning: 'url(#dotWarning)', +}; + +const MarkerEndType = { + primary: 'url(#arrowPrimary)', + danger: 'url(#arrowDanger)', + warning: 'url(#arrowWarning)', +}; + +export const getMarkerStart = (color: string) => { + const colorKey = color as keyof typeof MarkerStartType; + return MarkerStartType[colorKey] ?? MarkerStartType.primary; +}; + +export const getMarkerEnd = (color: string) => { + const colorKey = color as keyof typeof MarkerEndType; + return MarkerEndType[colorKey] ?? MarkerEndType.primary; +}; + +export const SvgDefsMarker = () => { + const { euiTheme } = useEuiTheme(); + + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx index 5a3e2f8b72b21..66b7eca015caf 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import React from 'react'; import styled from '@emotion/styled'; import { rgba } from 'polished'; import { @@ -88,52 +87,3 @@ export const EdgeLabelOnHover = styled(EdgeLabel) { - return ( - - - - ); -}; - -export const MarkerType = { - primary: 'url(#primary)', - danger: 'url(#danger)', - warning: 'url(#warning)', -}; - -export const getMarker = (color: string) => { - const colorKey = color as keyof typeof MarkerType; - return MarkerType[colorKey] ?? MarkerType.primary; -}; - -export const SvgDefsMarker = () => { - const { euiTheme } = useEuiTheme(); - - return ( - - - - - - - - ); -}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index a97a1c74698ca..f08e40111b7f8 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -18,7 +18,7 @@ import { import type { Edge, FitViewOptions, Node, ReactFlowInstance } from '@xyflow/react'; import { useGeneratedHtmlId } from '@elastic/eui'; import type { CommonProps } from '@elastic/eui'; -import { SvgDefsMarker } from '../edge/styles'; +import { SvgDefsMarker } from '../edge/markers'; import { HexagonNode, PentagonNode, @@ -243,7 +243,9 @@ const processGraph = ( data: { ...edgeData, sourceShape: nodesById[edgeData.source].shape, + sourceColor: nodesById[edgeData.source].color, targetShape: nodesById[edgeData.target].shape, + targetColor: nodesById[edgeData.target].color, }, }; }); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx index 570c1332a8834..65d0b5a2b89b8 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -40,20 +40,8 @@ export const GraphPopover: React.FC = ({ {...rest} panelProps={{ css: css` - .euiPopover__arrow[data-popover-arrow='left']:before { - border-inline-start-color: ${euiTheme.colors?.body}; - } - - .euiPopover__arrow[data-popover-arrow='right']:before { - border-inline-end-color: ${euiTheme.colors?.body}; - } - - .euiPopover__arrow[data-popover-arrow='bottom']:before { - border-block-end-color: ${euiTheme.colors?.body}; - } - - .euiPopover__arrow[data-popover-arrow='top']:before { - border-block-start-color: ${euiTheme.colors?.body}; + .euiPopover__arrow { + --euiPopoverBackgroundColor: ${euiTheme.colors?.body}; } background-color: ${euiTheme.colors?.body}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx index 7dc46ac6eb82c..bea6f851bab17 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { ThemeProvider } from '@emotion/react'; import { Story } from '@storybook/react'; -import { NodeButton, type NodeButtonProps, NodeShapeContainer } from './styles'; +import { type NodeButtonProps, NodeShapeContainer } from './styles'; +import { NodeExpandButton } from './node_expand_button'; export default { title: 'Components/Graph Components', @@ -24,7 +25,7 @@ const Template: Story = (args) => ( Hover me - + ); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx index ac6f51284a98d..b46c44c69d1b8 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { Handle, Position } from '@xyflow/react'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { @@ -16,6 +16,7 @@ import { NodeIcon, NodeButton, HandleStyleOverride, + useNodeFillColor, } from './styles'; import { DiamondHoverShape, DiamondShape } from './shapes/diamond_shape'; import { NodeExpandButton } from './node_expand_button'; @@ -51,7 +52,7 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -60,6 +61,7 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 4}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx index 05a61977cdcb1..10ac415398717 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { Handle, NodeResizeControl, Position } from '@xyflow/react'; +import { Handle, Position } from '@xyflow/react'; import { HandleStyleOverride } from './styles'; import type { NodeProps } from '../types'; @@ -15,25 +15,20 @@ export const EdgeGroupNode: React.FC = memo((props: NodeProps) => { // Handles order horizontally is: in > inside > out > outside return ( <> - - - - + + = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -59,6 +60,7 @@ export const EllipseNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx index f5ee7d92605cc..ebde4e2334e21 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { Handle, Position } from '@xyflow/react'; import { NodeShapeContainer, @@ -15,6 +15,7 @@ import { NodeIcon, NodeButton, HandleStyleOverride, + useNodeFillColor, } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { HexagonHoverShape, HexagonShape } from './shapes/hexagon_shape'; @@ -51,7 +52,7 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -60,6 +61,7 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2 + 2}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx index 07581c1e3d3dd..80d354cd77d6b 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx @@ -7,14 +7,16 @@ import React, { useCallback, useState } from 'react'; import { StyledNodeExpandButton, RoundEuiButtonIcon, ExpandButtonSize } from './styles'; +import type { EntityNodeViewModel, LabelNodeViewModel } from '..'; export interface NodeExpandButtonProps { x?: string; y?: string; + color?: EntityNodeViewModel['color'] | LabelNodeViewModel['color']; onClick?: (e: React.MouseEvent, unToggleCallback: () => void) => void; } -export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { +export const NodeExpandButton = ({ x, y, color, onClick }: NodeExpandButtonProps) => { // State to track whether the icon is "plus" or "minus" const [isToggled, setIsToggled] = useState(false); @@ -30,7 +32,7 @@ export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { return ( = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -65,6 +66,7 @@ export const PentagonNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx index 8b55a0898586c..f923641a25a50 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { Handle, Position } from '@xyflow/react'; import { NodeShapeContainer, @@ -15,6 +15,7 @@ import { NodeIcon, NodeButton, HandleStyleOverride, + useNodeFillColor, } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { RectangleHoverShape, RectangleShape } from './shapes/rectangle_shape'; @@ -51,7 +52,7 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -60,6 +61,7 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 4}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize / 2) / 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx index 2982c4145370e..c4305c6fded6b 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx @@ -9,7 +9,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { type EuiIconProps, - type _EuiBackgroundColor, + type EuiTextProps, EuiButtonIcon, EuiIcon, EuiText, @@ -19,12 +19,14 @@ import { import { rgba } from 'polished'; import { getSpanIcon } from './get_span_icon'; import type { NodeExpandButtonProps } from './node_expand_button'; +import type { EntityNodeViewModel, LabelNodeViewModel } from '..'; export const LABEL_PADDING_X = 15; export const LABEL_BORDER_WIDTH = 1; export const NODE_WIDTH = 90; export const NODE_HEIGHT = 90; export const NODE_LABEL_WIDTH = 160; +type NodeColor = EntityNodeViewModel['color'] | LabelNodeViewModel['color']; export const LabelNodeContainer = styled.div` text-wrap: nowrap; @@ -32,8 +34,12 @@ export const LabelNodeContainer = styled.div` height: 24px; `; -export const LabelShape = styled(EuiText)` - background: ${(props) => useEuiBackgroundColor(props.color as _EuiBackgroundColor)}; +interface LabelShapeProps extends EuiTextProps { + color: LabelNodeViewModel['color']; +} + +export const LabelShape = styled(EuiText)` + background: ${(props) => useNodeFillColor(props.color)}; border: ${(props) => { const { euiTheme } = useEuiTheme(); return `solid ${ @@ -209,6 +215,11 @@ export const HandleStyleOverride: React.CSSProperties = { border: 'none', }; +export const useNodeFillColor = (color: NodeColor | undefined) => { + const fillColor = (color === 'danger' ? 'primary' : color) ?? 'primary'; + return useEuiBackgroundColor(fillColor); +}; + export const GroupStyleOverride = (size?: { width: number; height: number; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts index b09f6a29f6c62..32e34a212af59 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts @@ -12,6 +12,7 @@ import type { LabelNodeDataModel, EdgeDataModel, NodeShape, + Color as NodeColor, } from '@kbn/cloud-security-posture-common/types/graph/latest'; import type { Node, NodeProps as xyNodeProps, Edge, EdgeProps as xyEdgeProps } from '@xyflow/react'; @@ -62,7 +63,9 @@ export type EdgeProps = xyEdgeProps< Edge< EdgeViewModel & { sourceShape: NodeShape; + sourceColor: NodeColor; targetShape: NodeShape; + targetColor: NodeColor; } > >; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts index d506bb856e766..5fb39c9be4993 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -40,6 +40,7 @@ interface GraphEdge { interface LabelEdges { source: string; target: string; + edgeType: EdgeDataModel['type']; } interface GraphContextServices { @@ -259,7 +260,17 @@ const createNodes = (records: GraphEdge[], context: Omit { // If actor is a user return ellipse if (users.includes(actorId)) { @@ -337,7 +352,7 @@ const determineEntityNodeShape = ( return { shape: 'diamond', icon: 'globe' }; } - return { shape: 'hexagon', icon: 'questionInCircle' }; + return { shape: 'hexagon' }; }; const sortNodes = (nodesMap: Record) => { @@ -368,7 +383,8 @@ const createEdgesAndGroups = (context: ParseContext) => { nodesMap, labelEdges[edgeLabelId].source, edgeLabelId, - labelEdges[edgeLabelId].target + labelEdges[edgeLabelId].target, + labelEdges[edgeLabelId].edgeType ); } else { const groupNode: GroupNodeDataModel = { @@ -377,10 +393,18 @@ const createEdgesAndGroups = (context: ParseContext) => { }; nodesMap[groupNode.id] = groupNode; let groupEdgesColor: Color = 'primary'; + let groupEdgesType: EdgeDataModel['type'] = 'dashed'; edgeLabelsIds.forEach((edgeLabelId) => { (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; - connectEntitiesAndLabelNode(edgesMap, nodesMap, groupNode.id, edgeLabelId, groupNode.id); + connectEntitiesAndLabelNode( + edgesMap, + nodesMap, + groupNode.id, + edgeLabelId, + groupNode.id, + labelEdges[edgeLabelId].edgeType + ); if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { groupEdgesColor = 'danger'; @@ -391,6 +415,10 @@ const createEdgesAndGroups = (context: ParseContext) => { // Use warning only if there's no danger color groupEdgesColor = 'warning'; } + + if (labelEdges[edgeLabelId].edgeType === 'solid') { + groupEdgesType = 'solid'; + } }); connectEntitiesAndLabelNode( @@ -399,6 +427,7 @@ const createEdgesAndGroups = (context: ParseContext) => { labelEdges[edgeLabelsIds[0]].source, groupNode.id, labelEdges[edgeLabelsIds[0]].target, + groupEdgesType, groupEdgesColor ); } @@ -411,11 +440,12 @@ const connectEntitiesAndLabelNode = ( sourceNodeId: string, labelNodeId: string, targetNodeId: string, + edgeType: EdgeDataModel['type'] = 'solid', colorOverride?: Color ) => { [ - connectNodes(nodesMap, sourceNodeId, labelNodeId, colorOverride), - connectNodes(nodesMap, labelNodeId, targetNodeId, colorOverride), + connectNodes(nodesMap, sourceNodeId, labelNodeId, edgeType, colorOverride), + connectNodes(nodesMap, labelNodeId, targetNodeId, edgeType, colorOverride), ].forEach((edge) => { edgesMap[edge.id] = edge; }); @@ -425,6 +455,7 @@ const connectNodes = ( nodesMap: Record, sourceNodeId: string, targetNodeId: string, + edgeType: EdgeDataModel['type'] = 'solid', colorOverride?: Color ): EdgeDataModel => { const sourceNode = nodesMap[sourceNodeId]; @@ -441,5 +472,6 @@ const connectNodes = ( source: sourceNodeId, target: targetNodeId, color: colorOverride ?? color, + type: edgeType, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation.md b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation.md new file mode 100644 index 0000000000000..6f91d4958650c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation.md @@ -0,0 +1,559 @@ +# Installation of Prebuilt Rules + +This is a test plan for the workflows of installing prebuilt rules. + +Status: `in progress`. The current test plan matches [Rule Immutability/Customization Milestone 3 epic](https://github.com/elastic/kibana/issues/174168). + +## Table of Contents + +- [Useful information](#useful-information) + - [Tickets](#tickets) + - [Terminology](#terminology) + - [Assumptions](#assumptions) + - [Non-functional requirements](#non-functional-requirements) + - [Functional requirements](#functional-requirements) +- [Scenarios](#scenarios) + - [Rule installation notifications on the Rule Management page](#rule-installation-and-upgrade-notifications-on-the-rule-management-page) + - [**Scenario: User is NOT notified when no prebuilt rules are installed and there are no prebuilt rules assets**](#scenario-user-is-not-notified-when-no-prebuilt-rules-are-installed-and-there-are-no-prebuilt-rules-assets) + - [**Scenario: User is NOT notified when all prebuilt rules are installed and up to date**](#scenario-user-is-not-notified-when-all-prebuilt-rules-are-installed-and-up-to-date) + - [**Scenario: User is notified when no prebuilt rules are installed and there are rules available to install**](#scenario-user-is-notified-when-no-prebuilt-rules-are-installed-and-there-are-rules-available-to-install) + - [**Scenario: User is notified when some prebuilt rules can be installed**](#scenario-user-is-notified-when-some-prebuilt-rules-can-be-installed) + - [**Scenario: User is notified when both rules to install and upgrade are available**](#scenario-user-is-notified-when-both-rules-to-install-and-upgrade-are-available) + - [**Scenario: User is notified after a prebuilt rule gets deleted**](#scenario-user-is-notified-after-a-prebuilt-rule-gets-deleted) + - [Rule installation workflow: base cases](#rule-installation-workflow-base-cases) + - [**Scenario: User can install prebuilt rules one by one**](#scenario-user-can-install-prebuilt-rules-one-by-one) + - [**Scenario: User can install multiple prebuilt rules selected on the page**](#scenario-user-can-install-multiple-prebuilt-rules-selected-on-the-page) + - [**Scenario: User can install all available prebuilt rules at once**](#scenario-user-can-install-all-available-prebuilt-rules-at-once) + - [**Scenario: Empty screen is shown when all prebuilt rules are installed**](#scenario-empty-screen-is-shown-when-all-prebuilt-rules-are-installed) + - [**Scenario: User can preview rules available for installation**](#scenario-user-can-preview-rules-available-for-installation) + - [**Scenario: User can install a rule using the rule preview**](#scenario-user-can-install-a-rule-using-the-rule-preview) + - [**Scenario: User can see correct rule information in preview before installing**](#scenario-user-can-see-correct-rule-information-in-preview-before-installing) + - [**Scenario: Tabs and sections without content should be hidden in preview before installing**](#scenario-tabs-and-sections-without-content-should-be-hidden-in-preview-before-installing) + - [Rule installation workflow: filtering, sorting, pagination](#rule-installation-workflow-filtering-sorting-pagination) + - [Rule installation workflow: misc cases](#rule-installation-workflow-misc-cases) + - [**Scenario: User opening the Add Rules page sees a loading skeleton until the package installation is completed**](#scenario-user-opening-the-add-rules-page-sees-a-loading-skeleton-until-the-package-installation-is-completed) + - [**Scenario: User can navigate from the Add Rules page to the Rule Management page via breadcrumbs**](#scenario-user-can-navigate-from-the-add-rules-page-to-the-rule-management-page-via-breadcrumbs) + - [Rule installation and upgrade via the Prebuilt rules API](#rule-installation-and-upgrade-via-the-prebuilt-rules-api) + - [**Scenario: API can install all prebuilt rules**](#scenario-api-can-install-all-prebuilt-rules) + - [**Scenario: API can install prebuilt rules that are not yet installed**](#scenario-api-can-install-prebuilt-rules-that-are-not-yet-installed) + - [**Scenario: API does not install prebuilt rules if they are up to date**](#scenario-api-does-not-installupgrade-prebuilt-rules-if-they-are-up-to-date) + - [Error handling](#error-handling) + - [**Scenario: Error is handled when any operation on prebuilt rules fails**](#scenario-error-is-handled-when-any-operation-on-prebuilt-rules-fails) + - [Authorization / RBAC](#authorization--rbac) + - [**Scenario: User with read privileges on Security Solution cannot install prebuilt rules**](#scenario-user-with-read-privileges-on-security-solution-cannot-install-prebuilt-rules) + +## Useful information + +### Tickets + +- [Rule Immutability/Customization epic](https://github.com/elastic/security-team/issues/1974)(internal) + +**Milestone 3 - Prebuilt Rules Customization:** +- [Milestone 3 epic ticket](https://github.com/elastic/kibana/issues/174168) +- [Tests for prebuilt rule upgrade workflow #202078](https://github.com/elastic/kibana/issues/202078) + +**Milestone 2:** +- [Ensure full test coverage for existing workflows of installing and upgrading prebuilt rules](https://github.com/elastic/kibana/issues/148176) +- [Write test plan and add test coverage for the new workflows of installing and upgrading prebuilt rules](https://github.com/elastic/kibana/issues/148192) + +### Terminology + +- **EPR**: [Elastic Package Registry](https://github.com/elastic/package-registry), service that hosts our **Package**. + +- **Package**: `security_detection_engine` Fleet package that we use to distribute prebuilt detection rules in the form of `security-rule` assets (saved objects). + +- **Real package**: actual latest stable package distributed and pulled from EPR via Fleet. + +- **Mock rules**: `security-rule` assets that are indexed into the `.kibana_security_solution` index directly in the test setup, either by using the ES client _in integration tests_ or by an API request _in Cypress tests_. + +- **Air-gapped environment**: an environment where Kibana doesn't have access to the internet. In general, EPR is not available in such environments, except the cases when the user runs a custom EPR inside the environment. + +- **CTA**: "call to action", usually a button, a link, or a callout message with a button, etc, that invites the user to do some action. + - CTA to install prebuilt rules - at this moment, it's a link button with a counter (implemented) and a callout with a link button (not yet implemented) on the Rule Management page. + - CTA to upgrade prebuilt rules - at this moment, it's a tab with a counter (implemented) and a callout with a link button (not yet implemented) on the Rule Management page. + +### Assumptions + +- Below scenarios only apply to prebuilt detection rules. +- Users should be able to install prebuilt rules on the `Basic` license and higher. +- EPR is available for fetching the package unless explicitly indicated otherwise. +- Only the latest **stable** package is checked for installation/upgrade and pre-release packages are ignored. + +### Non-functional requirements + +- Notifications, rule installation workflows should work: + - regardless of the package type: with historical rule versions or without; + - regardless of the package registry availability: i.e., they should also work in air-gapped environments. +- Rule installation and upgrade workflows should work with packages containing up to 15000 historical rule versions. This is the max number of versions of all rules in the package. This limit is enforced by Fleet. +- Kibana should not crash with Out Of Memory exception during package installation. +- For test purposes, it should be possible to use detection rules package versions lower than the latest. + +### Functional requirements + +- User should be able to install prebuilt rules with and without previewing what exactly they would install (rule properties). +- If user chooses to preview a prebuilt rule to be installed/upgraded, we currently show this preview in a flyout. +- In the prebuilt rule preview a tab that doesn't have any sections should not be displayed and a section that doesn't have any properties also should not be displayed. + +Examples of rule properties we show in the prebuilt rule preview flyout: + +```Gherkin +Examples: +| rule_type | property | tab | section | +│ All rule types │ Author │ Overview │ About │ +│ All rule types │ Building block │ Overview │ About │ +│ All rule types │ Severity │ Overview │ About │ +│ All rule types │ Severity override │ Overview │ About │ +│ All rule types │ Risk score │ Overview │ About │ +│ All rule types │ Risk score override │ Overview │ About │ +│ All rule types │ Reference URLs │ Overview │ About │ +│ All rule types │ False positive examples │ Overview │ About │ +│ All rule types │ Custom highlighted fields │ Overview │ About │ +│ All rule types │ License │ Overview │ About │ +│ All rule types │ Rule name override │ Overview │ About │ +│ All rule types │ MITRE ATT&CK™ │ Overview │ About │ +│ All rule types │ Timestamp override │ Overview │ About │ +│ All rule types │ Tags │ Overview │ About │ +│ All rule types │ Type │ Overview │ Definition │ +│ All rule types │ Related integrations │ Overview │ Definition │ +│ All rule types │ Required fields │ Overview │ Definition │ +│ All rule types │ Timeline template │ Overview │ Definition │ +│ All rule types │ Runs every │ Overview │ Schedule │ +│ All rule types │ Additional look-back time │ Overview │ Schedule │ +│ All rule types │ Setup guide │ Overview │ Setup guide │ +│ All rule types │ Investigation guide │ Investigation guide │ Investigation guide │ +│ Custom Query │ Index patterns │ Overview │ Definition │ +│ Custom Query │ Data view ID │ Overview │ Definition │ +│ Custom Query │ Data view index pattern │ Overview │ Definition │ +│ Custom Query │ Custom query │ Overview │ Definition │ +│ Custom Query │ Filters │ Overview │ Definition │ +│ Custom Query │ Saved query name │ Overview │ Definition │ +│ Custom Query │ Saved query filters │ Overview │ Definition │ +│ Custom Query │ Saved query │ Overview │ Definition │ +│ Custom Query │ Suppress alerts by │ Overview │ Definition │ +│ Custom Query │ Suppress alerts for │ Overview │ Definition │ +│ Custom Query │ If a suppression field is missing │ Overview │ Definition │ +│ Machine Learning │ Anomaly score threshold │ Overview │ Definition │ +│ Machine Learning │ Machine Learning job │ Overview │ Definition │ +│ Threshold │ Threshold │ Overview │ Definition │ +│ Threshold │ Index patterns │ Overview │ Definition │ +│ Threshold │ Data view ID │ Overview │ Definition │ +│ Threshold │ Data view index pattern │ Overview │ Definition │ +│ Threshold │ Custom query │ Overview │ Definition │ +│ Threshold │ Filters │ Overview │ Definition │ +│ Event Correlation │ EQL query │ Overview │ Definition │ +│ Event Correlation │ Filters │ Overview │ Definition │ +│ Event Correlation │ Index patterns │ Overview │ Definition │ +│ Event Correlation │ Data view ID │ Overview │ Definition │ +│ Event Correlation │ Data view index pattern │ Overview │ Definition │ +│ Indicator Match │ Indicator index patterns │ Overview │ Definition │ +│ Indicator Match │ Indicator mapping │ Overview │ Definition │ +│ Indicator Match │ Indicator filters │ Overview │ Definition │ +│ Indicator Match │ Indicator index query │ Overview │ Definition │ +│ Indicator Match │ Index patterns │ Overview │ Definition │ +│ Indicator Match │ Data view ID │ Overview │ Definition │ +│ Indicator Match │ Data view index pattern │ Overview │ Definition │ +│ Indicator Match │ Custom query │ Overview │ Definition │ +│ Indicator Match │ Filters │ Overview │ Definition │ +│ New Terms │ Fields │ Overview │ Definition │ +│ New Terms │ History Window Size │ Overview │ Definition │ +│ New Terms │ Index patterns │ Overview │ Definition │ +│ New Terms │ Data view ID │ Overview │ Definition │ +│ New Terms │ Data view index pattern │ Overview │ Definition │ +│ New Terms │ Custom query │ Overview │ Definition │ +│ New Terms │ Filters │ Overview │ Definition │ +│ ESQL │ ESQL query │ Overview │ Definition │ +│ ESQL │ Suppress alerts by │ Overview │ Definition │ +│ ESQL │ Suppress alerts for │ Overview │ Definition │ +│ ESQL │ If a suppression field is missing │ Overview │ Definition │ +``` + +## Scenarios + +### Rule installation notifications on the Rule Management page + +#### **Scenario: User is NOT notified when no prebuilt rules are installed and there are no prebuilt rules assets** + +**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. + +```Gherkin +Given no prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Rule Management page +Then user should NOT see a CTA to install prebuilt rules +And user should NOT see a number of rules available to install +And user should NOT see a CTA to upgrade prebuilt rules +And user should NOT see a number of rules available to upgrade +And user should NOT see the Rule Updates table +``` + +#### **Scenario: User is NOT notified when all prebuilt rules are installed and up to date** + +**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. + +```Gherkin +Given the latest prebuilt rule assets exist in Kibana +And all the latest prebuilt rules from those rule assets are installed +When user opens the Rule Management page +Then user should NOT see a CTA to install prebuilt rules +And user should NOT see a number of rules available to install +And user should NOT see a CTA to upgrade prebuilt rules +And user should NOT see a number of rules available to upgrade +And user should NOT see the Rule Updates table +``` + +#### **Scenario: User is notified when no prebuilt rules are installed and there are rules available to install** + +**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. + +```Gherkin +Given X prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +And there are X prebuilt rules available to install +When user opens the Rule Management page +Then user should see a CTA to install prebuilt rules +And user should see a number of rules available to install (X) +And user should NOT see a CTA to upgrade prebuilt rules +And user should NOT see a number of rules available to upgrade +And user should NOT see the Rule Updates table +``` + +#### **Scenario: User is notified when some prebuilt rules can be installed** + +**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. + +```Gherkin +Given Y prebuilt rule assets exist in Kibana +And X (where X < Y) prebuilt rules are installed +And there are Y more prebuilt rules available to install +And for all X installed rules there are no new versions available +When user opens the Rule Management page +Then user should see a CTA to install prebuilt rules +And user should see the number of rules available to install (Y) +And user should NOT see a CTA to upgrade prebuilt rules +And user should NOT see a number of rules available to upgrade +And user should NOT see the Rule Updates table +``` + +#### **Scenario: User is notified when both rules to install and upgrade are available** + +**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. + +```Gherkin +Given Y prebuilt rule assets exist in Kibana +And X (where X < Y) prebuilt rules are installed +And Z (where Z < X) installed rules have matching prebuilt rule assets with higher version available +And for Z of the installed rules there are new versions available +When user opens the Rule Management page +Then user should see a CTA to install prebuilt rules +And user should see the number of rules available to install (Y) +And user should see a CTA to upgrade prebuilt rules +And user should see the number of rules available to upgrade (Z) +``` + +#### **Scenario: User is notified after a prebuilt rule gets deleted** + +**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. + +```Gherkin +Given X prebuilt rules are installed in Kibana +And there are no more prebuilt rules available to install +When user opens the Rule Management page +And user deletes Y prebuilt rules +Then user should see a CTA to install prebuilt rules +And user should see rules available to install +``` + +### Rule installation workflow: base cases + +#### **Scenario: User can install prebuilt rules one by one** + +**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /installation/\* endpoints in integration. + +```Gherkin +Given X prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then prebuilt rules available for installation should be displayed in the table +When user installs one individual rule without previewing it +Then success message should be displayed after installation +And the installed rule should be removed from the table +When user navigates back to the Rule Management page +Then user should see a CTA to install prebuilt rules +And user should see the number of rules available to install decreased by 1 +``` + +#### **Scenario: User can install multiple prebuilt rules selected on the page** + +**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /installation/\* endpoints in integration. + +```Gherkin +Given X prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then prebuilt rules available for installation should be displayed in the table +When user selects rules +Then user should see a CTA to install number of rules +When user clicks the CTA +Then success message should be displayed after installation +And all the installed rules should be removed from the table +When user navigates back to the Rule Management page +Then user should see a CTA to install prebuilt rules +And user should see the number of rules available to install decreased by number of installed rules + +Examples: + | Y | + | a few rules on the page, e.g. 2 | + | all rules on the page, e.g. 12 | +``` + +#### **Scenario: User can install all available prebuilt rules at once** + +**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /installation/\* endpoints in integration. + +```Gherkin +Given X prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then prebuilt rules available for installation should be displayed in the table +When user installs all rules +Then success message should be displayed after installation +And all the rules should be removed from the table +And user should see a message indicating that all available rules have been installed +And user should see a CTA that leads to the Rule Management page +When user clicks on the CTA +Then user should be navigated back to Rule Management page +And user should NOT see a CTA to install prebuilt rules +And user should NOT see a number of rules available to install +``` + +#### **Scenario: Empty screen is shown when all prebuilt rules are installed** + +**Automation**: 1 e2e test with mock rules + 1 integration test. + +```Gherkin +Given all the available prebuilt rules are installed in Kibana +When user opens the Add Rules page +Then user should see a message indicating that all available rules have been installed +And user should see a CTA that leads to the Rule Management page +``` + +#### **Scenario: User can preview rules available for installation** + +**Automation**: 1 e2e test + +```Gherkin +Given 2 prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then all rules available for installation should be displayed in the table +When user opens the rule preview for the 1st rule +Then the preview should open +When user closes the preview +Then it should disappear +``` + +#### **Scenario: User can install a rule using the rule preview** + +**Automation**: 1 e2e test + +```Gherkin +Given 2 prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then all rules available for installation should be displayed in the table +When user opens the rule preview for the rule +Then the preview should open +When user installs the rule using a CTA in the rule preview +Then the rule should be installed +And a success message should be displayed after installation +And the rule should be removed from the Add Rules table +When user navigates back to the Rule Management page +Then user should see a CTA to install prebuilt rules +And user should see the number of rules available to install as initial number minus 1 +``` + +#### **Scenario: User can see correct rule information in preview before installing** + +**Automation**: 1 e2e test + +```Gherkin +Given X prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then all X rules available for installation should be displayed in the table +When user opens a rule preview for any rule +Then the preview should appear +And all properties of a rule should be displayed in the correct tab and section of the preview (see examples of rule properties above) +``` + +#### **Scenario: Optional tabs and sections without content should be hidden in preview before installing** + +**Automation**: 1 e2e test + +```Gherkin +Given 1 prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +And this rule has neither Setup guide nor Investigation guide +When user opens the Add Rules page +Then all rules available for installation should be displayed in the table +When user opens the rule preview for this rule +Then the preview should open +And the Setup Guide section should NOT be displayed in the Overview tab +And the Investigation Guide tab should NOT be displayed +``` + +### Rule installation workflow: filtering, sorting, pagination + +TODO: add scenarios https://github.com/elastic/kibana/issues/166215 + +### Rule installation workflow: misc cases + +#### **Scenario: User opening the Add Rules page sees a loading skeleton until the package installation is completed** + +**Automation**: unit tests. + +```Gherkin +Given prebuilt rules package is not installed +When user opens the Add Rules page +Then user should see a loading skeleton until the package installation is completed +``` + +#### **Scenario: User can navigate from the Add Rules page to the Rule Management page via breadcrumbs** + +**Automation**: 1 e2e test. + +```Gherkin +Given user is on the Add Rules page +When user navigates to the Rule Management page via breadcrumbs +Then the Rule Management page should be displayed +``` + +### Rule installation via the Prebuilt rules API + +There's a legacy prebuilt rules API and a new one. Both should be tested against two types of the package: with and without historical rule versions. + +#### **Scenario: API can install all prebuilt rules** + +**Automation**: 8 integration tests with mock rules: 4 examples below \* 2 (we split checking API response and installed rules into two different tests). + +```Gherkin +Given the package is installed +And the package contains N rules +When user installs all rules via install +Then the endpoint should return success response (HTTP 200 code) with +And N rule objects should be created +And each rule object should have correct id and version + +Examples: + | package_type | api | install_response | + | with historical versions | legacy | installed: N, updated: 0 | + | w/o historical versions | legacy | installed: N, updated: 0 | + | with historical versions | new | total: N, succeeded: N | + | w/o historical versions | new | total: N, succeeded: N | +``` + +Notes: + +- Legacy API: + - install: `PUT /api/detection_engine/rules/prepackaged` +- New API: + - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` + +#### **Scenario: API can install prebuilt rules that are not yet installed** + +**Automation**: 4 integration tests with mock rules. + +```Gherkin +Given the package is installed +And the package contains N rules +When user installs all rules via install +And deletes one of the installed rules +And gets prebuilt rules status via status +Then the endpoint should return successful response (HTTP 200 code) with +When user installs all rules via install again +Then the endpoint should return successful response (HTTP 200 code) with + +Examples: + | package_type | api | status_response | install_response | + | with historical versions | legacy | not_installed: 1 | installed: 1, updated: 0 | + | w/o historical versions | legacy | not_installed: 1 | installed: 1, updated: 0 | + | with historical versions | new | to_install: 1 | total: 1, succeeded: 1 | + | w/o historical versions | new | to_install: 1 | total: 1, succeeded: 1 | +``` + +Notes: + +- Legacy API: + - install: `PUT /api/detection_engine/rules/prepackaged` + - status: `GET /api/detection_engine/rules/prepackaged/_status` +- New API: + - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` + - status: `GET /internal/detection_engine/prebuilt_rules/status` + + +#### **Scenario: API does not install prebuilt rules if they are up to date** + +**Automation**: 4 integration tests with mock rules. + +```Gherkin +Given the package is installed +And the package contains N rules +When user installs all rules via install +And user gets prebuilt rules status via status +Then the endpoint should return successful response (HTTP 200 code) with +When user calls install +Then the endpoint should return successful response (HTTP 200 code) with +When user calls upgrade +Then the endpoint should return successful response (HTTP 200 code) with + +Examples: + | package_type | api | status_response | install_response | upgrade_response | + | with historical versions | legacy | not_installed: 0, not_updated: 0 | installed: 0, updated: 0 | installed: 0, updated: 0 | + | w/o historical versions | legacy | not_installed: 0, not_updated: 0 | installed: 0, updated: 0 | installed: 0, updated: 0 | + | with historical versions | new | to_install: 0, to_upgrade: 0 | total: 0, succeeded: 0 | total: 0, succeeded: 0 | + | w/o historical versions | new | to_install: 0, to_upgrade: 0 | total: 0, succeeded: 0 | total: 0, succeeded: 0 | +``` + +Notes: + +- Legacy API: + - install: `PUT /api/detection_engine/rules/prepackaged` + - upgrade: `PUT /api/detection_engine/rules/prepackaged` + - status: `GET /api/detection_engine/rules/prepackaged/_status` +- New API: + - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` + - upgrade: `POST /internal/detection_engine/prebuilt_rules/upgrade/_perform` + - status: `GET /internal/detection_engine/prebuilt_rules/status` + +### Error handling + +#### **Scenario: Error is handled when any installation operation on prebuilt rules fails** + +**Automation**: e2e test with mock rules + +```Gherkin +When user is prebuilt rules +And this operation fails +Then user should see an error message + +Examples: + | operation | + | installing all | + | installing selected | + | installing individual | +``` + +### Authorization / RBAC + +#### **Scenario: User with read privileges on Security Solution cannot install prebuilt rules** + +**Automation**: 1 e2e test with mock rules + 3 integration tests with mock rules for the status and installation endpoints. + +```Gherkin +Given user with "Security: read" privileges on Security Solution +And prebuilt rule assets exist in Kibana +And no prebuilt rules are installed +When user opens the Add Rules page +Then user should see prebuilt rules available to install +But user should not be able to install them +``` \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/package_installation_and_upgrade.md b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/package_installation_and_upgrade.md new file mode 100644 index 0000000000000..57e16684facca --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/package_installation_and_upgrade.md @@ -0,0 +1,165 @@ +# Prebuilt Rules Package Installation and Upgrade + +This is a test plan for the workflows of installing and updating the Prebuilt Rules package. + +> For the test plans for installing and upgrading prebuilt rules, see [Installation of Prebuilt Rules](./installation.md) and [Upgrade of Prebuilt Rules](./upgrade.md). + +Status: `in progress`. The current test plan matches [Rule Immutability/Customization Milestone 3 epic](https://github.com/elastic/kibana/issues/174168). + +## Table of Contents + +- [Useful information](#useful-information) + - [Tickets](#tickets) + - [Terminology](#terminology) + - [Assumptions](#assumptions) + - [Non-functional requirements](#non-functional-requirements) + - [Functional requirements](#functional-requirements) +- [Scenarios](#scenarios) + - [Package installation](#package-installation) + - [**Scenario: Package is installed via Fleet**](#scenario-package-is-installed-via-fleet) + - [**Scenario: Package is installed via bundled Fleet package in Kibana**](#scenario-package-is-installed-via-bundled-fleet-package-in-kibana) + - [**Scenario: Large package can be installed on a small Kibana instance**](#scenario-large-package-can-be-installed-on-a-small-kibana-instance) + - [Scenarios for the real package](#scenarios-for-the-real-package) + - [**Scenario: User can install prebuilt rules from scratch, then install new rules and upgrade existing rules from the new package**](#scenario-user-can-install-prebuilt-rules-from-scratch-then-install-new-rules-and-upgrade-existing-rules-from-the-new-package) + - [Kibana upgrade](#kibana-upgrade) + - [**Scenario: User can use prebuilt rules after upgrading Kibana from version A to B**](#scenario-user-can-use-prebuilt-rules-after-upgrading-kibana-from-version-a-to-b) + +## Useful information + +### Tickets + +- [Rule Immutability/Customization epic](https://github.com/elastic/security-team/issues/1974)(internal) + +**Milestone 3 - Prebuilt Rules Customization:** +- [Milestone 3 epic ticket](https://github.com/elastic/kibana/issues/174168) +- [Tests for prebuilt rule upgrade workflow #202078](https://github.com/elastic/kibana/issues/202078) + +**Milestone 2:** +- [Ensure full test coverage for existing workflows of installing and upgrading prebuilt rules](https://github.com/elastic/kibana/issues/148176) +- [Write test plan and add test coverage for the new workflows of installing and upgrading prebuilt rules](https://github.com/elastic/kibana/issues/148192) + +### Terminology + +- **EPR**: [Elastic Package Registry](https://github.com/elastic/package-registry), service that hosts our **Package**. + +- **Package**: `security_detection_engine` Fleet package that we use to distribute prebuilt detection rules in the form of `security-rule` assets (saved objects). + +- **Real package**: actual latest stable package distributed and pulled from EPR via Fleet. + +- **Mock rules**: `security-rule` assets that are indexed into the `.kibana_security_solution` index directly in the test setup, either by using the ES client _in integration tests_ or by an API request _in Cypress tests_. + +- **Air-gapped environment**: an environment where Kibana doesn't have access to the internet. In general, EPR is not available in such environments, except the cases when the user runs a custom EPR inside the environment. + +- **CTA**: "call to action", usually a button, a link, or a callout message with a button, etc, that invites the user to do some action. + - CTA to install prebuilt rules - at this moment, it's a link button with a counter (implemented) and a callout with a link button (not yet implemented) on the Rule Management page. + - CTA to upgrade prebuilt rules - at this moment, it's a tab with a counter (implemented) and a callout with a link button (not yet implemented) on the Rule Management page. + +### Assumptions + +- Below scenarios only apply to prebuilt detection rules. +- Users should be able to install and upgrade prebuilt rules on the `Basic` license and higher. +- EPR is available for fetching the package unless explicitly indicated otherwise. +- Only the latest **stable** package is checked for installation/upgrade and pre-release packages are ignored. + +### Non-functional requirements + +- Package installation, rule installation and rule upgrade workflows should work: + - regardless of the package type: with historical rule versions or without; + - regardless of the package registry availability: i.e., they should also work in air-gapped environments. +- Rule installation and upgrade workflows should work with packages containing up to 15000 historical rule versions. This is the max number of versions of all rules in the package. This limit is enforced by Fleet. +- Kibana should not crash with Out Of Memory exception during package installation. +- For test purposes, it should be possible to use detection rules package versions lower than the latest. + +### Functional requirements + +- User should be able to install prebuilt rules with and without previewing what exactly they would install (rule properties). +- User should be able to upgrade prebuilt rules with and without previewing what updates they would apply (rule properties of target rule versions). + +## Scenarios + +### Package installation + +#### **Scenario: Package is installed via Fleet** + +**Automation**: 2 e2e tests that install the real package. + +```Gherkin +Given the prebuilt rules package is not installed +When user opens any Security Solution page +Then the package gets installed in the background from EPR +``` + +#### **Scenario: Package is installed via bundled Fleet package in Kibana** + +**Automation**: 2 integration tests. + +```Gherkin +Given the package is not installed +And user is in an air-gapped environment +When user opens any Security Solution page +Then the package gets installed in the background from packages bundled into Kibana +``` + +#### **Scenario: Large package can be installed on a memory restricted Kibana instance** + +**Automation**: 1 integration test. + +```Gherkin +Given the package is not installed +And the package contains the largest amount of historical rule versions (15000) +And the Kibana instance has a memory heap size of 700 Mb (see note below) +When user opens any Security Solution page +Then the package is installed without Kibana crashing with an Out Of Memory error +``` + +**Note**: 600 Mb seems to always crash Kibana with an OOM error. 700 Mb runs with no issues in the Flaky test runner with 100 iterations: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2215. + +### Scenarios for the real package + +#### **Scenario: User can install prebuilt rules from scratch, then install new rules and upgrade existing rules from the new package** + +**Automation**: 1 integration test with real packages. + +```Gherkin +Given there are two package versions: A and B where A < B +And the package of A version is installed +When user calls the status endpoint +Then it should return a 200 response with some number of rules to install and 0 rules to upgrade +When user calls the installation/_review endpoint +Then it should return a 200 response matching the response of the status endpoint +When user calls the installation/_perform_ endpoint +Then it should return a 200 response matching the response of the status endpoint +And rules returned in this response should exist as alert saved objects +When user installs version B of the package +Then it should be installed successfully +When user calls the status endpoint +Then it should return a 200 response with some number of new rules to install and some number of rules to upgrade +When user calls the installation/_review endpoint +Then it should return a 200 response matching the response of the status endpoint +When user calls the installation/_perform_ endpoint +Then rules returned in this response should exist as alert saved objects +When user calls the upgrade/_review endpoint +Then it should return a 200 response matching the response of the status endpoint +When user calls the upgrade/_perform_ endpoint +Then rules returned in this response should exist as alert saved objects +``` + +### Kibana upgrade + +#### **Scenario: User can use prebuilt rules after upgrading Kibana from version A to B** + +**Automation**: not automated, manual testing required. + +```Gherkin +Given user is upgrading Kibana from version to version +And the version Kibana instance has already installed prebuilt rules +When the Kibana upgrade is complete +Then user should be able to install new prebuilt rules +And delete installed prebuilt rules +And upgrade installed prebuilt rules that have newer versions in Kibana version + +Examples: + | A | B | + | 8.7 | 8.9.0 | + | 7.17.x | 8.9.0 | +``` diff --git a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade.md similarity index 55% rename from x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md rename to x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade.md index e5479ec502865..4beb517f9598a 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md +++ b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade.md @@ -1,8 +1,8 @@ -# Installation and Upgrade of Prebuilt Rules +# Upgrade of Prebuilt Rules -This is a test plan for the workflows of installing and upgrading prebuilt rules. +This is a test plan for the workflow of upgrading prebuilt rules. -Status: `in progress`. The current test plan matches `Milestone 2` of the [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) epic. It does not cover any past functionality that was removed or functionality to be implemented in the future. The plan is about to change in the future Milestones. +Status: `in progress`. The current test plan matches [Rule Immutability/Customization Milestone 3 epic](https://github.com/elastic/kibana/issues/174168). ## Table of Contents @@ -12,84 +12,73 @@ Status: `in progress`. The current test plan matches `Milestone 2` of the [Rule - [Assumptions](#assumptions) - [Non-functional requirements](#non-functional-requirements) - [Functional requirements](#functional-requirements) -- [Scenarios](#scenarios) - - - [Package installation](#package-installation) - - [**Scenario: Package is installed via Fleet**](#scenario-package-is-installed-via-fleet) - - [**Scenario: Package is installed via bundled Fleet package in Kibana**](#scenario-package-is-installed-via-bundled-fleet-package-in-kibana) - - [**Scenario: Large package can be installed on a small Kibana instance**](#scenario-large-package-can-be-installed-on-a-small-kibana-instance) - - [Rule installation and upgrade via the Prebuilt rules API](#rule-installation-and-upgrade-via-the-prebuilt-rules-api) - - [**Scenario: API can install all prebuilt rules**](#scenario-api-can-install-all-prebuilt-rules) - - [**Scenario: API can install prebuilt rules that are not yet installed**](#scenario-api-can-install-prebuilt-rules-that-are-not-yet-installed) - - [**Scenario: API can upgrade prebuilt rules that are outdated**](#scenario-api-can-upgrade-prebuilt-rules-that-are-outdated) - - [**Scenario: API does not install or upgrade prebuilt rules if they are up to date**](#scenario-api-does-not-install-or-upgrade-prebuilt-rules-if-they-are-up-to-date) - - [Scenarios for the real package](#scenarios-for-the-real-package) - - [**Scenario: User can install prebuilt rules from scratch, then install new rules and upgrade existing rules from the new package**](#scenario-user-can-install-prebuilt-rules-from-scratch-then-install-new-rules-and-upgrade-existing-rules-from-the-new-package) - - [Rule installation and upgrade notifications on the Rule Management page](#rule-installation-and-upgrade-notifications-on-the-rule-management-page) - - [**Scenario: User is NOT notified when no prebuilt rules are installed and there are no prebuilt rules assets**](#scenario-user-is-not-notified-when-no-prebuilt-rules-are-installed-and-there-are-no-prebuilt-rules-assets) - - [**Scenario: User is NOT notified when all prebuilt rules are installed and up to date**](#scenario-user-is-not-notified-when-all-prebuilt-rules-are-installed-and-up-to-date) - - [**Scenario: User is notified when no prebuilt rules are installed and there are rules available to install**](#scenario-user-is-notified-when-no-prebuilt-rules-are-installed-and-there-are-rules-available-to-install) - - [**Scenario: User is notified when some prebuilt rules can be installed**](#scenario-user-is-notified-when-some-prebuilt-rules-can-be-installed) - - [**Scenario: User is notified when some prebuilt rules can be upgraded**](#scenario-user-is-notified-when-some-prebuilt-rules-can-be-upgraded) - - [**Scenario: User is notified when both rules to install and upgrade are available**](#scenario-user-is-notified-when-both-rules-to-install-and-upgrade-are-available) - - [**Scenario: User is notified after a prebuilt rule gets deleted**](#scenario-user-is-notified-after-a-prebuilt-rule-gets-deleted) - - [Rule installation workflow: base cases](#rule-installation-workflow-base-cases) - - [**Scenario: User can install prebuilt rules one by one**](#scenario-user-can-install-prebuilt-rules-one-by-one) - - [**Scenario: User can install multiple prebuilt rules selected on the page**](#scenario-user-can-install-multiple-prebuilt-rules-selected-on-the-page) - - [**Scenario: User can install all available prebuilt rules at once**](#scenario-user-can-install-all-available-prebuilt-rules-at-once) - - [**Scenario: Empty screen is shown when all prebuilt rules are installed**](#scenario-empty-screen-is-shown-when-all-prebuilt-rules-are-installed) - - [**Scenario: User can preview rules available for installation**](#scenario-user-can-preview-rules-available-for-installation) - - [**Scenario: User can install a rule using the rule preview**](#scenario-user-can-install-a-rule-using-the-rule-preview) - - [**Scenario: User can see correct rule information in preview before installing**](#scenario-user-can-see-correct-rule-information-in-preview-before-installing) - - [**Scenario: Tabs and sections without content should be hidden in preview before installing**](#scenario-tabs-and-sections-without-content-should-be-hidden-in-preview-before-installing) - - [Rule installation workflow: filtering, sorting, pagination](#rule-installation-workflow-filtering-sorting-pagination) - - [Rule installation workflow: misc cases](#rule-installation-workflow-misc-cases) - - [**Scenario: User opening the Add Rules page sees a loading skeleton until the package installation is completed**](#scenario-user-opening-the-add-rules-page-sees-a-loading-skeleton-until-the-package-installation-is-completed) - - [**Scenario: User can navigate from the Add Rules page to the Rule Management page via breadcrumbs**](#scenario-user-can-navigate-from-the-add-rules-page-to-the-rule-management-page-via-breadcrumbs) - - [Rule upgrade workflow: base cases](#rule-upgrade-workflow-base-cases) - - [**Scenario: User can upgrade prebuilt rules one by one**](#scenario-user-can-upgrade-prebuilt-rules-one-by-one) - - [**Scenario: User can upgrade multiple prebuilt rules selected on the page**](#scenario-user-can-upgrade-multiple-prebuilt-rules-selected-on-the-page) - - [**Scenario: User can upgrade all available prebuilt rules at once**](#scenario-user-can-upgrade-all-available-prebuilt-rules-at-once) - - [**Scenario: User can preview rules available for upgrade**](#scenario-user-can-preview-rules-available-for-upgrade) - - [**Scenario: User can upgrade a rule using the rule preview**](#scenario-user-can-upgrade-a-rule-using-the-rule-preview) - - [**Scenario: User can see correct rule information in preview before upgrading**](#scenario-user-can-see-correct-rule-information-in-preview-before-upgrading) - - [**Scenario: Tabs and sections without content should be hidden in preview before upgrading**](#scenario-tabs-and-sections-without-content-should-be-hidden-in-preview-before-upgrading) - - [Rule upgrade workflow: filtering, sorting, pagination](#rule-upgrade-workflow-filtering-sorting-pagination) - - [Rule upgrade workflow: viewing rule changes in JSON diff view](#rule-upgrade-workflow-viewing-rule-changes-in-json-diff-view) - - [**Scenario: User can see changes in a side-by-side JSON diff view**](#scenario-user-can-see-changes-in-a-side-by-side-json-diff-view) - - [**Scenario: User can see precisely how property values would change after upgrade**](#scenario-user-can-see-precisely-how-property-values-would-change-after-upgrade) - - [**Scenario: Rule actions and exception lists should not be shown as modified**](#scenario-rule-actions-and-exception-lists-should-not-be-shown-as-modified) - - [**Scenario: Dynamic properties should not be included in preview**](#scenario-dynamic-properties-should-not-be-included-in-preview) - - [**Scenario: Technical properties should not be included in preview**](#scenario-technical-properties-should-not-be-included-in-preview) - - [**Scenario: Properties with semantically equal values should not be shown as modified**](#scenario-properties-with-semantically-equal-values-should-not-be-shown-as-modified) - - [**Scenario: Unchanged sections of a rule should be hidden by default**](#scenario-unchanged-sections-of-a-rule-should-be-hidden-by-default) - - [**Scenario: Properties should be sorted alphabetically**](#scenario-properties-should-be-sorted-alphabetically) - - [Rule upgrade workflow: viewing rule changes in per-field diff view](#rule-upgrade-workflow-viewing-rule-changes-in-per-field-diff-view) - - [**Scenario: User can see changes in a side-by-side per-field diff view**](#scenario-user-can-see-changes-in-a-side-by-side-per-field-diff-view) - - [**Scenario: Field groupings should be rendered together in the same accordion panel**](#scenario-field-groupings-should-be-rendered-together-in-the-same-accordion-panel) - - [**Scenario: Undefined values are displayed with empty diffs**](#scenario-undefined-values-are-displayed-with-empty-diffs) - - [**Scenario: Field diff components have the same grouping and order as in rule details overview**](#scenario-field-diff-components-have-the-same-grouping-and-order-as-in-rule-details-overview) - - [Rule upgrade workflow: preserving rule bound data](#rule-upgrade-workflow-preserving-rule-bound-data) - - [**Scenario: Rule bound data is preserved after upgrading a rule to a newer version with the same rule type**](#scenario-rule-bound-data-is-preserved-after-upgrading-a-rule-to-a-newer-version-with-the-same-rule-type) - - [**Scenario: Rule bound data is preserved after upgrading a rule to a newer version with a different rule type**](#scenario-rule-bound-data-is-preserved-after-upgrading-a-rule-to-a-newer-version-with-a-different-rule-type) - - [Rule upgrade workflow: misc cases](#rule-upgrade-workflow-misc-cases) - - [**Scenario: User doesn't see the Rule Updates tab until the package installation is completed**](#scenario-user-doesnt-see-the-rule-updates-tab-until-the-package-installation-is-completed) - - [Error handling](#error-handling) - - [**Scenario: Error is handled when any operation on prebuilt rules fails**](#scenario-error-is-handled-when-any-operation-on-prebuilt-rules-fails) - - [Authorization / RBAC](#authorization--rbac) - - [**Scenario: User with read privileges on Security Solution cannot install prebuilt rules**](#scenario-user-with-read-privileges-on-security-solution-cannot-install-prebuilt-rules) - - [**Scenario: User with read privileges on Security Solution cannot upgrade prebuilt rules**](#scenario-user-with-read-privileges-on-security-solution-cannot-upgrade-prebuilt-rules) - - [Kibana upgrade](#kibana-upgrade) - - [**Scenario: User can use prebuilt rules after upgrading Kibana from version A to B**](#scenario-user-can-use-prebuilt-rules-after-upgrading-kibana-from-version-a-to-b) + - [Scenarios](#scenarios) + - [Rule installation and upgrade notifications on the Rule Management page](#rule-installation-and-upgrade-notifications-on-the-rule-management-page) + - [**Scenario: User is NOT notified when all installed prebuilt rules are up to date**](#scenario-user-is-not-notified-when-all-installed-prebuilt-rules-are-up-to-date) + - [**Scenario: User is notified when some prebuilt rules can be upgraded**](#scenario-user-is-notified-when-some-prebuilt-rules-can-be-upgraded) + - [**Scenario: User is notified when both rules to install and upgrade are available**](#scenario-user-is-notified-when-both-rules-to-install-and-upgrade-are-available) + - [Rule upgrade workflow: individual upgrade from Rule Updates table](#rule-upgrade-workflow-individual-and-bulk-updates-from-rule-updates-table) + - [**Scenario: User can upgrade conflict-free prebuilt rules one by one**](#scenario-user-can-upgrade-conflict-free-prebuilt-rules-one-by-one) + - [**Scenario: User cannot upgrade prebuilt rules one by one from Rules Update table if they have conflicts**](#scenario-user-cannot-upgrade-prebuilt-rules-one-by-one-from-rules-update-table-if-they-have-conflicts) + - [Rule upgrade workflow: bulk upgrade from Rule Updates table](#rule-upgrade-workflow-individual-and-bulk-updates-from-rule-updates-table) + - [**Scenario: User can upgrade multiple conflict-free prebuilt rules selected on the page**](#scenario-user-can-upgrade-multiple-conflict-free-prebuilt-rules-selected-on-the-page) + - [**Scenario: User cannot upgrade multiple prebuilt rules selected on the page when they have upgrade conflicts**](#scenario-user-cannot-upgrade-multiple-prebuilt-rules-selected-on-the-page-when-they-have-upgrade-conflicts) + - [**Scenario: User can upgrade all available conflict-free prebuilt rules at once**](#scenario-user-can-upgrade-all-available-conflict-free-prebuilt-rules-at-once) + - [**Scenario: User cannot upgrade all prebuilt rules at once if they have upgrade conflicts**](#scenario-user-cannot-upgrade-all-prebuilt-rules-at-once-if-they-have-upgrade-conflicts) + - [**Scenario: User can upgrade only conflict-free rules when a mix of rules with and without conflicts are selected for upgrade in the Rules Table**](#scenario-user-can-upgrade-only-conflict-free-rules-when-a-mix-of-rules-with-and-without-conflicts-are-selected-for-upgrade-in-the-rules-table) + - [**Scenario: User can upgrade only conflict-free rules when user attempts to upgrade all rules and only a subset contains upgrade conflicts**](#scenario-user-can-upgrade-only-conflict-free-rules-when-user-attempts-to-upgrade-all-rules-and-only-a-subset-contains-upgrade-conflicts) + - [Rule upgrade workflow: upgrading rules with rule type change](#rule-upgrade-workflow-upgrading-rules-with-rule-type-change) + - [**Scenario: User can upgrade rule with rule type change individually**](#scenario-user-can-upgrade-rule-with-rule-type-change-individually) + - [**Scenario: User can bulk upgrade selected rules with rule type changes**](#scenario-user-can-bulk-upgrade-selected-rules-with-rule-type-changes) + - [**Scenario: User can bulk upgrade all rules with rule type changes**](#scenario-user-can-bulk-upgrade-all-rules-with-rule-type-changes) + - [Rule upgrade workflow: rule previews](#rule-upgrade-workflow-rule-previews) + - [**Scenario: User can preview rules available for upgrade**](#scenario-user-can-preview-rules-available-for-upgrade) + - [**Scenario: User can upgrade a rule using the rule preview**](#scenario-user-can-upgrade-a-rule-using-the-rule-preview) + - [**Scenario: User can see correct rule information in preview before upgrading**](#scenario-user-can-see-correct-rule-information-in-preview-before-upgrading) + - [**Scenario: Tabs and sections without content should be hidden in preview before upgrading**](#scenario-tabs-and-sections-without-content-should-be-hidden-in-preview-before-upgrading) + - [Rule upgrade workflow: filtering, sorting, pagination](#rule-upgrade-workflow-filtering-sorting-pagination) + - [MILESTONE 2 (Legacy) - Rule upgrade workflow: viewing rule changes in JSON diff view](#milestone-2-legacy---rule-upgrade-workflow-viewing-rule-changes-in-json-diff-view) + - [**Scenario: User can see changes in a side-by-side JSON diff view**](#scenario-user-can-see-changes-in-a-side-by-side-json-diff-view) + - [**Scenario: User can see precisely how property values would change after upgrade**](#scenario-user-can-see-precisely-how-property-values-would-change-after-upgrade) + - [**Scenario: Rule actions and exception lists should not be shown as modified**](#scenario-rule-actions-and-exception-lists-should-not-be-shown-as-modified) + - [**Scenario: Dynamic properties should not be included in preview**](#scenario-dynamic-properties-should-not-be-included-in-preview) + - [**Scenario: Technical properties should not be included in preview**](#scenario-technical-properties-should-not-be-included-in-preview) + - [**Scenario: Properties with semantically equal values should not be shown as modified**](#scenario-properties-with-semantically-equal-values-should-not-be-shown-as-modified) + - [**Scenario: Unchanged sections of a rule should be hidden by default**](#scenario-unchanged-sections-of-a-rule-should-be-hidden-by-default) + - [**Scenario: Properties should be sorted alphabetically**](#scenario-properties-should-be-sorted-alphabetically) + - [MILESTONE 2 (Legacy) - Rule upgrade workflow: viewing rule changes in per-field diff view](#milestone-2-legacy---rule-upgrade-workflow-viewing-rule-changes-in-per-field-diff-view) + - [**Scenario: User can see changes in a side-by-side per-field diff view**](#scenario-user-can-see-changes-in-a-side-by-side-per-field-diff-view) + - [**Scenario: User can see changes when updated rule is a different rule type**](#scenario-user-can-see-changes-when-updated-rule-is-a-different-rule-type) + - [**Scenario: Field groupings should be rendered together in the same accordion panel**](#scenario-field-groupings-should-be-rendered-together-in-the-same-accordion-panel) + - [**Scenario: Undefined values are displayed with empty diffs**](#scenario-undefined-values-are-displayed-with-empty-diffs) + - [**Scenario: Field diff components have the same grouping and order as in rule details overview**](#scenario-field-diff-components-have-the-same-grouping-and-order-as-in-rule-details-overview) + - [Rule upgrade workflow: preserving rule bound data](#rule-upgrade-workflow-preserving-rule-bound-data) + - [**Scenario: Rule bound data is preserved after upgrading a rule to a newer version with the same rule type**](#scenario-rule-bound-data-is-preserved-after-upgrading-a-rule-to-a-newer-version-with-the-same-rule-type) + - [**Scenario: Rule bound data is preserved after upgrading a rule to a newer version with a different rule type**](#scenario-rule-bound-data-is-preserved-after-upgrading-a-rule-to-a-newer-version-with-a-different-rule-type) + - [Rule upgrade workflow: misc cases](#rule-upgrade-workflow-misc-cases) + - [**Scenario: User doesn't see the Rule Updates tab until the package installation is completed**](#scenario-user-doesnt-see-the-rule-updates-tab-until-the-package-installation-is-completed) + - [Error handling](#error-handling) + - [**Scenario: Error is handled when any upgrade operation on prebuilt rules fails**](#scenario-error-is-handled-when-any-upgrade-operation-on-prebuilt-rules-fails) + - [Rule upgrade via the Prebuilt rules API](#rule-upgrade-via-the-prebuilt-rules-api) + - [**Scenario: API can upgrade prebuilt rules that are outdated**](#scenario-api-can-upgrade-prebuilt-rules-that-are-outdated) + - [**Scenario: API does not upgrade prebuilt rules if they are up to date**](#scenario-api-does-not-upgrade-prebuilt-rules-if-they-are-up-to-date) + - [Authorization / RBAC](#authorization-rbac) + - [**Scenario: User with read privileges on Security Solution cannot upgrade prebuilt rules**](#scenario-user-with-read-privileges-on-security-solution-cannot-upgrade-prebuilt-rules) + ## Useful information ### Tickets - [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) epic + +**Milestone 3 - Prebuilt Rules Customization:** +- [Milestone 3 epic ticket](https://github.com/elastic/kibana/issues/174168) +- [Tests for prebuilt rule upgrade workflow #202078](https://github.com/elastic/kibana/issues/202078) + +**Milestone 2:** - [Ensure full test coverage for existing workflows of installing and upgrading prebuilt rules](https://github.com/elastic/kibana/issues/148176) - [Write test plan and add test coverage for the new workflows of installing and upgrading prebuilt rules](https://github.com/elastic/kibana/issues/148192) -- [Document the new UI for installing and upgrading prebuilt detection rules](https://github.com/elastic/security-docs/issues/3496) ### Terminology @@ -205,266 +194,15 @@ Examples: ## Scenarios -### Package installation - -#### **Scenario: Package is installed via Fleet** - -**Automation**: 2 e2e tests that install the real package. - -```Gherkin -Given the package is not installed -When user opens the Rule Management page -Then the package gets installed in the background from EPR -``` - -#### **Scenario: Package is installed via bundled Fleet package in Kibana** - -**Automation**: 2 integration tests. - -```Gherkin -Given the package is not installed -And user is in an air-gapped environment -When user opens the Rule Management page -Then the package gets installed in the background from packages bundled into Kibana -``` - -#### **Scenario: Large package can be installed on a small Kibana instance** - -**Automation**: 1 integration test. - -```Gherkin -Given the package is not installed -And the package contains the largest amount of historical rule versions (15000) -And the Kibana instance has a memory heap size of 700 Mb (see note below) -When user opens the Rule Management page -Then the package is installed without Kibana crashing with an Out Of Memory error -``` - -**Note**: 600 Mb seems to always crash Kibana with an OOM error. 700 Mb runs with no issues in the Flaky test runner with 100 iterations: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2215. - -### Rule installation and upgrade via the Prebuilt rules API - -There's a legacy prebuilt rules API and a new one. Both should be tested against two types of the package: with and without historical rule versions. - -#### **Scenario: API can install all prebuilt rules** - -**Automation**: 8 integration tests with mock rules: 4 examples below \* 2 (we split checking API response and installed rules into two different tests). - -```Gherkin -Given the package is installed -And the package contains N rules -When user installs all rules via install -Then the endpoint should return 200 with -And N rule objects should be created -And each rule object should have correct id and version - -Examples: - | package_type | api | install_response | - | with historical versions | legacy | installed: N, updated: 0 | - | w/o historical versions | legacy | installed: N, updated: 0 | - | with historical versions | new | total: N, succeeded: N | - | w/o historical versions | new | total: N, succeeded: N | -``` - -Notes: - -- Legacy API: - - install: `PUT /api/detection_engine/rules/prepackaged` -- New API: - - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` - -#### **Scenario: API can install prebuilt rules that are not yet installed** - -**Automation**: 4 integration tests with mock rules. - -```Gherkin -Given the package is installed -And the package contains N rules -When user installs all rules via install -And deletes one of the installed rules -And gets prebuilt rules status via status -Then the endpoint should return 200 with -When user installs all rules via install again -Then the endpoint should return 200 with - -Examples: - | package_type | api | status_response | install_response | - | with historical versions | legacy | not_installed: 1 | installed: 1, updated: 0 | - | w/o historical versions | legacy | not_installed: 1 | installed: 1, updated: 0 | - | with historical versions | new | to_install: 1 | total: 1, succeeded: 1 | - | w/o historical versions | new | to_install: 1 | total: 1, succeeded: 1 | -``` - -Notes: - -- Legacy API: - - install: `PUT /api/detection_engine/rules/prepackaged` - - status: `GET /api/detection_engine/rules/prepackaged/_status` -- New API: - - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` - - status: `GET /internal/detection_engine/prebuilt_rules/status` - -#### **Scenario: API can upgrade prebuilt rules that are outdated** - -**Automation**: 4 integration tests with mock rules. - -```Gherkin -Given the package is installed -And the package contains N rules -When user installs all rules via install -And new X+1 version of a rule asset -And user gets prebuilt rules status via status -Then the endpoint should return 200 with -When user upgrades all rules via upgrade -Then the endpoint should return 200 with - -Examples: - | package_type | api | assets_update | status_response | upgrade_response | - | with historical versions | legacy | gets added | not_updated: 1 | installed: 0, updated: 1 | - | w/o historical versions | legacy | replaces X | not_updated: 1 | installed: 0, updated: 1 | - | with historical versions | new | gets added | to_upgrade: 1 | total: 1, succeeded: 1 | - | w/o historical versions | new | replaces X | to_upgrade: 1 | total: 1, succeeded: 1 | -``` +### Rule upgrade notifications on the Rule Management page -TODO: Check why for the legacy API Dmitrii has added 2 integration tests for `rule package with historical versions` instead of 1: - -- `should update outdated prebuilt rules when previous historical versions available` -- `should update outdated prebuilt rules when previous historical versions unavailable` - -(NOTE: the second scenario tests that, if a new version of a rule is released, it can upgrade the current instance of that rule even if the historical versions of that rule are no longer in the package) - -Notes: - -- Legacy API: - - install: `PUT /api/detection_engine/rules/prepackaged` - - upgrade: `PUT /api/detection_engine/rules/prepackaged` - - status: `GET /api/detection_engine/rules/prepackaged/_status` -- New API: - - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` - - upgrade: `POST /internal/detection_engine/prebuilt_rules/upgrade/_perform` - - status: `GET /internal/detection_engine/prebuilt_rules/status` - -#### **Scenario: API does not install or upgrade prebuilt rules if they are up to date** - -**Automation**: 4 integration tests with mock rules. - -```Gherkin -Given the package is installed -And the package contains N rules -When user installs all rules via install -And user gets prebuilt rules status via status -Then the endpoint should return 200 with -When user calls install -Then the endpoint should return 200 with -When user calls upgrade -Then the endpoint should return 200 with - -Examples: - | package_type | api | status_response | install_response | upgrade_response | - | with historical versions | legacy | not_installed: 0, not_updated: 0 | installed: 0, updated: 0 | installed: 0, updated: 0 | - | w/o historical versions | legacy | not_installed: 0, not_updated: 0 | installed: 0, updated: 0 | installed: 0, updated: 0 | - | with historical versions | new | to_install: 0, to_upgrade: 0 | total: 0, succeeded: 0 | total: 0, succeeded: 0 | - | w/o historical versions | new | to_install: 0, to_upgrade: 0 | total: 0, succeeded: 0 | total: 0, succeeded: 0 | -``` - -Notes: - -- Legacy API: - - install: `PUT /api/detection_engine/rules/prepackaged` - - upgrade: `PUT /api/detection_engine/rules/prepackaged` - - status: `GET /api/detection_engine/rules/prepackaged/_status` -- New API: - - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` - - upgrade: `POST /internal/detection_engine/prebuilt_rules/upgrade/_perform` - - status: `GET /internal/detection_engine/prebuilt_rules/status` - -### Scenarios for the real package - -#### **Scenario: User can install prebuilt rules from scratch, then install new rules and upgrade existing rules from the new package** - -**Automation**: 1 integration test with real packages. - -```Gherkin -Given there are two package versions: N-1 and N -And the package of N-1 version is installed -When user calls the status endpoint -Then it should return a 200 response with some number of rules to install and 0 rules to upgrade -When user calls the installation/_review endpoint -Then it should return a 200 response matching the response of the status endpoint -When user calls the installation/_perform_ endpoint -Then it should return a 200 response matching the response of the status endpoint -And rules returned in this response should exist as alert saved objects -When user installs the package of N version -Then it should be installed successfully -When user calls the status endpoint -Then it should return a 200 response with some number of new rules to install and some number of rules to upgrade -When user calls the installation/_review endpoint -Then it should return a 200 response matching the response of the status endpoint -When user calls the installation/_perform_ endpoint -Then rules returned in this response should exist as alert saved objects -When user calls the upgrade/_review endpoint -Then it should return a 200 response matching the response of the status endpoint -When user calls the upgrade/_perform_ endpoint -Then rules returned in this response should exist as alert saved objects -``` - -### Rule installation and upgrade notifications on the Rule Management page - -#### **Scenario: User is NOT notified when no prebuilt rules are installed and there are no prebuilt rules assets** - -**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. - -```Gherkin -Given no prebuilt rules are installed in Kibana -And no prebuilt rule assets exist -When user opens the Rule Management page -Then user should NOT see a CTA to install prebuilt rules -And user should NOT see a number of rules available to install -And user should NOT see a CTA to upgrade prebuilt rules -And user should NOT see a number of rules available to upgrade -And user should NOT see the Rule Updates table -``` - -#### **Scenario: User is NOT notified when all prebuilt rules are installed and up to date** +#### **Scenario: User is NOT notified when all installed prebuilt rules are up to date** **Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. ```Gherkin Given all the latest prebuilt rules are installed in Kibana When user opens the Rule Management page -Then user should NOT see a CTA to install prebuilt rules -And user should NOT see a number of rules available to install -And user should NOT see a CTA to upgrade prebuilt rules -And user should NOT see a number of rules available to upgrade -And user should NOT see the Rule Updates table -``` - -#### **Scenario: User is notified when no prebuilt rules are installed and there are rules available to install** - -**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. - -```Gherkin -Given no prebuilt rules are installed in Kibana -And there are X prebuilt rules available to install -When user opens the Rule Management page -Then user should see a CTA to install prebuilt rules -And user should see a number of rules available to install (X) -And user should NOT see a CTA to upgrade prebuilt rules -And user should NOT see a number of rules available to upgrade -And user should NOT see the Rule Updates table -``` - -#### **Scenario: User is notified when some prebuilt rules can be installed** - -**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. - -```Gherkin -Given X prebuilt rules are installed in Kibana -And there are Y more prebuilt rules available to install -And for all X installed rules there are no new versions available -When user opens the Rule Management page -Then user should see a CTA to install prebuilt rules -And user should see the number of rules available to install (Y) And user should NOT see a CTA to upgrade prebuilt rules And user should NOT see a number of rules available to upgrade And user should NOT see the Rule Updates table @@ -500,242 +238,210 @@ And user should see a CTA to upgrade prebuilt rules And user should see the number of rules available to upgrade (Z) ``` -#### **Scenario: User is notified after a prebuilt rule gets deleted** +### Rule upgrade workflow: individual updates from Rule Updates table -**Automation**: 1 e2e test with mock rules + 1 integration test with mock rules for the /status endpoint. +#### **Scenario: User can upgrade conflict-free prebuilt rules one by one** + +**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /upgrade/\* endpoints in integration. ```Gherkin Given X prebuilt rules are installed in Kibana -And there are no more prebuilt rules available to install -When user opens the Rule Management page -And user deletes Y prebuilt rules -Then user should see a CTA to install prebuilt rules -And user should see the number of rules available to install (Y) +And for Y of the installed rules there are new versions available +When user is on the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +When user upgrades one individual rule without previewing it +Then success message should be displayed after upgrade +And the upgraded rule should be removed from the table +And user should see the number of rules available to upgrade decreased by 1 ``` -### Rule installation workflow: base cases - -#### **Scenario: User can install prebuilt rules one by one** +#### **Scenario: User cannot upgrade prebuilt rules one by one from Rules Update table if they have conflicts** -**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /installation/\* endpoints in integration. +**Automation**: 1 e2e test with mock rules ```Gherkin -Given no prebuilt rules are installed in Kibana -And there are X prebuilt rules available to install -When user opens the Add Rules page -Then prebuilt rules available for installation should be displayed in the table -When user installs one individual rule without previewing it -Then success message should be displayed after installation -And the installed rule should be removed from the table -When user navigates back to the Rule Management page -Then user should see a CTA to install prebuilt rules -And user should see the number of rules available to install decreased by 1 +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And user is on the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +And for Z (where Z < Y) of the rules with upgrades there are upgrade conflicts +Then for those Z rules the Upgrade Rule button should be disabled +And the user should not be able to upgrade them directly from the table +And there should be a message/tooltip indicating why the rule cannot be upgraded directly ``` -#### **Scenario: User can install multiple prebuilt rules selected on the page** +### Rule upgrade workflow: bulk updates from Rule Updates table -**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /installation/\* endpoints in integration. +#### **Scenario: User can upgrade multiple conflict-free prebuilt rules selected on the page** + +**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /upgrade/\* endpoints in integration. ```Gherkin -Given no prebuilt rules are installed in Kibana -And there are X prebuilt rules available to install -When user opens the Add Rules page -Then prebuilt rules available for installation should be displayed in the table -When user selects rules -Then user should see a CTA to install number of rules +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And user opens the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +When user selects Z (where Z < Y) rules, which have no upgrade conflicts +Then user should see a CTA to upgrade rules When user clicks the CTA -Then success message should be displayed after installation -And all the installed rules should be removed from the table -When user navigates back to the Rule Management page -Then user should see a CTA to install prebuilt rules -And user should see the number of rules available to install decreased by number of installed rules +Then success message should be displayed after upgrade +And all the upgraded rules should be removed from the table +And user should see the number of rules available to upgrade decreased by number of upgraded rules Examples: - | Y | + | Z | | a few rules on the page, e.g. 2 | | all rules on the page, e.g. 12 | ``` -#### **Scenario: User can install all available prebuilt rules at once** +#### **Scenario: User cannot upgrade selected prebuilt rules with conflicts** -**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /installation/\* endpoints in integration. +**Automation**: 1 e2e test with mock rules ```Gherkin -Given no prebuilt rules are installed in Kibana -And there are X prebuilt rules available to install -When user opens the Add Rules page -Then prebuilt rules available for installation should be displayed in the table -When user installs all rules -Then success message should be displayed after installation -And all the rules should be removed from the table -And user should see a message indicating that all available rules have been installed -And user should see a CTA that leads to the Rule Management page -When user clicks on the CTA -Then user should be navigated back to Rule Management page -And user should NOT see a CTA to install prebuilt rules -And user should NOT see a number of rules available to install -``` - -#### **Scenario: Empty screen is shown when all prebuilt rules are installed** - -**Automation**: 1 e2e test with mock rules + 1 integration test. +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And all of those Y new versions have conflicts with the current versions +And user is on the Rule Management page +When user is on the Rule Updates table +When user selects rules, all of which have upgrade conflicts +Then user should see a CTA to upgrade number of rules, which should be disabled +When user hovers on the CTA to upgrade multiple rules +Then a message should be displayed that informs the user why the rules cannot be updated -```Gherkin -Given all the available prebuilt rules are installed in Kibana -When user opens the Add Rules page -Then user should see a message indicating that all available rules have been installed -And user should see a CTA that leads to the Rule Management page +Examples: + | Z | + | a few rules on the page, e.g. 2 | + | all rules on the page, e.g. 12 | ``` -#### **Scenario: User can preview rules available for installation** +#### **Scenario: User can upgrade all available conflict-free prebuilt rules at once** -**Automation**: 1 e2e test +**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /upgrade/\* endpoints in integration. ```Gherkin -Given no prebuilt rules are installed in Kibana -And there are 2 rules available to install -When user opens the Add Rules page -Then all rules available for installation should be displayed in the table -When user opens the rule preview for the 1st rule -Then the preview should open -When user closes the preview -Then it should disappear +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And those Y new versions don't have conflicts with the current versions +When user is on the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +When user upgrades all rules +Then success message should be displayed after upgrade +And user should NOT see a CTA to upgrade prebuilt rules +And user should NOT see a number of rules available to upgrade ``` -#### **Scenario: User can install a rule using the rule preview** +#### **Scenario: User cannot upgrade all prebuilt rules at once if they have upgrade conflicts** -**Automation**: 1 e2e test +**Automation**: 1 e2e test with mock rules ```Gherkin -Given no prebuilt rules are installed in Kibana -And there are 2 rules available to install -When user opens the Add Rules page -Then all rules available for installation should be displayed in the table -When user opens the rule preview for the rule -Then the preview should open -When user installs the rule using a CTA in the rule preview -Then the rule should be installed -And a success message should be displayed after installation -And the rule should be removed from the Add Rules table -When user navigates back to the Rule Management page -Then user should see a CTA to install prebuilt rules -And user should see the number of rules available to install as initial number minus 1 +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And all Y new versions have conflicts with the current versions +When user opens the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +Then user should see a CTA to upgrade all rules +And the CTA to upgrade all rules should be disabled +When user hovers on the CTA to upgrade all rules +Then a message should be displayed that informs the user why the rules cannot be updated ``` -#### **Scenario: User can see correct rule information in preview before installing** +#### **Scenario: User can upgrade only conflict-free rules when a mix of rules with and without conflicts are selected for upgrade** -**Automation**: 1 e2e test +**Automation**: 1 e2e test with mock rules ```Gherkin -Given no prebuilt rules are installed in Kibana -And there are X prebuilt rules of all types available to install -When user opens the Add Rules page -Then all X rules available for installation should be displayed in the table -When user opens a rule preview for any rule -Then the preview should appear -And all properties of a rule should be displayed in the correct tab and section of the preview (see examples of rule properties above) -``` - -#### **Scenario: Tabs and sections without content should be hidden in preview before installing** - -**Automation**: 1 e2e test +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And a subset Z of the rules have conflicts with the current versions +And user is on the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +And user selects rules, which is a mixture of rules with and without upgrade conflicts +Then user should see a CTA to upgrade number of rules, which is enabled +When user clicks the CTA +A modal window should inform the user that only W rules without conflicts will be upgraded +When user confirms the action in the modal +Then success message should be displayed after upgrade informing that W rules were updated +And the W upgraded rules should be removed from the table +And the remaining Z - W rules should still be present in the table +And user should see the number of rules available to upgrade decreased by W number of upgraded rules -```Gherkin -Given no prebuilt rules are installed in Kibana -And there is at least 1 rule available to install -And this rule has neither Setup guide nor Investigation guide -When user opens the Add Rules page -Then all rules available for installation should be displayed in the table -When user opens the rule preview for this rule -Then the preview should open -And the Setup Guide section should NOT be displayed in the Overview tab -And the Investigation Guide tab should NOT be displayed +Examples: + | Z | + | a few rules on the page, e.g. 2 | + | all rules on the page, e.g. 12 | ``` -### Rule installation workflow: filtering, sorting, pagination - -TODO: add scenarios https://github.com/elastic/kibana/issues/166215 - -### Rule installation workflow: misc cases - -#### **Scenario: User opening the Add Rules page sees a loading skeleton until the package installation is completed** +#### **Scenario: User can upgrade only conflict-free rules when attempting to upgrade all rules** -**Automation**: unit tests. +**Automation**: 1 e2e test with mock rules ```Gherkin -Given prebuilt rules package is not installed -When user opens the Add Rules page -Then user should see a loading skeleton until the package installation is completed +Given X prebuilt rules are installed in Kibana +And for Y of the installed rules there are new versions available +And Z (where Z < Y) rules have conflicts with the current versions +And user is on the Rule Updates table +Then Y rules available for upgrade should be displayed in the table +Then user should see an enabled CTA to upgrade all rules +When user clicks the CTA +A modal window should inform the user that only K (where K < Y) rules without conflicts will be upgraded +When user confirms the action in the modal +Then success message should be displayed after upgrade informing that K rules were updated +And the K upgraded rules should be removed from the table +And the remaining M = Y - K rules should still be present in the table +And user should see the number of rules available to upgrade decreased by K number of upgraded rules ``` -#### **Scenario: User can navigate from the Add Rules page to the Rule Management page via breadcrumbs** - -**Automation**: 1 e2e test. -```Gherkin -Given user is on the Add Rules page -When user navigates to the Rule Management page via breadcrumbs -Then the Rule Management page should be displayed -``` +### Rule upgrade workflow: upgrading rules with rule type change -### Rule upgrade workflow: base cases +#### **Scenario: User can upgrade rule with rule type change individually** -#### **Scenario: User can upgrade prebuilt rules one by one** - -**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /upgrade/\* endpoints in integration. +**Automation**: 1 e2e test with mock rules ```Gherkin -Given X prebuilt rules are installed in Kibana -And for Y of the installed rules there are new versions available -And user is on the Rule Management page +Given a prebuilt rule is installed in Kibana +And this rule has an update available that changes its rule type When user opens the Rule Updates table -Then Y rules available for upgrade should be displayed in the table -When user upgrades one individual rule without previewing it -Then success message should be displayed after upgrade -And the upgraded rule should be removed from the table -And user should see the number of rules available to upgrade decreased by 1 +Then this rule should be displayed in the table +And the Upgrade Rule button should be disabled +And the user should not be able to upgrade them directly from the table +And there should be a message/tooltip indicating why the rule cannot be upgraded directly ``` -#### **Scenario: User can upgrade multiple prebuilt rules selected on the page** +#### **Scenario: User can bulk upgrade selected rules with rule type changes** -**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /upgrade/\* endpoints in integration. + +**Automation**: 1 e2e test with mock rules ```Gherkin Given X prebuilt rules are installed in Kibana -And for Y of the installed rules there are new versions available -And user is on the Rule Management page +And Y of these rules have updates available that change their rule types When user opens the Rule Updates table -Then Y rules available for upgrade should be displayed in the table -When user selects rules -Then user should see a CTA to upgrade number of rules -When user clicks the CTA -Then success message should be displayed after upgrade -And all the upgraded rules should be removed from the table -And user should see the number of rules available to upgrade decreased by number of upgraded rules - -Examples: - | Z | - | a few rules on the page, e.g. 2 | - | all rules on the page, e.g. 12 | +Then Y rules should be displayed in the table +When user selects Z rules (where Z < Y) with rule type changes +The button to upgrade the Z selected rules should be disabled +And the user should not be able to upgrade them directly from the table +And there should be a message/tooltip indicating why the rule cannot be upgraded directly ``` -#### **Scenario: User can upgrade all available prebuilt rules at once** +#### **Scenario: User can bulk upgrade all rules with rule type changes** -**Automation**: 1 e2e test with mock rules + integration tests with mock rules that would test /status and /upgrade/\* endpoints in integration. +**Automation**: 1 e2e test with mock rules ```Gherkin Given X prebuilt rules are installed in Kibana -And for Y of the installed rules there are new versions available -And user is on the Rule Management page +And all X rules have updates available that change their rule types When user opens the Rule Updates table -Then Y rules available for upgrade should be displayed in the table -When user upgrades all rules -Then success message should be displayed after upgrade -And user should NOT see a CTA to upgrade prebuilt rules -And user should NOT see a number of rules available to upgrade -And user should NOT see the Rule Updates table +Then X rules should be displayed in the table +The button to upgrade all rules with should be disabled +And the user should not be able to upgrade them directly from the table +And there should be a message/tooltip indicating why the rule cannot be upgraded directly ``` +### Rule upgrade workflow: rule previews + #### **Scenario: User can preview rules available for upgrade** ```Gherkin @@ -807,7 +513,9 @@ And the Investigation Guide tab should NOT be displayed TODO: add scenarios https://github.com/elastic/kibana/issues/166215 -### Rule upgrade workflow: viewing rule changes in JSON diff view +### MILESTONE 2 (Legacy) - Rule upgrade workflow: viewing rule changes in JSON diff view + +> These flow were created for Milestone 2 of the Prebuilt Rules Customization epic, before users could customize prebuilt rules. This section should be deleted once Milestone 3 goes live. #### **Scenario: User can see changes in a side-by-side JSON diff view** @@ -953,7 +661,9 @@ When a user expands all hidden sections Then all properties of the rule should be sorted alphabetically ``` -### Rule upgrade workflow: viewing rule changes in per-field diff view +### MILESTONE 2 (Legacy) - Rule upgrade workflow: viewing rule changes in per-field diff view + +> These flow were created for Milestone 2 of the Prebuilt Rules Customization epic, before users could customize prebuilt rules. This section should be deleted once Milestone 3 goes live. #### **Scenario: User can see changes in a side-by-side per-field diff view** @@ -1091,7 +801,7 @@ Then user should NOT see the Rule Updates tab until the package installation is ### Error handling -#### **Scenario: Error is handled when any operation on prebuilt rules fails** +#### **Scenario: Error is handled when any upgrade operation on prebuilt rules fails** **Automation**: e2e test with mock rules @@ -1102,29 +812,92 @@ Then user should see an error message Examples: | operation | - | installing all | - | installing selected | - | installing individual | | upgrading all | | upgrading selected | | upgrading individual | ``` -### Authorization / RBAC -#### **Scenario: User with read privileges on Security Solution cannot install prebuilt rules** +### Rule upgrade via the Prebuilt rules API + +There's a legacy prebuilt rules API and a new one. Both should be tested against two types of the package: with and without historical rule versions. + +#### **Scenario: API can upgrade prebuilt rules that are outdated** -**Automation**: 1 e2e test with mock rules + 3 integration tests with mock rules for the status and installation endpoints. +**Automation**: 4 integration tests with mock rules. ```Gherkin -Given user with "Security: read" privileges on Security Solution -And no prebuilt rules are installed in Kibana -And there are prebuilt rules available to install -When user opens the Add Rules page -Then user should see prebuilt rules available to install -But user should not be able to install them +Given the package is installed +And the package contains N rules +When user installs all rules via install +And new X+1 version of a rule asset +And user gets prebuilt rules status via status +Then the endpoint should return 200 with +When user upgrades all rules via upgrade +Then the endpoint should return 200 with + +Examples: + | package_type | api | assets_update | status_response | upgrade_response | + | with historical versions | legacy | gets added | not_updated: 1 | installed: 0, updated: 1 | + | w/o historical versions | legacy | replaces X | not_updated: 1 | installed: 0, updated: 1 | + | with historical versions | new | gets added | to_upgrade: 1 | total: 1, succeeded: 1 | + | w/o historical versions | new | replaces X | to_upgrade: 1 | total: 1, succeeded: 1 | ``` +TODO: Check why for the legacy API Dmitrii has added 2 integration tests for `rule package with historical versions` instead of 1: + +- `should update outdated prebuilt rules when previous historical versions available` +- `should update outdated prebuilt rules when previous historical versions unavailable` + +(NOTE: the second scenario tests that, if a new version of a rule is released, it can upgrade the current instance of that rule even if the historical versions of that rule are no longer in the package) + +Notes: + +- Legacy API: + - install: `PUT /api/detection_engine/rules/prepackaged` + - upgrade: `PUT /api/detection_engine/rules/prepackaged` + - status: `GET /api/detection_engine/rules/prepackaged/_status` +- New API: + - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` + - upgrade: `POST /internal/detection_engine/prebuilt_rules/upgrade/_perform` + - status: `GET /internal/detection_engine/prebuilt_rules/status` + +#### **Scenario: API does not upgrade prebuilt rules if they are up to date** + +**Automation**: 4 integration tests with mock rules. + +```Gherkin +Given the package is installed +And the package contains N rules +When user installs all rules via install +And user gets prebuilt rules status via status +Then the endpoint should return 200 with +When user calls install +Then the endpoint should return 200 with +When user calls upgrade +Then the endpoint should return 200 with + +Examples: + | package_type | api | status_response | install_response | upgrade_response | + | with historical versions | legacy | not_installed: 0, not_updated: 0 | installed: 0, updated: 0 | installed: 0, updated: 0 | + | w/o historical versions | legacy | not_installed: 0, not_updated: 0 | installed: 0, updated: 0 | installed: 0, updated: 0 | + | with historical versions | new | to_install: 0, to_upgrade: 0 | total: 0, succeeded: 0 | total: 0, succeeded: 0 | + | w/o historical versions | new | to_install: 0, to_upgrade: 0 | total: 0, succeeded: 0 | total: 0, succeeded: 0 | +``` + +Notes: + +- Legacy API: + - install: `PUT /api/detection_engine/rules/prepackaged` + - upgrade: `PUT /api/detection_engine/rules/prepackaged` + - status: `GET /api/detection_engine/rules/prepackaged/_status` +- New API: + - install: `POST /internal/detection_engine/prebuilt_rules/installation/_perform` + - upgrade: `POST /internal/detection_engine/prebuilt_rules/upgrade/_perform` + - status: `GET /internal/detection_engine/prebuilt_rules/status` + +### Authorization / RBAC + #### **Scenario: User with read privileges on Security Solution cannot upgrade prebuilt rules** **Automation**: 1 e2e test with mock rules + 3 integration tests with mock rules for the status and upgrade endpoints. @@ -1137,24 +910,4 @@ When user opens the Rule Management page And user opens the Rule Updates table Then user should see prebuilt rules available to upgrade But user should not be able to upgrade them -``` - -### Kibana upgrade - -#### **Scenario: User can use prebuilt rules after upgrading Kibana from version A to B** - -**Automation**: not automated, manual testing required. - -```Gherkin -Given user is upgrading Kibana from version to version -And the instance contains already installed prebuilt rules -When the upgrade is complete -Then user should be able to install new prebuilt rules -And delete installed prebuilt rules -And upgrade installed prebuilt rules that have newer versions in - -Examples: - | A | B | - | 8.7 | 8.9.0 | - | 7.17.x | 8.9.0 | -``` +``` \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx index 594629e9f9e27..911c5a6bdba40 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx @@ -9,11 +9,9 @@ import React from 'react'; import { render } from '@testing-library/react'; import { AlertsPreview } from './alerts_preview'; import { TestProviders } from '../../../common/mock/test_providers'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { ParsedAlertsData } from '../../../overview/components/detection_response/alerts_by_status/types'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; -import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; const mockAlertsData: ParsedAlertsData = { open: { @@ -35,18 +33,14 @@ const mockAlertsData: ParsedAlertsData = { // Mock hooks jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); -jest.mock('../../../entity_analytics/api/hooks/use_risk_score'); -jest.mock('@kbn/expandable-flyout'); describe('AlertsPreview', () => { const mockOpenLeftPanel = jest.fn(); beforeEach(() => { - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, }); - (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 1 } }, }); @@ -58,7 +52,11 @@ describe('AlertsPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); @@ -68,7 +66,11 @@ describe('AlertsPreview', () => { it('renders correct alerts number', () => { const { getByTestId } = render( - + ); @@ -78,7 +80,11 @@ describe('AlertsPreview', () => { it('should render the correct number of distribution bar section based on the number of severities', () => { const { queryAllByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx index 8592ed61abe33..cd5fcc93495a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { capitalize } from 'lodash'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui'; @@ -18,8 +18,11 @@ import type { } from '../../../overview/components/detection_response/alerts_by_status/types'; import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers'; -import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; -import { useNavigateEntityInsight } from '../../hooks/use_entity_insight'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; const AlertsCount = ({ alertsTotal, @@ -58,14 +61,14 @@ const AlertsCount = ({ export const AlertsPreview = ({ alertsData, - field, - value, isPreviewMode, + openDetailsPanel, + isLinkEnabled, }: { alertsData: ParsedAlertsData; - field: 'host.name' | 'user.name'; - value: string; isPreviewMode?: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; }) => { const { euiTheme } = useEuiTheme(); @@ -90,15 +93,16 @@ export const AlertsPreview = ({ const hasNonClosedAlerts = totalAlertsCount > 0; - const { goToEntityInsightTab } = useNavigateEntityInsight({ - field, - value, - queryIdExtension: isPreviewMode ? 'ALERTS_PREVIEW_TRUE' : 'ALERTS_PREVIEW_FALSE', - subTab: CspInsightLeftPanelSubTab.ALERTS, - }); + const goToEntityInsightTab = useCallback(() => { + openDetailsPanel({ + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.ALERTS, + }); + }, [openDetailsPanel]); + const link = useMemo( () => - !isPreviewMode + isLinkEnabled ? { callback: goToEntityInsightTab, tooltip: ( @@ -109,7 +113,7 @@ export const AlertsPreview = ({ ), } : undefined, - [isPreviewMode, goToEntityInsightTab] + [isLinkEnabled, goToEntityInsightTab] ); return ( ({ value, field, isPreviewMode, + isLinkEnabled, + openDetailsPanel, }: { value: string; field: 'host.name' | 'user.name'; isPreviewMode?: boolean; + isLinkEnabled: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; }) => { const { euiTheme } = useEuiTheme(); const insightContent: React.ReactElement[] = []; @@ -55,9 +60,9 @@ export const EntityInsight = ({ <> @@ -67,14 +72,26 @@ export const EntityInsight = ({ if (showMisconfigurationsPreview) insightContent.push( <> - + ); if (showVulnerabilitiesPreview) insightContent.push( <> - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx index a3c6bcd38d261..2d79ecdb2783f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx @@ -10,25 +10,19 @@ import { render } from '@testing-library/react'; import { MisconfigurationsPreview } from './misconfiguration_preview'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; -import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { TestProviders } from '../../../common/mock/test_providers'; // Mock hooks jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); -jest.mock('../../../entity_analytics/api/hooks/use_risk_score'); -jest.mock('@kbn/expandable-flyout'); describe('MisconfigurationsPreview', () => { const mockOpenLeftPanel = jest.fn(); beforeEach(() => { - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, }); - (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 1 } }, }); @@ -37,7 +31,12 @@ describe('MisconfigurationsPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index c7c1889a5838b..2db803fbcda3a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { css } from '@emotion/react'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; @@ -20,8 +20,11 @@ import { uiMetricService, } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; -import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; -import { useNavigateEntityInsight } from '../../hooks/use_entity_insight'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; @@ -88,10 +91,14 @@ export const MisconfigurationsPreview = ({ value, field, isPreviewMode, + isLinkEnabled, + openDetailsPanel, }: { value: string; field: 'host.name' | 'user.name'; isPreviewMode?: boolean; + isLinkEnabled: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; }) => { const { hasMisconfigurationFindings, passedFindings, failedFindings } = useHasMisconfigurations( field, @@ -103,15 +110,16 @@ export const MisconfigurationsPreview = ({ }, []); const { euiTheme } = useEuiTheme(); - const { goToEntityInsightTab } = useNavigateEntityInsight({ - field, - value, - queryIdExtension: 'MISCONFIGURATION_PREVIEW', - subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, - }); + const goToEntityInsightTab = useCallback(() => { + openDetailsPanel({ + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, + }); + }, [openDetailsPanel]); + const link = useMemo( () => - !isPreviewMode + isLinkEnabled ? { callback: goToEntityInsightTab, tooltip: ( @@ -122,7 +130,7 @@ export const MisconfigurationsPreview = ({ ), } : undefined, - [isPreviewMode, goToEntityInsightTab] + [isLinkEnabled, goToEntityInsightTab] ); return ( { const mockOpenLeftPanel = jest.fn(); beforeEach(() => { - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, }); - (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 1 } }, }); @@ -37,7 +31,12 @@ describe('VulnerabilitiesPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx index bbdf05b001637..eb5f022eecc95 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { css } from '@emotion/react'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; @@ -23,8 +23,11 @@ import { } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { METRIC_TYPE } from '@kbn/analytics'; import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; -import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; -import { useNavigateEntityInsight } from '../../hooks/use_entity_insight'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; const VulnerabilitiesCount = ({ vulnerabilitiesTotal, @@ -63,10 +66,14 @@ export const VulnerabilitiesPreview = ({ value, field, isPreviewMode, + isLinkEnabled, + openDetailsPanel, }: { value: string; field: 'host.name' | 'user.name'; isPreviewMode?: boolean; + isLinkEnabled: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; }) => { useEffect(() => { uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW); @@ -93,15 +100,16 @@ export const VulnerabilitiesPreview = ({ const { euiTheme } = useEuiTheme(); - const { goToEntityInsightTab } = useNavigateEntityInsight({ - field, - value, - queryIdExtension: 'VULNERABILITIES_PREVIEW', - subTab: CspInsightLeftPanelSubTab.VULNERABILITIES, - }); + const goToEntityInsightTab = useCallback(() => { + openDetailsPanel({ + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.VULNERABILITIES, + }); + }, [openDetailsPanel]); + const link = useMemo( () => - !isPreviewMode + isLinkEnabled ? { callback: goToEntityInsightTab, tooltip: ( @@ -112,7 +120,7 @@ export const VulnerabilitiesPreview = ({ ), } : undefined, - [isPreviewMode, goToEntityInsightTab] + [isLinkEnabled, goToEntityInsightTab] ); return ( ; DashboardsLandingCallout: React.ComponentType<{}>; - EnablementModalCallout: React.ComponentType<{}>; + AdditionalChargesMessage: React.ComponentType<{}>; }>; export type SetComponents = (components: ContractComponents) => void; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx index c16eb7e557a4b..b2c97a33b2ff4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx @@ -182,9 +182,9 @@ describe('EntityStoreEnablementModal', () => { }); it('should render additional charges message when available', async () => { - const EnablementModalCalloutMock = () => ; + const AdditionalChargesMessageMock = () => ; mockUseContractComponents.mockReturnValue({ - EnablementModalCallout: EnablementModalCalloutMock, + AdditionalChargesMessage: AdditionalChargesMessageMock, }); await renderComponent(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx index e3d01ec82907e..a3f4461998905 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx @@ -73,7 +73,7 @@ export const EntityStoreEnablementModal: React.FC - {EnablementModalCallout && } + {AdditionalChargesMessage && } = () => { riskScoreData={{ ...mockRiskScoreState, data: [] }} queryId={'testQuery'} recalculatingScore={false} + isLinkEnabled /> @@ -34,7 +35,7 @@ export const Default: Story = () => { ); }; -export const PreviewMode: Story = () => { +export const LinkEnabledInPreviewMode: Story = () => { return ( @@ -43,6 +44,8 @@ export const PreviewMode: Story = () => { riskScoreData={{ ...mockRiskScoreState, data: [] }} queryId={'testQuery'} recalculatingScore={false} + openDetailsPanel={() => {}} + isLinkEnabled isPreviewMode /> @@ -50,3 +53,21 @@ export const PreviewMode: Story = () => { ); }; + +export const LinkDisabled: Story = () => { + return ( + + +
+ {}} + isLinkEnabled={false} + /> +
+
+
+ ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index 9cc773df320b0..04f6ec369e302 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -41,6 +41,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -63,6 +64,28 @@ describe('FlyoutRiskSummary', () => { (mockHostRiskScoreState.data?.[0].host.risk.category_2_score ?? 0) }` ); + + expect(getByTestId('riskInputsTitleLink')).toBeInTheDocument(); + expect(getByTestId('riskInputsTitleIcon')).toBeInTheDocument(); + }); + + it('renders link without icon when in preview mode', () => { + const { getByTestId, queryByTestId } = render( + + {}} + recalculatingScore={false} + isLinkEnabled + isPreviewMode + /> + + ); + + expect(getByTestId('risk-summary-table')).toBeInTheDocument(); + expect(getByTestId('riskInputsTitleLink')).toBeInTheDocument(); + expect(queryByTestId('riskInputsTitleIcon')).not.toBeInTheDocument(); }); it('renders risk summary table when riskScoreData is empty', () => { @@ -73,6 +96,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -87,6 +111,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -94,7 +119,7 @@ describe('FlyoutRiskSummary', () => { expect(queryByTestId('riskInputsTitleLink')).not.toBeInTheDocument(); }); - it('risk summary header does not render expand icon when in preview mode', () => { + it('risk summary header does not render link when link is not enabled', () => { const { queryByTestId } = render( { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} - isPreviewMode + isLinkEnabled={false} /> ); expect(queryByTestId('riskInputsTitleLink')).not.toBeInTheDocument(); - expect(queryByTestId('riskInputsTitleIcon')).not.toBeInTheDocument(); }); it('renders visualization embeddable', () => { @@ -119,6 +143,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -134,6 +159,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -149,6 +175,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -176,6 +203,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -198,6 +226,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -220,6 +249,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index 0c42543a7f91e..f655f346f93f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -23,6 +23,7 @@ import { euiThemeVars } from '@kbn/ui-theme'; import dateMath from '@kbn/datemath'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; import { ONE_WEEK_IN_HOURS } from '../../../flyout/entity_details/shared/constants'; @@ -49,7 +50,8 @@ export interface RiskSummaryProps { riskScoreData: RiskScoreState; recalculatingScore: boolean; queryId: string; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; isPreviewMode?: boolean; } @@ -58,6 +60,7 @@ const FlyoutRiskSummaryComponent = ({ recalculatingScore, queryId, openDetailsPanel, + isLinkEnabled, isPreviewMode, }: RiskSummaryProps) => { const { telemetry } = useKibana().services; @@ -178,8 +181,8 @@ const FlyoutRiskSummaryComponent = ({ link: riskScoreData.loading ? undefined : { - callback: openDetailsPanel - ? () => openDetailsPanel(EntityDetailsLeftPanelTab.RISK_INPUTS) + callback: isLinkEnabled + ? () => openDetailsPanel({ tab: EntityDetailsLeftPanelTab.RISK_INPUTS }) : undefined, tooltip: ( {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('no observed data', () => ( @@ -62,6 +63,7 @@ storiesOf('Components/HostPanelContent', module) hostName={'test-host-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('loading', () => ( @@ -87,5 +89,6 @@ storiesOf('Components/HostPanelContent', module) hostName={'test-host-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx index 9c2ce61dea7fc..5b8746675cfdf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx @@ -17,7 +17,7 @@ import { ObservedEntity } from '../shared/components/observed_entity'; import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '.'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHostFields } from './hooks/use_observed_host_fields'; -import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import type { EntityDetailsPath } from '../shared/components/left_panel/left_panel_header'; interface HostPanelContentProps { observedHost: ObservedEntityData; @@ -25,11 +25,12 @@ interface HostPanelContentProps { contextID: string; scopeId: string; isDraggable: boolean; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; hostName: string; onAssetCriticalityChange: () => void; recalculatingScore: boolean; isPreviewMode?: boolean; + isLinkEnabled: boolean; } export const HostPanelContent = ({ @@ -43,6 +44,7 @@ export const HostPanelContent = ({ openDetailsPanel, onAssetCriticalityChange, isPreviewMode, + isLinkEnabled, }: HostPanelContentProps) => { const observedFields = useObservedHostFields(observedHost); @@ -56,6 +58,7 @@ export const HostPanelContent = ({ queryId={HOST_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} isPreviewMode={isPreviewMode} + isLinkEnabled={isLinkEnabled} /> @@ -64,7 +67,13 @@ export const HostPanelContent = ({ entity={{ name: hostName, type: 'host' }} onChange={onAssetCriticalityChange} /> - + { + const original = jest.requireActual('../../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + ...original.useKibana(), + services: { + ...original.useKibana().services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +const mockProps = { + hostName: 'testHost', + scopeId: 'testScopeId', + isRiskScoreExist: false, + hasMisconfigurationFindings: false, + hasVulnerabilitiesFindings: false, + hasNonClosedAlerts: false, + contextID: 'testContextID', + isPreviewMode: false, +}; + +const tab = EntityDetailsLeftPanelTab.RISK_INPUTS; +const subTab = CspInsightLeftPanelSubTab.MISCONFIGURATIONS; + +const mockOpenLeftPanel = jest.fn(); +const mockOpenFlyout = jest.fn(); + +describe('useNavigateToHostDetails', () => { + describe('when preview navigation is enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToHostDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: HostDetailsPanelKey, + params: { + name: mockProps.hostName, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasVulnerabilitiesFindings: mockProps.hasVulnerabilitiesFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + + it('returns callback that opens flyout when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToHostDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenFlyout).toHaveBeenCalledWith({ + right: { + id: HostPanelKey, + params: { + contextID: mockProps.contextID, + scopeId: mockProps.scopeId, + hostName: mockProps.hostName, + isDraggable: undefined, + }, + }, + left: { + id: HostDetailsPanelKey, + params: { + name: mockProps.hostName, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasVulnerabilitiesFindings: mockProps.hasVulnerabilitiesFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }, + }); + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + }); + }); + + describe('when preview navigation is not enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToHostDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: HostDetailsPanelKey, + params: { + name: mockProps.hostName, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasVulnerabilitiesFindings: mockProps.hasVulnerabilitiesFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + + it('returns empty callback and isLinkEnabled is false when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToHostDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(false); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_navigate_to_host_details.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_navigate_to_host_details.ts new file mode 100644 index 0000000000000..2834446193e8b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_navigate_to_host_details.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useMemo } from 'react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useKibana } from '../../../../common/lib/kibana'; +import { HostPanelKey } from '..'; +import { HostDetailsPanelKey } from '../../host_details_left'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +interface UseNavigateToHostDetailsParams { + hostName: string; + scopeId: string; + isRiskScoreExist: boolean; + hasMisconfigurationFindings: boolean; + hasVulnerabilitiesFindings: boolean; + hasNonClosedAlerts: boolean; + isPreviewMode?: boolean; + contextID: string; + isDraggable?: boolean; +} + +interface UseNavigateToHostDetailsResult { + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; +} + +export const useNavigateToHostDetails = ({ + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + isPreviewMode, + contextID, + isDraggable, +}: UseNavigateToHostDetailsParams): UseNavigateToHostDetailsResult => { + const { telemetry } = useKibana().services; + const { openLeftPanel, openFlyout } = useExpandableFlyoutApi(); + const isNewNavigationEnabled = useIsExperimentalFeatureEnabled( + 'newExpandableFlyoutNavigationEnabled' + ); + + telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { + entity: 'host', + }); + + const isLinkEnabled = useMemo(() => { + return !isPreviewMode || (isNewNavigationEnabled && isPreviewMode); + }, [isNewNavigationEnabled, isPreviewMode]); + + const openDetailsPanel = useCallback( + (path?: EntityDetailsPath) => { + const left = { + id: HostDetailsPanelKey, + params: { + name: hostName, + scopeId, + isRiskScoreExist, + path, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + }, + }; + + const right = { + id: HostPanelKey, + params: { + contextID, + scopeId, + hostName, + isDraggable, + }, + }; + + // When new navigation is enabled, nevigation in preview is enabled and open a new flyout + if (isNewNavigationEnabled && isPreviewMode) { + openFlyout({ right, left }); + } + // When not in preview mode, open left panel as usual + else if (!isPreviewMode) { + openLeftPanel(left); + } + }, + [ + isNewNavigationEnabled, + isPreviewMode, + openFlyout, + openLeftPanel, + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + contextID, + isDraggable, + ] + ); + + return { openDetailsPanel, isLinkEnabled }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index abf7d5cf591dd..6df18796d45e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations'; import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities'; @@ -18,7 +17,6 @@ import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_ref import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import type { Refetch } from '../../../common/types'; import { useCalculateEntityRiskScore } from '../../../entity_analytics/api/hooks/use_calculate_entity_risk_score'; -import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { useQueryInspector } from '../../../common/components/page/manage_query'; @@ -33,10 +31,10 @@ import { HostPanelHeader } from './header'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHost } from './hooks/use_observed_host'; -import { HostDetailsPanelKey } from '../host_details_left'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { HostPreviewPanelFooter } from '../host_preview/footer'; -import { EntityEventTypes } from '../../../common/lib/telemetry'; +import { useNavigateToHostDetails } from './hooks/use_navigate_to_host_details'; + export interface HostPanelProps extends Record { contextID: string; scopeId: string; @@ -67,8 +65,6 @@ export const HostPanel = ({ isDraggable, isPreviewMode, }: HostPanelProps) => { - const { telemetry } = useKibana().services; - const { openLeftPanel } = useExpandableFlyoutApi(); const { to, from, isInitializing, setQuery, deleteQuery } = useGlobalTime(); const hostNameFilterQuery = useMemo( () => (hostName ? buildHostNamesFilter([hostName]) : undefined), @@ -119,45 +115,26 @@ export const HostPanel = ({ setQuery, }); - const openTabPanel = useCallback( - (tab?: EntityDetailsLeftPanelTab) => { - telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { - entity: 'host', - }); - - openLeftPanel({ - id: HostDetailsPanelKey, - params: { - name: hostName, - scopeId, - isRiskScoreExist, - path: tab ? { tab } : undefined, - hasMisconfigurationFindings, - hasVulnerabilitiesFindings, - hasNonClosedAlerts, - }, - }); - }, - [ - telemetry, - openLeftPanel, - hostName, - scopeId, - isRiskScoreExist, - hasMisconfigurationFindings, - hasVulnerabilitiesFindings, - hasNonClosedAlerts, - ] - ); + const { openDetailsPanel, isLinkEnabled } = useNavigateToHostDetails({ + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + isPreviewMode, + contextID, + isDraggable, + }); const openDefaultPanel = useCallback( () => - openTabPanel( - isRiskScoreExist + openDetailsPanel({ + tab: isRiskScoreExist ? EntityDetailsLeftPanelTab.RISK_INPUTS - : EntityDetailsLeftPanelTab.CSP_INSIGHTS - ), - [isRiskScoreExist, openTabPanel] + : EntityDetailsLeftPanelTab.CSP_INSIGHTS, + }), + [isRiskScoreExist, openDetailsPanel] ); const observedHost = useObservedHost(hostName, scopeId); @@ -204,7 +181,8 @@ export const HostPanel = ({ contextID={contextID} scopeId={scopeId} isDraggable={!!isDraggable} - openDetailsPanel={!isPreviewMode ? openTabPanel : undefined} + openDetailsPanel={openDetailsPanel} + isLinkEnabled={isLinkEnabled} recalculatingScore={recalculatingScore} onAssetCriticalityChange={calculateEntityRiskScore} isPreviewMode={isPreviewMode} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index 254985b865840..2d7fc23115eb7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -31,6 +31,11 @@ export enum CspInsightLeftPanelSubTab { ALERTS = 'alertsTabId', } +export interface EntityDetailsPath { + tab: EntityDetailsLeftPanelTab; + subTab?: CspInsightLeftPanelSubTab; +} + export interface PanelHeaderProps { /** * Id of the tab selected in the parent component to display its content diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx index 632f32dca57b0..b02f81b0f445e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx @@ -20,6 +20,7 @@ describe('ManagedUser', () => { scopeId: '', isDraggable: false, openDetailsPanel: () => {}, + isLinkEnabled: true, }; it('renders', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx index a67de667612c8..48cb42e2a4335 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx @@ -18,7 +18,7 @@ import { import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/css'; -import type { EntityDetailsLeftPanelTab } from '../../shared/components/left_panel/left_panel_header'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; import { UserAssetTableType } from '../../../../explore/users/store/model'; import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details'; import { ManagedUserDatasetKey } from '../../../../../common/search_strategy/security_solution/users/managed_details'; @@ -44,11 +44,15 @@ export const ManagedUser = ({ contextID, isDraggable, openDetailsPanel, + isPreviewMode, + isLinkEnabled, }: { managedUser: ManagedUserData; contextID: string; isDraggable: boolean; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; + isPreviewMode?: boolean; + isLinkEnabled: boolean; }) => { const entraManagedUser = managedUser.data?.[ManagedUserDatasetKey.ENTRA]; const oktaManagedUser = managedUser.data?.[ManagedUserDatasetKey.OKTA]; @@ -127,6 +131,8 @@ export const ManagedUser = ({ managedUser={entraManagedUser.fields} tableType={UserAssetTableType.assetEntra} openDetailsPanel={openDetailsPanel} + isLinkEnabled={isLinkEnabled} + isPreviewMode={isPreviewMode} > { managedUser={mockEntraUserFields} tableType={UserAssetTableType.assetEntra} openDetailsPanel={() => {}} + isLinkEnabled >
@@ -28,5 +29,45 @@ describe('ManagedUserAccordion', () => { ); expect(getByTestId('test-children')).toBeInTheDocument(); + expect(getByTestId('managed-user-accordion-userAssetEntraTitleLink')).toBeInTheDocument(); + expect(getByTestId('managed-user-accordion-userAssetEntraTitleIcon')).toBeInTheDocument(); + }); + + it('renders link without icon when in preview mode', () => { + const { getByTestId, queryByTestId } = render( + + {}} + isLinkEnabled + isPreviewMode + > +
+ + + ); + + expect(getByTestId('managed-user-accordion-userAssetEntraTitleLink')).toBeInTheDocument(); + expect(queryByTestId('managed-user-accordion-userAssetEntraTitleIcon')).not.toBeInTheDocument(); + }); + + it('does not render link when link is not enabled', () => { + const { queryByTestId } = render( + + {}} + isLinkEnabled={false} + > +
+ + + ); + + expect(queryByTestId('managed-user-accordion-userAssetEntraTitleLink')).not.toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx index 8d9007713549e..4e4745b33fc06 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { get } from 'lodash/fp'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; import { EntityDetailsLeftPanelTab } from '../../shared/components/left_panel/left_panel_header'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details'; @@ -23,7 +24,9 @@ interface ManagedUserAccordionProps { title: string; managedUser: ManagedUserFields; tableType: UserAssetTableType; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; + isPreviewMode?: boolean; } export const ManagedUserAccordion: React.FC = ({ @@ -32,6 +35,8 @@ export const ManagedUserAccordion: React.FC = ({ managedUser, tableType, openDetailsPanel, + isLinkEnabled, + isPreviewMode, }) => { const xsFontSize = useEuiFontSize('xxs').fontSize; const timestamp = get('@timestamp[0]', managedUser) as unknown as string | undefined; @@ -41,7 +46,7 @@ export const ManagedUserAccordion: React.FC = ({ data-test-subj={`managed-user-accordion-${tableType}`} header={{ title, - iconType: 'arrowStart', + iconType: !isPreviewMode ? 'arrowStart' : undefined, headerContent: timestamp && ( = ({ ), link: { - callback: openDetailsPanel + callback: isLinkEnabled ? () => - openDetailsPanel( - tableType === UserAssetTableType.assetOkta - ? EntityDetailsLeftPanelTab.OKTA - : EntityDetailsLeftPanelTab.ENTRA - ) + openDetailsPanel({ + tab: + tableType === UserAssetTableType.assetOkta + ? EntityDetailsLeftPanelTab.OKTA + : EntityDetailsLeftPanelTab.ENTRA, + }) : undefined, tooltip: ( {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('integration disabled', () => ( @@ -56,6 +57,7 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('no managed data', () => ( @@ -74,6 +76,7 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('no observed data', () => ( @@ -112,6 +115,7 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('loading', () => ( @@ -154,5 +158,6 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index 08295038a1bd8..975e780582ac6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -22,7 +22,7 @@ import { FlyoutBody } from '../../shared/components/flyout_body'; import { ObservedEntity } from '../shared/components/observed_entity'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedUserItems } from './hooks/use_observed_user_items'; -import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import type { EntityDetailsPath } from '../shared/components/left_panel/left_panel_header'; import { EntityInsight } from '../../../cloud_security_posture/components/entity_insight'; interface UserPanelContentProps { @@ -35,8 +35,9 @@ interface UserPanelContentProps { scopeId: string; isDraggable: boolean; onAssetCriticalityChange: () => void; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; isPreviewMode?: boolean; + isLinkEnabled: boolean; } export const UserPanelContent = ({ @@ -51,6 +52,7 @@ export const UserPanelContent = ({ openDetailsPanel, onAssetCriticalityChange, isPreviewMode, + isLinkEnabled, }: UserPanelContentProps) => { const observedFields = useObservedUserItems(observedUser); const isManagedUserEnable = useIsExperimentalFeatureEnabled('newUserDetailsFlyoutManagedUser'); @@ -65,6 +67,7 @@ export const UserPanelContent = ({ queryId={USER_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} isPreviewMode={isPreviewMode} + isLinkEnabled={isLinkEnabled} /> @@ -73,7 +76,13 @@ export const UserPanelContent = ({ entity={{ name: userName, type: 'user' }} onChange={onAssetCriticalityChange} /> - + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.test.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.test.ts new file mode 100644 index 0000000000000..58f9860389d69 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useNavigateToUserDetails } from './use_navigate_to_user_details'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../shared/components/left_panel/left_panel_header'; +import { UserDetailsPanelKey } from '../../user_details_left'; +import { UserPanelKey } from '..'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; + +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../../../common/hooks/use_experimental_features'); + +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + ...original.useKibana(), + services: { + ...original.useKibana().services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +const mockProps = { + userName: 'testUser', + scopeId: 'testScopeId', + isRiskScoreExist: false, + hasMisconfigurationFindings: false, + hasNonClosedAlerts: false, + contextID: 'testContextID', + isPreviewMode: false, + email: ['test@test.com'], +}; + +const tab = EntityDetailsLeftPanelTab.RISK_INPUTS; +const subTab = CspInsightLeftPanelSubTab.MISCONFIGURATIONS; + +const mockOpenLeftPanel = jest.fn(); +const mockOpenFlyout = jest.fn(); + +describe('useNavigateToUserDetails', () => { + describe('when preview navigation is enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToUserDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: UserDetailsPanelKey, + params: { + user: { + name: mockProps.userName, + email: mockProps.email, + }, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + }); + + it('returns callback that opens flyout when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToUserDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenFlyout).toHaveBeenCalledWith({ + right: { + id: UserPanelKey, + params: { + contextID: mockProps.contextID, + scopeId: mockProps.scopeId, + userName: mockProps.userName, + }, + }, + left: { + id: UserDetailsPanelKey, + params: { + user: { + name: mockProps.userName, + email: mockProps.email, + }, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }, + }); + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + }); + }); + + describe('when preview navigation is disabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToUserDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: UserDetailsPanelKey, + params: { + user: { + name: mockProps.userName, + email: mockProps.email, + }, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + + it('returns empty callback and isLinkEnabled is false when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToUserDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(false); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.ts new file mode 100644 index 0000000000000..1eed953f703b6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useCallback } from 'react'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; +import { UserPanelKey } from '..'; +import { useKibana } from '../../../../common/lib/kibana'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; +import { UserDetailsPanelKey } from '../../user_details_left'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +interface UseNavigateToUserDetailsParams { + userName: string; + email?: string[]; + scopeId: string; + contextID: string; + isDraggable?: boolean; + isRiskScoreExist: boolean; + hasMisconfigurationFindings: boolean; + hasNonClosedAlerts: boolean; + isPreviewMode?: boolean; +} + +interface UseNavigateToUserDetailsResult { + /** + * Opens the user details panel + */ + openDetailsPanel: (path: EntityDetailsPath) => void; + /** + * Whether the link is enabled + */ + isLinkEnabled: boolean; +} + +export const useNavigateToUserDetails = ({ + userName, + email, + scopeId, + contextID, + isDraggable, + isRiskScoreExist, + hasMisconfigurationFindings, + hasNonClosedAlerts, + isPreviewMode, +}: UseNavigateToUserDetailsParams): UseNavigateToUserDetailsResult => { + const { telemetry } = useKibana().services; + const { openLeftPanel, openFlyout } = useExpandableFlyoutApi(); + const isNewNavigationEnabled = useIsExperimentalFeatureEnabled( + 'newExpandableFlyoutNavigationEnabled' + ); + + const isLinkEnabled = !isPreviewMode || (isNewNavigationEnabled && isPreviewMode); + + const openDetailsPanel = useCallback( + (path: EntityDetailsPath) => { + telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { entity: 'user' }); + + const left = { + id: UserDetailsPanelKey, + params: { + isRiskScoreExist, + scopeId, + user: { + name: userName, + email, + }, + path, + hasMisconfigurationFindings, + hasNonClosedAlerts, + }, + }; + + const right = { + id: UserPanelKey, + params: { + contextID, + userName, + scopeId, + isDraggable, + }, + }; + + // When new navigation is enabled, nevigation in preview is enabled and open a new flyout + if (isNewNavigationEnabled && isPreviewMode) { + openFlyout({ right, left }); + } + // When not in preview mode, open left panel as usual + else if (!isPreviewMode) { + openLeftPanel(left); + } + }, + [ + telemetry, + openLeftPanel, + isRiskScoreExist, + scopeId, + userName, + email, + hasMisconfigurationFindings, + hasNonClosedAlerts, + isNewNavigationEnabled, + isPreviewMode, + openFlyout, + contextID, + isDraggable, + ] + ); + + return { openDetailsPanel, isLinkEnabled }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index 182740a5afa57..7d11cb80369c4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations'; import { TableId } from '@kbn/securitysolution-data-table'; import { useNonClosedAlerts } from '../../../cloud_security_posture/hooks/use_non_closed_alerts'; @@ -15,7 +14,6 @@ import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_ref import type { Refetch } from '../../../common/types'; import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import { useCalculateEntityRiskScore } from '../../../entity_analytics/api/hooks/use_calculate_entity_risk_score'; -import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details'; import { useManagedUser } from '../shared/hooks/use_managed_user'; @@ -30,12 +28,11 @@ import { FlyoutLoading } from '../../shared/components/flyout_loading'; import { FlyoutNavigation } from '../../shared/components/flyout_navigation'; import { UserPanelContent } from './content'; import { UserPanelHeader } from './header'; -import { UserDetailsPanelKey } from '../user_details_left'; import { useObservedUser } from './hooks/use_observed_user'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { UserPreviewPanelFooter } from '../user_preview/footer'; import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types'; -import { EntityEventTypes } from '../../../common/lib/telemetry'; +import { useNavigateToUserDetails } from './hooks/use_navigate_to_user_details'; export interface UserPanelProps extends Record { contextID: string; @@ -65,7 +62,6 @@ export const UserPanel = ({ isDraggable, isPreviewMode, }: UserPanelProps) => { - const { telemetry } = useKibana().services; const userNameFilterQuery = useMemo( () => (userName ? buildUserNamesFilter([userName]) : undefined), [userName] @@ -120,47 +116,26 @@ export const UserPanel = ({ setQuery, }); - const { openLeftPanel } = useExpandableFlyoutApi(); - const openPanelTab = useCallback( - (tab?: EntityDetailsLeftPanelTab) => { - telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { - entity: 'user', - }); - - openLeftPanel({ - id: UserDetailsPanelKey, - params: { - isRiskScoreExist: !!userRiskData?.user?.risk, - scopeId, - user: { - name: userName, - email, - }, - path: tab ? { tab } : undefined, - hasMisconfigurationFindings, - hasNonClosedAlerts, - }, - }); - }, - [ - telemetry, - openLeftPanel, - userRiskData?.user?.risk, - scopeId, - userName, - email, - hasMisconfigurationFindings, - hasNonClosedAlerts, - ] - ); + const { openDetailsPanel, isLinkEnabled } = useNavigateToUserDetails({ + userName, + email, + scopeId, + contextID, + isDraggable, + isRiskScoreExist: !!userRiskData?.user?.risk, + hasMisconfigurationFindings, + hasNonClosedAlerts, + isPreviewMode, + }); + const openPanelFirstTab = useCallback( () => - openPanelTab( - isRiskScoreExist + openDetailsPanel({ + tab: isRiskScoreExist ? EntityDetailsLeftPanelTab.RISK_INPUTS - : EntityDetailsLeftPanelTab.CSP_INSIGHTS - ), - [isRiskScoreExist, openPanelTab] + : EntityDetailsLeftPanelTab.CSP_INSIGHTS, + }), + [isRiskScoreExist, openDetailsPanel] ); const hasUserDetailsData = @@ -213,8 +188,9 @@ export const UserPanel = ({ contextID={contextID} scopeId={scopeId} isDraggable={!!isDraggable} - openDetailsPanel={!isPreviewMode ? openPanelTab : undefined} + openDetailsPanel={openDetailsPanel} isPreviewMode={isPreviewMode} + isLinkEnabled={isLinkEnabled} /> {isPreviewMode && ( { +export const AdditionalChargesMessage: React.FC = () => { return (
{ADDITIONAL_CHARGES_MESSAGE} @@ -18,4 +18,4 @@ export const EnablementModalCallout: React.FC = () => { }; // eslint-disable-next-line import/no-default-export -export default EnablementModalCallout; +export default AdditionalChargesMessage; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/components/enablement_modal_callout/index.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/components/additional_charges_message/index.tsx similarity index 68% rename from x-pack/solutions/security/plugins/security_solution_serverless/public/components/enablement_modal_callout/index.tsx rename to x-pack/solutions/security/plugins/security_solution_serverless/public/components/additional_charges_message/index.tsx index 0bc65a33d6530..c35e4ed653ffa 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/components/enablement_modal_callout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/components/additional_charges_message/index.tsx @@ -7,13 +7,13 @@ import React from 'react'; import type { Services } from '../../common/services'; import { ServicesProvider } from '../../common/services'; -import { EnablementModalCallout } from './lazy'; +import { AdditionalChargesMessage } from './lazy'; -export const getEnablementModalCallout = (services: Services): React.ComponentType => - function EnablementModalCalloutComponent() { +export const getAdditionalChargesMessage = (services: Services): React.ComponentType => + function AdditionalChargesMessageComponent() { return ( - + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/components/enablement_modal_callout/lazy.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/components/additional_charges_message/lazy.tsx similarity index 70% rename from x-pack/solutions/security/plugins/security_solution_serverless/public/components/enablement_modal_callout/lazy.tsx rename to x-pack/solutions/security/plugins/security_solution_serverless/public/components/additional_charges_message/lazy.tsx index 547a15fc535e9..e2708b7351019 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/components/enablement_modal_callout/lazy.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/components/additional_charges_message/lazy.tsx @@ -8,10 +8,10 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -const EnablementModalCalloutLazy = lazy(() => import('./enablement_modal_callout')); +const AdditionalChargesMessageLazy = lazy(() => import('./additional_charges_message')); -export const EnablementModalCallout = () => ( +export const AdditionalChargesMessage = () => ( }> - + ); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/plugin.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/plugin.ts index 30e0f86ccdacf..09eb56ec0edcb 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/plugin.ts @@ -23,7 +23,7 @@ import { type ExperimentalFeatures, } from '../common/experimental_features'; import { setOnboardingSettings } from './onboarding'; -import { getEnablementModalCallout } from './components/enablement_modal_callout'; +import { getAdditionalChargesMessage } from './components/additional_charges_message'; export class SecuritySolutionServerlessPlugin implements @@ -70,7 +70,7 @@ export class SecuritySolutionServerlessPlugin securitySolution.setComponents({ DashboardsLandingCallout: getDashboardsLandingCallout(services), - EnablementModalCallout: getEnablementModalCallout(services), + AdditionalChargesMessage: getAdditionalChargesMessage(services), }); setOnboardingSettings(services); diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index e2be81a7d40e5..18a61b85c5f40 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -75,7 +75,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('Validation', () => { - it('should return 400 when missing `eventIds` field', async () => { + it('should return 400 when missing `originEventIds` field', async () => { await postGraph(supertest, { // @ts-expect-error ignore error for testing query: { @@ -171,6 +171,7 @@ export default function (providerContext: FtrProviderContext) { 'primary', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('dashed'); }); }); @@ -201,6 +202,7 @@ export default function (providerContext: FtrProviderContext) { 'danger', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -231,6 +233,7 @@ export default function (providerContext: FtrProviderContext) { 'primary', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -261,6 +264,7 @@ export default function (providerContext: FtrProviderContext) { 'danger', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -303,6 +307,7 @@ export default function (providerContext: FtrProviderContext) { 'warning', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('dashed'); }); }); @@ -351,10 +356,11 @@ export default function (providerContext: FtrProviderContext) { : 'primary', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('dashed'); }); }); - it('should support more than 1 eventIds', async () => { + it('should support more than 1 originEventIds', async () => { const response = await postGraph(supertest, { query: { originEventIds: [ @@ -384,6 +390,7 @@ export default function (providerContext: FtrProviderContext) { 'danger', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -429,6 +436,7 @@ export default function (providerContext: FtrProviderContext) { idx <= 1 ? 'danger' : 'warning', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal(idx <= 1 ? 'solid' : 'dashed'); }); });