diff --git a/docs/discover/log-pattern-analysis.asciidoc b/docs/discover/log-pattern-analysis.asciidoc index f3aff8549a01a..20e07aeb1d8d6 100644 --- a/docs/discover/log-pattern-analysis.asciidoc +++ b/docs/discover/log-pattern-analysis.asciidoc @@ -1,7 +1,7 @@ [[run-pattern-analysis-discover]] == Run a pattern analysis on your log data -preview::[] +preview::["This functionality is in technical preview, requires a link:https://www.elastic.co/subscriptions[Platinum subscription], and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features."] include::../user/ml/index.asciidoc[tag=log-pattern-analysis-intro] Log pattern analysis works on every text field. diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 4fd489b5bbebd..63691b878eaa7 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -77,6 +77,15 @@ spec: # - BPF # (since Linux 5.8) allows loading of BPF programs, create most map types, load BTF, iterate programs and maps. # - PERFMON # (since Linux 5.8) allows attaching of BPF programs used for performance metrics and observability operations. # - SYS_RESOURCE # Allow use of special resources or raising of resource limits. Used by 'Defend for Containers' to modify 'rlimit_memlock' + ######################################################################################## + # The following capabilities are needed for Universal Profiling. + # More fine graded capabilities are only available for newer Linux kernels. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #procMount: "Unmasked" + #privileged: true + #capabilities: + # add: + # - SYS_ADMIN resources: limits: memory: 700Mi @@ -113,6 +122,9 @@ spec: mountPath: /sys/kernel/debug - name: elastic-agent-state mountPath: /usr/share/elastic-agent/state + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # mountPath: /var/cache/Elastic volumes: - name: datastreams configMap: @@ -142,8 +154,8 @@ spec: - name: var-lib hostPath: path: /var/lib - # Needed for 'Defend for containers' integration (cloud-defend) - # If you are not using this integration, then these volumes and the corresponding + # Needed for 'Defend for containers' integration (cloud-defend) and Universal Profiling + # If you are not using one of these integrations, then these volumes and the corresponding # mounts can be removed. - name: sys-kernel-debug hostPath: @@ -154,6 +166,12 @@ spec: hostPath: path: /var/lib/elastic-agent/kube-system/state type: DirectoryOrCreate + # Mount required for Universal Profiling. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # hostPath: + # path: /var/cache/Elastic + # type: DirectoryOrCreate --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -380,6 +398,15 @@ spec: # - BPF # (since Linux 5.8) allows loading of BPF programs, create most map types, load BTF, iterate programs and maps. # - PERFMON # (since Linux 5.8) allows attaching of BPF programs used for performance metrics and observability operations. # - SYS_RESOURCE # Allow use of special resources or raising of resource limits. Used by 'Defend for Containers' to modify 'rlimit_memlock' + ######################################################################################## + # The following capabilities are needed for Universal Profiling. + # More fine graded capabilities are only available for newer Linux kernels. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #procMount: "Unmasked" + #privileged: true + #capabilities: + # add: + # - SYS_ADMIN resources: limits: memory: 700Mi @@ -412,6 +439,9 @@ spec: mountPath: /sys/kernel/debug - name: elastic-agent-state mountPath: /usr/share/elastic-agent/state + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # mountPath: /var/cache/Elastic volumes: - name: proc hostPath: @@ -440,8 +470,8 @@ spec: hostPath: path: /etc/machine-id type: File - # Needed for 'Defend for containers' integration (cloud-defend) - # If you are not using this integration, then these volumes and the corresponding + # Needed for 'Defend for containers' integration (cloud-defend) and Universal Profiling + # If you are not using one of these integrations, then these volumes and the corresponding # mounts can be removed. - name: sys-kernel-debug hostPath: @@ -452,6 +482,12 @@ spec: hostPath: path: /var/lib/elastic-agent-managed/kube-system/state type: DirectoryOrCreate + # Mount required for Universal Profiling. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # hostPath: + # path: /var/cache/Elastic + # type: DirectoryOrCreate --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 310c10aaa1581..9af52e3c724c7 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from '@kbn/core/public'; @@ -34,6 +34,7 @@ import { } from '@kbn/securitysolution-list-utils'; import { DataViewBase } from '@kbn/es-query'; import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; +import deepEqual from 'fast-deep-equal'; import { AndOrBadge } from '../and_or_badge'; @@ -41,7 +42,6 @@ import { BuilderExceptionListItemComponent } from './exception_item_renderer'; import { BuilderLogicButtons } from './logic_buttons'; import { getTotalErrorExist } from './selectors'; import { EntryFieldError, State, exceptionsBuilderReducer } from './reducer'; - const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; `; @@ -131,6 +131,7 @@ export const ExceptionBuilderComponent = ({ disableNested: isNestedDisabled, disableOr: isOrDisabled, }); + const [areAllEntriesDeleted, setAreAllEntriesDeleted] = useState(false); const { addNested, @@ -252,6 +253,7 @@ export const ExceptionBuilderComponent = ({ // just add a default entry to it if (updatedExceptions.length === 0) { setDefaultExceptions(item); + setAreAllEntriesDeleted(true); } else if (updatedExceptions.length > 0 && exceptionListItemSchema.is(item)) { setUpdateExceptionsToDelete([...exceptionsToDelete, item]); } else { @@ -394,12 +396,36 @@ export const ExceptionBuilderComponent = ({ } }, [exceptions, handleAddNewExceptionItem]); + /** + * This component relies on the "exceptionListItems" to pre-fill its entries, + * but any subsequent updates to the entries are not reflected back to + * the "exceptionListItems". To ensure correct behavior, we need to only + * fill the entries from the "exceptionListItems" during initialization. + * + * In the initialization phase, if there are "exceptionListItems" with + * pre-filled entries, the exceptions array will be empty. However, + * there are cases where the "exceptionListItems" may not be sent + * correctly during initialization, leading to the exceptions + * array being filled with empty entries. Therefore, we need to + * check if the exception is correctly populated with a valid + * "field" when the "exceptionListItems" has entries. that's why + * "exceptionsEntriesPopulated" is used + * + * It's important to differentiate this case from when the user + * deletes all the entries and the "exceptionListItems" has pre-filled values. + * that's why "allEntriesDeleted" is used + * + * deepEqual(exceptionListItems, exceptions) to handle the exceptionListItems in + * the EventFiltersFlyout + */ useEffect(() => { - if (exceptionListItems.length > 0) { + if (!exceptionListItems.length || deepEqual(exceptionListItems, exceptions)) return; + const exceptionsEntriesPopulated = exceptions.some((exception) => + exception.entries.some((entry) => entry.field) + ); + if (!exceptionsEntriesPopulated && !areAllEntriesDeleted) setUpdateExceptions(exceptionListItems); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [areAllEntriesDeleted, exceptionListItems, exceptions, setUpdateExceptions]); return ( diff --git a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx index 5fc9fd4f997a1..4d143f77b35d0 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx @@ -13,6 +13,7 @@ import { PartialTheme, Settings, Tooltip, + FlameSpec, } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { Maybe } from '@kbn/observability-plugin/common/typings'; @@ -27,13 +28,15 @@ import { ComparisonMode } from '../normalization_menu'; interface Props { id: string; - comparisonMode: ComparisonMode; + comparisonMode?: ComparisonMode; primaryFlamegraph?: ElasticFlameGraph; comparisonFlamegraph?: ElasticFlameGraph; baseline?: number; comparison?: number; showInformationWindow: boolean; toggleShowInformationWindow: () => void; + searchText?: string; + onChangeSearchText?: FlameSpec['onSearchTextChange']; } export function FlameGraph({ @@ -45,6 +48,8 @@ export function FlameGraph({ comparison, showInformationWindow, toggleShowInformationWindow, + searchText, + onChangeSearchText, }: Props) { const theme = useEuiTheme(); @@ -165,6 +170,8 @@ export function FlameGraph({ valueFormatter={(value) => `${value}`} animation={{ duration: 100 }} controlProviderCallback={{}} + search={searchText ? { text: searchText } : undefined} + onSearchTextChange={onChangeSearchText} /> diff --git a/x-pack/plugins/profiling/public/components/stack_frame_summary.tsx b/x-pack/plugins/profiling/public/components/stack_frame_summary.tsx deleted file mode 100644 index 9f25b4d9d8900..0000000000000 --- a/x-pack/plugins/profiling/public/components/stack_frame_summary.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React from 'react'; -import { getCalleeFunction, getCalleeSource, StackFrameMetadata } from '../../common/profiling'; - -export function StackFrameSummary({ frame }: { frame: StackFrameMetadata }) { - return ( - - -
- - {getCalleeFunction(frame)} - -
-
- - {getCalleeSource(frame) || '‎'} - -
- ); -} diff --git a/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx b/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx new file mode 100644 index 0000000000000..a92934df79009 --- /dev/null +++ b/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx @@ -0,0 +1,51 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; +import React from 'react'; +import { getCalleeFunction, getCalleeSource, StackFrameMetadata } from '../../../common/profiling'; + +interface Props { + frame: StackFrameMetadata; + onFrameClick?: (functionName: string) => void; +} + +function CalleeFunctionText({ calleeFunctionName }: { calleeFunctionName: string }) { + return ( + + {calleeFunctionName} + + ); +} + +export function StackFrameSummary({ frame, onFrameClick }: Props) { + const calleeFunctionName = getCalleeFunction(frame); + + function handleOnClick() { + if (onFrameClick) { + onFrameClick(calleeFunctionName); + } + } + + return ( + + +
+ {onFrameClick ? ( + + + + ) : ( + + )} +
+
+ + {getCalleeSource(frame) || '‎'} + +
+ ); +} diff --git a/x-pack/plugins/profiling/public/components/stacked_bar_chart/index.tsx b/x-pack/plugins/profiling/public/components/stacked_bar_chart/index.tsx index 22cb6e19e701c..a8031583535f0 100644 --- a/x-pack/plugins/profiling/public/components/stacked_bar_chart/index.tsx +++ b/x-pack/plugins/profiling/public/components/stacked_bar_chart/index.tsx @@ -17,7 +17,6 @@ import { Tooltip, XYChartElementEvent, TooltipContainer, - CustomTooltip, } from '@elastic/charts'; import { EuiPanel } from '@elastic/eui'; import { keyBy } from 'lodash'; @@ -57,7 +56,7 @@ export function StackedBarChart({ const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme(); - const CustomTooltipWithSubChart: CustomTooltip = () => { + function CustomTooltipWithSubChart() { if (!highlightedSample) { return null; } @@ -90,7 +89,7 @@ export function StackedBarChart({ ); - }; + } return ( diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx index daf9d213e8e0c..7320f11d24ec1 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx @@ -46,19 +46,28 @@ interface Row { } function TotalSamplesStat({ - totalSamples, - newSamples, + baselineTotalSamples, + baselineScaleFactor, + comparisonTotalSamples, + comparisonScaleFactor, }: { - totalSamples: number; - newSamples: number | undefined; + baselineTotalSamples: number; + baselineScaleFactor?: number; + comparisonTotalSamples?: number; + comparisonScaleFactor?: number; }) { - const value = totalSamples.toLocaleString(); + const scaledBaselineTotalSamples = scaleValue({ + value: baselineTotalSamples, + scaleFactor: baselineScaleFactor, + }); + + const value = scaledBaselineTotalSamples.toLocaleString(); const sampleHeader = i18n.translate('xpack.profiling.functionsView.totalSampleCountLabel', { defaultMessage: ' Total sample estimate: ', }); - if (newSamples === undefined || newSamples === 0) { + if (comparisonTotalSamples === undefined || comparisonTotalSamples === 0) { return ( {value}} @@ -67,8 +76,13 @@ function TotalSamplesStat({ ); } - const diffSamples = totalSamples - newSamples; - const percentDelta = (diffSamples / (totalSamples - diffSamples)) * 100; + const scaledComparisonTotalSamples = scaleValue({ + value: comparisonTotalSamples, + scaleFactor: comparisonScaleFactor, + }); + + const diffSamples = scaledBaselineTotalSamples - scaledComparisonTotalSamples; + const percentDelta = (diffSamples / (scaledBaselineTotalSamples - diffSamples)) * 100; return ( void; } function scaleValue({ value, scaleFactor = 1 }: { value: number; scaleFactor?: number }) { @@ -162,6 +177,7 @@ export function TopNFunctionsTable({ isDifferentialView, baselineScaleFactor, comparisonScaleFactor, + onFrameClick, }: Props) { const [selectedRow, setSelectedRow] = useState(); const isEstimatedA = (topNFunctions?.SamplingRate ?? 1.0) !== 1.0; @@ -260,7 +276,9 @@ export function TopNFunctionsTable({ name: i18n.translate('xpack.profiling.functionsView.functionColumnLabel', { defaultMessage: 'Function', }), - render: (_, { frame }) => , + render: (_, { frame }) => { + return ; + }, width: '50%', }, { @@ -404,8 +422,10 @@ export function TopNFunctionsTable({ return ( <> diff --git a/x-pack/plugins/profiling/public/routing/index.tsx b/x-pack/plugins/profiling/public/routing/index.tsx index d674a99aa530c..4c2861cf5d220 100644 --- a/x-pack/plugins/profiling/public/routing/index.tsx +++ b/x-pack/plugins/profiling/public/routing/index.tsx @@ -13,7 +13,9 @@ import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/fun import { StackTracesDisplayOption, TopNType } from '../../common/stack_traces'; import { ComparisonMode, NormalizationMode } from '../components/normalization_menu'; import { RedirectTo } from '../components/redirect_to'; -import { FlameGraphsView } from '../views/flame_graphs_view'; +import { FlameGraphsView } from '../views/flamegraphs'; +import { DifferentialFlameGraphsView } from '../views/flamegraphs/differential_flamegraphs'; +import { FlameGraphView } from '../views/flamegraphs/flamegraph'; import { FunctionsView } from '../views/functions'; import { DifferentialTopNFunctionsView } from '../views/functions/differential_topn'; import { TopNFunctionsView } from '../views/functions/topn'; @@ -109,9 +111,14 @@ const routes = { })} href="/flamegraphs/flamegraph" > - + ), + params: t.type({ + query: t.partial({ + searchText: t.string, + }), + }), }, '/flamegraphs/differential': { element: ( @@ -121,7 +128,7 @@ const routes = { })} href="/flamegraphs/differential" > - + ), params: t.type({ @@ -134,19 +141,23 @@ const routes = { t.literal(ComparisonMode.Absolute), t.literal(ComparisonMode.Relative), ]), - }), - t.partial({ normalizationMode: t.union([ t.literal(NormalizationMode.Scale), t.literal(NormalizationMode.Time), ]), + }), + t.partial({ baseline: toNumberRt, comparison: toNumberRt, + searchText: t.string, }), ]), }), defaults: { query: { + comparisonRangeFrom: 'now-15m', + comparisonRangeTo: 'now', + comparisonKuery: '', comparisonMode: ComparisonMode.Absolute, normalizationMode: NormalizationMode.Time, }, diff --git a/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts b/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts index 08c08e2eb6014..9e0ec087523ad 100644 --- a/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts +++ b/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts @@ -31,7 +31,7 @@ export function getFlamegraphModel({ colorSuccess, colorDanger, colorNeutral, - comparisonMode, + comparisonMode = ComparisonMode.Absolute, comparison, baseline, }: { @@ -40,7 +40,7 @@ export function getFlamegraphModel({ colorSuccess: string; colorDanger: string; colorNeutral: string; - comparisonMode: ComparisonMode; + comparisonMode?: ComparisonMode; baseline?: number; comparison?: number; }): { diff --git a/x-pack/plugins/profiling/public/views/flame_graphs_view/index.tsx b/x-pack/plugins/profiling/public/views/flame_graphs_view/index.tsx deleted file mode 100644 index b15936141f4ef..0000000000000 --- a/x-pack/plugins/profiling/public/views/flame_graphs_view/index.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderContentProps } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import React, { useState } from 'react'; -import { useProfilingParams } from '../../hooks/use_profiling_params'; -import { useProfilingRouter } from '../../hooks/use_profiling_router'; -import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path'; -import { useTimeRange } from '../../hooks/use_time_range'; -import { useTimeRangeAsync } from '../../hooks/use_time_range_async'; -import { AsyncComponent } from '../../components/async_component'; -import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies'; -import { FlameGraph } from '../../components/flamegraph'; -import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template'; -import { RedirectTo } from '../../components/redirect_to'; -import { FlameGraphSearchPanel } from './flame_graph_search_panel'; -import { - ComparisonMode, - NormalizationMode, - NormalizationOptions, -} from '../../components/normalization_menu'; - -export function FlameGraphsView({ children }: { children: React.ReactElement }) { - const { - query, - query: { rangeFrom, rangeTo, kuery }, - } = useProfilingParams('/flamegraphs/*'); - - const timeRange = useTimeRange({ rangeFrom, rangeTo }); - - const comparisonTimeRange = useTimeRange( - 'comparisonRangeFrom' in query - ? { rangeFrom: query.comparisonRangeFrom, rangeTo: query.comparisonRangeTo, optional: true } - : { rangeFrom: undefined, rangeTo: undefined, optional: true } - ); - - const comparisonKuery = 'comparisonKuery' in query ? query.comparisonKuery : ''; - const comparisonMode = 'comparisonMode' in query ? query.comparisonMode : ComparisonMode.Absolute; - - const normalizationMode: NormalizationMode = get( - query, - 'normalizationMode', - NormalizationMode.Time - ); - - const baselineScale: number = get(query, 'baseline', 1); - const comparisonScale: number = get(query, 'comparison', 1); - - const totalSeconds = timeRange.inSeconds.end - timeRange.inSeconds.start; - const totalComparisonSeconds = - (new Date(comparisonTimeRange.end!).getTime() - - new Date(comparisonTimeRange.start!).getTime()) / - 1000; - - const baselineTime = 1; - const comparisonTime = totalSeconds / totalComparisonSeconds; - - const normalizationOptions: NormalizationOptions = { - baselineScale, - baselineTime, - comparisonScale, - comparisonTime, - }; - - const { - services: { fetchElasticFlamechart }, - } = useProfilingDependencies(); - - const state = useTimeRangeAsync( - ({ http }) => { - return Promise.all([ - fetchElasticFlamechart({ - http, - timeFrom: timeRange.inSeconds.start, - timeTo: timeRange.inSeconds.end, - kuery, - }), - comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end - ? fetchElasticFlamechart({ - http, - timeFrom: comparisonTimeRange.inSeconds.start, - timeTo: comparisonTimeRange.inSeconds.end, - kuery: comparisonKuery, - }) - : Promise.resolve(undefined), - ]).then(([primaryFlamegraph, comparisonFlamegraph]) => { - return { - primaryFlamegraph, - comparisonFlamegraph, - }; - }); - }, - [ - timeRange.inSeconds.start, - timeRange.inSeconds.end, - kuery, - comparisonTimeRange.inSeconds.start, - comparisonTimeRange.inSeconds.end, - comparisonKuery, - fetchElasticFlamechart, - ] - ); - - const { data } = state; - - const routePath = useProfilingRoutePath(); - - const profilingRouter = useProfilingRouter(); - - const isDifferentialView = routePath === '/flamegraphs/differential'; - - const tabs: Required['tabs'] = [ - { - label: i18n.translate('xpack.profiling.flameGraphsView.flameGraphTabLabel', { - defaultMessage: 'Flamegraph', - }), - isSelected: !isDifferentialView, - href: profilingRouter.link('/flamegraphs/flamegraph', { query }), - }, - { - label: i18n.translate('xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel', { - defaultMessage: 'Differential flamegraph', - }), - isSelected: isDifferentialView, - href: profilingRouter.link('/flamegraphs/differential', { - // @ts-expect-error Code gets too complicated to satisfy TS constraints - query: { - ...query, - comparisonRangeFrom: query.rangeFrom, - comparisonRangeTo: query.rangeTo, - comparisonKuery: query.kuery, - }, - }), - }, - ]; - - const [showInformationWindow, setShowInformationWindow] = useState(false); - function toggleShowInformationWindow() { - setShowInformationWindow((prev) => !prev); - } - - if (routePath === '/flamegraphs') { - return ; - } - - const isNormalizedByTime = normalizationMode === NormalizationMode.Time; - - return ( - - - - - - - - - - {children} - - - - ); -} diff --git a/x-pack/plugins/profiling/public/views/flame_graphs_view/flame_graph_search_panel.tsx b/x-pack/plugins/profiling/public/views/flamegraphs/differential_flamegraphs/differential_flame_graph_search_panel.tsx similarity index 59% rename from x-pack/plugins/profiling/public/views/flame_graphs_view/flame_graph_search_panel.tsx rename to x-pack/plugins/profiling/public/views/flamegraphs/differential_flamegraphs/differential_flame_graph_search_panel.tsx index 6d1416f91079b..3b737eb4498af 100644 --- a/x-pack/plugins/profiling/public/views/flame_graphs_view/flame_graph_search_panel.tsx +++ b/x-pack/plugins/profiling/public/views/flamegraphs/differential_flamegraphs/differential_flame_graph_search_panel.tsx @@ -6,30 +6,27 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { useProfilingParams } from '../../hooks/use_profiling_params'; -import { useProfilingRouter } from '../../hooks/use_profiling_router'; -import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path'; -import { PrimaryAndComparisonSearchBar } from '../../components/primary_and_comparison_search_bar'; -import { PrimaryProfilingSearchBar } from '../../components/profiling_app_page_template/primary_profiling_search_bar'; +import { useProfilingParams } from '../../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../../hooks/use_profiling_router'; +import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path'; +import { PrimaryAndComparisonSearchBar } from '../../../components/primary_and_comparison_search_bar'; import { ComparisonMode, NormalizationMode, NormalizationOptions, NormalizationMenu, -} from '../../components/normalization_menu'; -import { DifferentialComparisonMode } from '../../components/differential_comparison_mode'; +} from '../../../components/normalization_menu'; +import { DifferentialComparisonMode } from '../../../components/differential_comparison_mode'; interface Props { - isDifferentialView: boolean; comparisonMode: ComparisonMode; normalizationMode: NormalizationMode; normalizationOptions: NormalizationOptions; } -export function FlameGraphSearchPanel({ +export function DifferentialFlameGraphSearchPanel({ comparisonMode, normalizationMode, - isDifferentialView, normalizationOptions, }: Props) { const { path, query } = useProfilingParams('/flamegraphs/*'); @@ -77,29 +74,25 @@ export function FlameGraphSearchPanel({ } return ( - {isDifferentialView ? : } + - {isDifferentialView && ( - <> - - {comparisonMode === ComparisonMode.Absolute && ( + + {comparisonMode === ComparisonMode.Absolute && ( + + - - - - - + - )} - + + )} diff --git a/x-pack/plugins/profiling/public/views/flamegraphs/differential_flamegraphs/index.tsx b/x-pack/plugins/profiling/public/views/flamegraphs/differential_flamegraphs/index.tsx new file mode 100644 index 0000000000000..474524fe15437 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/flamegraphs/differential_flamegraphs/index.tsx @@ -0,0 +1,144 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useState } from 'react'; +import { AsyncComponent } from '../../../components/async_component'; +import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies'; +import { FlameGraph } from '../../../components/flamegraph'; +import { NormalizationMode, NormalizationOptions } from '../../../components/normalization_menu'; +import { useProfilingParams } from '../../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../../hooks/use_profiling_router'; +import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { useTimeRangeAsync } from '../../../hooks/use_time_range_async'; +import { DifferentialFlameGraphSearchPanel } from './differential_flame_graph_search_panel'; + +export function DifferentialFlameGraphsView() { + const { + query, + query: { + rangeFrom, + rangeTo, + kuery, + comparisonRangeFrom, + comparisonRangeTo, + comparisonKuery, + comparisonMode, + baseline = 1, + comparison = 1, + normalizationMode, + searchText, + }, + } = useProfilingParams('/flamegraphs/differential'); + const routePath = useProfilingRoutePath(); + const profilingRouter = useProfilingRouter(); + const [showInformationWindow, setShowInformationWindow] = useState(false); + + const timeRange = useTimeRange({ rangeFrom, rangeTo }); + + const comparisonTimeRange = useTimeRange({ + rangeFrom: comparisonRangeFrom, + rangeTo: comparisonRangeTo, + optional: true, + }); + + const { + services: { fetchElasticFlamechart }, + } = useProfilingDependencies(); + + const state = useTimeRangeAsync( + ({ http }) => { + return Promise.all([ + fetchElasticFlamechart({ + http, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, + kuery, + }), + comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end + ? fetchElasticFlamechart({ + http, + timeFrom: comparisonTimeRange.inSeconds.start, + timeTo: comparisonTimeRange.inSeconds.end, + kuery: comparisonKuery, + }) + : Promise.resolve(undefined), + ]).then(([primaryFlamegraph, comparisonFlamegraph]) => { + return { + primaryFlamegraph, + comparisonFlamegraph, + }; + }); + }, + [ + timeRange.inSeconds.start, + timeRange.inSeconds.end, + kuery, + comparisonTimeRange.inSeconds.start, + comparisonTimeRange.inSeconds.end, + comparisonKuery, + fetchElasticFlamechart, + ] + ); + + const totalSeconds = timeRange.inSeconds.end - timeRange.inSeconds.start; + const totalComparisonSeconds = + (new Date(comparisonTimeRange.end!).getTime() - + new Date(comparisonTimeRange.start!).getTime()) / + 1000; + + const baselineTime = 1; + const comparisonTime = totalSeconds / totalComparisonSeconds; + + const normalizationOptions: NormalizationOptions = { + baselineScale: baseline, + baselineTime, + comparisonScale: comparison, + comparisonTime, + }; + + const { data } = state; + + const isNormalizedByTime = normalizationMode === NormalizationMode.Time; + + function toggleShowInformationWindow() { + setShowInformationWindow((prev) => !prev); + } + + function handleSearchTextChange(newSearchText: string) { + // @ts-expect-error Code gets too complicated to satisfy TS constraints + profilingRouter.push(routePath, { query: { ...query, searchText: newSearchText } }); + } + + return ( + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/flamegraphs/flamegraph/index.tsx b/x-pack/plugins/profiling/public/views/flamegraphs/flamegraph/index.tsx new file mode 100644 index 0000000000000..50d536e90fbe9 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/flamegraphs/flamegraph/index.tsx @@ -0,0 +1,81 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui'; +import React, { useState } from 'react'; +import { AsyncComponent } from '../../../components/async_component'; +import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies'; +import { FlameGraph } from '../../../components/flamegraph'; +import { PrimaryProfilingSearchBar } from '../../../components/profiling_app_page_template/primary_profiling_search_bar'; +import { useProfilingParams } from '../../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../../hooks/use_profiling_router'; +import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { useTimeRangeAsync } from '../../../hooks/use_time_range_async'; + +export function FlameGraphView() { + const { + query, + query: { rangeFrom, rangeTo, kuery, searchText }, + } = useProfilingParams('/flamegraphs/flamegraph'); + + const timeRange = useTimeRange({ rangeFrom, rangeTo }); + + const { + services: { fetchElasticFlamechart }, + } = useProfilingDependencies(); + + const state = useTimeRangeAsync( + ({ http }) => { + return fetchElasticFlamechart({ + http, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, + kuery, + }); + }, + [timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchElasticFlamechart] + ); + + const { data } = state; + + const routePath = useProfilingRoutePath(); + + const profilingRouter = useProfilingRouter(); + + const [showInformationWindow, setShowInformationWindow] = useState(false); + function toggleShowInformationWindow() { + setShowInformationWindow((prev) => !prev); + } + + function handleSearchTextChange(newSearchText: string) { + // @ts-expect-error Code gets too complicated to satisfy TS constraints + profilingRouter.push(routePath, { query: { ...query, searchText: newSearchText } }); + } + + return ( + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/flamegraphs/index.tsx b/x-pack/plugins/profiling/public/views/flamegraphs/index.tsx new file mode 100644 index 0000000000000..0d773a327984d --- /dev/null +++ b/x-pack/plugins/profiling/public/views/flamegraphs/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiPageHeaderContentProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template'; +import { RedirectTo } from '../../components/redirect_to'; +import { useProfilingParams } from '../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../hooks/use_profiling_router'; +import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path'; + +export function FlameGraphsView({ children }: { children: React.ReactElement }) { + const { query } = useProfilingParams('/flamegraphs/*'); + const routePath = useProfilingRoutePath(); + const profilingRouter = useProfilingRouter(); + + if (routePath === '/flamegraphs') { + return ; + } + + const isDifferentialView = routePath === '/flamegraphs/differential'; + + const tabs: Required['tabs'] = [ + { + label: i18n.translate('xpack.profiling.flameGraphsView.flameGraphTabLabel', { + defaultMessage: 'Flamegraph', + }), + isSelected: !isDifferentialView, + href: profilingRouter.link('/flamegraphs/flamegraph', { query }), + }, + { + label: i18n.translate('xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel', { + defaultMessage: 'Differential flamegraph', + }), + isSelected: isDifferentialView, + href: profilingRouter.link('/flamegraphs/differential', { + // @ts-expect-error Code gets too complicated to satisfy TS constraints + query: { + ...query, + comparisonRangeFrom: query.rangeFrom, + comparisonRangeTo: query.rangeTo, + comparisonKuery: query.kuery, + }, + }), + }, + ]; + + return ( + + {children} + + ); +} diff --git a/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx b/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx index 01e2a21eb4524..1c87247d227bf 100644 --- a/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx +++ b/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx @@ -135,6 +135,13 @@ export function DifferentialTopNFunctionsView() { const isNormalizedByTime = normalizationMode === NormalizationMode.Time; + function handleOnFrameClick(functionName: string) { + profilingRouter.push('/flamegraphs/flamegraph', { + path: {}, + query: { ...query, searchText: functionName }, + }); + } + return ( <> @@ -169,6 +176,7 @@ export function DifferentialTopNFunctionsView() { totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start} isDifferentialView={true} baselineScaleFactor={isNormalizedByTime ? baselineTime : baseline} + onFrameClick={handleOnFrameClick} /> @@ -196,6 +204,7 @@ export function DifferentialTopNFunctionsView() { isDifferentialView={true} baselineScaleFactor={isNormalizedByTime ? comparisonTime : comparison} comparisonScaleFactor={isNormalizedByTime ? baselineTime : baseline} + onFrameClick={handleOnFrameClick} /> diff --git a/x-pack/plugins/profiling/public/views/functions/topn/index.tsx b/x-pack/plugins/profiling/public/views/functions/topn/index.tsx index 08129d6320a11..768f5256fe3c3 100644 --- a/x-pack/plugins/profiling/public/views/functions/topn/index.tsx +++ b/x-pack/plugins/profiling/public/views/functions/topn/index.tsx @@ -46,6 +46,13 @@ export function TopNFunctionsView() { const profilingRouter = useProfilingRouter(); + function handleOnFrameClick(functionName: string) { + profilingRouter.push('/flamegraphs/flamegraph', { + path: {}, + query: { ...query, searchText: functionName }, + }); + } + return ( <> @@ -69,6 +76,7 @@ export function TopNFunctionsView() { }} totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start} isDifferentialView={false} + onFrameClick={handleOnFrameClick} /> diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts index 7497316927ee3..f333376b40231 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts @@ -7,8 +7,10 @@ import { deleteAlertsAndRules } from '../../../tasks/common'; import { + expandFirstAlert, goToClosedAlertsOnRuleDetailsPage, goToOpenedAlertsOnRuleDetailsPage, + openAddEndpointExceptionFromAlertActionButton, openAddEndpointExceptionFromFirstAlert, } from '../../../tasks/alerts'; import { login, visitWithoutDateRange } from '../../../tasks/login'; @@ -26,13 +28,22 @@ import { } from '../../../tasks/es_archiver'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { + addExceptionEntryFieldValue, + addExceptionEntryFieldValueValue, addExceptionFlyoutItemName, + editExceptionFlyoutItemName, selectCloseSingleAlerts, submitNewExceptionItem, validateExceptionConditionField, } from '../../../tasks/exceptions'; import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts'; -import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions'; +import { + ADD_AND_BTN, + EXCEPTION_CARD_ITEM_CONDITIONS, + EXCEPTION_CARD_ITEM_NAME, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_EXIST_PROMPT, +} from '../../../screens/exceptions'; import { removeException, goToAlertsTab, @@ -41,10 +52,11 @@ import { describe('Endpoint Exceptions workflows from Alert', () => { const expectedNumberOfAlerts = 1; - before(() => { - esArchiverResetKibana(); - }); + const ITEM_NAME = 'Sample Exception List Item'; + const ITEM_NAME_EDIT = 'Sample Exception List Item'; + const ADDITIONAL_ENTRY = 'host.hostname'; beforeEach(() => { + esArchiverResetKibana(); login(); deleteAlertsAndRules(); esArchiverLoad('endpoint'); @@ -69,7 +81,7 @@ describe('Endpoint Exceptions workflows from Alert', () => { validateExceptionConditionField('file.Ext.code_signature'); selectCloseSingleAlerts(); - addExceptionFlyoutItemName('Sample Exception'); + addExceptionFlyoutItemName(ITEM_NAME); submitNewExceptionItem(); // Alerts table should now be empty from having added exception and closed @@ -100,4 +112,39 @@ describe('Endpoint Exceptions workflows from Alert', () => { cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alert`); }); + + it('Should be able to create Endpoint exception from Alerts take action button, and change multiple exception items without resetting to initial auto-prefilled entries', () => { + // Open first Alert Summary + expandFirstAlert(); + + // The Endpoint should populated with predefined fields + openAddEndpointExceptionFromAlertActionButton(); + + // As the endpoint.alerts-* is used to trigger the alert the + // file.Ext.code_signature will be auto-populated + validateExceptionConditionField('file.Ext.code_signature'); + addExceptionFlyoutItemName(ITEM_NAME); + + cy.get(ADD_AND_BTN).click(); + // edit conditions + addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 6); + addExceptionEntryFieldValueValue('foo', 4); + + // Change the name again + editExceptionFlyoutItemName(ITEM_NAME_EDIT); + + // validate the condition is still "agent.name" or got rest after the name is changed + validateExceptionConditionField(ADDITIONAL_ENTRY); + + selectCloseSingleAlerts(); + submitNewExceptionItem(); + + // Endpoint Exception will move to Endpoint List under Exception tab of rule + goToEndpointExceptionsTab(); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', ADDITIONAL_ENTRY); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts index 935da49546b49..59af4592d2ebe 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts @@ -12,8 +12,10 @@ import { createRule } from '../../../tasks/api_calls/rules'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { addExceptionFromFirstAlert, + expandFirstAlert, goToClosedAlertsOnRuleDetailsPage, goToOpenedAlertsOnRuleDetailsPage, + openAddRuleExceptionFromAlertActionButton, } from '../../../tasks/alerts'; import { addExceptionEntryFieldValue, @@ -26,6 +28,9 @@ import { validateExceptionItemAffectsTheCorrectRulesInRulePage, validateExceptionConditionField, validateExceptionCommentCountAndText, + editExceptionFlyoutItemName, + validateHighlightedFieldsPopulatedAsExceptionConditions, + validateEmptyExceptionConditionField, } from '../../../tasks/exceptions'; import { esArchiverLoad, @@ -42,26 +47,44 @@ import { import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; -import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions'; +import { + ADD_AND_BTN, + ENTRY_DELETE_BTN, + EXCEPTION_CARD_ITEM_CONDITIONS, + EXCEPTION_CARD_ITEM_NAME, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_EXIST_PROMPT, +} from '../../../screens/exceptions'; import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +const loadEndpointRuleAndAlerts = () => { + esArchiverLoad('endpoint'); + login(); + createRule(getEndpointRule()); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForAlertsToPopulate(); +}; + describe('Rule Exceptions workflows from Alert', () => { const EXPECTED_NUMBER_OF_ALERTS = '1 alert'; - const ITEM_NAME = 'Sample Exception List Item'; + const ITEM_NAME = 'Sample Exception Item'; + const ITEM_NAME_EDIT = 'Sample Exception Item Edit'; + const ADDITIONAL_ENTRY = 'host.hostname'; const newRule = getNewRule(); beforeEach(() => { esArchiverResetKibana(); - deleteAlertsAndRules(); }); after(() => { esArchiverUnload('exceptions'); + deleteAlertsAndRules(); }); afterEach(() => { esArchiverUnload('exceptions_2'); }); - it('Creates an exception item from alert actions overflow menu and close all matching alerts', () => { + it('Should create a Rule exception item from alert actions overflow menu and close all matching alerts', () => { esArchiverLoad('exceptions'); login(); postDataView('exceptions-*'); @@ -119,14 +142,8 @@ describe('Rule Exceptions workflows from Alert', () => { cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); }); - - it('Creates an exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields ', () => { - esArchiverLoad('endpoint'); - login(); - createRule(getEndpointRule()); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForAlertsToPopulate(); + it('Should create a Rule exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields', () => { + loadEndpointRuleAndAlerts(); cy.get(LOADING_INDICATOR).should('not.exist'); addExceptionFromFirstAlert(); @@ -144,9 +161,45 @@ describe('Rule Exceptions workflows from Alert', () => { * fields are based on the alert document that should be generated * when the endpoint rule runs */ - highlightedFieldsBasedOnAlertDoc.forEach((field, index) => { - validateExceptionConditionField(field); - }); + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); + + /** + * Validate that the comments are opened by default with one comment added + * showing a text contains information about the pre-filled conditions + */ + validateExceptionCommentCountAndText( + 1, + 'Exception conditions are pre-filled with relevant data from alert with "id"' + ); + + addExceptionFlyoutItemName(ITEM_NAME); + submitNewExceptionItem(); + }); + it('Should create a Rule exception from Alerts take action button and change multiple exception items without resetting to initial auto-prefilled entries', () => { + loadEndpointRuleAndAlerts(); + + cy.get(LOADING_INDICATOR).should('not.exist'); + + // Open first Alert Summary + expandFirstAlert(); + + // The Rule exception should populated with highlighted fields + openAddRuleExceptionFromAlertActionButton(); + + const highlightedFieldsBasedOnAlertDoc = [ + 'host.name', + 'agent.id', + 'user.name', + 'process.executable', + 'file.path', + ]; + + /** + * Validate the highlighted fields are auto populated, these + * fields are based on the alert document that should be generated + * when the endpoint rule runs + */ + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); /** * Validate that the comments are opened by default with one comment added @@ -154,10 +207,74 @@ describe('Rule Exceptions workflows from Alert', () => { */ validateExceptionCommentCountAndText( 1, - 'Exception conditions are pre-filled with relevant data from' + 'Exception conditions are pre-filled with relevant data from alert with "id"' ); addExceptionFlyoutItemName(ITEM_NAME); + + cy.get(ADD_AND_BTN).click(); + + // edit conditions + addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 5); + addExceptionEntryFieldValueValue('foo', 5); + + // Change the name again + editExceptionFlyoutItemName(ITEM_NAME_EDIT); + + // validate the condition is still 'host.hostname' or got rest after the name is changed + validateExceptionConditionField(ADDITIONAL_ENTRY); + submitNewExceptionItem(); + + goToExceptionsTab(); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', 'host.hostname'); + }); + it('Should delete all prefilled exception entries when creating a Rule exception from Alerts take action button without resetting to initial auto-prefilled entries', () => { + loadEndpointRuleAndAlerts(); + + cy.get(LOADING_INDICATOR).should('not.exist'); + + // Open first Alert Summary + expandFirstAlert(); + + // The Rule exception should populated with highlighted fields + openAddRuleExceptionFromAlertActionButton(); + + const highlightedFieldsBasedOnAlertDoc = [ + 'host.name', + 'agent.id', + 'user.name', + 'process.executable', + 'file.path', + ]; + + /** + * Validate the highlighted fields are auto populated, these + * fields are based on the alert document that should be generated + * when the endpoint rule runs + */ + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); + + /** + * Delete all the highlighted fields to see if any condition + * will prefuilled again. + */ + const highlightedFieldsCount = highlightedFieldsBasedOnAlertDoc.length - 1; + highlightedFieldsBasedOnAlertDoc.forEach((_, index) => + cy + .get(ENTRY_DELETE_BTN) + .eq(highlightedFieldsCount - index) + .click() + ); + + /** + * Validate that there are no highlighted fields are auto populated + * after the deletion + */ + validateEmptyExceptionConditionField(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index c5d2aebee7c54..67d61925fa164 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -92,16 +92,18 @@ export const openAddEndpointExceptionFromFirstAlert = () => { cy.get(FIELD_INPUT).should('be.visible'); }; -export const openAddExceptionFromAlertDetails = () => { - cy.get(EXPAND_ALERT_BTN).first().click({ force: true }); - +export const openAddRuleExceptionFromAlertActionButton = () => { cy.get(TAKE_ACTION_BTN).click(); cy.get(TAKE_ACTION_MENU).should('be.visible'); - cy.get(ADD_EXCEPTION_BTN).click(); - cy.get(ADD_EXCEPTION_BTN).should('not.be.visible'); + cy.get(ADD_EXCEPTION_BTN, { timeout: 10000 }).first().click(); }; +export const openAddEndpointExceptionFromAlertActionButton = () => { + cy.get(TAKE_ACTION_BTN).click(); + cy.get(TAKE_ACTION_MENU).should('be.visible'); + cy.get(ADD_ENDPOINT_EXCEPTION_BTN, { timeout: 10000 }).first().click(); +}; export const closeFirstAlert = () => { expandFirstAlertActions(); cy.get(CLOSE_ALERT_BTN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts index 884c4521985fc..9195153d46f6f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts @@ -167,6 +167,9 @@ export const addExceptionConditions = (exception: Exception) => { export const validateExceptionConditionField = (value: string) => { cy.get(EXCEPTION_ITEM_CONTAINER).contains('span', value); }; +export const validateEmptyExceptionConditionField = () => { + cy.get(FIELD_INPUT).should('be.empty'); +}; export const submitNewExceptionItem = () => { cy.get(CONFIRM_BTN).click(); cy.get(CONFIRM_BTN).should('not.exist'); @@ -279,3 +282,8 @@ export const deleteFirstExceptionItemInListDetailPage = () => { // Delete exception cy.get(EXCEPTION_ITEM_OVERFLOW_ACTION_DELETE).click(); }; +export const validateHighlightedFieldsPopulatedAsExceptionConditions = ( + highlightedFields: string[] +) => { + return highlightedFields.every((field) => validateExceptionConditionField(field)); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts index 4b4ad1dac1b5e..215bb3fc29923 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts @@ -89,6 +89,7 @@ export const ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT = (alertId: string) => 'xpack.securitySolution.ruleExceptions.addExceptionFlyout.addRuleExceptionFromAlertComment', { values: { alertId }, - defaultMessage: 'Exception conditions are pre-filled with relevant data from {alertId}.', + defaultMessage: + 'Exception conditions are pre-filled with relevant data from alert with "id" {alertId}.', } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx index 38ece2d1ba43c..0f32e2b4d1ab8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx @@ -96,7 +96,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({ } else { return null; } - }, [shouldShowComments, exceptionItemComments]); + }, [exceptionItemComments, shouldShowComments]); const formattedComments = useMemo((): EuiCommentProps[] => { if (exceptionItemComments && exceptionItemComments.length > 0) { @@ -105,11 +105,10 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({ return []; } }, [exceptionItemComments]); - return (