[RAM][O11Y] Integrate Conditional Actions with several Observability rule types (#159522)

## Summary

Closes #159520 

<img width="573" alt="Screenshot 2023-06-12 at 3 12 27 PM"
src="ec16b8d7-25a5-435c-bf29-7392747b8c0f">


### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
Zacqary Adam Xeper 2023-06-27 11:36:49 -05:00 committed by GitHub
parent 3bd43abd69
commit faadf347ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 111 additions and 11 deletions

View file

@ -72,6 +72,7 @@ type AuthorizedConsumers = Record<string, HasPrivileges>;
export interface RegistryAlertTypeWithAuth extends RegistryRuleType {
authorizedConsumers: AuthorizedConsumers;
hasGetSummarizedAlerts?: boolean;
hasFieldsForAAD?: boolean;
}
type IsAuthorizedAtProducerLevel = boolean;

View file

@ -88,6 +88,7 @@ describe('ruleTypesRoute', () => {
producer: 'test',
enabled_in_license: true,
has_get_summarized_alerts: true,
has_fields_for_a_a_d: false,
},
];
rulesClient.listRuleTypes.mockResolvedValueOnce(new Set(listTypes));
@ -113,6 +114,7 @@ describe('ruleTypesRoute', () => {
"default_schedule_interval": "10m",
"does_set_recovery_context": false,
"enabled_in_license": true,
"has_fields_for_a_a_d": false,
"has_get_summarized_alerts": true,
"id": "1",
"is_exportable": true,

View file

@ -26,6 +26,7 @@ const rewriteBodyRes: RewriteResponseCase<RegistryAlertTypeWithAuth[]> = (result
defaultScheduleInterval,
doesSetRecoveryContext,
hasGetSummarizedAlerts,
hasFieldsForAAD,
...rest
}) => ({
...rest,
@ -41,6 +42,7 @@ const rewriteBodyRes: RewriteResponseCase<RegistryAlertTypeWithAuth[]> = (result
default_schedule_interval: defaultScheduleInterval,
does_set_recovery_context: doesSetRecoveryContext,
has_get_summarized_alerts: !!hasGetSummarizedAlerts,
has_fields_for_a_a_d: !!hasFieldsForAAD,
})
);
};

View file

@ -704,6 +704,7 @@ describe('Create Lifecycle', () => {
"defaultScheduleInterval": undefined,
"doesSetRecoveryContext": false,
"enabledInLicense": false,
"hasFieldsForAAD": false,
"hasGetSummarizedAlerts": false,
"id": "test",
"isExportable": true,

View file

@ -61,6 +61,7 @@ export interface RegistryRuleType
| 'ruleTaskTimeout'
| 'defaultScheduleInterval'
| 'doesSetRecoveryContext'
| 'fieldsForAAD'
> {
id: string;
enabledInLicense: boolean;
@ -372,6 +373,7 @@ export class RuleTypeRegistry {
doesSetRecoveryContext,
alerts,
getSummarizedAlerts,
fieldsForAAD,
},
]: [string, UntypedNormalizedRuleType]) => ({
id,
@ -392,6 +394,7 @@ export class RuleTypeRegistry {
minimumLicenseRequired
).isValid,
hasGetSummarizedAlerts: !!getSummarizedAlerts,
hasFieldsForAAD: Boolean(fieldsForAAD),
...(alerts ? { alerts } : {}),
})
)

View file

@ -49,6 +49,7 @@ import {
WARNING_ACTIONS,
} from './inventory_metric_threshold_executor';
import { MetricsRulesTypeAlertDefinition } from '../register_rule_types';
import { O11Y_AAD_FIELDS } from '../../../../common/constants';
const condition = schema.object({
threshold: schema.arrayOf(schema.number()),
@ -146,5 +147,6 @@ export async function registerMetricInventoryThresholdRuleType(
},
getSummarizedAlerts: libs.metricsRules.createGetSummarizedAlerts(),
alerts: MetricsRulesTypeAlertDefinition,
fieldsForAAD: O11Y_AAD_FIELDS,
});
}

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { PluginSetupContract } from '@kbn/alerting-plugin/server';
import { O11Y_AAD_FIELDS } from '../../../../common/constants';
import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor';
import { extractReferences, injectReferences } from './log_threshold_references_manager';
import {
@ -165,5 +166,6 @@ export async function registerLogThresholdRuleType(
injectReferences,
},
alerts: LogsRulesTypeAlertDefinition,
fieldsForAAD: O11Y_AAD_FIELDS,
});
}

View file

@ -14,6 +14,7 @@ import {
AlertInstanceContext as AlertContext,
} from '@kbn/alerting-plugin/server';
import { RecoveredActionGroupId } from '@kbn/alerting-plugin/common';
import { O11Y_AAD_FIELDS } from '../../../../common/constants';
import {
createMetricAnomalyExecutor,
FIRED_ACTIONS,
@ -67,6 +68,7 @@ export const registerMetricAnomalyRuleType = (
minimumLicenseRequired: 'basic',
isExportable: true,
executor: createMetricAnomalyExecutor(libs, ml),
fieldsForAAD: O11Y_AAD_FIELDS,
actionVariables: {
context: [
{ name: 'alertState', description: alertStateActionVariableDescription },

View file

@ -252,6 +252,7 @@ describe('alert_form', () => {
}
actionTypeRegistry={actionTypeRegistry}
featureId="alerting"
producerId="alerting"
/>
</KibanaReactContext.Provider>
</I18nProvider>

View file

@ -23,6 +23,7 @@ import type {
} from '@kbn/alerting-plugin/common';
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { NOTIFICATION_DEFAULT_FREQUENCY } from '../../../../../common/constants';
import type { FieldHook } from '../../../../shared_imports';
import { useFormContext } from '../../../../shared_imports';
@ -243,13 +244,13 @@ export const RuleActionsField: React.FC<Props> = ({
setActionFrequencyProperty: setActionFrequency,
setActionAlertsFilterProperty,
featureId: SecurityConnectorFeatureId,
producerId: AlertConsumers.SIEM,
defaultActionMessage: FORM_FOR_EACH_ALERT_BODY_MESSAGE,
defaultSummaryMessage: FORM_SUMMARY_BODY_MESSAGE,
hideActionHeader: true,
hasSummary: true,
notifyWhenSelectOptions: NOTIFY_WHEN_OPTIONS,
defaultRuleFrequency: NOTIFICATION_DEFAULT_FREQUENCY,
showActionAlertsFilter: true,
}),
[
actions,

View file

@ -100,6 +100,7 @@ const baseProps = {
recoveryActionGroup: 'recovered',
actionTypeRegistry,
minimumThrottleInterval: [1, 'm'] as [number | undefined, string],
producerId: 'infratstructure',
setActions: jest.fn(),
setActionIdByIndex: jest.fn(),
setActionParamsProperty: jest.fn(),

View file

@ -0,0 +1,28 @@
/*
* 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 { DataViewField } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import useAsync from 'react-use/lib/useAsync';
import type { AsyncState } from 'react-use/lib/useAsync';
import { TriggersAndActionsUiServices } from '../..';
export function useRuleAADFields(ruleTypeId?: string): AsyncState<DataViewField[]> {
const { http } = useKibana<TriggersAndActionsUiServices>().services;
const aadFields = useAsync(async () => {
if (!ruleTypeId) return [];
const fields = await http.get<DataViewField[]>(`${BASE_RAC_ALERTS_API_PATH}/aad_fields`, {
query: { ruleTypeId },
});
return fields;
});
return aadFields;
}

View file

@ -25,6 +25,7 @@ const rewriteBodyReq: RewriteRequestCase<RuleType> = ({
does_set_recovery_context: doesSetRecoveryContext,
default_schedule_interval: defaultScheduleInterval,
has_get_summarized_alerts: hasGetSummarizedAlerts,
has_fields_for_a_a_d: hasFieldsForAAD,
...rest
}: AsApiContract<RuleType>) => ({
enabledInLicense,
@ -38,6 +39,7 @@ const rewriteBodyReq: RewriteRequestCase<RuleType> = ({
doesSetRecoveryContext,
defaultScheduleInterval,
hasGetSummarizedAlerts,
hasFieldsForAAD,
...rest,
});

View file

@ -6,6 +6,7 @@
*/
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { ValidFeatureId } from '@kbn/rule-data-utils';
import { Filter } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { EuiSwitch, EuiSpacer } from '@elastic/eui';
@ -16,11 +17,17 @@ import { AlertsSearchBar } from '../alerts_search_bar';
interface ActionAlertsFilterQueryProps {
state?: AlertsFilter['query'];
onChange: (update?: AlertsFilter['query']) => void;
appName: string;
featureIds: ValidFeatureId[];
ruleTypeId?: string;
}
export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = ({
state,
onChange,
appName,
featureIds,
ruleTypeId,
}) => {
const [query, setQuery] = useState(state ?? { kql: '', filters: [] });
@ -61,7 +68,7 @@ export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = (
label={i18n.translate(
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryToggleLabel',
{
defaultMessage: 'if alert matches a query',
defaultMessage: 'If alert matches a query',
}
)}
checked={queryEnabled}
@ -72,9 +79,10 @@ export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = (
<>
<EuiSpacer size="s" />
<AlertsSearchBar
appName="siem"
appName={appName}
featureIds={featureIds}
ruleTypeId={ruleTypeId}
disableQueryLanguageSwitcher={true}
featureIds={['siem']}
query={query.kql}
filters={query.filters ?? []}
onQueryChange={onQueryChange}

View file

@ -146,7 +146,7 @@ export const ActionAlertsFilterTimeframe: React.FC<ActionAlertsFilterTimeframePr
label={i18n.translate(
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterTimeframeToggleLabel',
{
defaultMessage: 'if alert is generated during timeframe',
defaultMessage: 'If alert is generated during timeframe',
}
)}
checked={timeframeEnabled}

View file

@ -314,6 +314,7 @@ describe('action_form', () => {
context: [{ name: 'contextVar', description: 'context var1' }],
}}
featureId="alerting"
producerId="alerting"
defaultActionGroupId={'default'}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => {
const recoveryActionGroupId = customRecoveredActionGroup

View file

@ -69,6 +69,7 @@ export interface ActionAccordionFormProps {
index: number
) => void;
featureId: string;
producerId: string;
messageVariables?: ActionVariables;
summaryMessageVariables?: ActionVariables;
setHasActionsDisabled?: (value: boolean) => void;
@ -83,7 +84,8 @@ export interface ActionAccordionFormProps {
minimumThrottleInterval?: [number | undefined, string];
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
defaultRuleFrequency?: RuleActionFrequency;
showActionAlertsFilter?: boolean;
ruleTypeId?: string;
hasFieldsForAAD?: boolean;
}
interface ActiveActionConnectorState {
@ -117,7 +119,9 @@ export const ActionForm = ({
minimumThrottleInterval,
notifyWhenSelectOptions,
defaultRuleFrequency = DEFAULT_FREQUENCY,
showActionAlertsFilter,
ruleTypeId,
producerId,
hasFieldsForAAD,
}: ActionAccordionFormProps) => {
const {
http,
@ -491,7 +495,10 @@ export const ActionForm = ({
minimumThrottleInterval={minimumThrottleInterval}
notifyWhenSelectOptions={notifyWhenSelectOptions}
defaultNotifyWhenValue={defaultRuleFrequency.notifyWhen}
showActionAlertsFilter={showActionAlertsFilter}
featureId={featureId}
producerId={producerId}
ruleTypeId={ruleTypeId}
hasFieldsForAAD={hasFieldsForAAD}
/>
);
})}

View file

@ -634,6 +634,8 @@ function getActionTypeForm({
summaryMessageVariables={summaryMessageVariables}
notifyWhenSelectOptions={notifyWhenSelectOptions}
defaultNotifyWhenValue={defaultNotifyWhenValue}
producerId="infrastructure"
featureId="infrastructure"
/>
);
}

View file

@ -8,6 +8,7 @@
import React, { Suspense, useEffect, useState, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ValidFeatureId, AlertConsumers } from '@kbn/rule-data-utils';
import {
EuiFlexGroup,
EuiFlexItem,
@ -87,7 +88,10 @@ export type ActionTypeFormProps = {
minimumThrottleInterval?: [number | undefined, string];
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
defaultNotifyWhenValue?: RuleNotifyWhenType;
showActionAlertsFilter?: boolean;
featureId: string;
producerId: string;
ruleTypeId?: string;
hasFieldsForAAD?: boolean;
} & Pick<
ActionAccordionFormProps,
| 'defaultActionGroupId'
@ -134,7 +138,10 @@ export const ActionTypeForm = ({
minimumThrottleInterval,
notifyWhenSelectOptions,
defaultNotifyWhenValue,
showActionAlertsFilter,
producerId,
featureId,
ruleTypeId,
hasFieldsForAAD,
}: ActionTypeFormProps) => {
const {
application: { capabilities },
@ -333,6 +340,8 @@ export const ActionTypeForm = ({
setActionGroupIdByIndex &&
!actionItem.frequency?.summary;
const showActionAlertsFilter = hasFieldsForAAD || producerId === AlertConsumers.SIEM;
const accordionContent = checkEnabledResult.isEnabled ? (
<>
<EuiSplitPanel.Inner
@ -418,6 +427,9 @@ export const ActionTypeForm = ({
<ActionAlertsFilterQuery
state={actionItem.alertsFilter?.query}
onChange={(query) => setActionAlertsFilterProperty('query', query, index)}
featureIds={[producerId as ValidFeatureId]}
appName={featureId!}
ruleTypeId={ruleTypeId}
/>
<EuiSpacer size="s" />
<ActionAlertsFilterTimeframe

View file

@ -13,12 +13,14 @@ import { SEARCH_BAR_PLACEHOLDER } from './translations';
import { AlertsSearchBarProps, QueryLanguageType } from './types';
import { useAlertDataView } from '../../hooks/use_alert_data_view';
import { TriggersAndActionsUiServices } from '../../..';
import { useRuleAADFields } from '../../hooks/use_rule_aad_fields';
// TODO Share buildEsQuery to be used between AlertsSearchBar and AlertsStateTable component https://github.com/elastic/kibana/issues/144615
export function AlertsSearchBar({
appName,
disableQueryLanguageSwitcher = false,
featureIds,
ruleTypeId,
query,
filters,
onQueryChange,
@ -40,6 +42,14 @@ export function AlertsSearchBar({
const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
const { value: dataView, loading, error } = useAlertDataView(featureIds);
const {
value: aadFields,
loading: fieldsLoading,
error: fieldsError,
} = useRuleAADFields(ruleTypeId);
const indexPatterns =
ruleTypeId && aadFields?.length ? [{ title: ruleTypeId, fields: aadFields }] : [dataView!];
const onSearchQuerySubmit = useCallback(
({ dateRange, query: nextQuery }: { dateRange: TimeRange; query?: Query }) => {
@ -72,7 +82,10 @@ export function AlertsSearchBar({
<SearchBar
appName={appName}
disableQueryLanguageSwitcher={disableQueryLanguageSwitcher}
indexPatterns={loading || error ? NO_INDEX_PATTERNS : [dataView!]}
// @ts-expect-error - DataView fields prop and SearchBar indexPatterns props are overly broad
indexPatterns={
loading || error || fieldsLoading || fieldsError ? NO_INDEX_PATTERNS : indexPatterns
}
placeholder={placeholder}
query={{ query: query ?? '', language: queryLanguage }}
filters={filters}

View file

@ -23,6 +23,7 @@ export interface AlertsSearchBarProps {
showSubmitButton?: boolean;
placeholder?: string;
submitOnBlur?: boolean;
ruleTypeId?: string;
onQueryChange?: (query: {
dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' };
query?: string;

View file

@ -661,6 +661,9 @@ export const RuleForm = ({
defaultActionGroupId={defaultActionGroupId}
hasSummary={selectedRuleType.hasGetSummarizedAlerts}
featureId={connectorFeatureId}
producerId={selectedRuleType.producer}
hasFieldsForAAD={selectedRuleType.hasFieldsForAAD}
ruleTypeId={rule.ruleTypeId}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) =>
isActionGroupDisabledForActionType(selectedRuleType, actionGroupId, actionTypeId)
}

View file

@ -337,6 +337,7 @@ export interface RuleType<
actionVariables: ActionVariables;
authorizedConsumers: Record<string, { read: boolean; all: boolean }>;
enabledInLicense: boolean;
hasFieldsForAAD?: boolean;
hasGetSummarizedAlerts?: boolean;
}

View file

@ -37,6 +37,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) {
name: 'Recovered',
},
enabled_in_license: true,
has_fields_for_a_a_d: false,
has_get_summarized_alerts: false,
rule_task_timeout: '5m',
};
@ -63,6 +64,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) {
minimum_license_required: 'basic',
is_exportable: true,
enabled_in_license: true,
has_fields_for_a_a_d: false,
has_get_summarized_alerts: false,
rule_task_timeout: '5m',
};

View file

@ -45,6 +45,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) {
minimum_license_required: 'basic',
is_exportable: true,
enabled_in_license: true,
has_fields_for_a_a_d: false,
has_get_summarized_alerts: false,
rule_task_timeout: '5m',
});
@ -133,6 +134,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) {
minimumLicenseRequired: 'basic',
isExportable: true,
enabledInLicense: true,
hasFieldsForAAD: false,
hasGetSummarizedAlerts: false,
ruleTaskTimeout: '5m',
});