[Security Solution] User can select event from event list and create a filter (#96940)

* Initial version of event filtering form/dialog. Pending to add all redux services

* Uses redux store instead of props to get the form values

* Manage errors on redux

* Creates even filter list on service constructor

* Add os type selector depending on form parent by props. Also added create action

* Allows add exception to an event. This commit has to be reviewed and maybe it will change depending on next changes

* Fix imports because changes on ExceptionBuilder component and add needed type export

* Adds constants. Rename eventFilters to eventFilter. Add http wrapper as a hook to check if the list has been created or not

* Adds missing files on last commit.

* Relocate async resource state to be shared between different pages

* Use async resource state to manage async operations on components. Relocate initial entry status to an utils module instead of hook.

* Adds comments into redux store from component

* Fixes typechecks and wrong imports

* Fixes translations and adds subheader and description modal

* Relocates form description

* Removes unused import

* Sanitize entries before submit to remove entry.id

* Missed file on last commit

* Use specific fields for endpoint_event type builder

* Split error field for each kind of errors to prevent unexpected renders. Adds unit test for event filter form component

* Set event.kind == event by default

* Changes folder names. Add notifications when success. Remove default event.king

* Adds notifications when api error and fixed multiple notifications showed for same error

* Adds new test for event filter modal and changes component name to be consistent

* Adds unit tests for event filter notification

* Adds middleware unit tests. Also isolate common event for all tests

* Adds unit tests for event filter reducer

* Adds unit tests for event filter selector

* Fixes same key on different multilanguages. Fixes naming incoherence

* Adds feature flag for event filtering

* Fixes unit tests and weird behavior when changing items after name or comments on event filter form

* Removes unused import

* Fixes unit tests. Add imports from lists plugin. Add expects on tests. Change some names

* Renames everything from eventFilter to eventFilters (plural)

* Rename state variable

* Create hook for notifications instead of a component. Removes className from modal body.

* Updates available fields for enpoint events builder

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
David Sánchez 2021-04-22 10:08:33 +02:00 committed by GitHub
parent 59bd5e5b54
commit bc240f0af7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2122 additions and 16 deletions

View file

@ -51,4 +51,12 @@ export {
export { buildExceptionFilter } from './exceptions';
export { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from './constants';
export {
ENDPOINT_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_ID,
EXCEPTION_LIST_URL,
EXCEPTION_LIST_ITEM_URL,
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_EVENT_FILTERS_LIST_NAME,
ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
} from './constants';

View file

@ -7,4 +7,4 @@
export { BuilderEntryItem } from './entry_renderer';
export { BuilderExceptionListItemComponent } from './exception_item_renderer';
export { ExceptionBuilderComponent } from './exception_items_renderer';
export { ExceptionBuilderComponent, OnChangeProps } from './exception_items_renderer';

View file

@ -39,3 +39,4 @@ export {
UseExceptionListsSuccess,
} from './exceptions/types';
export * as ExceptionBuilder from './exceptions/components/builder/index';
export { transformNewItemOutput } from './exceptions/transforms';

View file

@ -45,6 +45,11 @@ export {
Type,
ENDPOINT_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_ID,
EXCEPTION_LIST_URL,
EXCEPTION_LIST_ITEM_URL,
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_EVENT_FILTERS_LIST_NAME,
ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
osType,
osTypeArray,
OsTypeArray,

View file

@ -29,6 +29,10 @@ import { SourcererScopeName } from '../../store/sourcerer/model';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { useTimelineEvents } from '../../../timelines/containers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
jest.mock('../../hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../timelines/components/graph_overlay', () => ({
GraphOverlay: jest.fn(() => <div />),
@ -135,6 +139,7 @@ describe('EventsViewer', () => {
});
describe('event details', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]);
});

View file

@ -0,0 +1,332 @@
[
"@timestamp",
"agent.id",
"agent.name",
"agent.type",
"agent.version",
"data_stream.dataset",
"data_stream.namespace",
"data_stream.type",
"destination.address",
"destination.bytes",
"destination.domain",
"destination.geo.city_name",
"destination.geo.continent_name",
"destination.geo.country_iso_code",
"destination.geo.country_name",
"destination.geo.location",
"destination.geo.name",
"destination.geo.region_iso_code",
"destination.geo.region_name",
"destination.ip",
"destination.packets",
"destination.port",
"destination.registered_domain",
"destination.top_level_domain",
"dll.code_signature.exists",
"dll.code_signature.status",
"dll.code_signature.subject_name",
"dll.code_signature.trusted",
"dll.code_signature.valid",
"dll.Ext",
"dll.Ext.code_signature",
"dll.Ext.code_signature.exists",
"dll.Ext.code_signature.status",
"dll.Ext.code_signature.subject_name",
"dll.Ext.code_signature.trusted",
"dll.Ext.code_signature.valid",
"dll.Ext.load_index",
"dll.hash.md5",
"dll.hash.sha1",
"dll.hash.sha256",
"dll.hash.sha512",
"dll.name",
"dll.path",
"dll.pe.company",
"dll.pe.description",
"dll.pe.file_version",
"dll.pe.imphash",
"dll.pe.original_file_name",
"dll.pe.product",
"dns.Ext",
"dns.Ext.options",
"dns.Ext.status",
"dns.question.name",
"dns.question.registered_domain",
"dns.question.subdomain",
"dns.question.top_level_domain",
"dns.question.type",
"dns.resolved_ip",
"ecs.version",
"elastic.agent",
"elastic.agent.id",
"Endpoint.policy",
"Endpoint.policy.applied",
"Endpoint.policy.applied.id",
"Endpoint.policy.applied.name",
"Endpoint.policy.applied.status",
"Endpoint.status",
"event.action",
"event.category",
"event.code",
"event.created",
"event.dataset",
"event.Ext",
"event.Ext.correlation",
"event.Ext.correlation.id",
"event.hash",
"event.id",
"event.ingested",
"event.module",
"event.outcome",
"event.provider",
"event.sequence",
"event.severity",
"event.type",
"file.accessed",
"file.attributes",
"file.created",
"file.ctime",
"file.device",
"file.directory",
"file.drive_letter",
"file.Ext",
"file.Ext.code_signature",
"file.Ext.code_signature.exists",
"file.Ext.code_signature.status",
"file.Ext.code_signature.subject_name",
"file.Ext.code_signature.trusted",
"file.Ext.code_signature.valid",
"file.Ext.entropy",
"file.Ext.header_data",
"file.Ext.monotonic_id",
"file.Ext.original",
"file.Ext.original.gid",
"file.Ext.original.group",
"file.Ext.original.mode",
"file.Ext.original.name",
"file.Ext.original.owner",
"file.Ext.original.path",
"file.Ext.original.uid",
"file.Ext.windows",
"file.Ext.windows.zone_identifier",
"file.extension",
"file.gid",
"file.group",
"file.hash.md5",
"file.hash.sha1",
"file.hash.sha256",
"file.hash.sha512",
"file.inode",
"file.mime_type",
"file.mode",
"file.mtime",
"file.name",
"file.owner",
"file.path",
"file.path.caseless",
"file.path.text",
"file.pe.company",
"file.pe.description",
"file.pe.file_version",
"file.pe.imphash",
"file.pe.original_file_name",
"file.pe.product",
"file.size",
"file.target_path",
"file.target_path.caseless",
"file.target_path.text",
"file.type",
"file.uid",
"group.domain",
"group.Ext",
"group.Ext.real",
"group.Ext.real.id",
"group.Ext.real.name",
"group.id",
"group.name",
"host.architecture",
"host.domain",
"host.hostname",
"host.id",
"host.ip",
"host.mac",
"host.name",
"host.os.Ext",
"host.os.Ext.variant",
"host.os.family",
"host.os.full",
"host.os.full.caseless",
"host.os.full.text",
"host.os.kernel",
"host.os.name",
"host.os.name.caseless",
"host.os.name.text",
"host.os.platform",
"host.os.version",
"host.type",
"host.uptime",
"http.request.body.bytes",
"http.request.body.content",
"http.request.body.content.text",
"http.request.bytes",
"http.response.body.bytes",
"http.response.body.content",
"http.response.body.content.text",
"http.response.bytes",
"http.response.Ext",
"http.response.Ext.version",
"http.response.status_code",
"message",
"network.bytes",
"network.community_id",
"network.direction",
"network.iana_number",
"network.packets",
"network.protocol",
"network.transport",
"network.type",
"package.name",
"process.args",
"process.args_count",
"process.code_signature.exists",
"process.code_signature.status",
"process.code_signature.subject_name",
"process.code_signature.trusted",
"process.code_signature.valid",
"process.command_line",
"process.command_line.caseless",
"process.command_line.text",
"process.entity_id",
"process.executable",
"process.executable.caseless",
"process.executable.text",
"process.exit_code",
"process.Ext",
"process.Ext.ancestry",
"process.Ext.authentication_id",
"process.Ext.code_signature",
"process.Ext.code_signature.exists",
"process.Ext.code_signature.status",
"process.Ext.code_signature.subject_name",
"process.Ext.code_signature.trusted",
"process.Ext.code_signature.valid",
"process.Ext.defense_evasions",
"process.Ext.session",
"process.Ext.token.elevation",
"process.Ext.token.elevation_type",
"process.Ext.token.integrity_level_name",
"process.hash.md5",
"process.hash.sha1",
"process.hash.sha256",
"process.hash.sha512",
"process.name",
"process.name.caseless",
"process.name.text",
"process.parent.args",
"process.parent.args_count",
"process.parent.code_signature.exists",
"process.parent.code_signature.status",
"process.parent.code_signature.subject_name",
"process.parent.code_signature.trusted",
"process.parent.code_signature.valid",
"process.parent.command_line",
"process.parent.command_line.caseless",
"process.parent.command_line.text",
"process.parent.entity_id",
"process.parent.executable",
"process.parent.executable.caseless",
"process.parent.executable.text",
"process.parent.exit_code",
"process.parent.Ext",
"process.parent.Ext.code_signature",
"process.parent.Ext.code_signature.exists",
"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.Ext.real",
"process.parent.Ext.real.pid",
"process.parent.hash.md5",
"process.parent.hash.sha1",
"process.parent.hash.sha256",
"process.parent.hash.sha512",
"process.parent.name",
"process.parent.name.caseless",
"process.parent.name.text",
"process.parent.pe.company",
"process.parent.pe.description",
"process.parent.pe.file_version",
"process.parent.pe.imphash",
"process.parent.pe.original_file_name",
"process.parent.pe.product",
"process.parent.pgid",
"process.parent.pid",
"process.parent.ppid",
"process.parent.thread.id",
"process.parent.thread.name",
"process.parent.title",
"process.parent.title.text",
"process.parent.uptime",
"process.parent.working_directory",
"process.parent.working_directory.caseless",
"process.parent.working_directory.text",
"process.pe.company",
"process.pe.description",
"process.pe.file_version",
"process.pe.imphash",
"process.pe.original_file_name",
"process.pe.product",
"process.pgid",
"process.pid",
"process.ppid",
"process.thread.id",
"process.thread.name",
"process.title",
"process.title.text",
"process.uptime",
"process.working_directory",
"process.working_directory.caseless",
"process.working_directory.text",
"registry.data.bytes",
"registry.data.strings",
"registry.hive",
"registry.key",
"registry.path",
"registry.value",
"source.address",
"source.bytes",
"source.domain",
"source.geo.city_name",
"source.geo.continent_name",
"source.geo.country_iso_code",
"source.geo.country_name",
"source.geo.location",
"source.geo.name",
"source.geo.region_iso_code",
"source.geo.region_name",
"source.ip",
"source.packets",
"source.port",
"source.registered_domain",
"source.top_level_domain",
"user.domain",
"user.email",
"user.Ext",
"user.Ext.real",
"user.Ext.real.id",
"user.Ext.real.name",
"user.full_name",
"user.full_name.text",
"user.group.domain",
"user.group.Ext",
"user.group.Ext.real",
"user.group.Ext.real.id",
"user.group.Ext.real.name",
"user.group.id",
"user.group.name",
"user.hash",
"user.id",
"user.name",
"user.name.text"
]

View file

@ -49,18 +49,29 @@ 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 exceptionableFields from './exceptionable_fields.json';
import exceptionableEndpointFields from './exceptionable_endpoint_fields.json';
import exceptionableEndpointEventFields from './exceptionable_endpoint_event_fields.json';
export const filterIndexPatterns = (
patterns: IIndexPattern,
type: ExceptionListType
): IIndexPattern => {
return type === 'endpoint'
? {
switch (type) {
case 'endpoint':
return {
...patterns,
fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)),
}
: patterns;
fields: patterns.fields.filter(({ name }) => exceptionableEndpointFields.includes(name)),
};
case 'endpoint_events':
return {
...patterns,
fields: patterns.fields.filter(({ name }) =>
exceptionableEndpointEventFields.includes(name)
),
};
default:
return patterns;
}
};
export const addIdToEntries = (entries: EntriesArray): EntriesArray => {

View file

@ -8,6 +8,7 @@
import { EndpointAction } from '../../management/pages/endpoint_hosts/store/action';
import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details';
import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action';
import { EventFiltersPageAction } from '../../management/pages/event_filters/store/action';
export { appActions } from './app';
export { dragAndDropActions } from './drag_and_drop';
@ -19,4 +20,5 @@ export type AppAction =
| EndpointAction
| RoutingAction
| PolicyDetailsAction
| TrustedAppsPageAction;
| TrustedAppsPageAction
| EventFiltersPageAction;

View file

@ -17,6 +17,7 @@ import {
} from '@elastic/eui';
import styled from 'styled-components';
import { getOr } from 'lodash/fp';
import { indexOf } from 'lodash';
import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
@ -47,6 +48,7 @@ import { ExceptionListType } from '../../../../../common/shared_imports';
import { AlertData, EcsHit } from '../../../../common/components/exceptions/types';
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index';
import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal';
interface AlertContextMenuProps {
ariaLabel?: string;
@ -81,6 +83,8 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
'',
[ecsRowData]
);
const isEvent = useMemo(() => indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]);
const ruleIndices = useMemo((): string[] => {
if (
ecsRowData.signal?.rule &&
@ -107,6 +111,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
setPopover(false);
}, []);
const [exceptionModalType, setOpenAddExceptionModal] = useState<ExceptionListType | null>(null);
const [isAddEventExceptionModalOpen, setIsAddEventExceptionModalOpen] = useState<boolean>(false);
const [{ canUserCRUD, hasIndexWrite, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData();
const isEndpointAlert = useMemo((): boolean => {
@ -124,6 +129,10 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
setOpenAddExceptionModal(null);
}, []);
const closeAddEventExceptionModal = useCallback((): void => {
setIsAddEventExceptionModalOpen(false);
}, []);
const onAddExceptionCancel = useCallback(() => {
closeAddExceptionModal();
}, [closeAddExceptionModal]);
@ -355,6 +364,28 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
);
}, [handleAddExceptionClick, canUserCRUD, hasIndexWrite]);
const handleAddEventExceptionClick = useCallback((): void => {
closePopover();
setIsAddEventExceptionModalOpen(true);
}, [closePopover]);
const addEventExceptionComponent = useMemo(
() => (
<EuiContextMenuItem
key="add-event-exception-menu-item"
aria-label="Add Event Exception"
data-test-subj="add-event-exception-menu-item"
id="addEventException"
onClick={handleAddEventExceptionClick}
>
<EuiText data-test-subj="addEventExceptionButton" size="m">
{i18n.ACTION_ADD_EVENT_EXCEPTION}
</EuiText>
</EuiContextMenuItem>
),
[handleAddEventExceptionClick]
);
const statusFilters = useMemo(() => {
if (!alertStatus) {
return [];
@ -378,8 +409,18 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
]);
const items = useMemo(
() => [...statusFilters, addEndpointExceptionComponent, addExceptionComponent],
[addEndpointExceptionComponent, addExceptionComponent, statusFilters]
() =>
!isEvent && ruleId
? [...statusFilters, addEndpointExceptionComponent, addExceptionComponent]
: [addEventExceptionComponent],
[
addEndpointExceptionComponent,
addExceptionComponent,
addEventExceptionComponent,
statusFilters,
ruleId,
isEvent,
]
);
return (
@ -412,6 +453,9 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
onRuleChange={onRuleChange}
/>
)}
{isAddEventExceptionModalOpen && ecsRowData != null && (
<EventFiltersModal data={ecsRowData} onCancel={closeAddEventExceptionModal} />
)}
</>
);
};

