mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
## Summary This adds threat matching rule type to the rule creator. Screen shot of creating a threat match <img width="1023" alt="Screen Shot 2020-09-30 at 3 31 09 PM" src="https://user-images.githubusercontent.com/1151048/94742158-791b1c00-0332-11eb-9d79-78ab431322f0.png"> --- Screen shot of the description after creating one <img width="1128" alt="Screen Shot 2020-09-30 at 3 29 32 PM" src="https://user-images.githubusercontent.com/1151048/94742203-8b955580-0332-11eb-837f-5b4383044a13.png"> --- Screen shot of first creating a threat match without values filled out <img width="1017" alt="Screen Shot 2020-09-30 at 3 27 29 PM" src="https://user-images.githubusercontent.com/1151048/94742222-95b75400-0332-11eb-9872-e7670e917941.png"> Additions and bug fixes: * Changes the threat index to be an array * Adds a threat_language to the REST schema so that we can use KQL, Lucene, (others in the future) * Adds plumbing for threat_list to work with the other REST endpoints such as PUT, PATCH, etc... * Adds the AND, OR dialog and user interface **Usage** If you are a team member using the team servers you can skip this usage section of creating threat index. Otherwise if you want to know how to create a mock threat index, instructions are below. Go to the folder: ```ts /kibana/x-pack/plugins/security_solution/server/lib/detection_engine/scripts ``` And post a small ECS threat mapping to the index called `mock-threat-list`: ```ts ./create_threat_mapping.sh ``` Then to post a small number of threats that represent simple port numbers you can run: ```ts ./create_threat_data.sh ``` However, feel free to also manually create them directly in your dev tools like so: ```ts # Posts a threat list item called some-name with an IP but change these out for valid data in your system PUT mock-threat-list-1/_doc/9999 { "@timestamp": "2020-09-09T20:30:45.725Z", "host": { "name": "some-name", "ip": "127.0.0.1" } } ``` ```ts # Posts a destination port number to watch PUT mock-threat-list-1/_doc/10000 { "@timestamp": "2020-09-08T20:30:45.725Z", "destination": { "port": "443" } } ``` ```ts # Posts a source port number to watch PUT mock-threat-list-1/_doc/10001 { "@timestamp": "2020-09-08T20:30:45.725Z", "source": { "port": "443" } } ``` ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
parent
2ea3604243
commit
a8938a34bf
81 changed files with 3224 additions and 59 deletions
|
@ -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: [
|
||||
|
@ -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: [
|
||||
|
|
|
@ -51,6 +51,7 @@ import {
|
|||
threat_query,
|
||||
threat_filters,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
} from '../types/threat_mapping';
|
||||
|
||||
import {
|
||||
|
@ -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
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -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: [
|
||||
|
@ -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: [
|
||||
|
|
|
@ -52,6 +52,7 @@ import {
|
|||
threat_query,
|
||||
threat_filters,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
} from '../types/threat_mapping';
|
||||
|
||||
import {
|
||||
|
@ -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
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -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: [
|
||||
|
@ -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: [
|
||||
|
|
|
@ -58,6 +58,7 @@ import {
|
|||
threat_query,
|
||||
threat_filters,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
} from '../types/threat_mapping';
|
||||
|
||||
import {
|
||||
|
@ -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
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
@ -97,6 +104,11 @@ export const patchRulesSchema = t.exact(
|
|||
note,
|
||||
version,
|
||||
exceptions_list: listArrayOrUndefined,
|
||||
threat_index,
|
||||
threat_query,
|
||||
threat_filters,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ import {
|
|||
threat_query,
|
||||
threat_filters,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
} from '../types/threat_mapping';
|
||||
|
||||
import { DefaultListArray } from '../types/lists_default_array';
|
||||
|
@ -144,6 +145,7 @@ export const dependentRulesSchema = t.partial({
|
|||
threat_index,
|
||||
threat_query,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -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 })),
|
||||
];
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
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>;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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';
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ThreatMappingEntries } from '../../../../common/detection_engine/schemas/types';
|
||||
|
||||
import { EntryDeleteButtonComponent } from './entry_delete_button';
|
||||
|
||||
const entries: ThreatMappingEntries = [
|
||||
{
|
||||
field: 'field.one',
|
||||
type: 'mapping',
|
||||
value: 'field.one',
|
||||
},
|
||||
];
|
||||
|
||||
describe('EntryDeleteButtonComponent', () => {
|
||||
test('it renders firstRowDeleteButton for very first entry', () => {
|
||||
const wrapper = mount(
|
||||
<EntryDeleteButtonComponent
|
||||
entryIndex={0}
|
||||
itemIndex={0}
|
||||
isOnlyItem={false}
|
||||
entries={entries}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="firstRowDeleteButton"] button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it does not render firstRowDeleteButton if entryIndex is not 0', () => {
|
||||
const wrapper = mount(
|
||||
<EntryDeleteButtonComponent
|
||||
entryIndex={1}
|
||||
itemIndex={0}
|
||||
isOnlyItem={false}
|
||||
entries={entries}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="firstRowDeleteButton"]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-test-subj="deleteButton"] button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it does not render firstRowDeleteButton if itemIndex is not 0', () => {
|
||||
const wrapper = mount(
|
||||
<EntryDeleteButtonComponent
|
||||
entryIndex={0}
|
||||
itemIndex={1}
|
||||
isOnlyItem={false}
|
||||
entries={entries}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="firstRowDeleteButton"]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-test-subj="deleteButton"] button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it invokes "onDelete" when button is clicked', () => {
|
||||
const onDelete = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<EntryDeleteButtonComponent
|
||||
entryIndex={0}
|
||||
itemIndex={1}
|
||||
isOnlyItem={false}
|
||||
entries={entries}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="deleteButton"] button').simulate('click');
|
||||
|
||||
expect(onDelete).toHaveBeenCalledTimes(1);
|
||||
expect(onDelete).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('it disables button if it is the only entry left and no field has been selected', () => {
|
||||
const emptyEntries: ThreatMappingEntries = [
|
||||
{
|
||||
field: '',
|
||||
type: 'mapping',
|
||||
value: 'field.one',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<EntryDeleteButtonComponent
|
||||
entryIndex={0}
|
||||
itemIndex={0}
|
||||
isOnlyItem
|
||||
entries={emptyEntries}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = wrapper.find('[data-test-subj="firstRowDeleteButton"] button').at(0);
|
||||
|
||||
expect(button.prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it does not disable button if it is the only entry left and field has been selected', () => {
|
||||
const wrapper = mount(
|
||||
<EntryDeleteButtonComponent
|
||||
entryIndex={1}
|
||||
itemIndex={0}
|
||||
isOnlyItem
|
||||
entries={entries}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = wrapper.find('[data-test-subj="deleteButton"] button').at(0);
|
||||
|
||||
expect(button.prop('disabled')).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Entry } from './types';
|
||||
|
||||
const MyFirstRowContainer = styled(EuiFlexItem)`
|
||||
padding-top: 20px;
|
||||
`;
|
||||
|
||||
interface EntryDeleteButtonProps {
|
||||
entries: Entry[];
|
||||
isOnlyItem: boolean;
|
||||
entryIndex: number;
|
||||
itemIndex: number;
|
||||
onDelete: (item: number) => void;
|
||||
}
|
||||
|
||||
export const EntryDeleteButtonComponent = React.memo<EntryDeleteButtonProps>(
|
||||
({ entries, isOnlyItem, entryIndex, itemIndex, onDelete }) => {
|
||||
const isDisabled: boolean =
|
||||
isOnlyItem &&
|
||||
entries.length === 1 &&
|
||||
itemIndex === 0 &&
|
||||
(entries[0].field == null || entries[0].field === '');
|
||||
|
||||
const handleDelete = useCallback((): void => {
|
||||
onDelete(entryIndex);
|
||||
}, [onDelete, entryIndex]);
|
||||
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={handleDelete}
|
||||
isDisabled={isDisabled}
|
||||
aria-label="entryDeleteButton"
|
||||
className="itemEntryDeleteButton"
|
||||
data-test-subj="itemEntryDeleteButton"
|
||||
/>
|
||||
);
|
||||
|
||||
if (entryIndex === 0 && itemIndex === 0) {
|
||||
// This logic was added to work around it including the field
|
||||
// labels in centering the delete icon for the first row
|
||||
return (
|
||||
<MyFirstRowContainer grow={false} data-test-subj="firstRowDeleteButton">
|
||||
{button}
|
||||
</MyFirstRowContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiFlexItem grow={false} data-test-subj="deleteButton">
|
||||
{button}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
EntryDeleteButtonComponent.displayName = 'EntryDeleteButton';
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { EntryItem } from './entry_item';
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { IndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
describe('EntryItem', () => {
|
||||
test('it renders field labels if "showLabel" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<EntryItem
|
||||
entry={{
|
||||
field: undefined,
|
||||
value: undefined,
|
||||
type: 'mapping',
|
||||
entryIndex: 0,
|
||||
}}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
showLabel={true}
|
||||
onChange={jest.fn()}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="threatFieldInputFormRow"]')).not.toEqual(0);
|
||||
});
|
||||
|
||||
test('it invokes "onChange" when new field is selected and resets value fields', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<EntryItem
|
||||
entry={{
|
||||
field: getField('ip'),
|
||||
type: 'mapping',
|
||||
value: getField('ip'),
|
||||
entryIndex: 0,
|
||||
}}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
showLabel={false}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
((wrapper.find(EuiComboBox).at(0).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange([{ label: 'machine.os' }]);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
{
|
||||
field: 'machine.os',
|
||||
type: 'mapping',
|
||||
value: 'ip',
|
||||
},
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes "onChange" when new value is selected', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<EntryItem
|
||||
entry={{
|
||||
field: getField('ip'),
|
||||
type: 'mapping',
|
||||
value: getField('ip'),
|
||||
entryIndex: 0,
|
||||
}}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
showLabel={false}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
((wrapper.find(EuiComboBox).at(1).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange([{ label: 'is not' }]);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({ field: 'ip', type: 'mapping', value: '' }, 0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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, useMemo } from 'react';
|
||||
import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IFieldType, IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { FieldComponent } from '../autocomplete/field';
|
||||
import { FormattedEntry, Entry } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { getEntryOnFieldChange, getEntryOnThreatFieldChange } from './helpers';
|
||||
|
||||
interface EntryItemProps {
|
||||
entry: FormattedEntry;
|
||||
indexPattern: IndexPattern;
|
||||
threatIndexPatterns: IndexPattern;
|
||||
showLabel: boolean;
|
||||
onChange: (arg: Entry, i: number) => void;
|
||||
}
|
||||
|
||||
const FlexItemWithLabel = styled(EuiFlexItem)`
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const FlexItemWithoutLabel = styled(EuiFlexItem)`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const EntryItem: React.FC<EntryItemProps> = ({
|
||||
entry,
|
||||
indexPattern,
|
||||
threatIndexPatterns,
|
||||
showLabel,
|
||||
onChange,
|
||||
}): JSX.Element => {
|
||||
const handleFieldChange = useCallback(
|
||||
([newField]: IFieldType[]): void => {
|
||||
const { updatedEntry, index } = getEntryOnFieldChange(entry, newField);
|
||||
onChange(updatedEntry, index);
|
||||
},
|
||||
[onChange, entry]
|
||||
);
|
||||
|
||||
const handleThreatFieldChange = useCallback(
|
||||
([newField]: IFieldType[]): void => {
|
||||
const { updatedEntry, index } = getEntryOnThreatFieldChange(entry, newField);
|
||||
onChange(updatedEntry, index);
|
||||
},
|
||||
[onChange, entry]
|
||||
);
|
||||
|
||||
const renderFieldInput = useMemo(() => {
|
||||
const comboBox = (
|
||||
<FieldComponent
|
||||
placeholder={i18n.FIELD_PLACEHOLDER}
|
||||
indexPattern={indexPattern}
|
||||
selectedField={entry.field}
|
||||
isClearable={false}
|
||||
isLoading={false}
|
||||
isDisabled={indexPattern == null}
|
||||
onChange={handleFieldChange}
|
||||
data-test-subj="entryField"
|
||||
fieldInputWidth={360}
|
||||
/>
|
||||
);
|
||||
|
||||
if (showLabel) {
|
||||
return (
|
||||
<EuiFormRow label={i18n.FIELD} data-test-subj="entryItemFieldInputFormRow">
|
||||
{comboBox}
|
||||
</EuiFormRow>
|
||||
);
|
||||
} else {
|
||||
return comboBox;
|
||||
}
|
||||
}, [handleFieldChange, indexPattern, entry, showLabel]);
|
||||
|
||||
const renderThreatFieldInput = useMemo(() => {
|
||||
const comboBox = (
|
||||
<FieldComponent
|
||||
placeholder={i18n.FIELD_PLACEHOLDER}
|
||||
indexPattern={threatIndexPatterns}
|
||||
selectedField={entry.value}
|
||||
isClearable={false}
|
||||
isLoading={false}
|
||||
isDisabled={threatIndexPatterns == null}
|
||||
onChange={handleThreatFieldChange}
|
||||
data-test-subj="threatEntryField"
|
||||
fieldInputWidth={360}
|
||||
/>
|
||||
);
|
||||
|
||||
if (showLabel) {
|
||||
return (
|
||||
<EuiFormRow label={i18n.THREAT_FIELD} data-test-subj="threatFieldInputFormRow">
|
||||
{comboBox}
|
||||
</EuiFormRow>
|
||||
);
|
||||
} else {
|
||||
return comboBox;
|
||||
}
|
||||
}, [handleThreatFieldChange, threatIndexPatterns, entry, showLabel]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="spaceAround"
|
||||
data-test-subj="itemEntryContainer"
|
||||
>
|
||||
<EuiFlexItem grow={false}>{renderFieldInput}</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup justifyContent="spaceAround" alignItems="center">
|
||||
{showLabel ? (
|
||||
<FlexItemWithLabel grow={true}>{i18n.MATCHES}</FlexItemWithLabel>
|
||||
) : (
|
||||
<FlexItemWithoutLabel grow={true}>{i18n.MATCHES}</FlexItemWithoutLabel>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{renderThreatFieldInput}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
EntryItem.displayName = 'EntryItem';
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* 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 {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { Entry, EmptyEntry, ThreatMapEntries, FormattedEntry } from './types';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {
|
||||
filterItems,
|
||||
getEntryOnFieldChange,
|
||||
getFormattedEntries,
|
||||
getFormattedEntry,
|
||||
getUpdatedEntriesOnDelete,
|
||||
} from './helpers';
|
||||
import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types';
|
||||
|
||||
const getMockIndexPattern = (): IndexPattern =>
|
||||
({
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern);
|
||||
|
||||
const getMockEntry = (): FormattedEntry => ({
|
||||
field: getField('ip'),
|
||||
value: getField('ip'),
|
||||
type: 'mapping',
|
||||
entryIndex: 0,
|
||||
});
|
||||
|
||||
describe('Helpers', () => {
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('UTC');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
moment.tz.setDefault('Browser');
|
||||
});
|
||||
|
||||
describe('#getFormattedEntry', () => {
|
||||
test('it returns entry with a value when "item.field" is of type "text" and matching keyword field exists', () => {
|
||||
const payloadIndexPattern: IndexPattern = {
|
||||
...getMockIndexPattern(),
|
||||
fields: [
|
||||
...fields,
|
||||
{
|
||||
name: 'machine.os.raw.text',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
],
|
||||
} as IndexPattern;
|
||||
const payloadItem: Entry = {
|
||||
field: 'machine.os.raw.text',
|
||||
type: 'mapping',
|
||||
value: 'some os',
|
||||
};
|
||||
const output = getFormattedEntry(payloadIndexPattern, payloadItem, 0);
|
||||
const expected: FormattedEntry = {
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
name: 'machine.os.raw.text',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
type: 'mapping',
|
||||
value: undefined,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFormattedEntries', () => {
|
||||
test('it returns formatted entry with fields undefined if it unable to find a matching index pattern field', () => {
|
||||
const payloadIndexPattern: IndexPattern = getMockIndexPattern();
|
||||
const payloadItems: Entry[] = [{ field: 'field.one', type: 'mapping', value: 'field.one' }];
|
||||
const output = getFormattedEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedEntry[] = [
|
||||
{
|
||||
entryIndex: 0,
|
||||
field: undefined,
|
||||
value: undefined,
|
||||
type: 'mapping',
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns formatted entries', () => {
|
||||
const payloadIndexPattern: IndexPattern = getMockIndexPattern();
|
||||
const payloadItems: Entry[] = [
|
||||
{ field: 'machine.os', type: 'mapping', value: 'machine.os' },
|
||||
{ field: 'ip', type: 'mapping', value: 'ip' },
|
||||
];
|
||||
const output = getFormattedEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedEntry[] = [
|
||||
{
|
||||
field: {
|
||||
name: 'machine.os',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
type: 'mapping',
|
||||
value: {
|
||||
name: 'machine.os',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
entryIndex: 0,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'ip',
|
||||
type: 'ip',
|
||||
esTypes: ['ip'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
type: 'mapping',
|
||||
value: {
|
||||
name: 'ip',
|
||||
type: 'ip',
|
||||
esTypes: ['ip'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
entryIndex: 1,
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUpdatedEntriesOnDelete', () => {
|
||||
test('it removes entry corresponding to "entryIndex"', () => {
|
||||
const payloadItem: ThreatMapEntries = {
|
||||
entries: [
|
||||
{ field: 'field.one', type: 'mapping', value: 'field.one' },
|
||||
{ field: 'field.two', type: 'mapping', value: 'field.two' },
|
||||
],
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0);
|
||||
const expected: ThreatMapEntries = {
|
||||
entries: [
|
||||
{
|
||||
field: 'field.two',
|
||||
type: 'mapping',
|
||||
value: 'field.two',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEntryOnFieldChange', () => {
|
||||
test('it returns field of type "match" with updated field', () => {
|
||||
const payloadItem = getMockEntry();
|
||||
const payloadIFieldType = getField('ip');
|
||||
const output = getEntryOnFieldChange(payloadItem, payloadIFieldType);
|
||||
const expected: { updatedEntry: Entry; index: number } = {
|
||||
index: 0,
|
||||
updatedEntry: {
|
||||
field: 'ip',
|
||||
type: 'mapping',
|
||||
value: 'ip',
|
||||
},
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#filterItems', () => {
|
||||
test('it removes entry items with "value" of "undefined"', () => {
|
||||
const entry: ThreatMapEntry = { field: 'host.name', type: 'mapping', value: 'host.name' };
|
||||
const mockEmpty: EmptyEntry = {
|
||||
field: 'host.name',
|
||||
type: 'mapping',
|
||||
value: undefined,
|
||||
};
|
||||
const items = filterItems([
|
||||
{
|
||||
entries: [entry],
|
||||
},
|
||||
{
|
||||
entries: [mockEmpty],
|
||||
},
|
||||
]);
|
||||
expect(items).toEqual([{ entries: [entry] }]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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 {
|
||||
ThreatMap,
|
||||
threatMap,
|
||||
ThreatMapping,
|
||||
} from '../../../../common/detection_engine/schemas/types';
|
||||
|
||||
import { IndexPattern, IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
import { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types';
|
||||
|
||||
/**
|
||||
* Formats the entry into one that is easily usable for the UI.
|
||||
*
|
||||
* @param patterns IndexPattern containing available fields on rule index
|
||||
* @param item item entry
|
||||
* @param itemIndex entry index
|
||||
*/
|
||||
export const getFormattedEntry = (
|
||||
indexPattern: IndexPattern,
|
||||
item: Entry,
|
||||
itemIndex: number
|
||||
): FormattedEntry => {
|
||||
const { fields } = indexPattern;
|
||||
const field = item.field;
|
||||
const threatField = item.value;
|
||||
const [foundField] = fields.filter(({ name }) => field != null && field === name);
|
||||
const [threatFoundField] = fields.filter(
|
||||
({ name }) => threatField != null && threatField === name
|
||||
);
|
||||
return {
|
||||
field: foundField,
|
||||
type: 'mapping',
|
||||
value: threatFoundField,
|
||||
entryIndex: itemIndex,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the entries to be easily usable for the UI
|
||||
*
|
||||
* @param patterns IndexPattern containing available fields on rule index
|
||||
* @param entries item entries
|
||||
*/
|
||||
export const getFormattedEntries = (
|
||||
indexPattern: IndexPattern,
|
||||
entries: Entry[]
|
||||
): FormattedEntry[] => {
|
||||
return entries.reduce<FormattedEntry[]>((acc, item, index) => {
|
||||
const newItemEntry = getFormattedEntry(indexPattern, item, index);
|
||||
return [...acc, newItemEntry];
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether an entire entry or item need to be removed
|
||||
*
|
||||
* @param item
|
||||
* @param entryIndex index of given entry
|
||||
*
|
||||
*/
|
||||
export const getUpdatedEntriesOnDelete = (
|
||||
item: ThreatMapEntries,
|
||||
entryIndex: number
|
||||
): ThreatMapEntries => {
|
||||
return {
|
||||
...item,
|
||||
entries: [...item.entries.slice(0, entryIndex), ...item.entries.slice(entryIndex + 1)],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines proper entry update when user selects new field
|
||||
*
|
||||
* @param item - current item entry values
|
||||
* @param newField - newly selected field
|
||||
*
|
||||
*/
|
||||
export const getEntryOnFieldChange = (
|
||||
item: FormattedEntry,
|
||||
newField: IFieldType
|
||||
): { updatedEntry: Entry; index: number } => {
|
||||
const { entryIndex } = item;
|
||||
return {
|
||||
updatedEntry: {
|
||||
field: newField != null ? newField.name : '',
|
||||
type: 'mapping',
|
||||
value: item.value != null ? item.value.name : '',
|
||||
},
|
||||
index: entryIndex,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines proper entry update when user selects new field
|
||||
*
|
||||
* @param item - current item entry values
|
||||
* @param newField - newly selected field
|
||||
*
|
||||
*/
|
||||
export const getEntryOnThreatFieldChange = (
|
||||
item: FormattedEntry,
|
||||
newField: IFieldType
|
||||
): { updatedEntry: Entry; index: number } => {
|
||||
const { entryIndex } = item;
|
||||
return {
|
||||
updatedEntry: {
|
||||
field: item.field != null ? item.field.name : '',
|
||||
type: 'mapping',
|
||||
value: newField != null ? newField.name : '',
|
||||
},
|
||||
index: entryIndex,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultEmptyEntry = (): EmptyEntry => ({
|
||||
field: '',
|
||||
type: 'mapping',
|
||||
value: '',
|
||||
});
|
||||
|
||||
export const getNewItem = (): ThreatMap => {
|
||||
return {
|
||||
entries: [
|
||||
{
|
||||
field: '',
|
||||
type: 'mapping',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const filterItems = (items: ThreatMapEntries[]): ThreatMapping => {
|
||||
return items.reduce<ThreatMapping>((acc, item) => {
|
||||
const newItem = { ...item, entries: item.entries };
|
||||
if (threatMap.is(newItem)) {
|
||||
return [...acc, newItem];
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a list of items checks each one to see if any of them have an empty field
|
||||
* or an empty value.
|
||||
* @param items The items to check if we have an empty entries.
|
||||
*/
|
||||
export const containsInvalidItems = (items: ThreatMapEntries[]): boolean => {
|
||||
return items.some((item) =>
|
||||
item.entries.some((subEntry) => subEntry.field === '' || subEntry.value === '')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a list of items checks if we have a single empty entry and if we do returns true.
|
||||
* @param items The items to check if we have a single empty entry.
|
||||
*/
|
||||
export const singleEntryThreat = (items: ThreatMapEntries[]): boolean => {
|
||||
return (
|
||||
items.length === 1 &&
|
||||
items[0].entries.length === 1 &&
|
||||
items[0].entries[0].field === '' &&
|
||||
items[0].entries[0].value === ''
|
||||
);
|
||||
};
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* 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 { waitFor } from '@testing-library/react';
|
||||
|
||||
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
import { ThreatMatchComponent } from './';
|
||||
import { ThreatMapEntries } from './types';
|
||||
import { IndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const getPayLoad = (): ThreatMapEntries[] => [
|
||||
{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] },
|
||||
];
|
||||
|
||||
const getDoublePayLoad = (): ThreatMapEntries[] => [
|
||||
{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] },
|
||||
{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] },
|
||||
];
|
||||
|
||||
describe('ThreatMatchComponent', () => {
|
||||
const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']);
|
||||
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
autocomplete: {
|
||||
getValueSuggestions: getValueSuggestionsMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getValueSuggestionsMock.mockClear();
|
||||
});
|
||||
|
||||
test('it displays empty entry if no "listItems" are passed in', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={[]}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="entryField"]').text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="threatEntryField"]').text()).toEqual('Search');
|
||||
});
|
||||
|
||||
test('it displays "Search" for "listItems" that are passed in', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={getPayLoad()}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="entryField"]').at(0).text()).toEqual('Search');
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('it displays "or", "and" enabled', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={[]}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="andButton"] button').prop('disabled')).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="orButton"] button').prop('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it adds an entry when "and" clicked', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={[]}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(1);
|
||||
|
||||
wrapper.find('[data-test-subj="andButton"] button').simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(2);
|
||||
expect(wrapper.find('[data-test-subj="entryField"]').at(0).text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="threatEntryField"]').at(0).text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="entryField"]').at(1).text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="threatEntryField"]').at(1).text()).toEqual('Search');
|
||||
});
|
||||
});
|
||||
|
||||
test('it adds an item when "or" clicked', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={[]}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiFlexGroup[data-test-subj="entriesContainer"]')).toHaveLength(1);
|
||||
|
||||
wrapper.find('[data-test-subj="orButton"] button').simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('EuiFlexGroup[data-test-subj="entriesContainer"]')).toHaveLength(2);
|
||||
expect(wrapper.find('[data-test-subj="entryField"]').at(0).text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="threatEntryField"]').at(0).text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="entryField"]').at(1).text()).toEqual('Search');
|
||||
expect(wrapper.find('[data-test-subj="threatEntryField"]').at(1).text()).toEqual('Search');
|
||||
});
|
||||
});
|
||||
|
||||
test('it removes one row if user deletes a row', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={getDoublePayLoad()}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="entriesContainer"]').length).toEqual(4);
|
||||
wrapper.find('[data-test-subj="firstRowDeleteButton"] button').simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="entriesContainer"]').length).toEqual(2);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('it displays "and" badge if at least one item includes more than one entry', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={[]}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeFalsy();
|
||||
|
||||
wrapper.find('[data-test-subj="andButton"] button').simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it does not display "and" badge if none of the items include more than one entry', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ThreatMatchComponent
|
||||
listItems={[]}
|
||||
indexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="orButton"] button').simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeFalsy();
|
||||
|
||||
wrapper.find('[data-test-subj="orButton"] button').simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* 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, useEffect, useReducer } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ThreatMapping } from '../../../../common/detection_engine/schemas/types';
|
||||
import { ListItemComponent } from './list_item';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { AndOrBadge } from '../and_or_badge';
|
||||
import { LogicButtons } from './logic_buttons';
|
||||
import { ThreatMapEntries } from './types';
|
||||
import { State, reducer } from './reducer';
|
||||
import { getDefaultEmptyEntry, getNewItem, filterItems } from './helpers';
|
||||
|
||||
const MyInvisibleAndBadge = styled(EuiFlexItem)`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
const MyAndBadge = styled(AndOrBadge)`
|
||||
& > .euiFlexItem {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const MyButtonsContainer = styled(EuiFlexItem)`
|
||||
margin: 16px 0;
|
||||
`;
|
||||
|
||||
const initialState: State = {
|
||||
andLogicIncluded: false,
|
||||
entries: [],
|
||||
entriesToDelete: [],
|
||||
};
|
||||
|
||||
interface OnChangeProps {
|
||||
entryItems: ThreatMapping;
|
||||
entriesToDelete: ThreatMapEntries[];
|
||||
}
|
||||
|
||||
interface ThreatMatchComponentProps {
|
||||
listItems: ThreatMapEntries[];
|
||||
indexPatterns: IndexPattern;
|
||||
threatIndexPatterns: IndexPattern;
|
||||
onChange: (arg: OnChangeProps) => void;
|
||||
}
|
||||
|
||||
export const ThreatMatchComponent = ({
|
||||
listItems,
|
||||
indexPatterns,
|
||||
threatIndexPatterns,
|
||||
onChange,
|
||||
}: ThreatMatchComponentProps) => {
|
||||
const [{ entries, entriesToDelete, andLogicIncluded }, dispatch] = useReducer(reducer(), {
|
||||
...initialState,
|
||||
});
|
||||
|
||||
const setUpdateEntries = useCallback(
|
||||
(items: ThreatMapEntries[]): void => {
|
||||
dispatch({
|
||||
type: 'setEntries',
|
||||
entries: items,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setDefaultEntries = useCallback(
|
||||
(item: ThreatMapEntries): void => {
|
||||
dispatch({
|
||||
type: 'setDefault',
|
||||
initialState,
|
||||
lastEntry: item,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleEntryItemChange = useCallback(
|
||||
(item: ThreatMapEntries, index: number): void => {
|
||||
const updatedEntries = [
|
||||
...entries.slice(0, index),
|
||||
{
|
||||
...item,
|
||||
},
|
||||
...entries.slice(index + 1),
|
||||
];
|
||||
|
||||
setUpdateEntries(updatedEntries);
|
||||
},
|
||||
[setUpdateEntries, entries]
|
||||
);
|
||||
|
||||
const handleDeleteEntryItem = useCallback(
|
||||
(item: ThreatMapEntries, itemIndex: number): void => {
|
||||
if (item.entries.length === 0) {
|
||||
const updatedEntries = [...entries.slice(0, itemIndex), ...entries.slice(itemIndex + 1)];
|
||||
// if it's the only item left, don't delete it just add a default entry to it
|
||||
if (updatedEntries.length === 0) {
|
||||
setDefaultEntries(item);
|
||||
} else {
|
||||
setUpdateEntries([...entries.slice(0, itemIndex), ...entries.slice(itemIndex + 1)]);
|
||||
}
|
||||
} else {
|
||||
handleEntryItemChange(item, itemIndex);
|
||||
}
|
||||
},
|
||||
[handleEntryItemChange, setUpdateEntries, entries, setDefaultEntries]
|
||||
);
|
||||
|
||||
const handleAddNewEntryItemEntry = useCallback((): void => {
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
const { entries: innerEntries } = lastEntry;
|
||||
|
||||
const updatedEntry: ThreatMapEntries = {
|
||||
...lastEntry,
|
||||
entries: [...innerEntries, getDefaultEmptyEntry()],
|
||||
};
|
||||
|
||||
setUpdateEntries([...entries.slice(0, entries.length - 1), { ...updatedEntry }]);
|
||||
}, [setUpdateEntries, entries]);
|
||||
|
||||
const handleAddNewEntryItem = useCallback((): void => {
|
||||
// There is a case where there are numerous list items, all with
|
||||
// empty `entries` array.
|
||||
const newItem = getNewItem();
|
||||
setUpdateEntries([...entries, { ...newItem }]);
|
||||
}, [setUpdateEntries, entries]);
|
||||
|
||||
const handleAddClick = useCallback((): void => {
|
||||
handleAddNewEntryItemEntry();
|
||||
}, [handleAddNewEntryItemEntry]);
|
||||
|
||||
// Bubble up changes to parent
|
||||
useEffect(() => {
|
||||
onChange({ entryItems: filterItems(entries), entriesToDelete });
|
||||
}, [onChange, entriesToDelete, entries]);
|
||||
|
||||
// Defaults to never be sans entry, instead
|
||||
// always falls back to an empty entry if user deletes all
|
||||
useEffect(() => {
|
||||
if (
|
||||
entries.length === 0 ||
|
||||
(entries.length === 1 && entries[0].entries != null && entries[0].entries.length === 0)
|
||||
) {
|
||||
handleAddNewEntryItem();
|
||||
}
|
||||
}, [entries, handleAddNewEntryItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listItems.length > 0) {
|
||||
setUpdateEntries(listItems);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
{entries.map((entryListItem, index) => (
|
||||
<EuiFlexItem grow={1} key={`${index}`}>
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
{index !== 0 &&
|
||||
(andLogicIncluded ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none" direction="row">
|
||||
<MyInvisibleAndBadge grow={false}>
|
||||
<MyAndBadge includeAntennas type="and" />
|
||||
</MyInvisibleAndBadge>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyAndBadge type="or" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyAndBadge type="or" />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
<EuiFlexItem grow={false}>
|
||||
<ListItemComponent
|
||||
key={`${index}`}
|
||||
listItem={entryListItem}
|
||||
listId={`${index}`}
|
||||
indexPattern={indexPatterns}
|
||||
threatIndexPatterns={threatIndexPatterns}
|
||||
listItemIndex={index}
|
||||
andLogicIncluded={andLogicIncluded}
|
||||
isOnlyItem={entries.length === 1}
|
||||
onDeleteEntryItem={handleDeleteEntryItem}
|
||||
onChangeEntryItem={handleEntryItemChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
||||
<MyButtonsContainer data-test-subj={'andOrOperatorButtons'}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
{andLogicIncluded && (
|
||||
<MyInvisibleAndBadge grow={false}>
|
||||
<AndOrBadge includeAntennas type="and" />
|
||||
</MyInvisibleAndBadge>
|
||||
)}
|
||||
<EuiFlexItem grow={1}>
|
||||
<LogicButtons
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
onOrClicked={handleAddNewEntryItem}
|
||||
onAndClicked={handleAddClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</MyButtonsContainer>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ThreatMatchComponent.displayName = 'ThreatMatch';
|
|
@ -0,0 +1,382 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../../../common/lib/kibana';
|
||||
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
|
||||
import { ListItemComponent } from './list_item';
|
||||
import { ThreatMapEntries } from './types';
|
||||
import { IndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const singlePayload = (): ThreatMapEntries => ({
|
||||
entries: [
|
||||
{
|
||||
field: 'field.one',
|
||||
type: 'mapping',
|
||||
value: 'field.one',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const doublePayload = (): ThreatMapEntries => ({
|
||||
entries: [
|
||||
{
|
||||
field: 'field.one',
|
||||
type: 'mapping',
|
||||
value: 'field.one',
|
||||
},
|
||||
{
|
||||
field: 'field.two',
|
||||
type: 'mapping',
|
||||
value: 'field.two',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('ListItemComponent', () => {
|
||||
const getValueSuggestionsMock = jest.fn().mockResolvedValue(['field.one', 'field.two']);
|
||||
|
||||
beforeAll(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
autocomplete: {
|
||||
getValueSuggestions: getValueSuggestionsMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getValueSuggestionsMock.mockClear();
|
||||
});
|
||||
|
||||
describe('and badge logic', () => {
|
||||
test('it renders "and" badge with extra top padding for the first item when "andLogicIncluded" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ListItemComponent
|
||||
listItem={doublePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={0}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={true}
|
||||
isOnlyItem={false}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders "and" badge when more than one item entry exists and it is not the first item', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ListItemComponent
|
||||
listItem={doublePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={1}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={true}
|
||||
isOnlyItem={false}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="entryItemEntryAndBadge"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ListItemComponent
|
||||
listItem={singlePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={1}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={true}
|
||||
isOnlyItem={false}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="entryItemEntryInvisibleAndBadge"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders no "and" badge when "andLogicIncluded" is "false"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ListItemComponent
|
||||
listItem={singlePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={1}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={false}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="entryItemEntryInvisibleAndBadge"]').exists()
|
||||
).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="entryItemEntryAndBadge"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete button logic', () => {
|
||||
test('it renders delete button disabled when it is only entry left', () => {
|
||||
const item: ThreatMapEntries = {
|
||||
entries: [{ ...singlePayload(), field: '', type: 'mapping', value: '' }],
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ListItemComponent
|
||||
listItem={item}
|
||||
listId={'123'}
|
||||
listItemIndex={0}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={true}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="itemEntryDeleteButton"] button').props().disabled
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it does not render delete button disabled when it is not the only entry left', () => {
|
||||
const wrapper = mount(
|
||||
<ListItemComponent
|
||||
listItem={singlePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={0}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={false}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="itemEntryDeleteButton"] button').props().disabled
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it does not render delete button disabled when "entryItemIndex" is not "0"', () => {
|
||||
const wrapper = mount(
|
||||
<ListItemComponent
|
||||
listItem={singlePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={1}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={false}
|
||||
// if entryItemIndex is not 0, wouldn't make sense for
|
||||
// this to be true, but done for testing purposes
|
||||
isOnlyItem={true}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="itemEntryDeleteButton"] button').props().disabled
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it does not render delete button disabled when more than one entry exists', () => {
|
||||
const wrapper = mount(
|
||||
<ListItemComponent
|
||||
listItem={doublePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={0}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={true}
|
||||
onDeleteEntryItem={jest.fn()}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="itemEntryDeleteButton"] button').at(0).props().disabled
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it invokes "onChangeEntryItem" when delete button clicked', () => {
|
||||
const mockOnDeleteEntryItem = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ListItemComponent
|
||||
listItem={doublePayload()}
|
||||
listId={'123'}
|
||||
listItemIndex={0}
|
||||
indexPattern={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
threatIndexPatterns={
|
||||
{
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
} as IndexPattern
|
||||
}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={true}
|
||||
onDeleteEntryItem={mockOnDeleteEntryItem}
|
||||
onChangeEntryItem={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="itemEntryDeleteButton"] button').at(0).simulate('click');
|
||||
|
||||
const expected: ThreatMapEntries = {
|
||||
entries: [
|
||||
{
|
||||
field: 'field.two',
|
||||
type: 'mapping',
|
||||
value: 'field.two',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(mockOnDeleteEntryItem).toHaveBeenCalledWith(expected, 0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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, { useMemo, useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { getFormattedEntries, getUpdatedEntriesOnDelete } from './helpers';
|
||||
import { FormattedEntry, ThreatMapEntries, Entry } from './types';
|
||||
import { EntryItem } from './entry_item';
|
||||
import { EntryDeleteButtonComponent } from './entry_delete_button';
|
||||
import { AndBadgeComponent } from './and_badge';
|
||||
|
||||
const MyOverflowContainer = styled(EuiFlexItem)`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface ListItemProps {
|
||||
listItem: ThreatMapEntries;
|
||||
listId: string;
|
||||
listItemIndex: number;
|
||||
indexPattern: IndexPattern;
|
||||
threatIndexPatterns: IndexPattern;
|
||||
andLogicIncluded: boolean;
|
||||
isOnlyItem: boolean;
|
||||
onDeleteEntryItem: (item: ThreatMapEntries, index: number) => void;
|
||||
onChangeEntryItem: (item: ThreatMapEntries, index: number) => void;
|
||||
}
|
||||
|
||||
export const ListItemComponent = React.memo<ListItemProps>(
|
||||
({
|
||||
listItem,
|
||||
listId,
|
||||
listItemIndex,
|
||||
indexPattern,
|
||||
threatIndexPatterns,
|
||||
isOnlyItem,
|
||||
andLogicIncluded,
|
||||
onDeleteEntryItem,
|
||||
onChangeEntryItem,
|
||||
}) => {
|
||||
const handleEntryChange = useCallback(
|
||||
(entry: Entry, entryIndex: number): void => {
|
||||
const updatedEntries: Entry[] = [
|
||||
...listItem.entries.slice(0, entryIndex),
|
||||
{ ...entry },
|
||||
...listItem.entries.slice(entryIndex + 1),
|
||||
];
|
||||
const updatedEntryItem: ThreatMapEntries = {
|
||||
...listItem,
|
||||
entries: updatedEntries,
|
||||
};
|
||||
onChangeEntryItem(updatedEntryItem, listItemIndex);
|
||||
},
|
||||
[onChangeEntryItem, listItem, listItemIndex]
|
||||
);
|
||||
|
||||
const handleDeleteEntry = useCallback(
|
||||
(entryIndex: number): void => {
|
||||
const updatedEntryItem = getUpdatedEntriesOnDelete(listItem, entryIndex);
|
||||
|
||||
onDeleteEntryItem(updatedEntryItem, listItemIndex);
|
||||
},
|
||||
[listItem, onDeleteEntryItem, listItemIndex]
|
||||
);
|
||||
|
||||
const entries = useMemo(
|
||||
(): FormattedEntry[] =>
|
||||
indexPattern != null && listItem.entries.length > 0
|
||||
? getFormattedEntries(indexPattern, listItem.entries)
|
||||
: [],
|
||||
[listItem.entries, indexPattern]
|
||||
);
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" data-test-subj="entriesContainer">
|
||||
{andLogicIncluded && (
|
||||
<AndBadgeComponent
|
||||
entriesLength={listItem.entries.length}
|
||||
entryItemIndex={listItemIndex}
|
||||
/>
|
||||
)}
|
||||
<MyOverflowContainer grow={6}>
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
{entries.map((item, index) => (
|
||||
<EuiFlexItem key={`${listId}-${index}`} grow={1}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" direction="row">
|
||||
<MyOverflowContainer grow={1}>
|
||||
<EntryItem
|
||||
entry={item}
|
||||
threatIndexPatterns={threatIndexPatterns}
|
||||
indexPattern={indexPattern}
|
||||
showLabel={listItemIndex === 0 && index === 0}
|
||||
onChange={handleEntryChange}
|
||||
/>
|
||||
</MyOverflowContainer>
|
||||
<EntryDeleteButtonComponent
|
||||
entries={listItem.entries}
|
||||
isOnlyItem={isOnlyItem}
|
||||
entryIndex={item.entryIndex}
|
||||
itemIndex={listItemIndex}
|
||||
onDelete={handleDeleteEntry}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</MyOverflowContainer>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ListItemComponent.displayName = 'ListItem';
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { storiesOf, addDecorator } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
|
||||
import { LogicButtons } from './logic_buttons';
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
|
||||
));
|
||||
|
||||
storiesOf('ThreatMatching|LogicButtons', module)
|
||||
.add('and/or buttons', () => {
|
||||
return (
|
||||
<LogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('and disabled', () => {
|
||||
return (
|
||||
<LogicButtons
|
||||
isAndDisabled
|
||||
isOrDisabled={false}
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('or disabled', () => {
|
||||
return (
|
||||
<LogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { LogicButtons } from './logic_buttons';
|
||||
|
||||
describe('LogicButtons', () => {
|
||||
test('it renders "and" and "or" buttons', () => {
|
||||
const wrapper = mount(
|
||||
<LogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
onOrClicked={jest.fn()}
|
||||
onAndClicked={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="andButton"] button')).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="orButton"] button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it invokes "onOrClicked" when "or" button is clicked', () => {
|
||||
const onOrClicked = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<LogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
onOrClicked={onOrClicked}
|
||||
onAndClicked={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="orButton"] button').simulate('click');
|
||||
|
||||
expect(onOrClicked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it invokes "onAndClicked" when "and" button is clicked', () => {
|
||||
const onAndClicked = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<LogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
onOrClicked={jest.fn()}
|
||||
onAndClicked={onAndClicked}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="andButton"] button').simulate('click');
|
||||
|
||||
expect(onAndClicked).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it disables "and" button if "isAndDisabled" is true', () => {
|
||||
const wrapper = mount(
|
||||
<LogicButtons
|
||||
isOrDisabled={false}
|
||||
isAndDisabled
|
||||
onOrClicked={jest.fn()}
|
||||
onAndClicked={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const andButton = wrapper.find('[data-test-subj="andButton"] button').at(0);
|
||||
|
||||
expect(andButton.prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it disables "or" button if "isOrDisabled" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<LogicButtons
|
||||
isOrDisabled
|
||||
isAndDisabled={false}
|
||||
onOrClicked={jest.fn()}
|
||||
onAndClicked={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const orButton = wrapper.find('[data-test-subj="orButton"] button').at(0);
|
||||
|
||||
expect(orButton.prop('disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const MyEuiButton = styled(EuiButton)`
|
||||
min-width: 95px;
|
||||
`;
|
||||
|
||||
interface LogicButtonsProps {
|
||||
isOrDisabled: boolean;
|
||||
isAndDisabled: boolean;
|
||||
onAndClicked: () => void;
|
||||
onOrClicked: () => void;
|
||||
}
|
||||
|
||||
export const LogicButtons: React.FC<LogicButtonsProps> = ({
|
||||
isOrDisabled = false,
|
||||
isAndDisabled = false,
|
||||
onAndClicked,
|
||||
onOrClicked,
|
||||
}) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyEuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onAndClicked}
|
||||
data-test-subj="andButton"
|
||||
isDisabled={isAndDisabled}
|
||||
>
|
||||
{i18n.AND}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyEuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onOrClicked}
|
||||
isDisabled={isOrDisabled}
|
||||
data-test-subj="orButton"
|
||||
>
|
||||
{i18n.OR}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { ThreatMapEntries } from './types';
|
||||
import { State, reducer } from './reducer';
|
||||
import { getDefaultEmptyEntry } from './helpers';
|
||||
import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types';
|
||||
|
||||
const initialState: State = {
|
||||
andLogicIncluded: false,
|
||||
entries: [],
|
||||
entriesToDelete: [],
|
||||
};
|
||||
|
||||
const getEntry = (): ThreatMapEntry => ({
|
||||
field: 'host.name',
|
||||
type: 'mapping',
|
||||
value: 'host.name',
|
||||
});
|
||||
|
||||
describe('reducer', () => {
|
||||
describe('#setEntries', () => {
|
||||
test('should return "andLogicIncluded" ', () => {
|
||||
const update = reducer()(initialState, {
|
||||
type: 'setEntries',
|
||||
entries: [],
|
||||
});
|
||||
const expected: State = {
|
||||
andLogicIncluded: false,
|
||||
entries: [],
|
||||
entriesToDelete: [],
|
||||
};
|
||||
expect(update).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should set "andLogicIncluded" to true if any of the entries include entries with length greater than 1 ', () => {
|
||||
const entries: ThreatMapEntries[] = [
|
||||
{
|
||||
entries: [getEntry(), getEntry()],
|
||||
},
|
||||
];
|
||||
const { andLogicIncluded } = reducer()(initialState, {
|
||||
type: 'setEntries',
|
||||
entries,
|
||||
});
|
||||
|
||||
expect(andLogicIncluded).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set "andLogicIncluded" to false if any of the entries include entries with length greater than 1 ', () => {
|
||||
const entries: ThreatMapEntries[] = [
|
||||
{
|
||||
entries: [getEntry()],
|
||||
},
|
||||
];
|
||||
const { andLogicIncluded } = reducer()(initialState, {
|
||||
type: 'setEntries',
|
||||
entries,
|
||||
});
|
||||
|
||||
expect(andLogicIncluded).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setDefault', () => {
|
||||
test('should restore initial state and add default empty entry to item" ', () => {
|
||||
const entries: ThreatMapEntries[] = [
|
||||
{
|
||||
entries: [getEntry()],
|
||||
},
|
||||
];
|
||||
|
||||
const update = reducer()(
|
||||
{
|
||||
andLogicIncluded: true,
|
||||
entries,
|
||||
entriesToDelete: [],
|
||||
},
|
||||
{
|
||||
type: 'setDefault',
|
||||
initialState,
|
||||
lastEntry: {
|
||||
entries: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(update).toEqual({
|
||||
...initialState,
|
||||
entries: [
|
||||
{
|
||||
entries: [getDefaultEmptyEntry()],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { ThreatMapEntries } from './types';
|
||||
import { getDefaultEmptyEntry } from './helpers';
|
||||
|
||||
export type ViewerModalName = 'addModal' | 'editModal' | null;
|
||||
|
||||
export interface State {
|
||||
andLogicIncluded: boolean;
|
||||
entries: ThreatMapEntries[];
|
||||
entriesToDelete: ThreatMapEntries[];
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'setEntries';
|
||||
entries: ThreatMapEntries[];
|
||||
}
|
||||
| {
|
||||
type: 'setDefault';
|
||||
initialState: State;
|
||||
lastEntry: ThreatMapEntries;
|
||||
};
|
||||
|
||||
export const reducer = () => (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'setEntries': {
|
||||
const isAndLogicIncluded =
|
||||
action.entries.filter(({ entries }) => entries.length > 1).length > 0;
|
||||
|
||||
const returnState = {
|
||||
...state,
|
||||
andLogicIncluded: isAndLogicIncluded,
|
||||
entries: action.entries,
|
||||
};
|
||||
return returnState;
|
||||
}
|
||||
case 'setDefault': {
|
||||
return {
|
||||
...state,
|
||||
...action.initialState,
|
||||
entries: [{ ...action.lastEntry, entries: [getDefaultEmptyEntry()] }],
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FIELD = i18n.translate('xpack.securitySolution.threatMatch.fieldDescription', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
|
||||
export const THREAT_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.threatMatch.threatFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Threat index field',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.threatMatch.fieldPlaceholderDescription',
|
||||
{
|
||||
defaultMessage: 'Search',
|
||||
}
|
||||
);
|
||||
|
||||
export const MATCHES = i18n.translate('xpack.securitySolution.threatMatch.matchesLabel', {
|
||||
defaultMessage: 'MATCHES',
|
||||
});
|
||||
|
||||
export const AND = i18n.translate('xpack.securitySolution.threatMatch.andDescription', {
|
||||
defaultMessage: 'AND',
|
||||
});
|
||||
|
||||
export const OR = i18n.translate('xpack.securitySolution.threatMatch.orDescription', {
|
||||
defaultMessage: 'OR',
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { ThreatMap, ThreatMapEntry } from '../../../../common/detection_engine/schemas/types';
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
export interface FormattedEntry {
|
||||
field: IFieldType | undefined;
|
||||
type: 'mapping';
|
||||
value: IFieldType | undefined;
|
||||
entryIndex: number;
|
||||
}
|
||||
|
||||
export interface EmptyEntry {
|
||||
field: string | undefined;
|
||||
type: 'mapping';
|
||||
value: string | undefined;
|
||||
}
|
||||
|
||||
export type Entry = ThreatMapEntry | EmptyEntry;
|
||||
|
||||
export type ThreatMapEntries = Omit<ThreatMap, 'entries'> & {
|
||||
entries: Entry[];
|
||||
};
|
|
@ -21,6 +21,8 @@ import { isEmpty } from 'lodash/fp';
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations';
|
||||
import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types';
|
||||
import { assertUnreachable } from '../../../../../common/utility_types';
|
||||
import * as i18nSeverity from '../severity_mapping/translations';
|
||||
import * as i18nRiskScore from '../risk_score_mapping/translations';
|
||||
|
@ -56,6 +58,7 @@ export const buildQueryBarDescription = ({
|
|||
query,
|
||||
savedId,
|
||||
indexPatterns,
|
||||
queryLabel,
|
||||
}: BuildQueryBarDescription): ListItems[] => {
|
||||
let items: ListItems[] = [];
|
||||
if (!isEmpty(filters)) {
|
||||
|
@ -89,7 +92,7 @@ export const buildQueryBarDescription = ({
|
|||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.QUERY_LABEL} </>,
|
||||
title: <>{queryLabel ?? i18n.QUERY_LABEL} </>,
|
||||
description: <>{query} </>,
|
||||
},
|
||||
];
|
||||
|
@ -416,3 +419,40 @@ export const buildThresholdDescription = (label: string, threshold: Threshold):
|
|||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const buildThreatMappingDescription = (
|
||||
title: string,
|
||||
threatMapping: ThreatMapping
|
||||
): ListItems[] => {
|
||||
const description = threatMapping.reduce<string>(
|
||||
(accumThreatMaps, threatMap, threatMapIndex, { length: threatMappingLength }) => {
|
||||
const matches = threatMap.entries.reduce<string>(
|
||||
(accumItems, item, itemsIndex, { length: threatMapLength }) => {
|
||||
if (threatMapLength === 1) {
|
||||
return `${item.field} ${MATCHES} ${item.value}`;
|
||||
} else if (itemsIndex === 0) {
|
||||
return `(${item.field} ${MATCHES} ${item.value})`;
|
||||
} else {
|
||||
return `${accumItems} ${AND} (${item.field} ${MATCHES} ${item.value})`;
|
||||
}
|
||||
},
|
||||
''
|
||||
);
|
||||
|
||||
if (threatMappingLength === 1) {
|
||||
return `${matches}`;
|
||||
} else if (threatMapIndex === 0) {
|
||||
return `(${matches})`;
|
||||
} else {
|
||||
return `${accumThreatMaps} ${OR} (${matches})`;
|
||||
}
|
||||
},
|
||||
''
|
||||
);
|
||||
return [
|
||||
{
|
||||
title,
|
||||
description,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp';
|
|||
import React, { memo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types';
|
||||
import {
|
||||
IIndexPattern,
|
||||
Filter,
|
||||
|
@ -36,11 +37,13 @@ import {
|
|||
buildRiskScoreDescription,
|
||||
buildRuleTypeDescription,
|
||||
buildThresholdDescription,
|
||||
buildThreatMappingDescription,
|
||||
} from './helpers';
|
||||
import { buildMlJobDescription } from './ml_job_description';
|
||||
import { buildActionsDescription } from './actions_description';
|
||||
import { buildThrottleDescription } from './throttle_description';
|
||||
import { Type } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { THREAT_QUERY_LABEL } from './translations';
|
||||
|
||||
const DescriptionListContainer = styled(EuiDescriptionList)`
|
||||
&.euiDescriptionList--column .euiDescriptionList__title {
|
||||
|
@ -156,6 +159,7 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => {
|
|||
});
|
||||
};
|
||||
|
||||
/* eslint complexity: ["error", 21]*/
|
||||
export const getDescriptionItem = (
|
||||
field: string,
|
||||
label: string,
|
||||
|
@ -189,7 +193,7 @@ export const getDescriptionItem = (
|
|||
} else if (field === 'falsePositives') {
|
||||
const values: string[] = get(field, data);
|
||||
return buildUnorderedListArrayDescription(label, field, values);
|
||||
} else if (Array.isArray(get(field, data))) {
|
||||
} else if (Array.isArray(get(field, data)) && field !== 'threatMapping') {
|
||||
const values: string[] = get(field, data);
|
||||
return buildStringArrayDescription(label, field, values);
|
||||
} else if (field === 'riskScore') {
|
||||
|
@ -214,6 +218,22 @@ export const getDescriptionItem = (
|
|||
return buildRuleTypeDescription(label, ruleType);
|
||||
} else if (field === 'kibanaSiemAppUrl') {
|
||||
return [];
|
||||
} else if (field === 'threatQueryBar') {
|
||||
const filters = addFilterStateIfNotThere(get('threatQueryBar.filters', data) ?? []);
|
||||
const query = get('threatQueryBar.query.query', data);
|
||||
const savedId = get('threatQueryBar.saved_id', data);
|
||||
return buildQueryBarDescription({
|
||||
field,
|
||||
filters,
|
||||
filterManager,
|
||||
query,
|
||||
savedId,
|
||||
indexPatterns,
|
||||
queryLabel: THREAT_QUERY_LABEL,
|
||||
});
|
||||
} else if (field === 'threatMapping') {
|
||||
const threatMap: ThreatMapping = get(field, data);
|
||||
return buildThreatMappingDescription(label, threatMap);
|
||||
}
|
||||
|
||||
const description: string = get(field, data);
|
||||
|
|
|
@ -20,6 +20,13 @@ export const QUERY_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const THREAT_QUERY_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.threatQueryLabel',
|
||||
{
|
||||
defaultMessage: 'Threat query',
|
||||
}
|
||||
);
|
||||
|
||||
export const SAVED_ID_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.savedIdLabel',
|
||||
{
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface BuildQueryBarDescription {
|
|||
query: string;
|
||||
savedId: string;
|
||||
indexPatterns?: IIndexPattern;
|
||||
queryLabel?: string;
|
||||
}
|
||||
|
||||
export interface BuildThreatDescription {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
isThresholdRule,
|
||||
isEqlRule,
|
||||
isQueryRule,
|
||||
isThreatMatchRule,
|
||||
} from '../../../../../common/detection_engine/utils';
|
||||
import { FieldHook } from '../../../../shared_imports';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -45,6 +46,7 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
|
|||
const setMl = useCallback(() => setType('machine_learning'), [setType]);
|
||||
const setQuery = useCallback(() => setType('query'), [setType]);
|
||||
const setThreshold = useCallback(() => setType('threshold'), [setType]);
|
||||
const setThreatMatch = useCallback(() => setType('threat_match'), [setType]);
|
||||
const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin;
|
||||
const licensingUrl = useKibana().services.application.getUrlForApp('kibana', {
|
||||
path: '#/management/stack/license_management',
|
||||
|
@ -86,6 +88,15 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
|
|||
[isReadOnly, ruleType, setThreshold]
|
||||
);
|
||||
|
||||
const threatMatchSelectableConfig = useMemo(
|
||||
() => ({
|
||||
isDisabled: isReadOnly,
|
||||
onClick: setThreatMatch,
|
||||
isSelected: isThreatMatchRule(ruleType),
|
||||
}),
|
||||
[isReadOnly, ruleType, setThreatMatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
|
@ -138,6 +149,18 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
|
|||
selectable={eqlSelectableConfig}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
data-test-subj="threatMatchRuleType"
|
||||
title={i18n.THREAT_MATCH_TYPE_TITLE}
|
||||
description={i18n.THREAT_MATCH_TYPE_DESCRIPTION}
|
||||
icon={<EuiIcon size="l" type="list" />}
|
||||
isDisabled={
|
||||
threatMatchSelectableConfig.isDisabled && !threatMatchSelectableConfig.isSelected
|
||||
}
|
||||
selectable={threatMatchSelectableConfig}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -62,3 +62,17 @@ export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate(
|
|||
defaultMessage: 'Aggregate query results to detect when number of matches exceeds threshold.',
|
||||
}
|
||||
);
|
||||
|
||||
export const THREAT_MATCH_TYPE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle',
|
||||
{
|
||||
defaultMessage: 'Threat Match',
|
||||
}
|
||||
);
|
||||
|
||||
export const THREAT_MATCH_TYPE_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchDescription',
|
||||
{
|
||||
defaultMessage: 'Upload value lists to write rules around a list of known bad attributes',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import styled from 'styled-components';
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import { IndexPattern } from 'src/plugins/data/public';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
|
@ -47,8 +48,13 @@ import {
|
|||
} from '../../../../shared_imports';
|
||||
import { schema } from './schema';
|
||||
import * as i18n from './translations';
|
||||
import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils';
|
||||
import {
|
||||
isEqlRule,
|
||||
isThreatMatchRule,
|
||||
isThresholdRule,
|
||||
} from '../../../../../common/detection_engine/utils';
|
||||
import { EqlQueryBar } from '../eql_query_bar';
|
||||
import { ThreatMatchInput } from '../threatmatch_input';
|
||||
import { useFetchIndex } from '../../../../common/containers/source';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
@ -62,11 +68,18 @@ const stepDefineDefaultValue: DefineStepRule = {
|
|||
index: [],
|
||||
machineLearningJobId: '',
|
||||
ruleType: 'query',
|
||||
threatIndex: [],
|
||||
queryBar: {
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [],
|
||||
saved_id: undefined,
|
||||
},
|
||||
threatQueryBar: {
|
||||
query: { query: '*:*', language: 'kuery' },
|
||||
filters: [],
|
||||
saved_id: undefined,
|
||||
},
|
||||
threatMapping: [],
|
||||
threshold: {
|
||||
field: [],
|
||||
value: '200',
|
||||
|
@ -121,14 +134,22 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
schema,
|
||||
});
|
||||
const { getFields, getFormData, reset, submit } = form;
|
||||
const [{ index: formIndex, ruleType: formRuleType }] = (useFormData({
|
||||
form,
|
||||
watch: ['index', 'ruleType'],
|
||||
}) as unknown) as [Partial<DefineStepRule>];
|
||||
const [{ index: formIndex, ruleType: formRuleType, threatIndex: formThreatIndex }] = (useFormData(
|
||||
{
|
||||
form,
|
||||
watch: ['index', 'ruleType', 'threatIndex'],
|
||||
}
|
||||
) as unknown) as [Partial<DefineStepRule>];
|
||||
const index = formIndex || initialState.index;
|
||||
const threatIndex = formThreatIndex || initialState.threatIndex;
|
||||
const ruleType = formRuleType || initialState.ruleType;
|
||||
const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index);
|
||||
|
||||
const [
|
||||
threatIndexPatternsLoading,
|
||||
{ browserFields: threatBrowserFields, indexPatterns: threatIndexPatterns },
|
||||
] = useFetchIndex(threatIndex);
|
||||
|
||||
// reset form when rule type changes
|
||||
useEffect(() => {
|
||||
reset({ resetValues: false });
|
||||
|
@ -146,7 +167,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
|
||||
const getData = useCallback(async () => {
|
||||
const result = await submit();
|
||||
return result?.isValid
|
||||
return result.isValid
|
||||
? result
|
||||
: {
|
||||
isValid: false,
|
||||
|
@ -184,6 +205,19 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
[browserFields]
|
||||
);
|
||||
|
||||
const ThreatMatchInputChildren = useCallback(
|
||||
({ threatMapping }) => (
|
||||
<ThreatMatchInput
|
||||
threatBrowserFields={threatBrowserFields}
|
||||
indexPatterns={indexPatterns as IndexPattern}
|
||||
threatIndexPatterns={threatIndexPatterns as IndexPattern}
|
||||
threatMapping={threatMapping}
|
||||
threatIndexPatternsLoading={threatIndexPatternsLoading}
|
||||
/>
|
||||
),
|
||||
[threatBrowserFields, threatIndexPatternsLoading, threatIndexPatterns, indexPatterns]
|
||||
);
|
||||
|
||||
return isReadOnlyView ? (
|
||||
<StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}>
|
||||
<StepRuleDescription
|
||||
|
@ -309,6 +343,23 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
</UseMultiFields>
|
||||
</>
|
||||
</RuleTypeEuiFormRow>
|
||||
<RuleTypeEuiFormRow
|
||||
$isVisible={isThreatMatchRule(ruleType)}
|
||||
data-test-subj="threatMatchInput"
|
||||
fullWidth
|
||||
>
|
||||
<>
|
||||
<UseMultiFields
|
||||
fields={{
|
||||
threatMapping: {
|
||||
path: 'threatMapping',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{ThreatMatchInputChildren}
|
||||
</UseMultiFields>
|
||||
</>
|
||||
</RuleTypeEuiFormRow>
|
||||
<UseField
|
||||
path="timeline"
|
||||
component={PickTimeline}
|
||||
|
|
|
@ -9,6 +9,11 @@ import { EuiText } from '@elastic/eui';
|
|||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
singleEntryThreat,
|
||||
containsInvalidItems,
|
||||
} from '../../../../common/components/threat_match/helpers';
|
||||
import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
import { esKuery } from '../../../../../../../../src/plugins/data/public';
|
||||
import { FieldValueQueryBar } from '../query_bar';
|
||||
|
@ -20,7 +25,14 @@ import {
|
|||
ValidationFunc,
|
||||
} from '../../../../shared_imports';
|
||||
import { DefineStepRule } from '../../../pages/detection_engine/rules/types';
|
||||
import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations';
|
||||
import {
|
||||
CUSTOM_QUERY_REQUIRED,
|
||||
INVALID_CUSTOM_QUERY,
|
||||
INDEX_HELPER_TEXT,
|
||||
THREAT_MATCH_INDEX_HELPER_TEXT,
|
||||
THREAT_MATCH_REQUIRED,
|
||||
THREAT_MATCH_EMPTIES,
|
||||
} from './translations';
|
||||
|
||||
export const schema: FormSchema<DefineStepRule> = {
|
||||
index: {
|
||||
|
@ -219,4 +231,126 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
],
|
||||
},
|
||||
},
|
||||
threatIndex: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel',
|
||||
{
|
||||
defaultMessage: 'Threat index patterns',
|
||||
}
|
||||
),
|
||||
helpText: <EuiText size="xs">{THREAT_MATCH_INDEX_HELPER_TEXT}</EuiText>,
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ formData }] = args;
|
||||
const needsValidation = isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
return fieldValidators.emptyField(
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A minimum of one index pattern is required.',
|
||||
}
|
||||
)
|
||||
)(...args);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
threatMapping: {
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel',
|
||||
{
|
||||
defaultMessage: 'Threat Mapping',
|
||||
}
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ path, formData }] = args;
|
||||
const needsValidation = isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
if (singleEntryThreat(formData.threatMapping)) {
|
||||
return {
|
||||
code: 'ERR_FIELD_MISSING',
|
||||
path,
|
||||
message: THREAT_MATCH_REQUIRED,
|
||||
};
|
||||
} else if (containsInvalidItems(formData.threatMapping)) {
|
||||
return {
|
||||
code: 'ERR_FIELD_MISSING',
|
||||
path,
|
||||
message: THREAT_MATCH_EMPTIES,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
threatQueryBar: {
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel',
|
||||
{
|
||||
defaultMessage: 'Threat index query',
|
||||
}
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path, formData }] = args;
|
||||
const needsValidation = isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { query, filters } = value as FieldValueQueryBar;
|
||||
|
||||
return isEmpty(query.query as string) && isEmpty(filters)
|
||||
? {
|
||||
code: 'ERR_FIELD_MISSING',
|
||||
path,
|
||||
message: CUSTOM_QUERY_REQUIRED,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path, formData }] = args;
|
||||
const needsValidation = isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
const { query } = value as FieldValueQueryBar;
|
||||
|
||||
if (!isEmpty(query.query as string) && query.language === 'kuery') {
|
||||
try {
|
||||
esKuery.fromKueryExpression(query.query);
|
||||
} catch (err) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path,
|
||||
message: INVALID_CUSTOM_QUERY,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -70,3 +70,24 @@ export const ENABLE_ML_JOB_WARNING = i18n.translate(
|
|||
'This ML job is not currently running. Please set this job to run via "ML job settings" before activating this rule.',
|
||||
}
|
||||
);
|
||||
|
||||
export const THREAT_MATCH_INDEX_HELPER_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription',
|
||||
{
|
||||
defaultMessage: 'Select threat indices',
|
||||
}
|
||||
);
|
||||
|
||||
export const THREAT_MATCH_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'At least one threat match is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const THREAT_MATCH_EMPTIES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError',
|
||||
{
|
||||
defaultMessage: 'All matches require both a field and threat index field.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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, EuiSpacer, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { ThreatMapEntries } from '../../../../common/components/threat_match/types';
|
||||
import { ThreatMatchComponent } from '../../../../common/components/threat_match';
|
||||
import { BrowserField } from '../../../../common/containers/source';
|
||||
import {
|
||||
FieldHook,
|
||||
Field,
|
||||
getUseField,
|
||||
UseField,
|
||||
getFieldValidityAndErrorMessage,
|
||||
} from '../../../../shared_imports';
|
||||
import { schema } from '../step_define_rule/schema';
|
||||
import { QueryBarDefineRule } from '../query_bar';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface ThreatMatchInputProps {
|
||||
threatMapping: FieldHook;
|
||||
threatBrowserFields: Readonly<Record<string, Partial<BrowserField>>>;
|
||||
threatIndexPatterns: IndexPattern;
|
||||
indexPatterns: IndexPattern;
|
||||
threatIndexPatternsLoading: boolean;
|
||||
}
|
||||
|
||||
const ThreatMatchInputComponent: React.FC<ThreatMatchInputProps> = ({
|
||||
threatMapping,
|
||||
indexPatterns,
|
||||
threatIndexPatterns,
|
||||
threatIndexPatternsLoading,
|
||||
threatBrowserFields,
|
||||
}: ThreatMatchInputProps) => {
|
||||
const { setValue, value: threatItems } = threatMapping;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(threatMapping);
|
||||
const handleBuilderOnChange = useCallback(
|
||||
({ entryItems }: { entryItems: ThreatMapEntries[] }): void => {
|
||||
setValue(entryItems);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={true}>
|
||||
<CommonUseField
|
||||
path="threatIndex"
|
||||
config={{
|
||||
...schema.threatIndex,
|
||||
labelAppend: null,
|
||||
}}
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepDefineRuleThreatMatchIndices',
|
||||
'data-test-subj': 'detectionEngineStepDefineRuleThreatMatchIndices',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
isDisabled: false,
|
||||
placeholder: '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<UseField
|
||||
path="threatQueryBar"
|
||||
config={{
|
||||
...schema.threatQueryBar,
|
||||
labelAppend: null,
|
||||
}}
|
||||
component={QueryBarDefineRule}
|
||||
componentProps={{
|
||||
browserFields: threatBrowserFields,
|
||||
idAria: 'detectionEngineStepDefineThreatRuleQueryBar',
|
||||
indexPattern: threatIndexPatterns,
|
||||
isDisabled: false,
|
||||
isLoading: threatIndexPatternsLoading,
|
||||
dataTestSubj: 'detectionEngineStepDefineThreatRuleQueryBar',
|
||||
openTimelineSearch: false,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
label={threatMapping.label}
|
||||
labelAppend={threatMapping.labelAppend}
|
||||
helpText={threatMapping.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<ThreatMatchComponent
|
||||
listItems={threatItems as ThreatMapEntries[]}
|
||||
indexPatterns={indexPatterns}
|
||||
threatIndexPatterns={threatIndexPatterns}
|
||||
data-test-subj="threatmatch-builder"
|
||||
id-aria="threatmatch-builder"
|
||||
onChange={handleBuilderOnChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreatMatchInput = React.memo(ThreatMatchInputComponent);
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const THREAT_MATCH_FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchField.threatMatchFieldPlaceholderText',
|
||||
{
|
||||
defaultMessage: 'All results',
|
||||
}
|
||||
);
|
|
@ -18,7 +18,14 @@ import {
|
|||
threshold,
|
||||
type,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { listArray } from '../../../../../common/detection_engine/schemas/types';
|
||||
import {
|
||||
listArray,
|
||||
threat_query,
|
||||
threat_index,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
threat_filters,
|
||||
} from '../../../../../common/detection_engine/schemas/types';
|
||||
import {
|
||||
CreateRulesSchema,
|
||||
PatchRulesSchema,
|
||||
|
@ -110,6 +117,11 @@ export const RuleSchema = t.intersection([
|
|||
status: t.string,
|
||||
status_date: t.string,
|
||||
threshold,
|
||||
threat_query,
|
||||
threat_filters,
|
||||
threat_index,
|
||||
threat_mapping,
|
||||
threat_language,
|
||||
timeline_id: t.string,
|
||||
timeline_title: t.string,
|
||||
timestamp_override,
|
||||
|
|
|
@ -212,10 +212,13 @@ export const mockDefineStepRule = (): DefineStepRule => ({
|
|||
machineLearningJobId: '',
|
||||
index: ['filebeat-'],
|
||||
queryBar: mockQueryBar,
|
||||
threatQueryBar: mockQueryBar,
|
||||
threatMapping: [],
|
||||
timeline: {
|
||||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Titled timeline',
|
||||
},
|
||||
threatIndex: [],
|
||||
threshold: {
|
||||
field: [''],
|
||||
value: '100',
|
||||
|
|
|
@ -73,28 +73,77 @@ export interface RuleFields {
|
|||
index: unknown;
|
||||
ruleType: unknown;
|
||||
threshold?: unknown;
|
||||
threatIndex?: unknown;
|
||||
threatQueryBar?: unknown;
|
||||
threatMapping?: unknown;
|
||||
threatLanguage?: unknown;
|
||||
}
|
||||
type QueryRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId' | 'threshold'>;
|
||||
type ThresholdRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId'>;
|
||||
type MlRuleFields<T> = Omit<T, 'queryBar' | 'index' | 'threshold'>;
|
||||
|
||||
type QueryRuleFields<T> = Omit<
|
||||
T,
|
||||
| 'anomalyThreshold'
|
||||
| 'machineLearningJobId'
|
||||
| 'threshold'
|
||||
| 'threatIndex'
|
||||
| 'threatQueryBar'
|
||||
| 'threatMapping'
|
||||
>;
|
||||
type ThresholdRuleFields<T> = Omit<
|
||||
T,
|
||||
'anomalyThreshold' | 'machineLearningJobId' | 'threatIndex' | 'threatQueryBar' | 'threatMapping'
|
||||
>;
|
||||
type MlRuleFields<T> = Omit<
|
||||
T,
|
||||
'queryBar' | 'index' | 'threshold' | 'threatIndex' | 'threatQueryBar' | 'threatMapping'
|
||||
>;
|
||||
type ThreatMatchRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId' | 'threshold'>;
|
||||
|
||||
const isMlFields = <T>(
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T>
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T>
|
||||
): fields is MlRuleFields<T> => has('anomalyThreshold', fields);
|
||||
|
||||
const isThresholdFields = <T>(
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T>
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T>
|
||||
): fields is ThresholdRuleFields<T> => has('threshold', fields);
|
||||
|
||||
export const filterRuleFieldsForType = <T extends RuleFields>(fields: T, type: Type) => {
|
||||
const isThreatMatchFields = <T>(
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T>
|
||||
): fields is ThreatMatchRuleFields<T> => has('threatIndex', fields);
|
||||
|
||||
export const filterRuleFieldsForType = <T extends RuleFields>(
|
||||
fields: T,
|
||||
type: Type
|
||||
): QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T> => {
|
||||
switch (type) {
|
||||
case 'machine_learning':
|
||||
const { index, queryBar, threshold, ...mlRuleFields } = fields;
|
||||
const {
|
||||
index,
|
||||
queryBar,
|
||||
threshold,
|
||||
threatIndex,
|
||||
threatQueryBar,
|
||||
threatMapping,
|
||||
...mlRuleFields
|
||||
} = fields;
|
||||
return mlRuleFields;
|
||||
case 'threshold':
|
||||
const { anomalyThreshold, machineLearningJobId, ...thresholdRuleFields } = fields;
|
||||
const {
|
||||
anomalyThreshold,
|
||||
machineLearningJobId,
|
||||
threatIndex: _removedThreatIndex,
|
||||
threatQueryBar: _removedThreatQueryBar,
|
||||
threatMapping: _removedThreatMapping,
|
||||
...thresholdRuleFields
|
||||
} = fields;
|
||||
return thresholdRuleFields;
|
||||
case 'threat_match':
|
||||
const {
|
||||
anomalyThreshold: _removedAnomalyThreshold,
|
||||
machineLearningJobId: _removedMachineLearningJobId,
|
||||
threshold: _removedThreshold,
|
||||
...threatMatchRuleFields
|
||||
} = fields;
|
||||
return threatMatchRuleFields;
|
||||
case 'query':
|
||||
case 'saved_query':
|
||||
case 'eql':
|
||||
|
@ -102,6 +151,9 @@ export const filterRuleFieldsForType = <T extends RuleFields>(fields: T, type: T
|
|||
anomalyThreshold: _a,
|
||||
machineLearningJobId: _m,
|
||||
threshold: _t,
|
||||
threatIndex: __removedThreatIndex,
|
||||
threatQueryBar: __removedThreatQueryBar,
|
||||
threatMapping: __removedThreatMapping,
|
||||
...queryRuleFields
|
||||
} = fields;
|
||||
return queryRuleFields;
|
||||
|
@ -140,6 +192,18 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep
|
|||
},
|
||||
}),
|
||||
}
|
||||
: isThreatMatchFields(ruleFields)
|
||||
? {
|
||||
index: ruleFields.index,
|
||||
filters: ruleFields.queryBar?.filters,
|
||||
language: ruleFields.queryBar?.query?.language,
|
||||
query: ruleFields.queryBar?.query?.query as string,
|
||||
saved_id: ruleFields.queryBar?.saved_id,
|
||||
threat_index: ruleFields.threatIndex,
|
||||
threat_query: ruleFields.threatQueryBar?.query?.query as string,
|
||||
threat_mapping: ruleFields.threatMapping,
|
||||
threat_language: ruleFields.threatQueryBar?.query?.language,
|
||||
}
|
||||
: {
|
||||
index: ruleFields.index,
|
||||
filters: ruleFields.queryBar?.filters,
|
||||
|
|
|
@ -183,10 +183,10 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
if (nextStep != null) {
|
||||
goToStep(nextStep);
|
||||
} else {
|
||||
const defineStep = await stepsData.current[RuleStep.defineRule];
|
||||
const aboutStep = await stepsData.current[RuleStep.aboutRule];
|
||||
const scheduleStep = await stepsData.current[RuleStep.scheduleRule];
|
||||
const actionsStep = await stepsData.current[RuleStep.ruleActions];
|
||||
const defineStep = stepsData.current[RuleStep.defineRule];
|
||||
const aboutStep = stepsData.current[RuleStep.aboutRule];
|
||||
const scheduleStep = stepsData.current[RuleStep.scheduleRule];
|
||||
const actionsStep = stepsData.current[RuleStep.ruleActions];
|
||||
|
||||
if (
|
||||
stepIsValid(defineStep) &&
|
||||
|
|
|
@ -82,6 +82,16 @@ describe('rule helpers', () => {
|
|||
field: ['host.name'],
|
||||
value: '50',
|
||||
},
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
threatQueryBar: {
|
||||
query: {
|
||||
query: '',
|
||||
language: '',
|
||||
},
|
||||
filters: [],
|
||||
saved_id: undefined,
|
||||
},
|
||||
timeline: {
|
||||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Titled timeline',
|
||||
|
@ -217,6 +227,16 @@ describe('rule helpers', () => {
|
|||
field: [],
|
||||
value: '100',
|
||||
},
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
threatQueryBar: {
|
||||
query: {
|
||||
query: '',
|
||||
language: '',
|
||||
},
|
||||
filters: [],
|
||||
saved_id: undefined,
|
||||
},
|
||||
timeline: {
|
||||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Untitled timeline',
|
||||
|
@ -249,6 +269,16 @@ describe('rule helpers', () => {
|
|||
field: [],
|
||||
value: '100',
|
||||
},
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
threatQueryBar: {
|
||||
query: {
|
||||
query: '',
|
||||
language: '',
|
||||
},
|
||||
filters: [],
|
||||
saved_id: undefined,
|
||||
},
|
||||
timeline: {
|
||||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Untitled timeline',
|
||||
|
|
|
@ -79,6 +79,13 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({
|
|||
anomalyThreshold: rule.anomaly_threshold ?? 50,
|
||||
machineLearningJobId: rule.machine_learning_job_id ?? '',
|
||||
index: rule.index ?? [],
|
||||
threatIndex: rule.threat_index ?? [],
|
||||
threatQueryBar: {
|
||||
query: { query: rule.threat_query ?? '', language: rule.threat_language ?? '' },
|
||||
filters: (rule.threat_filters ?? []) as Filter[],
|
||||
saved_id: undefined,
|
||||
},
|
||||
threatMapping: rule.threat_mapping ?? [],
|
||||
queryBar: {
|
||||
query: { query: rule.query ?? '', language: rule.language ?? '' },
|
||||
filters: (rule.filters ?? []) as Filter[],
|
||||
|
@ -341,7 +348,6 @@ export const getActionMessageRuleParams = (ruleType: Type): string[] => {
|
|||
'threat',
|
||||
'type',
|
||||
'version',
|
||||
// 'lists',
|
||||
];
|
||||
|
||||
const ruleParamsKeys = [
|
||||
|
|
|
@ -22,7 +22,11 @@ import {
|
|||
Type,
|
||||
Severity,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { List } from '../../../../../common/detection_engine/schemas/types';
|
||||
import {
|
||||
List,
|
||||
ThreatIndex,
|
||||
ThreatMapping,
|
||||
} from '../../../../../common/detection_engine/schemas/types';
|
||||
|
||||
export interface EuiBasicTableSortTypes {
|
||||
field: string;
|
||||
|
@ -124,6 +128,9 @@ export interface DefineStepRule {
|
|||
ruleType: Type;
|
||||
timeline: FieldValueTimeline;
|
||||
threshold: FieldValueThreshold;
|
||||
threatIndex: ThreatIndex;
|
||||
threatQueryBar: FieldValueQueryBar;
|
||||
threatMapping: ThreatMapping;
|
||||
}
|
||||
|
||||
export interface ScheduleStepRule {
|
||||
|
|
|
@ -399,6 +399,7 @@ export const getResult = (): RuleAlertType => ({
|
|||
timestampOverride: undefined,
|
||||
threatFilters: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
references: ['http://www.example.com', 'https://ww.example.com'],
|
||||
|
|
|
@ -96,6 +96,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
|
|||
threat_index: threatIndex,
|
||||
threat_mapping: threatMapping,
|
||||
threat_query: threatQuery,
|
||||
threat_language: threatLanguage,
|
||||
threshold,
|
||||
throttle,
|
||||
timestamp_override: timestampOverride,
|
||||
|
@ -186,6 +187,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
|
|||
threatMapping,
|
||||
threatQuery,
|
||||
threatIndex,
|
||||
threatLanguage,
|
||||
threshold,
|
||||
timestampOverride,
|
||||
references,
|
||||
|
|
|
@ -84,6 +84,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
|
|||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
throttle,
|
||||
timestamp_override: timestampOverride,
|
||||
to,
|
||||
|
@ -175,6 +176,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
|
|||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
|
|
@ -163,6 +163,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
|
|||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
threshold,
|
||||
timestamp_override: timestampOverride,
|
||||
to,
|
||||
|
@ -223,11 +224,12 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
|
|||
to,
|
||||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threshold,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
@ -272,6 +274,11 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
references,
|
||||
note,
|
||||
version,
|
||||
|
|
|
@ -87,6 +87,11 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
timestamp_override: timestampOverride,
|
||||
throttle,
|
||||
references,
|
||||
|
@ -147,6 +152,11 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
|
|
@ -78,6 +78,11 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
timestamp_override: timestampOverride,
|
||||
throttle,
|
||||
references,
|
||||
|
@ -146,6 +151,11 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
|
|
@ -91,6 +91,11 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
throttle,
|
||||
timestamp_override: timestampOverride,
|
||||
references,
|
||||
|
@ -158,6 +163,11 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
|
|
@ -81,6 +81,11 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
throttle,
|
||||
timestamp_override: timestampOverride,
|
||||
references,
|
||||
|
@ -148,6 +153,11 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
|
|
@ -156,7 +156,7 @@ describe('utils', () => {
|
|||
],
|
||||
},
|
||||
];
|
||||
threatRule.params.threatIndex = 'index-123';
|
||||
threatRule.params.threatIndex = ['index-123'];
|
||||
threatRule.params.threatFilters = threatFilters;
|
||||
threatRule.params.threatMapping = threatMapping;
|
||||
threatRule.params.threatQuery = '*:*';
|
||||
|
@ -164,7 +164,7 @@ describe('utils', () => {
|
|||
const rule = transformAlertToRule(threatRule);
|
||||
expect(rule).toEqual(
|
||||
expect.objectContaining({
|
||||
threat_index: 'index-123',
|
||||
threat_index: ['index-123'],
|
||||
threat_filters: threatFilters,
|
||||
threat_mapping: threatMapping,
|
||||
threat_query: '*:*',
|
||||
|
|
|
@ -150,6 +150,7 @@ export const transformAlertToRule = (
|
|||
threat_index: alert.params.threatIndex,
|
||||
threat_query: alert.params.threatQuery,
|
||||
threat_mapping: alert.params.threatMapping,
|
||||
threat_language: alert.params.threatLanguage,
|
||||
throttle: ruleActions?.ruleThrottle || 'no_actions',
|
||||
timestamp_override: alert.params.timestampOverride,
|
||||
note: alert.params.note,
|
||||
|
|
|
@ -42,6 +42,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
threat: [],
|
||||
threatFilters: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
threatQuery: undefined,
|
||||
threatIndex: undefined,
|
||||
threshold: undefined,
|
||||
|
@ -92,6 +93,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
threatIndex: undefined,
|
||||
threatMapping: undefined,
|
||||
threatQuery: undefined,
|
||||
threatLanguage: undefined,
|
||||
threshold: undefined,
|
||||
timestampOverride: undefined,
|
||||
to: 'now',
|
||||
|
|
|
@ -45,6 +45,7 @@ export const createRules = async ({
|
|||
threat,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatLanguage,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threshold,
|
||||
|
@ -96,6 +97,7 @@ export const createRules = async ({
|
|||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
|
|
@ -50,6 +50,7 @@ export const installPrepackagedRules = (
|
|||
threat,
|
||||
threat_filters: threatFilters,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
threat_query: threatQuery,
|
||||
threat_index: threatIndex,
|
||||
threshold,
|
||||
|
@ -101,6 +102,7 @@ export const installPrepackagedRules = (
|
|||
threat,
|
||||
threatFilters,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
threatQuery,
|
||||
threatIndex,
|
||||
threshold,
|
||||
|
|
|
@ -149,6 +149,11 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({
|
|||
tags: [],
|
||||
threat: [],
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
timestampOverride: undefined,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
|
@ -193,6 +198,11 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({
|
|||
tags: [],
|
||||
threat: [],
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
timestampOverride: undefined,
|
||||
to: 'now',
|
||||
type: 'machine_learning',
|
||||
|
|
|
@ -44,6 +44,11 @@ export const patchRules = async ({
|
|||
tags,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
@ -87,6 +92,11 @@ export const patchRules = async ({
|
|||
tags,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
@ -126,6 +136,11 @@ export const patchRules = async ({
|
|||
severityMapping,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
|
|
@ -91,6 +91,7 @@ import {
|
|||
ThreatQueryOrUndefined,
|
||||
ThreatMappingOrUndefined,
|
||||
ThreatFiltersOrUndefined,
|
||||
ThreatLanguageOrUndefined,
|
||||
} from '../../../../common/detection_engine/schemas/types/threat_mapping';
|
||||
|
||||
import { AlertsClient, PartialAlert } from '../../../../../alerts/server';
|
||||
|
@ -219,6 +220,7 @@ export interface CreateRulesOptions {
|
|||
threatIndex: ThreatIndexOrUndefined;
|
||||
threatQuery: ThreatQueryOrUndefined;
|
||||
threatMapping: ThreatMappingOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: To;
|
||||
type: Type;
|
||||
|
@ -264,6 +266,11 @@ export interface UpdateRulesOptions {
|
|||
tags: Tags;
|
||||
threat: Threat;
|
||||
threshold: ThresholdOrUndefined;
|
||||
threatFilters: ThreatFiltersOrUndefined;
|
||||
threatIndex: ThreatIndexOrUndefined;
|
||||
threatQuery: ThreatQueryOrUndefined;
|
||||
threatMapping: ThreatMappingOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: To;
|
||||
type: Type;
|
||||
|
@ -307,6 +314,11 @@ export interface PatchRulesOptions {
|
|||
tags: TagsOrUndefined;
|
||||
threat: ThreatOrUndefined;
|
||||
threshold: ThresholdOrUndefined;
|
||||
threatFilters: ThreatFiltersOrUndefined;
|
||||
threatIndex: ThreatIndexOrUndefined;
|
||||
threatQuery: ThreatQueryOrUndefined;
|
||||
threatMapping: ThreatMappingOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: ToOrUndefined;
|
||||
type: TypeOrUndefined;
|
||||
|
|
|
@ -47,6 +47,11 @@ export const updatePrepackagedRules = async (
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
timestamp_override: timestampOverride,
|
||||
references,
|
||||
version,
|
||||
|
@ -97,6 +102,11 @@ export const updatePrepackagedRules = async (
|
|||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
references,
|
||||
version,
|
||||
note,
|
||||
|
|
|
@ -43,6 +43,11 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({
|
|||
tags: [],
|
||||
threat: [],
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
timestampOverride: undefined,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
|
@ -88,6 +93,11 @@ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({
|
|||
tags: [],
|
||||
threat: [],
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
timestampOverride: undefined,
|
||||
to: 'now',
|
||||
type: 'machine_learning',
|
||||
|
|
|
@ -45,6 +45,11 @@ export const updateRules = async ({
|
|||
tags,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
@ -89,6 +94,11 @@ export const updateRules = async ({
|
|||
tags,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
@ -134,6 +144,11 @@ export const updateRules = async ({
|
|||
severityMapping,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
timestampOverride,
|
||||
to,
|
||||
type,
|
||||
|
|
|
@ -55,6 +55,11 @@ describe('utils', () => {
|
|||
tags: undefined,
|
||||
threat: undefined,
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
to: undefined,
|
||||
timestampOverride: undefined,
|
||||
type: undefined,
|
||||
|
@ -98,6 +103,11 @@ describe('utils', () => {
|
|||
tags: undefined,
|
||||
threat: undefined,
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
to: undefined,
|
||||
timestampOverride: undefined,
|
||||
type: undefined,
|
||||
|
@ -141,6 +151,11 @@ describe('utils', () => {
|
|||
tags: undefined,
|
||||
threat: undefined,
|
||||
threshold: undefined,
|
||||
threatFilters: undefined,
|
||||
threatIndex: undefined,
|
||||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
to: undefined,
|
||||
timestampOverride: undefined,
|
||||
type: undefined,
|
||||
|
|
|
@ -42,7 +42,14 @@ import {
|
|||
EventCategoryOverrideOrUndefined,
|
||||
} from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { PartialFilter } from '../types';
|
||||
import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types';
|
||||
import {
|
||||
ListArrayOrUndefined,
|
||||
ThreatFiltersOrUndefined,
|
||||
ThreatIndexOrUndefined,
|
||||
ThreatLanguageOrUndefined,
|
||||
ThreatMappingOrUndefined,
|
||||
ThreatQueryOrUndefined,
|
||||
} from '../../../../common/detection_engine/schemas/types';
|
||||
|
||||
export const calculateInterval = (
|
||||
interval: string | undefined,
|
||||
|
@ -86,6 +93,11 @@ export interface UpdateProperties {
|
|||
tags: TagsOrUndefined;
|
||||
threat: ThreatOrUndefined;
|
||||
threshold: ThresholdOrUndefined;
|
||||
threatFilters: ThreatFiltersOrUndefined;
|
||||
threatIndex: ThreatIndexOrUndefined;
|
||||
threatQuery: ThreatQueryOrUndefined;
|
||||
threatMapping: ThreatMappingOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: ToOrUndefined;
|
||||
type: TypeOrUndefined;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"type": "threat_match",
|
||||
"query": "*:*",
|
||||
"tags": ["tag_1", "tag_2"],
|
||||
"threat_index": "mock-threat-list",
|
||||
"threat_index": ["mock-threat-list"],
|
||||
"threat_query": "*:*",
|
||||
"threat_mapping": [
|
||||
{
|
||||
|
|
|
@ -60,6 +60,7 @@ export const sampleRuleAlertParams = (
|
|||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatIndex: undefined,
|
||||
threatLanguage: undefined,
|
||||
timelineId: undefined,
|
||||
timelineTitle: undefined,
|
||||
timestampOverride: undefined,
|
||||
|
|
|
@ -168,6 +168,7 @@ export const buildRuleWithoutOverrides = (
|
|||
threat_index: ruleParams.threatIndex,
|
||||
threat_query: ruleParams.threatQuery,
|
||||
threat_mapping: ruleParams.threatMapping,
|
||||
threat_language: ruleParams.threatLanguage,
|
||||
};
|
||||
return removeInternalTagsFromRule(rule);
|
||||
};
|
||||
|
|
|
@ -50,9 +50,10 @@ const signalSchema = schema.object({
|
|||
exceptions_list: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.8. Once we use a migration script please remove this.
|
||||
exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
|
||||
threatFilters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
|
||||
threatIndex: schema.maybe(schema.string()),
|
||||
threatIndex: schema.maybe(schema.arrayOf(schema.string())),
|
||||
threatQuery: schema.maybe(schema.string()),
|
||||
threatMapping: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
|
||||
threatLanguage: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -112,6 +112,7 @@ export const signalRulesAlertType = ({
|
|||
threatQuery,
|
||||
threatIndex,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
type,
|
||||
exceptionsList,
|
||||
} = params;
|
||||
|
@ -389,6 +390,7 @@ export const signalRulesAlertType = ({
|
|||
throttle,
|
||||
threatFilters: threatFilters ?? [],
|
||||
threatQuery,
|
||||
threatLanguage,
|
||||
buildRuleMessage,
|
||||
threatIndex,
|
||||
});
|
||||
|
|
|
@ -45,6 +45,7 @@ export const createThreatSignal = async ({
|
|||
throttle,
|
||||
threatFilters,
|
||||
threatQuery,
|
||||
threatLanguage,
|
||||
buildRuleMessage,
|
||||
threatIndex,
|
||||
name,
|
||||
|
@ -105,8 +106,9 @@ export const createThreatSignal = async ({
|
|||
callCluster: services.callCluster,
|
||||
exceptionItems,
|
||||
query: threatQuery,
|
||||
language: threatLanguage,
|
||||
threatFilters,
|
||||
index: [threatIndex],
|
||||
index: threatIndex,
|
||||
searchAfter,
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
|
|
|
@ -41,6 +41,7 @@ export const createThreatSignals = async ({
|
|||
throttle,
|
||||
threatFilters,
|
||||
threatQuery,
|
||||
threatLanguage,
|
||||
buildRuleMessage,
|
||||
threatIndex,
|
||||
name,
|
||||
|
@ -59,7 +60,8 @@ export const createThreatSignals = async ({
|
|||
exceptionItems,
|
||||
threatFilters,
|
||||
query: threatQuery,
|
||||
index: [threatIndex],
|
||||
language: threatLanguage,
|
||||
index: threatIndex,
|
||||
searchAfter: undefined,
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
|
@ -99,6 +101,7 @@ export const createThreatSignals = async ({
|
|||
threatQuery,
|
||||
buildRuleMessage,
|
||||
threatIndex,
|
||||
threatLanguage,
|
||||
name,
|
||||
currentThreatList: threatList,
|
||||
currentResult: results,
|
||||
|
|
|
@ -21,6 +21,7 @@ export const MAX_PER_PAGE = 9000;
|
|||
export const getThreatList = async ({
|
||||
callCluster,
|
||||
query,
|
||||
language,
|
||||
index,
|
||||
perPage,
|
||||
searchAfter,
|
||||
|
@ -33,7 +34,13 @@ export const getThreatList = async ({
|
|||
if (calculatedPerPage > 10000) {
|
||||
throw new TypeError('perPage cannot exceed the size of 10000');
|
||||
}
|
||||
const queryFilter = getQueryFilter(query, 'kuery', threatFilters, index, exceptionItems);
|
||||
const queryFilter = getQueryFilter(
|
||||
query,
|
||||
language ?? 'kuery',
|
||||
threatFilters,
|
||||
index,
|
||||
exceptionItems
|
||||
);
|
||||
const response: SearchResponse<ThreatListItem> = await callCluster('search', {
|
||||
body: {
|
||||
query: queryFilter,
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
ThreatQuery,
|
||||
ThreatMapping,
|
||||
ThreatMappingEntries,
|
||||
ThreatIndex,
|
||||
ThreatLanguageOrUndefined,
|
||||
} from '../../../../../common/detection_engine/schemas/types/threat_mapping';
|
||||
import { PartialFilter, RuleTypeParams } from '../../types';
|
||||
import { AlertServices } from '../../../../../../alerts/server';
|
||||
|
@ -57,7 +59,8 @@ export interface CreateThreatSignalsOptions {
|
|||
threatFilters: PartialFilter[];
|
||||
threatQuery: ThreatQuery;
|
||||
buildRuleMessage: BuildRuleMessage;
|
||||
threatIndex: string;
|
||||
threatIndex: ThreatIndex;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
@ -93,7 +96,8 @@ export interface CreateThreatSignalOptions {
|
|||
threatFilters: PartialFilter[];
|
||||
threatQuery: ThreatQuery;
|
||||
buildRuleMessage: BuildRuleMessage;
|
||||
threatIndex: string;
|
||||
threatIndex: ThreatIndex;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
name: string;
|
||||
currentThreatList: SearchResponse<ThreatListItem>;
|
||||
currentResult: SearchAfterAndBulkCreateReturnType;
|
||||
|
@ -138,6 +142,7 @@ export interface BooleanFilter {
|
|||
export interface GetThreatListOptions {
|
||||
callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
|
||||
query: string;
|
||||
language: ThreatLanguageOrUndefined;
|
||||
index: string[];
|
||||
perPage?: number;
|
||||
searchAfter: string[] | undefined;
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
ThreatIndexOrUndefined,
|
||||
ThreatQueryOrUndefined,
|
||||
ThreatMappingOrUndefined,
|
||||
ThreatLanguageOrUndefined,
|
||||
} from '../../../common/detection_engine/schemas/types/threat_mapping';
|
||||
|
||||
import { LegacyCallAPIOptions } from '../../../../../../src/core/server';
|
||||
|
@ -85,6 +86,7 @@ export interface RuleTypeParams {
|
|||
threatIndex: ThreatIndexOrUndefined;
|
||||
threatQuery: ThreatQueryOrUndefined;
|
||||
threatMapping: ThreatMappingOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: To;
|
||||
type: Type;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue