diff --git a/public/pages/Correlations/components/ExperimentalBanner.tsx b/public/pages/Correlations/components/ExperimentalBanner.tsx new file mode 100644 index 00000000..8e5d7dc3 --- /dev/null +++ b/public/pages/Correlations/components/ExperimentalBanner.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; + +export const ExperimentalBanner = () => { + return ( + <> + +

+ The feature is experimental and should not be used in a production environment. Any index + patterns, visualization, and observability panels will be impacted if the feature is + deactivated. For more information see  + + Security Analytics documentation + + . To leave feedback, visit  + + forum.opensearch.org + +

+
+ + + ); +}; \ No newline at end of file diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index 7a23b751..a9b09807 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -42,16 +42,19 @@ import { CorrelationRuleQuery, DataSourceProps, } from '../../../../types'; -import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; +import { BREADCRUMBS, NOTIFICATIONS_HREF, OS_NOTIFICATION_PLUGIN, ROUTES } from '../../../utils/constants'; import { CoreServicesContext } from '../../../components/core_services'; import { RouteComponentProps, useParams } from 'react-router-dom'; import { validateName } from '../../../utils/validation'; import { FieldMappingService, IndexService } from '../../../services'; -import { errorNotificationToast, getDataSources, getLogTypeOptions } from '../../../utils/helpers'; +import { errorNotificationToast, getDataSources, getLogTypeOptions, getPlugins } from '../../../utils/helpers'; import { severityOptions } from '../../../pages/Alerts/utils/constants'; import _ from 'lodash'; -import { NotificationChannelTypeOptions } from '../components/ConfigureAlerts/models/interfaces'; -import AlertCondition from '../components/ConfigureAlerts/components/AlertCondition'; +import { NotificationChannelOption, NotificationChannelTypeOptions } from '../components/ConfigureAlerts/models/interfaces'; +import { getEmptyAlertCondition, getNotificationChannels, parseAlertSeverityToOption, parseNotificationChannelsToOptions } from '../components/ConfigureAlerts/utils/helpers'; +import { NotificationsCallOut } from '../../../../public/components/NotificationsCallOut'; +import { BrowserServices } from '../../../../public/models/interfaces'; +import { ExperimentalBanner } from '../components/ExperimentalBanner'; export interface CreateCorrelationRuleProps extends DataSourceProps { indexService: IndexService; @@ -62,11 +65,7 @@ export interface CreateCorrelationRuleProps extends DataSourceProps { { rule: CorrelationRuleModel; isReadOnly: boolean } >['history']; notifications: NotificationsStart | null; - alertCondition: AlertCondition; - allNotificationChannels: NotificationChannelTypeOptions[]; - hasNotificationPlugin: boolean; - loadingNotifications: boolean; - refreshNotificationChannels: () => void; + services: BrowserServices; } export interface CorrelationOption { @@ -116,12 +115,15 @@ export const CreateCorrelationRule: React.FC = ( ...correlationRuleStateDefaultValue, }); const [action, setAction] = useState('Create'); + const [hasNotificationPlugin, setHasNotificationPlugin] = useState(false); + const [loadingNotifications, setLoadingNotifications] = useState(true); + const [notificationChannels, setNotificationChannels] = useState([]); const [logTypeOptions, setLogTypeOptions] = useState([]); const [period, setPeriod] = useState({ interval: 1, unit: 'MINUTES' }); const [dataFilterEnabled, setDataFilterEnabled] = useState(false); const [groupByEnabled, setGroupByEnabled] = useState(false); const [showForm, setShowForm] = useState(false); - const [showNotificationDetails, setShowNotificationDetails] = useState(true); + const [showNotificationDetails, setShowNotificationDetails] = useState(false); const resetForm = useRef(false); const onRuleSeverityChange = (selectedOptions: Array>) => { @@ -214,56 +216,20 @@ export const CreateCorrelationRule: React.FC = ( [dataFilterEnabled, groupByEnabled] ); - const channelId = alertCondition.actions[0].destination_id; - const selectedNotificationChannelOption: NotificationChannelOption[] = []; - if (channelId) { - allNotificationChannels.forEach((typeOption) => { - const matchingChannel = typeOption.options.find((option) => option.value === channelId); - if (matchingChannel) selectedNotificationChannelOption.push(matchingChannel); - }); - } - - onNotificationChannelsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { - const { - alertCondition, - onAlertTriggerChanged, - detector, - detector: { triggers }, - indexNum, - } = this.props; - - const actions = alertCondition.actions; - if (selectedOptions.length > 0) { - actions[0].destination_id = selectedOptions[0].value!; - } else { - actions[0].destination_id = ''; + useEffect(() => { + const setInitalNotificationValues = async () => { + const { services } = props; + const plugins = await getPlugins(services.opensearchService); + if (plugins) { + setHasNotificationPlugin(plugins.includes(OS_NOTIFICATION_PLUGIN)); } - - triggers.splice(indexNum, 1, { - ...alertCondition, - actions: actions, - }); - onAlertTriggerChanged({ ...detector, triggers: triggers }); - }; - - onMessageSubjectChange = (subject: string, emitMetrics: boolean = true) => { - const { - alertCondition: { actions }, - } = this.props; - actions[0].name = subject; - actions[0].subject_template.source = subject; - this.updateTrigger({ actions: actions }, emitMetrics); }; - - onMessageBodyChange = (message: string, emitMetrics: boolean = true) => { - const { - alertCondition: { actions }, - } = this.props; - actions[0].message_template.source = message; - this.updateTrigger({ actions: actions }, emitMetrics); - }; - - useEffect(() => { + const setNotificationChannelValues = async () => { + const channels = await getNotificationChannels(props.services.notificationsService); + const parsedChannels = parseNotificationChannelsToOptions(channels); + setNotificationChannels(parsedChannels); + setLoadingNotifications(false); + } if (props.history.location.state?.rule) { setAction('Edit'); setInitialValues(props.history.location.state?.rule); @@ -274,16 +240,17 @@ export const CreateCorrelationRule: React.FC = ( setInitialValues(ruleRes); } }; - + setAction('Edit'); setInitialRuleValues(); } - const setupLogTypeOptions = async () => { const options = await getLogTypeOptions(); setLogTypeOptions(options); }; setupLogTypeOptions(); + setInitalNotificationValues(); + setNotificationChannelValues(); resetForm.current = true; }, [props.dataSource]); @@ -298,6 +265,84 @@ export const CreateCorrelationRule: React.FC = ( }); }, [initialValues]); + + const onNotificationChannelsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { + const newActions = initialValues.trigger.actions; + if (selectedOptions.length > 0) { + newActions[0].destination_id = selectedOptions[0].value!; + } else { + newActions[0].destination_id = ''; + } + setInitialValues(prevState => ({ + ...prevState, + trigger: { + ...prevState.trigger, + newActions, + }, + })); + }; + + const onMessageSubjectChange = (subject: string) => { + const actions = initialValues.trigger.actions; + actions[0].name = subject; + actions[0].subject_template.source = subject; + }; + + const onMessageBodyChange = (message: string) => { + const actions = initialValues.trigger.actions; + actions[0].message_template.source = message; + }; + + const prepareMessage = (updateMessage: boolean = false, onMount: boolean = false) => { + const alertCondition = initialValues.trigger; + const lineBreak = '\n'; + const lineBreakAndTab = '\n\t'; + + const alertConditionName = `Triggered alert condition: ${alertCondition.name}`; + const alertConditionSeverity = `Severity: ${ + parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity + }`; + const correlationRuleName = `Correlation Rule name: ${initialValues.name}`; + const defaultSubject = [alertConditionName, alertConditionSeverity, correlationRuleName].join(' - '); + + if (updateMessage || !alertCondition.actions[0]?.subject_template.source) + onMessageSubjectChange(defaultSubject); + + if (updateMessage || !alertCondition.actions[0]?.message_template.source) { + const selectedNames = alertCondition.ids; + const corrRuleQueries = `Detector data sources:${lineBreakAndTab}${initialValues.queries.join( + `,${lineBreakAndTab}` + )}`; + const ruleNames = `Rule Names:${lineBreakAndTab}${selectedNames.join(`,${lineBreakAndTab}`)}`; + const ruleSeverities = `Rule Severities:${lineBreakAndTab}${alertCondition.sev_levels.join( + `,${lineBreakAndTab}` + )}`; + + const alertConditionSelections = []; + if (selectedNames.length) { + alertConditionSelections.push(ruleNames); + alertConditionSelections.push(lineBreak); + } + if (alertCondition.sev_levels.length) { + alertConditionSelections.push(ruleSeverities); + alertConditionSelections.push(lineBreak); + } + + const alertConditionDetails = [ + alertConditionName, + alertConditionSeverity, + correlationRuleName, + corrRuleQueries, + ]; + let defaultMessageBody = alertConditionDetails.join(lineBreak); + if (alertConditionSelections.length) + defaultMessageBody = + defaultMessageBody + lineBreak + lineBreak + alertConditionSelections.join(lineBreak); + onMessageBodyChange(defaultMessageBody); + } + }; + + const submit = async (values: CorrelationRuleModel) => { let error; if ((error = validateCorrelationRule(values))) { @@ -767,6 +812,18 @@ export const CreateCorrelationRule: React.FC = ( ]); }, []); + const alertCondition = initialValues.trigger; + alertCondition.actions = getEmptyAlertCondition().actions; + const channelId = alertCondition.actions[0].destination_id; + const selectedNotificationChannelOption: NotificationChannelOption[] = []; + if (channelId) { + notificationChannels.forEach((typeOption) => { + const matchingChannel = typeOption.options.find((option) => option.value === channelId); + if (matchingChannel) selectedNotificationChannelOption.push(matchingChannel); + }); + console.log("Heyyyas ", channelId); + } + return ( <> @@ -918,12 +975,13 @@ export const CreateCorrelationRule: React.FC = ( > {createForm(queries, touched, errors, props)} + -

Alert Trigger [Experimental]

+

Alert Trigger

@@ -997,7 +1055,7 @@ export const CreateCorrelationRule: React.FC = ( {setShowNotificationDetails(e.target.checked)}} + onChange={(e) => { setShowNotificationDetails(e.target.checked) }} /> @@ -1016,13 +1074,12 @@ export const CreateCorrelationRule: React.FC = ( placeholder={'Select notification channel.'} async={true} isLoading={loadingNotifications} - options={allNotificationChannels as EuiComboBoxOptionOption[]} + options={notificationChannels as EuiComboBoxOptionOption[]} selectedOptions={ selectedNotificationChannelOption as EuiComboBoxOptionOption[] } onChange={onNotificationChannelsChange} singleSelection={{ asPlainText: true }} - onFocus={refreshNotificationChannels} isDisabled={!hasNotificationPlugin} /> @@ -1049,7 +1106,7 @@ export const CreateCorrelationRule: React.FC = (

Notification message

@@ -1070,8 +1127,8 @@ export const CreateCorrelationRule: React.FC = ( > this.onMessageSubjectChange(e.target.value)} + value={initialValues.trigger.actions[0]?.subject_template.source} + onChange={(e) => onMessageSubjectChange(e.target.value)} required={true} fullWidth={true} /> @@ -1089,8 +1146,8 @@ export const CreateCorrelationRule: React.FC = ( > this.onMessageBodyChange(e.target.value)} + value={initialValues.trigger.actions[0]?.message_template.source} + onChange={(e) => onMessageBodyChange(e.target.value)} required={true} fullWidth={true} /> @@ -1099,7 +1156,7 @@ export const CreateCorrelationRule: React.FC = ( - this.prepareMessage(true)}> + prepareMessage(true)}> Generate message @@ -1110,36 +1167,35 @@ export const CreateCorrelationRule: React.FC = ( )} - - + )} -
+ { - action === 'Create' || action === 'Edit' ? ( - <> - - - - Cancel - - - { - props.handleSubmit(); - }} - fill={true} - > - {action === 'Edit' ? 'Update' : 'Create '} correlation rule - - - - - ) : null - } + action === 'Create' || action === 'Edit' ? ( + <> + + + + Cancel + + + { + props.handleSubmit(); + }} + fill={true} + > + {action === 'Edit' ? 'Update' : 'Create '} correlation rule + + + + + ) : null + } - ); + ); }} - + ); }; diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index f66b7141..2202669d 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -58,6 +58,7 @@ import { DataSourceMenuWrapper } from '../../components/MDS/DataSourceMenuWrappe import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; import { DataSourceContext, DataSourceContextConsumer } from '../../services/DataSourceContext'; import { dataSourceInfo } from '../../services/utils/constants'; +import { getPlugins } from '../../utils/helpers'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -389,7 +390,6 @@ export default class Main extends Component { {(saContext: SecurityAnalyticsContextType | null) => { const services = saContext?.services; const metrics = saContext?.metrics!; - return ( @@ -649,6 +649,7 @@ export default class Main extends Component { fieldMappingService={services?.fieldMappingService} notifications={core?.notifications} dataSource={selectedDataSource} + services={services} /> )} /> @@ -661,6 +662,7 @@ export default class Main extends Component { fieldMappingService={services?.fieldMappingService} notifications={core?.notifications} dataSource={selectedDataSource} + services={services} /> )} />