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

[Security Solution][Detection Engine] Adds threat matching to the rule creator #78955

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@ -60,7 +60,7 @@ export const getAddPrepackagedThreatMatchRulesSchemaMock = (): AddPrepackagedRul
rule_id: 'rule-1',
version: 1,
threat_query: '*:*',
threat_index: 'list-index',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
Expand Down Expand Up @@ -118,7 +118,7 @@ export const getAddPrepackagedThreatMatchRulesSchemaDecodedMock = (): AddPrepack
exceptions_list: [],
rule_id: 'rule-1',
threat_query: '*:*',
threat_index: 'list-index',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';

import {
Expand Down Expand Up @@ -128,6 +129,7 @@ export const addPrepackagedRulesSchema = t.intersection([
threat_mapping, // defaults to "undefined" if not set during decode
threat_query, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
})
),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRu
language: 'kuery',
rule_id: ruleId,
threat_query: '*:*',
threat_index: 'list-index',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
Expand Down Expand Up @@ -124,7 +124,7 @@ export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaD
exceptions_list: [],
rule_id: 'rule-1',
threat_query: '*:*',
threat_index: 'list-index',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';

import {
Expand Down Expand Up @@ -124,6 +125,7 @@ export const createRulesSchema = t.intersection([
threat_query, // defaults to "undefined" if not set during decode
threat_filters, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
})
),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): ImportRu
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
threat_index: 'index-123',
threat_index: ['index-123'],
threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
threat_query: '*:*',
threat_filters: [
Expand Down Expand Up @@ -136,7 +136,7 @@ export const getImportThreatMatchRulesSchemaDecodedMock = (): ImportRulesSchemaD
rule_id: 'rule-1',
immutable: false,
threat_query: '*:*',
threat_index: 'index-123',
threat_index: ['index-123'],
threat_mapping: [
{
entries: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';

import {
Expand Down Expand Up @@ -147,6 +148,7 @@ export const importRulesSchema = t.intersection([
threat_mapping, // defaults to "undefined" if not set during decode
threat_query, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
})
),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ import {
severity_mapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';
import { listArrayOrUndefined } from '../types/lists';

/**
Expand Down Expand Up @@ -97,6 +104,11 @@ export const patchRulesSchema = t.exact(
note,
version,
exceptions_list: listArrayOrUndefined,
threat_index,
threat_query,
threat_filters,
threat_mapping,
threat_language,
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ import {
SeverityMapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';

import {
DefaultStringArray,
Expand Down Expand Up @@ -122,6 +129,11 @@ export const updateRulesSchema = t.intersection([
note, // defaults to "undefined" if not set during decode
version, // defaults to "undefined" if not set during decode
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
threat_mapping, // defaults to "undefined" if not set during decode
threat_query, // defaults to "undefined" if not set during decode
threat_filters, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
})
),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R
return {
...getRulesSchemaMock(anchorDate),
type: 'threat_match',
threat_index: 'index-123',
threat_index: ['index-123'],
threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
threat_query: '*:*',
threat_filters: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ describe('rules_schema', () => {
const message = pipe(checked, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'invalid keys "threat_index,threat_mapping,[{"entries":[{"field":"host.name","type":"mapping","value":"host.name"}]}],threat_query,threat_filters,[{"bool":{"must":[{"query_string":{"query":"host.name: linux","analyze_wildcard":true,"time_zone":"Zulu"}}],"filter":[],"should":[],"must_not":[]}}]"',
'invalid keys "threat_index,["index-123"],threat_mapping,[{"entries":[{"field":"host.name","type":"mapping","value":"host.name"}]}],threat_query,threat_filters,[{"bool":{"must":[{"query_string":{"query":"host.name: linux","analyze_wildcard":true,"time_zone":"Zulu"}}],"filter":[],"should":[],"must_not":[]}}]"',
]);
expect(message.schema).toEqual({});
});
Expand Down Expand Up @@ -764,7 +764,7 @@ describe('rules_schema', () => {

test('should return 5 fields for a rule of type "threat_match"', () => {
const fields = addThreatMatchFields({ type: 'threat_match' });
expect(fields.length).toEqual(5);
expect(fields.length).toEqual(6);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';

import { DefaultListArray } from '../types/lists_default_array';
Expand Down Expand Up @@ -144,6 +145,7 @@ export const dependentRulesSchema = t.partial({
threat_index,
threat_query,
threat_mapping,
threat_language,
});

/**
Expand Down Expand Up @@ -277,6 +279,7 @@ export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly):
t.exact(t.type({ threat_query: dependentRulesSchema.props.threat_query })),
t.exact(t.type({ threat_index: dependentRulesSchema.props.threat_index })),
t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })),
t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })),
t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })),
t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })),
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export * from './positive_integer';
export * from './positive_integer_greater_than_zero';
export * from './references_default_array';
export * from './risk_score';
export * from './threat_mapping';
export * from './uuid';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/* eslint-disable @typescript-eslint/naming-convention */

import * as t from 'io-ts';
import { language } from '../common/schemas';
import { NonEmptyString } from './non_empty_string';

export const threat_query = t.string;
Expand All @@ -19,29 +20,38 @@ export type ThreatFilters = t.TypeOf<typeof threat_filters>;
export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]);
export type ThreatFiltersOrUndefined = t.TypeOf<typeof threatFiltersOrUndefined>;

export const threatMappingEntries = t.array(
t.exact(
t.type({
field: NonEmptyString,
type: t.keyof({ mapping: null }),
value: NonEmptyString,
})
)
export const threatMapEntry = t.exact(
t.type({
field: NonEmptyString,
type: t.keyof({ mapping: null }),
value: NonEmptyString,
})
);

export type ThreatMapEntry = t.TypeOf<typeof threatMapEntry>;

export const threatMappingEntries = t.array(threatMapEntry);
export type ThreatMappingEntries = t.TypeOf<typeof threatMappingEntries>;

export const threat_mapping = t.array(
t.exact(
t.type({
entries: threatMappingEntries,
})
)
export const threatMap = t.exact(
t.type({
entries: threatMappingEntries,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be made to be a non empty array? I just thought of that when I saw the containsEmptyItem helper. I remember having to do lots of checks like that with exceptions and finally just changed it so that it's required to not be empty since at the very least (in the UI) there's one item with empty values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me add that to a potential follow up. I totally agree, just don't know about the time I have left with the other tasks but I will add it to my list as I will have to make one of those icky specific types and all the tests for it.

})
);
export type ThreatMap = t.TypeOf<typeof threatMap>;

export const threat_mapping = t.array(threatMap);
export type ThreatMapping = t.TypeOf<typeof threat_mapping>;

export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]);
export type ThreatMappingOrUndefined = t.TypeOf<typeof threatMappingOrUndefined>;

export const threat_index = t.string;
export const threat_index = t.array(t.string);
export type ThreatIndex = t.TypeOf<typeof threat_index>;
export const threatIndexOrUndefined = t.union([threat_index, t.undefined]);
export type ThreatIndexOrUndefined = t.TypeOf<typeof threatIndexOrUndefined>;

export const threat_language = t.union([language, t.undefined]);
export type ThreatLanguage = t.TypeOf<typeof threat_language>;
export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]);
export type ThreatLanguageOrUndefined = t.TypeOf<typeof threatLanguageOrUndefined>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';

