[Security Solution] Fix alerts table grouping severity stats not showing badge (#216738)

## Summary

This PR fixes an issue introduced by [this
PR](https://github.com/elastic/kibana/pull/184635) back in `8.16`. I
don't think anyone noticed the problem until now...

In the PR linked above, the name of the property responsible to render
custom components in the group stats section of the alerts table
grouping was changed from `renderer` to `component` but there was (at
least) one usage that had not been updated. Because that usage wasn't
correctly typed and there was no unit tests to verify the behavior, the
issue went unnoticed...

### Notes

This whole code should be refactored eventually. This is not the purpose
of this PR. This only focuses on fixing the issue, adding proper types
and unit tests.

| Before | After |
| ------------- | ------------- |
| ![Screenshot 2025-04-01 at 5 00
16 PM](https://github.com/user-attachments/assets/c64b8140-4c16-4618-b8b0-0c295e9e35d5)
| ![Screenshot 2025-04-01 at 5 05
48 PM](https://github.com/user-attachments/assets/5b06ee16-b6eb-4d33-9510-75a80c569718)
|

### 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
This commit is contained in:
Philippe Oberti 2025-04-02 15:42:46 +02:00 committed by GitHub
parent 01ef04980f
commit c1939bb647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 156 additions and 41 deletions

View file

@ -10,70 +10,201 @@ import { getStats } from '.';
describe('getStats', () => {
it('returns array of badges which corresponds to the field name', () => {
const badgesRuleName = getStats('kibana.alert.rule.name', {
key: ['Rule name test', 'Some description'],
usersCountAggregation: {
value: 10,
},
key: [],
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
countSeveritySubAggregation: { value: 1 },
usersCountAggregation: { value: 3 },
hostsCountAggregation: { value: 5 },
doc_count: 10,
});
expect(badgesRuleName.length).toBe(4);
expect(
badgesRuleName.find(
(badge) => badge.badge != null && badge.title === 'Users:' && badge.badge.value === 10
(badge) => badge.title === 'Severity:' && badge.component != null && badge.badge == null
)
).toBeTruthy();
expect(
badgesRuleName.find(
(badge) => badge.badge != null && badge.title === 'Alerts:' && badge.badge.value === 10
(badge) =>
badge.title === 'Users:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 3
)
).toBeTruthy();
expect(
badgesRuleName.find(
(badge) =>
badge.title === 'Hosts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 5
)
).toBeTruthy();
expect(
badgesRuleName.find(
(badge) =>
badge.title === 'Alerts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 10
)
).toBeTruthy();
const badgesHostName = getStats('host.name', {
key: 'Host',
rulesCountAggregation: {
value: 3,
},
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
countSeveritySubAggregation: { value: 1 },
usersCountAggregation: { value: 5 },
rulesCountAggregation: { value: 3 },
doc_count: 2,
});
expect(badgesHostName.length).toBe(4);
expect(
badgesHostName.find(
(badge) => badge.badge != null && badge.title === 'Rules:' && badge.badge.value === 3
(badge) => badge.title === 'Severity:' && badge.component != null && badge.badge == null
)
).toBeTruthy();
expect(
badgesHostName.find(
(badge) =>
badge.title === 'Users:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 5
)
).toBeTruthy();
expect(
badgesHostName.find(
(badge) =>
badge.title === 'Rules:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 3
)
).toBeTruthy();
expect(
badgesHostName.find(
(badge) =>
badge.title === 'Alerts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 2
)
).toBeTruthy();
const badgesUserName = getStats('user.name', {
key: 'User test',
hostsCountAggregation: {
value: 1,
},
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
countSeveritySubAggregation: { value: 1 },
rulesCountAggregation: { value: 2 },
hostsCountAggregation: { value: 1 },
doc_count: 1,
});
expect(badgesUserName.length).toBe(4);
expect(
badgesUserName.find(
(badge) => badge.badge != null && badge.title === `Hosts:` && badge.badge.value === 1
(badge) => badge.title === 'Severity:' && badge.component != null && badge.badge == null
)
).toBeTruthy();
expect(
badgesUserName.find(
(badge) =>
badge.title === 'Hosts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 1
)
).toBeTruthy();
expect(
badgesUserName.find(
(badge) =>
badge.title === 'Rules:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 2
)
).toBeTruthy();
expect(
badgesUserName.find(
(badge) =>
badge.title === 'Alerts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 1
)
).toBeTruthy();
const badgesSourceIp = getStats('source.ip', {
key: 'User test',
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
countSeveritySubAggregation: { value: 1 },
rulesCountAggregation: { value: 16 },
hostsCountAggregation: { value: 17 },
doc_count: 18,
});
expect(badgesSourceIp.length).toBe(4);
expect(
badgesSourceIp.find(
(badge) => badge.title === 'Severity:' && badge.component != null && badge.badge == null
)
).toBeTruthy();
expect(
badgesSourceIp.find(
(badge) =>
badge.title === 'Hosts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 17
)
).toBeTruthy();
expect(
badgesSourceIp.find(
(badge) =>
badge.title === 'Rules:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 16
)
).toBeTruthy();
expect(
badgesSourceIp.find(
(badge) =>
badge.title === 'Alerts:' &&
badge.component == null &&
badge.badge != null &&
badge.badge.value === 18
)
).toBeTruthy();
});
it('returns default badges if the field specific does not exist', () => {
it('should return default badges if the field specific does not exist', () => {
const badges = getStats('process.name', {
key: 'process',
rulesCountAggregation: {
value: 3,
},
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
countSeveritySubAggregation: { value: 1 },
rulesCountAggregation: { value: 3 },
doc_count: 10,
});
expect(badges.length).toBe(2);
expect(badges.length).toBe(3);
expect(
badges.find(
(badge) => badge.badge != null && badge.title === 'Rules:' && badge.badge.value === 3
(badge) => badge.title === 'Severity:' && badge.component != null && badge.badge == null
)
).toBeTruthy();
expect(
badges.find(
(badge) => badge.badge != null && badge.title === 'Alerts:' && badge.badge.value === 10
(badge) => badge.title === 'Rules:' && badge.component == null && badge.badge?.value === 3
)
).toBeTruthy();
expect(
badges.find(
(badge) => badge.title === 'Alerts:' && badge.component == null && badge.badge?.value === 10
)
).toBeTruthy();
});

View file

@ -7,7 +7,7 @@
import { EuiIcon } from '@elastic/eui';
import React from 'react';
import type { RawBucket, GroupStatsItem } from '@kbn/grouping';
import type { GroupStatsItem, RawBucket } from '@kbn/grouping';
import type { AlertsGroupingAggregation } from './types';
import * as i18n from '../translations';
@ -77,16 +77,16 @@ export const getStats = (
? multiSeverity
: singleSeverityComponent;
const severityStat = !severityComponent
const severityStat: GroupStatsItem[] = !severityComponent
? []
: [
{
title: i18n.STATS_GROUP_SEVERITY,
renderer: severityComponent,
component: severityComponent,
},
];
const defaultBadges = [
const defaultBadges: GroupStatsItem[] = [
{
title: i18n.STATS_GROUP_ALERTS,
badge: {
@ -133,22 +133,6 @@ export const getStats = (
...defaultBadges,
];
case 'user.name':
return [
...severityStat,
{
title: i18n.STATS_GROUP_HOSTS,
badge: {
value: bucket.hostsCountAggregation?.value ?? 0,
},
},
{
title: i18n.STATS_GROUP_RULES,
badge: {
value: bucket.rulesCountAggregation?.value ?? 0,
},
},
...defaultBadges,
];
case 'source.ip':
return [
...severityStat,