View file

@ -151,6 +151,13 @@ export const ACTION_ADD_EXCEPTION = i18n.translate(
}
);
export const ACTION_ADD_EVENT_EXCEPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.actions.addEventException',
{
defaultMessage: 'Add Endpoint event exception',
}
);
export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException',
{

View file

@ -26,6 +26,8 @@ export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails';
export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints';
/** Namespace within the Management state where trusted apps page state is maintained */
export const MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE = 'trustedApps';
/** Namespace within the Management state where event filters page state is maintained */
export const MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE = 'eventFilters';
export const MANAGEMENT_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 50];
export const MANAGEMENT_DEFAULT_PAGE = 0;

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
ExceptionListType,
ExceptionListTypeEnum,
EXCEPTION_LIST_URL,
EXCEPTION_LIST_ITEM_URL,
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_EVENT_FILTERS_LIST_NAME,
ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
} from '../../../../common/shared_imports';
export const EVENT_FILTER_LIST_TYPE: ExceptionListType = ExceptionListTypeEnum.ENDPOINT_EVENTS;
export const EVENT_FILTER_LIST = {
name: ENDPOINT_EVENT_FILTERS_LIST_NAME,
namespace_type: 'agnostic',
description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
type: EVENT_FILTER_LIST_TYPE,
};
export { ENDPOINT_EVENT_FILTERS_LIST_ID, EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL };

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpStart } from 'kibana/public';
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { Immutable } from '../../../../../common/endpoint/types';
import { EVENT_FILTER_LIST, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../constants';
export interface EventFiltersService {
addEventFilters(
exception: Immutable<ExceptionListItemSchema | CreateExceptionListItemSchema>
): Promise<ExceptionListItemSchema>;
}
export class EventFiltersHttpService implements EventFiltersService {
private listHasBeenCreated: boolean;
constructor(private http: HttpStart) {
this.listHasBeenCreated = false;
}
private async createEndpointEventList() {
try {
await this.http.post<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(EVENT_FILTER_LIST),
});
} catch (err) {
// Ignore 409 errors. List already created
if (err.response.status === 409) this.listHasBeenCreated = true;
else throw err;
}
}
private async httpWrapper() {
if (!this.listHasBeenCreated) await this.createEndpointEventList();
return this.http;
}
async addEventFilters(exception: ExceptionListItemSchema | CreateExceptionListItemSchema) {
return (await this.httpWrapper()).post<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
body: JSON.stringify(exception),
});
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { AsyncResourceState } from '../../../state/async_resource_state';
export interface EventFiltersListPageState {
entries: ExceptionListItemSchema[];
form: {
entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined;
hasNameError: boolean;
hasItemsError: boolean;
submissionResourceState: AsyncResourceState<ExceptionListItemSchema>;
};
}

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Action } from 'redux';
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { AsyncResourceState } from '../../../state/async_resource_state';
export type EventFiltersInitForm = Action<'eventFiltersInitForm'> & {
payload: {
entry: ExceptionListItemSchema | CreateExceptionListItemSchema;
};
};
export type EventFiltersChangeForm = Action<'eventFiltersChangeForm'> & {
payload: {
entry: ExceptionListItemSchema | CreateExceptionListItemSchema;
hasNameError?: boolean;
hasItemsError?: boolean;
};
};
export type EventFiltersCreateStart = Action<'eventFiltersCreateStart'>;
export type EventFiltersCreateSuccess = Action<'eventFiltersCreateSuccess'> & {
payload: {
exception: ExceptionListItemSchema;
};
};
export type EventFiltersCreateError = Action<'eventFiltersCreateError'>;
export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged'> & {
payload: AsyncResourceState<ExceptionListItemSchema>;
};
export type EventFiltersPageAction =
| EventFiltersCreateStart
| EventFiltersInitForm
| EventFiltersChangeForm
| EventFiltersCreateStart
| EventFiltersCreateSuccess
| EventFiltersCreateError
| EventFiltersFormStateChanged;

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EventFiltersListPageState } from '../state';
export const initialEventFiltersPageState = (): EventFiltersListPageState => ({
entries: [],
form: {
entry: undefined,
hasNameError: false,
hasItemsError: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
},
});

