Skip to content

Commit 4ca52e6

Browse files
authored
[Security Solution][Detections] Refactor ML calls for newest ML permissions (#74582)
## Summary Addresses #73567. ML Users (role: `machine_learning_user`) were previously able to invoke the ML Recognizer API, which we use to get not-yet-installed ML Jobs relevant to our index patterns. As of #64662 this is not true, and so we receive errors from components using the underlying hook, `useSiemJobs`. To solve this I've created two separate hooks to replace `useSiemJobs`: * `useSecurityJobs` * used on ML Popover * includes uninstalled ML Jobs * checks (and returns) `isMlAdmin` before fetching data * `useInstalledSecurityJobs` * used on ML Jobs Dropdown and Anomalies Table * includes only installed ML Jobs * checks (and returns) `isMlUser` before fetching data Note that we while we now receive the knowledge to do so, we do not always inform the user in the case of invalid permissions, and instead have the following behaviors: #### User has insufficient license * ML Popover: shows an upgrade CTA * Anomalies Tables: show no data * Rule Creation: ML Rule option is disabled, shows upgrade CTA * Rule Details: ML Job Id is displayed as text #### User is ML User * ML Popover: not shown * Anomalies Tables: show no data * Rule Creation: ML Rule option is disabled * Rule Details: ML Job Id is displayed as text #### User is ML Admin * ML Popover: shown * Anomalies Tables: show data __for installed ML Jobs__ * This is the same as previous logic, but worth calling out that you can't view historical anomalies * Rule Creation: ML Rule option is enabled, all ML Jobs available * Rule Details: ML Job Id is displayed as hyperlink, job status badge shown ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
1 parent a735a9f commit 4ca52e6

File tree

56 files changed

+733
-407
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+733
-407
lines changed

x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export interface MlSummaryJob {
3636
export interface AuditMessage {
3737
job_id: string;
3838
msgTime: number;
39-
level: number;
40-
highestLevel: number;
39+
level: string;
40+
highestLevel: string;
4141
highestLevelText: string;
4242
text: string;
4343
}

x-pack/plugins/ml/public/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from '../common/constants/anomalies';
99
export * from '../common/types/data_recognizer';
1010
export * from '../common/types/capabilities';
1111
export * from '../common/types/anomalies';
12+
export * from '../common/types/anomaly_detection_jobs';
1213
export * from '../common/types/modules';
1314
export * from '../common/types/audit_message';
1415

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { emptyMlCapabilities } from './empty_ml_capabilities';
8+
import { hasMlLicense } from './has_ml_license';
9+
10+
describe('hasMlLicense', () => {
11+
test('it returns false when license is not platinum or trial', () => {
12+
const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: false };
13+
expect(hasMlLicense(capabilities)).toEqual(false);
14+
});
15+
16+
test('it returns true when license is platinum or trial', () => {
17+
const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: true };
18+
expect(hasMlLicense(capabilities)).toEqual(true);
19+
});
20+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities';
8+
9+
export const hasMlLicense = (capabilities: MlCapabilitiesResponse): boolean =>
10+
capabilities.isPlatinumOrTrialLicense;

x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { MlSummaryJob } from '../../../ml/common/types/anomaly_detection_jobs';
87
import { ML_GROUP_IDS } from '../constants';
98

10-
export const isSecurityJob = (job: MlSummaryJob): boolean =>
9+
export const isSecurityJob = (job: { groups: string[] }): boolean =>
1110
job.groups.some((group) => ML_GROUP_IDS.includes(group));

x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import { useState, useEffect, useMemo } from 'react';
99
import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants';
1010
import { anomaliesTableData } from '../api/anomalies_table_data';
1111
import { InfluencerInput, Anomalies, CriteriaFields } from '../types';
12-
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
13-
import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs';
14-
import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities';
15-
import { useStateToaster, errorToToaster } from '../../toasters';
1612

1713
import * as i18n from './translations';
1814
import { useTimeZone, useUiSetting$ } from '../../../lib/kibana';
15+
import { useAppToasts } from '../../../hooks/use_app_toasts';
16+
import { useInstalledSecurityJobs } from '../hooks/use_installed_security_jobs';
1917

2018
interface Args {
2119
influencers?: InfluencerInput[];
@@ -58,15 +56,13 @@ export const useAnomaliesTableData = ({
5856
skip = false,
5957
}: Args): Return => {
6058
const [tableData, setTableData] = useState<Anomalies | null>(null);
61-
const [, siemJobs] = useSiemJobs(true);
59+
const { isMlUser, jobs } = useInstalledSecurityJobs();
6260
const [loading, setLoading] = useState(true);
63-
const capabilities = useMlCapabilities();
64-
const userPermissions = hasMlUserPermissions(capabilities);
65-
const [, dispatchToaster] = useStateToaster();
61+
const { addError } = useAppToasts();
6662
const timeZone = useTimeZone();
6763
const [anomalyScore] = useUiSetting$<number>(DEFAULT_ANOMALY_SCORE);
6864

69-
const siemJobIds = siemJobs.filter((job) => job.isInstalled).map((job) => job.id);
65+
const jobIds = jobs.map((job) => job.id);
7066
const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]);
7167
const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]);
7268

