[Security Solution][Detections] Refactor ML calls for newest ML permissions (#74582)
## Summary Addresses https://github.com/elastic/kibana/issues/73567. ML Users (role: `machine_learning_user`) were previously able to invoke the ML Recognizer API, which we use to get not-yet-installed ML Jobs relevant to our index patterns. As of https://github.com/elastic/kibana/pull/64662 this is not true, and so we receive errors from components using the underlying hook, `useSiemJobs`. To solve this I've created two separate hooks to replace `useSiemJobs`: * `useSecurityJobs` * used on ML Popover * includes uninstalled ML Jobs * checks (and returns) `isMlAdmin` before fetching data * `useInstalledSecurityJobs` * used on ML Jobs Dropdown and Anomalies Table * includes only installed ML Jobs * checks (and returns) `isMlUser` before fetching data Note that we while we now receive the knowledge to do so, we do not always inform the user in the case of invalid permissions, and instead have the following behaviors: #### User has insufficient license * ML Popover: shows an upgrade CTA * Anomalies Tables: show no data * Rule Creation: ML Rule option is disabled, shows upgrade CTA * Rule Details: ML Job Id is displayed as text #### User is ML User * ML Popover: not shown * Anomalies Tables: show no data * Rule Creation: ML Rule option is disabled * Rule Details: ML Job Id is displayed as text #### User is ML Admin * ML Popover: shown * Anomalies Tables: show data __for installed ML Jobs__ * This is the same as previous logic, but worth calling out that you can't view historical anomalies * Rule Creation: ML Rule option is enabled, all ML Jobs available * Rule Details: ML Job Id is displayed as hyperlink, job status badge shown ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
a735a9f825
commit
4ca52e678a
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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));
|
||||
|
|
|
@ -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<Anomalies | null>(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$<number>(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];
|
||||
|
|
|
@ -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<MlSummaryJob[]> =>
|
||||
http.fetch<MlSummaryJob[]>('/api/ml/jobs/jobs_summary', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ jobIds: jobIds ?? [] }),
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
});
|
|
@ -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<MlCapabilitiesResponse> => {
|
||||
return KibanaServices.get().http.fetch<MlCapabilitiesResponse>('/api/ml/ml_capabilities', {
|
||||
export const getMlCapabilities = async ({
|
||||
http,
|
||||
signal,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
}): Promise<MlCapabilitiesResponse> =>
|
||||
http.fetch<MlCapabilitiesResponse>('/api/ml/ml_capabilities', {
|
||||
method: 'GET',
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<MlSummaryJob[]>([]);
|
||||
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 };
|
||||
};
|
|
@ -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);
|
|
@ -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<MlCapabilitiesProvider>(
|
||||
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 (
|
||||
<MlCapabilitiesContext.Provider value={capabilities}>{children}</MlCapabilitiesContext.Provider>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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:
|
|
@ -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<JobSummary[]> =>
|
||||
KibanaServices.get().http.fetch<JobSummary[]>('/api/ml/jobs/jobs_summary', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<SecurityJob[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
const [siemDefaultIndex] = useUiSetting$<string[]>(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 };
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<SiemJob>((jobSummary) => ({
|
||||
.map<SecurityJob>((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);
|
||||
};
|
|
@ -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<SiemJob[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
const userPermissions = hasMlUserPermissions(mlCapabilities);
|
||||
const [siemDefaultIndex] = useUiSetting$<string[]>(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];
|
||||
};
|
|
@ -25,7 +25,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = `
|
|||
<EuiFilterGroup>
|
||||
<GroupsFilterPopover
|
||||
onSelectedGroupsChanged={[Function]}
|
||||
siemJobs={
|
||||
securityJobs={
|
||||
Array [
|
||||
Object {
|
||||
"datafeedId": "datafeed-linux_anomalous_network_activity_ecs",
|
||||
|
|
|
@ -7,20 +7,23 @@
|
|||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { GroupsFilterPopoverComponent } from './groups_filter_popover';
|
||||
import { mockSiemJobs } from '../../__mocks__/api';
|
||||
import { SiemJob } from '../../types';
|
||||
import { mockSecurityJobs } from '../../api.mock';
|
||||
import { SecurityJob } from '../../types';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
|
||||
describe('GroupsFilterPopover', () => {
|
||||
let siemJobs: SiemJob[];
|
||||
let securityJobs: SecurityJob[];
|
||||
|
||||
beforeEach(() => {
|
||||
siemJobs = cloneDeep(mockSiemJobs);
|
||||
securityJobs = cloneDeep(mockSecurityJobs);
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<GroupsFilterPopoverComponent siemJobs={siemJobs} onSelectedGroupsChanged={jest.fn()} />
|
||||
<GroupsFilterPopoverComponent
|
||||
securityJobs={securityJobs}
|
||||
onSelectedGroupsChanged={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
@ -29,7 +32,7 @@ describe('GroupsFilterPopover', () => {
|
|||
const mockOnSelectedGroupsChanged = jest.fn();
|
||||
const wrapper = mount(
|
||||
<GroupsFilterPopoverComponent
|
||||
siemJobs={siemJobs}
|
||||
securityJobs={securityJobs}
|
||||
onSelectedGroupsChanged={mockOnSelectedGroupsChanged}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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<SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string[]>([]);
|
||||
|
||||
const groups = siemJobs
|
||||
const groups = securityJobs
|
||||
.map((j) => j.groups)
|
||||
.flat()
|
||||
.filter((g) => g !== 'siem' && g !== 'security');
|
||||
|
|
|
@ -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(
|
||||
<JobsTableFiltersComponent siemJobs={siemJobs} onFilterChanged={jest.fn()} />
|
||||
<JobsTableFiltersComponent securityJobs={securityJobs} onFilterChanged={jest.fn()} />
|
||||
);
|
||||
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(
|
||||
<JobsTableFiltersComponent siemJobs={siemJobs} onFilterChanged={onFilterChanged} />
|
||||
<JobsTableFiltersComponent securityJobs={securityJobs} onFilterChanged={onFilterChanged} />
|
||||
);
|
||||
|
||||
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(
|
||||
<JobsTableFiltersComponent siemJobs={siemJobs} onFilterChanged={onFilterChanged} />
|
||||
<JobsTableFiltersComponent securityJobs={securityJobs} onFilterChanged={onFilterChanged} />
|
||||
);
|
||||
|
||||
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(
|
||||
<JobsTableFiltersComponent siemJobs={siemJobs} onFilterChanged={onFilterChanged} />
|
||||
<JobsTableFiltersComponent securityJobs={securityJobs} onFilterChanged={onFilterChanged} />
|
||||
);
|
||||
|
||||
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(
|
||||
<JobsTableFiltersComponent siemJobs={siemJobs} onFilterChanged={onFilterChanged} />
|
||||
<JobsTableFiltersComponent securityJobs={securityJobs} onFilterChanged={onFilterChanged} />
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click');
|
||||
|
|
|
@ -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<SetStateAction<JobsFilters>>;
|
||||
}
|
||||
|
||||
|
@ -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<string>('');
|
||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||
const [showCustomJobs, setShowCustomJobs] = useState<boolean>(false);
|
||||
|
@ -71,7 +74,10 @@ export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTab
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFilterGroup>
|
||||
<GroupsFilterPopover siemJobs={siemJobs} onSelectedGroupsChanged={setSelectedGroups} />
|
||||
<GroupsFilterPopover
|
||||
securityJobs={securityJobs}
|
||||
onSelectedGroupsChanged={setSelectedGroups}
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -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(
|
||||
<JobSwitchComponent
|
||||
job={siemJobs[0]}
|
||||
isSiemJobsLoading={false}
|
||||
job={securityJobs[0]}
|
||||
isSecurityJobsLoading={false}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -34,8 +34,8 @@ describe('JobSwitch', () => {
|
|||
test('should call onJobStateChange when the switch is clicked to be true/open', () => {
|
||||
const wrapper = mount(
|
||||
<JobSwitchComponent
|
||||
job={siemJobs[0]}
|
||||
isSiemJobsLoading={false}
|
||||
job={securityJobs[0]}
|
||||
isSecurityJobsLoading={false}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -57,8 +57,8 @@ describe('JobSwitch', () => {
|
|||
test('should have a switch when it is not in the loading state', () => {
|
||||
const wrapper = mount(
|
||||
<JobSwitchComponent
|
||||
isSiemJobsLoading={false}
|
||||
job={siemJobs[0]}
|
||||
isSecurityJobsLoading={false}
|
||||
job={securityJobs[0]}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -68,8 +68,8 @@ describe('JobSwitch', () => {
|
|||
test('should not have a switch when it is in the loading state', () => {
|
||||
const wrapper = mount(
|
||||
<JobSwitchComponent
|
||||
isSiemJobsLoading={true}
|
||||
job={siemJobs[0]}
|
||||
isSecurityJobsLoading={true}
|
||||
job={securityJobs[0]}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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<void>;
|
||||
job: SecurityJob;
|
||||
isSecurityJobsLoading: boolean;
|
||||
onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export const JobSwitchComponent = ({
|
||||
job,
|
||||
isSiemJobsLoading,
|
||||
isSecurityJobsLoading,
|
||||
onJobStateChange,
|
||||
}: JobSwitchProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -47,7 +47,7 @@ export const JobSwitchComponent = ({
|
|||
return (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? (
|
||||
{isSecurityJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? (
|
||||
<EuiLoadingSpinner size="m" data-test-subj="job-switch-loader" />
|
||||
) : (
|
||||
<StaticSwitch
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { JobsTableComponent } from './jobs_table';
|
||||
import { mockSiemJobs } from '../__mocks__/api';
|
||||
import { mockSecurityJobs } from '../api.mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { SiemJob } from '../types';
|
||||
import { SecurityJob } from '../types';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
|
||||
describe('JobsTableComponent', () => {
|
||||
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(
|
||||
<JobsTableComponent
|
||||
isLoading={true}
|
||||
jobs={siemJobs}
|
||||
jobs={securityJobs}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -36,7 +36,7 @@ describe('JobsTableComponent', () => {
|
|||
const wrapper = mount(
|
||||
<JobsTableComponent
|
||||
isLoading={true}
|
||||
jobs={siemJobs}
|
||||
jobs={securityJobs}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -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(
|
||||
<JobsTableComponent
|
||||
isLoading={true}
|
||||
jobs={siemJobs}
|
||||
jobs={securityJobs}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -63,7 +63,7 @@ describe('JobsTableComponent', () => {
|
|||
const wrapper = mount(
|
||||
<JobsTableComponent
|
||||
isLoading={false}
|
||||
jobs={siemJobs}
|
||||
jobs={securityJobs}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -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(
|
||||
<JobsTableComponent
|
||||
isLoading={false}
|
||||
jobs={siemJobs}
|
||||
jobs={securityJobs}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
@ -91,7 +91,7 @@ describe('JobsTableComponent', () => {
|
|||
const wrapper = mount(
|
||||
<JobsTableComponent
|
||||
isLoading={true}
|
||||
jobs={siemJobs}
|
||||
jobs={securityJobs}
|
||||
onJobStateChange={onJobStateChangeMock}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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<void>,
|
||||
onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise<void>,
|
||||
basePath: string
|
||||
) => [
|
||||
{
|
||||
name: i18n.COLUMN_JOB_NAME,
|
||||
render: ({ id, description }: SiemJob) => (
|
||||
render: ({ id, description }: SecurityJob) => (
|
||||
<JobNameWrapper>
|
||||
<EuiLink
|
||||
data-test-subj="jobs-table-link"
|
||||
|
@ -62,7 +62,7 @@ const getJobsTableColumns = (
|
|||
},
|
||||
{
|
||||
name: i18n.COLUMN_GROUPS,
|
||||
render: ({ groups }: SiemJob) => (
|
||||
render: ({ groups }: SecurityJob) => (
|
||||
<EuiFlexGroup wrap responsive={true} gutterSize="xs">
|
||||
{groups.map((group) => (
|
||||
<EuiFlexItem grow={false} key={group}>
|
||||
|
@ -76,9 +76,13 @@ const getJobsTableColumns = (
|
|||
|
||||
{
|
||||
name: i18n.COLUMN_RUN_JOB,
|
||||
render: (job: SiemJob) =>
|
||||
render: (job: SecurityJob) =>
|
||||
job.isCompatible ? (
|
||||
<JobSwitch job={job} isSiemJobsLoading={isLoading} onJobStateChange={onJobStateChange} />
|
||||
<JobSwitch
|
||||
job={job}
|
||||
isSecurityJobsLoading={isLoading}
|
||||
onJobStateChange={onJobStateChange}
|
||||
/>
|
||||
) : (
|
||||
<EuiIcon aria-label="Warning" size="s" type="alert" color="warning" />
|
||||
),
|
||||
|
@ -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<void>;
|
||||
jobs: SecurityJob[];
|
||||
onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTableProps) => {
|
||||
|
|
|
@ -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 (
|
||||
<EuiPopover
|
||||
|
@ -127,7 +126,7 @@ export const MlPopover = React.memo(() => {
|
|||
<UpgradeContents />
|
||||
</EuiPopover>
|
||||
);
|
||||
} else if (hasMlAdminPermissions(capabilities)) {
|
||||
} else if (isMlAdmin) {
|
||||
// If the user has Platinum License & ML Admin Permissions, show Anomaly Detection button & full config UI
|
||||
return (
|
||||
<EuiPopover
|
||||
|
@ -156,7 +155,7 @@ export const MlPopover = React.memo(() => {
|
|||
|
||||
<EuiSpacer />
|
||||
|
||||
<JobsTableFilters siemJobs={siemJobs} onFilterChanged={setFilterProperties} />
|
||||
<JobsTableFilters securityJobs={jobs} onFilterChanged={setFilterProperties} />
|
||||
|
||||
<ShowingCount filterResultsLength={filteredJobs.length} />
|
||||
|
||||
|
@ -194,7 +193,7 @@ export const MlPopover = React.memo(() => {
|
|||
)}
|
||||
|
||||
<JobsTable
|
||||
isLoading={isLoadingSiemJobs || isLoading}
|
||||
isLoading={isLoadingSecurityJobs || isLoading}
|
||||
jobs={filteredJobs}
|
||||
onJobStateChange={handleJobStateChange}
|
||||
/>
|
||||
|
@ -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<Action>,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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$<number>(DEFAULT_ANOMALY_SCORE);
|
||||
|
||||
const mergedFilterQuery = getAnomaliesFilterQuery(
|
||||
filterQuery,
|
||||
anomaliesFilterQuery,
|
||||
siemJobs,
|
||||
jobs,
|
||||
anomalyScore,
|
||||
flowTarget,
|
||||
ip
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<StepRuleDescriptionProps> =
|
|||
}) => {
|
||||
const kibana = useKibana();
|
||||
const [filterManager] = useState<FilterManager>(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<StepRuleDescriptionProps> =
|
|||
buildMlJobDescription(
|
||||
get(key, data) as string,
|
||||
(get(key, schema) as { label: string }).label,
|
||||
siemJobs
|
||||
jobs
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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(<MlJobDescription job={job} />);
|
||||
const wrapper = shallow(<MlJobDescription job={mockOpenedJob} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1);
|
||||
});
|
||||
|
@ -47,7 +30,7 @@ describe('AuditIcon', () => {
|
|||
|
||||
describe('JobStatusBadge', () => {
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<JobStatusBadge job={job} />);
|
||||
const wrapper = shallow(<JobStatusBadge job={mockOpenedJob} />);
|
||||
|
||||
expect(wrapper.find('EuiBadge')).toHaveLength(1);
|
||||
});
|
||||
|
|
|
@ -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 ? <MlJobDescription job={siemJob} /> : jobId,
|
||||
description: job ? <MlJobDescription job={job} /> : jobId,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<MlJobSelectProps> = ({ 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<MlJobSelectProps> = ({ describedByIds = [], f
|
|||
disabled: true,
|
||||
};
|
||||
|
||||
const jobOptions = siemJobs.map((job) => ({
|
||||
const jobOptions = jobs.map((job) => ({
|
||||
value: job.id,
|
||||
inputDisplay: job.id,
|
||||
dropdownDisplay: <JobDisplay title={job.id} description={job.description} />,
|
||||
|
@ -107,9 +107,9 @@ export const MlJobSelect: React.FC<MlJobSelectProps> = ({ 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 (
|
||||
<MlJobSelectEuiFlexGroup>
|
||||
|
@ -126,7 +126,7 @@ export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], f
|
|||
<EuiFlexItem>
|
||||
<EuiSuperSelect
|
||||
hasDividers
|
||||
isLoading={isLoading}
|
||||
isLoading={loading}
|
||||
onChange={handleJobChange}
|
||||
options={options}
|
||||
valueOfSelected={jobId || 'placeholder'}
|
||||
|
|
|
@ -12,10 +12,11 @@ import deepEqual from 'fast-deep-equal';
|
|||
import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
|
||||
import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
|
||||
import { useMlCapabilities } from '../../../../common/components/ml_popover/hooks/use_ml_capabilities';
|
||||
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { useUiSetting$ } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
filterRuleFieldsForType,
|
||||
|
@ -187,7 +188,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
componentProps={{
|
||||
describedByIds: ['detectionEngineStepDefineRuleType'],
|
||||
isReadOnly: isUpdateView,
|
||||
hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense,
|
||||
hasValidLicense: hasMlLicense(mlCapabilities),
|
||||
isMlAdmin: hasMlAdminPermissions(mlCapabilities),
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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<AllRulesProps>(
|
|||
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<PaginationOptions>) => {
|
||||
dispatch({
|
||||
|
|
|
@ -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<PropsFromRedux> = ({
|
|||
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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue