mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Detections] Display rule warning when action is disabled but rule ran successfully (#182741)
## Summary Ref: https://github.com/elastic/security-team/issues/8699 Co-authored-by: Yara Tercero <yctercero@users.noreply.github.com> Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
This commit is contained in:
parent
454ef7f5a3
commit
00c922fae3
11 changed files with 189 additions and 4 deletions
|
@ -35,6 +35,7 @@ const createSetupMock = () => {
|
|||
getActionsHealth: jest.fn(),
|
||||
getActionsConfigurationUtilities: jest.fn(),
|
||||
setEnabledConnectorTypes: jest.fn(),
|
||||
isActionTypeEnabled: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -420,6 +420,43 @@ describe('Actions Plugin', () => {
|
|||
expect(pluginStart.isActionTypeEnabled('.slack')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set connector type enabled and check isActionTypeEnabled with plugin setup method', async () => {
|
||||
setup(getConfig());
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
|
||||
pluginSetup.registerType({
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
minimumLicenseRequired: 'basic',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
executor,
|
||||
});
|
||||
pluginSetup.registerType({
|
||||
id: '.slack',
|
||||
name: 'Slack',
|
||||
minimumLicenseRequired: 'gold',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
executor,
|
||||
});
|
||||
pluginSetup.setEnabledConnectorTypes(['.server-log']);
|
||||
|
||||
// checking isActionTypeEnabled via plugin setup, not plugin start
|
||||
expect(pluginSetup.isActionTypeEnabled('.server-log')).toBeTruthy();
|
||||
expect(pluginSetup.isActionTypeEnabled('.slack')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set all the connector types enabled when null or ["*"] passed', async () => {
|
||||
setup(getConfig());
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
|
|
|
@ -141,6 +141,7 @@ export interface PluginSetupContract {
|
|||
getActionsHealth: () => { hasPermanentEncryptionKey: boolean };
|
||||
getActionsConfigurationUtilities: () => ActionsConfigurationUtilities;
|
||||
setEnabledConnectorTypes: (connectorTypes: EnabledConnectorTypes) => void;
|
||||
isActionTypeEnabled(id: string, options?: { notifyUsage: boolean }): boolean;
|
||||
}
|
||||
|
||||
export interface PluginStartContract {
|
||||
|
@ -403,6 +404,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
);
|
||||
}
|
||||
},
|
||||
isActionTypeEnabled: (id, options = { notifyUsage: false }) => {
|
||||
return this.actionTypeRegistry!.isActionTypeEnabled(id, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import { mlPluginServerMock } from '@kbn/ml-plugin/server/mocks';
|
|||
import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server';
|
||||
import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks';
|
||||
import { eventLogServiceMock } from '@kbn/event-log-plugin/server/mocks';
|
||||
import type { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server';
|
||||
import type { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alerting-plugin/server';
|
||||
import type { ConfigType } from '../../../../config';
|
||||
import type { AlertAttributes } from '../types';
|
||||
|
@ -49,6 +50,21 @@ export const createRuleTypeMocks = (
|
|||
getConfig: () => ({ run: { alerts: { max: DEFAULT_MAX_ALERTS } } }),
|
||||
} as AlertingPluginSetupContract;
|
||||
|
||||
const actions = {
|
||||
registerType: jest.fn(),
|
||||
|
||||
registerSubActionConnectorType: jest.fn(),
|
||||
|
||||
isPreconfiguredConnector: (connectorId: string) => false,
|
||||
|
||||
getSubActionConnectorClass: jest.fn(),
|
||||
getCaseConnectorClass: jest.fn(),
|
||||
getActionsHealth: jest.fn(),
|
||||
getActionsConfigurationUtilities: jest.fn(),
|
||||
setEnabledConnectorTypes: jest.fn(),
|
||||
isActionTypeEnabled: () => true,
|
||||
} as ActionsPluginSetupContract;
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
|
||||
const mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
|
@ -92,6 +108,7 @@ export const createRuleTypeMocks = (
|
|||
|
||||
return {
|
||||
dependencies: {
|
||||
actions,
|
||||
alerting,
|
||||
config$: mockedConfig$,
|
||||
lists: listMock.createSetup(),
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
hasTimestampFields,
|
||||
isMachineLearningParams,
|
||||
isEsqlParams,
|
||||
getDisabledActionsWarningText,
|
||||
} from './utils/utils';
|
||||
import { DEFAULT_MAX_SIGNALS, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants';
|
||||
import type { CreateSecurityRuleTypeWrapper } from './types';
|
||||
|
@ -67,6 +68,7 @@ export const securityRuleTypeFieldMap = {
|
|||
export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
||||
({
|
||||
lists,
|
||||
actions,
|
||||
logger,
|
||||
config,
|
||||
publicBaseUrl,
|
||||
|
@ -472,6 +474,28 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
|||
};
|
||||
}
|
||||
|
||||
const disabledActions = rule.actions.filter(
|
||||
(action) => !actions.isActionTypeEnabled(action.actionTypeId)
|
||||
);
|
||||
|
||||
const createdSignalsCount = result.createdSignals.length;
|
||||
|
||||
if (disabledActions.length > 0) {
|
||||
const disabledActionsWarning = getDisabledActionsWarningText({
|
||||
alertsCreated: createdSignalsCount > 0,
|
||||
disabledActions,
|
||||
});
|
||||
if (result.warningMessages.length) {
|
||||
result.warningMessages.push(disabledActionsWarning);
|
||||
} else {
|
||||
warningMessage = [
|
||||
...(warningMessage ? [warningMessage] : []),
|
||||
disabledActionsWarning,
|
||||
].join(', ');
|
||||
wroteWarningStatus = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.warningMessages.length) {
|
||||
await ruleExecutionLogger.logStatusChange({
|
||||
newStatus: RuleExecutionStatusEnum['partial failure'],
|
||||
|
@ -484,8 +508,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
|||
});
|
||||
}
|
||||
|
||||
const createdSignalsCount = result.createdSignals.length;
|
||||
|
||||
if (result.success) {
|
||||
ruleExecutionLogger.debug('Security Rule execution completed');
|
||||
ruleExecutionLogger.debug(
|
||||
|
|
|
@ -37,8 +37,9 @@ describe('Custom Query Alerts', () => {
|
|||
const publicBaseUrl = 'http://somekibanabaseurl.com';
|
||||
|
||||
const { dependencies, executor, services } = mocks;
|
||||
const { alerting, lists, logger, ruleDataClient } = dependencies;
|
||||
const { actions, alerting, lists, logger, ruleDataClient } = dependencies;
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
actions,
|
||||
lists,
|
||||
logger,
|
||||
config: createMockConfig(),
|
||||
|
|
|
@ -129,6 +129,7 @@ export type SecurityAlertType<
|
|||
|
||||
export interface CreateSecurityRuleTypeWrapperProps {
|
||||
lists: SetupPlugins['lists'];
|
||||
actions: SetupPlugins['actions'];
|
||||
logger: Logger;
|
||||
config: ConfigType;
|
||||
publicBaseUrl: string | undefined;
|
||||
|
|
|
@ -11,6 +11,8 @@ import type { TransportResult } from '@elastic/elasticsearch';
|
|||
import { ALERT_REASON, ALERT_RULE_PARAMETERS, ALERT_UUID, TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common';
|
||||
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { listMock } from '@kbn/lists-plugin/server/mocks';
|
||||
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
|
||||
|
@ -45,6 +47,7 @@ import {
|
|||
getField,
|
||||
addToSearchAfterReturn,
|
||||
getUnprocessedExceptionsWarnings,
|
||||
getDisabledActionsWarningText,
|
||||
} from './utils';
|
||||
import type { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from '../types';
|
||||
import {
|
||||
|
@ -1751,4 +1754,74 @@ describe('utils', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDisabledActionsWarningText', () => {
|
||||
const alertsCreated = true;
|
||||
const alertsNotCreated = false;
|
||||
|
||||
const singleDisabledAction = [{ actionTypeId: '.webhook' }];
|
||||
const multipleDisabledActionsDiffType = [
|
||||
{ actionTypeId: '.webhook' },
|
||||
{ actionTypeId: '.pagerduty' },
|
||||
];
|
||||
const multipleDisabledActionsSameType = [
|
||||
{ actionTypeId: '.webhook' },
|
||||
{ actionTypeId: '.webhook' },
|
||||
];
|
||||
test('returns string for single disabled action with alerts generated', () => {
|
||||
const warning = getDisabledActionsWarningText({
|
||||
alertsCreated,
|
||||
disabledActions: singleDisabledAction as SanitizedRuleAction[],
|
||||
});
|
||||
expect(warning).toEqual(
|
||||
'This rule generated alerts but did not send external notifications because rule action connector .webhook is not enabled. To send notifications, you need a higher Security Analytics license / tier'
|
||||
);
|
||||
});
|
||||
test('returns string for single disabled action with no alerts generated', () => {
|
||||
const warning = getDisabledActionsWarningText({
|
||||
alertsCreated: alertsNotCreated,
|
||||
disabledActions: singleDisabledAction as SanitizedRuleAction[],
|
||||
});
|
||||
expect(warning).toEqual(
|
||||
'Rule action connector .webhook is not enabled. To send notifications, you need a higher Security Analytics license / tier'
|
||||
);
|
||||
});
|
||||
test('returns string for multiple distinct disabled action types with alerts generated', () => {
|
||||
const warning = getDisabledActionsWarningText({
|
||||
alertsCreated,
|
||||
disabledActions: multipleDisabledActionsDiffType as SanitizedRuleAction[],
|
||||
});
|
||||
expect(warning).toEqual(
|
||||
'This rule generated alerts but did not send external notifications because rule action connectors .webhook, .pagerduty are not enabled. To send notifications, you need a higher Security Analytics license / tier'
|
||||
);
|
||||
});
|
||||
test('returns string for multiple distinct disabled action types with alerts NOT generated', () => {
|
||||
const warning = getDisabledActionsWarningText({
|
||||
alertsCreated: alertsNotCreated,
|
||||
disabledActions: multipleDisabledActionsDiffType as SanitizedRuleAction[],
|
||||
});
|
||||
expect(warning).toEqual(
|
||||
'Rule action connectors .webhook, .pagerduty are not enabled. To send notifications, you need a higher Security Analytics license / tier'
|
||||
);
|
||||
});
|
||||
test('returns string for multiple same type disabled action types with alerts generated', () => {
|
||||
const warning = getDisabledActionsWarningText({
|
||||
alertsCreated,
|
||||
disabledActions: multipleDisabledActionsSameType as SanitizedRuleAction[],
|
||||
});
|
||||
expect(warning).toEqual(
|
||||
'This rule generated alerts but did not send external notifications because rule action connector .webhook is not enabled. To send notifications, you need a higher Security Analytics license / tier'
|
||||
);
|
||||
});
|
||||
|
||||
test('returns string for multiple same type disabled action types with alerts NOT generated', () => {
|
||||
const warning = getDisabledActionsWarningText({
|
||||
alertsCreated: alertsNotCreated,
|
||||
disabledActions: multipleDisabledActionsSameType as SanitizedRuleAction[],
|
||||
});
|
||||
expect(warning).toEqual(
|
||||
'Rule action connector .webhook is not enabled. To send notifications, you need a higher Security Analytics license / tier'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ import type {
|
|||
} from '@kbn/alerting-plugin/server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/server';
|
||||
import type { ExceptionListClient, ListClient, ListPluginSetup } from '@kbn/lists-plugin/server';
|
||||
import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common';
|
||||
import type { TimestampOverride } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { Privilege } from '../../../../../common/api/detection_engine';
|
||||
import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
|
||||
|
@ -1006,3 +1007,26 @@ export const getMaxSignalsWarning = (): string => {
|
|||
export const getSuppressionMaxSignalsWarning = (): string => {
|
||||
return `This rule reached the maximum alert limit for the rule execution. Some alerts were not created or suppressed.`;
|
||||
};
|
||||
|
||||
export const getDisabledActionsWarningText = ({
|
||||
alertsCreated,
|
||||
disabledActions,
|
||||
}: {
|
||||
alertsCreated: boolean;
|
||||
disabledActions: SanitizedRuleAction[];
|
||||
}) => {
|
||||
const uniqueActionTypes = new Set(disabledActions.map((action) => action.actionTypeId));
|
||||
|
||||
const actionTypesJoined = [...uniqueActionTypes].join(', ');
|
||||
|
||||
// This rule generated alerts but did not send external notifications because rule action connectors ${actionTypes} aren't enabled. To send notifications, you need a higher Security Analytics tier.
|
||||
const alertsGeneratedText = alertsCreated
|
||||
? 'This rule generated alerts but did not send external notifications because rule action'
|
||||
: 'Rule action';
|
||||
|
||||
if (uniqueActionTypes.size > 1) {
|
||||
return `${alertsGeneratedText} connectors ${actionTypesJoined} are not enabled. To send notifications, you need a higher Security Analytics license / tier`;
|
||||
} else {
|
||||
return `${alertsGeneratedText} connector ${actionTypesJoined} is not enabled. To send notifications, you need a higher Security Analytics license / tier`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -300,6 +300,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
|
||||
const securityRuleTypeOptions = {
|
||||
lists: plugins.lists,
|
||||
actions: plugins.actions,
|
||||
logger: this.logger,
|
||||
config: this.config,
|
||||
publicBaseUrl: core.http.basePath.publicBaseUrl,
|
||||
|
|
|
@ -16,6 +16,10 @@ import type {
|
|||
PluginSetupContract as AlertingPluginSetup,
|
||||
PluginStartContract as AlertingPluginStart,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import type {
|
||||
PluginSetupContract as ActionsPluginSetup,
|
||||
PluginStartContract as ActionsPluginStartContract,
|
||||
} from '@kbn/actions-plugin/server';
|
||||
import type { CasesServerStart, CasesServerSetup } from '@kbn/cases-plugin/server';
|
||||
import type { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import type { IEventLogClientService, IEventLogService } from '@kbn/event-log-plugin/server';
|
||||
|
@ -42,12 +46,12 @@ import type { SharePluginStart } from '@kbn/share-plugin/server';
|
|||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server';
|
||||
import type { ElasticAssistantPluginStart } from '@kbn/elastic-assistant-plugin/server';
|
||||
import type { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import type { ProductFeaturesService } from './lib/product_features_service/product_features_service';
|
||||
import type { ExperimentalFeatures } from '../common';
|
||||
|
||||
export interface SecuritySolutionPluginSetupDependencies {
|
||||
alerting: AlertingPluginSetup;
|
||||
actions: ActionsPluginSetup;
|
||||
cases: CasesServerSetup;
|
||||
cloud: CloudSetup;
|
||||
data: DataPluginSetup;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue