Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM] Create ML Rules #58053

Merged
merged 66 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
a61dd5e
Remove unnecessary linter exceptions
rylnd Feb 19, 2020
5843a9a
WIP: Simple form to test creation of ML rules
rylnd Feb 19, 2020
34bf432
WIP: Adds POST to backend, and type/payload changes necessary to make…
rylnd Feb 19, 2020
56a0766
Simplify logic with Math.min
rylnd Feb 19, 2020
0b19ddc
WIP: Failed spike of making an http call
rylnd Feb 19, 2020
28ede95
WIP: Hacking together an ML client
rylnd Mar 10, 2020
bec1a90
Threading through our new ML Rule params
rylnd Mar 10, 2020
d9639af
Retrieve our anomalies during rule execution
rylnd Mar 10, 2020
190c7bb
WIP: Generate ECS-compatible ML Signals
rylnd Mar 12, 2020
919dfef
Extract setting of rule failure to helper function
rylnd Mar 12, 2020
051f638
Remove unused import
rylnd Mar 12, 2020
7ae29bf
Define a field for our Rule Type selection
rylnd Mar 12, 2020
6b25593
Hide Query Fields when ML is selected
rylnd Mar 13, 2020
d5b03a2
Add input field for anomaly threshold
rylnd Mar 13, 2020
82093ce
Display numberic values in the readonly view of a step
rylnd Mar 13, 2020
bb8abc2
Add field for selecting an ML job
rylnd Mar 13, 2020
526e8a3
Format our new ML Fields when sending them to the server
rylnd Mar 13, 2020
a5d05e0
Put back code that respects a rule's schedule
rylnd Mar 13, 2020
75eaa12
ML fields are optional in our creation step
rylnd Mar 13, 2020
68cbf21
Only send along type-specific Rule fields from form
rylnd Mar 13, 2020
7a62dba
Rename anomalies query
rylnd Mar 13, 2020
31ac415
Remove spike page with simple form
rylnd Mar 14, 2020
26822eb
Remove unneeded ES option
rylnd Mar 14, 2020
7434bcb
Fix bulk create logic
rylnd Mar 14, 2020
fb7c5f1
Rename argument
rylnd Mar 14, 2020
cec8075
Create Rule form stores all values, but filters by type for use
rylnd Mar 14, 2020
b80d5d0
Fix editing of ML fields on Rule Create
rylnd Mar 14, 2020
05d4dda
Clear form errors when switching between rule types
rylnd Mar 15, 2020
79d9cb4
Validate the selection of an ML Job
rylnd Mar 15, 2020
5424746
Fix type errors on frontend
rylnd Mar 15, 2020
a689d33
Don't set defaults for query-specific rules
rylnd Mar 15, 2020
9cb88c7
Return ML Fields in Rule responses
rylnd Mar 15, 2020
85678cb
Fix editing of ML rules by changing who controls the field values
rylnd Mar 16, 2020
316a720
Merge branch 'master' into ml_signals_spike
rylnd Mar 16, 2020
2cd97f1
Fix type errors related to new ML fields
rylnd Mar 17, 2020
23532ab
Fix failing route tests
rylnd Mar 17, 2020
41e5af1
Fix integration tests
rylnd Mar 17, 2020
0a8caf0
Fix non-ML Rule creation
rylnd Mar 17, 2020
9aca40c
More informative logging during ML signal generation
rylnd Mar 17, 2020
330cc0c
Merge branch 'master' into ml_signals_spike
rylnd Mar 17, 2020
92f9c57
Prefer keyof for string union types
rylnd Mar 17, 2020
addc6ac
Tidy up our new form components
rylnd Mar 17, 2020
0ca3858
Prefer destructuring to lodash's omit
rylnd Mar 17, 2020
190600c
Fix mock params for helper functions
rylnd Mar 17, 2020
a683ef5
Remove any type
rylnd Mar 17, 2020
94c1774
Fix mock types
rylnd Mar 17, 2020
327a877
Update outdated tests
rylnd Mar 17, 2020
e85feb5
Add some tests around our helper function
rylnd Mar 17, 2020
e80dd2c
Remove uses of any in favor of actual types
rylnd Mar 17, 2020
5e85c7e
Annotate our anomalies with @timestamp field
rylnd Mar 17, 2020
eed90df
ml_job_id -> machine_learning_job_id
rylnd Mar 18, 2020
abfcfc1
PR Feedback
rylnd Mar 18, 2020
f0a1f0f
Cleaning up our new ML types
rylnd Mar 18, 2020
e703568
Use implicit type to avoid the need for a ts-ignore
rylnd Mar 18, 2020
8ff89b0
New ML params are not nullable
rylnd Mar 18, 2020
edf354e
Query and language are conditional based on rule type
rylnd Mar 18, 2020
a949ce5
Remove defaulted parameter in API test
rylnd Mar 18, 2020
1f1b9e9
Merge branch 'master' into ml_signals_spike
rylnd Mar 18, 2020
cf8d8c3
Use explicit types over implicit ones
rylnd Mar 18, 2020
f692e01
Add integration test for creation of ML Rule
rylnd Mar 18, 2020
1aa4321
Add ML fields to route schemae
rylnd Mar 18, 2020
8ff01e9
Fix router test for creating an ML rule
rylnd Mar 18, 2020
16b25d1
Remove null check against index for query rules
rylnd Mar 18, 2020
b4fd572
Add regression test for API compatibility
rylnd Mar 18, 2020
462d410
Respect the index pattern determined at runtime when performing searc…
rylnd Mar 18, 2020
9f9f324
Fix type errors in our bulk create tests
rylnd Mar 18, 2020
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 @@ -6,26 +6,35 @@