View file

@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { applyMiddleware, createStore, Store } from 'redux';
import {
createSpyMiddleware,
MiddlewareActionSpyHelper,
} from '../../../../common/store/test_utils';
import { AppAction } from '../../../../common/store/actions';
import { createEventFiltersPageMiddleware } from './middleware';
import { eventFiltersPageReducer } from './reducer';
import { EventFiltersService } from '../service';
import { EventFiltersListPageState } from '../state';
import { initialEventFiltersPageState } from './builders';
import { getInitialExceptionFromEvent } from './utils';
import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils';
const initialState: EventFiltersListPageState = initialEventFiltersPageState();
const createEventFiltersServiceMock = (): jest.Mocked<EventFiltersService> => ({
addEventFilters: jest.fn(),
});
const createStoreSetup = (eventFiltersService: EventFiltersService) => {
const spyMiddleware = createSpyMiddleware<EventFiltersListPageState>();
return {
spyMiddleware,
store: createStore(
eventFiltersPageReducer,
applyMiddleware(
createEventFiltersPageMiddleware(eventFiltersService),
spyMiddleware.actionSpyMiddleware
)
),
};
};
describe('middleware', () => {
describe('initial state', () => {
it('sets initial state properly', async () => {
expect(createStoreSetup(createEventFiltersServiceMock()).store.getState()).toStrictEqual(
initialState
);
});
});
describe('submit creation event filter', () => {
let service: jest.Mocked<EventFiltersService>;
let store: Store<EventFiltersListPageState>;
let spyMiddleware: MiddlewareActionSpyHelper<EventFiltersListPageState, AppAction>;
beforeEach(() => {
service = createEventFiltersServiceMock();
const storeSetup = createStoreSetup(service);
store = storeSetup.store as Store<EventFiltersListPageState>;
spyMiddleware = storeSetup.spyMiddleware;
});
it('does not submit when entry is undefined', async () => {
store.dispatch({ type: 'eventFiltersCreateStart' });
expect(store.getState()).toStrictEqual({
...initialState,
form: {
...store.getState().form,
submissionResourceState: { type: 'UninitialisedResourceState' },
},
});
});
it('does submit when entry is not undefined', async () => {
service.addEventFilters.mockResolvedValue(createdEventFilterEntryMock());
const entry = getInitialExceptionFromEvent(ecsEventMock());
store.dispatch({
type: 'eventFiltersInitForm',
payload: { entry },
});
store.dispatch({ type: 'eventFiltersCreateStart' });
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
expect(store.getState()).toStrictEqual({
...initialState,
form: {
...store.getState().form,
submissionResourceState: {
type: 'LoadedResourceState',
data: createdEventFilterEntryMock(),
},
},
});
});
it('does throw error when creating', async () => {
service.addEventFilters.mockRejectedValue({
body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' },
});
const entry = getInitialExceptionFromEvent(ecsEventMock());
store.dispatch({
type: 'eventFiltersInitForm',
payload: { entry },
});
store.dispatch({ type: 'eventFiltersCreateStart' });
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
expect(store.getState()).toStrictEqual({
...initialState,
form: {
...store.getState().form,
submissionResourceState: {
type: 'FailedResourceState',
lastLoadedState: undefined,
error: {
error: 'Internal Server Error',
message: 'error message',
statusCode: 500,
},
},
},
});
});
});
});

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AppAction } from '../../../../common/store/actions';
import {
ImmutableMiddleware,
ImmutableMiddlewareAPI,
ImmutableMiddlewareFactory,
} from '../../../../common/store';
import { EventFiltersHttpService, EventFiltersService } from '../service';
import { EventFiltersListPageState } from '../state';
import { getLastLoadedResourceState } from '../../../state/async_resource_state';
import { CreateExceptionListItemSchema, transformNewItemOutput } from '../../../../shared_imports';
const eventFiltersCreate = async (
store: ImmutableMiddlewareAPI<EventFiltersListPageState, AppAction>,
eventFiltersService: EventFiltersService
) => {
const submissionResourceState = store.getState().form.submissionResourceState;
try {
const formEntry = store.getState().form.entry;
if (!formEntry) return;
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'LoadingResourceState',
previousState: { type: 'UninitialisedResourceState' },
},
});
const sanitizedEntry = transformNewItemOutput(formEntry as CreateExceptionListItemSchema);
const exception = await eventFiltersService.addEventFilters(sanitizedEntry);
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'LoadedResourceState',
data: exception,
},
});
} catch (error) {
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'FailedResourceState',
error: error.body || error,
lastLoadedState: getLastLoadedResourceState(submissionResourceState),
},
});
}
};
export const createEventFiltersPageMiddleware = (
eventFiltersService: EventFiltersService
): ImmutableMiddleware<EventFiltersListPageState, AppAction> => {
return (store) => (next) => async (action) => {
next(action);
if (action.type === 'eventFiltersCreateStart') {
await eventFiltersCreate(store, eventFiltersService);
}
};
};
export const eventFiltersPageMiddlewareFactory: ImmutableMiddlewareFactory<EventFiltersListPageState> = (
coreStart
) => createEventFiltersPageMiddleware(new EventFiltersHttpService(coreStart.http));

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { initialEventFiltersPageState } from './builders';
import { eventFiltersPageReducer } from './reducer';
import { getInitialExceptionFromEvent } from './utils';
import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils';
const initialState = initialEventFiltersPageState();
describe('reducer', () => {
describe('EventFiltersForm', () => {
it('sets the initial form values', () => {
const entry = getInitialExceptionFromEvent(ecsEventMock());
const result = eventFiltersPageReducer(initialState, {
type: 'eventFiltersInitForm',
payload: { entry },
});
expect(result).toStrictEqual({
...initialState,
form: {
...initialState.form,
entry,
hasNameError: !entry.name,
submissionResourceState: {
type: 'UninitialisedResourceState',
},
},
});
});
it('change form values', () => {
const entry = getInitialExceptionFromEvent(ecsEventMock());
const nameChanged = 'name changed';
const result = eventFiltersPageReducer(initialState, {
type: 'eventFiltersChangeForm',
payload: { entry: { ...entry, name: nameChanged } },
});
expect(result).toStrictEqual({
...initialState,
form: {
...initialState.form,
entry: {
...entry,
name: nameChanged,
},
hasNameError: false,
submissionResourceState: {
type: 'UninitialisedResourceState',
},
},
});
});
it('change form status', () => {
const result = eventFiltersPageReducer(initialState, {
type: 'eventFiltersFormStateChanged',
payload: {
type: 'LoadedResourceState',
data: createdEventFilterEntryMock(),
},
});
expect(result).toStrictEqual({
...initialState,
form: {
...initialState.form,
submissionResourceState: {
type: 'LoadedResourceState',
data: createdEventFilterEntryMock(),
},
},
});
});
});
});

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ImmutableReducer } from '../../../../common/store';
import { Immutable } from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import {
EventFiltersInitForm,
EventFiltersChangeForm,
EventFiltersFormStateChanged,
} from './action';
import { EventFiltersListPageState } from '../state';
import { initialEventFiltersPageState } from './builders';
type StateReducer = ImmutableReducer<EventFiltersListPageState, AppAction>;
type CaseReducer<T extends AppAction> = (
state: Immutable<EventFiltersListPageState>,
action: Immutable<T>
) => Immutable<EventFiltersListPageState>;
const eventFiltersInitForm: CaseReducer<EventFiltersInitForm> = (state, action) => {
return {
...state,
form: {
...state.form,
entry: action.payload.entry,
hasNameError: !action.payload.entry.name,
submissionResourceState: {
type: 'UninitialisedResourceState',
},
},
};
};
const eventFiltersChangeForm: CaseReducer<EventFiltersChangeForm> = (state, action) => {
return {
...state,
form: {
...state.form,
entry: action.payload.entry,
hasItemsError:
action.payload.hasItemsError !== undefined
? action.payload.hasItemsError
: state.form.hasItemsError,
hasNameError:
action.payload.hasNameError !== undefined
? action.payload.hasNameError
: state.form.hasNameError,
},
};
};
const eventFiltersFormStateChanged: CaseReducer<EventFiltersFormStateChanged> = (state, action) => {
return {
...state,
form: {
...state.form,
submissionResourceState: action.payload,
},
};
};
export const eventFiltersPageReducer: StateReducer = (
state = initialEventFiltersPageState(),
action
) => {
switch (action.type) {
case 'eventFiltersInitForm':
return eventFiltersInitForm(state, action);
case 'eventFiltersChangeForm':
return eventFiltersChangeForm(state, action);
case 'eventFiltersFormStateChanged':
return eventFiltersFormStateChanged(state, action);
}
return state;
};

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EventFiltersListPageState } from '../state';
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { ServerApiError } from '../../../../common/types';
import {
isLoadingResourceState,
isLoadedResourceState,
isFailedResourceState,
} from '../../../state/async_resource_state';
export const getFormEntry = (
state: EventFiltersListPageState
): CreateExceptionListItemSchema | ExceptionListItemSchema | undefined => {
return state.form.entry;
};
export const getFormHasError = (state: EventFiltersListPageState): boolean => {
return state.form.hasItemsError || state.form.hasNameError;
};
export const isCreationInProgress = (state: EventFiltersListPageState): boolean => {
return isLoadingResourceState(state.form.submissionResourceState);
};
export const isCreationSuccessful = (state: EventFiltersListPageState): boolean => {
return isLoadedResourceState(state.form.submissionResourceState);
};
export const getCreationError = (state: EventFiltersListPageState): ServerApiError | undefined => {
const submissionResourceState = state.form.submissionResourceState;
return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined;
};

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { initialEventFiltersPageState } from './builders';
import { getFormEntry, getFormHasError } from './selector';
import { ecsEventMock } from '../test_utils';
import { getInitialExceptionFromEvent } from './utils';
const initialState = initialEventFiltersPageState();
describe('selectors', () => {
describe('getFormEntry()', () => {
it('returns undefined when there is no entry', () => {
expect(getFormEntry(initialState)).toBe(undefined);
});
it('returns entry when there is an entry on form', () => {
const entry = getInitialExceptionFromEvent(ecsEventMock());
const state = {
...initialState,
form: {
...initialState.form,
entry,
},
};
expect(getFormEntry(state)).toBe(entry);
});
});
describe('getFormHasError()', () => {
it('returns false when there is no entry', () => {
expect(getFormHasError(initialState)).toBeFalsy();
});
it('returns true when entry with name error', () => {
const state = {
...initialState,
form: {
...initialState.form,
hasNameError: true,
},
};
expect(getFormHasError(state)).toBeTruthy();
});
it('returns true when entry with item error', () => {
const state = {
...initialState,
form: {
...initialState.form,
hasItemsError: true,
},
};
expect(getFormHasError(state)).toBeTruthy();
});
it('returns true when entry with item error and name error', () => {
const state = {
...initialState,
form: {
...initialState.form,
hasItemsError: true,
hasNameError: true,
},
};
expect(getFormHasError(state)).toBeTruthy();
});
it('returns false when entry without errors', () => {
const state = {
...initialState,
form: {
...initialState.form,
hasItemsError: false,
hasNameError: false,
},
};
expect(getFormHasError(state)).toBeFalsy();
});
});
});

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import uuid from 'uuid';
import { CreateExceptionListItemSchema } from '../../../../shared_imports';
import { Ecs } from '../../../../../common/ecs';
import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../constants';
export const getInitialExceptionFromEvent = (data: Ecs): CreateExceptionListItemSchema => ({
comments: [],
description: '',
entries:
data.event && data.process
? [
{
field: 'event.category',
operator: 'included',
type: 'match',
value: (data.event.category ?? [])[0],
},
{
field: 'process.executable',
operator: 'included',
type: 'match',
value: (data.process.executable ?? [])[0],
},
]
: [],
item_id: undefined,
list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
meta: {
temporaryUuid: uuid.v4(),
},
name: '',
namespace_type: 'agnostic',
tags: [],
type: 'simple',
// TODO: Try to fix this type casting
os_types: [(data.host ? data.host.os?.family ?? [] : [])[0] as 'windows' | 'linux' | 'macos'],
});

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { combineReducers, createStore } from 'redux';
import { Ecs } from '../../../../../common/ecs';
import {
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
} from '../../../common/constants';
import { ExceptionListItemSchema } from '../../../../shared_imports';
import { eventFiltersPageReducer } from '../store/reducer';
export const createGlobalNoMiddlewareStore = () => {
return createStore(
combineReducers({
[MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer,
}),
})
);
};
export const ecsEventMock = (): Ecs => ({
_id: 'unLfz3gB2mJZsMY3ytx3',
timestamp: '2021-04-14T15:34:15.330Z',
_index: '.ds-logs-endpoint.events.process-default-2021.04.12-000001',
event: {
category: ['network'],
id: ['2c4f51be-7736-4ab8-a255-54e7023c4653'],
kind: ['event'],
type: ['start'],
},
host: {
name: ['Host-tvs68wo3qc'],
os: {
family: ['windows'],
},
id: ['a563b365-2bee-40df-adcd-ae84d889f523'],
ip: ['10.242.233.187'],
},
user: {
name: ['uegem17ws4'],
domain: ['hr8jofpkxp'],
},
agent: {
type: ['endpoint'],
},
process: {
hash: {
md5: ['c4653870-99b8-4f36-abde-24812d08a289'],
},
parent: {
pid: [4852],
},
pid: [3652],
name: ['lsass.exe'],
args: ['"C:\\lsass.exe" \\6z9'],
entity_id: ['9qotd1i8rf'],
executable: ['C:\\lsass.exe'],
},
});
export const createdEventFilterEntryMock = (): ExceptionListItemSchema => ({
_version: 'WzM4MDgsMV0=',
meta: undefined,
comments: [],
created_at: '2021-04-19T10:30:36.425Z',
created_by: 'elastic',
description: '',
entries: [
{ field: 'event.category', operator: 'included', type: 'match', value: 'process' },
{ field: 'process.executable', operator: 'included', type: 'match', value: 'C:\\iexlorer.exe' },
],
id: '47598790-a0fa-11eb-8458-69ac85f1fa18',
item_id: '93f65a04-6f5c-4f9e-9be5-e674b3c2392f',
list_id: '.endpointEventFilterList',
name: 'Test',
namespace_type: 'agnostic',
os_types: ['windows'],
tags: [],
tie_breaker_id: 'c42f3dbd-292f-49e8-83ab-158d024a4d8b',
type: 'simple',
updated_at: '2021-04-19T10:30:36.428Z',
updated_by: 'elastic',
});

