diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 97079253606f1..64f6c9c32d4cb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -439,8 +439,13 @@ export const fullResponseSchema = t.intersection([ ]); export type FullResponseSchema = t.TypeOf; +export interface RulePreviewLogs { + errors: string[]; + warnings: string[]; + startedAt?: string; +} + export interface PreviewResponse { previewId: string | undefined; - errors: string[] | undefined; - warnings: string[] | undefined; + logs: RulePreviewLogs[] | undefined; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/callout_group.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/callout_group.test.tsx deleted file mode 100644 index 7bdb5eaf806bd..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/callout_group.test.tsx +++ /dev/null @@ -1,24 +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 { render, screen } from '@testing-library/react'; -import React from 'react'; -import { CalloutGroup } from './callout_group'; - -describe('Callout Group', () => { - test('renders errors', () => { - render(); - expect(screen.getAllByTestId('preview-error')[0]).toHaveTextContent('error1'); - expect(screen.getAllByTestId('preview-error')[1]).toHaveTextContent('error2'); - }); - - test('renders warnings', () => { - render(); - expect(screen.getAllByTestId('preview-warning')[0]).toHaveTextContent('warning1'); - expect(screen.getAllByTestId('preview-warning')[1]).toHaveTextContent('warning2'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/callout_group.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/callout_group.tsx deleted file mode 100644 index a060a785b1c1b..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/callout_group.tsx +++ /dev/null @@ -1,32 +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 React, { Fragment } from 'react'; -import { EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui'; - -export const CalloutGroup: React.FC<{ items: string[]; isError?: boolean }> = ({ - items, - isError, -}) => - items.length > 0 ? ( - <> - {items.map((item, i) => ( - - - - -

{item}

-
-
-
- ))} - - ) : null; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts index 4e04b99cd0bb7..a6202cc1e6be9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts @@ -21,12 +21,6 @@ describe('query_preview/helpers', () => { }); test('returns false if timeframe selection is "Last hour" and average hits per hour is less than one execution duration', () => { - const isItNoisy = isNoisy(10, 'h'); - - expect(isItNoisy).toBeFalsy(); - }); - - test('returns false if timeframe selection is "Last hour" and hits is 0', () => { const isItNoisy = isNoisy(0, 'h'); expect(isItNoisy).toBeFalsy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts index 86e75a26dc11f..85ec0d20f97ad 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts @@ -24,7 +24,7 @@ import { ESQuery } from '../../../../../common/typed_json'; */ export const isNoisy = (hits: number, timeframe: Unit): boolean => { if (timeframe === 'h') { - return hits > 20; + return hits > 1; } else if (timeframe === 'd') { return hits / 24 > 1; } else if (timeframe === 'w') { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx index 529dce99aabb7..0bf229000088a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx @@ -71,13 +71,13 @@ describe('PreviewQuery', () => { ]); (usePreviewRoute as jest.Mock).mockReturnValue({ + hasNoiseWarning: false, addNoiseWarning: jest.fn(), createPreview: jest.fn(), clearPreview: jest.fn(), - errors: [], + logs: [], isPreviewRequestInProgress: false, previewId: undefined, - warnings: [], }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index a5ed9b9e5bed4..05dd0ff12c559 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Unit } from '@elastic/datemath'; import { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import styled from 'styled-components'; @@ -17,15 +17,17 @@ import { EuiButton, EuiSpacer, } from '@elastic/eui'; +import { useSecurityJobs } from '../../../../../public/common/components/ml_popover/hooks/use_security_jobs'; import { FieldValueQueryBar } from '../query_bar'; import * as i18n from './translations'; import { usePreviewRoute } from './use_preview_route'; import { PreviewHistogram } from './preview_histogram'; import { getTimeframeOptions } from './helpers'; -import { CalloutGroup } from './callout_group'; +import { PreviewLogsComponent } from './preview_logs'; import { useKibana } from '../../../../common/lib/kibana'; import { LoadingHistogram } from './loading_histogram'; import { FieldValueThreshold } from '../threshold_input'; +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; export interface RulePreviewProps { index: string[]; @@ -63,6 +65,8 @@ const RulePreviewComponent: React.FC = ({ anomalyThreshold, }) => { const { spaces } = useKibana().services; + const { loading: isMlLoading, jobs } = useSecurityJobs(false); + const [spaceId, setSpaceId] = useState(''); useEffect(() => { if (spaces) { @@ -70,14 +74,24 @@ const RulePreviewComponent: React.FC = ({ } }, [spaces]); + const areRelaventMlJobsRunning = useMemo(() => { + if (ruleType !== 'machine_learning') { + return true; // Don't do the expensive logic if we don't need it + } + if (isMlLoading) { + const selectedJobs = jobs.filter(({ id }) => machineLearningJobId.includes(id)); + return selectedJobs.every((job) => isJobStarted(job.jobState, job.datafeedState)); + } + }, [jobs, machineLearningJobId, ruleType, isMlLoading]); + const [timeFrame, setTimeFrame] = useState(defaultTimeRange); const { addNoiseWarning, createPreview, - errors, isPreviewRequestInProgress, previewId, - warnings, + logs, + hasNoiseWarning, } = usePreviewRoute({ index, isDisabled, @@ -123,7 +137,7 @@ const RulePreviewComponent: React.FC = ({ @@ -134,7 +148,7 @@ const RulePreviewComponent: React.FC = ({ {isPreviewRequestInProgress && } - {!isPreviewRequestInProgress && previewId && spaceId && query && ( + {!isPreviewRequestInProgress && previewId && spaceId && ( = ({ addNoiseWarning={addNoiseWarning} spaceId={spaceId} threshold={threshold} - query={query} index={index} /> )} - - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx index bdc4c84520cdd..5326d9235ee30 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx @@ -14,7 +14,6 @@ import { TestProviders } from '../../../../common/mock'; import { usePreviewHistogram } from './use_preview_histogram'; import { PreviewHistogram } from './preview_histogram'; -import { mockQueryBar } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; jest.mock('../../../../common/containers/use_global_time'); jest.mock('./use_preview_histogram'); @@ -55,7 +54,6 @@ describe('PreviewHistogram', () => { previewId={'test-preview-id'} spaceId={'default'} ruleType={'query'} - query={mockQueryBar} index={['']} /> @@ -91,7 +89,6 @@ describe('PreviewHistogram', () => { previewId={'test-preview-id'} spaceId={'default'} ruleType={'query'} - query={mockQueryBar} index={['']} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 5452501ec65c7..c027c7fc17bc7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -21,7 +21,6 @@ import { BarChart } from '../../../../common/components/charts/barchart'; import { usePreviewHistogram } from './use_preview_histogram'; import { formatDate } from '../../../../common/components/super_date_picker'; import { FieldValueThreshold } from '../threshold_input'; -import { FieldValueQueryBar } from '../query_bar'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -37,7 +36,6 @@ interface PreviewHistogramProps { spaceId: string; threshold?: FieldValueThreshold; ruleType: Type; - query: FieldValueQueryBar; index: string[]; } @@ -50,7 +48,6 @@ export const PreviewHistogram = ({ spaceId, threshold, ruleType, - query, index, }: PreviewHistogramProps) => { const { setQuery, isInitializing } = useGlobalTime(); @@ -68,7 +65,6 @@ export const PreviewHistogram = ({ endDate, spaceId, threshold: isThresholdRule ? threshold : undefined, - query, index, ruleType, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx new file mode 100644 index 0000000000000..726c2b5df964c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx @@ -0,0 +1,111 @@ +/* + * 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, { Fragment, useMemo } from 'react'; +import { EuiCallOut, EuiText, EuiSpacer, EuiAccordion } from '@elastic/eui'; +import { RulePreviewLogs } from '../../../../../common/detection_engine/schemas/request'; +import * as i18n from './translations'; + +interface PreviewLogsComponentProps { + logs: RulePreviewLogs[]; + hasNoiseWarning: boolean; +} + +interface SortedLogs { + startedAt?: string; + logs: string[]; +} + +interface LogAccordionProps { + logs: SortedLogs[]; + isError?: boolean; +} + +const addLogs = (startedAt: string | undefined, logs: string[], allLogs: SortedLogs[]) => + logs.length ? [{ startedAt, logs }, ...allLogs] : allLogs; + +export const PreviewLogsComponent: React.FC = ({ + logs, + hasNoiseWarning, +}) => { + const sortedLogs = useMemo( + () => + logs.reduce<{ + errors: SortedLogs[]; + warnings: SortedLogs[]; + }>( + ({ errors, warnings }, curr) => ({ + errors: addLogs(curr.startedAt, curr.errors, errors), + warnings: addLogs(curr.startedAt, curr.warnings, warnings), + }), + { errors: [], warnings: [] } + ), + [logs] + ); + return ( + <> + + {hasNoiseWarning ?? } + + + + ); +}; + +const LogAccordion: React.FC = ({ logs, isError }) => { + const firstLog = logs[0]; + const restOfLogs = logs.slice(1); + return firstLog ? ( + <> + + {restOfLogs.length > 0 ? ( + + {restOfLogs.map((log, key) => ( + + ))} + + ) : null} + + + ) : null; +}; + +export const CalloutGroup: React.FC<{ + logs: string[]; + startedAt?: string; + isError?: boolean; +}> = ({ logs, startedAt, isError }) => { + return logs.length > 0 ? ( + <> + {logs.map((log, i) => ( + + + +

{log}

+
+
+ +
+ ))} + + ) : null; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts index de43b2e68e6db..23fcf62f32a6c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts @@ -153,3 +153,17 @@ export const QUERY_PREVIEW_EQL_SEQUENCE_DESCRIPTION = i18n.translate( 'No histogram is available at this time for EQL sequence queries. You can use the inspect in the top right corner to view query details.', } ); + +export const QUERY_PREVIEW_SEE_ALL_ERRORS = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSeeAllErrors', + { + defaultMessage: 'See all errors', + } +); + +export const QUERY_PREVIEW_SEE_ALL_WARNINGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSeeAllWarnings', + { + defaultMessage: 'See all warnings', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx index 74b4abd265c15..0d63092ee4eef 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; +import { useMatrixHistogramCombined } from '../../../../common/containers/matrix_histogram'; import { MatrixHistogramType } from '../../../../../common/search_strategy'; import { convertToBuildEsQuery } from '../../../../common/lib/keury'; import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/common'; @@ -14,7 +14,6 @@ import { useKibana } from '../../../../common/lib/kibana'; import { QUERY_PREVIEW_ERROR } from './translations'; import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; import { FieldValueThreshold } from '../threshold_input'; -import { FieldValueQueryBar } from '../query_bar'; interface PreviewHistogramParams { previewId: string | undefined; @@ -22,7 +21,6 @@ interface PreviewHistogramParams { startDate: string; spaceId: string; threshold?: FieldValueThreshold; - query: FieldValueQueryBar; index: string[]; ruleType: Type; } @@ -33,15 +31,10 @@ export const usePreviewHistogram = ({ endDate, spaceId, threshold, - query, index, ruleType, }: PreviewHistogramParams) => { const { uiSettings } = useKibana().services; - const { - query: { query: queryString, language }, - filters, - } = query; const [filterQuery, error] = convertToBuildEsQuery({ config: getEsQueryConfig(uiSettings), @@ -49,11 +42,8 @@ export const usePreviewHistogram = ({ fields: [], title: index.join(), }, - queries: [ - { query: `signal.rule.id:${previewId}`, language: 'kuery' }, - { query: queryString, language }, - ], - filters, + queries: [{ query: `kibana.alert.rule.uuid:${previewId}`, language: 'kuery' }], + filters: [], }); const stackByField = useMemo(() => { @@ -76,5 +66,5 @@ export const usePreviewHistogram = ({ }; }, [startDate, endDate, filterQuery, spaceId, error, threshold, stackByField]); - return useMatrixHistogram(matrixHistogramRequest); + return useMatrixHistogramCombined(matrixHistogramRequest); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx index 3bf8f284f0247..2e51090f37a94 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx @@ -9,10 +9,10 @@ import { useEffect, useState, useCallback } from 'react'; import { Unit } from '@elastic/datemath'; import { Type, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { FieldValueQueryBar } from '../query_bar'; -import { QUERY_PREVIEW_NOISE_WARNING } from './translations'; import { usePreviewRule } from '../../../containers/detection_engine/rules/use_preview_rule'; import { formatPreviewRule } from '../../../pages/detection_engine/rules/create/helpers'; import { FieldValueThreshold } from '../threshold_input'; +import { RulePreviewLogs } from '../../../../../common/detection_engine/schemas/request'; interface PreviewRouteParams { isDisabled: boolean; @@ -44,21 +44,22 @@ export const usePreviewRoute = ({ const [isRequestTriggered, setIsRequestTriggered] = useState(false); const { isLoading, response, rule, setRule } = usePreviewRule(timeFrame); - const [warnings, setWarnings] = useState(response.warnings ?? []); + const [logs, setLogs] = useState(response.logs ?? []); + const [hasNoiseWarning, setHasNoiseWarning] = useState(false); useEffect(() => { - setWarnings(response.warnings ?? []); + setLogs(response.logs ?? []); }, [response]); - const addNoiseWarning = useCallback( - () => setWarnings([QUERY_PREVIEW_NOISE_WARNING, ...warnings]), - [warnings] - ); + const addNoiseWarning = useCallback(() => { + setHasNoiseWarning(true); + }, [setHasNoiseWarning]); const clearPreview = useCallback(() => { setRule(null); - setWarnings([]); + setLogs([]); setIsRequestTriggered(false); + setHasNoiseWarning(false); }, [setRule]); useEffect(() => { @@ -112,12 +113,12 @@ export const usePreviewRoute = ({ ]); return { + hasNoiseWarning, addNoiseWarning, createPreview: () => setIsRequestTriggered(true), clearPreview, - errors: response.errors ?? [], isPreviewRequestInProgress: isLoading, previewId: response.previewId ?? '', - warnings, + logs, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts index c4ccea91d99c8..4b0ad0507263d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts @@ -21,8 +21,7 @@ import { transformOutput } from './transforms'; const emptyPreviewRule: PreviewResponse = { previewId: undefined, - errors: [], - warnings: [], + logs: [], }; export const usePreviewRule = (timeframe: Unit = 'h') => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index db41df644b3dc..6967324d3ce45 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -14,7 +14,7 @@ import { EuiFlexGroup, } from '@elastic/eui'; import React, { useCallback, useRef, useState, useMemo } from 'react'; -import styled, { StyledComponent } from 'styled-components'; +import styled from 'styled-components'; import { useCreateRule } from '../../../../containers/detection_engine/rules'; import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; @@ -74,19 +74,6 @@ const MyEuiPanel = styled(EuiPanel)<{ MyEuiPanel.displayName = 'MyEuiPanel'; -const StepDefineRuleAccordion: StyledComponent< - typeof EuiAccordion, - any, // eslint-disable-line - { ref: React.MutableRefObject }, - never -> = styled(EuiAccordion)` - .euiAccordion__childWrapper { - overflow: visible; - } -`; - -StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion'; - const CreateRulePageComponent: React.FC = () => { const [ { @@ -311,7 +298,7 @@ const CreateRulePageComponent: React.FC = () => { title={i18n.PAGE_TITLE} /> - { onSubmit={submitStepDefineRule} descriptionColumns="singleSplit" /> - + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 033a5ecd84b9b..b93125d614f12 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -20,7 +20,10 @@ import { SetupPlugins } from '../../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { DETECTION_ENGINE_RULES_PREVIEW } from '../../../../../common/constants'; -import { previewRulesSchema } from '../../../../../common/detection_engine/schemas/request'; +import { + previewRulesSchema, + RulePreviewLogs, +} from '../../../../../common/detection_engine/schemas/request'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; import { @@ -86,11 +89,15 @@ export const previewRulesRoute = async ( RULE_PREVIEW_INVOCATION_COUNT.MONTH, ].includes(invocationCount) ) { - return response.ok({ body: { errors: ['Invalid invocation count'] } }); + return response.ok({ + body: { logs: [{ errors: ['Invalid invocation count'], warnings: [] }] }, + }); } if (request.body.type === 'threat_match') { - return response.ok({ body: { errors: ['Preview for rule type not supported'] } }); + return response.ok({ + body: { logs: [{ errors: ['Preview for rule type not supported'], warnings: [] }] }, + }); } const internalRule = convertCreateAPIToInternalSchema(request.body, siemClient, false); @@ -110,6 +117,7 @@ export const previewRulesRoute = async ( const username = security?.authc.getCurrentUser(request)?.username; const { previewRuleExecutionLogClient, warningsAndErrorsStore } = createWarningsAndErrors(); const runState: Record = {}; + const logs: RulePreviewLogs[] = []; const previewRuleTypeWrapper = createSecurityRuleTypeWrapper({ ...securityRuleTypeOptions, @@ -184,6 +192,24 @@ export const previewRulesRoute = async ( tags: [], updatedBy: rule.updatedBy, })) as TState; + + // Save and reset error and warning logs + const currentLogs = { + errors: warningsAndErrorsStore + .filter((item) => item.newStatus === RuleExecutionStatus.failed) + .map((item) => item.message ?? 'Unkown Error'), + warnings: warningsAndErrorsStore + .filter( + (item) => + item.newStatus === RuleExecutionStatus['partial failure'] || + item.newStatus === RuleExecutionStatus.warning + ) + .map((item) => item.message ?? 'Unknown Warning'), + startedAt: startedAt.toDate().toISOString(), + }; + logs.push(currentLogs); + previewRuleExecutionLogClient.clearWarningsAndErrorsStore(); + previousStartedAt = startedAt.toDate(); startedAt.add(parseInterval(internalRule.schedule.interval)); invocationCount--; @@ -252,18 +278,6 @@ export const previewRulesRoute = async ( break; } - const errors = warningsAndErrorsStore - .filter((item) => item.newStatus === RuleExecutionStatus.failed) - .map((item) => item.message); - - const warnings = warningsAndErrorsStore - .filter( - (item) => - item.newStatus === RuleExecutionStatus['partial failure'] || - item.newStatus === RuleExecutionStatus.warning - ) - .map((item) => item.message); - // Refreshes alias to ensure index is able to be read before returning await context.core.elasticsearch.client.asInternalUser.indices.refresh( { @@ -275,8 +289,7 @@ export const previewRulesRoute = async ( return response.ok({ body: { previewId, - errors, - warnings, + logs, }, }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts index 5ab32f27c349e..2a000b7a31968 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts @@ -19,10 +19,14 @@ import { } from '../../rule_execution_log'; import { IRuleStatusSOAttributes } from '../../rules/types'; +interface PreviewRuleExecutionLogClient extends IRuleExecutionLogClient { + clearWarningsAndErrorsStore: () => void; +} + export const createWarningsAndErrors = () => { const warningsAndErrorsStore: LogStatusChangeArgs[] = []; - const previewRuleExecutionLogClient: IRuleExecutionLogClient = { + const previewRuleExecutionLogClient: PreviewRuleExecutionLogClient = { find( args: FindExecutionLogArgs ): Promise>> { @@ -64,6 +68,10 @@ export const createWarningsAndErrors = () => { warningsAndErrorsStore.push(args); return Promise.resolve(); }, + + clearWarningsAndErrorsStore() { + warningsAndErrorsStore.length = 0; + }, }; return { previewRuleExecutionLogClient, warningsAndErrorsStore }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts index e58a8aede7985..4832da8d0cfaa 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts @@ -41,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .send(getSimplePreviewRule()) .expect(200); - expect(body).to.eql(getSimpleRulePreviewOutput(body.previewId)); + expect(body).to.eql(getSimpleRulePreviewOutput(body.previewId, body.logs)); }); it("shouldn't cause a 409 conflict if we attempt to create the same rule_id twice", async () => { @@ -64,8 +64,10 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .send(getSimplePreviewRule('', 3)) .expect(200); - const { errors } = getSimpleRulePreviewOutput(undefined, ['Invalid invocation count']); - expect(body).to.eql({ errors }); + const { logs } = getSimpleRulePreviewOutput(undefined, [ + { errors: ['Invalid invocation count'], warnings: [] }, + ]); + expect(body).to.eql({ logs }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index e13174d66fb1d..4797f614c930e 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -35,6 +35,7 @@ import { ThresholdCreateSchema, PreviewRulesSchema, ThreatMatchCreateSchema, + RulePreviewLogs, } from '../../plugins/security_solution/common/detection_engine/schemas/request'; import { signalsMigrationType } from '../../plugins/security_solution/server/lib/detection_engine/migrations/saved_objects'; import { @@ -404,16 +405,13 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial * execution process and a `previewId` generated server side for later preview querying * * @param previewId Rule id generated by the server itself - * @param errors Errors returned by executor and route file, defaults to empty array - * @param warnings Warnings returned by executor and route file, defaults to empty array + * @param logs Errors and warnings returned by executor and route file, defaults to empty array */ export const getSimpleRulePreviewOutput = ( previewId = undefined, - errors: string[] = [], - warnings: string[] = [] + logs: RulePreviewLogs[] = [] ) => ({ - errors, - warnings, + logs, previewId, });