Skip to content

Commit

Permalink
Add RiskBadge to Asset Inventory data grid (elastic#206798)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic/security-team#11462.

Implements a RiskBadge component that maps scores with colours reusing
existing risk levels and palettes. Please, let me know if there's any
existing component I should reuse instead.

### Screenshots

| Before | After |
|--------|--------|
| <img width="55" alt="Screenshot 2025-01-16 at 17 19 09"
src="https://github.com/user-attachments/assets/de8ba686-7d50-4d7b-848f-3270e4c9f09f"
/> | <img width="55" alt="Screenshot 2025-01-16 at 17 19 01"
src="https://github.com/user-attachments/assets/187c1aa1-a1d4-489b-83c0-9edb4e61cf75"
/> |

### Definition of done

- [x] Implement a coloured badge to the **Risk** column of the Asset
Inventory DataGrid that displays a badge representing the risk level of
the asset.
- [x] Implement the badge styling:
- Use the **Risk Palette** defined in the [Risk Palette
Utility](https://github.com/opauloh/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/common/utils.ts)
for color mapping.
- Determine the color of the badge based on the **Risk Ranges** defined
in the [Risk Levels
Utility](https://github.com/opauloh/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/risk_engine/risk_levels.ts).
- [x] Ensure the badge includes:
- The textual representation of the risk level (e.g., "Low," "Medium,"
"High," "Critical").
- A tooltip that displays additional details about the risk level when
hovering over the badge.
- [x] Add unit tests to verify:
  - Correct colour mapping based on risk ranges.
  - Proper rendering of the badge in the DataGrid.
  - Tooltip displays the expected information.
- ~~[ ] Update mock data for the DataGrid to include risk values for
testing and development.~~ Done in previous PR that introduced mocked
data.

### Checklist

- [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
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

No risks since it's new functionality under a feature toggle.
  • Loading branch information
albertoblaz authored Jan 22, 2025
1 parent fc72ba9 commit 4146dd6
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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 { waitForEuiToolTipVisible } from '@elastic/eui/lib/test/rtl';
import { screen, render, cleanup, fireEvent } from '@testing-library/react';
import { RiskSeverity } from '../../../common/search_strategy';
import { RiskBadge } from './risk_badge';

describe('AssetInventory', () => {
describe('RiskBadge', () => {
beforeEach(() => {
cleanup();
});

it('renders unknown risk with 0 risk score', async () => {
render(<RiskBadge risk={0} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('0');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Unknown);
});
it('renders unknown risk with 19 risk score', async () => {
render(<RiskBadge risk={19} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('19');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Unknown);
});
it('renders low risk with 20 risk score', async () => {
render(<RiskBadge risk={20} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('20');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Low);
});
it('renders low risk with 39 risk score', async () => {
render(<RiskBadge risk={39} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('39');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Low);
});
it('renders moderate risk with 40 risk score', async () => {
render(<RiskBadge risk={40} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('40');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Moderate);
});
it('renders moderate risk with 69 risk score', async () => {
render(<RiskBadge risk={69} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('69');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Moderate);
});
it('renders high risk with 70 risk score', async () => {
render(<RiskBadge risk={70} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('70');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.High);
});
it('renders high risk with 89 risk score', async () => {
render(<RiskBadge risk={89} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('89');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.High);
});
it('renders critical risk with 90 risk score', async () => {
render(<RiskBadge risk={90} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('90');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Critical);
});
it('renders critical risk with 100 risk score', async () => {
render(<RiskBadge risk={100} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('100');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Critical);
});
it('renders critical risk with risk score over limit (100)', async () => {
render(<RiskBadge risk={400} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('400');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Critical);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { RiskSeverity } from '../../../common/search_strategy';
import { RISK_SEVERITY_COLOUR } from '../../entity_analytics/common/utils';
import { getRiskLevel } from '../../../common/entity_analytics/risk_engine/risk_levels';

export interface RiskBadgeProps {
risk: number;
'data-test-subj'?: string;
}

const tooltips = {
[RiskSeverity.Unknown]: i18n.translate(
'xpack.securitySolution.assetInventory.allAssets.risks.unknown',
{ defaultMessage: RiskSeverity.Unknown }
),
[RiskSeverity.Low]: i18n.translate('xpack.securitySolution.assetInventory.allAssets.risks.low', {
defaultMessage: RiskSeverity.Low,
}),
[RiskSeverity.Moderate]: i18n.translate(
'xpack.securitySolution.assetInventory.allAssets.risks.moderate',
{ defaultMessage: RiskSeverity.Moderate }
),
[RiskSeverity.High]: i18n.translate(
'xpack.securitySolution.assetInventory.allAssets.risks.high',
{ defaultMessage: RiskSeverity.High }
),
[RiskSeverity.Critical]: i18n.translate(
'xpack.securitySolution.assetInventory.allAssets.risks.critical',
{ defaultMessage: RiskSeverity.Critical }
),
};

export const RiskBadge = ({ risk, ...props }: RiskBadgeProps) => {
const riskLevel = getRiskLevel(risk);
const color = RISK_SEVERITY_COLOUR[riskLevel];
return (
<EuiToolTip content={tooltips[riskLevel]}>
<EuiBadge {...props} color={color}>
{risk}
</EuiBadge>
</EuiToolTip>
);
};
Original file line number Diff line number Diff line change
@@ -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.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../../../../../../..',
roots: ['<rootDir>/x-pack/solutions/security/plugins/security_solution/public/asset_inventory'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/asset_inventory',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/**/*.{ts,tsx}',
],
moduleNameMapper: require('../../server/__mocks__/module_name_map'),
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { AssetCriticalityBadge } from '../../entity_analytics/components/asset_c
import { EmptyState } from '../components/empty_state';
import { AdditionalControls } from '../components/additional_controls';
import { AssetInventorySearchBar } from '../components/search_bar';
import { RiskBadge } from '../components/risk_badge';

import { useDataViewContext } from '../hooks/data_view_context';
import { useStyles } from '../hooks/use_styles';
Expand Down Expand Up @@ -92,7 +93,7 @@ const columnHeaders: Record<string, string> = {
const customCellRenderer = (rows: DataTableRecord[]) => ({
'asset.risk': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
const risk = rows[rowIndex].flattened['asset.risk'] as number;
return risk;
return <RiskBadge risk={risk} />;
},
'asset.criticality': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
const criticality = rows[rowIndex].flattened[
Expand Down

0 comments on commit 4146dd6

Please sign in to comment.