Skip to content

Commit

Permalink
[Security Solution] Rule Preview Follow-up (#121249)
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee authored Jan 6, 2022
1 parent b0c3ec5 commit 6dc463d
Show file tree
Hide file tree
Showing 19 changed files with 223 additions and 152 deletions.
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;
} 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) => (
<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

0 comments on commit 6dc463d

Please sign in to comment.