[Security Solution][Detections] Make Endpoint Exception field options aware of OS, introduce OS selection to Endpoint Exceptions flow (#95014)

This commit is contained in:
Kevin Logan 2021-04-30 16:18:40 -04:00 committed by GitHub
parent 3d8f1b1b3b
commit 5203859cf9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 347 additions and 57 deletions

View file

@ -704,4 +704,43 @@ describe('BuilderEntryItem', () => {
expect(mockSetErrorExists).toHaveBeenCalledWith(true);
});
test('it disabled field inputs correctly when passed "isDisabled=true"', () => {
wrapper = mount(
<BuilderEntryItem
autocompleteService={autocompleteStartMock}
entry={{
correspondingKeywordField: undefined,
entryIndex: 0,
field: getField('ip'),
id: '123',
nested: undefined,
operator: isOneOfOperator,
parent: undefined,
value: ['1234'],
}}
httpService={mockKibanaHttpService}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
listType="detection"
onChange={jest.fn()}
setErrorsExist={jest.fn()}
osTypes={['windows']}
showLabel={false}
isDisabled={true}
/>
);
expect(
wrapper.find('[data-test-subj="exceptionBuilderEntryField"] input').props().disabled
).toBeTruthy();
expect(
wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"] input').props().disabled
).toBeTruthy();
expect(
wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"] input').props().disabled
).toBeTruthy();
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import styled from 'styled-components';
@ -22,6 +22,7 @@ import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_
import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists';
import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common';
import { getEmptyValue } from '../../../common/empty_value';
import { OsTypeArray } from '../../../../common/schemas/common';
import {
getEntryOnFieldChange,
@ -45,15 +46,18 @@ export interface EntryItemProps {
entry: FormattedBuilderEntry;
httpService: HttpStart;
indexPattern: IIndexPattern;
showLabel: boolean;
osTypes?: OsTypeArray;
listType: ExceptionListType;
listTypeSpecificIndexPatternFilter?: (
pattern: IIndexPattern,
type: ExceptionListType
type: ExceptionListType,
osTypes?: OsTypeArray
) => IIndexPattern;
onChange: (arg: BuilderEntry, i: number) => void;
onlyShowListOperators?: boolean;
setErrorsExist: (arg: boolean) => void;
showLabel: boolean;
isDisabled?: boolean;
}
export const BuilderEntryItem: React.FC<EntryItemProps> = ({
@ -62,12 +66,14 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
entry,
httpService,
indexPattern,
osTypes,
listType,
listTypeSpecificIndexPatternFilter,
onChange,
onlyShowListOperators = false,
setErrorsExist,
showLabel,
isDisabled = false,
}): JSX.Element => {
const handleError = useCallback(
(err: boolean): void => {
@ -120,13 +126,22 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
[onChange, entry]
);
const isFieldComponentDisabled = useMemo(
(): boolean =>
isDisabled ||
indexPattern == null ||
(indexPattern != null && indexPattern.fields.length === 0),
[isDisabled, indexPattern]
);
const renderFieldInput = useCallback(
(isFirst: boolean): JSX.Element => {
const filteredIndexPatterns = getFilteredIndexPatterns(
indexPattern,
entry,
listType,
listTypeSpecificIndexPatternFilter
listTypeSpecificIndexPatternFilter,
osTypes
);
const comboBox = (
<FieldComponent
@ -139,7 +154,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
selectedField={entry.field}
isClearable={false}
isLoading={false}
isDisabled={indexPattern == null}
isDisabled={isDisabled || indexPattern == null}
onChange={handleFieldChange}
data-test-subj="exceptionBuilderEntryField"
fieldInputWidth={275}
@ -160,7 +175,15 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
);
}
},
[indexPattern, entry, listType, listTypeSpecificIndexPatternFilter, handleFieldChange]
[
indexPattern,
entry,
listType,
listTypeSpecificIndexPatternFilter,
handleFieldChange,
osTypes,
isDisabled,
]
);
const renderOperatorInput = (isFirst: boolean): JSX.Element => {
@ -177,9 +200,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
placeholder={i18n.EXCEPTION_OPERATOR_PLACEHOLDER}
selectedField={entry.field}
operator={entry.operator}
isDisabled={
indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0)
}
isDisabled={isFieldComponentDisabled}
operatorOptions={operatorOptions}
isLoading={false}
isClearable={false}
@ -214,9 +235,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER}
selectedField={entry.correspondingKeywordField ?? entry.field}
selectedValue={value}
isDisabled={
indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0)
}
isDisabled={isFieldComponentDisabled}
isLoading={false}
isClearable={false}
indexPattern={indexPattern}
@ -239,9 +258,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
: entry.field
}
selectedValue={values}
isDisabled={
indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0)
}
isDisabled={isFieldComponentDisabled}
isLoading={false}
isClearable={false}
indexPattern={indexPattern}
@ -261,9 +278,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
placeholder={i18n.EXCEPTION_FIELD_LISTS_PLACEHOLDER}
selectedValue={id}
isLoading={false}
isDisabled={
indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0)
}
isDisabled={isFieldComponentDisabled}
isClearable={false}
onChange={handleFieldListValueChange}
data-test-subj="exceptionBuilderEntryFieldList"

