From 4ca52e678a86bc4c9ffef02a9886f6242fdb2273 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 12 Aug 2020 19:26:06 -0500 Subject: [PATCH] [Security Solution][Detections] Refactor ML calls for newest ML permissions (#74582) ## Summary Addresses https://github.com/elastic/kibana/issues/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 https://github.com/elastic/kibana/pull/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) --- .../anomaly_detection_jobs/summary_job.ts | 4 +- x-pack/plugins/ml/public/shared.ts | 1 + .../machine_learning/has_ml_license.test.ts | 20 ++++ .../common/machine_learning/has_ml_license.ts | 10 ++ .../machine_learning/is_security_job.ts | 3 +- .../ml/anomaly/use_anomalies_table_data.ts | 28 ++--- .../components/ml/api/get_jobs_summary.ts | 35 ++++++ .../components/ml/api/get_ml_capabilities.ts | 13 ++- .../ml/hooks/use_get_jobs_summary.ts | 12 ++ .../ml/hooks/use_get_ml_capabilities.ts | 12 ++ .../hooks/use_installed_security_jobs.test.ts | 99 ++++++++++++++++ .../ml/hooks/use_installed_security_jobs.ts | 63 ++++++++++ .../hooks/use_ml_capabilities.ts} | 2 +- .../permissions/ml_capabilities_provider.tsx | 47 +++----- .../ml/tables/anomalies_host_table.tsx | 2 +- .../ml/tables/anomalies_network_table.tsx | 2 +- .../{ml_popover/hooks => ml}/translations.ts | 0 .../{__mocks__/api.tsx => api.mock.ts} | 12 +- .../components/ml_popover/{api.tsx => api.ts} | 19 --- .../components/ml_popover/helpers.test.tsx | 12 +- .../common/components/ml_popover/helpers.tsx | 8 +- .../hooks/use_security_jobs.test.ts | 110 ++++++++++++++++++ .../ml_popover/hooks/use_security_jobs.ts | 95 +++++++++++++++ ...tsx => use_security_jobs_helpers.test.tsx} | 28 ++--- ...pers.tsx => use_security_jobs_helpers.tsx} | 71 +++++------ .../ml_popover/hooks/use_siem_jobs.tsx | 81 ------------- .../jobs_table_filters.test.tsx.snap | 2 +- .../filters/groups_filter_popover.test.tsx | 15 ++- .../filters/groups_filter_popover.tsx | 14 +-- .../filters/jobs_table_filters.test.tsx | 18 +-- .../jobs_table/filters/jobs_table_filters.tsx | 16 ++- .../ml_popover/jobs_table/job_switch.test.tsx | 24 ++-- .../ml_popover/jobs_table/job_switch.tsx | 12 +- .../ml_popover/jobs_table/jobs_table.test.tsx | 24 ++-- .../ml_popover/jobs_table/jobs_table.tsx | 27 +++-- .../components/ml_popover/ml_popover.tsx | 29 +++-- .../common/components/ml_popover/types.ts | 30 +---- .../anomalies_query_tab_body/index.tsx | 6 +- .../anomalies_query_tab_body/utils.ts | 9 +- .../common/hooks/use_app_toasts.mock.ts | 14 +++ .../common/lib/kibana/__mocks__/index.ts | 1 + .../public/common/mock/kibana_core.ts | 2 + .../public/common/mock/kibana_react.ts | 18 --- .../rules/description_step/index.tsx | 6 +- .../ml_job_description.test.tsx | 25 +--- .../description_step/ml_job_description.tsx | 14 +-- .../rules/ml_job_select/index.test.tsx | 6 +- .../components/rules/ml_job_select/index.tsx | 12 +- .../rules/step_define_rule/index.tsx | 5 +- .../detection_engine/rules/all/index.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 6 +- .../public/hosts/pages/details/index.tsx | 2 +- .../public/hosts/pages/hosts.tsx | 2 +- .../network/components/ip_overview/index.tsx | 2 +- .../public/network/pages/index.tsx | 2 +- .../components/host_overview/index.tsx | 2 +- 56 files changed, 733 insertions(+), 407 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts create mode 100644 x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts rename x-pack/plugins/security_solution/public/common/components/{ml_popover/hooks/use_ml_capabilities.tsx => ml/hooks/use_ml_capabilities.ts} (80%) rename x-pack/plugins/security_solution/public/common/components/{ml_popover/hooks => ml}/translations.ts (100%) rename x-pack/plugins/security_solution/public/common/components/ml_popover/{__mocks__/api.tsx => api.mock.ts} (99%) rename x-pack/plugins/security_solution/public/common/components/ml_popover/{api.tsx => api.ts} (89%) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts rename x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/{use_siem_jobs_helpers.test.tsx => use_security_jobs_helpers.test.tsx} (83%) rename x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/{use_siem_jobs_helpers.tsx => use_security_jobs_helpers.tsx} (59%) delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 210267306027..d0bce8508e82 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -36,8 +36,8 @@ export interface MlSummaryJob { export interface AuditMessage { job_id: string; msgTime: number; - level: number; - highestLevel: number; + level: string; + highestLevel: string; highestLevelText: string; text: string; } diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index 4b1d7ee733dc..ec884bfac535 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -9,6 +9,7 @@ export * from '../common/constants/anomalies'; export * from '../common/types/data_recognizer'; export * from '../common/types/capabilities'; export * from '../common/types/anomalies'; +export * from '../common/types/anomaly_detection_jobs'; export * from '../common/types/modules'; export * from '../common/types/audit_message'; diff --git a/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts new file mode 100644 index 000000000000..1ffc2e16b78f --- /dev/null +++ b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { emptyMlCapabilities } from './empty_ml_capabilities'; +import { hasMlLicense } from './has_ml_license'; + +describe('hasMlLicense', () => { + test('it returns false when license is not platinum or trial', () => { + const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: false }; + expect(hasMlLicense(capabilities)).toEqual(false); + }); + + test('it returns true when license is platinum or trial', () => { + const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: true }; + expect(hasMlLicense(capabilities)).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts new file mode 100644 index 000000000000..c0b6862ac30f --- /dev/null +++ b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities'; + +export const hasMlLicense = (capabilities: MlCapabilitiesResponse): boolean => + capabilities.isPlatinumOrTrialLicense; diff --git a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts index 43cfa4ad5996..f5783fc9b397 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlSummaryJob } from '../../../ml/common/types/anomaly_detection_jobs'; import { ML_GROUP_IDS } from '../constants'; -export const isSecurityJob = (job: MlSummaryJob): boolean => +export const isSecurityJob = (job: { groups: string[] }): boolean => job.groups.some((group) => ML_GROUP_IDS.includes(group)); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 6fbb308672e5..e6597de892bf 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -9,13 +9,11 @@ import { useState, useEffect, useMemo } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; -import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; -import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; -import { useStateToaster, errorToToaster } from '../../toasters'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useInstalledSecurityJobs } from '../hooks/use_installed_security_jobs'; interface Args { influencers?: InfluencerInput[]; @@ -58,15 +56,13 @@ export const useAnomaliesTableData = ({ skip = false, }: Args): Return => { const [tableData, setTableData] = useState(null); - const [, siemJobs] = useSiemJobs(true); + const { isMlUser, jobs } = useInstalledSecurityJobs(); const [loading, setLoading] = useState(true); - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); const timeZone = useTimeZone(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); - const siemJobIds = siemJobs.filter((job) => job.isInstalled).map((job) => job.id); + const jobIds = jobs.map((job) => job.id); const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); @@ -81,11 +77,11 @@ export const useAnomaliesTableData = ({ earliestMs: number, latestMs: number ) { - if (userPermissions && !skip && siemJobIds.length > 0) { + if (isMlUser && !skip && jobIds.length > 0) { try { const data = await anomaliesTableData( { - jobIds: siemJobIds, + jobIds, criteriaFields: criteriaFieldsInput, aggregationInterval: 'auto', threshold: getThreshold(anomalyScore, threshold), @@ -104,13 +100,13 @@ export const useAnomaliesTableData = ({ } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.SIEM_TABLE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.SIEM_TABLE_FETCH_FAILURE }); setLoading(false); } } - } else if (!userPermissions && isSubscribed) { + } else if (!isMlUser && isSubscribed) { setLoading(false); - } else if (siemJobIds.length === 0 && isSubscribed) { + } else if (jobIds.length === 0 && isSubscribed) { setLoading(false); } else if (isSubscribed) { setTableData(null); @@ -132,9 +128,9 @@ export const useAnomaliesTableData = ({ startDateMs, endDateMs, skip, - userPermissions, + isMlUser, // eslint-disable-next-line react-hooks/exhaustive-deps - siemJobIds.sort().join(), + jobIds.sort().join(), ]); return [loading, tableData]; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts new file mode 100644 index 000000000000..15f823814d7f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from '../../../../../../../../src/core/public'; +import { MlSummaryJob } from '../../../../../../ml/public'; + +export interface GetJobsSummaryArgs { + http: HttpSetup; + jobIds?: string[]; + signal: AbortSignal; +} + +/** + * Fetches a summary of all ML jobs currently installed + * + * @param http HTTP Service + * @param jobIds Array of job IDs to filter against + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const getJobsSummary = async ({ + http, + jobIds, + signal, +}: GetJobsSummaryArgs): Promise => + http.fetch('/api/ml/jobs/jobs_summary', { + method: 'POST', + body: JSON.stringify({ jobIds: jobIds ?? [] }), + asSystemRequest: true, + signal, + }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts index 32f6f888ab8d..8ee765c1dea4 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from '../../../../../../../../src/core/public'; import { MlCapabilitiesResponse } from '../../../../../../ml/public'; -import { KibanaServices } from '../../../lib/kibana'; import { InfluencerInput } from '../types'; export interface Body { @@ -21,10 +21,15 @@ export interface Body { maxExamples: number; } -export const getMlCapabilities = async (signal: AbortSignal): Promise => { - return KibanaServices.get().http.fetch('/api/ml/ml_capabilities', { +export const getMlCapabilities = async ({ + http, + signal, +}: { + http: HttpSetup; + signal: AbortSignal; +}): Promise => + http.fetch('/api/ml/ml_capabilities', { method: 'GET', asSystemRequest: true, signal, }); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts new file mode 100644 index 000000000000..a80bfb59649c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; +import { getJobsSummary } from '../api/get_jobs_summary'; + +const _getJobsSummary = withOptionalSignal(getJobsSummary); + +export const useGetJobsSummary = () => useAsync(_getJobsSummary); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts new file mode 100644 index 000000000000..aabd8c7b62e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getMlCapabilities } from '../api/get_ml_capabilities'; +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; + +const _getMlCapabilities = withOptionalSignal(getMlCapabilities); + +export const useGetMlCapabilities = () => useAsync(_getMlCapabilities); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts new file mode 100644 index 000000000000..72690a177392 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock'; +import { mockJobsSummaryResponse } from '../../ml_popover/api.mock'; +import { getJobsSummary } from '../api/get_jobs_summary'; +import { useInstalledSecurityJobs } from './use_installed_security_jobs'; + +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); +jest.mock('../../../../../common/machine_learning/has_ml_license'); +jest.mock('../../../hooks/use_app_toasts'); +jest.mock('../api/get_jobs_summary'); + +describe('useInstalledSecurityJobs', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + (getJobsSummary as jest.Mock).mockResolvedValue(mockJobsSummaryResponse); + }); + + describe('when the user has permissions', () => { + beforeEach(() => { + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + (hasMlLicense as jest.Mock).mockReturnValue(true); + }); + + it('returns jobs and permissions', async () => { + const { result, waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(result.current.jobs).toHaveLength(3); + expect(result.current.jobs).toEqual( + expect.arrayContaining([ + { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'stopped', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1557353420495, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1557434782207, + memory_status: 'hard_limit', + processed_record_count: 582251, + }, + ]) + ); + expect(result.current.isMlUser).toEqual(true); + expect(result.current.isLicensed).toEqual(true); + }); + + it('filters out non-security jobs', async () => { + const { result, waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(result.current.jobs.length).toBeGreaterThan(0); + expect(result.current.jobs.every(isSecurityJob)).toEqual(true); + }); + + it('renders a toast error if the ML call fails', async () => { + (getJobsSummary as jest.Mock).mockRejectedValue('whoops'); + const { waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(appToastsMock.addError).toHaveBeenCalledWith('whoops', { + title: 'Security job fetch failure', + }); + }); + }); + + describe('when the user does not have valid permissions', () => { + beforeEach(() => { + (hasMlUserPermissions as jest.Mock).mockReturnValue(false); + (hasMlLicense as jest.Mock).mockReturnValue(false); + }); + + it('returns empty jobs and false predicates', () => { + const { result } = renderHook(() => useInstalledSecurityJobs()); + + expect(result.current.jobs).toEqual([]); + expect(result.current.isMlUser).toEqual(false); + expect(result.current.isLicensed).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts new file mode 100644 index 000000000000..a9a728f81cc6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { MlSummaryJob } from '../../../../../../ml/public'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useMlCapabilities } from './use_ml_capabilities'; +import * as i18n from '../translations'; +import { useGetJobsSummary } from './use_get_jobs_summary'; + +export interface UseInstalledSecurityJobsReturn { + loading: boolean; + jobs: MlSummaryJob[]; + isMlUser: boolean; + isLicensed: boolean; +} + +/** + * Returns a collection of installed ML jobs (MlSummaryJob) relevant to + * Security Solution, i.e. all installed jobs in the `security` ML group. + * Use the corresponding helper functions to filter the job list as + * necessary (running jobs, etc). + * + */ +export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { + const [jobs, setJobs] = useState([]); + const { addError } = useAppToasts(); + const mlCapabilities = useMlCapabilities(); + const http = useHttp(); + const { error, loading, result, start } = useGetJobsSummary(); + + const isMlUser = hasMlUserPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + if (isMlUser && isLicensed) { + start({ http }); + } + }, [http, isMlUser, isLicensed, start]); + + useEffect(() => { + if (result) { + const securityJobs = result.filter(isSecurityJob); + setJobs(securityJobs); + } + }, [result]); + + useEffect(() => { + if (error) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + }, [addError, error]); + + return { isLicensed, isMlUser, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts similarity index 80% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx rename to x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts index d897b2554b4f..4f804a355e4b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts @@ -6,6 +6,6 @@ import { useContext } from 'react'; -import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider'; +import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider'; export const useMlCapabilities = () => useContext(MlCapabilitiesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx index c83271a56be5..c12c8d78da71 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx @@ -8,9 +8,9 @@ import React, { useState, useEffect } from 'react'; import { MlCapabilitiesResponse } from '../../../../../../ml/public'; import { emptyMlCapabilities } from '../../../../../common/machine_learning/empty_ml_capabilities'; -import { getMlCapabilities } from '../api/get_ml_capabilities'; -import { errorToToaster, useStateToaster } from '../../toasters'; - +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useGetMlCapabilities } from '../hooks/use_get_ml_capabilities'; import * as i18n from './translations'; interface MlCapabilitiesProvider extends MlCapabilitiesResponse { @@ -32,36 +32,27 @@ export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ c const [capabilities, setCapabilities] = useState( emptyMlCapabilitiesProvider ); - const [, dispatchToaster] = useStateToaster(); + const http = useHttp(); + const { addError } = useAppToasts(); + const { start, result, error } = useGetMlCapabilities(); useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + start({ http }); + }, [http, start]); - async function fetchMlCapabilities() { - try { - const mlCapabilities = await getMlCapabilities(abortCtrl.signal); - if (isSubscribed) { - setCapabilities({ ...mlCapabilities, capabilitiesFetched: true }); - } - } catch (error) { - if (isSubscribed) { - errorToToaster({ - title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE, - error, - dispatchToaster, - }); - } - } + useEffect(() => { + if (result) { + setCapabilities({ ...result, capabilitiesFetched: true }); } + }, [result]); - fetchMlCapabilities(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useEffect(() => { + if (error) { + addError(error, { + title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE, + }); + } + }, [addError, error]); return ( {children} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 9bfae686b1a5..7fdf41e6b650 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -16,7 +16,7 @@ import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts'; import { Loader } from '../../loader'; import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies'; import { AnomaliesHostTableProps } from '../types'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { hostEquality } from './host_equality'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index af27d411b990..124d8d9a794c 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -14,7 +14,7 @@ import { convertAnomaliesToNetwork } from './convert_anomalies_to_network'; import { Loader } from '../../loader'; import { AnomaliesNetworkTableProps } from '../types'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/components/ml/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts rename to x-pack/plugins/security_solution/public/common/components/ml/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts index 54bb0a96207e..0e8f033ff0cf 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MlSummaryJob } from '../../../../../ml/public'; import { Group, - JobSummary, Module, RecognizerModule, SetupMlResponse, - SiemJob, + SecurityJob, StartDatafeedResponse, StopDatafeedResponse, -} from '../types'; +} from './types'; export const mockGroupsResponse: Group[] = [ { @@ -31,7 +31,7 @@ export const mockGroupsResponse: Group[] = [ { id: 'suricata', jobIds: ['suricata_alert_rate'], calendarIds: [] }, ]; -export const mockOpenedJob: JobSummary = { +export const mockOpenedJob: MlSummaryJob = { datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', datafeedIndices: ['auditbeat-*'], datafeedState: 'started', @@ -48,7 +48,7 @@ export const mockOpenedJob: JobSummary = { processed_record_count: 3425264, }; -export const mockJobsSummaryResponse: JobSummary[] = [ +export const mockJobsSummaryResponse: MlSummaryJob[] = [ { id: 'rc-rare-process-windows-5', description: @@ -491,7 +491,7 @@ export const mockStopDatafeedsSuccess: StopDatafeedResponse = { 'datafeed-linux_anomalous_network_service': { stopped: true }, }; -export const mockSiemJobs: SiemJob[] = [ +export const mockSecurityJobs: SecurityJob[] = [ { id: 'linux_anomalous_network_activity_ecs', description: diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts similarity index 89% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts index 7c72098209a0..dd0fb33fd2bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts @@ -9,7 +9,6 @@ import { CloseJobsResponse, ErrorResponse, GetModulesProps, - JobSummary, MlSetupArgs, Module, RecognizerModule, @@ -165,21 +164,3 @@ export const stopDatafeeds = async ({ return [stopDatafeedsResponse, closeJobsResponse]; }; - -/** - * Fetches a summary of all ML jobs currently installed - * - * NOTE: If not sending jobIds in the body, you must at least send an empty body or the server will - * return a 500 - * - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const getJobsSummary = async (signal: AbortSignal): Promise => - KibanaServices.get().http.fetch('/api/ml/jobs/jobs_summary', { - method: 'POST', - body: JSON.stringify({}), - asSystemRequest: true, - signal, - }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx index 0b8da6be57e1..2a2db46d4230 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockSiemJobs } from './__mocks__/api'; +import { mockSecurityJobs } from './api.mock'; import { filterJobs, getStablePatternTitles, searchFilter } from './helpers'; describe('helpers', () => { describe('filterJobs', () => { test('returns all jobs when no filter is suplied', () => { const filteredJobs = filterJobs({ - jobs: mockSiemJobs, + jobs: mockSecurityJobs, selectedGroups: [], showCustomJobs: false, showElasticJobs: false, @@ -23,17 +23,17 @@ describe('helpers', () => { describe('searchFilter', () => { test('returns all jobs when nullfilterQuery is provided', () => { - const jobsToDisplay = searchFilter(mockSiemJobs); - expect(jobsToDisplay.length).toEqual(mockSiemJobs.length); + const jobsToDisplay = searchFilter(mockSecurityJobs); + expect(jobsToDisplay.length).toEqual(mockSecurityJobs.length); }); test('returns correct DisplayJobs when filterQuery matches job.id', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'rare_process'); + const jobsToDisplay = searchFilter(mockSecurityJobs, 'rare_process'); expect(jobsToDisplay.length).toEqual(2); }); test('returns correct DisplayJobs when filterQuery matches job.description', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'Detect unusually'); + const jobsToDisplay = searchFilter(mockSecurityJobs, 'Detect unusually'); expect(jobsToDisplay.length).toEqual(2); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx index 5989d052f7cd..daf9da855c0f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SiemJob } from './types'; +import { SecurityJob } from './types'; /** * Returns a filtered array of Jobs according to JobsTableFilters selections @@ -22,12 +22,12 @@ export const filterJobs = ({ showElasticJobs, filterQuery, }: { - jobs: SiemJob[]; + jobs: SecurityJob[]; selectedGroups: string[]; showCustomJobs: boolean; showElasticJobs: boolean; filterQuery: string; -}): SiemJob[] => +}): SecurityJob[] => searchFilter( jobs .filter((job) => !showCustomJobs || (showCustomJobs && !job.isElasticJob)) @@ -44,7 +44,7 @@ export const filterJobs = ({ * @param jobs to filter * @param filterQuery user-provided search string to filter for occurrence in job names/description */ -export const searchFilter = (jobs: SiemJob[], filterQuery?: string): SiemJob[] => +export const searchFilter = (jobs: SecurityJob[], filterQuery?: string): SecurityJob[] => jobs.filter((job) => filterQuery == null ? true diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts new file mode 100644 index 000000000000..80f50912a84f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock'; +import { getJobsSummary } from '../../ml/api/get_jobs_summary'; +import { checkRecognizer, getModules } from '../api'; +import { SecurityJob } from '../types'; +import { + mockJobsSummaryResponse, + mockGetModuleResponse, + checkRecognizerSuccess, +} from '../api.mock'; +import { useSecurityJobs } from './use_security_jobs'; + +jest.mock('../../../../../common/machine_learning/has_ml_admin_permissions'); +jest.mock('../../../../../common/machine_learning/has_ml_license'); +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_app_toasts'); +jest.mock('../../ml/hooks/use_ml_capabilities'); +jest.mock('../../ml/api/get_jobs_summary'); +jest.mock('../api'); + +describe('useSecurityJobs', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + + describe('when user has valid permissions', () => { + beforeEach(() => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); + (hasMlLicense as jest.Mock).mockReturnValue(true); + (getJobsSummary as jest.Mock).mockResolvedValue(mockJobsSummaryResponse); + (getModules as jest.Mock).mockResolvedValue(mockGetModuleResponse); + (checkRecognizer as jest.Mock).mockResolvedValue(checkRecognizerSuccess); + }); + + it('combines multiple ML calls into an array of SecurityJobs', async () => { + const expectedSecurityJob: SecurityJob = { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'stopped', + defaultIndexPattern: '', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1557353420495, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isCompatible: true, + isElasticJob: false, + isInstalled: true, + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1557434782207, + memory_status: 'hard_limit', + moduleId: '', + processed_record_count: 582251, + }; + + const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(result.current.jobs).toHaveLength(6); + expect(result.current.jobs).toEqual(expect.arrayContaining([expectedSecurityJob])); + }); + + it('returns those permissions', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(result.current.isMlAdmin).toEqual(true); + expect(result.current.isLicensed).toEqual(true); + }); + + it('renders a toast error if an ML call fails', async () => { + (getModules as jest.Mock).mockRejectedValue('whoops'); + const { waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(appToastsMock.addError).toHaveBeenCalledWith('whoops', { + title: 'Security job fetch failure', + }); + }); + }); + + describe('when the user does not have valid permissions', () => { + beforeEach(() => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); + (hasMlLicense as jest.Mock).mockReturnValue(false); + }); + + it('returns empty jobs and false predicates', () => { + const { result } = renderHook(() => useSecurityJobs(false)); + + expect(result.current.jobs).toEqual([]); + expect(result.current.isMlAdmin).toEqual(false); + expect(result.current.isLicensed).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts new file mode 100644 index 000000000000..e8809e8366ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useUiSetting$, useHttp } from '../../../lib/kibana'; +import { checkRecognizer, getModules } from '../api'; +import { SecurityJob } from '../types'; +import { createSecurityJobs } from './use_security_jobs_helpers'; +import { useMlCapabilities } from '../../ml/hooks/use_ml_capabilities'; +import * as i18n from '../../ml/translations'; +import { getJobsSummary } from '../../ml/api/get_jobs_summary'; + +export interface UseSecurityJobsReturn { + loading: boolean; + jobs: SecurityJob[]; + isMlAdmin: boolean; + isLicensed: boolean; +} + +/** + * Compiles a collection of SecurityJobs, which are a list of all jobs relevant to the Security Solution App. This + * includes all installed jobs in the `Security` ML group, and all jobs within ML Modules defined in + * ml_module (whether installed or not). Use the corresponding helper functions to filter the job + * list as necessary. E.g. installed jobs, running jobs, etc. + * + * NOTE: If the user is not an ml admin, jobs will be empty and isMlAdmin will be false. + * + * @param refetchData + */ +export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => { + const [jobs, setJobs] = useState([]); + const [loading, setLoading] = useState(true); + const mlCapabilities = useMlCapabilities(); + const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const http = useHttp(); + const { addError } = useAppToasts(); + + const isMlAdmin = hasMlAdminPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchSecurityJobIdsFromGroupsData() { + if (isMlAdmin && isLicensed) { + try { + // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex + const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ + getJobsSummary({ http, signal: abortCtrl.signal }), + getModules({ signal: abortCtrl.signal }), + checkRecognizer({ + indexPatternName: siemDefaultIndex, + signal: abortCtrl.signal, + }), + ]); + + const compositeSecurityJobs = createSecurityJobs( + jobSummaryData, + modulesData, + compatibleModules + ); + + if (isSubscribed) { + setJobs(compositeSecurityJobs); + } + } catch (error) { + if (isSubscribed) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchSecurityJobIdsFromGroupsData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [refetchData, isMlAdmin, isLicensed, siemDefaultIndex, addError, http]); + + return { isLicensed, isMlAdmin, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx index fc9f369a305a..7fb4e6f59d9f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx @@ -6,29 +6,29 @@ import { composeModuleAndInstalledJobs, - createSiemJobs, + createSecurityJobs, getAugmentedFields, getInstalledJobs, getModuleJobs, - moduleToSiemJob, -} from './use_siem_jobs_helpers'; + moduleToSecurityJob, +} from './use_security_jobs_helpers'; import { checkRecognizerSuccess, mockGetModuleResponse, mockJobsSummaryResponse, -} from '../__mocks__/api'; +} from '../api.mock'; // TODO: Expand test coverage -describe('useSiemJobsHelpers', () => { - describe('moduleToSiemJob', () => { - test('correctly converts module to SiemJob', () => { - const siemJob = moduleToSiemJob( +describe('useSecurityJobsHelpers', () => { + describe('moduleToSecurityJob', () => { + test('correctly converts module to SecurityJob', () => { + const securityJob = moduleToSecurityJob( mockGetModuleResponse[0], mockGetModuleResponse[0].jobs[0], false ); - expect(siemJob).toEqual({ + expect(securityJob).toEqual({ datafeedId: '', datafeedIndices: [], datafeedState: '', @@ -86,19 +86,19 @@ describe('useSiemJobsHelpers', () => { const installedJobs = getInstalledJobs(mockJobsSummaryResponse, moduleJobs, [ 'siem_auditbeat', ]); - const siemJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs); - expect(siemJobs.length).toEqual(6); + const securityJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs); + expect(securityJobs.length).toEqual(6); }); }); - describe('createSiemJobs', () => { + describe('createSecurityJobs', () => { test('returns correct number of jobs when creating jobs with successful responses', () => { - const siemJobs = createSiemJobs( + const securityJobs = createSecurityJobs( mockJobsSummaryResponse, mockGetModuleResponse, checkRecognizerSuccess ); - expect(siemJobs.length).toEqual(6); + expect(securityJobs.length).toEqual(6); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx index adbd712ffeb3..d0109fd29b5f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx @@ -5,26 +5,26 @@ */ import { - AugmentedSiemJobFields, - JobSummary, + AugmentedSecurityJobFields, Module, ModuleJob, RecognizerModule, - SiemJob, + SecurityJob, } from '../types'; import { mlModules } from '../ml_modules'; +import { MlSummaryJob } from '../../../../../../ml/public'; /** - * Helper function for converting from ModuleJob -> SiemJob + * Helper function for converting from ModuleJob -> SecurityJob * @param module * @param moduleJob * @param isCompatible */ -export const moduleToSiemJob = ( +export const moduleToSecurityJob = ( module: Module, moduleJob: ModuleJob, isCompatible: boolean -): SiemJob => { +): SecurityJob => { return { datafeedId: '', datafeedIndices: [], @@ -46,7 +46,7 @@ export const moduleToSiemJob = ( }; /** - * Returns fields necessary to augment a ModuleJob to a SiemJob + * Returns fields necessary to augment a ModuleJob to a SecurityJob * * @param jobId * @param moduleJobs @@ -54,9 +54,9 @@ export const moduleToSiemJob = ( */ export const getAugmentedFields = ( jobId: string, - moduleJobs: SiemJob[], + moduleJobs: SecurityJob[], compatibleModuleIds: string[] -): AugmentedSiemJobFields => { +): AugmentedSecurityJobFields => { const moduleJob = moduleJobs.find((mj) => mj.id === jobId); return moduleJob !== undefined ? { @@ -74,24 +74,27 @@ export const getAugmentedFields = ( }; /** - * Process Modules[] from the `get_module` ML API into SiemJobs[] by filtering to SIEM specific + * Process Modules[] from the `get_module` ML API into SecurityJobs[] by filtering to Security specific * modules and unpacking jobs from each module * * @param modulesData * @param compatibleModuleIds */ -export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string[]): SiemJob[] => +export const getModuleJobs = ( + modulesData: Module[], + compatibleModuleIds: string[] +): SecurityJob[] => modulesData .filter((module) => mlModules.includes(module.id)) .map((module) => [ ...module.jobs.map((moduleJob) => - moduleToSiemJob(module, moduleJob, compatibleModuleIds.includes(module.id)) + moduleToSecurityJob(module, moduleJob, compatibleModuleIds.includes(module.id)) ), ]) .flat(); /** - * Process JobSummary[] from the `jobs_summary` ML API into SiemJobs[] by filtering to to SIEM jobs + * Process data from the `jobs_summary` ML API into SecurityJobs[] by filtering to Security jobs * and augmenting with moduleId/defaultIndexPattern/isCompatible * * @param jobSummaryData @@ -99,57 +102,57 @@ export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string * @param compatibleModuleIds */ export const getInstalledJobs = ( - jobSummaryData: JobSummary[], - moduleJobs: SiemJob[], + jobSummaryData: MlSummaryJob[], + moduleJobs: SecurityJob[], compatibleModuleIds: string[] -): SiemJob[] => +): SecurityJob[] => jobSummaryData .filter(({ groups }) => groups.includes('siem') || groups.includes('security')) - .map((jobSummary) => ({ + .map((jobSummary) => ({ ...jobSummary, ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds), isInstalled: true, })); /** - * Combines installed jobs + moduleSiemJobs that don't overlap and sorts by name asc + * Combines installed jobs + moduleSecurityJobs that don't overlap and sorts by name asc * * @param installedJobs - * @param moduleSiemJobs + * @param moduleSecurityJobs */ export const composeModuleAndInstalledJobs = ( - installedJobs: SiemJob[], - moduleSiemJobs: SiemJob[] -): SiemJob[] => { + installedJobs: SecurityJob[], + moduleSecurityJobs: SecurityJob[] +): SecurityJob[] => { const installedJobsIds = installedJobs.map((installedJob) => installedJob.id); return [ ...installedJobs, - ...moduleSiemJobs.filter((mj) => !installedJobsIds.includes(mj.id)), + ...moduleSecurityJobs.filter((mj) => !installedJobsIds.includes(mj.id)), ].sort((a, b) => a.id.localeCompare(b.id)); }; /** - * Creates a list of SiemJobs by composing JobSummary jobs (installed jobs) and Module - * jobs (pre-packaged SIEM jobs) into a single job object that can be used throughout the SIEM app + * Creates a list of SecurityJobs by composing jobs summaries (installed jobs) and Module + * jobs (pre-packaged Security jobs) into a single job object that can be used throughout the Security app * * @param jobSummaryData * @param modulesData * @param compatibleModules */ -export const createSiemJobs = ( - jobSummaryData: JobSummary[], +export const createSecurityJobs = ( + jobSummaryData: MlSummaryJob[], modulesData: Module[], compatibleModules: RecognizerModule[] -): SiemJob[] => { +): SecurityJob[] => { // Create lookup of compatible modules const compatibleModuleIds = compatibleModules.map((module) => module.id); - // Process modulesData: Filter to SIEM specific modules, and unpack jobs from modules - const moduleSiemJobs = getModuleJobs(modulesData, compatibleModuleIds); + // Process modulesData: Filter to Security specific modules, and unpack jobs from modules + const moduleSecurityJobs = getModuleJobs(modulesData, compatibleModuleIds); - // Process jobSummaryData: Filter to SIEM jobs, and augment with moduleId/defaultIndexPattern/isCompatible - const installedJobs = getInstalledJobs(jobSummaryData, moduleSiemJobs, compatibleModuleIds); + // Process jobSummaryData: Filter to Security jobs, and augment with moduleId/defaultIndexPattern/isCompatible + const installedJobs = getInstalledJobs(jobSummaryData, moduleSecurityJobs, compatibleModuleIds); - // Combine installed jobs + moduleSiemJobs that don't overlap, and sort by name asc - return composeModuleAndInstalledJobs(installedJobs, moduleSiemJobs); + // Combine installed jobs + moduleSecurityJobs that don't overlap, and sort by name asc + return composeModuleAndInstalledJobs(installedJobs, moduleSecurityJobs); }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx deleted file mode 100644 index 7f0a8dea1913..000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; -import { checkRecognizer, getJobsSummary, getModules } from '../api'; -import { SiemJob } from '../types'; -import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; -import { errorToToaster, useStateToaster } from '../../toasters'; -import { useUiSetting$ } from '../../../lib/kibana'; - -import * as i18n from './translations'; -import { createSiemJobs } from './use_siem_jobs_helpers'; -import { useMlCapabilities } from './use_ml_capabilities'; - -type Return = [boolean, SiemJob[]]; - -/** - * Compiles a collection of SiemJobs, which are a list of all jobs relevant to the SIEM App. This - * includes all installed jobs in the `SIEM` ML group, and all jobs within ML Modules defined in - * ml_module (whether installed or not). Use the corresponding helper functions to filter the job - * list as necessary. E.g. installed jobs, running jobs, etc. - * - * @param refetchData - */ -export const useSiemJobs = (refetchData: boolean): Return => { - const [siemJobs, setSiemJobs] = useState([]); - const [loading, setLoading] = useState(true); - const mlCapabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(mlCapabilities); - const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const [, dispatchToaster] = useStateToaster(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - setLoading(true); - - async function fetchSiemJobIdsFromGroupsData() { - if (userPermissions) { - try { - // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex - const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ - getJobsSummary(abortCtrl.signal), - getModules({ signal: abortCtrl.signal }), - checkRecognizer({ - indexPatternName: siemDefaultIndex, - signal: abortCtrl.signal, - }), - ]); - - const compositeSiemJobs = createSiemJobs(jobSummaryData, modulesData, compatibleModules); - - if (isSubscribed) { - setSiemJobs(compositeSiemJobs); - } - } catch (error) { - if (isSubscribed) { - errorToToaster({ title: i18n.SIEM_JOB_FETCH_FAILURE, error, dispatchToaster }); - } - } - } - if (isSubscribed) { - setLoading(false); - } - } - - fetchSiemJobIdsFromGroupsData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refetchData, userPermissions]); - - return [loading, siemJobs]; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index 747ac63551b5..9bee321e9fbd 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -25,7 +25,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -29,7 +32,7 @@ describe('GroupsFilterPopover', () => { const mockOnSelectedGroupsChanged = jest.fn(); const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index d879942b8b10..362fb94dc1ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -15,30 +15,30 @@ import { EuiSpacer, } from '@elastic/eui'; import * as i18n from './translations'; -import { SiemJob } from '../../types'; +import { SecurityJob } from '../../types'; import { toggleSelectedGroup } from './toggle_selected_group'; interface GroupsFilterPopoverProps { - siemJobs: SiemJob[]; + securityJobs: SecurityJob[]; onSelectedGroupsChanged: Dispatch>; } /** - * Popover for selecting which SiemJob groups to filter on. Component extracts unique groups and - * their counts from the provided SiemJobs. The 'siem' & 'security' groups are filtered out as all jobs will be + * Popover for selecting which SecurityJob groups to filter on. Component extracts unique groups and + * their counts from the provided SecurityJobs. The 'siem' & 'security' groups are filtered out as all jobs will be * siem/security jobs * - * @param siemJobs jobs to fetch groups from to display for filtering + * @param securityJobs jobs to fetch groups from to display for filtering * @param onSelectedGroupsChanged change listener to be notified when group selection changes */ export const GroupsFilterPopoverComponent = ({ - siemJobs, + securityJobs, onSelectedGroupsChanged, }: GroupsFilterPopoverProps) => { const [isGroupPopoverOpen, setIsGroupPopoverOpen] = useState(false); const [selectedGroups, setSelectedGroups] = useState([]); - const groups = siemJobs + const groups = securityJobs .map((j) => j.groups) .flat() .filter((g) => g !== 'siem' && g !== 'security'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx index 5b656adc3e58..6b7699d57aed 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx @@ -7,20 +7,20 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { JobsTableFiltersComponent } from './jobs_table_filters'; -import { SiemJob } from '../../types'; +import { SecurityJob } from '../../types'; import { cloneDeep } from 'lodash/fp'; -import { mockSiemJobs } from '../../__mocks__/api'; +import { mockSecurityJobs } from '../../api.mock'; describe('JobsTableFilters', () => { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -28,7 +28,7 @@ describe('JobsTableFilters', () => { test('when you click Elastic Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-elastic-jobs-filter-button"]').first().simulate('click'); @@ -45,7 +45,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); @@ -62,7 +62,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter once, then Elastic Jobs filter, state is updated and selected changed', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); @@ -88,7 +88,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter twice, state is updated and it is revert', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 4cfb7f8ad2b5..f25ea667b341 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -15,11 +15,11 @@ import { } from '@elastic/eui'; import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; import * as i18n from './translations'; -import { JobsFilters, SiemJob } from '../../types'; +import { JobsFilters, SecurityJob } from '../../types'; import { GroupsFilterPopover } from './groups_filter_popover'; interface JobsTableFiltersProps { - siemJobs: SiemJob[]; + securityJobs: SecurityJob[]; onFilterChanged: Dispatch>; } @@ -27,10 +27,13 @@ interface JobsTableFiltersProps { * Collection of filters for filtering data within the JobsTable. Contains search bar, Elastic/Custom * Jobs filter button toggle, and groups selection * - * @param siemJobs jobs to fetch groups from to display for filtering + * @param securityJobs jobs to fetch groups from to display for filtering * @param onFilterChanged change listener to be notified on filter changes */ -export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTableFiltersProps) => { +export const JobsTableFiltersComponent = ({ + securityJobs, + onFilterChanged, +}: JobsTableFiltersProps) => { const [filterQuery, setFilterQuery] = useState(''); const [selectedGroups, setSelectedGroups] = useState([]); const [showCustomJobs, setShowCustomJobs] = useState(false); @@ -71,7 +74,10 @@ export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTab - + diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx index ade8c6fe8052..e58d76bd1dde 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx @@ -9,22 +9,22 @@ import React from 'react'; import { JobSwitchComponent } from './job_switch'; import { cloneDeep } from 'lodash/fp'; -import { mockSiemJobs } from '../__mocks__/api'; -import { SiemJob } from '../types'; +import { mockSecurityJobs } from '../api.mock'; +import { SecurityJob } from '../types'; describe('JobSwitch', () => { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); onJobStateChangeMock = jest.fn(); }); test('renders correctly against snapshot', () => { const wrapper = shallow( ); @@ -34,8 +34,8 @@ describe('JobSwitch', () => { test('should call onJobStateChange when the switch is clicked to be true/open', () => { const wrapper = mount( ); @@ -57,8 +57,8 @@ describe('JobSwitch', () => { test('should have a switch when it is not in the loading state', () => { const wrapper = mount( ); @@ -68,8 +68,8 @@ describe('JobSwitch', () => { test('should not have a switch when it is in the loading state', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx index d370d475bd6e..3ad71ee6b691 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx @@ -12,7 +12,7 @@ import { isJobFailed, isJobStarted, } from '../../../../../common/machine_learning/helpers'; -import { SiemJob } from '../types'; +import { SecurityJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -24,14 +24,14 @@ const StaticSwitch = styled(EuiSwitch)` StaticSwitch.displayName = 'StaticSwitch'; export interface JobSwitchProps { - job: SiemJob; - isSiemJobsLoading: boolean; - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise; + job: SecurityJob; + isSecurityJobsLoading: boolean; + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise; } export const JobSwitchComponent = ({ job, - isSiemJobsLoading, + isSecurityJobsLoading, onJobStateChange, }: JobSwitchProps) => { const [isLoading, setIsLoading] = useState(false); @@ -47,7 +47,7 @@ export const JobSwitchComponent = ({ return ( - {isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? ( + {isSecurityJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? ( ) : ( { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); onJobStateChangeMock = jest.fn(); }); @@ -25,7 +25,7 @@ describe('JobsTableComponent', () => { const wrapper = shallow( ); @@ -36,7 +36,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); @@ -46,11 +46,11 @@ describe('JobsTableComponent', () => { }); test('should render the hyperlink with URI encodings which points specifically to the job id', () => { - siemJobs[0].id = 'job id with spaces'; + securityJobs[0].id = 'job id with spaces'; const wrapper = mount( ); @@ -63,7 +63,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); @@ -73,14 +73,14 @@ describe('JobsTableComponent', () => { .simulate('click', { target: { checked: true }, }); - expect(onJobStateChangeMock.mock.calls[0]).toEqual([siemJobs[0], 1571022859393, true]); + expect(onJobStateChangeMock.mock.calls[0]).toEqual([securityJobs[0], 1571022859393, true]); }); test('should have a switch when it is not in the loading state', () => { const wrapper = mount( ); @@ -91,7 +91,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx index f28a99c9947d..be911a1cd853 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx @@ -25,7 +25,7 @@ import styled from 'styled-components'; import { useBasePath } from '../../../lib/kibana'; import * as i18n from './translations'; import { JobSwitch } from './job_switch'; -import { SiemJob } from '../types'; +import { SecurityJob } from '../types'; const JobNameWrapper = styled.div` margin: 5px 0; @@ -38,12 +38,12 @@ const truncateThreshold = 200; const getJobsTableColumns = ( isLoading: boolean, - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise, + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise, basePath: string ) => [ { name: i18n.COLUMN_JOB_NAME, - render: ({ id, description }: SiemJob) => ( + render: ({ id, description }: SecurityJob) => ( ( + render: ({ groups }: SecurityJob) => ( {groups.map((group) => ( @@ -76,9 +76,13 @@ const getJobsTableColumns = ( { name: i18n.COLUMN_RUN_JOB, - render: (job: SiemJob) => + render: (job: SecurityJob) => job.isCompatible ? ( - + ) : ( ), @@ -87,13 +91,16 @@ const getJobsTableColumns = ( } as const, ]; -const getPaginatedItems = (items: SiemJob[], pageIndex: number, pageSize: number): SiemJob[] => - items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); +const getPaginatedItems = ( + items: SecurityJob[], + pageIndex: number, + pageSize: number +): SecurityJob[] => items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); export interface JobTableProps { isLoading: boolean; - jobs: SiemJob[]; - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise; + jobs: SecurityJob[]; + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise; } export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTableProps) => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx index 0ebf36747184..f2bf2273c4b3 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx @@ -12,19 +12,17 @@ import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; -import { hasMlAdminPermissions } from '../../../../common/machine_learning/has_ml_admin_permissions'; import { errorToToaster, useStateToaster, ActionToaster } from '../toasters'; import { setupMlJob, startDatafeeds, stopDatafeeds } from './api'; import { filterJobs } from './helpers'; -import { useSiemJobs } from './hooks/use_siem_jobs'; import { JobsTableFilters } from './jobs_table/filters/jobs_table_filters'; import { JobsTable } from './jobs_table/jobs_table'; import { ShowingCount } from './jobs_table/showing_count'; import { PopoverDescription } from './popover_description'; import * as i18n from './translations'; -import { JobsFilters, SiemJob } from './types'; +import { JobsFilters, SecurityJob } from './types'; import { UpgradeContents } from './upgrade_contents'; -import { useMlCapabilities } from './hooks/use_ml_capabilities'; +import { useSecurityJobs } from './hooks/use_security_jobs'; const PopoverContentsDiv = styled.div` max-width: 684px; @@ -87,24 +85,25 @@ export const MlPopover = React.memo(() => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [filterProperties, setFilterProperties] = useState(defaultFilterProps); - const [isLoadingSiemJobs, siemJobs] = useSiemJobs(refreshToggle); + const { isMlAdmin, isLicensed, loading: isLoadingSecurityJobs, jobs } = useSecurityJobs( + refreshToggle + ); const [, dispatchToaster] = useStateToaster(); - const capabilities = useMlCapabilities(); const docLinks = useKibana().services.docLinks; const handleJobStateChange = useCallback( - (job: SiemJob, latestTimestampMs: number, enable: boolean) => + (job: SecurityJob, latestTimestampMs: number, enable: boolean) => enableDatafeed(job, latestTimestampMs, enable, dispatch, dispatchToaster), [dispatch, dispatchToaster] ); const filteredJobs = filterJobs({ - jobs: siemJobs, + jobs, ...filterProperties, }); - const incompatibleJobCount = siemJobs.filter((j) => !j.isCompatible).length; + const incompatibleJobCount = jobs.filter((j) => !j.isCompatible).length; - if (!capabilities.isPlatinumOrTrialLicense) { + if (!isLicensed) { // If the user does not have platinum show upgrade UI return ( { ); - } else if (hasMlAdminPermissions(capabilities)) { + } else if (isMlAdmin) { // If the user has Platinum License & ML Admin Permissions, show Anomaly Detection button & full config UI return ( { - + @@ -194,7 +193,7 @@ export const MlPopover = React.memo(() => { )} @@ -209,7 +208,7 @@ export const MlPopover = React.memo(() => { // Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch const enableDatafeed = async ( - job: SiemJob, + job: SecurityJob, latestTimestampMs: number, enable: boolean, dispatch: Dispatch, @@ -257,7 +256,7 @@ const enableDatafeed = async ( dispatch({ type: 'refresh' }); }; -const submitTelemetry = (job: SiemJob, enabled: boolean) => { +const submitTelemetry = (job: SecurityJob, enabled: boolean) => { // Report type of job enabled/disabled track( METRIC_TYPE.COUNT, diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts index f39daa0b9a7f..c839f5110fe7 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessageBase } from '../../../../../ml/public'; import { MlError } from '../ml/types'; +import { MlSummaryJob } from '../../../../../ml/public'; export interface Group { id: string; @@ -98,28 +98,6 @@ export interface MlSetupArgs { prefix?: string; } -/** - * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API - */ -export interface JobSummary { - auditMessage?: AuditMessageBase; - datafeedId: string; - datafeedIndices: string[]; - datafeedState: string; - description: string; - earliestTimestampMs?: number; - latestResultsTimestampMs?: number; - groups: string[]; - hasDatafeed: boolean; - id: string; - isSingleMetricViewerJob: boolean; - jobState: string; - latestTimestampMs?: number; - memory_status: string; - nodeName?: string; - processed_record_count: number; -} - export interface Detector { detector_description: string; function: string; @@ -133,10 +111,10 @@ export interface CustomURL { } /** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary + * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and MlSummaryJob * that includes necessary metadata like moduleName, defaultIndexPattern, etc. */ -export interface SiemJob extends JobSummary { +export interface SecurityJob extends MlSummaryJob { moduleId: string; defaultIndexPattern: string; isCompatible: boolean; @@ -144,7 +122,7 @@ export interface SiemJob extends JobSummary { isElasticJob: boolean; } -export interface AugmentedSiemJobFields { +export interface AugmentedSecurityJobFields { moduleId: string; defaultIndexPattern: string; isCompatible: boolean; diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index 76270a7c08cd..94019b26c180 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; -import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useInstalledSecurityJobs } from '../../../components/ml/hooks/use_installed_security_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { histogramConfigs } from './histogram_configs'; @@ -38,13 +38,13 @@ export const AnomaliesQueryTabBody = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [, siemJobs] = useSiemJobs(true); + const { jobs } = useInstalledSecurityJobs(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); const mergedFilterQuery = getAnomaliesFilterQuery( filterQuery, anomaliesFilterQuery, - siemJobs, + jobs, anomalyScore, flowTarget, ip diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts index 10d5d1c60a6c..5248801d723b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -6,21 +6,20 @@ import deepmerge from 'deepmerge'; +import { MlSummaryJob } from '../../../../../../ml/public'; import { ESTermQuery } from '../../../../../common/typed_json'; import { createFilter } from '../../helpers'; -import { SiemJob } from '../../../components/ml_popover/types'; import { FlowTarget } from '../../../../graphql/types'; export const getAnomaliesFilterQuery = ( filterQuery: string | ESTermQuery | undefined, anomaliesFilterQuery: object = {}, - siemJobs: SiemJob[] = [], + securityJobs: MlSummaryJob[] = [], anomalyScore: number, flowTarget?: FlowTarget, ip?: string ): string => { - const siemJobIds = siemJobs - .filter((job) => job.isInstalled) + const securityJobIds = securityJobs .map((job) => job.id) .map((jobId) => ({ match_phrase: { @@ -38,7 +37,7 @@ export const getAnomaliesFilterQuery = ( filter: [ { bool: { - should: siemJobIds, + should: securityJobIds, minimum_should_match: 1, }, }, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts new file mode 100644 index 000000000000..1af4ba3ba923 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const createAppToastsMock = () => ({ + addError: jest.fn(), + addSuccess: jest.fn(), +}); + +export const useAppToastsMock = { + create: createAppToastsMock, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 2c52acd3ec74..5f4285f2747a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -17,6 +17,7 @@ export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => export const useKibana = jest.fn(createUseKibanaMock()); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); +export const useHttp = jest.fn(() => useKibana().services.http); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts index 13b3c4b249bf..f8eed75cf9bf 100644 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts +++ b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts @@ -6,8 +6,10 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { securityMock } from '../../../../../plugins/security/public/mocks'; export const createKibanaCoreStartMock = () => coreMock.createStart(); export const createKibanaPluginsStartMock = () => ({ data: dataPluginMock.createStartContract(), + security: securityMock.createSetup(), }); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts index c5d50e137948..bdb8ca85b0d7 100644 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts +++ b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts @@ -96,28 +96,10 @@ export const createUseKibanaMock = () => { export const createStartServices = () => { const core = createKibanaCoreStartMock(); const plugins = createKibanaPluginsStartMock(); - const security = { - authc: { - getCurrentUser: jest.fn(), - areAPIKeysEnabled: jest.fn(), - }, - sessionTimeout: { - start: jest.fn(), - stop: jest.fn(), - extend: jest.fn(), - }, - license: { - isEnabled: jest.fn(), - getFeatures: jest.fn(), - features$: jest.fn(), - }, - __legacyCompat: { logoutUrl: 'logoutUrl', tenant: 'tenant' }, - }; const services = ({ ...core, ...plugins, - security, } as unknown) as StartServices; return services; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 47c12d193417..00141c9a453d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -38,7 +38,7 @@ import { buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { buildMlJobDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; @@ -67,7 +67,7 @@ export const StepRuleDescriptionComponent: React.FC = }) => { const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const [, siemJobs] = useSiemJobs(true); + const { jobs } = useSecurityJobs(false); const keys = Object.keys(schema); const listItems = keys.reduce((acc: ListItems[], key: string) => { @@ -77,7 +77,7 @@ export const StepRuleDescriptionComponent: React.FC = buildMlJobDescription( get(key, data) as string, (get(key, schema) as { label: string }).label, - siemJobs + jobs ), ]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx index c82a465f08c3..3152fef12c65 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx @@ -7,31 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { mockOpenedJob } from '../../../../common/components/ml_popover/api.mock'; import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description'; -jest.mock('../../../../common/lib/kibana'); -const job = { - moduleId: 'moduleId', - defaultIndexPattern: 'defaultIndexPattern', - isCompatible: true, - isInstalled: true, - isElasticJob: true, - datafeedId: 'datafeedId', - datafeedIndices: [], - datafeedState: 'datafeedState', - description: 'description', - groups: [], - hasDatafeed: true, - id: 'id', - isSingleMetricViewerJob: false, - jobState: 'jobState', - memory_status: 'memory_status', - processed_record_count: 0, -}; +jest.mock('../../../../common/lib/kibana'); describe('MlJobDescription', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1); }); @@ -47,7 +30,7 @@ describe('AuditIcon', () => { describe('JobStatusBadge', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('EuiBadge')).toHaveLength(1); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx index d7e06511e793..6baa2abab33d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx @@ -8,9 +8,9 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { MlSummaryJob } from '../../../../../../ml/public'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { useKibana } from '../../../../common/lib/kibana'; -import { SiemJob } from '../../../../common/components/ml_popover/types'; import { ListItems } from './types'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; @@ -21,7 +21,7 @@ enum MessageLevels { } const AuditIconComponent: React.FC<{ - message: SiemJob['auditMessage']; + message: MlSummaryJob['auditMessage']; }> = ({ message }) => { if (!message) { return null; @@ -47,7 +47,7 @@ const AuditIconComponent: React.FC<{ export const AuditIcon = React.memo(AuditIconComponent); -const JobStatusBadgeComponent: React.FC<{ job: SiemJob }> = ({ job }) => { +const JobStatusBadgeComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { const isStarted = isJobStarted(job.jobState, job.datafeedState); const color = isStarted ? 'secondary' : 'danger'; const text = isStarted ? ML_JOB_STARTED : ML_JOB_STOPPED; @@ -69,7 +69,7 @@ const Wrapper = styled.div` overflow: hidden; `; -const MlJobDescriptionComponent: React.FC<{ job: SiemJob }> = ({ job }) => { +const MlJobDescriptionComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { const jobUrl = useKibana().services.application.getUrlForApp( `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})` ); @@ -92,12 +92,12 @@ export const MlJobDescription = React.memo(MlJobDescriptionComponent); export const buildMlJobDescription = ( jobId: string, label: string, - siemJobs: SiemJob[] + jobs: MlSummaryJob[] ): ListItems => { - const siemJob = siemJobs.find((job) => job.id === jobId); + const job = jobs.find(({ id }) => id === jobId); return { title: label, - description: siemJob ? : jobId, + description: job ? : jobId, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx index 6f6581e4de1c..4a08adbedab3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MlJobSelect } from './index'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useFormFieldMock } from '../../../../common/mock'; -jest.mock('../../../../common/components/ml_popover/hooks/use_siem_jobs'); +jest.mock('../../../../common/components/ml_popover/hooks/use_security_jobs'); jest.mock('../../../../common/lib/kibana'); describe('MlJobSelect', () => { beforeAll(() => { - (useSiemJobs as jest.Mock).mockReturnValue([false, []]); + (useSecurityJobs as jest.Mock).mockReturnValue({ loading: false, jobs: [] }); }); it('renders correctly', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx index cdfdf4ca6b66..b0aa0329fe8f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx @@ -19,7 +19,7 @@ import { import styled from 'styled-components'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useKibana } from '../../../../common/lib/kibana'; import { ML_JOB_SELECT_PLACEHOLDER_TEXT, @@ -81,7 +81,7 @@ interface MlJobSelectProps { export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { const jobId = field.value as string; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [isLoading, siemJobs] = useSiemJobs(false); + const { loading, jobs } = useSecurityJobs(false); const mlUrl = useKibana().services.application.getUrlForApp('ml'); const handleJobChange = useCallback( (machineLearningJobId: string) => { @@ -96,7 +96,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f disabled: true, }; - const jobOptions = siemJobs.map((job) => ({ + const jobOptions = jobs.map((job) => ({ value: job.id, inputDisplay: job.id, dropdownDisplay: , @@ -107,9 +107,9 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f const isJobRunning = useMemo(() => { // If the selected job is not found in the list, it means the placeholder is selected // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' - const job = siemJobs.find((j) => j.id === jobId); + const job = jobs.find(({ id }) => id === jobId); return job == null || isJobStarted(job.jobState, job.datafeedState); - }, [siemJobs, jobId]); + }, [jobs, jobId]); return ( @@ -126,7 +126,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f = ({ componentProps={{ describedByIds: ['detectionEngineStepDefineRuleType'], isReadOnly: isUpdateView, - hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, + hasValidLicense: hasMlLicense(mlCapabilities), isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 85dce907084e..110691328b13 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -47,8 +47,9 @@ import { getColumns, getMonitoringColumns } from './columns'; import { showRulesTable } from './helpers'; import { allRulesReducer, State } from './reducer'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; -import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -145,8 +146,7 @@ export const AllRules = React.memo( const { formatUrl } = useFormatUrl(SecurityPageName.detections); // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const setRules = useCallback((newRules: Rule[], newPagination: Partial) => { dispatch({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 016d0c7c67a9..8a969a4cf098 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -71,8 +71,9 @@ import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_o import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; import { RuleStatus } from '../../../../components/rules//rule_status'; -import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -161,8 +162,7 @@ export const RuleDetailsPageComponent: FC = ({ const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const ruleDetailTabs = getRuleDetailsTabs(rule); // persist rule until refresh is complete diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 34840b282662..67f563e944f4 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -17,7 +17,7 @@ import { LastEventTime } from '../../../common/components/last_event_time'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { SiemNavigation } from '../../../common/components/navigation'; import { KpiHostsComponent } from '../../components/kpi_hosts'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index e4e69443c510..2b19249dc426 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -34,7 +34,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; -import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { OverviewEmpty } from '../../overview/components/overview_empty'; import { Display } from './display'; import { HostsTabs } from './hosts_tabs'; diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx index cf08b084d219..d6dfe1769308 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx @@ -30,7 +30,7 @@ import { DescriptionListStyled, OverviewWrapper } from '../../../common/componen import { Loader } from '../../../common/components/loader'; import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index 9ac05cc98bb4..07abe7bc8c20 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; -import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; import { FlowTarget } from '../../graphql/types'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 0a15b039b96a..c7aba6fcc8a5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -23,7 +23,7 @@ import { HostItem } from '../../../graphql/types'; import { Loader } from '../../../common/components/loader'; import { IPDetailsLink } from '../../../common/components/links'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page';