View file

@ -0,0 +1,118 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EventFiltersForm } from '.';
import { RenderResult, act, render } from '@testing-library/react';
import { fireEvent } from '@testing-library/dom';
import { stubIndexPatternWithFields } from 'src/plugins/data/common/index_patterns/index_pattern.stub';
import { getInitialExceptionFromEvent } from '../../../store/utils';
import { Provider } from 'react-redux';
import { useFetchIndex } from '../../../../../../common/containers/source';
import { ThemeProvider } from 'styled-components';
import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils';
import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock';
import { NAME_ERROR, NAME_PLACEHOLDER } from './translations';
import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana';
jest.mock('../../../../../../common/lib/kibana');
jest.mock('../../../../../../common/containers/source');
const mockTheme = getMockTheme({
eui: {
paddingSizes: { m: '2' },
},
});
describe('Event filter form', () => {
let component: RenderResult;
let store: ReturnType<typeof createGlobalNoMiddlewareStore>;
const renderForm = () => {
const Wrapper: React.FC = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme={mockTheme}>{children}</ThemeProvider>
</Provider>
);
return render(<EventFiltersForm />, { wrapper: Wrapper });
};
const renderComponentWithdata = () => {
const entry = getInitialExceptionFromEvent(ecsEventMock());
act(() => {
store.dispatch({
type: 'eventFiltersInitForm',
payload: { entry },
});
});
return renderForm();
};
beforeEach(() => {
(useFetchIndex as jest.Mock).mockImplementation(() => [
false,
{
indexPatterns: stubIndexPatternWithFields,
},
]);
(useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' });
(useKibana as jest.Mock).mockReturnValue({
services: {
http: {},
data: {},
notifications: {},
},
});
store = createGlobalNoMiddlewareStore();
});
it('should renders correctly without data', () => {
component = renderForm();
expect(component.getByTestId('loading-spinner')).not.toBeNull();
});
it('should renders correctly with data', () => {
component = renderComponentWithdata();
expect(component.getByText(ecsEventMock().process!.executable![0])).not.toBeNull();
expect(component.getByText(NAME_ERROR)).not.toBeNull();
});
it('should change name', async () => {
component = renderComponentWithdata();
const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER);
act(() => {
fireEvent.change(nameInput, {
target: {
value: 'Exception name',
},
});
});
expect(store.getState()!.management!.eventFilters!.form!.entry!.name).toBe('Exception name');
expect(store.getState()!.management!.eventFilters!.form!.hasNameError).toBeFalsy();
});
it('should change comments', async () => {
component = renderComponentWithdata();
const commentInput = component.getByPlaceholderText('Add a new comment...');
act(() => {
fireEvent.change(commentInput, {
target: {
value: 'Exception comment',
},
});
});
expect(store.getState()!.management!.eventFilters!.form!.entry!.comments![0].comment).toBe(
'Exception comment'
);
});
});