View file

@ -13,6 +13,7 @@ import { AutocompleteStart } from 'src/plugins/data/public';
import { ExceptionListType } from '../../../../common';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { OsTypeArray } from '../../../../common/schemas';
import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types';
import { BuilderAndBadgeComponent } from './and_badge';
@ -41,18 +42,21 @@ interface BuilderExceptionListItemProps {
autocompleteService: AutocompleteStart;
exceptionItem: ExceptionsBuilderExceptionItem;
exceptionItemIndex: number;
osTypes?: OsTypeArray;
indexPattern: IIndexPattern;
andLogicIncluded: boolean;
isOnlyItem: boolean;
listType: ExceptionListType;
listTypeSpecificIndexPatternFilter?: (
pattern: IIndexPattern,
type: ExceptionListType
type: ExceptionListType,
osTypes?: OsTypeArray
) => IIndexPattern;
onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
setErrorsExist: (arg: boolean) => void;
onlyShowListOperators?: boolean;
isDisabled?: boolean;
}
export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionListItemProps>(
@ -61,6 +65,7 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
httpService,
autocompleteService,
exceptionItem,
osTypes,
exceptionItemIndex,
indexPattern,
isOnlyItem,
@ -71,6 +76,7 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
onChangeExceptionItem,
setErrorsExist,
onlyShowListOperators = false,
isDisabled = false,
}) => {
const handleEntryChange = useCallback(
(entry: BuilderEntry, entryIndex: number): void => {
@ -138,6 +144,8 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
onChange={handleEntryChange}
onlyShowListOperators={onlyShowListOperators}
setErrorsExist={setErrorsExist}
osTypes={osTypes}
isDisabled={isDisabled}
showLabel={
exceptionItemIndex === 0 && index === 0 && item.nested !== 'child'
}

View file

@ -23,6 +23,7 @@ import {
exceptionListItemSchema,
} from '../../../../common';
import { AndOrBadge } from '../and_or_badge';
import { OsTypeArray } from '../../../../common/schemas';
import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types';
import { BuilderExceptionListItemComponent } from './exception_item_renderer';
@ -72,6 +73,7 @@ export interface ExceptionBuilderProps {
autocompleteService: AutocompleteStart;
exceptionListItems: ExceptionsBuilderExceptionItem[];
httpService: HttpStart;
osTypes?: OsTypeArray;
indexPatterns: IIndexPattern;
isAndDisabled: boolean;
isNestedDisabled: boolean;
@ -85,6 +87,7 @@ export interface ExceptionBuilderProps {
) => IIndexPattern;
onChange: (arg: OnChangeProps) => void;
ruleName: string;
isDisabled?: boolean;
}
export const ExceptionBuilderComponent = ({
@ -102,6 +105,8 @@ export const ExceptionBuilderComponent = ({
listTypeSpecificIndexPatternFilter,
onChange,
ruleName,
isDisabled = false,
osTypes,
}: ExceptionBuilderProps): JSX.Element => {
const [
{
@ -187,7 +192,6 @@ export const ExceptionBuilderComponent = ({
(shouldAddNested: boolean): void => {
dispatch({
addNested: shouldAddNested,
type: 'setAddNested',
});
},
@ -342,6 +346,10 @@ export const ExceptionBuilderComponent = ({
});
}, [onChange, exceptionsToDelete, exceptions, errorExists]);
useEffect(() => {
setUpdateExceptions([]);
}, [osTypes, setUpdateExceptions]);
// Defaults builder to never be sans entry, instead
// always falls back to an empty entry if user deletes all
useEffect(() => {
@ -401,6 +409,8 @@ export const ExceptionBuilderComponent = ({
onDeleteExceptionItem={handleDeleteExceptionItem}
onlyShowListOperators={containsValueListEntry(exceptions)}
setErrorsExist={setErrorsExist}
osTypes={osTypes}
isDisabled={isDisabled}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -417,8 +427,8 @@ export const ExceptionBuilderComponent = ({
<EuiFlexItem grow={1}>
<BuilderLogicButtons
isOrDisabled={isOrDisabled ? isOrDisabled : disableOr}
isAndDisabled={disableAnd}
isNestedDisabled={disableNested}
isAndDisabled={isAndDisabled ? isAndDisabled : disableAnd}
isNestedDisabled={isNestedDisabled ? isNestedDisabled : disableNested}
isNested={addNested}
showNestedButton
onOrClicked={handleAddNewExceptionItem}

View file

@ -37,6 +37,7 @@ import {
isOperator,
} from '../autocomplete/operators';
import { OperatorOption } from '../autocomplete/types';
import { OsTypeArray } from '../../../../common/schemas';
import {
BuilderEntry,
@ -279,9 +280,10 @@ export const getFilteredIndexPatterns = (
patterns: IIndexPattern,
item: FormattedBuilderEntry,
type: ExceptionListType,
preFilter?: (i: IIndexPattern, t: ExceptionListType) => IIndexPattern
preFilter?: (i: IIndexPattern, t: ExceptionListType, o?: OsTypeArray) => IIndexPattern,
osTypes?: OsTypeArray
): IIndexPattern => {
const indexPatterns = preFilter != null ? preFilter(patterns, type) : patterns;
const indexPatterns = preFilter != null ? preFilter(patterns, type, osTypes) : patterns;
if (item.nested === 'child' && item.parent != null) {
// when user has selected a nested entry, only fields with the common parent are shown

View file

@ -161,6 +161,9 @@ describe('When the add exception modal is opened', () => {
it('should contain the endpoint specific documentation text', () => {
expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy();
});
it('should render the os selection dropdown', () => {
expect(wrapper.find('[data-test-subj="os-selection-dropdown"]').exists()).toBeTruthy();
});
});
describe('when there is alert data passed to an endpoint list exception', () => {
@ -218,6 +221,9 @@ describe('When the add exception modal is opened', () => {
it('should not display the eql sequence callout', () => {
expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
});
it('should not render the os selection dropdown', () => {
expect(wrapper.find('[data-test-subj="os-selection-dropdown"]').exists()).toBeFalsy();
});
});
describe('when there is alert data passed to a detection list exception', () => {

View file

@ -22,6 +22,8 @@ import {
EuiFormRow,
EuiText,
EuiCallOut,
EuiComboBox,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import {
hasEqlSequenceQuery,
@ -60,6 +62,7 @@ import { ErrorInfo, ErrorCallout } from '../error_callout';
import { AlertData, ExceptionsBuilderExceptionItem } from '../types';
import { useFetchIndex } from '../../../containers/source';
import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs';
import { OsTypeArray, OsType } from '../../../../../../lists/common/schemas';
export interface AddExceptionModalProps {
ruleName: string;
@ -293,6 +296,16 @@ export const AddExceptionModal = memo(function AddExceptionModal({
[setShouldBulkCloseAlert]
);
const hasAlertData = useMemo((): boolean => {
return alertData !== undefined;
}, [alertData]);
const [selectedOs, setSelectedOs] = useState<OsType | undefined>();
const osTypesSelection = useMemo((): OsTypeArray => {
return hasAlertData ? retrieveAlertOsTypes(alertData) : selectedOs ? [selectedOs] : [];
}, [hasAlertData, alertData, selectedOs]);
const enrichExceptionItems = useCallback((): Array<
ExceptionListItemSchema | CreateExceptionListItemSchema
> => {
@ -302,11 +315,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({
? enrichNewExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }])
: exceptionItemsToAdd;
if (exceptionListType === 'endpoint') {
const osTypes = retrieveAlertOsTypes(alertData);
const osTypes = osTypesSelection;
enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes));
}
return enriched;
}, [comment, exceptionItemsToAdd, exceptionListType, alertData]);
}, [comment, exceptionItemsToAdd, exceptionListType, osTypesSelection]);
const onAddExceptionConfirm = useCallback((): void => {
if (addOrUpdateExceptionItems != null) {
@ -343,10 +356,55 @@ export const AddExceptionModal = memo(function AddExceptionModal({
return false;
}, [maybeRule]);
const OsOptions: Array<EuiComboBoxOptionOption<OsType>> = useMemo((): Array<
EuiComboBoxOptionOption<OsType>
> => {
return [
{
label: sharedI18n.OPERATING_SYSTEM_WINDOWS,
value: 'windows',
},
{
label: sharedI18n.OPERATING_SYSTEM_MAC,
value: 'macos',
},
{
label: sharedI18n.OPERATING_SYSTEM_LINUX,
value: 'linux',
},
];
}, []);
const handleOSSelectionChange = useCallback(
(selectedOptions): void => {
setSelectedOs(selectedOptions[0].value);
},
[setSelectedOs]
);
const selectedOStoOptions = useMemo((): Array<EuiComboBoxOptionOption<OsType>> => {
return OsOptions.filter((option) => {
return selectedOs === option.value;
});
}, [selectedOs, OsOptions]);
const singleSelectionOptions = useMemo(() => {
return { asPlainText: true };
}, []);
const hasOsSelection = useMemo(() => {
return exceptionListType === 'endpoint' && !hasAlertData;
}, [exceptionListType, hasAlertData]);
const isExceptionBuilderFormDisabled = useMemo(() => {
return hasOsSelection && selectedOs === undefined;
}, [hasOsSelection, selectedOs]);
return (
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
<ModalHeader>
<EuiModalHeaderTitle>{addExceptionMessage}</EuiModalHeaderTitle>
<EuiSpacer size="xs" />
<ModalHeaderSubtitle className="eui-textTruncate" title={ruleName}>
{ruleName}
</ModalHeaderSubtitle>
@ -395,6 +453,22 @@ export const AddExceptionModal = memo(function AddExceptionModal({
)}
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
<EuiSpacer />
{exceptionListType === 'endpoint' && !hasAlertData && (
<>
<EuiFormRow label={sharedI18n.OPERATING_SYSTEM_LABEL}>
<EuiComboBox
placeholder={i18n.OPERATING_SYSTEM_PLACEHOLDER}
singleSelection={singleSelectionOptions}
options={OsOptions}
selectedOptions={selectedOStoOptions}
onChange={handleOSSelectionChange}
isClearable={false}
data-test-subj="os-selection-dropdown"
/>
</EuiFormRow>
<EuiSpacer size="l" />
</>
)}
<ExceptionBuilder.ExceptionBuilderComponent
allowLargeValueLists={
!isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type)
@ -403,17 +477,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({
autocompleteService={data.autocomplete}
exceptionListItems={initialExceptionItems}
listType={exceptionListType}
osTypes={osTypesSelection}
listId={ruleExceptionList.list_id}
listNamespaceType={ruleExceptionList.namespace_type}
listTypeSpecificIndexPatternFilter={filterIndexPatterns}
ruleName={ruleName}
indexPatterns={indexPatterns}
isOrDisabled={false}
isAndDisabled={false}
isNestedDisabled={false}
isOrDisabled={isExceptionBuilderFormDisabled}
isAndDisabled={isExceptionBuilderFormDisabled}
isNestedDisabled={isExceptionBuilderFormDisabled}
data-test-subj="alert-exception-builder"
id-aria="alert-exception-builder"
onChange={handleBuilderOnChange}
isDisabled={isExceptionBuilderFormDisabled}
/>
<EuiSpacer />
@ -450,7 +526,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
</EuiFormRow>
{exceptionListType === 'endpoint' && (
<>
<EuiSpacer />
<EuiSpacer size="s" />
<EuiText data-test-subj="add-exception-endpoint-text" color="subdued" size="s">
{i18n.ENDPOINT_QUARANTINE_TEXT}
</EuiText>

View file

@ -90,3 +90,10 @@ export const ADD_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
"This rule's query contains an EQL sequence statement. The exception created will apply to all events in the sequence.",
}
);
export const OPERATING_SYSTEM_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder',
{
defaultMessage: 'Select an operating system',
}
);

View file

@ -53,6 +53,7 @@ import {
import { Loader } from '../../loader';
import { ErrorInfo, ErrorCallout } from '../error_callout';
import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs';
import { OsTypeArray, OsType } from '../../../../../../lists/common/schemas';
interface EditExceptionModalProps {
ruleName: string;
@ -281,6 +282,21 @@ export const EditExceptionModal = memo(function EditExceptionModal({
return false;
}, [maybeRule]);
const osDisplay = (osTypes: OsTypeArray): string => {
const translateOS = (currentOs: OsType): string => {
return currentOs === 'linux'
? sharedI18n.OPERATING_SYSTEM_LINUX
: currentOs === 'macos'
? sharedI18n.OPERATING_SYSTEM_MAC
: sharedI18n.OPERATING_SYSTEM_WINDOWS;
};
return osTypes
.reduce((osString, currentOs) => {
return `${translateOS(currentOs)}, ${osString}`;
}, '')
.slice(0, -2);
};
return (
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
<ModalHeader>
@ -289,6 +305,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE
: i18n.EDIT_EXCEPTION_TITLE}
</EuiModalHeaderTitle>
<EuiSpacer size="xs" />
<ModalHeaderSubtitle className="eui-textTruncate" title={ruleName}>
{ruleName}
</ModalHeaderSubtitle>
@ -314,6 +331,17 @@ export const EditExceptionModal = memo(function EditExceptionModal({
)}
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
<EuiSpacer />
{exceptionListType === 'endpoint' && (
<>
<EuiText size="xs">
<dl>
<dt>{sharedI18n.OPERATING_SYSTEM_LABEL}</dt>
<dd>{osDisplay(exceptionItem.os_types)}</dd>
</dl>
</EuiText>
<EuiSpacer />
</>
)}
<ExceptionBuilder.ExceptionBuilderComponent
allowLargeValueLists={
!isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type)
@ -328,6 +356,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
ruleName={ruleName}
isOrDisabled
isAndDisabled={false}
osTypes={exceptionItem.os_types}
isNestedDisabled={false}
data-test-subj="edit-exception-modal-builder"
id-aria="edit-exception-modal-builder"
@ -359,7 +388,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
</EuiFormRow>
{exceptionListType === 'endpoint' && (
<>
<EuiSpacer />
<EuiSpacer size="s" />
<EuiText data-test-subj="edit-exception-endpoint-text" color="subdued" size="s">
{i18n.ENDPOINT_QUARANTINE_TEXT}
</EuiText>

View file

@ -6,33 +6,25 @@
"Target.process.Ext.code_signature.valid",
"Target.process.Ext.services",
"Target.process.Ext.user",
"Target.process.command_line.caseless",
"Target.process.executable.caseless",
"Target.process.hash.md5",
"Target.process.hash.sha1",
"Target.process.hash.sha256",
"Target.process.hash.sha512",
"Target.process.name.caseless",
"Target.process.parent.Ext.code_signature.status",
"Target.process.parent.Ext.code_signature.subject_name",
"Target.process.parent.Ext.code_signature.trusted",
"Target.process.parent.Ext.code_signature.valid",
"Target.process.parent.command_line.caseless",
"Target.process.parent.executable.caseless",
"Target.process.parent.hash.md5",
"Target.process.parent.hash.sha1",
"Target.process.parent.hash.sha256",
"Target.process.parent.hash.sha512",
"Target.process.parent.name.caseless",
"Target.process.parent.pgid",
"Target.process.parent.working_directory.caseless",
"Target.process.pe.company",
"Target.process.pe.description",
"Target.process.pe.file_version",
"Target.process.pe.original_file_name",
"Target.process.pe.product",
"Target.process.pgid",
"Target.process.working_directory.caseless",
"agent.id",
"agent.type",
"agent.version",
@ -66,14 +58,12 @@
"file.mode",
"file.name",
"file.owner",
"file.path.caseless",
"file.pe.company",
"file.pe.description",
"file.pe.file_version",
"file.pe.original_file_name",
"file.pe.product",
"file.size",
"file.target_path.caseless",
"file.type",
"file.uid",
"group.Ext.real.id",
@ -84,9 +74,7 @@
"host.id",
"host.os.Ext.variant",
"host.os.family",
"host.os.full.caseless",
"host.os.kernel",
"host.os.name.caseless",
"host.os.platform",
"host.os.version",
"host.type",
@ -96,33 +84,25 @@
"process.Ext.code_signature.valid",
"process.Ext.services",
"process.Ext.user",
"process.command_line.caseless",
"process.executable.caseless",
"process.hash.md5",
"process.hash.sha1",
"process.hash.sha256",
"process.hash.sha512",
"process.name.caseless",
"process.parent.Ext.code_signature.status",
"process.parent.Ext.code_signature.subject_name",
"process.parent.Ext.code_signature.trusted",
"process.parent.Ext.code_signature.valid",
"process.parent.command_line.caseless",
"process.parent.executable.caseless",
"process.parent.hash.md5",
"process.parent.hash.sha1",
"process.parent.hash.sha256",
"process.parent.hash.sha512",
"process.parent.name.caseless",
"process.parent.pgid",
"process.parent.working_directory.caseless",
"process.pe.company",
"process.pe.description",
"process.pe.file_version",
"process.pe.original_file_name",
"process.pe.product",
"process.pgid",
"process.working_directory.caseless",
"rule.uuid",
"user.domain",
"user.email",

View file

@ -0,0 +1,22 @@
[
"file.path",
"file.target_path",
"Target.process.command_line",
"Target.process.executable",
"Target.process.name",
"Target.process.parent.command_line",
"Target.process.parent.executable",
"Target.process.parent.name",
"Target.process.parent.working_directory",
"Target.process.working_directory",
"host.os.full",
"host.os.name",
"process.command_line",
"process.executable",
"process.name",
"process.parent.command_line",
"process.parent.executable",
"process.parent.name",
"process.parent.working_directory",
"process.working_directory"
]

View file

@ -0,0 +1,22 @@
[
"file.path.caseless",
"file.target_path.caseless",
"Target.process.command_line.caseless",
"Target.process.executable.caseless",
"Target.process.name.caseless",
"Target.process.parent.command_line.caseless",
"Target.process.parent.executable.caseless",
"Target.process.parent.name.caseless",
"Target.process.parent.working_directory.caseless",
"Target.process.working_directory.caseless",
"host.os.full.caseless",
"host.os.name.caseless",
"process.command_line.caseless",
"process.executable.caseless",
"process.name.caseless",
"process.parent.command_line.caseless",
"process.parent.executable.caseless",
"process.parent.name.caseless",
"process.parent.working_directory.caseless",
"process.working_directory.caseless"
]

View file

@ -98,6 +98,30 @@ const mockEndpointFields = [
},
];
const mockLinuxEndpointFields = [
{
name: 'file.path',
type: 'string',
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
},
{
name: 'file.Ext.code_signature.status',
type: 'string',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
subType: { nested: { path: 'file.Ext.code_signature' } },
},
];
export const getEndpointField = (name: string) =>
mockEndpointFields.find((field) => field.name === name) as IFieldType;
@ -113,7 +137,7 @@ describe('Exception helpers', () => {
describe('#filterIndexPatterns', () => {
test('it returns index patterns without filtering if list type is "detection"', () => {
const mockIndexPatterns = getMockIndexPattern();
const output = filterIndexPatterns(mockIndexPatterns, 'detection');
const output = filterIndexPatterns(mockIndexPatterns, 'detection', ['windows']);
expect(output).toEqual(mockIndexPatterns);
});
@ -123,10 +147,20 @@ describe('Exception helpers', () => {
...getMockIndexPattern(),
fields: [...fields, ...mockEndpointFields],
};
const output = filterIndexPatterns(mockIndexPatterns, 'endpoint');
const output = filterIndexPatterns(mockIndexPatterns, 'endpoint', ['windows']);
expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] });
});
test('it returns filtered index patterns if list type is "endpoint" and os contains "linux"', () => {
const mockIndexPatterns = {
...getMockIndexPattern(),
fields: [...fields, ...mockLinuxEndpointFields],
};
const output = filterIndexPatterns(mockIndexPatterns, 'endpoint', ['linux']);
expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockLinuxEndpointFields] });
});
});
describe('#getOperatorType', () => {

View file

@ -49,18 +49,28 @@ import { Ecs } from '../../../../common/ecs';
import { CodeSignature } from '../../../../common/ecs/file';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
import { addIdToItem, removeIdFromItem } from '../../../../common';
import exceptionableLinuxFields from './exceptionable_linux_fields.json';
import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json';
import exceptionableEndpointFields from './exceptionable_endpoint_fields.json';
import exceptionableEndpointEventFields from './exceptionable_endpoint_event_fields.json';
export const filterIndexPatterns = (
patterns: IIndexPattern,
type: ExceptionListType
type: ExceptionListType,
osTypes?: OsTypeArray
): IIndexPattern => {
switch (type) {
case 'endpoint':
const osFilterForEndpoint: (name: string) => boolean = osTypes?.includes('linux')
? (name: string) =>
exceptionableLinuxFields.includes(name) || exceptionableEndpointFields.includes(name)
: (name: string) =>
exceptionableWindowsMacFields.includes(name) ||
exceptionableEndpointFields.includes(name);
return {
...patterns,
fields: patterns.fields.filter(({ name }) => exceptionableEndpointFields.includes(name)),
fields: patterns.fields.filter(({ name }) => osFilterForEndpoint(name)),
};
case 'endpoint_events':
return {
@ -511,9 +521,11 @@ export const getPrepopulatedEndpointException = ({
eventCode: string;
alertEcsData: Flattened<Ecs>;
}): ExceptionsBuilderExceptionItem => {
const { file } = alertEcsData;
const { file, host } = alertEcsData;
const filePath = file?.path ?? '';
const sha256Hash = file?.hash?.sha256 ?? '';
const filePathDefault = host?.os?.family === 'linux' ? 'file.path' : 'file.path.caseless';
return {
...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }),
entries: addIdToEntries([
@ -536,7 +548,7 @@ export const getPrepopulatedEndpointException = ({
],
},
{
field: 'file.path.caseless',
field: filePathDefault,
operator: 'included',
type: 'match',
value: filePath ?? '',

View file

@ -61,6 +61,13 @@ export const OPERATING_SYSTEM = i18n.translate(
}
);
export const OPERATING_SYSTEM_LABEL = i18n.translate(
'xpack.securitySolution.exceptions.operatingSystemFullLabel',
{
defaultMessage: 'Operating System',
}
);
export const SEARCH_DEFAULT = i18n.translate(
'xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder',
{
@ -240,3 +247,24 @@ export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate(
defaultMessage: 'Failed to remove exception list',
}
);
export const OPERATING_SYSTEM_WINDOWS = i18n.translate(
'xpack.securitySolution.exceptions.operatingSystemWindows',
{
defaultMessage: 'Windows',
}
);
export const OPERATING_SYSTEM_MAC = i18n.translate(
'xpack.securitySolution.exceptions.operatingSystemMac',
{
defaultMessage: 'macOS',
}
);
export const OPERATING_SYSTEM_LINUX = i18n.translate(
'xpack.securitySolution.exceptions.operatingSystemLinux',
{
defaultMessage: 'Linux',
}
);