@@ -81,11 +77,11 @@ export const useAnomaliesTableData = ({
8177
earliestMs: number,
8278
latestMs: number
8379
) {
84-
if (userPermissions && !skip && siemJobIds.length > 0) {
80+
if (isMlUser && !skip && jobIds.length > 0) {
8581
try {
8682
const data = await anomaliesTableData(
8783
{
88-
jobIds: siemJobIds,
84+
jobIds,
8985
criteriaFields: criteriaFieldsInput,
9086
aggregationInterval: 'auto',
9187
threshold: getThreshold(anomalyScore, threshold),
@@ -104,13 +100,13 @@ export const useAnomaliesTableData = ({
104100
}
105101
} catch (error) {
106102
if (isSubscribed) {
107-
errorToToaster({ title: i18n.SIEM_TABLE_FETCH_FAILURE, error, dispatchToaster });
103+
addError(error, { title: i18n.SIEM_TABLE_FETCH_FAILURE });
108104
setLoading(false);
109105
}
110106
}
111-
} else if (!userPermissions && isSubscribed) {
107+
} else if (!isMlUser && isSubscribed) {
112108
setLoading(false);
113-
} else if (siemJobIds.length === 0 && isSubscribed) {
109+
} else if (jobIds.length === 0 && isSubscribed) {
114110
setLoading(false);
115111
} else if (isSubscribed) {
116112
setTableData(null);
@@ -132,9 +128,9 @@ export const useAnomaliesTableData = ({
132128
startDateMs,
133129
endDateMs,
134130
skip,
135-
userPermissions,
131+
isMlUser,
136132
// eslint-disable-next-line react-hooks/exhaustive-deps
137-
siemJobIds.sort().join(),
133+
jobIds.sort().join(),
138134
]);
139135

140136
return [loading, tableData];
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { HttpSetup } from '../../../../../../../../src/core/public';
8+
import { MlSummaryJob } from '../../../../../../ml/public';
9+
10+
export interface GetJobsSummaryArgs {
11+
http: HttpSetup;
12+
jobIds?: string[];
13+
signal: AbortSignal;
14+
}
15+
16+
/**
17+
* Fetches a summary of all ML jobs currently installed
18+
*
19+
* @param http HTTP Service
20+
* @param jobIds Array of job IDs to filter against
21+
* @param signal to cancel request
22+
*
23+
* @throws An error if response is not OK
24+
*/
25+
export const getJobsSummary = async ({
26+
http,
27+
jobIds,
28+
signal,
29+
}: GetJobsSummaryArgs): Promise<MlSummaryJob[]> =>
30+
http.fetch<MlSummaryJob[]>('/api/ml/jobs/jobs_summary', {
31+
method: 'POST',
32+
body: JSON.stringify({ jobIds: jobIds ?? [] }),
33+
asSystemRequest: true,
34+
signal,
35+
});

x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { HttpSetup } from '../../../../../../../../src/core/public';
78
import { MlCapabilitiesResponse } from '../../../../../../ml/public';
8-
import { KibanaServices } from '../../../lib/kibana';
99
import { InfluencerInput } from '../types';
1010

1111
export interface Body {
@@ -21,10 +21,15 @@ export interface Body {
2121
maxExamples: number;
2222
}
2323

24-
export const getMlCapabilities = async (signal: AbortSignal): Promise<MlCapabilitiesResponse> => {
25-
return KibanaServices.get().http.fetch<MlCapabilitiesResponse>('/api/ml/ml_capabilities', {
24+
export const getMlCapabilities = async ({
25+
http,
26+
signal,
27+
}: {
28+
http: HttpSetup;
29+
signal: AbortSignal;
30+
}): Promise<MlCapabilitiesResponse> =>
31+
http.fetch<MlCapabilitiesResponse>('/api/ml/ml_capabilities', {
2632
method: 'GET',
2733
asSystemRequest: true,
2834
signal,
2935
});
30-
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { useAsync, withOptionalSignal } from '../../../../shared_imports';
8+
import { getJobsSummary } from '../api/get_jobs_summary';
9+
10+
const _getJobsSummary = withOptionalSignal(getJobsSummary);
11+
12+
export const useGetJobsSummary = () => useAsync(_getJobsSummary);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { getMlCapabilities } from '../api/get_ml_capabilities';
8+
import { useAsync, withOptionalSignal } from '../../../../shared_imports';
9+
10+
const _getMlCapabilities = withOptionalSignal(getMlCapabilities);
11+
12+
export const useGetMlCapabilities = () => useAsync(_getMlCapabilities);

0 commit comments

Comments
 (0)