Skip to content

Commit

Permalink
[Security Solution][Exceptions] Exception modal bulk close alerts tha…
Browse files Browse the repository at this point in the history
…t match exception attributes (#71321)

* progress on bulk close

* works but could be slow

* clean up, add tests

* fix reduce types

* address 'event.' fields

* remove duplicate import

* don't replace nested fields

* my best friend typescript
  • Loading branch information
peluja1012 committed Jul 14, 2020
1 parent c86ad7b commit f4091df
Show file tree
Hide file tree
Showing 12 changed files with 1,143 additions and 580 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
entriesMatch,
entriesNested,
ExceptionListItemSchema,
CreateExceptionListItemSchema,
} from '../shared_imports';
import { Language, Query } from './schemas/common/schemas';

Expand Down Expand Up @@ -45,32 +46,35 @@ export const getLanguageBooleanOperator = ({
export const operatorBuilder = ({
operator,
language,
exclude,
}: {
operator: Operator;
language: Language;
exclude: boolean;
}): string => {
const not = getLanguageBooleanOperator({
language,
value: 'not',
});

switch (operator) {
case 'included':
return `${not} `;
default:
return '';
if ((exclude && operator === 'included') || (!exclude && operator === 'excluded')) {
return `${not} `;
} else {
return '';
}
};

export const buildExists = ({
item,
language,
exclude,
}: {
item: EntryExists;
language: Language;
exclude: boolean;
}): string => {
const { operator, field } = item;
const exceptionOperator = operatorBuilder({ operator, language });
const exceptionOperator = operatorBuilder({ operator, language, exclude });

switch (language) {
case 'kuery':
Expand All @@ -85,22 +89,26 @@ export const buildExists = ({
export const buildMatch = ({
item,
language,
exclude,
}: {
item: EntryMatch;
language: Language;
exclude: boolean;
}): string => {
const { value, operator, field } = item;
const exceptionOperator = operatorBuilder({ operator, language });
const exceptionOperator = operatorBuilder({ operator, language, exclude });

return `${exceptionOperator}${field}:${value}`;
};

export const buildMatchAny = ({
item,
language,
exclude,
}: {
item: EntryMatchAny;
language: Language;
exclude: boolean;
}): string => {
const { value, operator, field } = item;

Expand All @@ -109,7 +117,7 @@ export const buildMatchAny = ({
return '';
default:
const or = getLanguageBooleanOperator({ language, value: 'or' });
const exceptionOperator = operatorBuilder({ operator, language });
const exceptionOperator = operatorBuilder({ operator, language, exclude });
const matchAnyValues = value.map((v) => v);

return `${exceptionOperator}${field}:(${matchAnyValues.join(` ${or} `)})`;
Expand All @@ -133,16 +141,18 @@ export const buildNested = ({
export const evaluateValues = ({
item,
language,
exclude,
}: {
item: Entry | EntryNested;
language: Language;
exclude: boolean;
}): string => {
if (entriesExists.is(item)) {
return buildExists({ item, language });
return buildExists({ item, language, exclude });
} else if (entriesMatch.is(item)) {
return buildMatch({ item, language });
return buildMatch({ item, language, exclude });
} else if (entriesMatchAny.is(item)) {
return buildMatchAny({ item, language });
return buildMatchAny({ item, language, exclude });
} else if (entriesNested.is(item)) {
return buildNested({ item, language });
} else {
Expand All @@ -163,7 +173,11 @@ export const formatQuery = ({
const or = getLanguageBooleanOperator({ language, value: 'or' });
const and = getLanguageBooleanOperator({ language, value: 'and' });
const formattedExceptions = exceptions.map((exception) => {
return `(${query} ${and} ${exception})`;
if (query === '') {
return `(${exception})`;
} else {
return `(${query} ${and} ${exception})`;
}
});

return formattedExceptions.join(` ${or} `);
Expand All @@ -175,15 +189,17 @@ export const formatQuery = ({
export const buildExceptionItemEntries = ({
lists,
language,
exclude,
}: {
lists: EntriesArray;
language: Language;
exclude: boolean;
}): string => {
const and = getLanguageBooleanOperator({ language, value: 'and' });
const exceptionItem = lists
.filter(({ type }) => type !== 'list')
.reduce<string[]>((accum, listItem) => {
const exceptionSegment = evaluateValues({ item: listItem, language });
const exceptionSegment = evaluateValues({ item: listItem, language, exclude });
return [...accum, exceptionSegment];
}, []);

Expand All @@ -194,15 +210,22 @@ export const buildQueryExceptions = ({
query,
language,
lists,
exclude = true,
}: {
query: Query;
language: Language;
lists: ExceptionListItemSchema[] | undefined;
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> | undefined;
exclude?: boolean;
}): DataQuery[] => {
if (lists != null) {
const exceptions = lists.map((exceptionItem) =>
buildExceptionItemEntries({ lists: exceptionItem.entries, language })
);
const exceptions = lists.reduce<string[]>((acc, exceptionItem) => {
return [
...acc,
...(exceptionItem.entries !== undefined
? [buildExceptionItemEntries({ lists: exceptionItem.entries, language, exclude })]
: []),
];
}, []);
const formattedQuery = formatQuery({ exceptions, language, query });
return [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,96 @@ describe('get_filter', () => {
});
});

describe('when "excludeExceptions" is false', () => {
test('it should work with a list', () => {
const esQuery = getQueryFilter(
'host.name: linux',
'kuery',
[],
['auditbeat-*'],
[getExceptionListItemSchemaMock()],
false
);
expect(esQuery).toEqual({
bool: {
filter: [
{
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
match: {
'host.name': 'linux',
},
},
],
},
},
{
bool: {
filter: [
{
nested: {
path: 'some.parentField',
query: {
bool: {
minimum_should_match: 1,
should: [
{
match: {
'some.parentField.nested.field': 'some value',
},
},
],
},
},
score_mode: 'none',
},
},
{
bool: {
minimum_should_match: 1,
should: [
{
match: {
'some.not.nested.field': 'some value',
},
},
],
},
},
],
},
},
],
},
},
],
must: [],
must_not: [],
should: [],
},
});
});

test('it should work with an empty list', () => {
const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], [], false);
expect(esQuery).toEqual({
bool: {
filter: [
{ bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } },
],
must: [],
must_not: [],
should: [],
},
});
});
});

test('it should work with a nested object queries', () => {
const esQuery = getQueryFilter(
'category:{ name:Frank and trusted:true }',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
buildEsQuery,
Query as DataQuery,
} from '../../../../../src/plugins/data/common';
import { ExceptionListItemSchema } from '../../../lists/common/schemas';
import {
ExceptionListItemSchema,
CreateExceptionListItemSchema,
} from '../../../lists/common/schemas';
import { buildQueryExceptions } from './build_exceptions_query';
import { Query, Language, Index } from './schemas/common/schemas';

Expand All @@ -20,14 +23,20 @@ export const getQueryFilter = (
language: Language,
filters: Array<Partial<Filter>>,
index: Index,
lists: ExceptionListItemSchema[]
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
excludeExceptions: boolean = true
) => {
const indexPattern: IIndexPattern = {
fields: [],
title: index.join(),
};

const queries: DataQuery[] = buildQueryExceptions({ query, language, lists });
const queries: DataQuery[] = buildQueryExceptions({
query,
language,
lists,
exclude: excludeExceptions,
});

const config = {
allowLeadingWildcards: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({

const onAddExceptionConfirm = useCallback(() => {
if (addOrUpdateExceptionItems !== null) {
if (shouldCloseAlert && alertData) {
addOrUpdateExceptionItems(enrichExceptionItems(), alertData.ecsData._id);
} else {
addOrUpdateExceptionItems(enrichExceptionItems());
}
const alertIdToClose = shouldCloseAlert && alertData ? alertData.ecsData._id : undefined;
const bulkCloseIndex =
shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined;
addOrUpdateExceptionItems(enrichExceptionItems(), alertIdToClose, bulkCloseIndex);
}
}, [addOrUpdateExceptionItems, enrichExceptionItems, shouldCloseAlert, alertData]);
}, [
addOrUpdateExceptionItems,
enrichExceptionItems,
shouldCloseAlert,
shouldBulkCloseAlert,
alertData,
signalIndexName,
]);

const isSubmitButtonDisabled = useCallback(
() => fetchOrCreateListError || exceptionItemsToAdd.length === 0,
Expand Down Expand Up @@ -330,7 +336,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
<EuiHorizontalRule />
<ModalBodySection>
{alertData !== undefined && (
<EuiFormRow>
<EuiFormRow fullWidth>
<EuiCheckbox
id="close-alert-on-add-add-exception-checkbox"
label="Close this alert"
Expand All @@ -339,10 +345,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({
/>
</EuiFormRow>
)}
<EuiFormRow>
<EuiFormRow fullWidth>
<EuiCheckbox
id="bulk-close-alert-on-add-add-exception-checkbox"
label={i18n.BULK_CLOSE_LABEL}
label={
shouldDisableBulkClose
? i18n.BULK_CLOSE_LABEL_DISABLED
: i18n.BULK_CLOSE_LABEL
}
checked={shouldBulkCloseAlert}
onChange={onBulkCloseAlertCheckboxChange}
disabled={shouldDisableBulkClose}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export const BULK_CLOSE_LABEL = i18n.translate(
}
);

export const BULK_CLOSE_LABEL_DISABLED = i18n.translate(
'xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled',
{
defaultMessage:
'Close all alerts that match attributes in this exception (Lists and non-ECS fields are not supported)',
}
);

export const EXCEPTION_BUILDER_INFO = i18n.translate(
'xpack.securitySolution.exceptions.addException.infoLabel',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({

const onEditExceptionConfirm = useCallback(() => {
if (addOrUpdateExceptionItems !== null) {
addOrUpdateExceptionItems(enrichExceptionItems());
const bulkCloseIndex =
shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined;
addOrUpdateExceptionItems(enrichExceptionItems(), undefined, bulkCloseIndex);
}
}, [addOrUpdateExceptionItems, enrichExceptionItems]);
}, [addOrUpdateExceptionItems, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName]);

const indexPatternConfig = useCallback(() => {
if (exceptionListType === 'endpoint') {
Expand Down Expand Up @@ -239,10 +241,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({
</ModalBodySection>
<EuiHorizontalRule />
<ModalBodySection>
<EuiFormRow>
<EuiFormRow fullWidth>
<EuiCheckbox
id="close-alert-on-add-add-exception-checkbox"
label={i18n.BULK_CLOSE_LABEL}
label={
shouldDisableBulkClose ? i18n.BULK_CLOSE_LABEL_DISABLED : i18n.BULK_CLOSE_LABEL
}
checked={shouldBulkCloseAlert}
onChange={onBulkCloseAlertCheckboxChange}
disabled={shouldDisableBulkClose}
Expand Down
Loading

0 comments on commit f4091df

Please sign in to comment.