diff --git a/common/constants.ts b/common/constants.ts index 059fb806d..85daac89d 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -4,3 +4,15 @@ */ export const DEFAULT_RULE_UUID = '25b9c01c-350d-4b95-bed1-836d04a4f324'; + +export enum ThreatIntelIocType { + IPAddress = 'ip', + Domain = 'domain', + FileHash = 'hash', +} + +export const IocLabel: { [k in ThreatIntelIocType]: string } = { + [ThreatIntelIocType.IPAddress]: 'IP-Address', + [ThreatIntelIocType.Domain]: 'Domains', + [ThreatIntelIocType.FileHash]: 'File hash', +}; diff --git a/cypress/integration/3_alerts.spec.js b/cypress/integration/3_alerts.spec.js index dc40577d6..88a42235a 100644 --- a/cypress/integration/3_alerts.spec.js +++ b/cypress/integration/3_alerts.spec.js @@ -60,8 +60,10 @@ describe('Alerts', () => { }); it('are generated', () => { + setupIntercept(cy, '/_security_analytics/alerts', 'getAlerts', 'GET'); // Refresh the table cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').click({ force: true }); + cy.wait('@getAlerts').should('have.property', 'state', 'Complete'); cy.wait(10000); diff --git a/public/components/Notifications/NotificationForm.tsx b/public/components/Notifications/NotificationForm.tsx new file mode 100644 index 000000000..5d01da2f5 --- /dev/null +++ b/public/components/Notifications/NotificationForm.tsx @@ -0,0 +1,185 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiAccordion, + EuiButton, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTextArea, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { NOTIFICATIONS_HREF } from '../../utils/constants'; +import { NotificationsCallOut } from '../NotificationsCallOut'; +import { + NotificationChannelOption, + NotificationChannelTypeOptions, + TriggerAction, +} from '../../../types'; +import { getIsNotificationPluginInstalled } from '../../utils/helpers'; + +export interface NotificationFormProps { + allNotificationChannels: NotificationChannelTypeOptions[]; + loadingNotifications: boolean; + action: TriggerAction; + prepareMessage: (updateMessage?: boolean, onMount?: boolean) => void; + refreshNotificationChannels: () => void; + onChannelsChange: (selectedOptions: EuiComboBoxOptionOption[]) => void; + onMessageBodyChange: (message: string) => void; + onMessageSubjectChange: (subject: string) => void; +} + +export const NotificationForm: React.FC = ({ + action, + allNotificationChannels, + loadingNotifications, + prepareMessage, + refreshNotificationChannels, + onChannelsChange, + onMessageBodyChange, + onMessageSubjectChange, +}) => { + const hasNotificationPlugin = getIsNotificationPluginInstalled(); + const [showNotificationDetails, setShowNotificationDetails] = useState(true); + const selectedNotificationChannelOption: NotificationChannelOption[] = []; + if (action.destination_id) { + allNotificationChannels.forEach((typeOption) => { + const matchingChannel = typeOption.options.find( + (option) => option.value === action.destination_id + ); + if (matchingChannel) selectedNotificationChannelOption.push(matchingChannel); + }); + } + + return ( + <> + setShowNotificationDetails(e.target.checked)} + /> + + {showNotificationDetails && ( + <> + + + +

Notification channel

+ + } + > + []} + selectedOptions={ + selectedNotificationChannelOption as EuiComboBoxOptionOption[] + } + onChange={onChannelsChange} + singleSelection={{ asPlainText: true }} + onFocus={refreshNotificationChannels} + isDisabled={!hasNotificationPlugin} + /> +
+
+ + + Manage channels + + +
+ + {!hasNotificationPlugin && ( + <> + + + + )} + + + + +

Notification message

+ + } + paddingSize={'l'} + initialIsOpen={false} + > + + + +

Message subject

+ + } + fullWidth={true} + > + onMessageSubjectChange(e.target.value)} + required={true} + fullWidth={true} + /> +
+
+ + + +

Message body