View file

@ -0,0 +1,207 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import {
EuiFieldText,
EuiSpacer,
EuiForm,
EuiFormRow,
EuiSuperSelect,
EuiSuperSelectOption,
EuiText,
} from '@elastic/eui';
import { isEmpty } from 'lodash';
import { OperatingSystem } from '../../../../../../../common/endpoint/types';
import { AddExceptionComments } from '../../../../../../common/components/exceptions/add_exception_comments';
import { filterIndexPatterns } from '../../../../../../common/components/exceptions/helpers';
import { Loader } from '../../../../../../common/components/loader';
import { useKibana } from '../../../../../../common/lib/kibana';
import { useFetchIndex } from '../../../../../../common/containers/source';
import { AppAction } from '../../../../../../common/store/actions';
import { ExceptionListItemSchema, ExceptionBuilder } from '../../../../../../shared_imports';
import { useEventFiltersSelector } from '../../hooks';
import { getFormEntry } from '../../../store/selector';
import {
FORM_DESCRIPTION,
NAME_LABEL,
NAME_ERROR,
NAME_PLACEHOLDER,
OS_LABEL,
RULE_NAME,
} from './translations';
import { OS_TITLES } from '../../../../../common/translations';
import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../../constants';
const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
OperatingSystem.MAC,
OperatingSystem.WINDOWS,
OperatingSystem.LINUX,
];
interface EventFiltersFormProps {
allowSelectOs?: boolean;
}
export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo(
({ allowSelectOs = false }) => {
const { http, data } = useKibana().services;
const dispatch = useDispatch<Dispatch<AppAction>>();
const exception = useEventFiltersSelector(getFormEntry);
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(['logs-endpoint.events.*']);
const osOptions: Array<EuiSuperSelectOption<OperatingSystem>> = useMemo(
() => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })),
[]
);
const [hasNameError, setHasNameError] = useState(!exception || !exception.name);
const [comment, setComment] = useState<string>('');
const handleOnBuilderChange = useCallback(
(arg: ExceptionBuilder.OnChangeProps) => {
if (isEmpty(arg.exceptionItems)) return;
dispatch({
type: 'eventFiltersChangeForm',
payload: {
entry: {
...arg.exceptionItems[0],
name: exception?.name ?? '',
comments: exception?.comments ?? [],
},
hasItemsError: arg.errorExists,
},
});
},
[dispatch, exception?.name, exception?.comments]
);
const handleOnChangeName = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (!exception) return;
setHasNameError(!e.target.value);
dispatch({
type: 'eventFiltersChangeForm',
payload: {
entry: { ...exception, name: e.target.value.toString() },
hasNameError: !e.target.value,
},
});
},
[dispatch, exception]
);
const handleOnChangeComment = useCallback(
(value: string) => {
setComment(value);
if (!exception) return;
dispatch({
type: 'eventFiltersChangeForm',
payload: {
entry: { ...exception, comments: [{ comment: value }] },
},
});
},
[dispatch, exception, setComment]
);
const exceptionBuilderComponentMemo = useMemo(
() => (
<ExceptionBuilder.ExceptionBuilderComponent
allowLargeValueLists
httpService={http}
autocompleteService={data.autocomplete}
exceptionListItems={[exception as ExceptionListItemSchema]}
listType={EVENT_FILTER_LIST_TYPE}
listId={ENDPOINT_EVENT_FILTERS_LIST_ID}
listNamespaceType={'agnostic'}
ruleName={RULE_NAME}
indexPatterns={indexPatterns}
isOrDisabled={false}
isAndDisabled={false}
isNestedDisabled={false}
data-test-subj="alert-exception-builder"
id-aria="alert-exception-builder"
onChange={handleOnBuilderChange}
listTypeSpecificIndexPatternFilter={filterIndexPatterns}
/>
),
[data, handleOnBuilderChange, http, indexPatterns, exception]
);
const nameInputMemo = useMemo(
() => (
<EuiFormRow label={NAME_LABEL} fullWidth isInvalid={hasNameError} error={NAME_ERROR}>
<EuiFieldText
id="eventFiltersFormInputName"
placeholder={NAME_PLACEHOLDER}
defaultValue={exception?.name ?? ''}
onChange={handleOnChangeName}
fullWidth
aria-label={NAME_PLACEHOLDER}
required
maxLength={256}
/>
</EuiFormRow>
),
[hasNameError, exception?.name, handleOnChangeName]
);
const osInputMemo = useMemo(
() => (
<EuiFormRow label={OS_LABEL} fullWidth>
<EuiSuperSelect
name="os"
options={osOptions}
valueOfSelected={
exception?.os_types ? exception.os_types[0] : OS_TITLES[OperatingSystem.WINDOWS]
}
// TODO: To be implemented when adding update/create from scratch action
// onChange={}}
/>
</EuiFormRow>
),
[exception?.os_types, osOptions]
);
const commentsInputMemo = useMemo(
() => (
<AddExceptionComments
newCommentValue={comment}
newCommentOnChange={handleOnChangeComment}
/>
),
[comment, handleOnChangeComment]
);
return !isIndexPatternLoading && exception ? (
<EuiForm component="div">
<EuiText size="s">{FORM_DESCRIPTION}</EuiText>
<EuiSpacer size="s" />
{nameInputMemo}
<EuiSpacer />
{allowSelectOs ? (
<>
{osInputMemo}
<EuiSpacer />
</>
) : null}
{exceptionBuilderComponentMemo}
<EuiSpacer />
{commentsInputMemo}
</EuiForm>
) : (
<Loader size="xl" />
);
}
);
EventFiltersForm.displayName = 'EventFiltersForm';

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const FORM_DESCRIPTION = i18n.translate(
'xpack.securitySolution.eventFilter.modal.description',
{
defaultMessage: "Events are filtered when the rule's conditions are met:",
}
);
export const NAME_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.eventFilter.form.name.placeholder',
{
defaultMessage: 'Event exception name',
}
);
export const NAME_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.name.label', {
defaultMessage: 'Name your event exception',
});
export const NAME_ERROR = i18n.translate('xpack.securitySolution.eventFilter.form.name.error', {
defaultMessage: "The name can't be empty",
});
export const OS_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.os.label', {
defaultMessage: 'Seelct OS',
});
export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form.rule.name', {
defaultMessage: 'Endpoint Event Filtering',
});

