Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution][Exceptions] Exception modal bulk close alerts that match exception attributes #71321

Merged
merged 8 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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;
Copy link
Contributor Author

@peluja1012 peluja1012 Jul 9, 2020

Choose a reason for hiding this comment

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

This is needed because the logic is negated when adding an exception (we want to exclude), but when closing alerts we don't want to do that.

}): 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