diff --git a/public/pages/Alerts/components/CorrelationAlertFlyout/CorrelationAlertFlyout.tsx b/public/pages/Alerts/components/CorrelationAlertFlyout/CorrelationAlertFlyout.tsx
new file mode 100644
index 000000000..7818c3cd7
--- /dev/null
+++ b/public/pages/Alerts/components/CorrelationAlertFlyout/CorrelationAlertFlyout.tsx
@@ -0,0 +1,274 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ EuiBasicTable,
+ EuiBasicTableColumn,
+ EuiButton,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiLink,
+ EuiSpacer,
+ EuiTitle,
+ } from '@elastic/eui';
+ import { RuleSource } from '../../../../../server/models/interfaces';
+ import React from 'react';
+ import { ContentPanel } from '../../../../components/ContentPanel';
+ import { ALERT_STATE, DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants';
+ import {
+ capitalizeFirstLetter,
+ createTextDetailsGroup,
+ errorNotificationToast,
+ formatRuleType,
+ renderTime,
+ } from '../../../../utils/helpers';
+ import { IndexPatternsService, OpenSearchService, DetectorsService } from '../../../../services';
+ import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
+ import { NotificationsStart } from 'opensearch-dashboards/public';
+ import { DataStore } from '../../../../store/DataStore';
+ import { CorrelationAlertItem, Detector, Finding } from '../../../../../types';
+
+ export interface CorrelationAlertFlyoutProps {
+ alertItem: CorrelationAlertItem;
+ notifications: NotificationsStart;
+ detectorService: DetectorsService;
+ opensearchService: OpenSearchService;
+ indexPatternService: IndexPatternsService;
+ onClose: () => void;
+ onAcknowledge: (selectedItems: CorrelationAlertItem[]) => void;
+ }
+
+ export interface CorrelationAlertFlyoutState {
+ acknowledged: boolean;
+ findingItems: Finding[];
+ loading: boolean;
+ rules: { [key: string]: RuleSource };
+ }
+
+ export class CorrelationAlertFlyout extends React.Component {
+ constructor(props: CorrelationAlertFlyoutProps) {
+ super(props);
+
+ this.state = {
+ acknowledged: props.alertItem.state === ALERT_STATE.ACKNOWLEDGED,
+ findingItems: [],
+ loading: false,
+ rules: {},
+ };
+ }
+
+ async componentDidMount() {
+ this.getFindings();
+ }
+
+ getFindings = async () => {
+ this.setState({ loading: true });
+ const { notifications } = this.props;
+ try {
+ const findingIds = this.props.alertItem.correlated_finding_ids;
+ const relatedFindings = await DataStore.findings.getFindingsByIds(
+ findingIds
+ );
+ this.setState({ findingItems: relatedFindings });
+ } catch (e: any) {
+ errorNotificationToast(notifications, 'retrieve', 'findings', e);
+ }
+ await this.getRules();
+ this.setState({ loading: false });
+ };
+
+ getRules = async () => {
+ const { notifications } = this.props;
+ try {
+ const { findingItems } = this.state;
+ const ruleIds: string[] = [];
+
+ // Extract ruleIds in order from findingItems
+ findingItems.forEach((finding) => {
+ finding.queries.forEach((query) => {
+ ruleIds.push(query.id);
+ });
+ });
+
+ if (ruleIds.length > 0) {
+ // Fetch rules based on ruleIds
+ const rules = await DataStore.rules.getAllRules({ _id: ruleIds });
+
+ // Prepare allRules object with rules mapped by _id
+ const allRules: { [id: string]: RuleSource } = {};
+ rules.forEach((hit) => {
+ allRules[hit._id] = hit._source;
+ });
+
+ // Update state with allRules
+ this.setState({ rules: allRules });
+ }
+ } catch (e: any) {
+ // Handle errors if any
+ errorNotificationToast(notifications, 'retrieve', 'rules', e);
+ }
+ };
+
+ createFindingTableColumns(): EuiBasicTableColumn[] {
+ const { rules } = this.state;
+
+ const backButton = (
+ DataStore.findings.closeFlyout()}
+ display="base"
+ size="s"
+ data-test-subj={'finding-details-flyout-back-button'}
+ />
+ );
+
+ return [
+ {
+ field: 'timestamp',
+ name: 'Time',
+ sortable: true,
+ dataType: 'date',
+ render: renderTime,
+ },
+ {
+ field: 'id',
+ name: 'Finding ID',
+ sortable: true,
+ dataType: 'string',
+ render: (id: string, finding: any) => (
+ {
+ const ruleId = finding.queries[0]?.id; // Assuming you retrieve rule ID from finding
+ const rule: RuleSource | undefined = rules[ruleId];
+
+ DataStore.findings.openFlyout(
+ {
+ ...finding,
+ detector: { _id: finding.detector_id as string, _index: '' },
+ ruleName: rule?.title || '',
+ ruleSeverity: rule?.level === 'critical' ? rule.level : finding['ruleSeverity'] || rule?.level,
+ },
+ [...this.state.findingItems, finding],
+ true,
+ backButton
+ );
+ }}
+ data-test-subj={'finding-details-flyout-button'}
+ >
+ {id.length > 7 ? `${id.slice(0, 7)}...` : id}
+
+ ),
+ },
+ {
+ field: 'detectionType',
+ name: 'Detection type',
+ render: (detectionType: string, finding: any) => detectionType || DEFAULT_EMPTY_DATA,
+ },
+ {
+ field: 'queries',
+ name: 'Log type',
+ sortable: true,
+ dataType: 'string',
+ render: (finding: any) => {
+ const ruleId = finding.queries[0]?.id; // Retrieve rule ID from the first query of the finding
+ const rule: RuleSource | undefined = rules[ruleId];
+
+ return formatRuleType(rule?.category || ''); // Pass category from rule as string, default to empty string if rule is undefined
+ },
+ },
+ ];
+ }
+
+
+ render() {
+ const { onClose, alertItem, onAcknowledge } = this.props;
+ const { trigger_name, state, severity, start_time, end_time } = alertItem;
+ const { acknowledged, findingItems, loading } = this.state;
+
+ return (
+
+
+
+
+
+ Alert details
+
+
+
+
+
+ {
+ this.setState({ acknowledged: true });
+ onAcknowledge([alertItem]);
+ }}
+ data-test-subj={'alert-details-flyout-acknowledge-button'}
+ >
+ Acknowledge
+
+
+
+
+
+
+
+
+
+
+ {createTextDetailsGroup([
+ { label: 'Alert trigger name', content: trigger_name },
+ { label: 'Alert status', content: capitalizeFirstLetter(state) },
+ {
+ label: 'Alert severity',
+ content: parseAlertSeverityToOption(severity)?.label || DEFAULT_EMPTY_DATA,
+ },
+ ])}
+ {createTextDetailsGroup([
+ { label: 'Start time', content: renderTime(start_time) },
+ { label: 'Last updated time', content: renderTime(end_time) },
+ {
+ label: 'Correlation rule',
+ content: alertItem.correlation_rule_name,
+ url: `#${ROUTES.CORRELATION_RULE_EDIT}/${alertItem.correlation_rule_id}`,
+ target: '_blank',
+ },
+ ])}
+
+
+
+
+
+ columns={this.createFindingTableColumns()}
+ items={findingItems}
+ loading={loading}
+ />
+
+
+
+ );
+ }
+ }
+
\ No newline at end of file
diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx
index 511579816..47f7f4e56 100644
--- a/public/pages/Alerts/containers/Alerts/Alerts.tsx
+++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx
@@ -18,6 +18,8 @@ import {
EuiToolTip,
EuiEmptyPrompt,
EuiTableSelectionType,
+ EuiTabs,
+ EuiTab,
} from '@elastic/eui';
import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter';
import dateMath from '@elastic/datemath';
@@ -41,7 +43,8 @@ import { CoreServicesContext } from '../../../../components/core_services';
import AlertsService from '../../../../services/AlertsService';
import DetectorService from '../../../../services/DetectorService';
import { AlertFlyout } from '../../components/AlertFlyout/AlertFlyout';
-import { FindingsService, IndexPatternsService, OpenSearchService } from '../../../../services';
+import { CorrelationAlertFlyout } from '../../components/CorrelationAlertFlyout/CorrelationAlertFlyout';
+import { CorrelationService, FindingsService, IndexPatternsService, OpenSearchService } from '../../../../services';
import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
import { DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT } from '../../utils/constants';
import {
@@ -56,7 +59,7 @@ import {
import { NotificationsStart } from 'opensearch-dashboards/public';
import { match, RouteComponentProps, withRouter } from 'react-router-dom';
import { ChartContainer } from '../../../../components/Charts/ChartContainer';
-import { AlertItem, DataSourceProps, DateTimeFilter, Detector } from '../../../../../types';
+import { AlertItem, CorrelationAlertItem, DataSourceProps, DateTimeFilter, Detector } from '../../../../../types';
import { DurationRange } from '@elastic/eui/src/components/date_picker/types';
import { DataStore } from '../../../../store/DataStore';
@@ -65,6 +68,7 @@ export interface AlertsProps extends RouteComponentProps, DataSourceProps {
detectorService: DetectorService;
findingService: FindingsService;
opensearchService: OpenSearchService;
+ correlationService: CorrelationService
notifications: NotificationsStart;
indexPatternService: IndexPatternsService;
match: match<{ detectorId: string }>;
@@ -76,15 +80,20 @@ export interface AlertsState {
groupBy: string;
recentlyUsedRanges: DurationRange[];
selectedItems: AlertItem[];
+ correlatedItems: CorrelationAlertItem[];
alerts: AlertItem[];
+ correlationAlerts: CorrelationAlertItem[];
flyoutData?: { alertItem: AlertItem };
+ flyoutCorrelationData?: { alertItem: CorrelationAlertItem };
alertsFiltered: boolean;
+ filteredCorrelationAlerts: CorrelationAlertItem[];
filteredAlerts: AlertItem[];
detectors: { [key: string]: Detector };
loading: boolean;
timeUnit: TimeUnit;
dateFormat: string;
widgetEmptyMessage: React.ReactNode | undefined;
+ tab: string;
}
const groupByOptions = [
@@ -96,6 +105,7 @@ export class Alerts extends Component {
static contextType = CoreServicesContext;
private abortControllers: AbortController[] = [];
+
constructor(props: AlertsProps) {
super(props);
@@ -111,13 +121,17 @@ export class Alerts extends Component {
groupBy: 'status',
recentlyUsedRanges: [DEFAULT_DATE_RANGE],
selectedItems: [],
+ correlatedItems: [],
alerts: [],
+ correlationAlerts: [],
alertsFiltered: false,
filteredAlerts: [],
+ filteredCorrelationAlerts:[],
detectors: {},
timeUnit: timeUnits.timeUnit,
dateFormat: timeUnits.dateFormat,
widgetEmptyMessage: undefined,
+ tab: 'findings'
};
}
@@ -133,12 +147,28 @@ export class Alerts extends Component {
prevProps.dateTimeFilter?.endTime !== dateTimeFilter.endTime ||
prevState.alerts !== this.state.alerts ||
prevState.alerts.length !== this.state.alerts.length;
-
+
+ const correlationAlertsChanged =
+ prevProps.dateTimeFilter?.startTime !== dateTimeFilter.startTime ||
+ prevProps.dateTimeFilter?.endTime !== dateTimeFilter.endTime ||
+ prevState.correlationAlerts !== this.state.correlationAlerts ||
+ prevState.correlationAlerts.length !== this.state.correlationAlerts.length;
+
+ if (prevState.tab != this.state.tab) {
+ if (this.state.tab == "findings") {
+ renderVisualization(this.generateVisualizationSpec(this.state.filteredAlerts), 'alerts-view');
+ } else {
+ renderVisualization(this.generateCorrelationVisualizationSpec(this.state.filteredCorrelationAlerts), 'alerts-view');
+ }
+ }
if (this.props.dataSource !== prevProps.dataSource) {
this.onRefresh();
} else if (alertsChanged) {
this.filterAlerts();
- } else if (this.state.groupBy !== prevState.groupBy) {
+ } else if (correlationAlertsChanged) {
+ this.filterCorrelationAlerts();
+ }
+ else if (this.state.groupBy !== prevState.groupBy) {
renderVisualization(this.generateVisualizationSpec(this.state.filteredAlerts), 'alerts-view');
}
}
@@ -173,6 +203,36 @@ export class Alerts extends Component {
renderVisualization(this.generateVisualizationSpec(filteredAlerts), 'alerts-view');
};
+ filterCorrelationAlerts = () => {
+ const { correlationAlerts } = this.state;
+ const {
+ dateTimeFilter = {
+ startTime: DEFAULT_DATE_RANGE.start,
+ endTime: DEFAULT_DATE_RANGE.end,
+ },
+ } = this.props;
+ const startMoment = dateMath.parse(dateTimeFilter.startTime);
+ const endMoment = dateMath.parse(dateTimeFilter.endTime);
+ const filteredCorrelationAlerts = correlationAlerts.filter((correlationAlert) =>
+ moment(correlationAlert.end_time).isBetween(moment(startMoment), moment(endMoment))
+ );
+ this.setState({
+ alertsFiltered: true,
+ filteredCorrelationAlerts: filteredCorrelationAlerts,
+ widgetEmptyMessage: filteredCorrelationAlerts.length ? undefined : (
+
+ No alerts.Adjust the time range to see more
+ results.
+
+ }
+ />
+ ),
+ });
+ renderVisualization(this.generateCorrelationVisualizationSpec(filteredCorrelationAlerts), 'alerts-view');
+ };
+
getColumns(): EuiBasicTableColumn[] {
return [
{
@@ -252,10 +312,93 @@ export class Alerts extends Component {
];
}
+ getCorrelationColumns(): EuiBasicTableColumn[] {
+ return [
+ {
+ field: 'start_time',
+ name: 'Start time',
+ sortable: true,
+ dataType: 'date',
+ render: renderTime,
+ },
+ {
+ field: 'trigger_name',
+ name: 'Alert trigger name',
+ sortable: false,
+ dataType: 'string',
+ render: (triggerName: string, alertItem: CorrelationAlertItem) => (
+ this.setCorrelationFlyout(alertItem)}>{triggerName}
+ ),
+ },
+ {
+ field: 'correlation_rule_name',
+ name: 'Correlation Rule Name',
+ sortable: true,
+ dataType: 'string',
+ render: (correlationRulename: string) => correlationRulename || DEFAULT_EMPTY_DATA,
+ },
+ {
+ field: 'state',
+ name: 'Status',
+ sortable: true,
+ dataType: 'string',
+ render: (status: string) => (status ? capitalizeFirstLetter(status) : DEFAULT_EMPTY_DATA),
+ },
+ {
+ field: 'severity',
+ name: 'Alert severity',
+ sortable: true,
+ dataType: 'string',
+ render: (severity: string) =>
+ parseAlertSeverityToOption(severity)?.label || DEFAULT_EMPTY_DATA,
+ },
+ {
+ name: 'Actions',
+ sortable: false,
+ actions: [
+ {
+ render: (alertItem: CorrelationAlertItem) => {
+ const disableAcknowledge = alertItem.state !== ALERT_STATE.ACTIVE;
+ return (
+
+ this.onAcknowledgeCorrelationAlert([alertItem])}
+ />
+
+ );
+ },
+ },
+ {
+ render: (alertItem: CorrelationAlertItem) => (
+
+ this.setCorrelationFlyout(alertItem)}
+ />
+
+ ),
+ },
+ ],
+ },
+ ];
+ }
+
setFlyout(alertItem?: AlertItem): void {
this.setState({ flyoutData: alertItem ? { alertItem } : undefined });
}
+ setCorrelationFlyout(alertItem?: CorrelationAlertItem): void {
+ this.setState({ flyoutCorrelationData: alertItem ? { alertItem } : undefined });
+ }
+
generateVisualizationSpec(alerts: AlertItem[]) {
const visData = alerts.map((alert) => {
const time = new Date(alert.start_time);
@@ -286,6 +429,36 @@ export class Alerts extends Component {
});
}
+ generateCorrelationVisualizationSpec(alerts: CorrelationAlertItem[]) {
+ const visData = alerts.map((alert) => {
+ const time = new Date(alert.start_time);
+ time.setMilliseconds(0);
+ time.setSeconds(0);
+
+ return {
+ alert: 1,
+ time,
+ status: alert.state,
+ severity: parseAlertSeverityToOption(alert.severity)?.label || alert.severity,
+ };
+ });
+ const {
+ dateTimeFilter = {
+ startTime: DEFAULT_DATE_RANGE.start,
+ endTime: DEFAULT_DATE_RANGE.end,
+ },
+ } = this.props;
+ const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime);
+ return getAlertsVisualizationSpec(visData, this.state.groupBy, {
+ timeUnit: chartTimeUnits.timeUnit,
+ dateFormat: chartTimeUnits.dateFormat,
+ domain: getDomainRange(
+ [dateTimeFilter.startTime, dateTimeFilter.endTime],
+ chartTimeUnits.timeUnit.unit
+ ),
+ });
+ }
+
createGroupByControl(): React.ReactNode {
return createSelectComponent(
groupByOptions,
@@ -306,6 +479,24 @@ export class Alerts extends Component {
this.abortPendingGetAlerts();
}
+ async getCorrelationAlerts() {
+ this.setState({ loading: true, correlationAlerts: [] });
+ const { correlationService, notifications, dateTimeFilter } = this.props;
+ try {
+ const correlationRes = await correlationService.getCorrelationAlerts();
+ const duration = getDuration(dateTimeFilter);
+ if (correlationRes.ok) {
+ this.setState({ correlationAlerts: correlationRes.response.correlationAlerts });
+ } else {
+ errorNotificationToast(notifications, 'retrieve', 'correlations', correlationRes.error);
+ }
+ } catch (e: any) {
+ errorNotificationToast(notifications, 'retrieve', 'correlationAlerts', e);
+ }
+ this.filterCorrelationAlerts();
+ this.setState({ loading: false });
+ }
+
async getAlerts(abort: AbortSignal) {
this.setState({ loading: true, alerts: [] });
const { detectorService, notifications, dateTimeFilter } = this.props;
@@ -330,7 +521,7 @@ export class Alerts extends Component {
abort,
duration,
(alerts) => {
- this.setState({ alerts: [...this.state.alerts, ...alerts]})
+ this.setState({ alerts: [...this.state.alerts, ...alerts] })
}
);
}
@@ -391,16 +582,25 @@ export class Alerts extends Component {
const abortController = new AbortController();
this.abortControllers.push(abortController);
this.getAlerts(abortController.signal);
+ this.getCorrelationAlerts();
renderVisualization(this.generateVisualizationSpec(this.state.filteredAlerts), 'alerts-view');
+ renderVisualization(this.generateCorrelationVisualizationSpec(this.state.filteredCorrelationAlerts), 'alerts-view');
};
onSelectionChange = (selectedItems: AlertItem[]) => {
this.setState({ selectedItems });
};
+ onCorrelationSelectionChange = (correlatedItems: CorrelationAlertItem[]) => {
+ this.setState({ correlatedItems });
+ };
+
onFlyoutClose = () => {
this.setState({ flyoutData: undefined });
};
+ onCorrelationFlyoutClose = () => {
+ this.setState({ flyoutCorrelationData: undefined });
+ };
onAcknowledge = async (selectedItems: AlertItem[] = []) => {
const { alertService, notifications } = this.props;
@@ -433,12 +633,46 @@ export class Alerts extends Component {
this.onRefresh();
};
+ onAcknowledgeCorrelationAlert = async (selectedItems: CorrelationAlertItem[] = []) => {
+ const { correlationService, notifications } = this.props;
+ let successCount = 0;
+ try {
+ // Separating the selected items by detector ID, and adding all selected alert IDs to an array for that detector ID.
+ const correlations: { [key: string]: string[] } = {};
+ selectedItems.forEach((item) => {
+ if (!correlations[item.correlation_rule_id]) correlations[item.correlation_rule_id] = [item.id];
+ else correlations[item.correlation_rule_id].push(item.id);
+ });
+
+ for (let corrId of Object.keys(correlations)) {
+ const alertIds = correlations[corrId];
+ if (alertIds.length > 0) {
+ const response = await correlationService.acknowledgeCorrelationAlerts(alertIds);
+ if (response.ok) {
+ successCount += alertIds.length;
+ } else {
+ errorNotificationToast(notifications, 'acknowledge', 'alerts', response.error);
+ }
+ }
+ }
+ } catch (e: any) {
+ errorNotificationToast(notifications, 'acknowledge', 'alerts', e);
+ }
+ if (successCount)
+ successNotificationToast(notifications, 'acknowledged', `${successCount} alerts`);
+ this.setState({ selectedItems: [] });
+ this.onRefresh();
+ };
+
render() {
const {
alerts,
alertsFiltered,
detectors,
filteredAlerts,
+ filteredCorrelationAlerts,
+ correlationAlerts,
+ flyoutCorrelationData,
flyoutData,
loading,
recentlyUsedRanges,
@@ -453,6 +687,8 @@ export class Alerts extends Component {
} = this.props;
const severities = new Set();
const statuses = new Set();
+ const corrSeverities = new Set();
+ const corrStatuses = new Set();
filteredAlerts.forEach((alert) => {
if (alert) {
severities.add(alert.severity);
@@ -460,6 +696,13 @@ export class Alerts extends Component {
}
});
+ filteredCorrelationAlerts.forEach((alert) => {
+ if (alert) {
+ corrSeverities.add(alert.severity);
+ corrStatuses.add(alert.state);
+ }
+ });
+
const search = {
box: {
placeholder: 'Search alerts',
@@ -489,12 +732,48 @@ export class Alerts extends Component {
],
};
+ const correlationSearch = {
+ box: {
+ placeholder: 'Search alerts',
+ schema: true,
+ },
+ filters: [
+ {
+ type: 'field_value_selection',
+ field: 'severity',
+ name: 'Alert severity',
+ options: Array.from(corrSeverities).map((severity) => ({
+ value: severity,
+ name: parseAlertSeverityToOption(severity)?.label || severity,
+ })),
+ multiSelect: 'or',
+ } as FieldValueSelectionFilterConfigType,
+ {
+ type: 'field_value_selection',
+ field: 'state',
+ name: 'Status',
+ options: Array.from(corrStatuses).map((status) => ({
+ value: status,
+ name: capitalizeFirstLetter(status) || status,
+ })),
+ multiSelect: 'or',
+ } as FieldValueSelectionFilterConfigType,
+ ],
+ };
+
+
const selection: EuiTableSelectionType = {
onSelectionChange: this.onSelectionChange,
selectable: (item: AlertItem) => item.state === ALERT_STATE.ACTIVE,
selectableMessage: (selectable) => (selectable ? '' : DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT),
};
+ const correlationSelection: EuiTableSelectionType = {
+ onSelectionChange: this.onCorrelationSelectionChange,
+ selectable: (item: CorrelationAlertItem) => item.state === ALERT_STATE.ACTIVE,
+ selectableMessage: (selectable) => (selectable ? '' : DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT),
+ };
+
const sorting: any = {
sort: {
field: 'start_time',
@@ -514,6 +793,15 @@ export class Alerts extends Component {
indexPatternService={this.props.indexPatternService}
/>
)}
+ {flyoutCorrelationData && (
+
+ )}
@@ -564,20 +852,46 @@ export class Alerts extends Component {
- `${item.id}`}
- isSelectable={true}
- pagination
- search={search}
- sorting={sorting}
- selection={selection}
- loading={loading}
- message={widgetEmptyMessage}
+
+ this.setState({ tab: 'findings' })} isSelected={this.state.tab === 'findings'}>
+ Findings
+
+ this.setState({ tab: 'correlations' })} isSelected={this.state.tab === 'correlations'}>
+ Correlations
+
+
+ {this.state.tab === 'findings' && (
+ // Content for the "Findings" tab
+ `${item.id}`}
+ isSelectable={true}
+ pagination
+ search={search}
+ sorting={sorting}
+ selection={selection}
+ loading={loading}
+ message={widgetEmptyMessage}
+ />
+ )}
+ {this.state.tab === 'correlations' && (
+ `${item.id}`}
+ isSelectable={true}
+ pagination
+ search={correlationSearch}
+ sorting={sorting}
+ selection={correlationSelection}
+ loading={loading}
+ message={widgetEmptyMessage}
/>
+ )}
+
>
);
diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx
index e428eeeaf..225c0e1ca 100644
--- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx
+++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { ChangeEvent, useCallback, useContext, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Form, Formik, FormikErrors, FormikTouched } from 'formik';
import { ContentPanel } from '../../../components/ContentPanel';
import { DataStore } from '../../../store/DataStore';
@@ -55,6 +55,7 @@ import { getEmptyAlertCondition, getNotificationChannels, parseAlertSeverityToOp
import { NotificationsCallOut } from '../../../../public/components/NotificationsCallOut';
import { BrowserServices } from '../../../../public/models/interfaces';
import { ExperimentalBanner } from '../components/ExperimentalBanner';
+import { ALERT_SEVERITY_OPTIONS } from '../components/ConfigureAlerts/utils/constants';
export interface CreateCorrelationRuleProps extends DataSourceProps {
indexService: IndexService;
@@ -127,26 +128,22 @@ export const CreateCorrelationRule: React.FC = (
const resetForm = useRef(false);
const [selectedNotificationChannelOption, setSelectedNotificationChannelOption] = useState([]);
- const onRuleSeverityChange = (selectedOptions: Array>) => {
- const sev_levels: string[] = selectedOptions
- .map(option => option.value)
- .filter((value): value is string => value !== undefined);
-
- setInitialValues(prevState => ({
- ...prevState,
+ const onAlertSeverityChange = (severity: string) => {
+ setInitialValues({
+ ...initialValues,
trigger: {
- ...prevState.trigger!,
- sev_levels,
+ ...initialValues.trigger!,
+ name: severity,
},
- }));
+ });
};
- const onNameChange = (e: ChangeEvent) => {
+ const onNameChange = (triggerName: string) => {
setInitialValues({
...initialValues,
trigger: {
...initialValues.trigger!,
- name: e.target.value,
+ name: triggerName,
},
});
};
@@ -831,7 +828,7 @@ export const CreateCorrelationRule: React.FC = (
if (alertCondition && alertCondition.actions) {
if (alertCondition.actions.length == 0)
alertCondition.actions = getEmptyAlertCondition().actions;
-
+
const channelId = alertCondition?.actions[0].destination_id;
const selectedNotificationChannelOption: NotificationChannelOption[] = [];
if (channelId) {
@@ -867,7 +864,6 @@ export const CreateCorrelationRule: React.FC = (
initialValues={initialValues}
validate={(values) => {
const errors: FormikErrors = {};
-
if (!values.name) {
errors.name = 'Rule name is required';
} else {
@@ -904,7 +900,7 @@ export const CreateCorrelationRule: React.FC = (
}}
enableReinitialize={true}
>
- {({ values: { name, queries, time_window }, touched, errors, ...props }) => {
+ {({ values: { name, queries, time_window, trigger }, touched, errors, ...props }) => {
if (resetForm.current) {
resetForm.current = false;
props.resetForm();
@@ -1042,8 +1038,12 @@ export const CreateCorrelationRule: React.FC = (
>
{
+ const triggerName = e.target.value || '';
+ props.handleChange('trigger?.name')(triggerName);
+ onNameChange(triggerName);
+ }}
data-test-subj="alert-condition-name"
/>
@@ -1059,10 +1059,18 @@ export const CreateCorrelationRule: React.FC = (
- (initialValues?.trigger?.sev_levels ?? []).includes(option.value || '')
- )}
+ singleSelection={{ asPlainText: true }}
+ selectedOptions={
+ trigger?.severity ? [parseAlertSeverityToOption(trigger?.severity)] : [ALERT_SEVERITY_OPTIONS.HIGHEST]
+ }
+ onChange={(e) => {
+ const selectedSeverity = e[0]?.value || '';
+ props.handleChange(`trigger?.severity`)(selectedSeverity);
+ if (selectedSeverity !== '') {
+ onAlertSeverityChange(selectedSeverity);
+ }
+ }}
+ isClearable={true}
data-test-subj="alert-severity-combo-box"
/>
@@ -1154,7 +1162,7 @@ export const CreateCorrelationRule: React.FC = (
>
onMessageSubjectChange(e.target.value)}
required={true}
fullWidth={true}
@@ -1173,7 +1181,7 @@ export const CreateCorrelationRule: React.FC = (
>
onMessageBodyChange(e.target.value)}
required={true}
fullWidth={true}
diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx
index 2202669d4..d35dca77e 100644
--- a/public/pages/Main/Main.tsx
+++ b/public/pages/Main/Main.tsx
@@ -574,6 +574,7 @@ export default class Main extends Component {
notifications={core?.notifications}
opensearchService={services.opensearchService}
indexPatternService={services.indexPatternsService}
+ correlationService={services.correlationsService}
dataSource={selectedDataSource}
/>
)}
diff --git a/public/services/CorrelationService.ts b/public/services/CorrelationService.ts
index c5ce896c9..c4b9a127f 100644
--- a/public/services/CorrelationService.ts
+++ b/public/services/CorrelationService.ts
@@ -14,12 +14,38 @@ import {
SearchCorrelationRulesResponse,
ICorrelationsService,
UpdateCorrelationRuleResponse,
+ GetCorrelationAlertsResponse,
+ AckCorrelationAlertsResponse
} from '../../types';
import { dataSourceInfo } from './utils/constants';
export default class CorrelationService implements ICorrelationsService {
constructor(private httpClient: HttpSetup) {}
+ acknowledgeCorrelationAlerts = async (
+ body: any
+ ): Promise> => {
+ const url = `..${API.ACK_CORRELATION_ALERTS}`;
+
+ return (await this.httpClient.post(url, {
+ body: JSON.stringify(body),
+ query: {
+ dataSourceId: dataSourceInfo.activeDataSource.id,
+ },
+ })) as ServerResponse;
+ };
+
+ getCorrelationAlerts = async (
+ ): Promise> => {
+ const url = `..${API.GET_CORRELATION_ALERTS}`;
+
+ return (await this.httpClient.get(url, {
+ query: {
+ dataSourceId: dataSourceInfo.activeDataSource.id,
+ },
+ })) as ServerResponse;
+ };
+
getCorrelatedFindings = async (
finding: string,
detector_type: string,
diff --git a/public/services/FindingsService.ts b/public/services/FindingsService.ts
index 4a5a890e4..93eb5dbec 100644
--- a/public/services/FindingsService.ts
+++ b/public/services/FindingsService.ts
@@ -20,7 +20,7 @@ export default class FindingsService {
getFindingsParams: GetFindingsParams
): Promise> => {
const findingIds = getFindingsParams.findingIds
- ? JSON.stringify(getFindingsParams.findingIds)
+ ? getFindingsParams.findingIds.join(',')
: undefined;
const query = {
sortOrder: 'desc',
diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts
index f8f74b095..587dc566f 100644
--- a/public/store/CorrelationsStore.ts
+++ b/public/store/CorrelationsStore.ts
@@ -4,11 +4,13 @@
*/
import {
+ AckCorrelationAlertsResponse,
CorrelationFieldCondition,
CorrelationFinding,
CorrelationRule,
CorrelationRuleQuery,
DetectorHit,
+ GetCorrelationAlertsResponse,
ICorrelationsStore,
IRulesStore,
} from '../../types';
@@ -89,7 +91,6 @@ export class CorrelationsStore implements ICorrelationsStore {
}
public async updateCorrelationRule(correlationRule: CorrelationRule): Promise {
- console.log("Correlation rule is ", correlationRule);
const response = await this.service.updateCorrelationRule(correlationRule.id, {
name: correlationRule.name,
time_window: correlationRule.time_window,
@@ -115,7 +116,6 @@ export class CorrelationsStore implements ICorrelationsStore {
}),
trigger: correlationRule.trigger,
});
- console.log("Resposne is ", response);
if (!response.ok) {
errorNotificationToast(this.notifications, 'update', 'correlation rule', response.error);
return false;
@@ -198,9 +198,9 @@ export class CorrelationsStore implements ICorrelationsStore {
start_time,
end_time
);
-
+
const result: { finding1: CorrelationFinding; finding2: CorrelationFinding }[] = [];
-
+
if (allCorrelationsRes.ok) {
const firstTenGrandCorrelations = allCorrelationsRes.response.findings.slice(0, 10000);
const allFindingIdsSet = new Set();
@@ -213,7 +213,7 @@ export class CorrelationsStore implements ICorrelationsStore {
let allFindings: { [id: string]: CorrelationFinding } = {};
const maxFindingsFetchedInSingleCall = 10000;
- for (let i = 0; i < allFindingIds.length; i+= maxFindingsFetchedInSingleCall) {
+ for (let i = 0; i < allFindingIds.length; i += maxFindingsFetchedInSingleCall) {
const findingIds = allFindingIds.slice(i, i + maxFindingsFetchedInSingleCall);
const findings = await this.fetchAllFindings(findingIds);
allFindings = {
@@ -268,10 +268,10 @@ export class CorrelationsStore implements ICorrelationsStore {
timestamp: new Date(f.timestamp).toLocaleString(),
detectionRule: rule
? {
- name: rule._source.title,
- severity: rule._source.level,
- tags: rule._source.tags,
- }
+ name: rule._source.title,
+ severity: rule._source.level,
+ tags: rule._source.tags,
+ }
: { name: DEFAULT_EMPTY_DATA, severity: DEFAULT_EMPTY_DATA },
};
});
@@ -296,7 +296,7 @@ export class CorrelationsStore implements ICorrelationsStore {
if (response?.ok) {
const correlatedFindings: CorrelationFinding[] = [];
const allFindingIds = response.response.findings.map(f => f.finding);
- const allFindings = await this.fetchAllFindings(allFindingIds);
+ const allFindings = await this.fetchAllFindings(allFindingIds);
response.response.findings.forEach((f) => {
if (allFindings[f.finding]) {
correlatedFindings.push({
@@ -327,6 +327,33 @@ export class CorrelationsStore implements ICorrelationsStore {
};
}
+ public async getAllCorrelationAlerts(
+ ): Promise {
+ const response = await this.service.getCorrelationAlerts();
+ if (response?.ok) {
+ return {
+ correlationAlerts: response.response.correlationAlerts,
+ total_alerts: response.response.total_alerts,
+ };
+ } else {
+ throw new Error('Failed to fetch correlated alerts');
+ }
+ }
+
+ public async acknowledgeCorrelationAlerts(
+ alertIds: string[]
+ ): Promise {
+ const response = await this.service.acknowledgeCorrelationAlerts(alertIds);
+ if (response?.ok) {
+ return {
+ acknowledged: response.response.acknowledged,
+ failed: response.response.failed,
+ };
+ } else {
+ throw new Error('Failed to acknowledge correlated alerts');
+ }
+ }
+
private parseRuleQueryString(queryString: string): CorrelationFieldCondition[] {
const queries: CorrelationFieldCondition[] = [];
if (!queryString) {
diff --git a/server/clusters/addCorrelationMethods.ts b/server/clusters/addCorrelationMethods.ts
index 698e8a482..f5fba333d 100644
--- a/server/clusters/addCorrelationMethods.ts
+++ b/server/clusters/addCorrelationMethods.ts
@@ -89,4 +89,20 @@ export function addCorrelationMethods(securityAnalytics: any, createAction: any)
needBody: false,
method: 'GET',
});
+
+ securityAnalytics[METHOD_NAMES.GET_CORRELATION_ALERTS] = createAction({
+ url: {
+ fmt: `${API.GET_CORRELATION_ALERTS}`,
+ },
+ needBody: false,
+ method: 'GET',
+ });
+
+ securityAnalytics[METHOD_NAMES.ACK_CORRELATION_ALERTS] = createAction({
+ url: {
+ fmt: `${API.ACK_CORRELATION_ALERTS}`,
+ },
+ needBody: true,
+ method: 'POST',
+ });
}
diff --git a/server/models/interfaces/index.ts b/server/models/interfaces/index.ts
index a8407d059..f108d5623 100644
--- a/server/models/interfaces/index.ts
+++ b/server/models/interfaces/index.ts
@@ -39,6 +39,8 @@ export interface SecurityAnalyticsApi {
readonly CORRELATIONS: string;
readonly LOGTYPE_BASE: string;
readonly METRICS: string;
+ readonly GET_CORRELATION_ALERTS: string;
+ readonly ACK_CORRELATION_ALERTS: string;
}
export interface NodeServices {
diff --git a/server/routes/CorrelationRoutes.ts b/server/routes/CorrelationRoutes.ts
index cce4b6314..739ea4a4b 100644
--- a/server/routes/CorrelationRoutes.ts
+++ b/server/routes/CorrelationRoutes.ts
@@ -87,4 +87,25 @@ export function setupCorrelationRoutes(services: NodeServices, router: IRouter)
},
correlationService.deleteCorrelationRule
);
+
+ router.get(
+ {
+ path: `${API.GET_CORRELATION_ALERTS}`,
+ validate: {
+ query: createQueryValidationSchema(),
+ },
+ },
+ correlationService.getAllCorrelationAlerts
+ );
+
+ router.post(
+ {
+ path: `${API.ACK_CORRELATION_ALERTS}`,
+ validate: {
+ body: schema.any(),
+ query: createQueryValidationSchema(),
+ },
+ },
+ correlationService.acknowledgeCorrelationAlerts
+ );
}
diff --git a/server/routes/FindingsRoutes.ts b/server/routes/FindingsRoutes.ts
index 6302b1a26..6a9b68757 100644
--- a/server/routes/FindingsRoutes.ts
+++ b/server/routes/FindingsRoutes.ts
@@ -25,7 +25,7 @@ export function setupFindingsRoutes(services: NodeServices, router: IRouter) {
detectionType: schema.maybe(schema.string()),
severity: schema.maybe(schema.string()),
searchString: schema.maybe(schema.string()),
- findingIds: schema.maybe(schema.arrayOf(schema.string())),
+ findingIds: schema.string(),
startTime: schema.maybe(schema.number()),
endTime: schema.maybe(schema.number())
}),
diff --git a/server/services/CorrelationService.ts b/server/services/CorrelationService.ts
index 06f7d504d..b3b6cd776 100644
--- a/server/services/CorrelationService.ts
+++ b/server/services/CorrelationService.ts
@@ -21,6 +21,67 @@ import {
import { MDSEnabledClientService } from './MDSEnabledClientService';
export default class CorrelationService extends MDSEnabledClientService {
+
+ acknowledgeCorrelationAlerts = async (
+ context: RequestHandlerContext,
+ request: OpenSearchDashboardsRequest,
+ response: OpenSearchDashboardsResponseFactory
+ ) => {
+ try {
+ const params: any = { body: request.body };
+ const client = this.getClient(request, context);
+ const ackCorrelationAlertsResp = await client(
+ CLIENT_CORRELATION_METHODS.ACK_CORRELATION_ALERTS,
+ params
+ );
+ return response.custom({
+ statusCode: 200,
+ body: {
+ ok: true,
+ response: ackCorrelationAlertsResp,
+ },
+ });
+ } catch (error: any) {
+ console.error('Security Analytics - CorrelationService - ackCorrelationAlertsResp:', error);
+ return response.custom({
+ statusCode: 200,
+ body: {
+ ok: false,
+ error: error.message,
+ },
+ });
+ }
+ };
+
+ getAllCorrelationAlerts = async (
+ context: RequestHandlerContext,
+ request: OpenSearchDashboardsRequest,
+ response: OpenSearchDashboardsResponseFactory
+ ) => {
+ try {
+ const client = this.getClient(request, context);
+ const getCorrelationAlertsResp = await client(
+ CLIENT_CORRELATION_METHODS.GET_CORRELATION_ALERTS,
+ );
+ return response.custom({
+ statusCode: 200,
+ body: {
+ ok: true,
+ response: getCorrelationAlertsResp,
+ },
+ });
+ } catch (error: any) {
+ console.error('Security Analytics - CorrelationService - getCorrelationAlerts:', error);
+ return response.custom({
+ statusCode: 200,
+ body: {
+ ok: false,
+ error: error.message,
+ },
+ });
+ }
+ };
+
createCorrelationRule = async (
context: RequestHandlerContext,
request: OpenSearchDashboardsRequest,
diff --git a/server/utils/constants.ts b/server/utils/constants.ts
index c1e115d2f..bca032d47 100644
--- a/server/utils/constants.ts
+++ b/server/utils/constants.ts
@@ -36,6 +36,8 @@ export const API: SecurityAnalyticsApi = {
CORRELATIONS: `${BASE_API_PATH}/correlations`,
LOGTYPE_BASE: `${BASE_API_PATH}/logtype`,
METRICS: `/api/security_analytics/stats`,
+ GET_CORRELATION_ALERTS: `${BASE_API_PATH}/correlationAlerts`,
+ ACK_CORRELATION_ALERTS: `${BASE_API_PATH}/_acknowledge/correlationAlerts`,
};
/**
@@ -66,6 +68,8 @@ export const METHOD_NAMES = {
DELETE_CORRELATION_RULE: 'deleteCorrelationRule',
GET_CORRELATED_FINDINGS: 'getCorrelatedFindings',
GET_ALL_CORRELATIONS: 'getAllCorrelations',
+ GET_CORRELATION_ALERTS: 'getAllCorrelationAlerts',
+ ACK_CORRELATION_ALERTS: 'acknowledgeCorrelationAlerts',
// Finding methods
GET_FINDINGS: 'getFindings',
@@ -120,6 +124,8 @@ export const CLIENT_CORRELATION_METHODS = {
DELETE_CORRELATION_RULE: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.DELETE_CORRELATION_RULE}`,
GET_CORRELATED_FINDINGS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CORRELATED_FINDINGS}`,
GET_ALL_CORRELATIONS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_ALL_CORRELATIONS}`,
+ GET_CORRELATION_ALERTS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CORRELATION_ALERTS}`,
+ ACK_CORRELATION_ALERTS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.ACK_CORRELATION_ALERTS}`
};
export const CLIENT_FIELD_MAPPINGS_METHODS = {
diff --git a/types/Alert.ts b/types/Alert.ts
index b85634f54..090a0c0da 100644
--- a/types/Alert.ts
+++ b/types/Alert.ts
@@ -70,6 +70,16 @@ export interface GetAlertsResponse {
detectorType: string;
}
+export interface GetCorrelationAlertsResponse {
+ correlationAlerts: CorrelationAlertResponse[];
+ total_alerts: number;
+}
+
+export interface AckCorrelationAlertsResponse {
+ acknowledged: CorrelationAlertResponse[];
+ failed: CorrelationAlertResponse[];
+}
+
export interface AlertItem {
id: string;
start_time: string;
@@ -82,6 +92,19 @@ export interface AlertItem {
acknowledged_time: string | null;
}
+export interface CorrelationAlertItem {
+ id: string;
+ start_time: string;
+ trigger_name: string;
+ correlation_rule_id: string;
+ correlation_rule_name: string;
+ state: string;
+ severity: string;
+ correlated_finding_ids: string[];
+ end_time: string;
+ acknowledged_time: string | null;
+}
+
export interface AlertResponse extends AlertItem {
version: number;
schema_version: number;
@@ -97,6 +120,20 @@ export interface AlertResponse extends AlertItem {
end_time: string | null;
}
+export interface CorrelationAlertResponse extends CorrelationAlertItem {
+ version: number;
+ schema_version: number;
+ trigger_id: string;
+ related_doc_ids: string[];
+ error_message: string | null;
+ alert_history: string[];
+ action_execution_results: {
+ action_id: string;
+ last_execution_time: number;
+ throttled_count: number;
+ }[];
+}
+
export interface AcknowledgeAlertsParams {
body: { alerts: string[] };
detector_id: string;