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 2102673060273..d0bce8508e82e 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 4b1d7ee733dcf..ec884bfac5351 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 0000000000000..1ffc2e16b78f7 --- /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 0000000000000..c0b6862ac30fe --- /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 43cfa4ad59964..f5783fc9b3973 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 6fbb308672e5d..e6597de892bff 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 0000000000000..15f823814d7fc --- /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 32f6f888ab8d7..8ee765c1dea47 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 0000000000000..a80bfb59649cb --- /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 0000000000000..aabd8c7b62e85 --- /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 0000000000000..72690a1773926 --- /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 0000000000000..a9a728f81cc6c --- /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 d897b2554b4fd..4f804a355e4b5 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 c83271a56be5a..c12c8d78da714 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 9bfae686b1a59..7fdf41e6b6500 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 af27d411b990d..124d8d9a794c1 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 54bb0a96207e1..0e8f033ff0cf3 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 7c72098209a06..dd0fb33fd2bc6 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 0b8da6be57e1b..2a2db46d42307 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 5989d052f7cd2..daf9da855c0f9 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 0000000000000..80f50912a84f2 --- /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 0000000000000..e8809e8366eed --- /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 fc9f369a305aa..7fb4e6f59d9f7 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 adbd712ffeb3e..d0109fd29b5fb 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 7f0a8dea1913e..0000000000000 --- 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 747ac63551b55..9bee321e9fbde 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 d879942b8b101..362fb94dc1ec4 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 5b656adc3e581..6b7699d57aedf 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 4cfb7f8ad2b5b..f25ea667b3411 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 ade8c6fe80525..e58d76bd1dde0 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 d370d475bd6e5..3ad71ee6b6919 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 f28a99c9947d5..be911a1cd8537 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 0ebf367471848..f2bf2273c4b3f 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 f39daa0b9a7fb..c839f5110fe7f 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 76270a7c08cd6..94019b26c180b 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 10d5d1c60a6c2..5248801d723b6 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 0000000000000..1af4ba3ba9233 --- /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 2c52acd3ec747..5f4285f2747ae 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 13b3c4b249bfe..f8eed75cf9bf1 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 c5d50e1379482..bdb8ca85b0d77 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 47c12d1934174..00141c9a453d8 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 c82a465f08c3a..3152fef12c652 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 d7e06511e7937..6baa2abab33d1 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 6f6581e4de1c3..4a08adbedab3f 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 cdfdf4ca6b66b..b0aa0329fe8f4 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 85dce907084e8..110691328b13b 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 016d0c7c67a9e..8a969a4cf098c 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 34840b2826626..67f563e944f42 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 e4e69443c510d..2b19249dc426f 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 cf08b084d2197..d6dfe1769308e 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 9ac05cc98bb45..07abe7bc8c209 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 0a15b039b96af..c7aba6fcc8a5b 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';