View file

@ -0,0 +1,170 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EventFiltersModal } from '.';
import { RenderResult, act, render } from '@testing-library/react';
import { fireEvent } from '@testing-library/dom';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils';
import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock';
import { MODAL_TITLE, MODAL_SUBTITLE, ACTIONS_CONFIRM, ACTIONS_CANCEL } from './translations';
import {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
} from '../../../../../../shared_imports';
jest.mock('../form');
jest.mock('../../hooks', () => {
const originalModule = jest.requireActual('../../hooks');
const useEventFiltersNotification = jest.fn().mockImplementation(() => {});
return {
...originalModule,
useEventFiltersNotification,
};
});
const mockTheme = getMockTheme({
eui: {
paddingSizes: { m: '2' },
euiBreakpoints: { l: '2' },
},
});
describe('Event filter modal', () => {
let component: RenderResult;
let store: ReturnType<typeof createGlobalNoMiddlewareStore>;
let onCancelMock: jest.Mock;
const renderForm = () => {
const Wrapper: React.FC = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme={mockTheme}>{children}</ThemeProvider>
</Provider>
);
return render(<EventFiltersModal data={ecsEventMock()} onCancel={onCancelMock} />, {
wrapper: Wrapper,
});
};
beforeEach(() => {
store = createGlobalNoMiddlewareStore();
onCancelMock = jest.fn();
});
it('should renders correctly', () => {
component = renderForm();
expect(component.getAllByText(MODAL_TITLE)).not.toBeNull();
expect(component.getByText(MODAL_SUBTITLE)).not.toBeNull();
expect(component.getAllByText(ACTIONS_CONFIRM)).not.toBeNull();
expect(component.getByText(ACTIONS_CANCEL)).not.toBeNull();
});
it('should dispatch action to init form store on mount', () => {
component = renderForm();
expect(store.getState()!.management!.eventFilters!.form!.entry).not.toBeNull();
});
it('should confirm form when button is disabled', () => {
component = renderForm();
const confirmButton = component.getByTestId('add-exception-confirm-button');
act(() => {
fireEvent.click(confirmButton);
});
expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe(
'UninitialisedResourceState'
);
});
it('should confirm form when button is enabled', () => {
component = renderForm();
store.dispatch({
type: 'eventFiltersChangeForm',
payload: {
entry: {
...(store.getState()!.management!.eventFilters!.form!
.entry as CreateExceptionListItemSchema),
name: 'test',
},
hasNameError: false,
},
});
const confirmButton = component.getByTestId('add-exception-confirm-button');
act(() => {
fireEvent.click(confirmButton);
});
expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe(
'UninitialisedResourceState'
);
expect(confirmButton.hasAttribute('disabled')).toBeFalsy();
});
it('should close when exception has been submitted correctly', () => {
component = renderForm();
expect(onCancelMock).toHaveBeenCalledTimes(0);
act(() => {
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'LoadedResourceState',
data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema,
},
});
});
expect(onCancelMock).toHaveBeenCalledTimes(1);
});
it('should close when click on cancel button', () => {
component = renderForm();
const cancelButton = component.getByText(ACTIONS_CANCEL);
expect(onCancelMock).toHaveBeenCalledTimes(0);
act(() => {
fireEvent.click(cancelButton);
});
expect(onCancelMock).toHaveBeenCalledTimes(1);
});
it('should close when close modal', () => {
component = renderForm();
const modalCloseButton = component.getByLabelText('Closes this modal window');
expect(onCancelMock).toHaveBeenCalledTimes(0);
act(() => {
fireEvent.click(modalCloseButton);
});
expect(onCancelMock).toHaveBeenCalledTimes(1);
});
it('should prevent close when is loading action', () => {
component = renderForm();
act(() => {
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'LoadingResourceState',
previousState: { type: 'UninitialisedResourceState' },
},
});
});
const cancelButton = component.getByText(ACTIONS_CANCEL);
expect(onCancelMock).toHaveBeenCalledTimes(0);
act(() => {
fireEvent.click(cancelButton);
});
expect(onCancelMock).toHaveBeenCalledTimes(0);
});
});

