diff --git a/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts
index 66820050516be..b8f8243ff9f87 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts
@@ -28,6 +28,10 @@ import {
USERS_TABLE_ALERT_CELL,
HOSTS_TABLE_ALERT_CELL,
HOSTS_TABLE,
+ ANOMALIES_TABLE_NEXT_PAGE_BUTTON,
+ ANOMALIES_TABLE_ENABLE_JOB_BUTTON,
+ ANOMALIES_TABLE_ENABLE_JOB_LOADER,
+ ANOMALIES_TABLE_COUNT_COLUMN,
} from '../../screens/entity_analytics';
import { openRiskTableFilterAndSelectTheLowOption } from '../../tasks/host_risk';
import { createRule } from '../../tasks/api_calls/rules';
@@ -35,6 +39,7 @@ import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { getNewRule } from '../../objects/rule';
import { clickOnFirstHostsAlerts, clickOnFirstUsersAlerts } from '../../tasks/risk_scores';
import { OPTION_LIST_LABELS, OPTION_LIST_VALUES } from '../../screens/common/filter_group';
+import { setRowsPerPageTo } from '../../tasks/table_pagination';
const TEST_USER_ALERTS = 2;
const TEST_USER_NAME = 'test';
@@ -239,13 +244,39 @@ describe('Entity Analytics Dashboard', () => {
});
describe('With anomalies data', () => {
+ before(() => {
+ esArchiverLoad('network');
+ });
+
+ after(() => {
+ esArchiverUnload('network');
+ });
+
beforeEach(() => {
visit(ENTITY_ANALYTICS_URL);
});
- it('renders table', () => {
+ it('renders table with pagination', () => {
cy.get(ANOMALIES_TABLE).should('be.visible');
- cy.get(ANOMALIES_TABLE_ROWS).should('have.length', 6);
+ cy.get(ANOMALIES_TABLE_ROWS).should('have.length', 10);
+
+ // navigates to next page
+ cy.get(ANOMALIES_TABLE_NEXT_PAGE_BUTTON).click();
+ cy.get(ANOMALIES_TABLE_ROWS).should('have.length', 10);
+
+ // updates rows per page to 25 items
+ setRowsPerPageTo(25);
+ cy.get(ANOMALIES_TABLE_ROWS).should('have.length', 25);
+ });
+
+ it('enables a job', () => {
+ cy.get(ANOMALIES_TABLE_ROWS)
+ .eq(5)
+ .within(() => {
+ cy.get(ANOMALIES_TABLE_ENABLE_JOB_BUTTON).click();
+ cy.get(ANOMALIES_TABLE_ENABLE_JOB_LOADER).should('be.visible');
+ cy.get(ANOMALIES_TABLE_COUNT_COLUMN).should('include.text', '0');
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts b/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts
index 6095151fee57b..f41ae2a175f05 100644
--- a/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts
@@ -47,6 +47,15 @@ export const ANOMALIES_TABLE =
export const ANOMALIES_TABLE_ROWS = '[data-test-subj="entity_analytics_anomalies"] .euiTableRow';
+export const ANOMALIES_TABLE_ENABLE_JOB_BUTTON = '[data-test-subj="enable-job"]';
+
+export const ANOMALIES_TABLE_ENABLE_JOB_LOADER = '[data-test-subj="job-switch-loader"]';
+
+export const ANOMALIES_TABLE_COUNT_COLUMN = '[data-test-subj="anomalies-table-column-count"]';
+
+export const ANOMALIES_TABLE_NEXT_PAGE_BUTTON =
+ '[data-test-subj="entity_analytics_anomalies"] [data-test-subj="pagination-button-next"]';
+
export const UPGRADE_CONFIRMATION_MODAL = (riskScoreEntity: RiskScoreEntity) =>
`[data-test-subj="${riskScoreEntity}-risk-score-upgrade-confirmation-modal"]`;
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts
index fa193a50afc8a..f4959bb223155 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts
@@ -162,14 +162,9 @@ describe('useNotableAnomaliesSearch', () => {
await waitForNextUpdate();
const names = result.current.data.map(({ name }) => name);
- expect(names).toEqual([
- firstJobSecurityName,
- secondJobSecurityName,
- 'packetbeat_dns_tunneling',
- 'packetbeat_rare_dns_question',
- 'packetbeat_rare_server_domain',
- 'suspicious_login_activity',
- ]);
+
+ expect(names[0]).toEqual(firstJobSecurityName);
+ expect(names[1]).toEqual(secondJobSecurityName);
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx
index b4c8265d23242..52d955414a099 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx
@@ -43,9 +43,9 @@ const JOB = {
isCompatible: true,
} as SecurityJob;
-const mockSetupMlJob = jest.fn().mockReturnValue(Promise.resolve());
-const mockStartDatafeeds = jest.fn().mockReturnValue(Promise.resolve());
-const mockStopDatafeeds = jest.fn().mockReturnValue(Promise.resolve());
+const mockSetupMlJob = jest.fn();
+const mockStartDatafeeds = jest.fn();
+const mockStopDatafeeds = jest.fn();
jest.mock('../api', () => ({
setupMlJob: () => mockSetupMlJob(),
@@ -69,186 +69,266 @@ jest.mock('../../../lib/kibana', () => {
describe('useSecurityJobsHelpers', () => {
afterEach(() => {
- mockSetupMlJob.mockReset();
mockStartDatafeeds.mockReset();
mockStopDatafeeds.mockReset();
mockSetupMlJob.mockReset();
- });
- it('renders isLoading=true when installing job', async () => {
- let resolvePromiseCb: (value: unknown) => void;
- mockSetupMlJob.mockReturnValue(
- new Promise((resolve) => {
- resolvePromiseCb = resolve;
- })
+ mockStartDatafeeds.mockReturnValue(
+ Promise.resolve({ [`datafeed-${jobId}`]: { started: true } })
);
- const { result, waitForNextUpdate } = renderHook(() => useEnableDataFeed(), {
- wrapper,
- });
- expect(result.current.isLoading).toBe(false);
+ mockStopDatafeeds.mockReturnValue(
+ Promise.resolve([{ [`datafeed-${jobId}`]: { stopped: true } }])
+ );
+ mockSetupMlJob.mockReturnValue(Promise.resolve());
+ });
- await act(async () => {
- const enableDataFeedPromise = result.current.enableDatafeed(JOB, TIMESTAMP, false);
+ describe('enableDatafeed', () => {
+ it('renders isLoading=true when installing job', async () => {
+ let resolvePromiseCb: (value: unknown) => void;
+ mockSetupMlJob.mockReturnValue(
+ new Promise((resolve) => {
+ resolvePromiseCb = resolve;
+ })
+ );
+ const { result, waitForNextUpdate } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ expect(result.current.isLoading).toBe(false);
- await waitForNextUpdate();
- expect(result.current.isLoading).toBe(true);
+ await act(async () => {
+ const enableDataFeedPromise = result.current.enableDatafeed(JOB, TIMESTAMP);
- resolvePromiseCb({});
- await enableDataFeedPromise;
- expect(result.current.isLoading).toBe(false);
- });
- });
+ await waitForNextUpdate();
+ expect(result.current.isLoading).toBe(true);
- it('does not call setupMlJob if job is already installed', async () => {
- mockSetupMlJob.mockReturnValue(Promise.resolve());
- const { result } = renderHook(() => useEnableDataFeed(), {
- wrapper,
+ resolvePromiseCb({});
+ await enableDataFeedPromise;
+ expect(result.current.isLoading).toBe(false);
+ });
});
- await act(async () => {
- await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP, false);
- });
+ it('does not call setupMlJob if job is already installed', async () => {
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
- expect(mockSetupMlJob).not.toBeCalled();
- });
+ await act(async () => {
+ await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP);
+ });
- it('calls setupMlJob if job is uninstalled', async () => {
- mockSetupMlJob.mockReturnValue(Promise.resolve());
- const { result } = renderHook(() => useEnableDataFeed(), {
- wrapper,
- });
- await act(async () => {
- await result.current.enableDatafeed({ ...JOB, isInstalled: false }, TIMESTAMP, false);
+ expect(mockSetupMlJob).not.toBeCalled();
});
- expect(mockSetupMlJob).toBeCalled();
- });
- it('calls startDatafeeds if enable param is true', async () => {
- const { result } = renderHook(() => useEnableDataFeed(), {
- wrapper,
- });
- await act(async () => {
- await result.current.enableDatafeed(JOB, TIMESTAMP, true);
+ it('calls setupMlJob if job is uninstalled', async () => {
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.enableDatafeed({ ...JOB, isInstalled: false }, TIMESTAMP);
+ });
+ expect(mockSetupMlJob).toBeCalled();
});
- expect(mockStartDatafeeds).toBeCalled();
- expect(mockStopDatafeeds).not.toBeCalled();
- });
- it('calls stopDatafeeds if enable param is false', async () => {
- const { result } = renderHook(() => useEnableDataFeed(), {
- wrapper,
- });
- await act(async () => {
- await result.current.enableDatafeed(JOB, TIMESTAMP, false);
+ it('calls startDatafeeds when enableDatafeed is called', async () => {
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.enableDatafeed(JOB, TIMESTAMP);
+ });
+ expect(mockStartDatafeeds).toBeCalled();
+ expect(mockStopDatafeeds).not.toBeCalled();
});
- expect(mockStartDatafeeds).not.toBeCalled();
- expect(mockStopDatafeeds).toBeCalled();
- });
- it('calls startDatafeeds with 2 weeks old start date', async () => {
- jest.useFakeTimers().setSystemTime(new Date('1989-03-07'));
+ it('calls startDatafeeds with 2 weeks old start date', async () => {
+ jest.useFakeTimers().setSystemTime(new Date('1989-03-07'));
- const { result } = renderHook(() => useEnableDataFeed(), {
- wrapper,
- });
- await act(async () => {
- await result.current.enableDatafeed(JOB, TIMESTAMP, true);
- });
- expect(mockStartDatafeeds).toBeCalledWith({
- datafeedIds: [`datafeed-test_job_id`],
- start: new Date('1989-02-21').getTime(),
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.enableDatafeed(JOB, TIMESTAMP);
+ });
+ expect(mockStartDatafeeds).toBeCalledWith({
+ datafeedIds: [`datafeed-test_job_id`],
+ start: new Date('1989-02-21').getTime(),
+ });
});
- });
- describe('telemetry', () => {
- it('reports telemetry when installing and enabling a job', async () => {
- mockSetupMlJob.mockReturnValue(new Promise((resolve) => resolve({})));
+ it('return enabled:true when startDataFeed successfully installed the job', async () => {
const { result } = renderHook(() => useEnableDataFeed(), {
wrapper,
});
-
await act(async () => {
- await result.current.enableDatafeed(JOB, TIMESTAMP, true);
+ const response = await result.current.enableDatafeed(JOB, TIMESTAMP);
+ expect(response.enabled).toBeTruthy();
});
+ });
- expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
- status: ML_JOB_TELEMETRY_STATUS.moduleInstalled,
- isElasticJob: true,
- jobId,
- moduleId,
+ it('return enabled:false when startDataFeed promise is rejected while installing a job', async () => {
+ mockStartDatafeeds.mockReturnValue(Promise.reject(new Error('test_error')));
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
});
-
- expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
- status: ML_JOB_TELEMETRY_STATUS.started,
- isElasticJob: true,
- jobId,
+ await act(async () => {
+ const response = await result.current.enableDatafeed(JOB, TIMESTAMP);
+ expect(response.enabled).toBeFalsy();
});
});
- it('reports telemetry when stopping a job', async () => {
+ it('return enabled:false when startDataFeed failed to install the job', async () => {
+ mockStartDatafeeds.mockReturnValue(
+ Promise.resolve({ [`datafeed-${jobId}`]: { started: false, error: 'test_error' } })
+ );
+
const { result } = renderHook(() => useEnableDataFeed(), {
wrapper,
});
await act(async () => {
- await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP, false);
+ const response = await result.current.enableDatafeed(JOB, TIMESTAMP);
+ expect(response.enabled).toBeFalsy();
+ });
+ });
+
+ describe('telemetry', () => {
+ it('reports telemetry when installing and enabling a job', async () => {
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+
+ await act(async () => {
+ await result.current.enableDatafeed(JOB, TIMESTAMP);
+ });
+
+ expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
+ status: ML_JOB_TELEMETRY_STATUS.moduleInstalled,
+ isElasticJob: true,
+ jobId,
+ moduleId,
+ });
+
+ expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
+ status: ML_JOB_TELEMETRY_STATUS.started,
+ isElasticJob: true,
+ jobId,
+ });
});
- expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
- status: ML_JOB_TELEMETRY_STATUS.stopped,
- isElasticJob: true,
- jobId,
+ it('reports telemetry when starting a job fails', async () => {
+ mockStartDatafeeds.mockReturnValue(Promise.reject(new Error('test_error')));
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP);
+ });
+
+ expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
+ status: ML_JOB_TELEMETRY_STATUS.startError,
+ errorMessage: 'Start job failure - test_error',
+ isElasticJob: true,
+ jobId,
+ });
+ });
+
+ it('reports telemetry when installing a module fails', async () => {
+ mockSetupMlJob.mockReturnValue(Promise.reject(new Error('test_error')));
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.enableDatafeed(JOB, TIMESTAMP);
+ });
+
+ expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
+ status: ML_JOB_TELEMETRY_STATUS.installationError,
+ errorMessage: 'Create job failure - test_error',
+ isElasticJob: true,
+ jobId,
+ moduleId,
+ });
});
});
+ });
- it('reports telemetry when stopping a job fails', async () => {
- mockStopDatafeeds.mockReturnValue(Promise.reject(new Error('test_error')));
+ describe('disableDatafeed', () => {
+ it('return enabled:false when disableDatafeed successfully uninstalled the job', async () => {
const { result } = renderHook(() => useEnableDataFeed(), {
wrapper,
});
await act(async () => {
- await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP, false);
- });
-
- expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
- status: ML_JOB_TELEMETRY_STATUS.stopError,
- errorMessage: 'Stop job failure - test_error',
- isElasticJob: true,
- jobId,
+ const response = await result.current.disableDatafeed(JOB);
+ expect(response.enabled).toBeFalsy();
});
});
- it('reports telemetry when starting a job fails', async () => {
- mockStartDatafeeds.mockReturnValue(Promise.reject(new Error('test_error')));
+ it('return enabled:true when promise is rejected while uninstalling the job', async () => {
+ mockStopDatafeeds.mockReturnValue(Promise.reject(new Error('test_error')));
const { result } = renderHook(() => useEnableDataFeed(), {
wrapper,
});
await act(async () => {
- await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP, true);
+ const response = await result.current.disableDatafeed(JOB);
+ expect(response.enabled).toBeTruthy();
});
+ });
+
+ it('return enabled:true when disableDatafeed fails to uninstall the job', async () => {
+ mockStopDatafeeds.mockReturnValue(
+ Promise.resolve([{ [`datafeed-${jobId}`]: { stopped: false, error: 'test_error' } }])
+ );
- expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
- status: ML_JOB_TELEMETRY_STATUS.startError,
- errorMessage: 'Start job failure - test_error',
- isElasticJob: true,
- jobId,
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ const response = await result.current.disableDatafeed(JOB);
+ expect(response.enabled).toBeTruthy();
});
});
- it('reports telemetry when installing a module fails', async () => {
- mockSetupMlJob.mockReturnValue(Promise.reject(new Error('test_error')));
+ it('calls stopDatafeeds when disableDatafeed is called', async () => {
const { result } = renderHook(() => useEnableDataFeed(), {
wrapper,
});
await act(async () => {
- await result.current.enableDatafeed(JOB, TIMESTAMP, true);
+ await result.current.disableDatafeed(JOB);
+ });
+ expect(mockStartDatafeeds).not.toBeCalled();
+ expect(mockStopDatafeeds).toBeCalled();
+ });
+
+ describe('telemetry', () => {
+ it('reports telemetry when stopping a job', async () => {
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.disableDatafeed({ ...JOB, isInstalled: true });
+ });
+
+ expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
+ status: ML_JOB_TELEMETRY_STATUS.stopped,
+ isElasticJob: true,
+ jobId,
+ });
});
- expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
- status: ML_JOB_TELEMETRY_STATUS.installationError,
- errorMessage: 'Create job failure - test_error',
- isElasticJob: true,
- jobId,
- moduleId,
+ it('reports telemetry when stopping a job fails', async () => {
+ mockStopDatafeeds.mockReturnValue(Promise.reject(new Error('test_error')));
+ const { result } = renderHook(() => useEnableDataFeed(), {
+ wrapper,
+ });
+ await act(async () => {
+ await result.current.disableDatafeed({ ...JOB, isInstalled: true });
+ });
+
+ expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({
+ status: ML_JOB_TELEMETRY_STATUS.stopError,
+ errorMessage: 'Stop job failure - test_error',
+ isElasticJob: true,
+ jobId,
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts
index 48b7a918af26c..393e132436c38 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { isEmpty } from 'lodash/fp';
import { useCallback, useState } from 'react';
import { useAppToasts } from '../../../hooks/use_app_toasts';
import { useKibana } from '../../../lib/kibana';
@@ -16,10 +17,10 @@ import {
} from '../../../lib/telemetry';
import { setupMlJob, startDatafeeds, stopDatafeeds } from '../api';
-import type { SecurityJob } from '../types';
+import type { ErrorResponse, SecurityJob } from '../types';
import * as i18n from './translations';
-// Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch
+// Enable/Disable Job & Datafeed
export const useEnableDataFeed = () => {
const { telemetry } = useKibana().services;
@@ -27,11 +28,14 @@ export const useEnableDataFeed = () => {
const [isLoading, setIsLoading] = useState(false);
const enableDatafeed = useCallback(
- async (job: SecurityJob, latestTimestampMs: number, enable: boolean) => {
- submitTelemetry(job, enable);
+ async (job: SecurityJob, latestTimestampMs: number) => {
+ setIsLoading(true);
+ track(
+ METRIC_TYPE.COUNT,
+ job.isElasticJob ? TELEMETRY_EVENT.SIEM_JOB_ENABLED : TELEMETRY_EVENT.CUSTOM_JOB_ENABLED
+ );
if (!job.isInstalled) {
- setIsLoading(true);
try {
await setupMlJob({
configTemplate: job.moduleId,
@@ -39,7 +43,6 @@ export const useEnableDataFeed = () => {
jobIdErrorFilter: [job.id],
groups: job.groups,
});
- setIsLoading(false);
telemetry.reportMLJobUpdate({
jobId: job.id,
isElasticJob: job.isElasticJob,
@@ -47,8 +50,8 @@ export const useEnableDataFeed = () => {
status: ML_JOB_TELEMETRY_STATUS.moduleInstalled,
});
} catch (error) {
- addError(error, { title: i18n.CREATE_JOB_FAILURE });
setIsLoading(false);
+ addError(error, { title: i18n.CREATE_JOB_FAILURE });
telemetry.reportMLJobUpdate({
jobId: job.id,
isElasticJob: job.isElasticJob,
@@ -56,7 +59,8 @@ export const useEnableDataFeed = () => {
status: ML_JOB_TELEMETRY_STATUS.installationError,
errorMessage: `${i18n.CREATE_JOB_FAILURE} - ${error.message}`,
});
- return;
+
+ return { enabled: false };
}
}
@@ -64,63 +68,89 @@ export const useEnableDataFeed = () => {
const date = new Date();
const maxStartTime = date.setDate(date.getDate() - 14);
- setIsLoading(true);
- if (enable) {
- const startTime = Math.max(latestTimestampMs, maxStartTime);
- try {
- await startDatafeeds({ datafeedIds: [`datafeed-${job.id}`], start: startTime });
- telemetry.reportMLJobUpdate({
- jobId: job.id,
- isElasticJob: job.isElasticJob,
- status: ML_JOB_TELEMETRY_STATUS.started,
- });
- } catch (error) {
- track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE);
- addError(error, { title: i18n.START_JOB_FAILURE });
- telemetry.reportMLJobUpdate({
- jobId: job.id,
- isElasticJob: job.isElasticJob,
- status: ML_JOB_TELEMETRY_STATUS.startError,
- errorMessage: `${i18n.START_JOB_FAILURE} - ${error.message}`,
- });
- }
- } else {
- try {
- await stopDatafeeds({ datafeedIds: [`datafeed-${job.id}`] });
- telemetry.reportMLJobUpdate({
- jobId: job.id,
- isElasticJob: job.isElasticJob,
- status: ML_JOB_TELEMETRY_STATUS.stopped,
- });
- } catch (error) {
- track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE);
- addError(error, { title: i18n.STOP_JOB_FAILURE });
- telemetry.reportMLJobUpdate({
- jobId: job.id,
- isElasticJob: job.isElasticJob,
- status: ML_JOB_TELEMETRY_STATUS.stopError,
- errorMessage: `${i18n.STOP_JOB_FAILURE} - ${error.message}`,
- });
+ const datafeedId = `datafeed-${job.id}`;
+
+ const startTime = Math.max(latestTimestampMs, maxStartTime);
+
+ try {
+ const response = await startDatafeeds({
+ datafeedIds: [datafeedId],
+ start: startTime,
+ });
+
+ if (response[datafeedId]?.error) {
+ throw new Error(response[datafeedId].error);
}
+
+ telemetry.reportMLJobUpdate({
+ jobId: job.id,
+ isElasticJob: job.isElasticJob,
+ status: ML_JOB_TELEMETRY_STATUS.started,
+ });
+
+ return { enabled: response[datafeedId] ? response[datafeedId].started : false };
+ } catch (error) {
+ track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE);
+ addError(error, { title: i18n.START_JOB_FAILURE });
+ telemetry.reportMLJobUpdate({
+ jobId: job.id,
+ isElasticJob: job.isElasticJob,
+ status: ML_JOB_TELEMETRY_STATUS.startError,
+ errorMessage: `${i18n.START_JOB_FAILURE} - ${error.message}`,
+ });
+ } finally {
+ setIsLoading(false);
}
- setIsLoading(false);
+
+ return { enabled: false };
},
[addError, telemetry]
);
- return { enableDatafeed, isLoading };
-};
+ const disableDatafeed = useCallback(
+ async (job: SecurityJob) => {
+ track(
+ METRIC_TYPE.COUNT,
+ job.isElasticJob ? TELEMETRY_EVENT.SIEM_JOB_DISABLED : TELEMETRY_EVENT.CUSTOM_JOB_DISABLED
+ );
+ setIsLoading(true);
-const submitTelemetry = (job: SecurityJob, enabled: boolean) => {
- // Report type of job enabled/disabled
- track(
- METRIC_TYPE.COUNT,
- job.isElasticJob
- ? enabled
- ? TELEMETRY_EVENT.SIEM_JOB_ENABLED
- : TELEMETRY_EVENT.SIEM_JOB_DISABLED
- : enabled
- ? TELEMETRY_EVENT.CUSTOM_JOB_ENABLED
- : TELEMETRY_EVENT.CUSTOM_JOB_DISABLED
+ const datafeedId = `datafeed-${job.id}`;
+
+ try {
+ const [response] = await stopDatafeeds({ datafeedIds: [datafeedId] });
+
+ if (isErrorResponse(response)) {
+ throw new Error(response.error);
+ }
+
+ telemetry.reportMLJobUpdate({
+ jobId: job.id,
+ isElasticJob: job.isElasticJob,
+ status: ML_JOB_TELEMETRY_STATUS.stopped,
+ });
+
+ return { enabled: response[datafeedId] ? !response[datafeedId].stopped : true };
+ } catch (error) {
+ track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE);
+ addError(error, { title: i18n.STOP_JOB_FAILURE });
+ telemetry.reportMLJobUpdate({
+ jobId: job.id,
+ isElasticJob: job.isElasticJob,
+ status: ML_JOB_TELEMETRY_STATUS.stopError,
+ errorMessage: `${i18n.STOP_JOB_FAILURE} - ${error.message}`,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+
+ return { enabled: true };
+ },
+ [addError, telemetry]
);
+
+ return { enableDatafeed, disableDatafeed, isLoading };
};
+
+const isErrorResponse = (response: ErrorResponse): response is ErrorResponse =>
+ !isEmpty(response.error);
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
index d518a42f2907d..5f3b834d777b8 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
@@ -59,14 +59,22 @@ export const MlPopover = React.memo(() => {
} = useSecurityJobs();
const docLinks = useKibana().services.docLinks;
- const { enableDatafeed, isLoading: isLoadingEnableDataFeed } = useEnableDataFeed();
+ const {
+ enableDatafeed,
+ disableDatafeed,
+ isLoading: isLoadingEnableDataFeed,
+ } = useEnableDataFeed();
const handleJobStateChange = useCallback(
async (job: SecurityJob, latestTimestampMs: number, enable: boolean) => {
- const result = await enableDatafeed(job, latestTimestampMs, enable);
+ if (enable) {
+ await enableDatafeed(job, latestTimestampMs);
+ } else {
+ await disableDatafeed(job);
+ }
+
refreshJobs();
- return result;
},
- [refreshJobs, enableDatafeed]
+ [refreshJobs, enableDatafeed, disableDatafeed]
);
const filteredJobs = filterJobs({
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_start_ml_jobs.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_start_ml_jobs.tsx
index a0afcad5901e4..cf12b8b0f1bf6 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_start_ml_jobs.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_start_ml_jobs.tsx
@@ -43,7 +43,7 @@ export const useStartMlJobs = (): ReturnUseStartMlJobs => {
}
const latestTimestampMs = job.latestTimestampMs ?? 0;
- await enableDatafeed(job, latestTimestampMs, true);
+ await enableDatafeed(job, latestTimestampMs);
})
);
refetchJobs();
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx
index ad9b3c751f59f..104ea748f106c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx
@@ -56,8 +56,7 @@ describe('MlAdminJobDescription', () => {
userEvent.click(screen.getByTestId('job-switch'));
expect(enableDatafeedSpy).toHaveBeenCalledWith(
securityJobNotStarted,
- securityJobNotStarted.latestTimestampMs,
- true
+ securityJobNotStarted.latestTimestampMs
);
await waitFor(() => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx
index 7d4616a004364..7f1236aef08cd 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx
@@ -25,14 +25,23 @@ const MlAdminJobDescriptionComponent: FC