From 8991c4fc0e0e1cf22b2cec1832413d72ccdce5f6 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 12 Aug 2020 20:45:49 -0600 Subject: [PATCH] [Security Solution][Detections] Refactor ML calls for newest ML permissions (#74582) (#74919) ## Summary Addresses https://github.com/elastic/kibana/issues/73567. ML Users (role: `machine_learning_user`) were previously able to invoke the ML Recognizer API, which we use to get not-yet-installed ML Jobs relevant to our index patterns. As of https://github.com/elastic/kibana/pull/64662 this is not true, and so we receive errors from components using the underlying hook, `useSiemJobs`. To solve this I've created two separate hooks to replace `useSiemJobs`: * `useSecurityJobs` * used on ML Popover * includes uninstalled ML Jobs * checks (and returns) `isMlAdmin` before fetching data * `useInstalledSecurityJobs` * used on ML Jobs Dropdown and Anomalies Table * includes only installed ML Jobs * checks (and returns) `isMlUser` before fetching data Note that we while we now receive the knowledge to do so, we do not always inform the user in the case of invalid permissions, and instead have the following behaviors: #### User has insufficient license * ML Popover: shows an upgrade CTA * Anomalies Tables: show no data * Rule Creation: ML Rule option is disabled, shows upgrade CTA * Rule Details: ML Job Id is displayed as text #### User is ML User * ML Popover: not shown * Anomalies Tables: show no data * Rule Creation: ML Rule option is disabled * Rule Details: ML Job Id is displayed as text #### User is ML Admin * ML Popover: shown * Anomalies Tables: show data __for installed ML Jobs__ * This is the same as previous logic, but worth calling out that you can't view historical anomalies * Rule Creation: ML Rule option is enabled, all ML Jobs available * Rule Details: ML Job Id is displayed as hyperlink, job status badge shown ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) Co-authored-by: Ryland Herrick --- .../anomaly_detection_jobs/summary_job.ts | 4 +- x-pack/plugins/ml/public/shared.ts | 1 + .../machine_learning/has_ml_license.test.ts | 20 ++++ .../common/machine_learning/has_ml_license.ts | 10 ++ .../machine_learning/is_security_job.ts | 3 +- .../ml/anomaly/use_anomalies_table_data.ts | 28 ++--- .../components/ml/api/get_jobs_summary.ts | 35 ++++++ .../components/ml/api/get_ml_capabilities.ts | 13 ++- .../ml/hooks/use_get_jobs_summary.ts | 12 ++ .../ml/hooks/use_get_ml_capabilities.ts | 12 ++ .../hooks/use_installed_security_jobs.test.ts | 99 ++++++++++++++++ .../ml/hooks/use_installed_security_jobs.ts | 63 ++++++++++ .../hooks/use_ml_capabilities.ts} | 2 +- .../permissions/ml_capabilities_provider.tsx | 47 +++----- .../ml/tables/anomalies_host_table.tsx | 2 +- .../ml/tables/anomalies_network_table.tsx | 2 +- .../{ml_popover/hooks => ml}/translations.ts | 0 .../{__mocks__/api.tsx => api.mock.ts} | 12 +- .../components/ml_popover/{api.tsx => api.ts} | 19 --- .../components/ml_popover/helpers.test.tsx | 12 +- .../common/components/ml_popover/helpers.tsx | 8 +- .../hooks/use_security_jobs.test.ts | 110 ++++++++++++++++++ .../ml_popover/hooks/use_security_jobs.ts | 95 +++++++++++++++ ...tsx => use_security_jobs_helpers.test.tsx} | 28 ++--- ...pers.tsx => use_security_jobs_helpers.tsx} | 71 +++++------ .../ml_popover/hooks/use_siem_jobs.tsx | 81 ------------- .../jobs_table_filters.test.tsx.snap | 2 +- .../filters/groups_filter_popover.test.tsx | 15 ++- .../filters/groups_filter_popover.tsx | 14 +-- .../filters/jobs_table_filters.test.tsx | 18 +-- .../jobs_table/filters/jobs_table_filters.tsx | 16 ++- .../ml_popover/jobs_table/job_switch.test.tsx | 24 ++-- .../ml_popover/jobs_table/job_switch.tsx | 12 +- .../ml_popover/jobs_table/jobs_table.test.tsx | 24 ++-- .../ml_popover/jobs_table/jobs_table.tsx | 27 +++-- .../components/ml_popover/ml_popover.tsx | 29 +++-- .../common/components/ml_popover/types.ts | 30 +---- .../anomalies_query_tab_body/index.tsx | 6 +- .../anomalies_query_tab_body/utils.ts | 9 +- .../common/hooks/use_app_toasts.mock.ts | 14 +++ .../common/lib/kibana/__mocks__/index.ts | 1 + .../public/common/mock/kibana_core.ts | 2 + .../public/common/mock/kibana_react.ts | 18 --- .../rules/description_step/index.tsx | 6 +- .../ml_job_description.test.tsx | 25 +--- .../description_step/ml_job_description.tsx | 14 +-- .../rules/ml_job_select/index.test.tsx | 6 +- .../components/rules/ml_job_select/index.tsx | 12 +- .../rules/step_define_rule/index.tsx | 5 +- .../detection_engine/rules/all/index.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 6 +- .../public/hosts/pages/details/index.tsx | 2 +- .../public/hosts/pages/hosts.tsx | 2 +- .../network/components/ip_overview/index.tsx | 2 +- .../public/network/pages/index.tsx | 2 +- .../components/host_overview/index.tsx | 2 +- 56 files changed, 733 insertions(+), 407 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts create mode 100644 x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts rename x-pack/plugins/security_solution/public/common/components/{ml_popover/hooks/use_ml_capabilities.tsx => ml/hooks/use_ml_capabilities.ts} (80%) rename x-pack/plugins/security_solution/public/common/components/{ml_popover/hooks => ml}/translations.ts (100%) rename x-pack/plugins/security_solution/public/common/components/ml_popover/{__mocks__/api.tsx => api.mock.ts} (99%) rename x-pack/plugins/security_solution/public/common/components/ml_popover/{api.tsx => api.ts} (89%) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts rename x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/{use_siem_jobs_helpers.test.tsx => use_security_jobs_helpers.test.tsx} (83%) rename x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/{use_siem_jobs_helpers.tsx => use_security_jobs_helpers.tsx} (59%) delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 2102673060273f..d0bce8508e82e7 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 4b1d7ee733dcfd..ec884bfac5351c 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 00000000000000..1ffc2e16b78f75 --- /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 00000000000000..c0b6862ac30fe5 --- /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 43cfa4ad599640..f5783fc9b3973a 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 6fbb308672e5d7..e6597de892bff3 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 00000000000000..15f823814d7fc8 --- /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 32f6f888ab8d71..8ee765c1dea476 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 00000000000000..a80bfb59649cb4 --- /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 00000000000000..aabd8c7b62e85f --- /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 00000000000000..72690a17739266 --- /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 00000000000000..a9a728f81cc6cd --- /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 d897b2554b4fdd..4f804a355e4b55 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 c83271a56be5a8..c12c8d78da714e 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 9bfae686b1a594..7fdf41e6b6500d 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 af27d411b990d9..124d8d9a794c1a 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 54bb0a96207e14..0e8f033ff0cf35 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 7c72098209a066..dd0fb33fd2bc67 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 0b8da6be57e1b5..2a2db46d423077 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 5989d052f7cd25..daf9da855c0f94 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 00000000000000..80f50912a84f28 --- /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 00000000000000..e8809e8366eed4 --- /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 fc9f369a305aa7..7fb4e6f59d9f7f 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 adbd712ffeb3e0..d0109fd29b5fb8 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 7f0a8dea1913e4..00000000000000 --- 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 747ac63551b55b..9bee321e9fbde4 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 d879942b8b1014..362fb94dc1ec4f 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 5b656adc3e5817..6b7699d57aedf9 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 4cfb7f8ad2b5bb..f25ea667b34118 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 ade8c6fe805257..e58d76bd1dde00 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 d370d475bd6e57..3ad71ee6b6919c 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 f28a99c9947d54..be911a1cd85378 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 0ebf3674718482..f2bf2273c4b3fb 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 f39daa0b9a7fbe..c839f5110fe7fd 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 76270a7c08cd63..94019b26c180b0 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 10d5d1c60a6c2b..5248801d723b60 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 00000000000000..1af4ba3ba9233c --- /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 2c52acd3ec747c..5f4285f2747ae1 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 13b3c4b249bfef..f8eed75cf9bf1a 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 c5d50e1379482b..bdb8ca85b0d777 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 47c12d19341740..00141c9a453d82 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 c82a465f08c3a7..3152fef12c6523 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 d7e06511e79373..6baa2abab33d1a 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 6f6581e4de1c37..4a08adbedab3f6 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 cdfdf4ca6b66bf..b0aa0329fe8f40 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 85dce907084e8a..110691328b13b5 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 016d0c7c67a9e8..8a969a4cf098cb 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 34840b28266268..67f563e944f42f 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 e4e69443c510d2..2b19249dc426fd 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 cf08b084d21979..d6dfe1769308e9 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 9ac05cc98bb454..07abe7bc8c209f 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 0a15b039b96af2..c7aba6fcc8a5b3 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';