+ + } + fullWidth={true} + > + onMessageBodyChange(e.target.value)} + required={true} + fullWidth={true} + /> +
+
+ + + + prepareMessage(true /* updateMessage */)} + > + Generate message + + + +
+
+ + + + )} + + ); +}; diff --git a/public/components/Utility/DescriptionGroup.tsx b/public/components/Utility/DescriptionGroup.tsx new file mode 100644 index 000000000..ef5a71875 --- /dev/null +++ b/public/components/Utility/DescriptionGroup.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiDescriptionList, + EuiFlexGroup, + EuiFlexGroupProps, + EuiFlexItem, + EuiFlexItemProps, +} from '@elastic/eui'; +import React from 'react'; + +export interface DescriptionGroupProps { + listItems: { title: NonNullable; description: NonNullable }[]; + itemProps?: Pick; + groupProps?: Pick; +} + +export const DescriptionGroup: React.FC = ({ + listItems, + itemProps, + groupProps, +}) => { + return ( + + {listItems.map((item, idx) => ( + + + + ))} + + ); +}; diff --git a/public/components/Utility/StatusWithIndicator.tsx b/public/components/Utility/StatusWithIndicator.tsx new file mode 100644 index 000000000..4883c815d --- /dev/null +++ b/public/components/Utility/StatusWithIndicator.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; + +export interface StatusWithIndicatorProps { + text: string; + indicatorColor: 'success' | 'text'; +} + +export const StatusWithIndicator: React.FC = ({ + text, + indicatorColor, +}) => { + return ( + + {text} + + ); +}; diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index 0601efe44..70ce57263 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -18,6 +18,7 @@ import { } from '../services'; import CorrelationService from '../services/CorrelationService'; import MetricsService from '../services/MetricsService'; +import ThreatIntelService from '../services/ThreatIntelService'; export interface BrowserServices { detectorsService: DetectorsService; @@ -33,6 +34,7 @@ export interface BrowserServices { indexPatternsService: IndexPatternsService; logTypeService: LogTypeService; metricsService: MetricsService; + threatIntelService: ThreatIntelService; } export interface RuleOptions { diff --git a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap index a8526e78b..1d122df44 100644 --- a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap +++ b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap @@ -43,6 +43,7 @@ exports[` spec renders the component 1`] = ` findingService={ FindingsService { "getFindings": [Function], + "getThreatIntelFindings": [Function], "httpClient": [MockFunction], } } @@ -124,6 +125,7 @@ exports[` spec renders the component 1`] = ` findingService={ FindingsService { "getFindings": [Function], + "getThreatIntelFindings": [Function], "httpClient": [MockFunction], } } diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index 17e5f1556..4b3cfe273 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -47,7 +47,7 @@ import { CoreServicesContext } from '../../../components/core_services'; import { RouteComponentProps, useParams } from 'react-router-dom'; import { validateName } from '../../../utils/validation'; import { FieldMappingService, IndexService, OpenSearchService, NotificationsService } from '../../../services'; -import { errorNotificationToast, getDataSources, getLogTypeOptions, getPlugins } from '../../../utils/helpers'; +import { errorNotificationToast, getDataSources, getFieldsForIndex, getLogTypeOptions, getPlugins } from '../../../utils/helpers'; import { severityOptions } from '../../../pages/Alerts/utils/constants'; import _ from 'lodash'; import { NotificationChannelOption, NotificationChannelTypeOptions } from '../../CreateDetector/components/ConfigureAlerts/models/interfaces'; @@ -402,24 +402,7 @@ export const CreateCorrelationRule: React.FC = ( const getLogFields = useCallback( async (indexName: string) => { - let fields: { - label: string; - value: string; - }[] = []; - - if (indexName) { - const result = await props.indexService.getIndexFields(indexName); - if (result?.ok) { - fields = result.response?.map((field) => ({ - label: field, - value: field, - })); - } - - return fields; - } - - return fields; + return getFieldsForIndex(props.indexService, indexName); }, [props.indexService.getIndexFields] ); diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx index 22608e617..e7192d92d 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx @@ -7,18 +7,13 @@ import React, { ChangeEvent, Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiAccordion, - EuiButton, EuiCheckbox, EuiComboBox, EuiComboBoxOptionOption, EuiFieldText, - EuiFlexGroup, - EuiFlexItem, EuiFormRow, EuiSpacer, - EuiSwitch, EuiText, - EuiTextArea, EuiTitle, } from '@elastic/eui'; import { AlertCondition } from '../../../../../../../models/interfaces'; @@ -29,11 +24,13 @@ import { } from '../../utils/helpers'; import { ALERT_SEVERITY_OPTIONS } from '../../utils/constants'; import { CreateDetectorRulesOptions } from '../../../../../../models/types'; -import { NotificationChannelOption, NotificationChannelTypeOptions } from '../../models/interfaces'; -import { NOTIFICATIONS_HREF } from '../../../../../../utils/constants'; import { getNameErrorMessage, validateName } from '../../../../../../utils/validation'; -import { NotificationsCallOut } from '../../../../../../components/NotificationsCallOut'; -import { Detector } from '../../../../../../../types'; +import { + Detector, + NotificationChannelOption, + NotificationChannelTypeOptions, +} from '../../../../../../../types'; +import { NotificationForm } from '../../../../../../components/Notifications/NotificationForm'; interface AlertConditionPanelProps extends RouteComponentProps { alertCondition: AlertCondition; @@ -42,7 +39,6 @@ interface AlertConditionPanelProps extends RouteComponentProps { detector: Detector; indexNum: number; isEdit: boolean; - hasNotificationPlugin: boolean; loadingNotifications: boolean; onAlertTriggerChanged: (newDetector: Detector, emitMetrics?: boolean) => void; refreshNotificationChannels: () => void; @@ -53,7 +49,6 @@ interface AlertConditionPanelState { nameIsInvalid: boolean; previewToggle: boolean; selectedNames: EuiComboBoxOptionOption[]; - showNotificationDetails: boolean; detectionRulesTriggerEnabled: boolean; threatIntelTriggerEnabled: boolean; } @@ -69,7 +64,6 @@ export default class AlertConditionPanel extends Component< nameIsInvalid: false, previewToggle: false, selectedNames: [], - showNotificationDetails: true, detectionRulesTriggerEnabled: props.alertCondition.detection_types.includes('rules'), threatIntelTriggerEnabled: props.alertCondition.detection_types.includes('threat_intel'), }; @@ -200,7 +194,7 @@ export default class AlertConditionPanel extends Component< this.updateTrigger({ tags }); }; - onNotificationChannelsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { + private onNotificationChannelsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { const { alertCondition, onAlertTriggerChanged, @@ -282,13 +276,11 @@ export default class AlertConditionPanel extends Component< loadingNotifications, refreshNotificationChannels, rulesOptions, - hasNotificationPlugin, } = this.props; const { nameFieldTouched, nameIsInvalid, selectedNames, - showNotificationDetails, detectionRulesTriggerEnabled, threatIntelTriggerEnabled, } = this.state; @@ -525,123 +517,16 @@ export default class AlertConditionPanel extends Component< - this.setState({ showNotificationDetails: e.target.checked })} + - - - - {showNotificationDetails && ( - <> - - - -

Notification channel

- - } - > - []} - selectedOptions={ - selectedNotificationChannelOption as EuiComboBoxOptionOption[] - } - onChange={this.onNotificationChannelsChange} - singleSelection={{ asPlainText: true }} - onFocus={refreshNotificationChannels} - isDisabled={!hasNotificationPlugin} - /> -
-
- - - Manage channels - - -
- - {!hasNotificationPlugin && ( - <> - - - - )} - - - - -

Notification message

- - } - paddingSize={'l'} - initialIsOpen={false} - > - - - -

Message subject

- - } - fullWidth={true} - > - this.onMessageSubjectChange(e.target.value)} - required={true} - fullWidth={true} - /> -
-
- - - -

Message body

- - } - fullWidth={true} - > - this.onMessageBodyChange(e.target.value)} - required={true} - fullWidth={true} - /> -
-
- - - - this.prepareMessage(true)}> - Generate message - - - -
-
- - - - )} ); } diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap index ba7edfb3a..773a5f95c 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap @@ -588,7 +588,7 @@ Object {