Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,13 @@ export const fullResponseSchema = t.intersection([
]);
export type FullResponseSchema = t.TypeOf<typeof fullResponseSchema>;

export interface RulePreviewLogs {
errors: string[];
warnings: string[];
startedAt?: string;
}

export interface PreviewResponse {
previewId: string | undefined;
errors: string[] | undefined;
warnings: string[] | undefined;
logs: RulePreviewLogs[] | undefined;
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reference somewhere that we're using to decide on these noisy thresholds?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going based off this comment, I think it was the spec in the original feature as well but we should get product to sign off for certain sure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a quick git blame show the 1 alert per hour logic committed 14 months ago

} else if (timeframe === 'd') {
return hits / 24 > 1;
} else if (timeframe === 'w') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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[];
Expand Down Expand Up @@ -63,21 +65,33 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
anomalyThreshold,
}) => {
const { spaces } = useKibana().services;
const { loading: isMlLoading, jobs } = useSecurityJobs(false);

const [spaceId, setSpaceId] = useState('');
useEffect(() => {
if (spaces) {
spaces.getActiveSpace().then((space) => setSpaceId(space.id));
}
}, [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<Unit>(defaultTimeRange);
const {
addNoiseWarning,
createPreview,
errors,
isPreviewRequestInProgress,
previewId,
warnings,
logs,
hasNoiseWarning,
} = usePreviewRoute({
index,
isDisabled,
Expand Down Expand Up @@ -123,7 +137,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
<PreviewButton
fill
isLoading={isPreviewRequestInProgress}
isDisabled={isDisabled}
isDisabled={isDisabled || !areRelaventMlJobsRunning}
onClick={createPreview}
data-test-subj="queryPreviewButton"
>
Expand All @@ -134,20 +148,18 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
</EuiFormRow>
<EuiSpacer size="s" />
{isPreviewRequestInProgress && <LoadingHistogram />}
{!isPreviewRequestInProgress && previewId && spaceId && query && (
{!isPreviewRequestInProgress && previewId && spaceId && (
<PreviewHistogram
ruleType={ruleType}
timeFrame={timeFrame}
previewId={previewId}
addNoiseWarning={addNoiseWarning}
spaceId={spaceId}
threshold={threshold}
query={query}
index={index}
/>
)}
<CalloutGroup items={errors} isError />
<CalloutGroup items={warnings} />
<PreviewLogsComponent logs={logs} hasNoiseWarning={hasNoiseWarning} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -55,7 +54,6 @@ describe('PreviewHistogram', () => {
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
query={mockQueryBar}
index={['']}
/>
</TestProviders>
Expand Down Expand Up @@ -91,7 +89,6 @@ describe('PreviewHistogram', () => {
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
query={mockQueryBar}
index={['']}
/>
</TestProviders>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,7 +36,6 @@ interface PreviewHistogramProps {
spaceId: string;
threshold?: FieldValueThreshold;
ruleType: Type;
query: FieldValueQueryBar;
index: string[];
}

Expand All @@ -50,7 +48,6 @@ export const PreviewHistogram = ({
spaceId,
threshold,
ruleType,
query,
index,
}: PreviewHistogramProps) => {
const { setQuery, isInitializing } = useGlobalTime();
Expand All @@ -68,7 +65,6 @@ export const PreviewHistogram = ({
endDate,
spaceId,
threshold: isThresholdRule ? threshold : undefined,
query,
index,
ruleType,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PreviewLogsComponentProps> = ({
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 (
<>
<EuiSpacer size="s" />
{hasNoiseWarning ?? <CalloutGroup logs={[i18n.QUERY_PREVIEW_NOISE_WARNING]} />}
<LogAccordion logs={sortedLogs.errors} isError />
<LogAccordion logs={sortedLogs.warnings} />
</>
);
};

const LogAccordion: React.FC<LogAccordionProps> = ({ logs, isError }) => {
const firstLog = logs[0];
const restOfLogs = logs.slice(1);
return firstLog ? (
<>
<CalloutGroup logs={firstLog.logs} startedAt={firstLog.startedAt} isError={isError} />
{restOfLogs.length > 0 ? (
<EuiAccordion
id={isError ? 'previewErrorAccordion' : 'previewWarningAccordion'}
buttonContent={
isError ? i18n.QUERY_PREVIEW_SEE_ALL_ERRORS : i18n.QUERY_PREVIEW_SEE_ALL_WARNINGS
}
>
{restOfLogs.map((log, key) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do the logs have a key defined? I thought the reducer above didn't actually add any keys

<CalloutGroup
key={`accordion-log-${key}`}
logs={log.logs}
startedAt={log.startedAt}
isError={isError}
/>
))}
</EuiAccordion>
) : null}
<EuiSpacer size="m" />
</>
) : null;
};

export const CalloutGroup: React.FC<{
logs: string[];
startedAt?: string;
isError?: boolean;
}> = ({ logs, startedAt, isError }) => {
return logs.length > 0 ? (
<>
{logs.map((log, i) => (
<Fragment key={i}>
<EuiCallOut
color={isError ? 'danger' : 'warning'}
iconType="alert"
data-test-subj={isError ? 'preview-error' : 'preview-warning'}
title={startedAt != null ? `[${startedAt}]` : null}
>
<EuiText>
<p>{log}</p>
</EuiText>
</EuiCallOut>
<EuiSpacer size="s" />
</Fragment>
))}
</>
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Loading