Skip to content

Commit 85ba636

Browse files
authored
[SecuritySolution][PrivMon] Rewrite dashboard queries to use FORK (#223212)
## Summary ### What is included? * Improves the auth dashboard to display system events * Add data view index patterns as visualisations index * Move ESQL query generation to a shared folder * Parse ESQL query and validate if fields exist in the dataview * Rewrite the ESQL query if a FORK command has missing fields * Add a visualisation warning message when there is no valid FORK branch ![Screenshot 2025-06-20 at 07 22 47](https://github.com/user-attachments/assets/3ff85561-33b6-4f40-8037-4e983d6e4057) ### Pros * To be able to render parts of the query depending on whether indices or fields exist in the cluster * The queries become much easier to read, maintain and fix ### Cons * We need to test the performance * FORK is in tech preview * The commands we can use in a fork are limited to “WHERE, LIMIT, SORT, EVAL, STATS, DISSECT” ### How to test it? * Open the dashboard without privmon data, some of the visualisations should display the warning message * Add privmon data, the visualisation should display the data (elastic/security-documents-generator#163) * Check if the visualisation displays the correct data. * To test if the FORK rewrite logic is working, I update the queries on my local environment to use a non-existent field and update the page. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [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 - [ ] 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)
1 parent 0404a6d commit 85ba636

File tree

18 files changed

+598
-150
lines changed

18 files changed

+598
-150
lines changed

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_access_detection/pad_chart/hooks/pad_esql_source_query_hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
*/
77

88
import { useIntervalForHeatmap } from './pad_heatmap_interval_hooks';
9-
import { getPrivilegedMonitorUsersJoin } from '../../../../helpers';
109
import type { AnomalyBand } from '../pad_anomaly_bands';
10+
import { getPrivilegedMonitorUsersJoin } from '../../../../queries/helpers';
1111

1212
const getHiddenBandsFilters = (anomalyBands: AnomalyBand[]) => {
1313
const hiddenBands = anomalyBands.filter((each) => each.hidden);

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/columns.test.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -138,38 +138,14 @@ describe('columns', () => {
138138
expect(screen.getByText('Console')).toBeInTheDocument();
139139
});
140140

141-
it('renders type column as "Direct" for /api/v1/authn*', () => {
141+
it('renders type column', () => {
142142
const col = columns[4] as EuiTableFieldDataColumnType<TableItemType>;
143-
render(<>{col.render?.('/api/v1/authn/something', baseRecord)}</>, {
143+
render(<>{col.render?.('Direct', baseRecord)}</>, {
144144
wrapper: TestProviders,
145145
});
146146
expect(screen.getByText('Direct')).toBeInTheDocument();
147147
});
148148

149-
it('renders type column as "Federated" for /oauth2/v1/authorize', () => {
150-
const col = columns[4] as EuiTableFieldDataColumnType<TableItemType>;
151-
render(<>{col.render?.('/oauth2/v1/authorize', baseRecord)}</>, { wrapper: TestProviders });
152-
expect(screen.getByText('Federated')).toBeInTheDocument();
153-
});
154-
155-
it('renders type column as "Federated" for /oauth2/v1/token', () => {
156-
const col = columns[4] as EuiTableFieldDataColumnType<TableItemType>;
157-
render(<>{col.render?.('/oauth2/v1/token', baseRecord)}</>, { wrapper: TestProviders });
158-
expect(screen.getByText('Federated')).toBeInTheDocument();
159-
});
160-
161-
it('renders type column as "Federated" for string containing /sso/saml', () => {
162-
const col = columns[4] as EuiTableFieldDataColumnType<TableItemType>;
163-
render(<>{col.render?.('/some/path/sso/saml', baseRecord)}</>, { wrapper: TestProviders });
164-
expect(screen.getByText('Federated')).toBeInTheDocument();
165-
});
166-
167-
it('renders type column as original value for unmatched string', () => {
168-
const col = columns[4] as EuiTableFieldDataColumnType<TableItemType>;
169-
render(<>{col.render?.('/api/v1/authn', baseRecord)}</>, { wrapper: TestProviders });
170-
expect(screen.getByText('Direct')).toBeInTheDocument();
171-
});
172-
173149
it('renders result column with badge', () => {
174150
const col = columns[5] as EuiTableFieldDataColumnType<TableItemType>;
175151
render(<>{col.render?.('success', baseRecord)}</>, { wrapper: TestProviders });

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/columns.tsx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -221,27 +221,20 @@ export const buildAuthenticationsColumns = (
221221
},
222222
},
223223
{
224-
field: 'url',
224+
field: 'type',
225225
name: (
226226
<FormattedMessage
227227
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.userActivity.columns.type"
228228
defaultMessage="Type"
229229
/>
230230
),
231-
render: (url?: string) => {
232-
if (!url) {
233-
return getEmptyTagValue();
234-
}
235-
236-
const type = getLoginTypeFromUrl(url);
237-
231+
render: (type?: string) => {
238232
if (!type) {
239233
return getEmptyTagValue();
240234
}
241235

242236
return type;
243237
},
244-
truncateText: true,
245238
},
246239
// TODO Add the column depending on this ticket output https://github.com/elastic/security-team/issues/12713
247240
// {
@@ -294,25 +287,3 @@ const getResultColor = (value: string) => {
294287
}
295288
return 'default';
296289
};
297-
298-
// TODO Verify if we can improve this logic https://github.com/elastic/security-team/issues/12713
299-
const getLoginTypeFromUrl = (url: string) => {
300-
if (url.startsWith('/api/v1/authn')) {
301-
return i18n.translate(
302-
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.userActivity.columns.type.direct',
303-
{ defaultMessage: 'Direct' }
304-
);
305-
}
306-
307-
if (
308-
url.startsWith('/oauth2/v1/authorize') ||
309-
url.startsWith('/oauth2/v1/token') ||
310-
url.includes('/sso/saml')
311-
) {
312-
return i18n.translate(
313-
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.userActivity.columns.type.federated',
314-
{ defaultMessage: 'Federated' }
315-
);
316-
}
317-
return undefined;
318-
};

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/esql_source_query.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/hooks.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import React, { useMemo } from 'react';
99
import { FormattedMessage } from '@kbn/i18n-react';
1010
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
11+
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
1112
import { useSpaceId } from '../../../../../common/hooks/use_space_id';
1213
import {
1314
generateListESQLQuery,
@@ -20,16 +21,14 @@ import {
2021
buildAuthenticationsColumns,
2122
} from './columns';
2223
import { getLensAttributes } from './get_lens_attributes';
23-
import {
24-
getAccountSwitchesEsqlSource,
25-
getAuthenticationsEsqlSource,
26-
getGrantedRightsEsqlSource,
27-
} from './esql_source_query';
2824
import {
2925
ACCOUNT_SWITCH_STACK_BY,
3026
AUTHENTICATIONS_STACK_BY,
3127
GRANTED_RIGHTS_STACK_BY,
3228
} from './constants';
29+
import { getAuthenticationsEsqlSource } from '../../queries/authentications_esql_query';
30+
import { getAccountSwitchesEsqlSource } from '../../queries/account_switches_esql_query';
31+
import { getGrantedRightsEsqlSource } from '../../queries/granted_rights_esql_query';
3332

3433
const toggleOptionsConfig = {
3534
[VisualizationToggleOptions.GRANTED_RIGHTS]: {
@@ -50,13 +49,24 @@ const toggleOptionsConfig = {
5049
};
5150

5251
export const usePrivilegedUserActivityParams = (
53-
selectedToggleOption: VisualizationToggleOptions
52+
selectedToggleOption: VisualizationToggleOptions,
53+
sourcererDataView: DataViewSpec
5454
) => {
5555
const spaceId = useSpaceId();
56+
57+
const indexPattern = sourcererDataView?.title ?? '';
58+
const fields = sourcererDataView?.fields;
59+
5660
const esqlSource = useMemo(
5761
() =>
58-
spaceId ? toggleOptionsConfig[selectedToggleOption].generateEsqlSource(spaceId) : undefined,
59-
[selectedToggleOption, spaceId]
62+
spaceId && indexPattern && fields
63+
? toggleOptionsConfig[selectedToggleOption].generateEsqlSource(
64+
spaceId,
65+
indexPattern,
66+
fields
67+
)
68+
: undefined,
69+
[selectedToggleOption, spaceId, indexPattern, fields]
6070
);
6171

6272
const generateTableQuery = useMemo(
@@ -75,11 +85,17 @@ export const usePrivilegedUserActivityParams = (
7585
[selectedToggleOption, openRightPanel]
7686
);
7787

88+
const hasLoadedDependencies = useMemo(
89+
() => Boolean(spaceId && indexPattern && fields),
90+
[spaceId, indexPattern, fields]
91+
);
92+
7893
return {
7994
getLensAttributes,
8095
generateVisualizationQuery,
8196
generateTableQuery,
8297
columns,
98+
hasLoadedDependencies,
8399
};
84100
};
85101

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/components/privileged_user_activity/index.test.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,39 +28,64 @@ jest.mock('../../../../../common/hooks/use_space_id', () => ({
2828
useSpaceId: jest.fn().mockReturnValue('default'),
2929
}));
3030

31+
const mockedSourcererDataView = {
32+
title: 'test-*',
33+
fields: {},
34+
};
35+
3136
describe('UserActivityPrivilegedUsersPanel', () => {
3237
it('renders panel title', () => {
33-
render(<UserActivityPrivilegedUsersPanel />, { wrapper: TestProviders });
38+
render(<UserActivityPrivilegedUsersPanel sourcererDataView={mockedSourcererDataView} />, {
39+
wrapper: TestProviders,
40+
});
41+
3442
expect(screen.getByText('Privileged user activity')).toBeInTheDocument();
3543
});
3644

3745
it('renders the toggle button group', () => {
38-
render(<UserActivityPrivilegedUsersPanel />, { wrapper: TestProviders });
39-
expect(screen.getByRole('group', { name: /ABOUT_CONTROL_LEGEND/i })).toBeInTheDocument();
46+
render(<UserActivityPrivilegedUsersPanel sourcererDataView={mockedSourcererDataView} />, {
47+
wrapper: TestProviders,
48+
});
49+
expect(
50+
screen.getByRole('group', { name: /Select a visualization to display/i })
51+
).toBeInTheDocument();
4052
});
4153

4254
it('renders the stack by select with options', () => {
43-
render(<UserActivityPrivilegedUsersPanel />, { wrapper: TestProviders });
55+
render(<UserActivityPrivilegedUsersPanel sourcererDataView={mockedSourcererDataView} />, {
56+
wrapper: TestProviders,
57+
});
4458
expect(screen.getByText('Stack by')).toBeInTheDocument();
4559
expect(screen.getByRole('option', { name: 'Privileged user' })).toBeInTheDocument();
4660
expect(screen.getByRole('option', { name: 'Target user' })).toBeInTheDocument();
4761
expect(screen.getByRole('option', { name: 'Granted right' })).toBeInTheDocument();
4862
});
4963

5064
it('renders the EsqlDashboardPanel', () => {
51-
render(<UserActivityPrivilegedUsersPanel />, { wrapper: TestProviders });
65+
render(<UserActivityPrivilegedUsersPanel sourcererDataView={mockedSourcererDataView} />, {
66+
wrapper: TestProviders,
67+
});
68+
// select a visualization that doesn't require dataview fields
69+
fireEvent.click(screen.getByTestId('account_switches'));
70+
5271
expect(screen.getByTestId('esql-dashboard-panel')).toBeInTheDocument();
5372
});
5473

5574
it('changes stack by option when select changes', () => {
56-
render(<UserActivityPrivilegedUsersPanel />, { wrapper: TestProviders });
75+
render(<UserActivityPrivilegedUsersPanel sourcererDataView={mockedSourcererDataView} />, {
76+
wrapper: TestProviders,
77+
});
5778
const select = screen.getByRole('combobox');
5879
fireEvent.change(select, { target: { value: 'group_name' } });
5980
expect((select as HTMLSelectElement).value).toBe('group_name');
6081
});
6182

6283
it('renders the "View all events by privileged users" link', () => {
63-
render(<UserActivityPrivilegedUsersPanel />, { wrapper: TestProviders });
84+
render(<UserActivityPrivilegedUsersPanel sourcererDataView={mockedSourcererDataView} />, {
85+
wrapper: TestProviders,
86+
});
87+
// select a visualization that doesn't require dataview fields
88+
fireEvent.click(screen.getByTestId('account_switches'));
6489
expect(screen.getByText('View all events')).toBeInTheDocument();
6590
});
6691
});

0 commit comments

Comments
 (0)