import * as t from 'io-ts';

export const RuleTypeSchema = t.keyof({
query: null,
saved_query: null,
machine_learning: null,
});
export type RuleType = t.TypeOf<typeof RuleTypeSchema>;

export const NewRuleSchema = t.intersection([
t.type({
description: t.string,
enabled: t.boolean,
filters: t.array(t.unknown),
index: t.array(t.string),
interval: t.string,
language: t.string,
name: t.string,
query: t.string,
risk_score: t.number,
severity: t.string,
type: t.union([t.literal('query'), t.literal('saved_query')]),
type: RuleTypeSchema,
}),
t.partial({
anomaly_threshold: t.number,
created_by: t.string,
false_positives: t.array(t.string),
filters: t.array(t.unknown),
from: t.string,
id: t.string,
index: t.array(t.string),
language: t.string,
machine_learning_job_id: t.string,
max_signals: t.number,
query: t.string,
references: t.array(t.string),
rule_id: t.string,
saved_id: t.string,
Expand Down Expand Up @@ -56,32 +65,34 @@ export const RuleSchema = t.intersection([
description: t.string,
enabled: t.boolean,
false_positives: t.array(t.string),
filters: t.array(t.unknown),
from: t.string,
id: t.string,
index: t.array(t.string),
interval: t.string,
immutable: t.boolean,
language: t.string,
name: t.string,
max_signals: t.number,
query: t.string,
references: t.array(t.string),
risk_score: t.number,
rule_id: t.string,
severity: t.string,
tags: t.array(t.string),
type: t.string,
type: RuleTypeSchema,
to: t.string,
threat: t.array(t.unknown),
updated_at: t.string,
updated_by: t.string,
}),
t.partial({
anomaly_threshold: t.number,
filters: t.array(t.unknown),
index: t.array(t.string),
language: t.string,
last_failure_at: t.string,
last_failure_message: t.string,
meta: MetaRule,
machine_learning_job_id: t.string,
output_index: t.string,
query: t.string,
saved_id: t.string,
status: t.string,
status_date: t.string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({

export const mockDefineStepRule = (isNew = false): DefineStepRule => ({
isNew,
ruleType: 'query',
anomalyThreshold: 50,
machineLearningJobId: '',
index: ['filebeat-'],
queryBar: mockQueryBar,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback } from 'react';
import { EuiFlexGrid, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui';

import { FieldHook } from '../../../../../shared_imports';

interface AnomalyThresholdSliderProps {
field: FieldHook;
}
type Event = React.ChangeEvent<HTMLInputElement>;
type EventArg = Event | React.MouseEvent<HTMLButtonElement>;

export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({ field }) => {
const threshold = field.value as number;
const onThresholdChange = useCallback(
(event: EventArg) => {
const thresholdValue = Number((event as Event).target.value);
field.setValue(thresholdValue);
},
[field]
);

return (
<EuiFormRow label={field.label} fullWidth>
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiRange
value={threshold}
onChange={onThresholdChange}
fullWidth
showInput
showRange
showTicks
tickInterval={25}
/>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFormRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true));
const mockFilterManager = new FilterManager(setupMock.uiSettings);

const mockQueryBar = {
query: {
query: 'test query',
language: 'kuery',
},
query: 'test query',
filters: [
{
$state: {
Expand Down Expand Up @@ -93,10 +90,7 @@ describe('helpers', () => {
describe('buildQueryBarDescription', () => {
test('returns empty array if no filters, query or savedId exist', () => {
const emptyMockQueryBar = {
query: {
query: '',
language: 'kuery',
},
query: '',
filters: [],
saved_id: '',
};
Expand All @@ -113,10 +107,7 @@ describe('helpers', () => {
test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => {
const mockQueryBarWithFilters = {
...mockQueryBar,
query: {
query: '',
language: 'kuery',
},
query: '',
saved_id: '',
};
const result: ListItems[] = buildQueryBarDescription({
Expand All @@ -135,10 +126,7 @@ describe('helpers', () => {
test('returns expected array of ListItems when filters AND indexPatterns exist', () => {
const mockQueryBarWithFilters = {
...mockQueryBar,
query: {
query: '',
language: 'kuery',
},
query: '',
saved_id: '',
};
const result: ListItems[] = buildQueryBarDescription({
Expand Down Expand Up @@ -171,16 +159,13 @@ describe('helpers', () => {
savedId: mockQueryBarWithQuery.saved_id,
});
expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>);
expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query.query} </>);
expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} </>);
});

test('returns expected array of ListItems when "savedId" exists', () => {
const mockQueryBarWithSavedId = {
...mockQueryBar,
query: {
query: '',
language: 'kuery',
},
query: '',
filters: [],
};
const result: ListItems[] = buildQueryBarDescription({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ export const buildQueryBarDescription = ({
},
];
}
if (!isEmpty(query.query)) {
if (!isEmpty(query)) {
items = [
...items,
{
title: <>{i18n.QUERY_LABEL} </>,
description: <>{query.query} </>,
description: <>{query} </>,
},
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty, chunk, get, pick } from 'lodash/fp';
import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp';
import React, { memo, useState } from 'react';
import styled from 'styled-components';

Expand All @@ -14,7 +14,6 @@ import {
Filter,
esFilters,
FilterManager,
Query,
} from '../../../../../../../../../../src/plugins/data/public';
import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations';
import { useKibana } from '../../../../../lib/kibana';
Expand Down Expand Up @@ -133,14 +132,14 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => {
export const getDescriptionItem = (
field: string,
rylnd marked this conversation as resolved.
Show resolved Hide resolved
label: string,
value: unknown,
data: unknown,
filterManager: FilterManager,
indexPatterns?: IIndexPattern
): ListItems[] => {
if (field === 'queryBar') {
const filters = addFilterStateIfNotThere(get('queryBar.filters', value) ?? []);
const query = get('queryBar.query', value) as Query;
const savedId = get('queryBar.saved_id', value);
const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []);
const query = get('queryBar.query.query', data);
const savedId = get('queryBar.saved_id', data);
rylnd marked this conversation as resolved.
Show resolved Hide resolved
return buildQueryBarDescription({
field,
filters,
Expand All @@ -150,43 +149,37 @@ export const getDescriptionItem = (
indexPatterns,
});
} else if (field === 'threat') {
const threat: IMitreEnterpriseAttack[] = get(field, value).filter(
const threat: IMitreEnterpriseAttack[] = get(field, data).filter(
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be better as just an unsafe cast rather than lodash get for a refactor like so:

    // TODO: use io-ts or a TypeGuard "is" to correctly narrow this later.
    const threats = data as { threat: IMitreEnterpriseAttack[] };
    const threat = threats.threat.filter(singleThreat => singleThreat.tactic.name !== 'none');

It would be cleaner and if/when you have the io-ts type you can run a decode on it and handle a left and a right with the decode for more run time safety to do maybe a throw error if a maintainer messes it up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deferred to #60567

(singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none'
);
return buildThreatDescription({ label, threat });
} else if (field === 'references') {
const urls: string[] = get(field, value);
const urls: string[] = get(field, data);
return buildUrlsDescription(label, urls);
} else if (field === 'falsePositives') {
const values: string[] = get(field, value);
const values: string[] = get(field, data);
return buildUnorderedListArrayDescription(label, field, values);
} else if (Array.isArray(get(field, value))) {
const values: string[] = get(field, value);
} else if (Array.isArray(get(field, data))) {
const values: string[] = get(field, data);
return buildStringArrayDescription(label, field, values);
} else if (field === 'severity') {
Copy link
Contributor

@FrankHassanabad FrankHassanabad Mar 17, 2020

Choose a reason for hiding this comment

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

Woa...This is very abnormal code flow you are refactoring here.

  } else if (field === 'falsePositives') {
    const values: string[] = get(field, data);
    return buildUnorderedListArrayDescription(label, field, values);
  } else if (Array.isArray(get(field, data))) {
    const values: string[] = get(field, data);
    return buildStringArrayDescription(label, field, values);
  } else if (field === 'severity') {

It checks if it is an array rather than a field value part way down but not the the field switch?

} else if (Array.isArray(get(field, data))) {

This is going to make future changes brittle and error prone depending on where we add the field value. I would ask you do us all a favor and add what the field value is through a call of:

} else if (field === 'whatever_string_value_this_is') {

That might not even be getting touched? It is really hard to understand code the more I look at it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deferred to #60567

const val: string = get(field, value);
const val: string = get(field, data);
return buildSeverityDescription(label, val);
} else if (field === 'riskScore') {
return [
{
title: label,
description: get(field, value),
},
];
} else if (field === 'timeline') {
const timeline = get(field, value) as FieldValueTimeline;
const timeline = get(field, data) as FieldValueTimeline;
return [
{
title: label,
description: timeline.title ?? DEFAULT_TIMELINE_TITLE,
},
];
} else if (field === 'note') {
const val: string = get(field, value);
const val: string = get(field, data);
return buildNoteDescription(label, val);
}
const description: string = get(field, value);
if (!isEmpty(description)) {

const description: string = get(field, data);
if (isNumber(description) || !isEmpty(description)) {
return [
{
title: label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
IIndexPattern,
Filter,
FilterManager,
Query,
} from '../../../../../../../../../../src/plugins/data/public';
import { IMitreEnterpriseAttack } from '../../types';

Expand All @@ -22,7 +21,7 @@ export interface BuildQueryBarDescription {
field: string;
filters: Filter[];
filterManager: FilterManager;
query: Query;
query: string;
savedId: string;
indexPatterns?: IIndexPattern;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSuperSelect, EuiText } from '@elastic/eui';

import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports';
import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs';

const JobDisplay = ({ title, description }: { title: string; description: string }) => (
<>
<strong>{title}</strong>
<EuiText size="xs" color="subdued">
<p>{description}</p>
</EuiText>
</>
);

interface MlJobSelectProps {
field: FieldHook;
}

export const MlJobSelect: React.FC<MlJobSelectProps> = ({ field }) => {
const jobId = field.value as string;
rylnd marked this conversation as resolved.
Show resolved Hide resolved
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const [isLoading, siemJobs] = useSiemJobs(false);
const handleJobChange = useCallback(
(machineLearningJobId: string) => {
field.setValue(machineLearningJobId);
},
[field]
);

const options = siemJobs.map(job => ({
value: job.id,
inputDisplay: job.id,
dropdownDisplay: <JobDisplay title={job.id} description={job.description} />,
}));

return (
<EuiFormRow fullWidth label={field.label} isInvalid={isInvalid} error={errorMessage}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiSuperSelect
hasDividers
isLoading={isLoading}
onChange={handleJobChange}
options={options}
valueOfSelected={jobId}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import * as i18n from './translations';
export interface FieldValueQueryBar {
filters: Filter[];
query: Query;
saved_id: string | null;
saved_id?: string;
}
interface QueryBarDefineRuleProps {
browserFields: BrowserFields;
Expand Down
Loading