View file

@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo, useEffect, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import styled, { css } from 'styled-components';
import {
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalFooter,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import { AppAction } from '../../../../../../common/store/actions';
import { Ecs } from '../../../../../../../common/ecs';
import { EventFiltersForm } from '../form';
import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks';
import {
getFormHasError,
isCreationInProgress,
isCreationSuccessful,
} from '../../../store/selector';
import { getInitialExceptionFromEvent } from '../../../store/utils';
import { MODAL_TITLE, MODAL_SUBTITLE, ACTIONS_CONFIRM, ACTIONS_CANCEL } from './translations';
export interface EventFiltersModalProps {
data: Ecs;
onCancel(): void;
}
const Modal = styled(EuiModal)`
${({ theme }) => css`
width: ${theme.eui.euiBreakpoints.l};
max-width: ${theme.eui.euiBreakpoints.l};
`}
`;
const ModalHeader = styled(EuiModalHeader)`
flex-direction: column;
align-items: flex-start;
`;
const ModalHeaderSubtitle = styled.div`
${({ theme }) => css`
color: ${theme.eui.euiColorMediumShade};
`}
`;
const ModalBodySection = styled.section`
${({ theme }) => css`
padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL};
overflow-y: scroll;
`}
`;
export const EventFiltersModal: React.FC<EventFiltersModalProps> = memo(({ data, onCancel }) => {
useEventFiltersNotification();
const dispatch = useDispatch<Dispatch<AppAction>>();
const formHasError = useEventFiltersSelector(getFormHasError);
const creationInProgress = useEventFiltersSelector(isCreationInProgress);
const creationSuccessful = useEventFiltersSelector(isCreationSuccessful);
useEffect(() => {
if (creationSuccessful) {
onCancel();
dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'UninitialisedResourceState',
},
});
}
}, [creationSuccessful, onCancel, dispatch]);
// Initialize the store with the event passed as prop to allow render the form. It acts as componentDidMount
useEffect(() => {
dispatch({
type: 'eventFiltersInitForm',
payload: { entry: getInitialExceptionFromEvent(data) },
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleOnCancel = useCallback(() => {
if (creationInProgress) return;
onCancel();
}, [creationInProgress, onCancel]);
const confirmButtonMemo = useMemo(
() => (
<EuiButton
data-test-subj="add-exception-confirm-button"
fill
disabled={formHasError || creationInProgress}
onClick={() => {
dispatch({ type: 'eventFiltersCreateStart' });
}}
isLoading={creationInProgress}
>
{ACTIONS_CONFIRM}
</EuiButton>
),
[dispatch, formHasError, creationInProgress]
);
return (
<Modal onClose={handleOnCancel} data-test-subj="add-exception-modal">
<ModalHeader>
<EuiModalHeaderTitle>{MODAL_TITLE}</EuiModalHeaderTitle>
<ModalHeaderSubtitle>{MODAL_SUBTITLE}</ModalHeaderSubtitle>
</ModalHeader>
<ModalBodySection>
<EventFiltersForm />
</ModalBodySection>
<EuiModalFooter>
<EuiButtonEmpty data-test-subj="cancelExceptionAddButton" onClick={handleOnCancel}>
{ACTIONS_CANCEL}
</EuiButtonEmpty>
{confirmButtonMemo}
</EuiModalFooter>
</Modal>
);
});
EventFiltersModal.displayName = 'EventFiltersModal';

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const MODAL_TITLE = i18n.translate('xpack.securitySolution.eventFilter.modal.title', {
defaultMessage: 'Add Endpoint Event Filter',
});
export const MODAL_SUBTITLE = i18n.translate('xpack.securitySolution.eventFilter.modal.subtitle', {
defaultMessage: 'Endpoint Security',
});
export const ACTIONS_CONFIRM = i18n.translate(
'xpack.securitySolution.eventFilter.modal.actions.confirm',
{
defaultMessage: 'Add Endpoint Event Filter',
}
);
export const ACTIONS_CANCEL = i18n.translate(
'xpack.securitySolution.eventFilter.modal.actions.cancel',
{
defaultMessage: 'cancel',
}
);

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { isCreationSuccessful, getFormEntry, getCreationError } from '../store/selector';
import { useToasts } from '../../../../common/lib/kibana';
import { getCreationSuccessMessage, getCreationErrorMessage } from './translations';
import { State } from '../../../../common/store';
import { EventFiltersListPageState } from '../state';
import {
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE as EVENT_FILTER_NS,
MANAGEMENT_STORE_GLOBAL_NAMESPACE as GLOBAL_NS,
} from '../../../common/constants';
export function useEventFiltersSelector<R>(selector: (state: EventFiltersListPageState) => R): R {
return useSelector((state: State) =>
selector(state[GLOBAL_NS][EVENT_FILTER_NS] as EventFiltersListPageState)
);
}
export const useEventFiltersNotification = () => {
const creationSuccessful = useEventFiltersSelector(isCreationSuccessful);
const creationError = useEventFiltersSelector(getCreationError);
const formEntry = useEventFiltersSelector(getFormEntry);
const toasts = useToasts();
const [wasAlreadyHandled] = useState(new WeakSet());
if (creationSuccessful && formEntry && !wasAlreadyHandled.has(formEntry)) {
wasAlreadyHandled.add(formEntry);
toasts.addSuccess(getCreationSuccessMessage(formEntry));
} else if (creationError && !wasAlreadyHandled.has(creationError)) {
wasAlreadyHandled.add(creationError);
toasts.addDanger(getCreationErrorMessage(creationError));
}
};

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { ServerApiError } from '../../../../common/types';
export const getCreationSuccessMessage = (
entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined
) => {
return i18n.translate('xpack.securitySolution.eventFilter.form.successToastTitle', {
defaultMessage: '"{name}" has been added to the event exceptions list.',
values: { name: entry?.name },
});
};
export const getCreationErrorMessage = (creationError: ServerApiError) => {
return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle', {
defaultMessage: 'There was an error creating the new exception: "{error}"',
values: { error: creationError.message },
});
};

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { Provider } from 'react-redux';
import { renderHook, act } from '@testing-library/react-hooks';
import { NotificationsStart } from 'kibana/public';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public/context';
import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../shared_imports';
import { createGlobalNoMiddlewareStore, ecsEventMock } from '../test_utils';
import { useEventFiltersNotification } from './hooks';
import { getCreationErrorMessage, getCreationSuccessMessage } from './translations';
import { getInitialExceptionFromEvent } from '../store/utils';
import {
getLastLoadedResourceState,
FailedResourceState,
} from '../../../state/async_resource_state';
const mockNotifications = () => coreMock.createStart({ basePath: '/mock' }).notifications;
const renderNotifications = (
store: ReturnType<typeof createGlobalNoMiddlewareStore>,
notifications: NotificationsStart
) => {
const Wrapper: React.FC = ({ children }) => (
<Provider store={store}>
<KibanaContextProvider services={{ notifications }}>{children}</KibanaContextProvider>
</Provider>
);
return renderHook(useEventFiltersNotification, { wrapper: Wrapper });
};
describe('EventFiltersNotification', () => {
it('renders correctly initially', () => {
const notifications = mockNotifications();
renderNotifications(createGlobalNoMiddlewareStore(), notifications);
expect(notifications.toasts.addSuccess).not.toBeCalled();
expect(notifications.toasts.addDanger).not.toBeCalled();
});
it('shows success notification when creation successful', () => {
const store = createGlobalNoMiddlewareStore();
const notifications = mockNotifications();
renderNotifications(store, notifications);
act(() => {
const entry = getInitialExceptionFromEvent(ecsEventMock());
store.dispatch({
type: 'eventFiltersInitForm',
payload: { entry },
});
});
act(() => {
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'LoadedResourceState',
data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema,
},
});
});
expect(notifications.toasts.addSuccess).toBeCalledWith(
getCreationSuccessMessage(
store.getState()!.management!.eventFilters!.form!.entry as CreateExceptionListItemSchema
)
);
expect(notifications.toasts.addDanger).not.toBeCalled();
});
it('shows error notification when creation fails', () => {
const store = createGlobalNoMiddlewareStore();
const notifications = mockNotifications();
renderNotifications(store, notifications);
act(() => {
const entry = getInitialExceptionFromEvent(ecsEventMock());
store.dispatch({
type: 'eventFiltersInitForm',
payload: { entry },
});
});
act(() => {
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
type: 'FailedResourceState',
error: { message: 'error message', statusCode: 500, error: 'error' },
lastLoadedState: getLastLoadedResourceState(
store.getState()!.management!.eventFilters!.form!.submissionResourceState
),
},
});
});
expect(notifications.toasts.addSuccess).not.toBeCalled();
expect(notifications.toasts.addDanger).toBeCalledWith(
getCreationErrorMessage(
(store.getState()!.management!.eventFilters!.form!
.submissionResourceState as FailedResourceState).error
)
);
});
});

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export * from './async_resource_state';
export * from '../../../state/async_resource_state';
export * from './trusted_apps_list_page_state';

View file

@ -6,7 +6,7 @@
*/
import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps';
import { AsyncResourceState } from '.';
import { AsyncResourceState } from '../../../state/async_resource_state';
import { GetPolicyListResponse } from '../../policy/types';
export interface Pagination {

View file

@ -15,8 +15,8 @@
* - update can fail due to multiple reasons and also needs to be communicated to the user
*/
import { Immutable } from '../../../../../common/endpoint/types';
import { ServerApiError } from '../../../../common/types';
import { Immutable } from '../../../common/endpoint/types';
import { ServerApiError } from '../../common/types';
/**
* Data type to represent uninitialised state of asynchronous resource.

View file

@ -15,10 +15,12 @@ import {
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
} from '../common/constants';
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware';
import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware';
import { eventFiltersPageMiddlewareFactory } from '../pages/event_filters/store/middleware';
type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE];
@ -42,5 +44,9 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
createSubStateSelector(MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE),
trustedAppsPageMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE),
eventFiltersPageMiddlewareFactory(coreStart, depsStart)
),
];
};

View file

@ -14,6 +14,7 @@ import {
MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
} from '../common/constants';
import { ImmutableCombineReducers } from '../../common/store';
import { Immutable } from '../../../common/endpoint/types';
@ -24,6 +25,8 @@ import {
} from '../pages/endpoint_hosts/store/reducer';
import { initialTrustedAppsPageState } from '../pages/trusted_apps/store/builders';
import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer';
import { initialEventFiltersPageState } from '../pages/event_filters/store/builders';
import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer';
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
@ -34,6 +37,7 @@ export const mockManagementState: Immutable<ManagementState> = {
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState,
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(),
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(),
};
/**
@ -43,4 +47,5 @@ export const managementReducer = immutableCombineReducers({
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer,
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer,
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer,
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer,
});

View file

@ -10,6 +10,7 @@ import { SecurityPageName } from '../app/types';
import { PolicyDetailsState } from './pages/policy/types';
import { EndpointState } from './pages/endpoint_hosts/types';
import { TrustedAppsListPageState } from './pages/trusted_apps/state';
import { EventFiltersListPageState } from './pages/event_filters/state';
/**
* The type for the management store global namespace. Used mostly internally to reference
@ -21,6 +22,7 @@ export type ManagementState = CombinedState<{
policyDetails: PolicyDetailsState;
endpoints: EndpointState;
trustedApps: TrustedAppsListPageState;
eventFilters: EventFiltersListPageState;
}>;
/**

View file

@ -59,4 +59,5 @@ export {
addEndpointExceptionList,
withOptionalSignal,
ExceptionBuilder,
transformNewItemOutput,
} from '../../lists/public';

View file

@ -17,6 +17,10 @@ import { EventColumnView } from './event_column_view';
import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer';
import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
jest.mock('../../../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../../../common/hooks/use_selector');
@ -29,6 +33,7 @@ jest.mock('../../../../../cases/components/timeline_actions/add_to_case_action',
});
describe('EventColumnView', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default);
const props = {

View file

@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { CellValueElementProps } from '../../cell_rendering';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
@ -96,6 +97,8 @@ export const EventColumnView = React.memo<Props>(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType
);
const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled');
// Each action button shall announce itself to screen readers via an `aria-label`
// in the following format:
// "button description, for the event in row {ariaRowindex}, with columns {columnValues}",
@ -183,7 +186,7 @@ export const EventColumnView = React.memo<Props>(
key="alert-context-menu"
ecsRowData={ecsData}
timelineId={timelineId}
disabled={eventType !== 'signal'}
disabled={eventType !== 'signal' && (!isEventFilteringEnabled || eventType !== 'raw')}
refetch={refetch}
onRuleChange={onRuleChange}
/>,
@ -205,6 +208,7 @@ export const EventColumnView = React.memo<Props>(
timelineId,
timelineType,
toggleShowNotes,
isEventFilteringEnabled,
]
);

View file

@ -21,6 +21,10 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { timelineActions } from '../../../store/timeline';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { defaultRowRenderers } from './renderers';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
jest.mock('../../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
const mockSort: Sort[] = [
{
@ -88,6 +92,8 @@ describe('Body', () => {
totalPages: 1,
};
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
describe('rendering', () => {
test('it renders the column headers', () => {
const wrapper = mount(