mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[8.6] [Session view] file/network alerts UX enhancements (#144768)
## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. [mplement UX enhancements to process alerts for file/network alerts.](https://github.com/orgs/elastic/projects/828/views/31) - UX enhancements to show different alerts categories(process, file, and network) - Each alert category has an associated icon - Group alerts show danger icons - Filter menu will be shown when there at least two alert categories - Click an alert category from the filter menu will filter the alerts and update alert count status message. Network Alerts <img width="1703" alt="image" src="https://user-images.githubusercontent.com/17135495/200449915-6250aa0d-6e81-481f-9733-5f948b87b378.png"> File and Process Alerts <img width="1712" alt="image" src="https://user-images.githubusercontent.com/17135495/200452712-f6714b80-22a9-48fe-9f74-406e73482fc0.png"> Group View <img width="1410" alt="image" src="https://user-images.githubusercontent.com/17135495/200453470-eb8bb92f-773d-4bca-b20d-ea73f4f8b4f8.png"> List View <img width="370" alt="image" src="https://user-images.githubusercontent.com/17135495/200453547-3170799e-23a0-462a-9e38-c6a9fb6ba748.png"> ### Checklist Delete any items that are not applicable to this PR. - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [X] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
fc11d73e89
commit
b1bb5917de
22 changed files with 1440 additions and 51 deletions
|
@ -67,3 +67,10 @@ export const TTY_LINE_SPLITTER_REGEX = /(\r?\n|\r\n?|\x1b\[\d+;\d*[Hf]?)/gi;
|
|||
// when showing the count of alerts in details panel tab, if the number
|
||||
// exceeds ALERT_COUNT_THRESHOLD we put a + next to it, e.g 999+
|
||||
export const ALERT_COUNT_THRESHOLD = 999;
|
||||
export const ALERT_ICONS: { [key: string]: string } = {
|
||||
process: 'gear',
|
||||
file: 'document',
|
||||
network: 'globe',
|
||||
};
|
||||
export const DEFAULT_ALERT_FILTER_VALUE = 'all';
|
||||
export const ALERT = 'alert';
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
EventAction,
|
||||
EventKind,
|
||||
ProcessMap,
|
||||
AlertTypeCount,
|
||||
ProcessEventAlertCategory,
|
||||
} from '../../types/process_tree';
|
||||
|
||||
export const mockEvents: ProcessEvent[] = [
|
||||
|
@ -158,7 +160,7 @@ export const mockEvents: ProcessEvent[] = [
|
|||
},
|
||||
event: {
|
||||
action: EventAction.fork,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
kind: EventKind.event,
|
||||
id: '1',
|
||||
},
|
||||
|
@ -316,7 +318,7 @@ export const mockEvents: ProcessEvent[] = [
|
|||
},
|
||||
event: {
|
||||
action: EventAction.exec,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
kind: EventKind.event,
|
||||
id: '2',
|
||||
},
|
||||
|
@ -459,7 +461,7 @@ export const mockEvents: ProcessEvent[] = [
|
|||
},
|
||||
event: {
|
||||
action: EventAction.end,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
kind: EventKind.event,
|
||||
id: '3',
|
||||
},
|
||||
|
@ -622,7 +624,7 @@ export const mockEvents: ProcessEvent[] = [
|
|||
},
|
||||
event: {
|
||||
action: EventAction.end,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
kind: EventKind.event,
|
||||
id: '4',
|
||||
},
|
||||
|
@ -645,6 +647,12 @@ export const mockEvents: ProcessEvent[] = [
|
|||
},
|
||||
] as ProcessEvent[];
|
||||
|
||||
export const mockAlertTypeCounts: AlertTypeCount[] = [
|
||||
{ category: ProcessEventAlertCategory.file, count: 0 },
|
||||
{ category: ProcessEventAlertCategory.network, count: 2 },
|
||||
{ category: ProcessEventAlertCategory.process, count: 1 },
|
||||
];
|
||||
|
||||
export const mockAlerts: ProcessEvent[] = [
|
||||
{
|
||||
kibana: {
|
||||
|
@ -797,7 +805,7 @@ export const mockAlerts: ProcessEvent[] = [
|
|||
},
|
||||
},
|
||||
},
|
||||
name: '',
|
||||
name: 'vi',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
|
@ -811,7 +819,7 @@ export const mockAlerts: ProcessEvent[] = [
|
|||
},
|
||||
event: {
|
||||
action: EventAction.exec,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
kind: EventKind.signal,
|
||||
id: '5',
|
||||
},
|
||||
|
@ -998,7 +1006,7 @@ export const mockAlerts: ProcessEvent[] = [
|
|||
},
|
||||
event: {
|
||||
action: EventAction.end,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
kind: EventKind.signal,
|
||||
id: '6',
|
||||
},
|
||||
|
@ -1021,6 +1029,402 @@ export const mockAlerts: ProcessEvent[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockFileAlert = {
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
category: 'Custom Query Rule',
|
||||
consumer: 'siem',
|
||||
name: 'File telemetry',
|
||||
uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf',
|
||||
enabled: true,
|
||||
description: 'File telemetry',
|
||||
risk_score: 21,
|
||||
severity: 'low',
|
||||
query: "process.executable: '/usr/bin/vi'",
|
||||
},
|
||||
status: 'active',
|
||||
workflow_status: 'open',
|
||||
reason: 'process event created low alert File telemetry.',
|
||||
original_time: '2021-11-23T15:25:05.202Z',
|
||||
original_event: {
|
||||
action: 'exit',
|
||||
},
|
||||
uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75',
|
||||
},
|
||||
},
|
||||
'@timestamp': '2021-11-23T15:26:34.860Z',
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
process: {
|
||||
pid: 3535,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
exit_code: 137,
|
||||
executable: '/usr/bin/vi',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726',
|
||||
parent: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
session_leader: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entry_leader: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
group_leader: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
file: {
|
||||
path: '/home/jon/new_file.txt',
|
||||
extension: 'txt',
|
||||
name: 'new_file.txt',
|
||||
},
|
||||
event: {
|
||||
action: EventAction.exec,
|
||||
category: ['file'],
|
||||
kind: EventKind.signal,
|
||||
id: '6',
|
||||
},
|
||||
host: {
|
||||
architecture: 'x86_64',
|
||||
hostname: 'james-fleet-714-2',
|
||||
id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
|
||||
ip: ['127.0.0.1', '::1', '10.132.0.50', 'fe80::7d39:3147:4d9a:f809'],
|
||||
mac: ['42:01:0a:84:00:32'],
|
||||
name: 'james-fleet-714-2',
|
||||
os: {
|
||||
family: 'centos',
|
||||
full: 'CentOS 7.9.2009',
|
||||
kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
|
||||
name: 'Linux',
|
||||
platform: 'centos',
|
||||
version: '7.9.2009',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockNetworkAlert = {
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
category: 'Custom Query Rule',
|
||||
consumer: 'siem',
|
||||
name: 'Network telemetry',
|
||||
uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf',
|
||||
enabled: true,
|
||||
description: 'Network telemetry',
|
||||
risk_score: 21,
|
||||
severity: 'low',
|
||||
query: "process.executable: '/usr/bin/vi'",
|
||||
},
|
||||
status: 'active',
|
||||
workflow_status: 'open',
|
||||
reason: 'process event created low alert File telemetry.',
|
||||
original_time: '2021-11-23T15:25:05.202Z',
|
||||
original_event: {
|
||||
action: 'exit',
|
||||
},
|
||||
uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75',
|
||||
},
|
||||
},
|
||||
'@timestamp': '2021-11-23T15:26:34.860Z',
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
process: {
|
||||
pid: 3535,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
exit_code: 137,
|
||||
executable: '/usr/bin/vi',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726',
|
||||
parent: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
session_leader: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entry_leader: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
group_leader: {
|
||||
pid: 2442,
|
||||
user: {
|
||||
name: 'vagrant',
|
||||
id: '1000',
|
||||
},
|
||||
group: {
|
||||
id: '1000',
|
||||
name: 'vagrant',
|
||||
},
|
||||
executable: '/usr/bin/bash',
|
||||
command_line: 'bash',
|
||||
interactive: true,
|
||||
entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc',
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
name: '',
|
||||
args_count: 2,
|
||||
args: ['vi', 'cmd/config.ini'],
|
||||
working_directory: '/home/vagrant',
|
||||
start: '2021-11-23T15:26:34.860Z',
|
||||
tty: {
|
||||
char_device: {
|
||||
major: 8,
|
||||
minor: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
network: {
|
||||
transport: 'TCP',
|
||||
protocol: 'http',
|
||||
type: 'IP4',
|
||||
},
|
||||
destination: {
|
||||
address: '127.0.0.1',
|
||||
ip: '127.0.0.1',
|
||||
port: 2222,
|
||||
},
|
||||
source: {
|
||||
address: '128.32.0.1',
|
||||
ip: '128.32.0.1',
|
||||
port: 1111,
|
||||
},
|
||||
event: {
|
||||
action: EventAction.exec,
|
||||
category: ['network'],
|
||||
kind: EventKind.signal,
|
||||
id: '6',
|
||||
},
|
||||
host: {
|
||||
architecture: 'x86_64',
|
||||
hostname: 'james-fleet-714-2',
|
||||
id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d',
|
||||
ip: ['127.0.0.1', '::1', '10.132.0.50', 'fe80::7d39:3147:4d9a:f809'],
|
||||
mac: ['42:01:0a:84:00:32'],
|
||||
name: 'james-fleet-714-2',
|
||||
os: {
|
||||
family: 'centos',
|
||||
full: 'CentOS 7.9.2009',
|
||||
kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021',
|
||||
name: 'Linux',
|
||||
platform: 'centos',
|
||||
version: '7.9.2009',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockData: ProcessEventsPage[] = [
|
||||
{
|
||||
events: mockEvents,
|
||||
|
@ -1278,7 +1682,7 @@ export const childProcessMock: Process = {
|
|||
'@timestamp': '2021-11-23T15:25:05.210Z',
|
||||
event: {
|
||||
kind: EventKind.event,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
action: EventAction.exec,
|
||||
id: '1',
|
||||
},
|
||||
|
@ -1364,7 +1768,7 @@ export const processMock: Process = {
|
|||
'@timestamp': '2021-11-23T15:25:04.210Z',
|
||||
event: {
|
||||
kind: EventKind.event,
|
||||
category: 'process',
|
||||
category: ['process'],
|
||||
action: EventAction.exec,
|
||||
id: '2',
|
||||
},
|
||||
|
|
19
x-pack/plugins/session_view/common/translations.ts
Normal file
19
x-pack/plugins/session_view/common/translations.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 ALERT_TYPE_TOOLTIP_PROCESS = i18n.translate('xpack.sessionView.processTooltip', {
|
||||
defaultMessage: 'Process alert',
|
||||
});
|
||||
|
||||
export const ALERT_TYPE_TOOLTIP_NETWORK = i18n.translate('xpack.sessionView.networkTooltip', {
|
||||
defaultMessage: 'Network alert',
|
||||
});
|
||||
|
||||
export const ALERT_TYPE_TOOLTIP_FILE = i18n.translate('xpack.sessionView.fileTooltip', {
|
||||
defaultMessage: 'File alert',
|
||||
});
|
|
@ -12,6 +12,19 @@ export interface AlertStatusEventEntityIdMap {
|
|||
};
|
||||
}
|
||||
|
||||
export const enum ProcessEventAlertCategory {
|
||||
all = 'all',
|
||||
file = 'file',
|
||||
network = 'network',
|
||||
process = 'process',
|
||||
}
|
||||
|
||||
export interface AlertTypeCount {
|
||||
category: ProcessEventAlertCategory;
|
||||
count: number;
|
||||
}
|
||||
export type DefaultAlertFilterType = 'all';
|
||||
|
||||
export const enum EventKind {
|
||||
event = 'event',
|
||||
signal = 'signal',
|
||||
|
@ -156,14 +169,34 @@ export interface ProcessEventAlert {
|
|||
rule?: ProcessEventAlertRule;
|
||||
}
|
||||
|
||||
export interface ProcessEventIPAddress {
|
||||
address?: string;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface ProcessEventNetwork {
|
||||
type?: string;
|
||||
transport?: string;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
export interface ProcessEvent {
|
||||
'@timestamp'?: string;
|
||||
event?: {
|
||||
kind?: EventKind;
|
||||
category?: string;
|
||||
category?: string[];
|
||||
action?: EventAction;
|
||||
id?: string;
|
||||
};
|
||||
file?: {
|
||||
extension?: string;
|
||||
path?: string;
|
||||
name?: string;
|
||||
};
|
||||
network?: ProcessEventNetwork;
|
||||
destination?: ProcessEventIPAddress;
|
||||
source?: ProcessEventIPAddress;
|
||||
user?: User;
|
||||
group?: Group;
|
||||
host?: ProcessEventHost;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { ProcessEventAlertCategory } from '../types/process_tree';
|
||||
import { getAlertIconTooltipContent } from './alert_icon_tooltip_content';
|
||||
|
||||
describe('getAlertTypeTooltipContent(category)', () => {
|
||||
it('should display `File alert` for tooltip content', () => {
|
||||
expect(getAlertIconTooltipContent(ProcessEventAlertCategory.file)).toEqual('File alert');
|
||||
});
|
||||
|
||||
it('should display `Process alert` for tooltip content', () => {
|
||||
expect(getAlertIconTooltipContent(ProcessEventAlertCategory.process)).toEqual('Process alert');
|
||||
});
|
||||
|
||||
it('should display `Network alert` for tooltip content', () => {
|
||||
expect(getAlertIconTooltipContent(ProcessEventAlertCategory.network)).toEqual('Network alert');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { ProcessEventAlertCategory } from '../types/process_tree';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export const getAlertIconTooltipContent = (processEventAlertCategory: string) => {
|
||||
let tooltipContent = '';
|
||||
switch (processEventAlertCategory) {
|
||||
case ProcessEventAlertCategory.file:
|
||||
tooltipContent = i18n.ALERT_TYPE_TOOLTIP_FILE;
|
||||
break;
|
||||
case ProcessEventAlertCategory.network:
|
||||
tooltipContent = i18n.ALERT_TYPE_TOOLTIP_NETWORK;
|
||||
break;
|
||||
default:
|
||||
tooltipContent = i18n.ALERT_TYPE_TOOLTIP_PROCESS;
|
||||
}
|
||||
return tooltipContent;
|
||||
};
|
|
@ -16,15 +16,19 @@ import {
|
|||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
formatDate,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { ProcessEvent } from '../../../common/types/process_tree';
|
||||
import { getAlertIconTooltipContent } from '../../../common/utils/alert_icon_tooltip_content';
|
||||
import { ALERT_ICONS } from '../../../common/constants';
|
||||
import { ProcessEvent, ProcessEventAlertCategory } from '../../../common/types/process_tree';
|
||||
import { useStyles } from './styles';
|
||||
import { DetailPanelAlertActions } from '../detail_panel_alert_actions';
|
||||
import { dataOrDash } from '../../utils/data_or_dash';
|
||||
import { useDateFormat } from '../../hooks';
|
||||
|
||||
import { getAlertCategoryDisplayText } from '../../utils/alert_category_display_text';
|
||||
export const ALERT_LIST_ITEM_TEST_ID = 'sessionView:detailPanelAlertListItem';
|
||||
export const ALERT_LIST_ITEM_ARGS_TEST_ID = 'sessionView:detailPanelAlertListItemArgs';
|
||||
export const ALERT_LIST_ITEM_FILE_PATH_TEST_ID = 'sessionView:detailPanelAlertListItemFilePath';
|
||||
export const ALERT_LIST_ITEM_TIMESTAMP_TEST_ID = 'sessionView:detailPanelAlertListItemTimestamp';
|
||||
|
||||
interface DetailPanelAlertsListItemDeps {
|
||||
|
@ -57,9 +61,16 @@ export const DetailPanelAlertListItem = ({
|
|||
const uuid = rule?.uuid || '';
|
||||
const name = rule?.name || '';
|
||||
|
||||
const { args } = event.process ?? {};
|
||||
|
||||
const { args, name: processName } = event.process ?? {};
|
||||
const { event: processEvent } = event;
|
||||
const forceState = !isInvestigated ? 'open' : undefined;
|
||||
const category = processEvent?.category?.[0];
|
||||
const processEventAlertCategory = category ?? ProcessEventAlertCategory.process;
|
||||
const alertCategoryDetailDisplayText =
|
||||
category !== ProcessEventAlertCategory.process
|
||||
? `${dataOrDash(processName)} ${getAlertCategoryDisplayText(event, category)}`
|
||||
: dataOrDash(args?.join(' '));
|
||||
const alertIconTooltipContent = getAlertIconTooltipContent(processEventAlertCategory);
|
||||
|
||||
return minimal ? (
|
||||
<div data-test-subj={ALERT_LIST_ITEM_TEST_ID} css={styles.firstAlertPad}>
|
||||
|
@ -86,7 +97,9 @@ export const DetailPanelAlertListItem = ({
|
|||
hasShadow={false}
|
||||
borderRadius="m"
|
||||
>
|
||||
<EuiText size="xs">{dataOrDash(args?.join(' '))}</EuiText>
|
||||
<EuiText data-test-subj={ALERT_LIST_ITEM_ARGS_TEST_ID} size="xs">
|
||||
{alertCategoryDetailDisplayText}
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule css={styles.minimalHR} margin="m" size="full" />
|
||||
</div>
|
||||
|
@ -98,7 +111,14 @@ export const DetailPanelAlertListItem = ({
|
|||
buttonContent={
|
||||
<EuiText css={styles.alertTitleContainer} size="s">
|
||||
<p css={styles.alertTitle}>
|
||||
<EuiIcon color="danger" type="alert" css={styles.alertIcon} />
|
||||
<EuiToolTip position="top" content={alertIconTooltipContent}>
|
||||
<EuiIcon
|
||||
color="danger"
|
||||
type={ALERT_ICONS[processEventAlertCategory]}
|
||||
css={styles.alertIcon}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
||||
{dataOrDash(name)}
|
||||
</p>
|
||||
</EuiText>
|
||||
|
@ -126,7 +146,7 @@ export const DetailPanelAlertListItem = ({
|
|||
borderRadius="m"
|
||||
>
|
||||
<EuiText data-test-subj={ALERT_LIST_ITEM_ARGS_TEST_ID} size="xs">
|
||||
{dataOrDash(args?.join(' '))}
|
||||
{alertCategoryDetailDisplayText}
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
{isInvestigated && (
|
||||
|
|
|
@ -34,17 +34,26 @@ Object {
|
|||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<span
|
||||
color="danger"
|
||||
data-euiicon-type="alert"
|
||||
/>
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<span
|
||||
color="danger"
|
||||
data-euiicon-type="gear"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
class="euiText emotion-EuiText"
|
||||
css="[object Object]"
|
||||
>
|
||||
cmd test alert
|
||||
<div
|
||||
class="euiText emotion-EuiText"
|
||||
data-test-subj="sessionView:sessionViewAlertDetailRuleName-6bb22512e0e588d1a2449b61f164b216e366fba2de39e65d002ae734d71a6c38-text"
|
||||
>
|
||||
cmd test alert
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -119,17 +128,26 @@ Object {
|
|||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<span
|
||||
color="danger"
|
||||
data-euiicon-type="alert"
|
||||
/>
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<span
|
||||
color="danger"
|
||||
data-euiicon-type="gear"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
class="euiText emotion-EuiText"
|
||||
css="[object Object]"
|
||||
>
|
||||
cmd test alert
|
||||
<div
|
||||
class="euiText emotion-EuiText"
|
||||
data-test-subj="sessionView:sessionViewAlertDetailRuleName-6bb22512e0e588d1a2449b61f164b216e366fba2de39e65d002ae734d71a6c38-text"
|
||||
>
|
||||
cmd test alert
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock';
|
||||
import {
|
||||
mockAlerts,
|
||||
mockFileAlert,
|
||||
mockNetworkAlert,
|
||||
} from '../../../common/mocks/constants/session_view_process.mock';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { ProcessTreeAlertDeps, ProcessTreeAlert } from '.';
|
||||
|
||||
|
@ -63,6 +67,43 @@ describe('ProcessTreeAlerts component', () => {
|
|||
expect(selectAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should get alert rule name text content when alert category is process', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlert {...props} />);
|
||||
const alertText = renderResult.queryByTestId(
|
||||
`sessionView:sessionViewAlertDetailRuleName-${mockAlert.kibana?.alert?.uuid}-text`
|
||||
);
|
||||
|
||||
const categoryDetailPanel = renderResult.queryByTestId(
|
||||
`sessionView:sessionViewAlertDetail-${mockFileAlert.kibana?.alert?.uuid}-text`
|
||||
);
|
||||
|
||||
expect(alertText).toBeTruthy();
|
||||
expect(alertText).toHaveTextContent('cmd test alert');
|
||||
expect(categoryDetailPanel).toBeNull();
|
||||
});
|
||||
|
||||
it('should get file path for text content when alert category is file', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlert {...props} alert={mockFileAlert} />);
|
||||
const fileAlertText = renderResult.queryByTestId(
|
||||
`sessionView:sessionViewAlertDetail-${mockFileAlert.kibana?.alert?.uuid}-text`
|
||||
);
|
||||
expect(fileAlertText).toBeTruthy();
|
||||
expect(fileAlertText).toHaveTextContent('/home/jon/new_file.txt');
|
||||
});
|
||||
|
||||
it('should get network display text for text content when alert category is network', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlert {...props} alert={mockNetworkAlert} />);
|
||||
|
||||
const networkAlertText = renderResult.queryByTestId(
|
||||
`sessionView:sessionViewAlertDetail-${mockNetworkAlert.kibana?.alert?.uuid}-text`
|
||||
);
|
||||
expect(networkAlertText).toBeTruthy();
|
||||
|
||||
expect(networkAlertText).toHaveTextContent(
|
||||
`${mockNetworkAlert?.destination?.address}:${mockNetworkAlert?.destination?.port}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute onShowAlertDetails callback when clicking on expand button', async () => {
|
||||
const onShowAlertDetails = jest.fn();
|
||||
renderResult = mockedContext.render(
|
||||
|
|
|
@ -5,13 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiIcon, EuiText, EuiButtonIcon } from '@elastic/eui';
|
||||
import { ProcessEvent, ProcessEventAlert } from '../../../common/types/process_tree';
|
||||
import React, { useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiBadge,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { ALERT_ICONS } from '../../../common/constants';
|
||||
import {
|
||||
ProcessEvent,
|
||||
ProcessEventAlert,
|
||||
ProcessEventAlertCategory,
|
||||
} from '../../../common/types/process_tree';
|
||||
import { dataOrDash } from '../../utils/data_or_dash';
|
||||
import { getBadgeColorFromAlertStatus } from './helpers';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
import { getAlertCategoryDisplayText } from '../../utils/alert_category_display_text';
|
||||
import { getAlertIconTooltipContent } from '../../../common/utils/alert_icon_tooltip_content';
|
||||
export interface ProcessTreeAlertDeps {
|
||||
alert: ProcessEvent;
|
||||
isInvestigated: boolean;
|
||||
|
@ -32,7 +47,13 @@ export const ProcessTreeAlert = ({
|
|||
const styles = useStyles({ isInvestigated, isSelected });
|
||||
|
||||
const { event } = alert;
|
||||
|
||||
const { uuid, rule, workflow_status: status } = alert.kibana?.alert || {};
|
||||
const category = event?.category?.[0];
|
||||
const alertIconType = useMemo(() => {
|
||||
if (category && category in ALERT_ICONS) return ALERT_ICONS[category];
|
||||
return ALERT_ICONS.process;
|
||||
}, [category]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInvestigated && uuid) {
|
||||
|
@ -55,8 +76,10 @@ export const ProcessTreeAlert = ({
|
|||
if (!(alert.kibana && rule)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name } = rule;
|
||||
const processEventAlertCategory = category ?? ProcessEventAlertCategory.process;
|
||||
const alertCategoryDetailDisplayText = getAlertCategoryDisplayText(alert, category);
|
||||
const alertIconTooltipContent = getAlertIconTooltipContent(processEventAlertCategory);
|
||||
|
||||
return (
|
||||
<div key={uuid} css={styles.alert} data-id={uuid}>
|
||||
|
@ -76,12 +99,37 @@ export const ProcessTreeAlert = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="alert" color="danger" />
|
||||
<EuiToolTip position="top" content={alertIconTooltipContent}>
|
||||
<EuiIcon type={alertIconType} color="danger" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText css={styles.alertName} size="s">
|
||||
{dataOrDash(name)}
|
||||
</EuiText>
|
||||
<div css={styles.processAlertDisplayContainer}>
|
||||
<EuiText
|
||||
data-test-subj={`sessionView:sessionViewAlertDetailRuleName-${uuid}-text`}
|
||||
css={styles.alertName}
|
||||
size="s"
|
||||
>
|
||||
{dataOrDash(name)}
|
||||
</EuiText>
|
||||
{alertCategoryDetailDisplayText && (
|
||||
<EuiPanel
|
||||
css={styles.processPanel}
|
||||
color="subdued"
|
||||
hasBorder
|
||||
hasShadow={false}
|
||||
borderRadius="m"
|
||||
>
|
||||
<EuiText
|
||||
data-test-subj={`sessionView:sessionViewAlertDetail-${uuid}-text`}
|
||||
css={styles.alertName}
|
||||
size="s"
|
||||
>
|
||||
<span className="alertCategoryDetailText">{alertCategoryDetailDisplayText}</span>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color={getBadgeColorFromAlertStatus(status)} css={styles.alertStatus}>
|
||||
|
|
|
@ -19,7 +19,7 @@ export const useStyles = ({ isInvestigated, isSelected }: StylesDeps) => {
|
|||
const { euiTheme, euiVars } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size, colors, font } = euiTheme;
|
||||
const { size, colors, font, border } = euiTheme;
|
||||
|
||||
const getHighlightColors = () => {
|
||||
let bgColor = 'none';
|
||||
|
@ -69,19 +69,36 @@ export const useStyles = ({ isInvestigated, isSelected }: StylesDeps) => {
|
|||
textTransform: 'capitalize',
|
||||
};
|
||||
|
||||
const processAlertDisplayContainer: CSSObject = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const alertName: CSSObject = {
|
||||
color: colors.title,
|
||||
'& .alertCategoryDetailText': {
|
||||
fontSize: size.m,
|
||||
},
|
||||
};
|
||||
|
||||
const actionBadge: CSSObject = {
|
||||
textTransform: 'capitalize',
|
||||
};
|
||||
|
||||
const processPanel: CSSObject = {
|
||||
marginLeft: '8px',
|
||||
border: `${border.width.thin} solid ${colors.lightShade}`,
|
||||
fontFamily: font.familyCode,
|
||||
padding: `${size.xs} ${size.s}`,
|
||||
};
|
||||
|
||||
return {
|
||||
alert,
|
||||
alertStatus,
|
||||
alertName,
|
||||
actionBadge,
|
||||
processPanel,
|
||||
processAlertDisplayContainer,
|
||||
};
|
||||
}, [euiTheme, isInvestigated, isSelected, euiVars]);
|
||||
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock';
|
||||
import {
|
||||
mockAlerts,
|
||||
mockAlertTypeCounts,
|
||||
} from '../../../common/mocks/constants/session_view_process.mock';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { ProcessTreeAlertsDeps, ProcessTreeAlerts } from '.';
|
||||
|
||||
|
@ -14,8 +17,9 @@ describe('ProcessTreeAlerts component', () => {
|
|||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
const props: ProcessTreeAlertsDeps = {
|
||||
const processTreeAlertsProps: ProcessTreeAlertsDeps = {
|
||||
alerts: mockAlerts,
|
||||
alertTypeCounts: mockAlertTypeCounts,
|
||||
onAlertSelected: jest.fn(),
|
||||
onShowAlertDetails: jest.fn(),
|
||||
};
|
||||
|
@ -26,13 +30,15 @@ describe('ProcessTreeAlerts component', () => {
|
|||
|
||||
describe('When ProcessTreeAlerts is mounted', () => {
|
||||
it('should return null if no alerts', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlerts {...props} alerts={[]} />);
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlerts {...processTreeAlertsProps} alerts={[]} />
|
||||
);
|
||||
|
||||
expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return an array of alert details', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlerts {...props} />);
|
||||
renderResult = mockedContext.render(<ProcessTreeAlerts {...processTreeAlertsProps} />);
|
||||
|
||||
expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy();
|
||||
mockAlerts.forEach((alert) => {
|
||||
|
@ -50,7 +56,7 @@ describe('ProcessTreeAlerts component', () => {
|
|||
it('should execute onAlertSelected when clicking on an alert', async () => {
|
||||
const mockFn = jest.fn();
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlerts {...props} onAlertSelected={mockFn} />
|
||||
<ProcessTreeAlerts {...processTreeAlertsProps} onAlertSelected={mockFn} />
|
||||
);
|
||||
|
||||
expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy();
|
||||
|
@ -63,4 +69,18 @@ describe('ProcessTreeAlerts component', () => {
|
|||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProcessTreeAlertsFilter Render', () => {
|
||||
it('should return ProcessTreeAlertsFilter component when alerts exist', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlerts {...processTreeAlertsProps} />);
|
||||
expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetailsFilter')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not return ProcessTreeAlertsFilter when no alerts exist', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlerts {...processTreeAlertsProps} alerts={[]} />
|
||||
);
|
||||
expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetailsFilter')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,16 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef, MouseEvent, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, MouseEvent, useCallback, useMemo } from 'react';
|
||||
import { useStyles } from './styles';
|
||||
import { ProcessEvent, ProcessEventAlert } from '../../../common/types/process_tree';
|
||||
import {
|
||||
ProcessEventAlertCategory,
|
||||
DefaultAlertFilterType,
|
||||
ProcessEvent,
|
||||
ProcessEventAlert,
|
||||
AlertTypeCount,
|
||||
} from '../../../common/types/process_tree';
|
||||
import { ProcessTreeAlert } from '../process_tree_alert';
|
||||
import { MOUSE_EVENT_PLACEHOLDER } from '../../../common/constants';
|
||||
import { DEFAULT_ALERT_FILTER_VALUE, MOUSE_EVENT_PLACEHOLDER } from '../../../common/constants';
|
||||
import { ProcessTreeAlertsFilter } from '../process_tree_alerts_filter';
|
||||
|
||||
export interface ProcessTreeAlertsDeps {
|
||||
alerts: ProcessEvent[];
|
||||
investigatedAlertId?: string;
|
||||
isProcessSelected?: boolean;
|
||||
alertTypeCounts: AlertTypeCount[];
|
||||
onAlertSelected: (e: MouseEvent) => void;
|
||||
onShowAlertDetails: (alertUuid: string) => void;
|
||||
}
|
||||
|
@ -23,10 +31,14 @@ export function ProcessTreeAlerts({
|
|||
alerts,
|
||||
investigatedAlertId,
|
||||
isProcessSelected = false,
|
||||
alertTypeCounts,
|
||||
onAlertSelected,
|
||||
onShowAlertDetails,
|
||||
}: ProcessTreeAlertsDeps) {
|
||||
const [selectedAlert, setSelectedAlert] = useState<ProcessEventAlert | null>(null);
|
||||
const [selectedProcessEventAlertCategory, setSelectedProcessEventAlertCategory] = useState<
|
||||
ProcessEventAlertCategory | DefaultAlertFilterType
|
||||
>(DEFAULT_ALERT_FILTER_VALUE);
|
||||
const styles = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -70,6 +82,24 @@ export function ProcessTreeAlerts({
|
|||
[onAlertSelected]
|
||||
);
|
||||
|
||||
const handleProcessEventAlertCategorySelected = useCallback((eventCategory) => {
|
||||
if (ProcessEventAlertCategory.hasOwnProperty(eventCategory)) {
|
||||
setSelectedProcessEventAlertCategory(eventCategory);
|
||||
} else {
|
||||
setSelectedProcessEventAlertCategory(ProcessEventAlertCategory.all);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredProcessEventAlerts = useMemo(() => {
|
||||
return alerts?.filter((processEventAlert: ProcessEvent) => {
|
||||
const processEventAlertCategory = processEventAlert.event?.category?.[0];
|
||||
if (selectedProcessEventAlertCategory === DEFAULT_ALERT_FILTER_VALUE) {
|
||||
return true;
|
||||
}
|
||||
return processEventAlertCategory === selectedProcessEventAlertCategory;
|
||||
});
|
||||
}, [selectedProcessEventAlertCategory, alerts]);
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -80,7 +110,14 @@ export function ProcessTreeAlerts({
|
|||
css={styles.container}
|
||||
data-test-subj="sessionView:sessionViewAlertDetails"
|
||||
>
|
||||
{alerts.map((alert: ProcessEvent, idx: number) => {
|
||||
<ProcessTreeAlertsFilter
|
||||
totalAlertsCount={alerts.length}
|
||||
alertTypeCounts={alertTypeCounts}
|
||||
filteredAlertsCount={filteredProcessEventAlerts.length}
|
||||
onAlertEventCategorySelected={handleProcessEventAlertCategorySelected}
|
||||
/>
|
||||
|
||||
{filteredProcessEventAlerts.map((alert: ProcessEvent, idx: number) => {
|
||||
const alertUuid = alert.kibana?.alert?.uuid || null;
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* 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 { mockAlertTypeCounts } from '../../../common/mocks/constants/session_view_process.mock';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { ProcessTreeAlertsFilter, ProcessTreeAlertsFilterDeps } from '.';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DEFAULT_ALERT_FILTER_VALUE } from '../../../common/constants';
|
||||
|
||||
describe('ProcessTreeAlertsFiltersFilter component', () => {
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
const props: ProcessTreeAlertsFilterDeps = {
|
||||
totalAlertsCount: 3,
|
||||
alertTypeCounts: mockAlertTypeCounts,
|
||||
filteredAlertsCount: 2,
|
||||
onAlertEventCategorySelected: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
});
|
||||
|
||||
describe('When ProcessTreeAlertsFiltersFilter is mounted', () => {
|
||||
it('should filter alerts count of out total alerts count when filtered alerts count and total alerts count are not equal', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlertsFilter {...props} />);
|
||||
|
||||
const filterCountStatus = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterStatus'
|
||||
);
|
||||
|
||||
expect(filterCountStatus).toBeTruthy();
|
||||
expect(filterCountStatus).toHaveTextContent('Showing 2 of 3 alerts');
|
||||
});
|
||||
|
||||
it('should show only total alert counts when filtered and total count are equal', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlertsFilter {...props} filteredAlertsCount={3} />
|
||||
);
|
||||
|
||||
const filterCountStatus = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterStatus'
|
||||
);
|
||||
|
||||
expect(filterCountStatus).toHaveTextContent('Showing 3 alerts');
|
||||
expect(filterCountStatus).not.toHaveTextContent('Showing 2 of 3 alerts');
|
||||
expect(filterCountStatus).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call onAlertEventCategorySelected with alert category when filter item is clicked ', () => {
|
||||
const mockAlertEventCategorySelectedEvent = jest.fn();
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlertsFilter
|
||||
{...props}
|
||||
onAlertEventCategorySelected={mockAlertEventCategorySelectedEvent}
|
||||
/>
|
||||
);
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
userEvent.click(filterButton);
|
||||
|
||||
renderResult.getByTestId('sessionView:sessionViewAlertDetailsFilterItem-network').click();
|
||||
|
||||
expect(mockAlertEventCategorySelectedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(mockAlertEventCategorySelectedEvent).toHaveBeenCalledWith('network');
|
||||
});
|
||||
|
||||
describe(' EuiFlexItem filter selector container ', () => {
|
||||
it('should alerts filter dropdown when at least two alerts categories exist', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlertsFilter {...props} />);
|
||||
|
||||
const filterSelection = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterSelectorContainer'
|
||||
);
|
||||
|
||||
expect(filterSelection).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not show alerts filter selector container when there is only one alert category ', async () => {
|
||||
const alertTypeCountsUpdated = mockAlertTypeCounts.map((alertType) =>
|
||||
alertType.category === 'process' ? { ...alertType, count: 1 } : { ...alertType, count: 0 }
|
||||
);
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlertsFilter {...props} alertTypeCounts={alertTypeCountsUpdated} />
|
||||
);
|
||||
|
||||
const filterSelection = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterSelectorContainer'
|
||||
);
|
||||
|
||||
expect(filterSelection).toBeNull();
|
||||
});
|
||||
|
||||
it('should open filter menu popover', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlertsFilter {...props} />);
|
||||
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenu = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterSelectorContainerMenu'
|
||||
);
|
||||
|
||||
expect(filterMenu).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EuiContextMenuItem filter when two alert categories exists', () => {
|
||||
it('should display network option in filter menu', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlertsFilter {...props} />);
|
||||
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenuItem = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterItem-network'
|
||||
);
|
||||
|
||||
expect(filterMenuItem).toHaveTextContent('View network alerts');
|
||||
expect(filterMenuItem).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display process option in filter menu', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlertsFilter {...props} />);
|
||||
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenuItem = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterItem-process'
|
||||
);
|
||||
|
||||
expect(filterMenuItem).toHaveTextContent('View process alerts');
|
||||
expect(filterMenuItem).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not display file option in filter menu', async () => {
|
||||
renderResult = mockedContext.render(<ProcessTreeAlertsFilter {...props} />);
|
||||
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenuItem = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterItem-file'
|
||||
);
|
||||
|
||||
expect(filterMenuItem).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EuiContextMenuItem filter when all alert categories exist', () => {
|
||||
const alertTypeCountsUpdated = mockAlertTypeCounts.map((alertType) => ({
|
||||
...alertType,
|
||||
count: 1,
|
||||
}));
|
||||
beforeEach(() => {
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlertsFilter {...props} alertTypeCounts={alertTypeCountsUpdated} />
|
||||
);
|
||||
});
|
||||
|
||||
it('should display network option in filter menu', async () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenuItem = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterItem-network'
|
||||
);
|
||||
|
||||
expect(filterMenuItem).toHaveTextContent('View network alerts');
|
||||
expect(filterMenuItem).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display process option in filter menu', async () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenuItem = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterItem-process'
|
||||
);
|
||||
|
||||
expect(filterMenuItem).toHaveTextContent('View process alerts');
|
||||
expect(filterMenuItem).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display file option in filter menu', async () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
|
||||
userEvent.click(filterButton);
|
||||
|
||||
const filterMenuItem = renderResult.queryByTestId(
|
||||
'sessionView:sessionViewAlertDetailsFilterItem-file'
|
||||
);
|
||||
|
||||
expect(filterMenuItem).toHaveTextContent('View file alerts');
|
||||
expect(filterMenuItem).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EmptyFilterButton display text', () => {
|
||||
const alertTypeCountsUpdated = mockAlertTypeCounts.map((alertType) => ({
|
||||
...alertType,
|
||||
count: 1,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
renderResult = mockedContext.render(
|
||||
<ProcessTreeAlertsFilter {...props} alertTypeCounts={alertTypeCountsUpdated} />
|
||||
);
|
||||
});
|
||||
it('should set the EmptyFilterButton text content to display "View: all alerts" by default ', () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
expect(filterButton).toHaveTextContent('View: all alerts');
|
||||
});
|
||||
|
||||
it('should set the EmptyFilterButton text content to display "View: file alerts" when file alert option is clicked', () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
userEvent.click(filterButton);
|
||||
|
||||
renderResult.getByTestId('sessionView:sessionViewAlertDetailsFilterItem-file').click();
|
||||
|
||||
expect(filterButton).toHaveTextContent('View: file alerts');
|
||||
});
|
||||
|
||||
it('should set the EmptyFilterButton text content to display "View: all alerts" when default filter option is clicked', () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
userEvent.click(filterButton);
|
||||
|
||||
renderResult.getByTestId('sessionView:sessionViewAlertDetailsFilterItem-default').click();
|
||||
|
||||
expect(filterButton).toHaveTextContent(`View: ${DEFAULT_ALERT_FILTER_VALUE} alerts`);
|
||||
});
|
||||
|
||||
it('should set the EmptyFilterButton text content to display "View: process alerts" when process alert option is clicked', () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
userEvent.click(filterButton);
|
||||
|
||||
renderResult.getByTestId('sessionView:sessionViewAlertDetailsFilterItem-process').click();
|
||||
|
||||
expect(filterButton).toHaveTextContent('View: process alerts');
|
||||
});
|
||||
|
||||
it('should set the EmptyFilterButton text content to display "View: network alerts" when network alert option is clicked', () => {
|
||||
const filterButton = renderResult.getByTestId(
|
||||
'sessionView:sessionViewAlertDetailsEmptyFilterButton'
|
||||
);
|
||||
userEvent.click(filterButton);
|
||||
|
||||
renderResult.getByTestId('sessionView:sessionViewAlertDetailsFilterItem-network').click();
|
||||
|
||||
expect(filterButton).toHaveTextContent('View: network alerts');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiContextMenuItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DEFAULT_ALERT_FILTER_VALUE } from '../../../common/constants';
|
||||
import {
|
||||
ProcessEventAlertCategory,
|
||||
DefaultAlertFilterType,
|
||||
AlertTypeCount,
|
||||
} from '../../../common/types/process_tree';
|
||||
import { useStyles } from './styles';
|
||||
import { FILTER_MENU_OPTIONS, SELECTED_PROCESS } from './translations';
|
||||
|
||||
export interface ProcessTreeAlertsFilterDeps {
|
||||
totalAlertsCount: number;
|
||||
alertTypeCounts: AlertTypeCount[];
|
||||
filteredAlertsCount: number;
|
||||
onAlertEventCategorySelected: (value: ProcessEventAlertCategory | DefaultAlertFilterType) => void;
|
||||
}
|
||||
|
||||
export const ProcessTreeAlertsFilter = ({
|
||||
totalAlertsCount,
|
||||
alertTypeCounts,
|
||||
filteredAlertsCount,
|
||||
onAlertEventCategorySelected,
|
||||
}: ProcessTreeAlertsFilterDeps) => {
|
||||
const { filterStatus, popover } = useStyles();
|
||||
|
||||
const [selectedProcessEventAlertCategory, setSelectedProcessEventAlertCategory] =
|
||||
useState<ProcessEventAlertCategory>(ProcessEventAlertCategory.all);
|
||||
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const onButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false);
|
||||
};
|
||||
|
||||
const onSelectedProcessEventAlertCategory = useCallback(
|
||||
(event) => {
|
||||
const [_, selectedAlertEvent] = event.target.textContent.split(' ');
|
||||
setSelectedProcessEventAlertCategory(selectedAlertEvent);
|
||||
onAlertEventCategorySelected(selectedAlertEvent);
|
||||
closePopover();
|
||||
},
|
||||
[onAlertEventCategorySelected]
|
||||
);
|
||||
|
||||
const doesMultipleAlertTypesExist = useMemo(() => {
|
||||
// Check if alerts consist of at least two alert event types
|
||||
const multipleAlertTypeCount = alertTypeCounts.reduce((sumOfAlertTypes, { count }) => {
|
||||
if (count > 0) {
|
||||
return (sumOfAlertTypes += 1);
|
||||
}
|
||||
return 0;
|
||||
}, 0);
|
||||
return multipleAlertTypeCount > 1;
|
||||
}, [alertTypeCounts]);
|
||||
|
||||
const alertEventCategoryFilterMenuButton = (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="sessionView:sessionViewAlertDetailsEmptyFilterButton"
|
||||
size="s"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={onButtonClick}
|
||||
>
|
||||
{SELECTED_PROCESS[selectedProcessEventAlertCategory]}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const alertEventCategoryFilterMenuItems = useMemo(() => {
|
||||
const getIconType = (eventCategory: ProcessEventAlertCategory | DefaultAlertFilterType) => {
|
||||
return eventCategory === selectedProcessEventAlertCategory ? 'check' : 'empty';
|
||||
};
|
||||
|
||||
const alertEventFilterMenuItems = alertTypeCounts
|
||||
.filter(({ count }) => count > 0)
|
||||
.map(({ category: processEventAlertCategory }) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj={`sessionView:sessionViewAlertDetailsFilterItem-${processEventAlertCategory}`}
|
||||
key={processEventAlertCategory}
|
||||
icon={getIconType(processEventAlertCategory)}
|
||||
onClick={onSelectedProcessEventAlertCategory}
|
||||
>
|
||||
{FILTER_MENU_OPTIONS[processEventAlertCategory]}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
data-test-subj={`sessionView:sessionViewAlertDetailsFilterItem-default`}
|
||||
key={DEFAULT_ALERT_FILTER_VALUE}
|
||||
icon={getIconType(DEFAULT_ALERT_FILTER_VALUE)}
|
||||
onClick={onSelectedProcessEventAlertCategory}
|
||||
>
|
||||
{FILTER_MENU_OPTIONS[ProcessEventAlertCategory.all]}
|
||||
</EuiContextMenuItem>,
|
||||
...alertEventFilterMenuItems,
|
||||
];
|
||||
}, [selectedProcessEventAlertCategory, alertTypeCounts, onSelectedProcessEventAlertCategory]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="sessionView:sessionViewAlertDetailsFilter">
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem css={filterStatus} style={{ paddingLeft: '16px' }}>
|
||||
<EuiText size="s" data-test-subj="sessionView:sessionViewAlertDetailsFilterStatus">
|
||||
{totalAlertsCount === filteredAlertsCount && (
|
||||
<FormattedMessage
|
||||
id="xpack.sessionView.alertTotalCountStatusLabel"
|
||||
defaultMessage="Showing {count} alerts"
|
||||
values={{
|
||||
count: <strong>{totalAlertsCount}</strong>,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalAlertsCount !== filteredAlertsCount && (
|
||||
<FormattedMessage
|
||||
id="xpack.sessionView.alertFilteredCountStatusLabel"
|
||||
defaultMessage=" Showing {count} alerts"
|
||||
values={{
|
||||
count: (
|
||||
<strong>
|
||||
{filteredAlertsCount} of {totalAlertsCount}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{doesMultipleAlertTypesExist && (
|
||||
<EuiFlexItem
|
||||
css={popover}
|
||||
grow={false}
|
||||
data-test-subj="sessionView:sessionViewAlertDetailsFilterSelectorContainer"
|
||||
>
|
||||
<EuiPopover
|
||||
button={alertEventCategoryFilterMenuButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
data-test-subj="sessionView:sessionViewAlertDetailsFilterSelectorContainerMenu"
|
||||
size="s"
|
||||
className={'filterMenu'}
|
||||
items={alertEventCategoryFilterMenuItems}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
|
||||
export const useStyles = () => {
|
||||
const cached = useMemo(() => {
|
||||
const filterStatus: CSSObject = {
|
||||
paddingLeft: '16px',
|
||||
'& .text': {
|
||||
fontWeight: 600,
|
||||
},
|
||||
};
|
||||
|
||||
const popover: CSSObject = {
|
||||
paddingRight: '16px',
|
||||
'& .filterMenu': {
|
||||
width: '180px',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
filterStatus,
|
||||
popover,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -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 SELECTED_PROCESS = {
|
||||
all: i18n.translate('xpack.sessionView.alertDetailsAllSelectedCategory', {
|
||||
defaultMessage: 'View: all alerts',
|
||||
}),
|
||||
process: i18n.translate('xpack.sessionView.alertDetailsProcessSelectedCategory', {
|
||||
defaultMessage: 'View: process alerts',
|
||||
}),
|
||||
network: i18n.translate('xpack.sessionView.alertDetailsNetworkSelectedCategory', {
|
||||
defaultMessage: 'View: network alerts',
|
||||
}),
|
||||
file: i18n.translate('xpack.sessionView.alertDetailsFileSelectedCategory', {
|
||||
defaultMessage: 'View: file alerts',
|
||||
}),
|
||||
};
|
||||
|
||||
export const FILTER_MENU_OPTIONS = {
|
||||
all: i18n.translate('xpack.sessionView.alertDetailsAllFilterItem', {
|
||||
defaultMessage: 'View all alerts',
|
||||
}),
|
||||
process: i18n.translate('xpack.sessionView.alertDetailsProcessFilterItem', {
|
||||
defaultMessage: 'View process alerts',
|
||||
}),
|
||||
network: i18n.translate('xpack.sessionView.alertDetailsNetworkFilterItem', {
|
||||
defaultMessage: 'View network alerts',
|
||||
}),
|
||||
file: i18n.translate('xpack.sessionView.alertDetailsFileFilterItem', {
|
||||
defaultMessage: 'View file alerts',
|
||||
}),
|
||||
};
|
|
@ -4,10 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiButton, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertTypeCount } from '../../../common/types/process_tree';
|
||||
import { useButtonStyles } from './use_button_styles';
|
||||
import { ALERT_ICONS } from '../../../common/constants';
|
||||
|
||||
const MAX_ALERT_COUNT = 99;
|
||||
|
||||
|
@ -53,15 +55,25 @@ export const ChildrenProcessesButton = ({
|
|||
|
||||
export const AlertButton = ({
|
||||
isExpanded,
|
||||
alertTypeCounts,
|
||||
onToggle,
|
||||
alertsCount,
|
||||
}: {
|
||||
isExpanded: boolean;
|
||||
alertTypeCounts: AlertTypeCount[];
|
||||
onToggle: () => void;
|
||||
alertsCount: number;
|
||||
}) => {
|
||||
const { alertButton, buttonArrow } = useButtonStyles();
|
||||
|
||||
const alertIcons: string[] = useMemo(
|
||||
() =>
|
||||
alertTypeCounts
|
||||
?.filter((alertTypeCount) => alertTypeCount.count > 0)
|
||||
?.map(({ category }, i) => ALERT_ICONS[category]),
|
||||
[alertTypeCounts]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
className={isExpanded ? 'isExpanded' : ''}
|
||||
|
@ -74,6 +86,9 @@ export const AlertButton = ({
|
|||
{alertsCount > 1 ? ALERTS : ALERT}
|
||||
{alertsCount > 1 &&
|
||||
(alertsCount > MAX_ALERT_COUNT ? ` (${MAX_ALERT_COUNT}+)` : ` (${alertsCount})`)}
|
||||
{alertIcons?.map((icon: string) => (
|
||||
<EuiIcon className="alertIcon" key={icon} size="s" type={icon} />
|
||||
))}
|
||||
<EuiIcon css={buttonArrow} size="s" type="arrowDown" />
|
||||
</EuiButton>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,12 @@ import React, {
|
|||
import { EuiButton, EuiIcon, EuiToolTip, formatDate } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Process } from '../../../common/types/process_tree';
|
||||
import { chain } from 'lodash';
|
||||
import {
|
||||
AlertTypeCount,
|
||||
Process,
|
||||
ProcessEventAlertCategory,
|
||||
} from '../../../common/types/process_tree';
|
||||
import { dataOrDash } from '../../utils/data_or_dash';
|
||||
import { useVisible } from '../../hooks/use_visible';
|
||||
import { ProcessTreeAlerts } from '../process_tree_alerts';
|
||||
|
@ -126,6 +131,18 @@ export function ProcessTreeNode({
|
|||
shouldAddListener: hasInvestigatedAlert,
|
||||
});
|
||||
|
||||
const alertTypeCounts = useMemo(() => {
|
||||
const alertCounts: AlertTypeCount[] = chain(alerts)
|
||||
.groupBy((alert) => alert.event?.category?.[0])
|
||||
.map((processAlerts, alertCategory) => ({
|
||||
category: alertCategory as ProcessEventAlertCategory,
|
||||
count: processAlerts.length,
|
||||
}))
|
||||
.value();
|
||||
|
||||
return alertCounts;
|
||||
}, [alerts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.id === selectedProcess?.id && nodeRef.current?.scrollIntoView) {
|
||||
nodeRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
@ -301,6 +318,7 @@ export function ProcessTreeNode({
|
|||
{hasAlerts && (
|
||||
<AlertButton
|
||||
onToggle={onAlertsToggle}
|
||||
alertTypeCounts={alertTypeCounts}
|
||||
isExpanded={alertsExpanded}
|
||||
alertsCount={alerts.length}
|
||||
/>
|
||||
|
@ -312,6 +330,7 @@ export function ProcessTreeNode({
|
|||
{alertsExpanded && (
|
||||
<ProcessTreeAlerts
|
||||
alerts={alerts}
|
||||
alertTypeCounts={alertTypeCounts}
|
||||
investigatedAlertId={investigatedAlertId}
|
||||
isProcessSelected={isSelected}
|
||||
onAlertSelected={onProcessClicked}
|
||||
|
|
|
@ -40,7 +40,7 @@ export const useButtonStyles = () => {
|
|||
background: transparentize(euiVars.euiColorVis6, 0.12),
|
||||
textDecoration: 'none',
|
||||
},
|
||||
'&.isExpanded > span svg': {
|
||||
'&.isExpanded > span svg:not(.alertIcon)': {
|
||||
transform: `rotate(180deg)`,
|
||||
},
|
||||
'&.isExpanded': {
|
||||
|
@ -55,7 +55,6 @@ export const useButtonStyles = () => {
|
|||
const buttonArrow: CSSObject = {
|
||||
marginLeft: size.xs,
|
||||
};
|
||||
|
||||
const alertButton: CSSObject = {
|
||||
...button,
|
||||
color: euiVars.euiColorDanger,
|
||||
|
@ -72,6 +71,14 @@ export const useButtonStyles = () => {
|
|||
background: `${euiVars.euiColorDanger}`,
|
||||
},
|
||||
},
|
||||
|
||||
'& .euiButton__text': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
' .alertIcon': {
|
||||
marginLeft: '4px',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const outputButton: CSSObject = {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 {
|
||||
mockAlerts,
|
||||
mockFileAlert,
|
||||
mockNetworkAlert,
|
||||
} from '../../common/mocks/constants/session_view_process.mock';
|
||||
import { getAlertCategoryDisplayText, getAlertNetworkDisplay } from './alert_category_display_text';
|
||||
import { ProcessEventAlertCategory } from '../../common/types/process_tree';
|
||||
|
||||
describe('getAlertCategoryDisplayText(alert, category)', () => {
|
||||
it('should display file path when alert category is file', () => {
|
||||
expect(getAlertCategoryDisplayText(mockFileAlert, ProcessEventAlertCategory?.file)).toEqual(
|
||||
mockFileAlert?.file?.path
|
||||
);
|
||||
});
|
||||
|
||||
it('should display rule name when alert category is process', () => {
|
||||
expect(getAlertCategoryDisplayText(mockAlerts[0], ProcessEventAlertCategory.process)).toEqual(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('should display rule name when alert category is undefined', () => {
|
||||
expect(getAlertCategoryDisplayText(mockAlerts[0], undefined)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should display rule name when file path is undefined', () => {
|
||||
const fileAlert = { ...mockFileAlert, file: {} };
|
||||
expect(getAlertCategoryDisplayText(fileAlert, ProcessEventAlertCategory.file)).toEqual(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
it('should display rule name when destination address is undefined and alert category is network', () => {
|
||||
const networkAlert = { ...mockNetworkAlert, destination: undefined };
|
||||
expect(getAlertCategoryDisplayText(networkAlert, ProcessEventAlertCategory.network)).toEqual(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlertNetworkDisplay(destination)', () => {
|
||||
it('should show destination address and port', () => {
|
||||
const text = `${mockNetworkAlert.destination.address}:${mockNetworkAlert.destination.port}`;
|
||||
expect(getAlertNetworkDisplay(mockNetworkAlert.destination)).toEqual(text);
|
||||
});
|
||||
|
||||
it('should show only ip address when port does not exist', () => {
|
||||
const text = `${mockNetworkAlert?.destination?.address}`;
|
||||
expect(
|
||||
getAlertNetworkDisplay({
|
||||
...mockNetworkAlert.destination,
|
||||
port: undefined,
|
||||
})
|
||||
).toEqual(text);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 {
|
||||
ProcessEvent,
|
||||
ProcessEventAlertCategory,
|
||||
ProcessEventIPAddress,
|
||||
} from '../../common/types/process_tree';
|
||||
import { dataOrDash } from './data_or_dash';
|
||||
|
||||
export const getAlertCategoryDisplayText = (alert: ProcessEvent, category: string | undefined) => {
|
||||
const destination = alert?.destination;
|
||||
const filePath = alert?.file?.path;
|
||||
|
||||
if (filePath && category === ProcessEventAlertCategory.file) return dataOrDash(filePath);
|
||||
if (destination?.address && category === ProcessEventAlertCategory.network)
|
||||
return dataOrDash(getAlertNetworkDisplay(destination));
|
||||
return;
|
||||
};
|
||||
|
||||
export const getAlertNetworkDisplay = (destination: ProcessEventIPAddress) => {
|
||||
const hasIpAddressPort = !!destination?.address && !!destination?.port;
|
||||
const ipAddressPort = `${destination?.address}:${destination?.port}`;
|
||||
return `${hasIpAddressPort ? ipAddressPort : destination?.address}`;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue