[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:
parent
59bd5e5b54
commit
bc240f0af7
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -39,3 +39,4 @@ export {
|
|||
UseExceptionListsSuccess,
|
||||
} from './exceptions/types';
|
||||
export * as ExceptionBuilder from './exceptions/components/builder/index';
|
||||
export { transformNewItemOutput } from './exceptions/transforms';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
]
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -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' },
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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));
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'],
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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',
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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));
|
||||
}
|
||||
};
|
|
@ -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 },
|
||||
});
|
||||
};
|
|
@ -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
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -5,5 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './async_resource_state';
|
||||
export * from '../../../state/async_resource_state';
|
||||
export * from './trusted_apps_list_page_state';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -59,4 +59,5 @@ export {
|
|||
addEndpointExceptionList,
|
||||
withOptionalSignal,
|
||||
ExceptionBuilder,
|
||||
transformNewItemOutput,
|
||||
} from '../../lists/public';
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue