Skip to content

Commit

Permalink
[Security Solution] Alerts Treemap and Multi-Dimensional Alert Groupi…
Browse files Browse the repository at this point in the history
…ng (#126896)

## [Security Solution] Alerts Treemap and Multi-Dimensional Alert Grouping

This PR introduces the new _Treemap_ and _Multi-Dimensional Alert Grouping_ to the Alerts page.

The initial commit was developed as an _ON week_ proof of concept (POC). It has since been updated to incorporate product and UX feedback.

### Alerts treemap

The new _Alerts_ page treemap is shown in the screenshot below:

![treemap](https://user-images.githubusercontent.com/4459398/178233664-c45be7ca-b03e-40b9-b423-aeeaa47461c0.png)

_Above: The new `treemap` in the Alerts page_

- Alerts are colored by risk score
- Clicking on a cell instantly filters the _Alerts_ page
- Treemap legend items may be added to filters and Timeline investigations
- The new treemap supports multi-dimensional grouping and filtering
  - Alerts are grouped by `kibana.alert.rule.name` and `host.name` by default

### Multi-Dimensional Alert Grouping

The table on the Alerts page, which previously supported grouping by a single field, has also been enhanced to support multi-dimensional grouping, per the screenshot below:

![alerts-table-multi-dimensional-grouping](https://user-images.githubusercontent.com/4459398/178240710-ecf66799-35a8-4874-8882-5ccfcccc86fe.png)

_Above: The table in the Alerts page, enhanced to support multi-dimensional grouping_

## Filtering the Alerts page by risk score

Every rule, including prebuilt Elastic rules and custom rules created by users, must specify a risk score at rule creation time, per the screenshot below:

![rule_risk_score_configuration](https://user-images.githubusercontent.com/4459398/156712042-19b71f53-f337-4aed-bebf-ce10ea2b9f63.png)

_Above: Every rule has a risk score specified when it's created_

The colors of the alerts displayed in the treemap are determined by the rule's risk score. This makes it easy to quickly filter the entire alerts page by clicking on the riskiest alerts.

Clicking on a cell in the treemap adds two filters, one for each group by field, per the screenshot below:

![two-filters](https://user-images.githubusercontent.com/4459398/178252768-7c66dc5e-8a3c-41d8-95e2-eeca20133127.png)

_Above: Two filters, (one for each group by field), are added to the page when a cell is clicked_

The Alerts page updates instantly when filters are added or removed. In the screenshot below, the 2nd filter was removed to filter the page to all `mimikatz process started` alerts:

![second-filter-removed](https://user-images.githubusercontent.com/4459398/178253726-66905d60-99da-4d76-9ea1-744cb53abd6f.png)

_Above: Removing the 2nd filter, a specific `host.name`, revealed all the hosts in the `mimikatz process started` alerts_

### Switching views

Users may switch between the following views:

- Table
- Trend (default)
- Treemap

per the screenshot below:

![view-selection](https://user-images.githubusercontent.com/4459398/178412669-f437cfe4-f819-45b5-8359-987a2a3c6645.png)

_Above: View selection_

The default _Trend_ view is shown in the screenshot below:

![trend-view](https://user-images.githubusercontent.com/4459398/178242769-58d6c800-69db-4e14-87e4-9232f3e35427.png)

_Above: The (default) Trend view_

- The Trend chart's legend has been enhanced to display counts, per the design detailed in issue <#120282>

- The Trend view only supports visualizing a single dimension. Hovering over the disabled `Group by top` select in the Trend view displays the tooltip shown in the screenshot below:

![tooltip](https://user-images.githubusercontent.com/4459398/178243356-9bfe7f54-5b31-4f61-a795-0cfa0a70285b.png)

_Above: The Trend view only supports visualizing a single dimension_

### Collapsing the panel

The panel may optionally be collapsed to save space, per the screenshot below:

![collapsed](https://user-images.githubusercontent.com/4459398/178244282-525d4c9f-a23b-4ec4-8f72-7e813e771687.png)

_Above: The panel (optionally) collapsed_

### User preferences are persisted to local storage

Previously, the _Group by_ selections on the Alerts page were always forgotten when users navigated away from the Alerts page. As a result, users had to re-select their preferred Group by fields every time they visited the page.

We now store all of the following preferences in local storage:

- View selection (Table, Trend, Treemap)
- Group by selections
- Panel collapse state

The preferences above are now restored the next time users return to the Alerts page.

### Group by field selection is synchronized between views

Group by field selection is synchronized between views. For example, if a user changes the Group by fields in the Treemap view and then switches to the Table view, the same Group by fields will be displayed in the Table view.

### Resetting Group by fields to their defaults

Users may reset the Group by fields to their defaults (`kibana.alert.rule.name` and `host.name`) for any visualization via the menu shown in the screenshot below:

![reset-group-by-fields](https://user-images.githubusercontent.com/4459398/178246997-70d89763-40d4-4c93-b0b6-b439fc3e22cd.png)

_Above: Resetting Group by fields to defaults via the menu_
  • Loading branch information
andrew-goldstein authored Jul 13, 2022
1 parent f80c90b commit 0301d20
Show file tree
Hide file tree
Showing 98 changed files with 7,449 additions and 264 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ import {
waitForAlerts,
markAcknowledgedFirstAlert,
goToAcknowledgedAlerts,
clearGroupByTopInput,
closeAlerts,
closeFirstAlert,
goToClosedAlerts,
goToOpenedAlerts,
openAlerts,
openFirstAlert,
selectCountTable,
} from '../../tasks/alerts';
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
Expand Down Expand Up @@ -53,6 +55,8 @@ describe('Changing alert status', () => {
cy.get(SELECTED_ALERTS).should('have.text', `Selected 3 alerts`);
closeAlerts();
waitForAlerts();
selectCountTable();
clearGroupByTopInput();
});

it('Open one alert when more than one closed alerts are selected', () => {
Expand Down Expand Up @@ -110,6 +114,8 @@ describe('Changing alert status', () => {
createCustomRuleEnabled(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
selectCountTable();
clearGroupByTopInput();
});
it('Mark one alert as acknowledged when more than one open alerts are selected', () => {
cy.get(ALERTS_COUNT)
Expand Down Expand Up @@ -148,6 +154,8 @@ describe('Changing alert status', () => {
createCustomRuleEnabled(getNewRule(), '1', '100m', 100);
visit(ALERTS_URL);
waitForAlertsToPopulate();
selectCountTable();
clearGroupByTopInput();
});
it('Closes and opens alerts', () => {
const numberOfAlertsToBeClosed = 3;
Expand Down Expand Up @@ -298,6 +306,8 @@ describe('Changing alert status', () => {
createCustomRuleEnabled(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
selectCountTable();
clearGroupByTopInput();
});
it('Mark one alert as acknowledged when more than one open alerts are selected', () => {
cy.get(ALERTS_COUNT)
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/security_solution/cypress/screens/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const ALERTS_COUNT =
export const ALERTS_TREND_SIGNAL_RULE_NAME_PANEL =
'[data-test-subj="render-content-kibana.alert.rule.name"]';

export const CHART_SELECT = '[data-test-subj="chartSelect"]';

export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]';

export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="close-alert-status"]';
Expand All @@ -45,6 +47,8 @@ export const EMPTY_ALERT_TABLE = '[data-test-subj="tGridEmptyState"]';

export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]';

export const GROUP_BY_TOP_INPUT = '[data-test-subj="groupByTop"] [data-test-subj="comboBoxInput"]';

export const HOST_NAME = '[data-test-subj^=formatted-field][data-test-subj$=host\\.name]';

export const ACKNOWLEDGED_ALERTS_FILTER_BTN = '[data-test-subj="acknowledgedAlerts"]';
Expand Down Expand Up @@ -74,6 +78,8 @@ export const RULE_NAME = '[data-test-subj^=formatted-field][data-test-subj$=rule

export const SELECTED_ALERTS = '[data-test-subj="selectedShowBulkActionsButton"]';

export const SELECT_TABLE = '[data-test-subj="table"]';

export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]';

export const SEVERITY = '[data-test-subj^=formatted-field][data-test-subj$=severity]';
Expand Down
14 changes: 13 additions & 1 deletion x-pack/plugins/security_solution/cypress/tasks/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ import {
ADD_EXCEPTION_BTN,
ALERT_RISK_SCORE_HEADER,
ALERT_CHECKBOX,
CHART_SELECT,
CLOSE_ALERT_BTN,
CLOSE_SELECTED_ALERTS_BTN,
CLOSED_ALERTS_FILTER_BTN,
EXPAND_ALERT_BTN,
GROUP_BY_TOP_INPUT,
ACKNOWLEDGED_ALERTS_FILTER_BTN,
LOADING_ALERTS_PANEL,
MANAGE_ALERT_DETECTION_RULES_BTN,
MARK_ALERT_ACKNOWLEDGED_BTN,
OPEN_ALERT_BTN,
OPENED_ALERTS_FILTER_BTN,
SEND_ALERT_TO_TIMELINE_BTN,
SELECT_TABLE,
TAKE_ACTION_POPOVER_BTN,
TIMELINE_CONTEXT_MENU_BTN,
} from '../screens/alerts';
Expand Down Expand Up @@ -65,7 +68,6 @@ export const closeAlerts = () => {

export const expandFirstAlertActions = () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.visible');
cy.get(TIMELINE_CONTEXT_MENU_BTN).find('svg').should('have.attr', 'data-is-loaded');
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true });
};

Expand Down Expand Up @@ -125,6 +127,16 @@ export const openAlerts = () => {
cy.get(OPEN_ALERT_BTN).click();
};

export const selectCountTable = () => {
cy.get(CHART_SELECT).click({ force: true });
cy.get(SELECT_TABLE).click();
};

export const clearGroupByTopInput = () => {
cy.get(GROUP_BY_TOP_INPUT).focus();
cy.get(GROUP_BY_TOP_INPUT).type('{backspace}');
};

export const goToAcknowledgedAlerts = () => {
cy.get(ACKNOWLEDGED_ALERTS_FILTER_BTN).click();
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ export const goToExceptionsTab = () => {
};

export const editException = () => {
cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click();
cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click({ force: true });

cy.get(EDIT_EXCEPTION_BTN).click();
cy.get(EDIT_EXCEPTION_BTN).click({ force: true });
};

export const removeException = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 { render, screen } from '@testing-library/react';
import React from 'react';

import { TestProviders } from '../../mock';
import {
mockAlertSearchResponse,
mockNoDataAlertSearchResponse,
} from './lib/mocks/mock_alert_search_response';
import * as i18n from './translations';
import type { Props } from '.';
import { AlertsTreemap } from '.';

const defaultProps: Props = {
data: mockAlertSearchResponse,
maxBuckets: 1000,
minChartHeight: 370,
stackByField0: 'kibana.alert.rule.name',
stackByField1: 'host.name',
};

describe('AlertsTreemap', () => {
describe('when the response has data', () => {
beforeEach(() => {
render(
<TestProviders>
<AlertsTreemap {...defaultProps} />
</TestProviders>
);
});

test('it renders the treemap', () => {
expect(screen.getByTestId('treemap').querySelector('.echChart')).toBeInTheDocument();
});

test('it renders the legend', () => {
expect(screen.getByTestId('draggable-legend')).toBeInTheDocument();
});
});

describe('when the response does NOT have data', () => {
beforeEach(() => {
render(
<TestProviders>
<AlertsTreemap {...defaultProps} data={mockNoDataAlertSearchResponse} />
</TestProviders>
);
});

test('it does NOT render the treemap', () => {
expect(screen.queryByTestId('treemap')).not.toBeInTheDocument();
});

test('it does NOT render the legend', () => {
expect(screen.queryByTestId('draggable-legend')).not.toBeInTheDocument();
});

test('it renders the "no data" message', () => {
expect(screen.getByText(i18n.NO_DATA_LABEL)).toBeInTheDocument();
});
});
});
Loading

0 comments on commit 0301d20

Please sign in to comment.