import { AndBadgeComponent } from './and_badge';

describe('AndBadgeComponent', () => {
test('it renders entryItemIndexItemEntryFirstRowAndBadge for very first item', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AndBadgeComponent entriesLength={2} entryItemIndex={0} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeTruthy();
});

test('it renders entryItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AndBadgeComponent entriesLength={1} entryItemIndex={0} />
</ThemeProvider>
);

expect(
wrapper.find('[data-test-subj="entryItemEntryInvisibleAndBadge"]').exists()
).toBeTruthy();
});

test('it renders regular "and" badge if item is not the first one and includes more than one entry', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AndBadgeComponent entriesLength={2} entryItemIndex={1} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="entryItemEntryAndBadge"]').exists()).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
yctercero marked this conversation as resolved.
Show resolved Hide resolved
* 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 from 'react';
import { EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';

import { AndOrBadge } from '../and_or_badge';

const MyInvisibleAndBadge = styled(EuiFlexItem)`
visibility: hidden;
`;

const MyFirstRowContainer = styled(EuiFlexItem)`
padding-top: 20px;
`;

interface AndBadgeProps {
entriesLength: number;
entryItemIndex: number;
}

export const AndBadgeComponent = React.memo<AndBadgeProps>(({ entriesLength, entryItemIndex }) => {
const badge = <AndOrBadge includeAntennas type="and" />;

if (entriesLength > 1 && entryItemIndex === 0) {
return (
<MyFirstRowContainer grow={false} data-test-subj="entryItemEntryFirstRowAndBadge">
{badge}
</MyFirstRowContainer>
);
} else if (entriesLength <= 1) {
return (
<MyInvisibleAndBadge grow={false} data-test-subj="entryItemEntryInvisibleAndBadge">
{badge}
</MyInvisibleAndBadge>
);
} else {
return (
<EuiFlexItem grow={false} data-test-subj="entryItemEntryAndBadge">
{badge}
</EuiFlexItem>
);
}
});

AndBadgeComponent.displayName = 'AndBadge';
Loading