Skip to content

Commit de05495

Browse files
PhilippeObertipgayvallet
authored andcommitted
[AI4DSOC] Change the Attack Discovery page to use the AI for SOC alerts table (elastic#218736)
## Summary While testing, we realized that the Attack Discovery alerts tab was showingn the `DetectionEngineAlertsTable`, even in the AI4DSOC tier. This PR updates the logic to show the correct alerts table depending on the tier: - AI4DSOC will show the same table as the Alert summary page - the other tiers will continue showing the same table as the Alerts page (`DetectionEngineAlertsTable`) Switching the table allows us to tackle at once all the other related issues: - wrong flyout was being shown - too many actions were being shown - wrong default columns, and wrong cell renderes ### Notes The approach is not ideal. We shouldn't have to check for the following ```typescript const AIForSOC = capabilities[SECURITY_FEATURE_ID].configurations; ``` in the code, but because of time constraints, this was the best approach. [A ticket](elastic#218731) has been opened to make sure we come back to this and implement the check the correct way later. Current (wrong) behavior https://github.com/user-attachments/assets/c41a25f1-ae9a-4bbf-9c02-9b1054f3a0e3 New behavior https://github.com/user-attachments/assets/0eb20a2f-ba00-42c0-9353-7ac788c9bea0 ### 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 Relates to elastic/security-team#11973
1 parent f8aa0e4 commit de05495

File tree

7 files changed

+532
-26
lines changed

7 files changed

+532
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render } from '@testing-library/react';
10+
import type { DataView } from '@kbn/data-views-plugin/common';
11+
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
12+
import { TestProviders } from '../../../../../../../common/mock';
13+
import { Table } from './table';
14+
import type { PackageListItem } from '@kbn/fleet-plugin/common';
15+
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
16+
17+
const dataView: DataView = createStubDataView({ spec: {} });
18+
const packages: PackageListItem[] = [
19+
{
20+
id: 'splunk',
21+
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
22+
name: 'splunk',
23+
status: installationStatuses.NotInstalled,
24+
title: 'Splunk',
25+
version: '0.1.0',
26+
},
27+
];
28+
const ruleResponse = {
29+
rules: [],
30+
isLoading: false,
31+
};
32+
const id = 'id';
33+
const query = { ids: { values: ['abcdef'] } };
34+
35+
describe('<Table />', () => {
36+
it('should render all components', () => {
37+
const { getByTestId } = render(
38+
<TestProviders>
39+
<Table
40+
dataView={dataView}
41+
id={id}
42+
packages={packages}
43+
query={query}
44+
ruleResponse={ruleResponse}
45+
/>
46+
</TestProviders>
47+
);
48+
49+
expect(getByTestId('alertsTableErrorPrompt')).toBeInTheDocument();
50+
});
51+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { memo, useMemo } from 'react';
9+
import type { DataView } from '@kbn/data-views-plugin/common';
10+
import { AlertsTable } from '@kbn/response-ops-alerts-table';
11+
import type { PackageListItem } from '@kbn/fleet-plugin/common';
12+
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
13+
import type { AdditionalTableContext } from '../../../../../../../detections/components/alert_summary/table/table';
14+
import {
15+
ACTION_COLUMN_WIDTH,
16+
ALERT_TABLE_CONSUMERS,
17+
columns,
18+
GRID_STYLE,
19+
ROW_HEIGHTS_OPTIONS,
20+
RULE_TYPE_IDS,
21+
TOOLBAR_VISIBILITY,
22+
} from '../../../../../../../detections/components/alert_summary/table/table';
23+
import { ActionsCell } from '../../../../../../../detections/components/alert_summary/table/actions_cell';
24+
import { getDataViewStateFromIndexFields } from '../../../../../../../common/containers/source/use_data_view';
25+
import { useKibana } from '../../../../../../../common/lib/kibana';
26+
import { CellValue } from '../../../../../../../detections/components/alert_summary/table/render_cell';
27+
import type { RuleResponse } from '../../../../../../../../common/api/detection_engine';
28+
29+
export interface TableProps {
30+
/**
31+
* DataView created for the alert summary page
32+
*/
33+
dataView: DataView;
34+
/**
35+
* Id to pass down to the ResponseOps alerts table
36+
*/
37+
id: string;
38+
/**
39+
* List of installed AI for SOC integrations
40+
*/
41+
packages: PackageListItem[];
42+
/**
43+
* Query that contains the id of the alerts to display in the table
44+
*/
45+
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
46+
/**
47+
* Result from the useQuery to fetch all rules
48+
*/
49+
ruleResponse: {
50+
/**
51+
* Result from fetching all rules
52+
*/
53+
rules: RuleResponse[];
54+
/**
55+
* True while rules are being fetched
56+
*/
57+
isLoading: boolean;
58+
};
59+
}
60+
61+
/**
62+
* Component used in the Attack Discovery alerts table, only in the AI4DSOC tier.
63+
* It leverages a lot of configurations and constants from the Alert summary page alerts table, and renders the ResponseOps AlertsTable.
64+
*/
65+
export const Table = memo(({ dataView, id, packages, query, ruleResponse }: TableProps) => {
66+
const {
67+
services: { application, data, fieldFormats, http, licensing, notifications, settings },
68+
} = useKibana();
69+
const services = useMemo(
70+
() => ({
71+
data,
72+
http,
73+
notifications,
74+
fieldFormats,
75+
application,
76+
licensing,
77+
settings,
78+
}),
79+
[application, data, fieldFormats, http, licensing, notifications, settings]
80+
);
81+
82+
const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]);
83+
84+
const { browserFields } = useMemo(
85+
() => getDataViewStateFromIndexFields('', dataViewSpec.fields),
86+
[dataViewSpec.fields]
87+
);
88+
89+
const additionalContext: AdditionalTableContext = useMemo(
90+
() => ({
91+
packages,
92+
ruleResponse,
93+
}),
94+
[packages, ruleResponse]
95+
);
96+
97+
return (
98+
<AlertsTable
99+
actionsColumnWidth={ACTION_COLUMN_WIDTH}
100+
additionalContext={additionalContext}
101+
browserFields={browserFields}
102+
columns={columns}
103+
consumers={ALERT_TABLE_CONSUMERS}
104+
gridStyle={GRID_STYLE}
105+
id={id}
106+
query={query}
107+
renderActionsCell={ActionsCell}
108+
renderCellValue={CellValue}
109+
rowHeightsOptions={ROW_HEIGHTS_OPTIONS}
110+
ruleTypeIds={RULE_TYPE_IDS}
111+
services={services}
112+
toolbarVisibility={TOOLBAR_VISIBILITY}
113+
/>
114+
);
115+
});
116+
117+
Table.displayName = 'Table';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { act, render } from '@testing-library/react';
10+
import {
11+
AiForSOCAlertsTab,
12+
CONTENT_TEST_ID,
13+
ERROR_TEST_ID,
14+
LOADING_PROMPT_TEST_ID,
15+
SKELETON_TEST_ID,
16+
} from './wrapper';
17+
import { useKibana } from '../../../../../../../common/lib/kibana';
18+
import { TestProviders } from '../../../../../../../common/mock';
19+
import { useFetchIntegrations } from '../../../../../../../detections/hooks/alert_summary/use_fetch_integrations';
20+
import { useFindRulesQuery } from '../../../../../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
21+
22+
jest.mock('./table', () => ({
23+
Table: () => <div />,
24+
}));
25+
jest.mock('../../../../../../../common/lib/kibana');
26+
jest.mock('../../../../../../../detections/hooks/alert_summary/use_fetch_integrations');
27+
jest.mock('../../../../../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
28+
29+
const id = 'id';
30+
const query = { ids: { values: ['abcdef'] } };
31+
32+
describe('<AiForSOCAlertsTab />', () => {
33+
beforeEach(() => {
34+
jest.clearAllMocks();
35+
36+
(useFetchIntegrations as jest.Mock).mockReturnValue({
37+
installedPackages: [],
38+
isLoading: false,
39+
});
40+
(useFindRulesQuery as jest.Mock).mockReturnValue({
41+
data: [],
42+
isLoading: false,
43+
});
44+
});
45+
46+
it('should render a loading skeleton while creating the dataView', async () => {
47+
(useKibana as jest.Mock).mockReturnValue({
48+
services: {
49+
data: {
50+
dataViews: {
51+
create: jest.fn(),
52+
clearInstanceCache: jest.fn(),
53+
},
54+
},
55+
http: { basePath: { prepend: jest.fn() } },
56+
},
57+
});
58+
59+
await act(async () => {
60+
const { getByTestId } = render(<AiForSOCAlertsTab id={id} query={query} />);
61+
62+
expect(getByTestId(LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
63+
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
64+
});
65+
});
66+
67+
it('should render a loading skeleton while fetching packages (integrations)', async () => {
68+
(useKibana as jest.Mock).mockReturnValue({
69+
services: {
70+
data: {
71+
dataViews: {
72+
create: jest.fn(),
73+
clearInstanceCache: jest.fn(),
74+
},
75+
},
76+
http: { basePath: { prepend: jest.fn() } },
77+
},
78+
});
79+
(useFetchIntegrations as jest.Mock).mockReturnValue({
80+
installedPackages: [],
81+
isLoading: true,
82+
});
83+
84+
await act(async () => {
85+
const { getByTestId } = render(<AiForSOCAlertsTab id={id} query={query} />);
86+
87+
await new Promise(process.nextTick);
88+
89+
expect(getByTestId(LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
90+
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
91+
});
92+
});
93+
94+
it('should render an error if the dataView fail to be created correctly', async () => {
95+
(useKibana as jest.Mock).mockReturnValue({
96+
services: {
97+
data: {
98+
dataViews: {
99+
create: jest.fn().mockReturnValue(undefined),
100+
clearInstanceCache: jest.fn(),
101+
},
102+
},
103+
},
104+
});
105+
106+
jest.mock('react', () => ({
107+
...jest.requireActual('react'),
108+
useEffect: jest.fn((f) => f()),
109+
}));
110+
111+
await act(async () => {
112+
const { getByTestId } = render(<AiForSOCAlertsTab id={id} query={query} />);
113+
114+
await new Promise(process.nextTick);
115+
116+
expect(getByTestId(LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
117+
expect(getByTestId(ERROR_TEST_ID)).toHaveTextContent('Unable to create data view');
118+
});
119+
});
120+
121+
it('should render the content', async () => {
122+
(useKibana as jest.Mock).mockReturnValue({
123+
services: {
124+
data: {
125+
dataViews: {
126+
create: jest
127+
.fn()
128+
.mockReturnValue({ getIndexPattern: jest.fn(), id: 'id', toSpec: jest.fn() }),
129+
clearInstanceCache: jest.fn(),
130+
},
131+
query: { filterManager: { getFilters: jest.fn() } },
132+
},
133+
},
134+
});
135+
136+
jest.mock('react', () => ({
137+
...jest.requireActual('react'),
138+
useEffect: jest.fn((f) => f()),
139+
}));
140+
141+
await act(async () => {
142+
const { getByTestId } = render(
143+
<TestProviders>
144+
<AiForSOCAlertsTab id={id} query={query} />
145+
</TestProviders>
146+
);
147+
148+
await new Promise(process.nextTick);
149+
150+
expect(getByTestId(LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
151+
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
152+
});
153+
});
154+
});

0 commit comments

Comments
 (0)