[Security Solution][Detections] Make Endpoint Exception field options aware of OS, introduce OS selection to Endpoint Exceptions flow (#95014)
This commit is contained in:
parent
3d8f1b1b3b
commit
5203859cf9
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
]
|
|
@ -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"
|
||||
]
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 ?? '',
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue