diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index d56344b7707d..1b867507905a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -10,6 +10,7 @@ import { v4 } from 'uuid'; import { Logger, SavedObject } from 'kibana/server'; import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import { mlPluginServerMock } from '../../../../../../ml/server/mocks'; import type { IRuleDataClient } from '../../../../../../rule_registry/server'; import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks'; @@ -84,6 +85,7 @@ export const createRuleTypeMocks = ( config$: mockedConfig$, lists: listMock.createSetup(), logger: loggerMock, + ml: mlPluginServerMock.createSetupContract(), ruleDataClient: ruleRegistryMocks.createRuleDataClient( '.alerts-security.alerts' ) as IRuleDataClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts index 75252cc3d47a..0fde90c991e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts @@ -7,3 +7,4 @@ export { createQueryAlertType } from './query/create_query_alert_type'; export { createIndicatorMatchAlertType } from './indicator_match/create_indicator_match_alert_type'; +export { createMlAlertType } from './ml/create_ml_alert_type'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts new file mode 100644 index 000000000000..40566ffa04e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mlPluginServerMock } from '../../../../../../ml/server/mocks'; + +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { bulkCreateMlSignals } from '../../signals/bulk_create_ml_signals'; + +import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { createMlAlertType } from './create_ml_alert_type'; + +import { RuleParams } from '../../schemas/rule_schemas'; + +jest.mock('../../signals/bulk_create_ml_signals'); + +jest.mock('../utils/get_list_client', () => ({ + getListClient: jest.fn().mockReturnValue({ + listClient: { + getListItemIndex: jest.fn(), + }, + exceptionsClient: jest.fn(), + }), +})); + +jest.mock('../../rule_execution_log/rule_execution_log_client'); + +jest.mock('../../signals/filters/filter_events_against_list', () => ({ + filterEventsAgainstList: jest.fn().mockReturnValue({ + _shards: { + failures: [], + }, + hits: { + hits: [ + { + is_interim: false, + }, + ], + }, + }), +})); + +let jobsSummaryMock: jest.Mock; +let mlMock: ReturnType; + +describe('Machine Learning Alerts', () => { + beforeEach(() => { + jobsSummaryMock = jest.fn(); + jobsSummaryMock.mockResolvedValue([ + { + id: 'test-ml-job', + jobState: 'started', + datafeedState: 'started', + }, + ]); + mlMock = mlPluginServerMock.createSetupContract(); + mlMock.jobServiceProvider.mockReturnValue({ + jobsSummary: jobsSummaryMock, + }); + + (bulkCreateMlSignals as jest.Mock).mockResolvedValue({ + success: true, + bulkCreateDuration: 0, + createdItemsCount: 1, + createdItems: [ + { + _id: '897234565234', + _index: 'test-index', + anomalyScore: 23, + }, + ], + errors: [], + }); + }); + + const params: Partial = { + anomalyThreshold: 23, + from: 'now-45m', + machineLearningJobId: ['test-ml-job'], + to: 'now', + type: 'machine_learning', + }; + + it('does not send an alert when no anomalies found', async () => { + jobsSummaryMock.mockResolvedValue([ + { + id: 'test-ml-job', + jobState: 'started', + datafeedState: 'started', + }, + ]); + const { dependencies, executor } = createRuleTypeMocks('machine_learning', params); + const mlAlertType = createMlAlertType({ + experimentalFeatures: allowedExperimentalValues, + lists: dependencies.lists, + logger: dependencies.logger, + mergeStrategy: 'allFields', + ml: mlMock, + ruleDataClient: dependencies.ruleDataClient, + ruleDataService: dependencies.ruleDataService, + version: '1.0.0', + }); + + dependencies.alerting.registerType(mlAlertType); + + await executor({ params }); + expect(dependencies.ruleDataClient.getWriter).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts new file mode 100644 index 000000000000..1d872df35de3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; +import { PersistenceServices } from '../../../../../../rule_registry/server'; +import { ML_ALERT_TYPE_ID } from '../../../../../common/constants'; +import { machineLearningRuleParams, MachineLearningRuleParams } from '../../schemas/rule_schemas'; +import { mlExecutor } from '../../signals/executors/ml'; +import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; +import { CreateRuleOptions } from '../types'; + +export const createMlAlertType = (createOptions: CreateRuleOptions) => { + const { lists, logger, mergeStrategy, ml, ruleDataClient, ruleDataService } = createOptions; + const createSecurityRuleType = createSecurityRuleTypeFactory({ + lists, + logger, + mergeStrategy, + ruleDataClient, + ruleDataService, + }); + return createSecurityRuleType({ + id: ML_ALERT_TYPE_ID, + name: 'Machine Learning Rule', + validate: { + params: { + validate: (object: unknown) => { + const [validated, errors] = validateNonExact(object, machineLearningRuleParams); + if (errors != null) { + throw new Error(errors); + } + if (validated == null) { + throw new Error('Validation of rule params failed'); + } + return validated; + }, + }, + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + producer: 'security-solution', + async executor(execOptions) { + const { + runOpts: { + buildRuleMessage, + bulkCreate, + exceptionItems, + listClient, + rule, + tuple, + wrapHits, + }, + services, + state, + } = execOptions; + + const result = await mlExecutor({ + buildRuleMessage, + bulkCreate, + exceptionItems, + listClient, + logger, + ml, + rule, + services, + tuple, + wrapHits, + }); + return { ...result, state }; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_indicator_match.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_indicator_match.sh old mode 100644 new mode 100755 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_ml.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_ml.sh new file mode 100755 index 000000000000..8ce3d56cd017 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_ml.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "anomalyThreshold": 23, + "author": [], + "description": "Basic Machine Learning Rule", + "exceptionsList": [], + "falsePositives": [], + "from": "now-45m", + "immutable": false, + "machineLearningJobId": ["test-ml-job"], + "maxSignals": 101, + "outputIndex": "", + "references": [], + "riskScore": 23, + "riskScoreMapping": [], + "ruleId": "1781d055-5c66-4adf-9c59-fc0fa58336a5", + "severity": "high", + "severityMapping": [], + "threat": [], + "to": "now", + "type": "machine_learning", + "version": 1 + }, + "consumer":"alerts", + "alertTypeId":"siem.mlRule", + "schedule":{ + "interval":"15m" + }, + "actions":[], + "tags":[ + "custom", + "ml", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic Machine Learning Rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index e781bfc50bee..f061240c4a6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -99,7 +99,7 @@ export type CreateSecurityRuleTypeFactory = (options: { ruleDataClient: IRuleDataClient; ruleDataService: IRuleDataPluginService; }) => < - TParams extends RuleParams & { index: string[] | undefined }, + TParams extends RuleParams & { index?: string[] | undefined }, TAlertInstanceContext extends AlertInstanceContext, TServices extends PersistenceServices, TState extends AlertTypeState @@ -124,6 +124,7 @@ export interface CreateRuleOptions { lists: SetupPlugins['lists']; logger: Logger; mergeStrategy: ConfigType['alertMergeStrategy']; + ml?: SetupPlugins['ml']; ruleDataClient: IRuleDataClient; version: string; ruleDataService: IRuleDataPluginService; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 734ccc4d5ba8..040ebb659abc 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -49,6 +49,7 @@ import { ILicense, LicensingPluginStart } from '../../licensing/server'; import { FleetStartContract } from '../../fleet/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { createQueryAlertType } from './lib/detection_engine/rule_types'; +import { createMlAlertType } from './lib/detection_engine/rule_types/ml/create_ml_alert_type'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; @@ -65,6 +66,7 @@ import { QUERY_ALERT_TYPE_ID, DEFAULT_SPACE_ID, INDICATOR_ALERT_TYPE_ID, + ML_ALERT_TYPE_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; @@ -129,6 +131,7 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} + export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config: ConfigType; @@ -246,6 +249,7 @@ export class Plugin implements IPlugin