From 8c2df251e1eb676d3fbaea29d4e103397a378daf Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 02:35:59 -0800
Subject: [PATCH 01/19] feat: add correlations overview page with a chart and
table, and new flyout for the table
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../components/MDS/DataSourceMenuWrapper.tsx | 1 +
.../containers/CorrelationsContainerUpd.tsx | 1529 +++++++++++++++++
public/pages/Main/Main.tsx | 60 +
public/store/CorrelationsStore.ts | 4 +-
public/utils/constants.ts | 2 +
5 files changed, 1594 insertions(+), 2 deletions(-)
create mode 100644 public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
diff --git a/public/components/MDS/DataSourceMenuWrapper.tsx b/public/components/MDS/DataSourceMenuWrapper.tsx
index 69ddc841..fae41f34 100644
--- a/public/components/MDS/DataSourceMenuWrapper.tsx
+++ b/public/components/MDS/DataSourceMenuWrapper.tsx
@@ -153,6 +153,7 @@ export const DataSourceMenuWrapper: React.FC = ({
ROUTES.LOG_TYPES,
ROUTES.RULES,
ROUTES.CORRELATIONS,
+ ROUTES.CORRELATIONS_UPD,
ROUTES.CORRELATION_RULES,
ROUTES.RULES_CREATE,
ROUTES.RULES_IMPORT,
diff --git a/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx b/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
new file mode 100644
index 00000000..9af8b18b
--- /dev/null
+++ b/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
@@ -0,0 +1,1529 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {
+ CorrelationFinding,
+ CorrelationGraphData,
+ DataSourceProps,
+ DateTimeFilter,
+ FindingItemType,
+} from '../../../../types';
+import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import {
+ getDefaultLogTypeFilterItemOptions,
+ defaultSeverityFilterItemOptions,
+ emptyGraphData,
+ getLabelFromLogType,
+ getSeverityColor,
+ getSeverityLabel,
+ graphRenderOptions,
+} from '../utils/constants';
+import {
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiPanel,
+ EuiCompressedSuperDatePicker,
+ EuiSpacer,
+ EuiSmallButtonEmpty,
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiSmallButtonIcon,
+ EuiText,
+ EuiEmptyPrompt,
+ EuiSmallButton,
+ EuiBadge,
+ EuiFilterGroup,
+ EuiHorizontalRule,
+ EuiButtonGroup,
+ EuiBasicTableColumn,
+ EuiToolTip,
+ EuiInMemoryTable,
+ EuiTextColor,
+ EuiLink,
+ EuiFieldSearch,
+} from '@elastic/eui';
+import { FilterItem, FilterGroup } from '../components/FilterGroup';
+import {
+ BREADCRUMBS,
+ DEFAULT_DATE_RANGE,
+ MAX_RECENTLY_USED_TIME_RANGES,
+ ROUTES,
+} from '../../../utils/constants';
+import { CorrelationGraph } from '../components/CorrelationGraph';
+import { FindingCard } from '../components/FindingCard';
+import { DataStore } from '../../../store/DataStore';
+import datemath from '@elastic/datemath';
+import { ruleSeverity } from '../../Rules/utils/constants';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { Network } from 'react-graph-vis';
+import { getLogTypeLabel } from '../../LogTypes/utils/helpers';
+import { NotificationsStart } from 'opensearch-dashboards/public';
+import {
+ capitalizeFirstLetter,
+ errorNotificationToast,
+ renderVisualization,
+ setBreadcrumbs,
+} from '../../../utils/helpers';
+import { PageHeader } from '../../../components/PageHeader/PageHeader';
+import moment from 'moment';
+import { ChartContainer } from '../../../components/Charts/ChartContainer';
+import {
+ addInteractiveLegends,
+ DateOpts,
+ defaultDateFormat,
+ defaultScaleDomain,
+ defaultTimeUnit,
+ getChartTimeUnit,
+ getDomainRange,
+ getTimeTooltip,
+ getVisualizationSpec,
+ getXAxis,
+ getYAxis,
+} from '../../Overview/utils/helpers';
+import { debounce } from 'lodash';
+
+export const DEFAULT_EMPTY_DATA = '-';
+
+interface CorrelationsProps
+ extends RouteComponentProps<
+ any,
+ any,
+ { finding: FindingItemType; correlatedFindings: CorrelationFinding[] }
+ >,
+ DataSourceProps {
+ setDateTimeFilter?: Function;
+ dateTimeFilter?: DateTimeFilter;
+ onMount: () => void;
+ notifications: NotificationsStart | null;
+}
+
+interface SpecificFindingCorrelations {
+ finding: CorrelationFinding;
+ correlatedFindings: CorrelationFinding[];
+}
+
+interface CorrelationsTableData {
+ id: string;
+ startTime: number;
+ correlationRule: string;
+ alertSeverity: string[];
+ logTypes: string[];
+ findingsSeverity: string[];
+ correlatedFindings: CorrelationFinding[];
+}
+
+interface FlyoutTableData {
+ timestamp: string;
+ mitreTactic: string[];
+ detectionRule: string;
+ severity: string;
+}
+
+interface CorrelationsState {
+ recentlyUsedRanges: any[];
+ graphData: CorrelationGraphData;
+ specificFindingInfo?: SpecificFindingCorrelations;
+ logTypeFilterOptions: FilterItem[];
+ severityFilterOptions: FilterItem[];
+ loadingGraphData: boolean;
+ isGraphView: Boolean;
+ correlationsTableData: CorrelationsTableData[];
+ connectedFindings: CorrelationFinding[][];
+ isFlyoutOpen: boolean;
+ selectedTableRow: CorrelationsTableData | null;
+ searchTerm: string;
+}
+
+export const renderTime = (time: number | string) => {
+ const momentTime = moment(time);
+ if (time && momentTime.isValid()) return momentTime.format('MM/DD/YY h:mm a');
+ return DEFAULT_EMPTY_DATA;
+};
+
+export interface CorrelationsTableProps {
+ finding: FindingItemType;
+ correlatedFindings: CorrelationFinding[];
+ history: RouteComponentProps['history'];
+ isLoading: boolean;
+ filterOptions: {
+ logTypes: Set;
+ ruleSeverity: Set;
+ };
+}
+
+export class CorrelationsUpd extends React.Component {
+ private correlationGraphNetwork?: Network;
+
+ constructor(props: CorrelationsProps) {
+ super(props);
+ this.state = {
+ recentlyUsedRanges: [DEFAULT_DATE_RANGE],
+ graphData: { ...emptyGraphData },
+ logTypeFilterOptions: [...getDefaultLogTypeFilterItemOptions()],
+ severityFilterOptions: [...defaultSeverityFilterItemOptions],
+ specificFindingInfo: undefined,
+ loadingGraphData: false,
+ isGraphView: true,
+ correlationsTableData: [],
+ connectedFindings: [],
+ isFlyoutOpen: false,
+ selectedTableRow: null,
+ searchTerm: '',
+ };
+ }
+
+ private get startTime() {
+ return this.props.dateTimeFilter?.startTime || DEFAULT_DATE_RANGE.start;
+ }
+
+ private get endTime() {
+ return this.props.dateTimeFilter?.endTime || DEFAULT_DATE_RANGE.end;
+ }
+
+ private shouldShowFinding(finding: CorrelationFinding) {
+ return (
+ this.state.logTypeFilterOptions.find((option) => option.id === finding.logType)?.checked ===
+ 'on' &&
+ this.state.severityFilterOptions.find(
+ (option) => option.id === finding.detectionRule.severity
+ )?.checked === 'on'
+ );
+ }
+
+ async componentDidMount(): Promise {
+ setBreadcrumbs([BREADCRUMBS.CORRELATIONS_UPD]);
+ this.updateState(true /* onMount */);
+ this.props.onMount();
+ this.fetchCorrelationsTableData();
+ }
+
+ componentDidUpdate(
+ prevProps: Readonly,
+ prevState: Readonly,
+ snapshot?: any
+ ): void {
+ if (prevProps.dataSource !== this.props.dataSource) {
+ this.onRefresh();
+ } else if (
+ prevState.logTypeFilterOptions !== this.state.logTypeFilterOptions ||
+ prevState.severityFilterOptions !== this.state.severityFilterOptions ||
+ prevProps.dateTimeFilter !== this.props.dateTimeFilter
+ ) {
+ this.updateState();
+ }
+ }
+
+ private async updateState(onMount: boolean = false) {
+ if (onMount && this.props.location.state) {
+ const state = this.props.location.state;
+
+ const specificFindingInfo: SpecificFindingCorrelations = {
+ finding: {
+ ...state.finding,
+ id: state.finding.id,
+ logType: state.finding.detector._source.detector_type,
+ timestamp: new Date(state.finding.timestamp).toLocaleString(),
+ detectionRule: {
+ name: (state.finding as any).ruleName,
+ severity: (state.finding as any).ruleSeverity,
+ tags: (state.finding as any).tags,
+ },
+ },
+ correlatedFindings: state.correlatedFindings.filter((finding) =>
+ this.shouldShowFinding(finding)
+ ),
+ };
+
+ if (!this.shouldShowFinding(specificFindingInfo.finding)) {
+ return;
+ }
+
+ this.setState({ specificFindingInfo });
+
+ // create graph data here
+ this.updateGraphDataState(specificFindingInfo);
+ } else {
+ // get all correlations and display them in the graph
+ const start = datemath.parse(this.startTime);
+ const end = datemath.parse(this.endTime);
+ const startTime = start?.valueOf() || Date.now();
+ const endTime = end?.valueOf() || Date.now();
+ this.setState({ loadingGraphData: true });
+ let allCorrelations = await DataStore.correlations.getAllCorrelationsInWindow(
+ startTime.toString(),
+ endTime.toString()
+ );
+ this.setState({ loadingGraphData: false });
+ allCorrelations = allCorrelations.filter((corr) => {
+ return this.shouldShowFinding(corr.finding1) && this.shouldShowFinding(corr.finding2);
+ });
+ const createdEdges = new Set();
+ const createdNodes = new Set();
+ const graphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.onNodeClick,
+ },
+ };
+ allCorrelations.forEach((correlation) => {
+ const possibleCombination1 = `${correlation.finding1.id}:${correlation.finding2.id}`;
+ const possibleCombination2 = `${correlation.finding2.id}:${correlation.finding1.id}`;
+
+ if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
+ return;
+ }
+
+ if (!createdNodes.has(correlation.finding1.id)) {
+ this.addNode(graphData.graph.nodes, correlation.finding1);
+ createdNodes.add(correlation.finding1.id);
+ }
+ if (!createdNodes.has(correlation.finding2.id)) {
+ this.addNode(graphData.graph.nodes, correlation.finding2);
+ createdNodes.add(correlation.finding2.id);
+ }
+ this.addEdge(graphData.graph.edges, correlation.finding1, correlation.finding2);
+ createdEdges.add(possibleCombination1);
+ });
+
+ this.setState({ graphData, specificFindingInfo: undefined });
+ }
+ }
+
+ private onNodeClick = async (params: any) => {
+ if (params.nodes.length !== 1) {
+ return;
+ }
+
+ const findingId = params.nodes[0];
+
+ if (this.state.specificFindingInfo?.finding.id === findingId) {
+ return;
+ }
+
+ this.setState({ loadingGraphData: true });
+
+ let detectorType: string;
+ const node = this.state.graphData.graph.nodes.find((node) => node.id === findingId)!;
+
+ if (node) {
+ detectorType = node.saLogType;
+ } else {
+ const finding = (await DataStore.findings.getFindingsByIds([findingId]))[0];
+ detectorType = finding?.detectionType;
+ }
+
+ if (!detectorType) {
+ errorNotificationToast(this.props.notifications, 'show', 'correlated findings');
+ return;
+ }
+
+ const correlatedFindingsInfo = await DataStore.correlations.getCorrelatedFindings(
+ findingId,
+ detectorType
+ );
+ const correlationRules = await DataStore.correlations.getCorrelationRules();
+ correlatedFindingsInfo.correlatedFindings = correlatedFindingsInfo.correlatedFindings.map(
+ (finding) => {
+ return {
+ ...finding,
+ correlationRule: correlationRules.find((rule) => finding.rules?.indexOf(rule.id) !== -1),
+ };
+ }
+ );
+ this.setState({ specificFindingInfo: correlatedFindingsInfo, loadingGraphData: false });
+ this.updateGraphDataState(correlatedFindingsInfo);
+ };
+
+ private updateGraphDataState(specificFindingInfo: SpecificFindingCorrelations) {
+ const graphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.onNodeClick,
+ },
+ };
+
+ this.addNode(graphData.graph.nodes, specificFindingInfo.finding);
+ const addedEdges = new Set();
+ const nodeIds = new Set();
+ specificFindingInfo.correlatedFindings.forEach((finding) => {
+ if (!nodeIds.has(finding.id)) {
+ this.addNode(graphData.graph.nodes, finding);
+ nodeIds.add(finding.id);
+ }
+
+ const possibleCombination1 = `${specificFindingInfo.finding.id}:${finding.id}`;
+ const possibleCombination2 = `${finding.id}:${specificFindingInfo.finding.id}`;
+ if (addedEdges.has(possibleCombination1) || addedEdges.has(possibleCombination2)) {
+ return;
+ }
+ this.addEdge(graphData.graph.edges, specificFindingInfo.finding, finding);
+ addedEdges.add(possibleCombination1);
+ });
+
+ this.setState({ graphData });
+ }
+
+ private addNode(nodes: any[], finding: CorrelationFinding) {
+ const borderColor = getSeverityColor(finding.detectionRule.severity).background;
+
+ nodes.push({
+ id: finding.id,
+ label: getLogTypeLabel(finding.logType),
+ title: this.createNodeTooltip(finding),
+ color: {
+ background: borderColor,
+ border: borderColor,
+ highlight: {
+ background: '#e7f5ff',
+ border: borderColor,
+ },
+ hover: {
+ background: '#e7f5ff',
+ border: borderColor,
+ },
+ },
+ size: 17,
+ borderWidth: 2,
+ font: {
+ multi: 'html',
+ size: 12,
+ },
+ chosen: true,
+ saLogType: finding.logType,
+ });
+ }
+
+ private addEdge(edges: any[], f1: CorrelationFinding, f2: CorrelationFinding) {
+ edges.push({
+ from: f1.id,
+ to: f2.id,
+ id: `${f1.id}:${f2.id}`,
+ chosen: false,
+ color: '#98A2B3', //ouiColorMediumShade,
+ label: f1.correlationScore || f2.correlationScore || '',
+ width: 2,
+ });
+ }
+
+ private createNodeTooltip = ({ detectionRule, timestamp, logType }: CorrelationFinding) => {
+ const { text, background } = getSeverityColor(detectionRule.severity);
+ const tooltipContent = (
+
+
+
+
+ {getSeverityLabel(detectionRule.severity)}
+
+
+
+ {getLabelFromLogType(logType)}
+
+
+
+
{timestamp}
+
+
Detection rule
+
+
{detectionRule.name}
+
+ );
+
+ const tooltipContentHTML = renderToStaticMarkup(tooltipContent);
+
+ const tooltip = document.createElement('div');
+ tooltip.innerHTML = tooltipContentHTML;
+
+ return tooltip.firstElementChild;
+ };
+
+ private onTimeChange = ({ start, end }: { start: string; end: string }) => {
+ let { recentlyUsedRanges } = this.state;
+ recentlyUsedRanges = recentlyUsedRanges.filter(
+ (range) => !(range.start === start && range.end === end)
+ );
+ recentlyUsedRanges.unshift({ start: start, end: end });
+ if (recentlyUsedRanges.length > MAX_RECENTLY_USED_TIME_RANGES)
+ recentlyUsedRanges = recentlyUsedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES);
+ const endTime = start === end ? DEFAULT_DATE_RANGE.end : end;
+ this.setState({
+ recentlyUsedRanges: recentlyUsedRanges,
+ });
+
+ this.props.setDateTimeFilter &&
+ this.props.setDateTimeFilter({
+ startTime: start,
+ endTime: endTime,
+ });
+ };
+
+ private onRefresh = () => {
+ this.updateState();
+ };
+
+ onLogTypeFilterChange = (items: FilterItem[]) => {
+ this.setState(
+ {
+ logTypeFilterOptions: items,
+ },
+ () => {
+ // If there's specific finding info, update the graph
+ if (this.state.specificFindingInfo) {
+ this.updateGraphDataState(this.state.specificFindingInfo);
+ }
+ // Force update to refresh the table with new filters
+ this.forceUpdate();
+ }
+ );
+ };
+
+ onSeverityFilterChange = (items: FilterItem[]) => {
+ this.setState(
+ {
+ severityFilterOptions: items,
+ },
+ () => {
+ // If there's specific finding info, update the graph
+ if (this.state.specificFindingInfo) {
+ this.updateGraphDataState(this.state.specificFindingInfo);
+ }
+ // Force update to refresh the table with new filters
+ this.forceUpdate();
+ }
+ );
+ };
+
+ closeFlyout = () => {
+ this.setState({ specificFindingInfo: undefined });
+ };
+
+ private openTableFlyout = (correlationTableRow: CorrelationsTableData) => {
+ let newGraphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.onNodeClick,
+ },
+ };
+
+ if (correlationTableRow.correlatedFindings) {
+ const correlationPairs = this.getCorrelationPairs(correlationTableRow.correlatedFindings);
+ newGraphData = this.prepareGraphData(correlationPairs);
+ }
+
+ // Set all required state at once
+ this.setState({
+ isFlyoutOpen: true,
+ selectedTableRow: correlationTableRow,
+ graphData: newGraphData,
+ });
+ };
+
+ private closeTableFlyout = () => {
+ this.setState({
+ isFlyoutOpen: false,
+ selectedTableRow: null,
+ graphData: {
+ graph: { nodes: [], edges: [] },
+ events: { click: this.onNodeClick },
+ },
+ });
+ };
+
+ onFindingInspect = async (id: string, logType: string) => {
+ // get finding data and set the specificFindingInfo
+ const specificFindingInfo = await DataStore.correlations.getCorrelatedFindings(id, logType);
+ this.setState({ specificFindingInfo });
+ this.updateGraphDataState(specificFindingInfo);
+ this.correlationGraphNetwork?.selectNodes([id], false);
+ };
+
+ resetFilters = () => {
+ this.setState({
+ logTypeFilterOptions: this.state.logTypeFilterOptions.map((option) => ({
+ ...option,
+ checked: 'on',
+ })),
+ severityFilterOptions: this.state.severityFilterOptions.map((option) => ({
+ ...option,
+ checked: 'on',
+ })),
+ specificFindingInfo: undefined,
+ });
+ };
+
+ setNetwork = (network: Network) => {
+ this.correlationGraphNetwork = network;
+ network.on('hoverNode', function (params) {
+ network.canvas.body.container.style.cursor = 'pointer';
+ });
+ network.on('blurNode', function (params) {
+ network.canvas.body.container.style.cursor = 'default';
+ });
+ };
+
+ renderCorrelationsGraph(loadingData: boolean) {
+ return this.state.graphData.graph.nodes.length > 0 || loadingData ? (
+ <>
+
+
+
+ Severity:
+
+
+ {ruleSeverity.map((sev, idx) => (
+
+
+ {sev.value}
+
+
+ ))}
+
+
+
+ >
+ ) : (
+
+ No correlations found
+
+ }
+ body={
+
+ There are no correlated findings in the system.
+
+ }
+ actions={[
+
+ Create correlation rule
+ ,
+ ]}
+ />
+ );
+ }
+
+ private mapConnectedCorrelations(
+ correlations: {
+ finding1: CorrelationFinding;
+ finding2: CorrelationFinding;
+ }[]
+ ) {
+ const connectionsMap = new Map>();
+ const findingsMap = new Map();
+
+ correlations.forEach((correlation) => {
+ const { finding1, finding2 } = correlation;
+
+ findingsMap.set(finding1.id, finding1);
+ findingsMap.set(finding2.id, finding2);
+
+ if (!connectionsMap.has(finding1.id)) {
+ connectionsMap.set(finding1.id, new Set());
+ }
+ connectionsMap.get(finding1.id)!.add(finding2.id);
+
+ if (!connectionsMap.has(finding2.id)) {
+ connectionsMap.set(finding2.id, new Set());
+ }
+ connectionsMap.get(finding2.id)!.add(finding1.id);
+ });
+
+ const visited = new Set();
+ const connectedGroups: CorrelationFinding[][] = [];
+
+ function dfs(findingId: string, currentGroup: CorrelationFinding[]) {
+ visited.add(findingId);
+ const finding = findingsMap.get(findingId);
+ if (finding) {
+ currentGroup.push(finding);
+ }
+
+ const connections = connectionsMap.get(findingId) || new Set();
+ connections.forEach((connectedId) => {
+ if (!visited.has(connectedId)) {
+ dfs(connectedId, currentGroup);
+ }
+ });
+ }
+
+ connectionsMap.forEach((_, findingId) => {
+ if (!visited.has(findingId)) {
+ const currentGroup: CorrelationFinding[] = [];
+ dfs(findingId, currentGroup);
+ if (currentGroup.length > 0) {
+ connectedGroups.push(currentGroup);
+ }
+ }
+ });
+
+ return connectedGroups;
+ }
+
+ private fetchCorrelationsTableData = async () => {
+ try {
+ const start = datemath.parse(this.startTime);
+ const end = datemath.parse(this.endTime);
+ const startTime = start?.valueOf() || Date.now();
+ const endTime = end?.valueOf() || Date.now();
+
+ let allCorrelations = await DataStore.correlations.getAllCorrelationsInWindow(
+ startTime.toString(),
+ endTime.toString()
+ );
+
+ const connectedFindings = this.mapConnectedCorrelations(allCorrelations);
+
+ this.setState({ connectedFindings: connectedFindings });
+
+ const tableData: CorrelationsTableData[] = [];
+
+ const allCorrelationRules = await DataStore.correlations.getCorrelationRules();
+ const allCorrelatedAlerts = await DataStore.correlations.getAllCorrelationAlerts();
+
+ const correlationRuleMapsAlerts: { [id: string]: string[] } = {};
+
+ allCorrelationRules.forEach((correlationRule) => {
+ const correlationRuleId = correlationRule.id;
+ correlationRuleMapsAlerts[correlationRuleId] = [];
+
+ allCorrelatedAlerts.correlationAlerts.forEach((correlatedAlert) => {
+ if (correlatedAlert.correlation_rule_id === correlationRuleId) {
+ correlationRuleMapsAlerts[correlationRuleId].push(correlatedAlert.severity);
+ }
+ });
+ });
+
+ for (const findingGroup of connectedFindings) {
+ let correlationRule = '';
+ const logTypes = new Set();
+ const findingsSeverity: string[] = [];
+ let alertsSeverity: string[] = [];
+
+ for (const finding of findingGroup) {
+ findingsSeverity.push(finding.detectionRule.severity);
+ logTypes.add(finding.logType);
+ }
+
+ // Call the APIs only if correlationRule has not been found yet to avoid repeated API calls.
+ if (correlationRule === '') {
+ if (findingGroup[0] && findingGroup[0].detector && findingGroup[0].detector._source) {
+ const correlatedFindingsResponse = await DataStore.correlations.getCorrelatedFindings(
+ findingGroup[0].id,
+ findingGroup[0]?.detector._source?.detector_type
+ );
+
+ // TODO -- GET THE RESOURCES FROM findings and correlatedFindings AND DISPLAY IN THE RESOURCES COLUMN.'
+ // TODO -- GETTING THE EXACT RESOURCE IS NOT FEASIBLE WITH THE CURRENT DATA MODEL, BUT FIND A WAY TO REPRSENT THE OBJECTS.
+ console.log('CORRELATED FINDINGS: ', correlatedFindingsResponse);
+
+ if (
+ correlatedFindingsResponse.correlatedFindings &&
+ correlatedFindingsResponse.correlatedFindings[0] &&
+ correlatedFindingsResponse.correlatedFindings[0].rules
+ ) {
+ const correlationRuleId = correlatedFindingsResponse.correlatedFindings[0].rules[0];
+ const correlationRuleObj =
+ (await DataStore.correlations.getCorrelationRule(correlationRuleId)) || '';
+ console.log('CORRELATION RULE: ', correlationRuleObj);
+ alertsSeverity = correlationRuleMapsAlerts[correlationRuleId];
+ if (correlationRuleObj) {
+ correlationRule = correlationRuleObj.name;
+ }
+ }
+ }
+ }
+
+ tableData.push({
+ id: `${startTime}_${findingGroup[0]?.id}`,
+ startTime: startTime,
+ correlationRule: correlationRule,
+ logTypes: Array.from(logTypes),
+ alertSeverity: alertsSeverity,
+ findingsSeverity: findingsSeverity,
+ correlatedFindings: findingGroup,
+ });
+ }
+
+ this.setState({
+ correlationsTableData: tableData,
+ });
+ } catch (error) {
+ console.error('Failed to fetch correlation rules:', error);
+ }
+ };
+
+ private getCorrelatedFindingsVisualizationSpec = (
+ visualizationData: any[],
+ dateOpts: DateOpts = {
+ timeUnit: defaultTimeUnit,
+ dateFormat: defaultDateFormat,
+ domain: defaultScaleDomain,
+ }
+ ) => {
+ return getVisualizationSpec('Correlated Findings data overview', visualizationData, [
+ addInteractiveLegends({
+ mark: {
+ type: 'bar',
+ clip: true,
+ },
+ encoding: {
+ tooltip: [getYAxis('correlatedFinding', 'Correlated Findings'), getTimeTooltip(dateOpts)],
+ x: getXAxis(dateOpts),
+ y: getYAxis('correlatedFinding', 'Count'),
+ color: {
+ field: 'title',
+ legend: {
+ title: 'Legend',
+ },
+ },
+ },
+ }),
+ ]);
+ };
+
+ private generateVisualizationSpec = (connectedFindings: CorrelationFinding[][]) => {
+ const visData = connectedFindings.map((correlatedFindings) => {
+ return {
+ title: 'Correlated Findings',
+ correlatedFinding: correlatedFindings.length,
+ time: correlatedFindings[0].timestamp,
+ };
+ });
+
+ const {
+ dateTimeFilter = {
+ startTime: DEFAULT_DATE_RANGE.start,
+ endTime: DEFAULT_DATE_RANGE.end,
+ },
+ } = this.props;
+
+ const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime);
+
+ return this.getCorrelatedFindingsVisualizationSpec(visData, {
+ timeUnit: chartTimeUnits.timeUnit,
+ dateFormat: chartTimeUnits.dateFormat,
+ domain: getDomainRange(
+ [dateTimeFilter.startTime, dateTimeFilter.endTime],
+ chartTimeUnits.timeUnit.unit
+ ),
+ });
+ };
+
+ private renderCorrelatedFindingsChart = () => {
+ renderVisualization(
+ this.generateVisualizationSpec(this.state.connectedFindings),
+ 'correlated-findings-view'
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ Correlated Findings
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ };
+
+ private getFilteredTableData = (tableData: CorrelationsTableData[]): CorrelationsTableData[] => {
+ const { logTypeFilterOptions, severityFilterOptions } = this.state;
+ const alertSeverityMap: { [key: string]: string } = {
+ '1': 'critical',
+ '2': 'high',
+ '3': 'medium',
+ '4': 'low',
+ '5': 'informational',
+ };
+
+ const selectedLogTypes = logTypeFilterOptions
+ .filter((item) => item.checked === 'on' && item.visible)
+ .map((item) => item.id);
+
+ const selectedSeverities = severityFilterOptions
+ .filter((item) => item.checked === 'on' && item.visible)
+ .map((item) => item.id.toLowerCase());
+
+ return tableData.filter((row) => {
+ const logTypeMatch = row.logTypes.some((logType) => selectedLogTypes.includes(logType));
+
+ const severityMatch = row.alertSeverity.some((severity) =>
+ selectedSeverities.includes(alertSeverityMap[severity])
+ );
+
+ const searchLower = this.state.searchTerm.toLowerCase();
+ const searchMatch =
+ this.state.searchTerm === '' ||
+ row.correlationRule?.toLowerCase().includes(searchLower) ||
+ row.logTypes.some((type) => type.toLowerCase().includes(searchLower)) ||
+ row.alertSeverity.some((severity) =>
+ alertSeverityMap[severity].toLowerCase().includes(searchLower)
+ ) ||
+ row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower));
+ return logTypeMatch && severityMatch && searchMatch;
+ });
+ };
+
+ private debouncedSearch = debounce((searchTerm: string) => {
+ this.setState({ searchTerm }, () => {
+ this.forceUpdate();
+ });
+ }, 300);
+
+ private renderSearchBar = () => {
+ return (
+ {
+ e.persist();
+ const searchValue = e.target.value;
+ this.setState({ searchTerm: searchValue });
+ this.debouncedSearch(searchValue);
+ }}
+ fullWidth={true}
+ isClearable={true}
+ compressed={true}
+ aria-label="Search correlations"
+ />
+ );
+ };
+
+ private renderCorrelationsTable = () => {
+ const alertSeverityMap: { [key: string]: string } = {
+ '1': 'critical',
+ '2': 'high',
+ '3': 'medium',
+ '4': 'low',
+ '5': 'informational',
+ };
+
+ const columns: EuiBasicTableColumn[] = [
+ {
+ field: 'startTime',
+ name: 'Start time',
+ sortable: true,
+ dataType: 'date',
+ render: (startTime: number) => {
+ return new Date(startTime).toLocaleString();
+ },
+ },
+ {
+ field: 'correlationRule',
+ name: 'Correlation Rule',
+ sortable: true,
+ render: (name: string) => name || 'N/A',
+ },
+ {
+ field: 'logTypes',
+ name: 'Log Types',
+ sortable: true,
+ render: (logTypes: string[]) => {
+ if (!logTypes || logTypes.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount = logTypes.length > MAX_DISPLAY ? logTypes.length - MAX_DISPLAY : 0;
+ const displayedLogTypes = logTypes.slice(0, MAX_DISPLAY).map((logType) => {
+ const label = logType;
+ return {label};
+ });
+ const tooltipContent = (
+ <>
+ {logTypes.slice(MAX_DISPLAY).map((logType) => {
+ const label = logType;
+ return (
+
+ {label}
+
+ );
+ })}
+ >
+ );
+ return (
+
+ {displayedLogTypes}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
+ {
+ field: 'alertSeverity',
+ name: 'Alert Severity',
+ sortable: true,
+ render: (alertSeverity: string[]) => {
+ if (!alertSeverity || alertSeverity.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount =
+ alertSeverity.length > MAX_DISPLAY ? alertSeverity.length - MAX_DISPLAY : 0;
+ const displayedSeverities = alertSeverity.slice(0, MAX_DISPLAY).map((severity) => {
+ const label = alertSeverityMap[severity];
+ const { background, text } = getSeverityColor(label);
+ return (
+
+ {label}
+
+ );
+ });
+
+ const tooltipContent = (
+ <>
+ {alertSeverity.slice(MAX_DISPLAY).map((severity) => {
+ const label = alertSeverityMap[severity];
+ const { background, text } = getSeverityColor(label);
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+ >
+ );
+
+ return (
+
+ {displayedSeverities}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
+ {
+ field: 'findingsSeverity',
+ name: 'Findings Severity',
+ sortable: true,
+ render: (findingsSeverity: string[]) => {
+ if (!findingsSeverity || findingsSeverity.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount =
+ findingsSeverity.length > MAX_DISPLAY ? findingsSeverity.length - MAX_DISPLAY : 0;
+ const displayedSeverities = findingsSeverity.slice(0, MAX_DISPLAY).map((severity) => {
+ const label = getSeverityLabel(severity);
+ const { background, text } = getSeverityColor(label);
+ return (
+
+ {label}
+
+ );
+ });
+
+ const tooltipContent = (
+
+ {findingsSeverity.slice(MAX_DISPLAY).map((severity) => {
+ const label = getSeverityLabel(severity);
+ const { background, text } = getSeverityColor(label);
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+
+ );
+
+ return (
+
+ {displayedSeverities}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
+ {
+ field: 'actions',
+ name: 'Actions',
+ render: (_, correlationTableRow: CorrelationsTableData) => {
+ return (
+
+ {
+ this.openTableFlyout(correlationTableRow);
+ }}
+ />
+
+ );
+ },
+ },
+ ];
+
+ const getRowProps = (item: any) => {
+ return {
+ 'data-test-subj': `row-${item.id}`,
+ key: item.id,
+ className: 'euiTableRow',
+ };
+ };
+
+ const filteredTableData = this.getFilteredTableData(this.state.correlationsTableData);
+
+ return (
+ <>
+ {this.renderCorrelatedFindingsChart()}
+
+
+ >
+ );
+ };
+
+ private prepareGraphData = (correlationPairs: CorrelationFinding[][] | [any, any][]) => {
+ const createdEdges = new Set();
+ const createdNodes = new Set();
+ const graphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.onNodeClick,
+ },
+ };
+
+ correlationPairs.forEach((correlation: CorrelationFinding[]) => {
+ const possibleCombination1 = `${correlation[0].id}:${correlation[1].id}`;
+ const possibleCombination2 = `${correlation[1].id}:${correlation[0].id}`;
+
+ if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
+ return;
+ }
+
+ if (!createdNodes.has(correlation[0].id)) {
+ this.addNode(graphData.graph.nodes, correlation[0]);
+ createdNodes.add(correlation[0].id);
+ }
+ if (!createdNodes.has(correlation[1].id)) {
+ this.addNode(graphData.graph.nodes, correlation[1]);
+ createdNodes.add(correlation[1].id);
+ }
+ this.addEdge(graphData.graph.edges, correlation[0], correlation[1]);
+ createdEdges.add(possibleCombination1);
+ });
+
+ return graphData;
+ };
+
+ private getCorrelationPairs = (correlatedFindings: any[]) => {
+ const pairs: [any, any][] = [];
+ for (let i = 0; i < correlatedFindings.length; i++) {
+ for (let j = i + 1; j < correlatedFindings.length; j++) {
+ pairs.push([correlatedFindings[i], correlatedFindings[j]]);
+ }
+ }
+ return pairs;
+ };
+
+ private renderTableFlyout = () => {
+ const { isFlyoutOpen, selectedTableRow, graphData } = this.state;
+
+ if (!isFlyoutOpen || !selectedTableRow) {
+ return null;
+ }
+
+ const findingsTableColumns: EuiBasicTableColumn[] = [
+ {
+ field: 'timestamp',
+ name: 'Time',
+ render: (timestamp: string) => new Date(timestamp).toLocaleString(),
+ sortable: true,
+ },
+ {
+ field: 'mitreTactic',
+ name: 'Mitre Tactic',
+ sortable: true,
+ },
+ {
+ field: 'detectionRule',
+ name: 'Detection Rule',
+ sortable: true,
+ },
+ {
+ field: 'severity',
+ name: 'Severity',
+ render: (ruleSeverity: string) => {
+ const severity = capitalizeFirstLetter(ruleSeverity) || DEFAULT_EMPTY_DATA;
+ const { background, text } = getSeverityColor(severity);
+
+ return (
+
+ {severity}
+
+ );
+ },
+ },
+ ];
+
+ const flyoutTableData: FlyoutTableData[] = [];
+
+ selectedTableRow.correlatedFindings.map((correlatedFinding) => {
+ flyoutTableData.push({
+ timestamp: correlatedFinding.timestamp,
+ mitreTactic:
+ correlatedFinding.detectionRule.tags?.map((mitreTactic) => mitreTactic.value) || [],
+ detectionRule: correlatedFinding.detectionRule.name,
+ severity: correlatedFinding.detectionRule.severity,
+ });
+ });
+
+ return (
+
+
+
+ Correlation Details
+
+
+
+
+
+
+ Time
+
+ {new Date(selectedTableRow.startTime).toLocaleString()}
+
+
+
+
+
+
+ Correlation Rule
+
+ {selectedTableRow.correlationRule}
+
+
+
+
+
+
+
+
+
+ Correlated Findings
+
+ {selectedTableRow.correlatedFindings && (
+
+ )}
+
+
+
+ Findings
+
+
+
+
+ {Array.from(
+ new Set(
+ selectedTableRow.correlatedFindings.flatMap(
+ (finding) => finding.detectionRule.tags?.map((tag) => tag.value) || []
+ )
+ )
+ ).map((tactic, i) => {
+ const link = `https://attack.mitre.org/techniques/${tactic
+ .split('.')
+ .slice(1)
+ .join('/')
+ .toUpperCase()}`;
+
+ return (
+
+
+
+ {tactic}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+ };
+
+ toggleView = () => {
+ this.setState((prevState) => ({
+ isGraphView: !prevState.isGraphView,
+ }));
+ };
+
+ render() {
+ const findingCardsData = this.state.specificFindingInfo;
+ const datePicker = (
+
+ );
+
+ return (
+ <>
+ {findingCardsData ? (
+
+
+
+
+
+ Correlation
+
+
+
+
+
+
+
+
+
+
+
+
+ Correlated Findings ({findingCardsData.correlatedFindings.length})
+
+
+ Higher correlation score indicated stronger correlation.
+
+
+
+ {findingCardsData.correlatedFindings.map((finding, index) => {
+ return (
+ <>
+
+
+ >
+ );
+ })}
+
+
+ ) : null}
+
+
+
+
+
+
+ Correlations
+
+
+ {datePicker}
+
+
+
+
+
+
+ {this.renderSearchBar()}
+
+
+
+
+
+
+
+
+ Reset filters
+
+
+
+ this.setState({ isGraphView: id === 'graph' })}
+ buttonSize="s"
+ />
+
+
+
+ {this.state.isGraphView
+ ? this.renderCorrelationsGraph(this.state.loadingGraphData)
+ : this.renderCorrelationsTable()}
+
+
+
+ {this.renderTableFlyout()}
+ >
+ );
+ }
+}
diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx
index fc56a0fd..00fba5fc 100644
--- a/public/pages/Main/Main.tsx
+++ b/public/pages/Main/Main.tsx
@@ -48,6 +48,7 @@ import { DataStore } from '../../store/DataStore';
import { CreateCorrelationRule } from '../Correlations/containers/CreateCorrelationRule';
import { CorrelationRules } from '../Correlations/containers/CorrelationRules';
import { Correlations } from '../Correlations/containers/CorrelationsContainer';
+import { CorrelationsUpd } from '../Correlations/containers/CorrelationsContainerUpd';
import { LogTypes } from '../LogTypes/containers/LogTypes';
import { LogType } from '../LogTypes/containers/LogType';
import { CreateLogType } from '../LogTypes/containers/CreateLogType';
@@ -85,6 +86,7 @@ enum Navigation {
Overview = 'Overview',
Alerts = 'Alerts',
Correlations = 'Correlations',
+ CorrelationsUpd = 'Correlations Updated',
CorrelationRules = 'Correlation rules',
LogTypes = 'Log types',
ThreatIntel = 'Threat Intelligence',
@@ -428,6 +430,44 @@ export default class Main extends Component {
},
],
},
+ {
+ name: Navigation.CorrelationsUpd,
+ id: Navigation.CorrelationsUpd,
+ onClick: () => {
+ this.setState({ selectedNavItemId: Navigation.CorrelationsUpd });
+ history.push(ROUTES.CORRELATIONS_UPD);
+ },
+ renderItem: (props: any) => {
+ return (
+
+
+ {
+ this.setState({ selectedNavItemId: Navigation.CorrelationsUpd });
+ history.push(ROUTES.CORRELATIONS_UPD);
+ }}
+ >
+ {props.children}
+
+
+
+ );
+ },
+ isSelected: selectedNavItemId === Navigation.CorrelationsUpd,
+ forceOpen: true,
+ items: [
+ {
+ name: Navigation.CorrelationRules,
+ id: Navigation.CorrelationRules,
+ onClick: () => {
+ this.setState({ selectedNavItemId: Navigation.CorrelationRules });
+ history.push(ROUTES.CORRELATION_RULES);
+ },
+ isSelected: selectedNavItemId === Navigation.CorrelationRules,
+ },
+ ],
+ },
],
},
];
@@ -775,6 +815,26 @@ export default class Main extends Component {
);
}}
/>
+ ) => {
+ return (
+
+ this.setState({
+ selectedNavItemId: Navigation.CorrelationsUpd,
+ })
+ }
+ dateTimeFilter={this.state.dateTimeFilter}
+ setDateTimeFilter={this.setDateTimeFilter}
+ dataSource={selectedDataSource}
+ notifications={core?.notifications}
+ />
+ );
+ }}
+ />
) => (
diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts
index 28b36931..d533d9b8 100644
--- a/public/store/CorrelationsStore.ts
+++ b/public/store/CorrelationsStore.ts
@@ -150,7 +150,7 @@ export class CorrelationsStore implements ICorrelationsStore {
name: hit._source.name,
time_window: hit._source.time_window || 300000,
queries,
- trigger: hit._source?.trigger
+ trigger: hit._source?.trigger,
};
}
@@ -176,7 +176,7 @@ export class CorrelationsStore implements ICorrelationsStore {
name: hit._source.name,
time_window: hit._source.time_window || 300000,
queries,
- trigger: hit._source?.trigger
+ trigger: hit._source?.trigger,
};
});
}
diff --git a/public/utils/constants.ts b/public/utils/constants.ts
index dec93b83..0942e533 100644
--- a/public/utils/constants.ts
+++ b/public/utils/constants.ts
@@ -58,6 +58,7 @@ export const ROUTES = Object.freeze({
EDIT_FIELD_MAPPINGS: '/edit-field-mappings',
EDIT_DETECTOR_ALERT_TRIGGERS: '/edit-alert-triggers',
CORRELATIONS: '/correlations',
+ CORRELATIONS_UPD: '/correlations-upd',
CORRELATION_RULES: '/correlations/rules',
CORRELATION_RULE_CREATE: '/correlations/create-rule',
CORRELATION_RULE_EDIT: '/correlations/rule',
@@ -105,6 +106,7 @@ export const BREADCRUMBS = Object.freeze({
RULES_DUPLICATE: { text: 'Duplicate rule', href: `#${ROUTES.RULES_DUPLICATE}` },
RULES_IMPORT: { text: 'Import rule', href: `#${ROUTES.RULES_IMPORT}` },
CORRELATIONS: { text: 'Correlations', href: `#${ROUTES.CORRELATIONS}` },
+ CORRELATIONS_UPD: { text: 'Correlations Updated', href: `#${ROUTES.CORRELATIONS_UPD}` },
CORRELATION_RULES: { text: 'Correlation rules', href: `#${ROUTES.CORRELATION_RULES}` },
CORRELATIONS_RULE_CREATE: (action: string) => ({
text: `${action} correlation rule`,
From c69efa5856a3c86331e40449d27eed998b274a40 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 10:53:15 -0800
Subject: [PATCH 02/19] fix: remove log statements
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../pages/Correlations/containers/CorrelationsContainerUpd.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx b/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
index 9af8b18b..f51c1664 100644
--- a/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
@@ -733,7 +733,6 @@ export class CorrelationsUpd extends React.Component
Date: Tue, 28 Jan 2025 11:19:43 -0800
Subject: [PATCH 03/19] fix: update changes to original component
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../components/MDS/DataSourceMenuWrapper.tsx | 1 -
.../containers/CorrelationsContainer.tsx | 955 ++++++++++-
.../containers/CorrelationsContainerUpd.tsx | 1527 -----------------
public/pages/Main/Main.tsx | 60 -
public/utils/constants.ts | 2 -
5 files changed, 925 insertions(+), 1620 deletions(-)
delete mode 100644 public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
diff --git a/public/components/MDS/DataSourceMenuWrapper.tsx b/public/components/MDS/DataSourceMenuWrapper.tsx
index fae41f34..69ddc841 100644
--- a/public/components/MDS/DataSourceMenuWrapper.tsx
+++ b/public/components/MDS/DataSourceMenuWrapper.tsx
@@ -153,7 +153,6 @@ export const DataSourceMenuWrapper: React.FC = ({
ROUTES.LOG_TYPES,
ROUTES.RULES,
ROUTES.CORRELATIONS,
- ROUTES.CORRELATIONS_UPD,
ROUTES.CORRELATION_RULES,
ROUTES.RULES_CREATE,
ROUTES.RULES_IMPORT,
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 87e691c3..fe6ce913 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -39,6 +39,13 @@ import {
EuiBadge,
EuiFilterGroup,
EuiHorizontalRule,
+ EuiButtonGroup,
+ EuiBasicTableColumn,
+ EuiToolTip,
+ EuiInMemoryTable,
+ EuiTextColor,
+ EuiLink,
+ EuiFieldSearch,
} from '@elastic/eui';
import { FilterItem, FilterGroup } from '../components/FilterGroup';
import {
@@ -56,8 +63,31 @@ import { renderToStaticMarkup } from 'react-dom/server';
import { Network } from 'react-graph-vis';
import { getLogTypeLabel } from '../../LogTypes/utils/helpers';
import { NotificationsStart } from 'opensearch-dashboards/public';
-import { errorNotificationToast, setBreadcrumbs } from '../../../utils/helpers';
+import {
+ capitalizeFirstLetter,
+ errorNotificationToast,
+ renderVisualization,
+ setBreadcrumbs,
+} from '../../../utils/helpers';
import { PageHeader } from '../../../components/PageHeader/PageHeader';
+import moment from 'moment';
+import { ChartContainer } from '../../../components/Charts/ChartContainer';
+import {
+ addInteractiveLegends,
+ DateOpts,
+ defaultDateFormat,
+ defaultScaleDomain,
+ defaultTimeUnit,
+ getChartTimeUnit,
+ getDomainRange,
+ getTimeTooltip,
+ getVisualizationSpec,
+ getXAxis,
+ getYAxis,
+} from '../../Overview/utils/helpers';
+import { debounce } from 'lodash';
+
+export const DEFAULT_EMPTY_DATA = '-';
interface CorrelationsProps
extends RouteComponentProps<
@@ -77,6 +107,23 @@ interface SpecificFindingCorrelations {
correlatedFindings: CorrelationFinding[];
}
+interface CorrelationsTableData {
+ id: string;
+ startTime: number;
+ correlationRule: string;
+ alertSeverity: string[];
+ logTypes: string[];
+ findingsSeverity: string[];
+ correlatedFindings: CorrelationFinding[];
+}
+
+interface FlyoutTableData {
+ timestamp: string;
+ mitreTactic: string[];
+ detectionRule: string;
+ severity: string;
+}
+
interface CorrelationsState {
recentlyUsedRanges: any[];
graphData: CorrelationGraphData;
@@ -84,6 +131,29 @@ interface CorrelationsState {
logTypeFilterOptions: FilterItem[];
severityFilterOptions: FilterItem[];
loadingGraphData: boolean;
+ isGraphView: Boolean;
+ correlationsTableData: CorrelationsTableData[];
+ connectedFindings: CorrelationFinding[][];
+ isFlyoutOpen: boolean;
+ selectedTableRow: CorrelationsTableData | null;
+ searchTerm: string;
+}
+
+export const renderTime = (time: number | string) => {
+ const momentTime = moment(time);
+ if (time && momentTime.isValid()) return momentTime.format('MM/DD/YY h:mm a');
+ return DEFAULT_EMPTY_DATA;
+};
+
+export interface CorrelationsTableProps {
+ finding: FindingItemType;
+ correlatedFindings: CorrelationFinding[];
+ history: RouteComponentProps['history'];
+ isLoading: boolean;
+ filterOptions: {
+ logTypes: Set;
+ ruleSeverity: Set;
+ };
}
export class Correlations extends React.Component {
@@ -98,6 +168,12 @@ export class Correlations extends React.Component {
- this.setState({ logTypeFilterOptions: items });
+ this.setState(
+ {
+ logTypeFilterOptions: items,
+ },
+ () => {
+ // If there's specific finding info, update the graph
+ if (this.state.specificFindingInfo) {
+ this.updateGraphDataState(this.state.specificFindingInfo);
+ }
+ // Force update to refresh the table with new filters
+ this.forceUpdate();
+ }
+ );
};
onSeverityFilterChange = (items: FilterItem[]) => {
- this.setState({ severityFilterOptions: items });
+ this.setState(
+ {
+ severityFilterOptions: items,
+ },
+ () => {
+ // If there's specific finding info, update the graph
+ if (this.state.specificFindingInfo) {
+ this.updateGraphDataState(this.state.specificFindingInfo);
+ }
+ // Force update to refresh the table with new filters
+ this.forceUpdate();
+ }
+ );
};
closeFlyout = () => {
this.setState({ specificFindingInfo: undefined });
};
+ private openTableFlyout = (correlationTableRow: CorrelationsTableData) => {
+ let newGraphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.onNodeClick,
+ },
+ };
+
+ if (correlationTableRow.correlatedFindings) {
+ const correlationPairs = this.getCorrelationPairs(correlationTableRow.correlatedFindings);
+ newGraphData = this.prepareGraphData(correlationPairs);
+ }
+
+ // Set all required state at once
+ this.setState({
+ isFlyoutOpen: true,
+ selectedTableRow: correlationTableRow,
+ graphData: newGraphData,
+ });
+ };
+
+ private closeTableFlyout = () => {
+ this.setState({
+ isFlyoutOpen: false,
+ selectedTableRow: null,
+ graphData: {
+ graph: { nodes: [], edges: [] },
+ events: { click: this.onNodeClick },
+ },
+ });
+ };
+
onFindingInspect = async (id: string, logType: string) => {
// get finding data and set the specificFindingInfo
const specificFindingInfo = await DataStore.correlations.getCorrelatedFindings(id, logType);
@@ -440,13 +576,30 @@ export class Correlations extends React.Component 0 || loadingData ? (
-
+ <>
+
+
+
+ Severity:
+
+
+ {ruleSeverity.map((sev, idx) => (
+
+
+ {sev.value}
+
+
+ ))}
+
+
+
+ >
) : (
>();
+ const findingsMap = new Map();
+
+ correlations.forEach((correlation) => {
+ const { finding1, finding2 } = correlation;
+
+ findingsMap.set(finding1.id, finding1);
+ findingsMap.set(finding2.id, finding2);
+
+ if (!connectionsMap.has(finding1.id)) {
+ connectionsMap.set(finding1.id, new Set());
+ }
+ connectionsMap.get(finding1.id)!.add(finding2.id);
+
+ if (!connectionsMap.has(finding2.id)) {
+ connectionsMap.set(finding2.id, new Set());
+ }
+ connectionsMap.get(finding2.id)!.add(finding1.id);
+ });
+
+ const visited = new Set();
+ const connectedGroups: CorrelationFinding[][] = [];
+
+ function dfs(findingId: string, currentGroup: CorrelationFinding[]) {
+ visited.add(findingId);
+ const finding = findingsMap.get(findingId);
+ if (finding) {
+ currentGroup.push(finding);
+ }
+
+ const connections = connectionsMap.get(findingId) || new Set();
+ connections.forEach((connectedId) => {
+ if (!visited.has(connectedId)) {
+ dfs(connectedId, currentGroup);
+ }
+ });
+ }
+
+ connectionsMap.forEach((_, findingId) => {
+ if (!visited.has(findingId)) {
+ const currentGroup: CorrelationFinding[] = [];
+ dfs(findingId, currentGroup);
+ if (currentGroup.length > 0) {
+ connectedGroups.push(currentGroup);
+ }
+ }
+ });
+
+ return connectedGroups;
+ }
+
+ private fetchCorrelationsTableData = async () => {
+ try {
+ const start = datemath.parse(this.startTime);
+ const end = datemath.parse(this.endTime);
+ const startTime = start?.valueOf() || Date.now();
+ const endTime = end?.valueOf() || Date.now();
+
+ let allCorrelations = await DataStore.correlations.getAllCorrelationsInWindow(
+ startTime.toString(),
+ endTime.toString()
+ );
+
+ const connectedFindings = this.mapConnectedCorrelations(allCorrelations);
+
+ this.setState({ connectedFindings: connectedFindings });
+
+ const tableData: CorrelationsTableData[] = [];
+
+ const allCorrelationRules = await DataStore.correlations.getCorrelationRules();
+ const allCorrelatedAlerts = await DataStore.correlations.getAllCorrelationAlerts();
+
+ const correlationRuleMapsAlerts: { [id: string]: string[] } = {};
+
+ allCorrelationRules.forEach((correlationRule) => {
+ const correlationRuleId = correlationRule.id;
+ correlationRuleMapsAlerts[correlationRuleId] = [];
+
+ allCorrelatedAlerts.correlationAlerts.forEach((correlatedAlert) => {
+ if (correlatedAlert.correlation_rule_id === correlationRuleId) {
+ correlationRuleMapsAlerts[correlationRuleId].push(correlatedAlert.severity);
+ }
+ });
+ });
+
+ for (const findingGroup of connectedFindings) {
+ let correlationRule = '';
+ const logTypes = new Set();
+ const findingsSeverity: string[] = [];
+ let alertsSeverity: string[] = [];
+
+ for (const finding of findingGroup) {
+ findingsSeverity.push(finding.detectionRule.severity);
+ logTypes.add(finding.logType);
+ }
+
+ // Call the APIs only if correlationRule has not been found yet to avoid repeated API calls.
+ if (correlationRule === '') {
+ if (findingGroup[0] && findingGroup[0].detector && findingGroup[0].detector._source) {
+ const correlatedFindingsResponse = await DataStore.correlations.getCorrelatedFindings(
+ findingGroup[0].id,
+ findingGroup[0]?.detector._source?.detector_type
+ );
+ if (
+ correlatedFindingsResponse.correlatedFindings &&
+ correlatedFindingsResponse.correlatedFindings[0] &&
+ correlatedFindingsResponse.correlatedFindings[0].rules
+ ) {
+ const correlationRuleId = correlatedFindingsResponse.correlatedFindings[0].rules[0];
+ const correlationRuleObj =
+ (await DataStore.correlations.getCorrelationRule(correlationRuleId)) || '';
+ alertsSeverity = correlationRuleMapsAlerts[correlationRuleId];
+ if (correlationRuleObj) {
+ correlationRule = correlationRuleObj.name;
+ }
+ }
+ }
+ }
+
+ tableData.push({
+ id: `${startTime}_${findingGroup[0]?.id}`,
+ startTime: startTime,
+ correlationRule: correlationRule,
+ logTypes: Array.from(logTypes),
+ alertSeverity: alertsSeverity,
+ findingsSeverity: findingsSeverity,
+ correlatedFindings: findingGroup,
+ });
+ }
+
+ this.setState({
+ correlationsTableData: tableData,
+ });
+ } catch (error) {
+ console.error('Failed to fetch correlation rules:', error);
+ }
+ };
+
+ private getCorrelatedFindingsVisualizationSpec = (
+ visualizationData: any[],
+ dateOpts: DateOpts = {
+ timeUnit: defaultTimeUnit,
+ dateFormat: defaultDateFormat,
+ domain: defaultScaleDomain,
+ }
+ ) => {
+ return getVisualizationSpec('Correlated Findings data overview', visualizationData, [
+ addInteractiveLegends({
+ mark: {
+ type: 'bar',
+ clip: true,
+ },
+ encoding: {
+ tooltip: [getYAxis('correlatedFinding', 'Correlated Findings'), getTimeTooltip(dateOpts)],
+ x: getXAxis(dateOpts),
+ y: getYAxis('correlatedFinding', 'Count'),
+ color: {
+ field: 'title',
+ legend: {
+ title: 'Legend',
+ },
+ },
+ },
+ }),
+ ]);
+ };
+
+ private generateVisualizationSpec = (connectedFindings: CorrelationFinding[][]) => {
+ const visData = connectedFindings.map((correlatedFindings) => {
+ return {
+ title: 'Correlated Findings',
+ correlatedFinding: correlatedFindings.length,
+ time: correlatedFindings[0].timestamp,
+ };
+ });
+
+ const {
+ dateTimeFilter = {
+ startTime: DEFAULT_DATE_RANGE.start,
+ endTime: DEFAULT_DATE_RANGE.end,
+ },
+ } = this.props;
+
+ const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime);
+
+ return this.getCorrelatedFindingsVisualizationSpec(visData, {
+ timeUnit: chartTimeUnits.timeUnit,
+ dateFormat: chartTimeUnits.dateFormat,
+ domain: getDomainRange(
+ [dateTimeFilter.startTime, dateTimeFilter.endTime],
+ chartTimeUnits.timeUnit.unit
+ ),
+ });
+ };
+
+ private renderCorrelatedFindingsChart = () => {
+ renderVisualization(
+ this.generateVisualizationSpec(this.state.connectedFindings),
+ 'correlated-findings-view'
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ Correlated Findings
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ };
+
+ private getFilteredTableData = (tableData: CorrelationsTableData[]): CorrelationsTableData[] => {
+ const { logTypeFilterOptions, severityFilterOptions } = this.state;
+ const alertSeverityMap: { [key: string]: string } = {
+ '1': 'critical',
+ '2': 'high',
+ '3': 'medium',
+ '4': 'low',
+ '5': 'informational',
+ };
+
+ const selectedLogTypes = logTypeFilterOptions
+ .filter((item) => item.checked === 'on' && item.visible)
+ .map((item) => item.id);
+
+ const selectedSeverities = severityFilterOptions
+ .filter((item) => item.checked === 'on' && item.visible)
+ .map((item) => item.id.toLowerCase());
+
+ return tableData.filter((row) => {
+ const logTypeMatch = row.logTypes.some((logType) => selectedLogTypes.includes(logType));
+
+ const severityMatch = row.alertSeverity.some((severity) =>
+ selectedSeverities.includes(alertSeverityMap[severity])
+ );
+
+ const searchLower = this.state.searchTerm.toLowerCase();
+ const searchMatch =
+ this.state.searchTerm === '' ||
+ row.correlationRule?.toLowerCase().includes(searchLower) ||
+ row.logTypes.some((type) => type.toLowerCase().includes(searchLower)) ||
+ row.alertSeverity.some((severity) =>
+ alertSeverityMap[severity].toLowerCase().includes(searchLower)
+ ) ||
+ row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower));
+ return logTypeMatch && severityMatch && searchMatch;
+ });
+ };
+
+ private debouncedSearch = debounce((searchTerm: string) => {
+ this.setState({ searchTerm }, () => {
+ this.forceUpdate();
+ });
+ }, 300);
+
+ private renderSearchBar = () => {
+ return (
+ {
+ e.persist();
+ const searchValue = e.target.value;
+ this.setState({ searchTerm: searchValue });
+ this.debouncedSearch(searchValue);
+ }}
+ fullWidth={true}
+ isClearable={true}
+ compressed={true}
+ aria-label="Search correlations"
+ />
+ );
+ };
+
+ private renderCorrelationsTable = () => {
+ const alertSeverityMap: { [key: string]: string } = {
+ '1': 'critical',
+ '2': 'high',
+ '3': 'medium',
+ '4': 'low',
+ '5': 'informational',
+ };
+
+ const columns: EuiBasicTableColumn[] = [
+ {
+ field: 'startTime',
+ name: 'Start time',
+ sortable: true,
+ dataType: 'date',
+ render: (startTime: number) => {
+ return new Date(startTime).toLocaleString();
+ },
+ },
+ {
+ field: 'correlationRule',
+ name: 'Correlation Rule',
+ sortable: true,
+ render: (name: string) => name || 'N/A',
+ },
+ {
+ field: 'logTypes',
+ name: 'Log Types',
+ sortable: true,
+ render: (logTypes: string[]) => {
+ if (!logTypes || logTypes.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount = logTypes.length > MAX_DISPLAY ? logTypes.length - MAX_DISPLAY : 0;
+ const displayedLogTypes = logTypes.slice(0, MAX_DISPLAY).map((logType) => {
+ const label = logType;
+ return {label};
+ });
+ const tooltipContent = (
+ <>
+ {logTypes.slice(MAX_DISPLAY).map((logType) => {
+ const label = logType;
+ return (
+
+ {label}
+
+ );
+ })}
+ >
+ );
+ return (
+
+ {displayedLogTypes}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
+ {
+ field: 'alertSeverity',
+ name: 'Alert Severity',
+ sortable: true,
+ render: (alertSeverity: string[]) => {
+ if (!alertSeverity || alertSeverity.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount =
+ alertSeverity.length > MAX_DISPLAY ? alertSeverity.length - MAX_DISPLAY : 0;
+ const displayedSeverities = alertSeverity.slice(0, MAX_DISPLAY).map((severity) => {
+ const label = alertSeverityMap[severity];
+ const { background, text } = getSeverityColor(label);
+ return (
+
+ {label}
+
+ );
+ });
+
+ const tooltipContent = (
+ <>
+ {alertSeverity.slice(MAX_DISPLAY).map((severity) => {
+ const label = alertSeverityMap[severity];
+ const { background, text } = getSeverityColor(label);
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+ >
+ );
+
+ return (
+
+ {displayedSeverities}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
+ {
+ field: 'findingsSeverity',
+ name: 'Findings Severity',
+ sortable: true,
+ render: (findingsSeverity: string[]) => {
+ if (!findingsSeverity || findingsSeverity.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount =
+ findingsSeverity.length > MAX_DISPLAY ? findingsSeverity.length - MAX_DISPLAY : 0;
+ const displayedSeverities = findingsSeverity.slice(0, MAX_DISPLAY).map((severity) => {
+ const label = getSeverityLabel(severity);
+ const { background, text } = getSeverityColor(label);
+ return (
+
+ {label}
+
+ );
+ });
+
+ const tooltipContent = (
+
+ {findingsSeverity.slice(MAX_DISPLAY).map((severity) => {
+ const label = getSeverityLabel(severity);
+ const { background, text } = getSeverityColor(label);
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+
+ );
+
+ return (
+
+ {displayedSeverities}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
+ {
+ field: 'actions',
+ name: 'Actions',
+ render: (_, correlationTableRow: CorrelationsTableData) => {
+ return (
+
+ {
+ this.openTableFlyout(correlationTableRow);
+ }}
+ />
+
+ );
+ },
+ },
+ ];
+
+ const getRowProps = (item: any) => {
+ return {
+ 'data-test-subj': `row-${item.id}`,
+ key: item.id,
+ className: 'euiTableRow',
+ };
+ };
+
+ const filteredTableData = this.getFilteredTableData(this.state.correlationsTableData);
+
+ return (
+ <>
+ {this.renderCorrelatedFindingsChart()}
+
+
+ >
+ );
+ };
+
+ private prepareGraphData = (correlationPairs: CorrelationFinding[][] | [any, any][]) => {
+ const createdEdges = new Set();
+ const createdNodes = new Set();
+ const graphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.onNodeClick,
+ },
+ };
+
+ correlationPairs.forEach((correlation: CorrelationFinding[]) => {
+ const possibleCombination1 = `${correlation[0].id}:${correlation[1].id}`;
+ const possibleCombination2 = `${correlation[1].id}:${correlation[0].id}`;
+
+ if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
+ return;
+ }
+
+ if (!createdNodes.has(correlation[0].id)) {
+ this.addNode(graphData.graph.nodes, correlation[0]);
+ createdNodes.add(correlation[0].id);
+ }
+ if (!createdNodes.has(correlation[1].id)) {
+ this.addNode(graphData.graph.nodes, correlation[1]);
+ createdNodes.add(correlation[1].id);
+ }
+ this.addEdge(graphData.graph.edges, correlation[0], correlation[1]);
+ createdEdges.add(possibleCombination1);
+ });
+
+ return graphData;
+ };
+
+ private getCorrelationPairs = (correlatedFindings: any[]) => {
+ const pairs: [any, any][] = [];
+ for (let i = 0; i < correlatedFindings.length; i++) {
+ for (let j = i + 1; j < correlatedFindings.length; j++) {
+ pairs.push([correlatedFindings[i], correlatedFindings[j]]);
+ }
+ }
+ return pairs;
+ };
+
+ private renderTableFlyout = () => {
+ const { isFlyoutOpen, selectedTableRow, graphData } = this.state;
+
+ if (!isFlyoutOpen || !selectedTableRow) {
+ return null;
+ }
+
+ const findingsTableColumns: EuiBasicTableColumn[] = [
+ {
+ field: 'timestamp',
+ name: 'Time',
+ render: (timestamp: string) => new Date(timestamp).toLocaleString(),
+ sortable: true,
+ },
+ {
+ field: 'mitreTactic',
+ name: 'Mitre Tactic',
+ sortable: true,
+ },
+ {
+ field: 'detectionRule',
+ name: 'Detection Rule',
+ sortable: true,
+ },
+ {
+ field: 'severity',
+ name: 'Severity',
+ render: (ruleSeverity: string) => {
+ const severity = capitalizeFirstLetter(ruleSeverity) || DEFAULT_EMPTY_DATA;
+ const { background, text } = getSeverityColor(severity);
+
+ return (
+
+ {severity}
+
+ );
+ },
+ },
+ ];
+
+ const flyoutTableData: FlyoutTableData[] = [];
+
+ selectedTableRow.correlatedFindings.map((correlatedFinding) => {
+ flyoutTableData.push({
+ timestamp: correlatedFinding.timestamp,
+ mitreTactic:
+ correlatedFinding.detectionRule.tags?.map((mitreTactic) => mitreTactic.value) || [],
+ detectionRule: correlatedFinding.detectionRule.name,
+ severity: correlatedFinding.detectionRule.severity,
+ });
+ });
+
+ return (
+
+
+
+ Correlation Details
+
+
+
+
+
+
+ Time
+
+ {new Date(selectedTableRow.startTime).toLocaleString()}
+
+
+
+
+
+
+ Correlation Rule
+
+ {selectedTableRow.correlationRule}
+
+
+
+
+
+
+
+
+
+ Correlated Findings
+
+ {selectedTableRow.correlatedFindings && (
+
+ )}
+
+
+
+ Findings
+
+
+
+
+ {Array.from(
+ new Set(
+ selectedTableRow.correlatedFindings.flatMap(
+ (finding) => finding.detectionRule.tags?.map((tag) => tag.value) || []
+ )
+ )
+ ).map((tactic, i) => {
+ const link = `https://attack.mitre.org/techniques/${tactic
+ .split('.')
+ .slice(1)
+ .join('/')
+ .toUpperCase()}`;
+
+ return (
+
+
+
+ {tactic}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+ };
+
+ toggleView = () => {
+ this.setState((prevState) => ({
+ isGraphView: !prevState.isGraphView,
+ }));
+ };
+
render() {
const findingCardsData = this.state.specificFindingInfo;
const datePicker = (
@@ -529,6 +1420,7 @@ export class Correlations extends React.Component
+
{findingCardsData.correlatedFindings.map((finding, index) => {
return (
<>
@@ -574,6 +1466,7 @@ export class Correlations extends React.Component
+ {this.renderSearchBar()}
-
-
-
-
- Severity:
-
+ this.setState({ isGraphView: id === 'graph' })}
+ buttonSize="s"
+ />
- {ruleSeverity.map((sev, idx) => (
-
-
- {sev.value}
-
-
- ))}
-
- {this.renderCorrelationsGraph(this.state.loadingGraphData)}
+ {this.state.isGraphView
+ ? this.renderCorrelationsGraph(this.state.loadingGraphData)
+ : this.renderCorrelationsTable()}
+ {this.renderTableFlyout()}
>
);
}
diff --git a/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx b/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
deleted file mode 100644
index f51c1664..00000000
--- a/public/pages/Correlations/containers/CorrelationsContainerUpd.tsx
+++ /dev/null
@@ -1,1527 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import {
- CorrelationFinding,
- CorrelationGraphData,
- DataSourceProps,
- DateTimeFilter,
- FindingItemType,
-} from '../../../../types';
-import React from 'react';
-import { RouteComponentProps } from 'react-router-dom';
-import {
- getDefaultLogTypeFilterItemOptions,
- defaultSeverityFilterItemOptions,
- emptyGraphData,
- getLabelFromLogType,
- getSeverityColor,
- getSeverityLabel,
- graphRenderOptions,
-} from '../utils/constants';
-import {
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTitle,
- EuiPanel,
- EuiCompressedSuperDatePicker,
- EuiSpacer,
- EuiSmallButtonEmpty,
- EuiFlyout,
- EuiFlyoutHeader,
- EuiFlyoutBody,
- EuiSmallButtonIcon,
- EuiText,
- EuiEmptyPrompt,
- EuiSmallButton,
- EuiBadge,
- EuiFilterGroup,
- EuiHorizontalRule,
- EuiButtonGroup,
- EuiBasicTableColumn,
- EuiToolTip,
- EuiInMemoryTable,
- EuiTextColor,
- EuiLink,
- EuiFieldSearch,
-} from '@elastic/eui';
-import { FilterItem, FilterGroup } from '../components/FilterGroup';
-import {
- BREADCRUMBS,
- DEFAULT_DATE_RANGE,
- MAX_RECENTLY_USED_TIME_RANGES,
- ROUTES,
-} from '../../../utils/constants';
-import { CorrelationGraph } from '../components/CorrelationGraph';
-import { FindingCard } from '../components/FindingCard';
-import { DataStore } from '../../../store/DataStore';
-import datemath from '@elastic/datemath';
-import { ruleSeverity } from '../../Rules/utils/constants';
-import { renderToStaticMarkup } from 'react-dom/server';
-import { Network } from 'react-graph-vis';
-import { getLogTypeLabel } from '../../LogTypes/utils/helpers';
-import { NotificationsStart } from 'opensearch-dashboards/public';
-import {
- capitalizeFirstLetter,
- errorNotificationToast,
- renderVisualization,
- setBreadcrumbs,
-} from '../../../utils/helpers';
-import { PageHeader } from '../../../components/PageHeader/PageHeader';
-import moment from 'moment';
-import { ChartContainer } from '../../../components/Charts/ChartContainer';
-import {
- addInteractiveLegends,
- DateOpts,
- defaultDateFormat,
- defaultScaleDomain,
- defaultTimeUnit,
- getChartTimeUnit,
- getDomainRange,
- getTimeTooltip,
- getVisualizationSpec,
- getXAxis,
- getYAxis,
-} from '../../Overview/utils/helpers';
-import { debounce } from 'lodash';
-
-export const DEFAULT_EMPTY_DATA = '-';
-
-interface CorrelationsProps
- extends RouteComponentProps<
- any,
- any,
- { finding: FindingItemType; correlatedFindings: CorrelationFinding[] }
- >,
- DataSourceProps {
- setDateTimeFilter?: Function;
- dateTimeFilter?: DateTimeFilter;
- onMount: () => void;
- notifications: NotificationsStart | null;
-}
-
-interface SpecificFindingCorrelations {
- finding: CorrelationFinding;
- correlatedFindings: CorrelationFinding[];
-}
-
-interface CorrelationsTableData {
- id: string;
- startTime: number;
- correlationRule: string;
- alertSeverity: string[];
- logTypes: string[];
- findingsSeverity: string[];
- correlatedFindings: CorrelationFinding[];
-}
-
-interface FlyoutTableData {
- timestamp: string;
- mitreTactic: string[];
- detectionRule: string;
- severity: string;
-}
-
-interface CorrelationsState {
- recentlyUsedRanges: any[];
- graphData: CorrelationGraphData;
- specificFindingInfo?: SpecificFindingCorrelations;
- logTypeFilterOptions: FilterItem[];
- severityFilterOptions: FilterItem[];
- loadingGraphData: boolean;
- isGraphView: Boolean;
- correlationsTableData: CorrelationsTableData[];
- connectedFindings: CorrelationFinding[][];
- isFlyoutOpen: boolean;
- selectedTableRow: CorrelationsTableData | null;
- searchTerm: string;
-}
-
-export const renderTime = (time: number | string) => {
- const momentTime = moment(time);
- if (time && momentTime.isValid()) return momentTime.format('MM/DD/YY h:mm a');
- return DEFAULT_EMPTY_DATA;
-};
-
-export interface CorrelationsTableProps {
- finding: FindingItemType;
- correlatedFindings: CorrelationFinding[];
- history: RouteComponentProps['history'];
- isLoading: boolean;
- filterOptions: {
- logTypes: Set;
- ruleSeverity: Set;
- };
-}
-
-export class CorrelationsUpd extends React.Component {
- private correlationGraphNetwork?: Network;
-
- constructor(props: CorrelationsProps) {
- super(props);
- this.state = {
- recentlyUsedRanges: [DEFAULT_DATE_RANGE],
- graphData: { ...emptyGraphData },
- logTypeFilterOptions: [...getDefaultLogTypeFilterItemOptions()],
- severityFilterOptions: [...defaultSeverityFilterItemOptions],
- specificFindingInfo: undefined,
- loadingGraphData: false,
- isGraphView: true,
- correlationsTableData: [],
- connectedFindings: [],
- isFlyoutOpen: false,
- selectedTableRow: null,
- searchTerm: '',
- };
- }
-
- private get startTime() {
- return this.props.dateTimeFilter?.startTime || DEFAULT_DATE_RANGE.start;
- }
-
- private get endTime() {
- return this.props.dateTimeFilter?.endTime || DEFAULT_DATE_RANGE.end;
- }
-
- private shouldShowFinding(finding: CorrelationFinding) {
- return (
- this.state.logTypeFilterOptions.find((option) => option.id === finding.logType)?.checked ===
- 'on' &&
- this.state.severityFilterOptions.find(
- (option) => option.id === finding.detectionRule.severity
- )?.checked === 'on'
- );
- }
-
- async componentDidMount(): Promise {
- setBreadcrumbs([BREADCRUMBS.CORRELATIONS_UPD]);
- this.updateState(true /* onMount */);
- this.props.onMount();
- this.fetchCorrelationsTableData();
- }
-
- componentDidUpdate(
- prevProps: Readonly,
- prevState: Readonly,
- snapshot?: any
- ): void {
- if (prevProps.dataSource !== this.props.dataSource) {
- this.onRefresh();
- } else if (
- prevState.logTypeFilterOptions !== this.state.logTypeFilterOptions ||
- prevState.severityFilterOptions !== this.state.severityFilterOptions ||
- prevProps.dateTimeFilter !== this.props.dateTimeFilter
- ) {
- this.updateState();
- }
- }
-
- private async updateState(onMount: boolean = false) {
- if (onMount && this.props.location.state) {
- const state = this.props.location.state;
-
- const specificFindingInfo: SpecificFindingCorrelations = {
- finding: {
- ...state.finding,
- id: state.finding.id,
- logType: state.finding.detector._source.detector_type,
- timestamp: new Date(state.finding.timestamp).toLocaleString(),
- detectionRule: {
- name: (state.finding as any).ruleName,
- severity: (state.finding as any).ruleSeverity,
- tags: (state.finding as any).tags,
- },
- },
- correlatedFindings: state.correlatedFindings.filter((finding) =>
- this.shouldShowFinding(finding)
- ),
- };
-
- if (!this.shouldShowFinding(specificFindingInfo.finding)) {
- return;
- }
-
- this.setState({ specificFindingInfo });
-
- // create graph data here
- this.updateGraphDataState(specificFindingInfo);
- } else {
- // get all correlations and display them in the graph
- const start = datemath.parse(this.startTime);
- const end = datemath.parse(this.endTime);
- const startTime = start?.valueOf() || Date.now();
- const endTime = end?.valueOf() || Date.now();
- this.setState({ loadingGraphData: true });
- let allCorrelations = await DataStore.correlations.getAllCorrelationsInWindow(
- startTime.toString(),
- endTime.toString()
- );
- this.setState({ loadingGraphData: false });
- allCorrelations = allCorrelations.filter((corr) => {
- return this.shouldShowFinding(corr.finding1) && this.shouldShowFinding(corr.finding2);
- });
- const createdEdges = new Set();
- const createdNodes = new Set();
- const graphData: CorrelationGraphData = {
- graph: {
- nodes: [],
- edges: [],
- },
- events: {
- click: this.onNodeClick,
- },
- };
- allCorrelations.forEach((correlation) => {
- const possibleCombination1 = `${correlation.finding1.id}:${correlation.finding2.id}`;
- const possibleCombination2 = `${correlation.finding2.id}:${correlation.finding1.id}`;
-
- if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
- return;
- }
-
- if (!createdNodes.has(correlation.finding1.id)) {
- this.addNode(graphData.graph.nodes, correlation.finding1);
- createdNodes.add(correlation.finding1.id);
- }
- if (!createdNodes.has(correlation.finding2.id)) {
- this.addNode(graphData.graph.nodes, correlation.finding2);
- createdNodes.add(correlation.finding2.id);
- }
- this.addEdge(graphData.graph.edges, correlation.finding1, correlation.finding2);
- createdEdges.add(possibleCombination1);
- });
-
- this.setState({ graphData, specificFindingInfo: undefined });
- }
- }
-
- private onNodeClick = async (params: any) => {
- if (params.nodes.length !== 1) {
- return;
- }
-
- const findingId = params.nodes[0];
-
- if (this.state.specificFindingInfo?.finding.id === findingId) {
- return;
- }
-
- this.setState({ loadingGraphData: true });
-
- let detectorType: string;
- const node = this.state.graphData.graph.nodes.find((node) => node.id === findingId)!;
-
- if (node) {
- detectorType = node.saLogType;
- } else {
- const finding = (await DataStore.findings.getFindingsByIds([findingId]))[0];
- detectorType = finding?.detectionType;
- }
-
- if (!detectorType) {
- errorNotificationToast(this.props.notifications, 'show', 'correlated findings');
- return;
- }
-
- const correlatedFindingsInfo = await DataStore.correlations.getCorrelatedFindings(
- findingId,
- detectorType
- );
- const correlationRules = await DataStore.correlations.getCorrelationRules();
- correlatedFindingsInfo.correlatedFindings = correlatedFindingsInfo.correlatedFindings.map(
- (finding) => {
- return {
- ...finding,
- correlationRule: correlationRules.find((rule) => finding.rules?.indexOf(rule.id) !== -1),
- };
- }
- );
- this.setState({ specificFindingInfo: correlatedFindingsInfo, loadingGraphData: false });
- this.updateGraphDataState(correlatedFindingsInfo);
- };
-
- private updateGraphDataState(specificFindingInfo: SpecificFindingCorrelations) {
- const graphData: CorrelationGraphData = {
- graph: {
- nodes: [],
- edges: [],
- },
- events: {
- click: this.onNodeClick,
- },
- };
-
- this.addNode(graphData.graph.nodes, specificFindingInfo.finding);
- const addedEdges = new Set();
- const nodeIds = new Set();
- specificFindingInfo.correlatedFindings.forEach((finding) => {
- if (!nodeIds.has(finding.id)) {
- this.addNode(graphData.graph.nodes, finding);
- nodeIds.add(finding.id);
- }
-
- const possibleCombination1 = `${specificFindingInfo.finding.id}:${finding.id}`;
- const possibleCombination2 = `${finding.id}:${specificFindingInfo.finding.id}`;
- if (addedEdges.has(possibleCombination1) || addedEdges.has(possibleCombination2)) {
- return;
- }
- this.addEdge(graphData.graph.edges, specificFindingInfo.finding, finding);
- addedEdges.add(possibleCombination1);
- });
-
- this.setState({ graphData });
- }
-
- private addNode(nodes: any[], finding: CorrelationFinding) {
- const borderColor = getSeverityColor(finding.detectionRule.severity).background;
-
- nodes.push({
- id: finding.id,
- label: getLogTypeLabel(finding.logType),
- title: this.createNodeTooltip(finding),
- color: {
- background: borderColor,
- border: borderColor,
- highlight: {
- background: '#e7f5ff',
- border: borderColor,
- },
- hover: {
- background: '#e7f5ff',
- border: borderColor,
- },
- },
- size: 17,
- borderWidth: 2,
- font: {
- multi: 'html',
- size: 12,
- },
- chosen: true,
- saLogType: finding.logType,
- });
- }
-
- private addEdge(edges: any[], f1: CorrelationFinding, f2: CorrelationFinding) {
- edges.push({
- from: f1.id,
- to: f2.id,
- id: `${f1.id}:${f2.id}`,
- chosen: false,
- color: '#98A2B3', //ouiColorMediumShade,
- label: f1.correlationScore || f2.correlationScore || '',
- width: 2,
- });
- }
-
- private createNodeTooltip = ({ detectionRule, timestamp, logType }: CorrelationFinding) => {
- const { text, background } = getSeverityColor(detectionRule.severity);
- const tooltipContent = (
-
-
-
-
- {getSeverityLabel(detectionRule.severity)}
-
-
-
- {getLabelFromLogType(logType)}
-
-
-
-
{timestamp}
-
-
Detection rule
-
-
{detectionRule.name}
-
- );
-
- const tooltipContentHTML = renderToStaticMarkup(tooltipContent);
-
- const tooltip = document.createElement('div');
- tooltip.innerHTML = tooltipContentHTML;
-
- return tooltip.firstElementChild;
- };
-
- private onTimeChange = ({ start, end }: { start: string; end: string }) => {
- let { recentlyUsedRanges } = this.state;
- recentlyUsedRanges = recentlyUsedRanges.filter(
- (range) => !(range.start === start && range.end === end)
- );
- recentlyUsedRanges.unshift({ start: start, end: end });
- if (recentlyUsedRanges.length > MAX_RECENTLY_USED_TIME_RANGES)
- recentlyUsedRanges = recentlyUsedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES);
- const endTime = start === end ? DEFAULT_DATE_RANGE.end : end;
- this.setState({
- recentlyUsedRanges: recentlyUsedRanges,
- });
-
- this.props.setDateTimeFilter &&
- this.props.setDateTimeFilter({
- startTime: start,
- endTime: endTime,
- });
- };
-
- private onRefresh = () => {
- this.updateState();
- };
-
- onLogTypeFilterChange = (items: FilterItem[]) => {
- this.setState(
- {
- logTypeFilterOptions: items,
- },
- () => {
- // If there's specific finding info, update the graph
- if (this.state.specificFindingInfo) {
- this.updateGraphDataState(this.state.specificFindingInfo);
- }
- // Force update to refresh the table with new filters
- this.forceUpdate();
- }
- );
- };
-
- onSeverityFilterChange = (items: FilterItem[]) => {
- this.setState(
- {
- severityFilterOptions: items,
- },
- () => {
- // If there's specific finding info, update the graph
- if (this.state.specificFindingInfo) {
- this.updateGraphDataState(this.state.specificFindingInfo);
- }
- // Force update to refresh the table with new filters
- this.forceUpdate();
- }
- );
- };
-
- closeFlyout = () => {
- this.setState({ specificFindingInfo: undefined });
- };
-
- private openTableFlyout = (correlationTableRow: CorrelationsTableData) => {
- let newGraphData: CorrelationGraphData = {
- graph: {
- nodes: [],
- edges: [],
- },
- events: {
- click: this.onNodeClick,
- },
- };
-
- if (correlationTableRow.correlatedFindings) {
- const correlationPairs = this.getCorrelationPairs(correlationTableRow.correlatedFindings);
- newGraphData = this.prepareGraphData(correlationPairs);
- }
-
- // Set all required state at once
- this.setState({
- isFlyoutOpen: true,
- selectedTableRow: correlationTableRow,
- graphData: newGraphData,
- });
- };
-
- private closeTableFlyout = () => {
- this.setState({
- isFlyoutOpen: false,
- selectedTableRow: null,
- graphData: {
- graph: { nodes: [], edges: [] },
- events: { click: this.onNodeClick },
- },
- });
- };
-
- onFindingInspect = async (id: string, logType: string) => {
- // get finding data and set the specificFindingInfo
- const specificFindingInfo = await DataStore.correlations.getCorrelatedFindings(id, logType);
- this.setState({ specificFindingInfo });
- this.updateGraphDataState(specificFindingInfo);
- this.correlationGraphNetwork?.selectNodes([id], false);
- };
-
- resetFilters = () => {
- this.setState({
- logTypeFilterOptions: this.state.logTypeFilterOptions.map((option) => ({
- ...option,
- checked: 'on',
- })),
- severityFilterOptions: this.state.severityFilterOptions.map((option) => ({
- ...option,
- checked: 'on',
- })),
- specificFindingInfo: undefined,
- });
- };
-
- setNetwork = (network: Network) => {
- this.correlationGraphNetwork = network;
- network.on('hoverNode', function (params) {
- network.canvas.body.container.style.cursor = 'pointer';
- });
- network.on('blurNode', function (params) {
- network.canvas.body.container.style.cursor = 'default';
- });
- };
-
- renderCorrelationsGraph(loadingData: boolean) {
- return this.state.graphData.graph.nodes.length > 0 || loadingData ? (
- <>
-
-
-
- Severity:
-
-
- {ruleSeverity.map((sev, idx) => (
-
-
- {sev.value}
-
-
- ))}
-
-
-
- >
- ) : (
-
- No correlations found
-
- }
- body={
-
- There are no correlated findings in the system.
-
- }
- actions={[
-
- Create correlation rule
- ,
- ]}
- />
- );
- }
-
- private mapConnectedCorrelations(
- correlations: {
- finding1: CorrelationFinding;
- finding2: CorrelationFinding;
- }[]
- ) {
- const connectionsMap = new Map>();
- const findingsMap = new Map();
-
- correlations.forEach((correlation) => {
- const { finding1, finding2 } = correlation;
-
- findingsMap.set(finding1.id, finding1);
- findingsMap.set(finding2.id, finding2);
-
- if (!connectionsMap.has(finding1.id)) {
- connectionsMap.set(finding1.id, new Set());
- }
- connectionsMap.get(finding1.id)!.add(finding2.id);
-
- if (!connectionsMap.has(finding2.id)) {
- connectionsMap.set(finding2.id, new Set());
- }
- connectionsMap.get(finding2.id)!.add(finding1.id);
- });
-
- const visited = new Set();
- const connectedGroups: CorrelationFinding[][] = [];
-
- function dfs(findingId: string, currentGroup: CorrelationFinding[]) {
- visited.add(findingId);
- const finding = findingsMap.get(findingId);
- if (finding) {
- currentGroup.push(finding);
- }
-
- const connections = connectionsMap.get(findingId) || new Set();
- connections.forEach((connectedId) => {
- if (!visited.has(connectedId)) {
- dfs(connectedId, currentGroup);
- }
- });
- }
-
- connectionsMap.forEach((_, findingId) => {
- if (!visited.has(findingId)) {
- const currentGroup: CorrelationFinding[] = [];
- dfs(findingId, currentGroup);
- if (currentGroup.length > 0) {
- connectedGroups.push(currentGroup);
- }
- }
- });
-
- return connectedGroups;
- }
-
- private fetchCorrelationsTableData = async () => {
- try {
- const start = datemath.parse(this.startTime);
- const end = datemath.parse(this.endTime);
- const startTime = start?.valueOf() || Date.now();
- const endTime = end?.valueOf() || Date.now();
-
- let allCorrelations = await DataStore.correlations.getAllCorrelationsInWindow(
- startTime.toString(),
- endTime.toString()
- );
-
- const connectedFindings = this.mapConnectedCorrelations(allCorrelations);
-
- this.setState({ connectedFindings: connectedFindings });
-
- const tableData: CorrelationsTableData[] = [];
-
- const allCorrelationRules = await DataStore.correlations.getCorrelationRules();
- const allCorrelatedAlerts = await DataStore.correlations.getAllCorrelationAlerts();
-
- const correlationRuleMapsAlerts: { [id: string]: string[] } = {};
-
- allCorrelationRules.forEach((correlationRule) => {
- const correlationRuleId = correlationRule.id;
- correlationRuleMapsAlerts[correlationRuleId] = [];
-
- allCorrelatedAlerts.correlationAlerts.forEach((correlatedAlert) => {
- if (correlatedAlert.correlation_rule_id === correlationRuleId) {
- correlationRuleMapsAlerts[correlationRuleId].push(correlatedAlert.severity);
- }
- });
- });
-
- for (const findingGroup of connectedFindings) {
- let correlationRule = '';
- const logTypes = new Set();
- const findingsSeverity: string[] = [];
- let alertsSeverity: string[] = [];
-
- for (const finding of findingGroup) {
- findingsSeverity.push(finding.detectionRule.severity);
- logTypes.add(finding.logType);
- }
-
- // Call the APIs only if correlationRule has not been found yet to avoid repeated API calls.
- if (correlationRule === '') {
- if (findingGroup[0] && findingGroup[0].detector && findingGroup[0].detector._source) {
- const correlatedFindingsResponse = await DataStore.correlations.getCorrelatedFindings(
- findingGroup[0].id,
- findingGroup[0]?.detector._source?.detector_type
- );
-
- // TODO -- GET THE RESOURCES FROM findings and correlatedFindings AND DISPLAY IN THE RESOURCES COLUMN.'
- // TODO -- GETTING THE EXACT RESOURCE IS NOT FEASIBLE WITH THE CURRENT DATA MODEL, BUT FIND A WAY TO REPRSENT THE OBJECTS.
-
- if (
- correlatedFindingsResponse.correlatedFindings &&
- correlatedFindingsResponse.correlatedFindings[0] &&
- correlatedFindingsResponse.correlatedFindings[0].rules
- ) {
- const correlationRuleId = correlatedFindingsResponse.correlatedFindings[0].rules[0];
- const correlationRuleObj =
- (await DataStore.correlations.getCorrelationRule(correlationRuleId)) || '';
- alertsSeverity = correlationRuleMapsAlerts[correlationRuleId];
- if (correlationRuleObj) {
- correlationRule = correlationRuleObj.name;
- }
- }
- }
- }
-
- tableData.push({
- id: `${startTime}_${findingGroup[0]?.id}`,
- startTime: startTime,
- correlationRule: correlationRule,
- logTypes: Array.from(logTypes),
- alertSeverity: alertsSeverity,
- findingsSeverity: findingsSeverity,
- correlatedFindings: findingGroup,
- });
- }
-
- this.setState({
- correlationsTableData: tableData,
- });
- } catch (error) {
- console.error('Failed to fetch correlation rules:', error);
- }
- };
-
- private getCorrelatedFindingsVisualizationSpec = (
- visualizationData: any[],
- dateOpts: DateOpts = {
- timeUnit: defaultTimeUnit,
- dateFormat: defaultDateFormat,
- domain: defaultScaleDomain,
- }
- ) => {
- return getVisualizationSpec('Correlated Findings data overview', visualizationData, [
- addInteractiveLegends({
- mark: {
- type: 'bar',
- clip: true,
- },
- encoding: {
- tooltip: [getYAxis('correlatedFinding', 'Correlated Findings'), getTimeTooltip(dateOpts)],
- x: getXAxis(dateOpts),
- y: getYAxis('correlatedFinding', 'Count'),
- color: {
- field: 'title',
- legend: {
- title: 'Legend',
- },
- },
- },
- }),
- ]);
- };
-
- private generateVisualizationSpec = (connectedFindings: CorrelationFinding[][]) => {
- const visData = connectedFindings.map((correlatedFindings) => {
- return {
- title: 'Correlated Findings',
- correlatedFinding: correlatedFindings.length,
- time: correlatedFindings[0].timestamp,
- };
- });
-
- const {
- dateTimeFilter = {
- startTime: DEFAULT_DATE_RANGE.start,
- endTime: DEFAULT_DATE_RANGE.end,
- },
- } = this.props;
-
- const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime);
-
- return this.getCorrelatedFindingsVisualizationSpec(visData, {
- timeUnit: chartTimeUnits.timeUnit,
- dateFormat: chartTimeUnits.dateFormat,
- domain: getDomainRange(
- [dateTimeFilter.startTime, dateTimeFilter.endTime],
- chartTimeUnits.timeUnit.unit
- ),
- });
- };
-
- private renderCorrelatedFindingsChart = () => {
- renderVisualization(
- this.generateVisualizationSpec(this.state.connectedFindings),
- 'correlated-findings-view'
- );
-
- return (
- <>
-
-
-
-
-
-
- Correlated Findings
-
-
-
-
-
-
-
-
-
-
- >
- );
- };
-
- private getFilteredTableData = (tableData: CorrelationsTableData[]): CorrelationsTableData[] => {
- const { logTypeFilterOptions, severityFilterOptions } = this.state;
- const alertSeverityMap: { [key: string]: string } = {
- '1': 'critical',
- '2': 'high',
- '3': 'medium',
- '4': 'low',
- '5': 'informational',
- };
-
- const selectedLogTypes = logTypeFilterOptions
- .filter((item) => item.checked === 'on' && item.visible)
- .map((item) => item.id);
-
- const selectedSeverities = severityFilterOptions
- .filter((item) => item.checked === 'on' && item.visible)
- .map((item) => item.id.toLowerCase());
-
- return tableData.filter((row) => {
- const logTypeMatch = row.logTypes.some((logType) => selectedLogTypes.includes(logType));
-
- const severityMatch = row.alertSeverity.some((severity) =>
- selectedSeverities.includes(alertSeverityMap[severity])
- );
-
- const searchLower = this.state.searchTerm.toLowerCase();
- const searchMatch =
- this.state.searchTerm === '' ||
- row.correlationRule?.toLowerCase().includes(searchLower) ||
- row.logTypes.some((type) => type.toLowerCase().includes(searchLower)) ||
- row.alertSeverity.some((severity) =>
- alertSeverityMap[severity].toLowerCase().includes(searchLower)
- ) ||
- row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower));
- return logTypeMatch && severityMatch && searchMatch;
- });
- };
-
- private debouncedSearch = debounce((searchTerm: string) => {
- this.setState({ searchTerm }, () => {
- this.forceUpdate();
- });
- }, 300);
-
- private renderSearchBar = () => {
- return (
- {
- e.persist();
- const searchValue = e.target.value;
- this.setState({ searchTerm: searchValue });
- this.debouncedSearch(searchValue);
- }}
- fullWidth={true}
- isClearable={true}
- compressed={true}
- aria-label="Search correlations"
- />
- );
- };
-
- private renderCorrelationsTable = () => {
- const alertSeverityMap: { [key: string]: string } = {
- '1': 'critical',
- '2': 'high',
- '3': 'medium',
- '4': 'low',
- '5': 'informational',
- };
-
- const columns: EuiBasicTableColumn[] = [
- {
- field: 'startTime',
- name: 'Start time',
- sortable: true,
- dataType: 'date',
- render: (startTime: number) => {
- return new Date(startTime).toLocaleString();
- },
- },
- {
- field: 'correlationRule',
- name: 'Correlation Rule',
- sortable: true,
- render: (name: string) => name || 'N/A',
- },
- {
- field: 'logTypes',
- name: 'Log Types',
- sortable: true,
- render: (logTypes: string[]) => {
- if (!logTypes || logTypes.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount = logTypes.length > MAX_DISPLAY ? logTypes.length - MAX_DISPLAY : 0;
- const displayedLogTypes = logTypes.slice(0, MAX_DISPLAY).map((logType) => {
- const label = logType;
- return {label};
- });
- const tooltipContent = (
- <>
- {logTypes.slice(MAX_DISPLAY).map((logType) => {
- const label = logType;
- return (
-
- {label}
-
- );
- })}
- >
- );
- return (
-
- {displayedLogTypes}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'alertSeverity',
- name: 'Alert Severity',
- sortable: true,
- render: (alertSeverity: string[]) => {
- if (!alertSeverity || alertSeverity.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount =
- alertSeverity.length > MAX_DISPLAY ? alertSeverity.length - MAX_DISPLAY : 0;
- const displayedSeverities = alertSeverity.slice(0, MAX_DISPLAY).map((severity) => {
- const label = alertSeverityMap[severity];
- const { background, text } = getSeverityColor(label);
- return (
-
- {label}
-
- );
- });
-
- const tooltipContent = (
- <>
- {alertSeverity.slice(MAX_DISPLAY).map((severity) => {
- const label = alertSeverityMap[severity];
- const { background, text } = getSeverityColor(label);
- return (
-
-
- {label}
-
-
- );
- })}
- >
- );
-
- return (
-
- {displayedSeverities}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'findingsSeverity',
- name: 'Findings Severity',
- sortable: true,
- render: (findingsSeverity: string[]) => {
- if (!findingsSeverity || findingsSeverity.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount =
- findingsSeverity.length > MAX_DISPLAY ? findingsSeverity.length - MAX_DISPLAY : 0;
- const displayedSeverities = findingsSeverity.slice(0, MAX_DISPLAY).map((severity) => {
- const label = getSeverityLabel(severity);
- const { background, text } = getSeverityColor(label);
- return (
-
- {label}
-
- );
- });
-
- const tooltipContent = (
-
- {findingsSeverity.slice(MAX_DISPLAY).map((severity) => {
- const label = getSeverityLabel(severity);
- const { background, text } = getSeverityColor(label);
- return (
-
-
- {label}
-
-
- );
- })}
-
- );
-
- return (
-
- {displayedSeverities}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'actions',
- name: 'Actions',
- render: (_, correlationTableRow: CorrelationsTableData) => {
- return (
-
- {
- this.openTableFlyout(correlationTableRow);
- }}
- />
-
- );
- },
- },
- ];
-
- const getRowProps = (item: any) => {
- return {
- 'data-test-subj': `row-${item.id}`,
- key: item.id,
- className: 'euiTableRow',
- };
- };
-
- const filteredTableData = this.getFilteredTableData(this.state.correlationsTableData);
-
- return (
- <>
- {this.renderCorrelatedFindingsChart()}
-
-
- >
- );
- };
-
- private prepareGraphData = (correlationPairs: CorrelationFinding[][] | [any, any][]) => {
- const createdEdges = new Set();
- const createdNodes = new Set();
- const graphData: CorrelationGraphData = {
- graph: {
- nodes: [],
- edges: [],
- },
- events: {
- click: this.onNodeClick,
- },
- };
-
- correlationPairs.forEach((correlation: CorrelationFinding[]) => {
- const possibleCombination1 = `${correlation[0].id}:${correlation[1].id}`;
- const possibleCombination2 = `${correlation[1].id}:${correlation[0].id}`;
-
- if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
- return;
- }
-
- if (!createdNodes.has(correlation[0].id)) {
- this.addNode(graphData.graph.nodes, correlation[0]);
- createdNodes.add(correlation[0].id);
- }
- if (!createdNodes.has(correlation[1].id)) {
- this.addNode(graphData.graph.nodes, correlation[1]);
- createdNodes.add(correlation[1].id);
- }
- this.addEdge(graphData.graph.edges, correlation[0], correlation[1]);
- createdEdges.add(possibleCombination1);
- });
-
- return graphData;
- };
-
- private getCorrelationPairs = (correlatedFindings: any[]) => {
- const pairs: [any, any][] = [];
- for (let i = 0; i < correlatedFindings.length; i++) {
- for (let j = i + 1; j < correlatedFindings.length; j++) {
- pairs.push([correlatedFindings[i], correlatedFindings[j]]);
- }
- }
- return pairs;
- };
-
- private renderTableFlyout = () => {
- const { isFlyoutOpen, selectedTableRow, graphData } = this.state;
-
- if (!isFlyoutOpen || !selectedTableRow) {
- return null;
- }
-
- const findingsTableColumns: EuiBasicTableColumn[] = [
- {
- field: 'timestamp',
- name: 'Time',
- render: (timestamp: string) => new Date(timestamp).toLocaleString(),
- sortable: true,
- },
- {
- field: 'mitreTactic',
- name: 'Mitre Tactic',
- sortable: true,
- },
- {
- field: 'detectionRule',
- name: 'Detection Rule',
- sortable: true,
- },
- {
- field: 'severity',
- name: 'Severity',
- render: (ruleSeverity: string) => {
- const severity = capitalizeFirstLetter(ruleSeverity) || DEFAULT_EMPTY_DATA;
- const { background, text } = getSeverityColor(severity);
-
- return (
-
- {severity}
-
- );
- },
- },
- ];
-
- const flyoutTableData: FlyoutTableData[] = [];
-
- selectedTableRow.correlatedFindings.map((correlatedFinding) => {
- flyoutTableData.push({
- timestamp: correlatedFinding.timestamp,
- mitreTactic:
- correlatedFinding.detectionRule.tags?.map((mitreTactic) => mitreTactic.value) || [],
- detectionRule: correlatedFinding.detectionRule.name,
- severity: correlatedFinding.detectionRule.severity,
- });
- });
-
- return (
-
-
-
- Correlation Details
-
-
-
-
-
-
- Time
-
- {new Date(selectedTableRow.startTime).toLocaleString()}
-
-
-
-
-
-
- Correlation Rule
-
- {selectedTableRow.correlationRule}
-
-
-
-
-
-
-
-
-
- Correlated Findings
-
- {selectedTableRow.correlatedFindings && (
-
- )}
-
-
-
- Findings
-
-
-
-
- {Array.from(
- new Set(
- selectedTableRow.correlatedFindings.flatMap(
- (finding) => finding.detectionRule.tags?.map((tag) => tag.value) || []
- )
- )
- ).map((tactic, i) => {
- const link = `https://attack.mitre.org/techniques/${tactic
- .split('.')
- .slice(1)
- .join('/')
- .toUpperCase()}`;
-
- return (
-
-
-
- {tactic}
-
-
-
- );
- })}
-
-
-
-
- );
- };
-
- toggleView = () => {
- this.setState((prevState) => ({
- isGraphView: !prevState.isGraphView,
- }));
- };
-
- render() {
- const findingCardsData = this.state.specificFindingInfo;
- const datePicker = (
-
- );
-
- return (
- <>
- {findingCardsData ? (
-
-
-
-
-
- Correlation
-
-
-
-
-
-
-
-
-
-
-
-
- Correlated Findings ({findingCardsData.correlatedFindings.length})
-
-
- Higher correlation score indicated stronger correlation.
-
-
-
- {findingCardsData.correlatedFindings.map((finding, index) => {
- return (
- <>
-
-
- >
- );
- })}
-
-
- ) : null}
-
-
-
-
-
-
- Correlations
-
-
- {datePicker}
-
-
-
-
-
-
- {this.renderSearchBar()}
-
-
-
-
-
-
-
-
- Reset filters
-
-
-
- this.setState({ isGraphView: id === 'graph' })}
- buttonSize="s"
- />
-
-
-
- {this.state.isGraphView
- ? this.renderCorrelationsGraph(this.state.loadingGraphData)
- : this.renderCorrelationsTable()}
-
-
-
- {this.renderTableFlyout()}
- >
- );
- }
-}
diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx
index 00fba5fc..fc56a0fd 100644
--- a/public/pages/Main/Main.tsx
+++ b/public/pages/Main/Main.tsx
@@ -48,7 +48,6 @@ import { DataStore } from '../../store/DataStore';
import { CreateCorrelationRule } from '../Correlations/containers/CreateCorrelationRule';
import { CorrelationRules } from '../Correlations/containers/CorrelationRules';
import { Correlations } from '../Correlations/containers/CorrelationsContainer';
-import { CorrelationsUpd } from '../Correlations/containers/CorrelationsContainerUpd';
import { LogTypes } from '../LogTypes/containers/LogTypes';
import { LogType } from '../LogTypes/containers/LogType';
import { CreateLogType } from '../LogTypes/containers/CreateLogType';
@@ -86,7 +85,6 @@ enum Navigation {
Overview = 'Overview',
Alerts = 'Alerts',
Correlations = 'Correlations',
- CorrelationsUpd = 'Correlations Updated',
CorrelationRules = 'Correlation rules',
LogTypes = 'Log types',
ThreatIntel = 'Threat Intelligence',
@@ -430,44 +428,6 @@ export default class Main extends Component {
},
],
},
- {
- name: Navigation.CorrelationsUpd,
- id: Navigation.CorrelationsUpd,
- onClick: () => {
- this.setState({ selectedNavItemId: Navigation.CorrelationsUpd });
- history.push(ROUTES.CORRELATIONS_UPD);
- },
- renderItem: (props: any) => {
- return (
-
-
- {
- this.setState({ selectedNavItemId: Navigation.CorrelationsUpd });
- history.push(ROUTES.CORRELATIONS_UPD);
- }}
- >
- {props.children}
-
-
-
- );
- },
- isSelected: selectedNavItemId === Navigation.CorrelationsUpd,
- forceOpen: true,
- items: [
- {
- name: Navigation.CorrelationRules,
- id: Navigation.CorrelationRules,
- onClick: () => {
- this.setState({ selectedNavItemId: Navigation.CorrelationRules });
- history.push(ROUTES.CORRELATION_RULES);
- },
- isSelected: selectedNavItemId === Navigation.CorrelationRules,
- },
- ],
- },
],
},
];
@@ -815,26 +775,6 @@ export default class Main extends Component {
);
}}
/>
- ) => {
- return (
-
- this.setState({
- selectedNavItemId: Navigation.CorrelationsUpd,
- })
- }
- dateTimeFilter={this.state.dateTimeFilter}
- setDateTimeFilter={this.setDateTimeFilter}
- dataSource={selectedDataSource}
- notifications={core?.notifications}
- />
- );
- }}
- />
) => (
diff --git a/public/utils/constants.ts b/public/utils/constants.ts
index 0942e533..dec93b83 100644
--- a/public/utils/constants.ts
+++ b/public/utils/constants.ts
@@ -58,7 +58,6 @@ export const ROUTES = Object.freeze({
EDIT_FIELD_MAPPINGS: '/edit-field-mappings',
EDIT_DETECTOR_ALERT_TRIGGERS: '/edit-alert-triggers',
CORRELATIONS: '/correlations',
- CORRELATIONS_UPD: '/correlations-upd',
CORRELATION_RULES: '/correlations/rules',
CORRELATION_RULE_CREATE: '/correlations/create-rule',
CORRELATION_RULE_EDIT: '/correlations/rule',
@@ -106,7 +105,6 @@ export const BREADCRUMBS = Object.freeze({
RULES_DUPLICATE: { text: 'Duplicate rule', href: `#${ROUTES.RULES_DUPLICATE}` },
RULES_IMPORT: { text: 'Import rule', href: `#${ROUTES.RULES_IMPORT}` },
CORRELATIONS: { text: 'Correlations', href: `#${ROUTES.CORRELATIONS}` },
- CORRELATIONS_UPD: { text: 'Correlations Updated', href: `#${ROUTES.CORRELATIONS_UPD}` },
CORRELATION_RULES: { text: 'Correlation rules', href: `#${ROUTES.CORRELATION_RULES}` },
CORRELATIONS_RULE_CREATE: (action: string) => ({
text: `${action} correlation rule`,
From 45b8f218ba8d231fa70ae763fc121e73a6225c94 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 13:30:04 -0800
Subject: [PATCH 04/19] feat: add resources column to the table
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 62 ++++++++++++++++++-
1 file changed, 61 insertions(+), 1 deletion(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index fe6ce913..52f5062a 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -115,6 +115,7 @@ interface CorrelationsTableData {
logTypes: string[];
findingsSeverity: string[];
correlatedFindings: CorrelationFinding[];
+ resources: string[];
}
interface FlyoutTableData {
@@ -717,6 +718,7 @@ export class Correlations extends React.Component();
const findingsSeverity: string[] = [];
let alertsSeverity: string[] = [];
+ const resources: string[] = [];
for (const finding of findingGroup) {
findingsSeverity.push(finding.detectionRule.severity);
@@ -741,6 +743,11 @@ export class Correlations extends React.Component {
+ query.conditions.map((condition) => {
+ resources.push(condition.name + ': ' + condition.value);
+ });
+ });
}
}
}
@@ -754,6 +761,7 @@ export class Correlations extends React.Component
alertSeverityMap[severity].toLowerCase().includes(searchLower)
) ||
- row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower));
+ row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower)) ||
+ row.resources.some((resource) => resource.toLowerCase().includes(searchLower));
return logTypeMatch && severityMatch && searchMatch;
});
};
@@ -1112,6 +1121,57 @@ export class Correlations extends React.Component {
+ if (!resources || resources.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount =
+ resources.length > MAX_DISPLAY ? resources.length - MAX_DISPLAY : 0;
+ const displayedResources = resources.slice(0, MAX_DISPLAY).map((resource) => {
+ return (
+
+ {resource}
+
+ );
+ });
+
+ const tooltipContent = (
+ <>
+ {resources.slice(MAX_DISPLAY).map((resource) => {
+ return (
+
+ {resource}
+
+ );
+ })}
+ >
+ );
+
+ return (
+
+ {displayedResources}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
+ },
{
field: 'actions',
name: 'Actions',
From e9610c1505c3b66aed8a4d2082075111a2ffa199 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 14:40:34 -0800
Subject: [PATCH 05/19] fix: maintain different state for graph in the flyout
than the main graph
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 52f5062a..b8eadfbf 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -138,6 +138,7 @@ interface CorrelationsState {
isFlyoutOpen: boolean;
selectedTableRow: CorrelationsTableData | null;
searchTerm: string;
+ flyoutGraphData: CorrelationGraphData;
}
export const renderTime = (time: number | string) => {
@@ -175,6 +176,7 @@ export class Correlations extends React.Component {
- const { isFlyoutOpen, selectedTableRow, graphData } = this.state;
+ const { isFlyoutOpen, selectedTableRow, flyoutGraphData } = this.state;
if (!isFlyoutOpen || !selectedTableRow) {
return null;
@@ -1356,14 +1358,14 @@ export class Correlations extends React.Component
)}
From 5a62891719cd18d01f6a0d55e9a6d002f32c96a4 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 14:48:29 -0800
Subject: [PATCH 06/19] fix: display mitre tactics as a list and add tooltip
for the field
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 42 +++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index b8eadfbf..5ce3bc6d 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -1287,6 +1287,48 @@ export class Correlations extends React.Component {
+ if (!mitreTactic || mitreTactic.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount =
+ mitreTactic.length > MAX_DISPLAY ? mitreTactic.length - MAX_DISPLAY : 0;
+ const displayedmitreTactic = mitreTactic.slice(0, MAX_DISPLAY).map((logType) => {
+ const label = logType;
+ return {label};
+ });
+ const tooltipContent = (
+ <>
+ {mitreTactic.slice(MAX_DISPLAY).map((logType) => {
+ const label = logType;
+ return (
+
+ {label}
+
+ );
+ })}
+ >
+ );
+ return (
+
+ {displayedmitreTactic}
+ {remainingCount > 0 && (
+
+ {`+${remainingCount} more`}
+
+ )}
+
+ );
+ },
},
{
field: 'detectionRule',
From 302790d1976d68dfd88b5c820cf3e655c0a7ec14 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 17:37:08 -0800
Subject: [PATCH 07/19] fix: update as per comments on PR
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 97 ++++++++-----------
1 file changed, 40 insertions(+), 57 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 5ce3bc6d..47e4b442 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -51,6 +51,7 @@ import { FilterItem, FilterGroup } from '../components/FilterGroup';
import {
BREADCRUMBS,
DEFAULT_DATE_RANGE,
+ DEFAULT_EMPTY_DATA,
MAX_RECENTLY_USED_TIME_RANGES,
ROUTES,
} from '../../../utils/constants';
@@ -70,7 +71,6 @@ import {
setBreadcrumbs,
} from '../../../utils/helpers';
import { PageHeader } from '../../../components/PageHeader/PageHeader';
-import moment from 'moment';
import { ChartContainer } from '../../../components/Charts/ChartContainer';
import {
addInteractiveLegends,
@@ -87,8 +87,6 @@ import {
} from '../../Overview/utils/helpers';
import { debounce } from 'lodash';
-export const DEFAULT_EMPTY_DATA = '-';
-
interface CorrelationsProps
extends RouteComponentProps<
any,
@@ -141,12 +139,6 @@ interface CorrelationsState {
flyoutGraphData: CorrelationGraphData;
}
-export const renderTime = (time: number | string) => {
- const momentTime = moment(time);
- if (time && momentTime.isValid()) return momentTime.format('MM/DD/YY h:mm a');
- return DEFAULT_EMPTY_DATA;
-};
-
export interface CorrelationsTableProps {
finding: FindingItemType;
correlatedFindings: CorrelationFinding[];
@@ -480,12 +472,9 @@ export class Correlations extends React.Component {
- // If there's specific finding info, update the graph
if (this.state.specificFindingInfo) {
this.updateGraphDataState(this.state.specificFindingInfo);
}
- // Force update to refresh the table with new filters
- this.forceUpdate();
}
);
};
@@ -496,12 +485,9 @@ export class Correlations extends React.Component {
- // If there's specific finding info, update the graph
if (this.state.specificFindingInfo) {
this.updateGraphDataState(this.state.specificFindingInfo);
}
- // Force update to refresh the table with new filters
- this.forceUpdate();
}
);
};
@@ -653,7 +639,8 @@ export class Correlations extends React.Component();
const connectedGroups: CorrelationFinding[][] = [];
- function dfs(findingId: string, currentGroup: CorrelationFinding[]) {
+ function depthFirstSearch(findingId: string, currentGroup: CorrelationFinding[]) {
+ // Get all the connected correlated findings for the given finding
visited.add(findingId);
const finding = findingsMap.get(findingId);
if (finding) {
@@ -663,7 +650,7 @@ export class Correlations extends React.Component();
connections.forEach((connectedId) => {
if (!visited.has(connectedId)) {
- dfs(connectedId, currentGroup);
+ depthFirstSearch(connectedId, currentGroup);
}
});
}
@@ -671,7 +658,7 @@ export class Correlations extends React.Component {
if (!visited.has(findingId)) {
const currentGroup: CorrelationFinding[] = [];
- dfs(findingId, currentGroup);
+ depthFirstSearch(findingId, currentGroup);
if (currentGroup.length > 0) {
connectedGroups.push(currentGroup);
}
@@ -695,7 +682,7 @@ export class Correlations extends React.Component {
- this.setState({ searchTerm }, () => {
- this.forceUpdate();
- });
+ this.setState({ searchTerm });
}, 300);
private renderSearchBar = () => {
@@ -937,7 +920,7 @@ export class Correlations extends React.Component[] = [
{
field: 'startTime',
- name: 'Start time',
+ name: 'Start Time',
sortable: true,
dataType: 'date',
render: (startTime: number) => {
@@ -948,7 +931,7 @@ export class Correlations extends React.Component name || 'N/A',
+ render: (name: string) => name || DEFAULT_EMPTY_DATA,
},
{
field: 'logTypes',
@@ -1426,43 +1409,43 @@ export class Correlations extends React.Component
-
- {Array.from(
- new Set(
- selectedTableRow.correlatedFindings.flatMap(
- (finding) => finding.detectionRule.tags?.map((tag) => tag.value) || []
+
+
+ Observed MITRE Attack Tactics
+
+
+
+ {Array.from(
+ new Set(
+ selectedTableRow.correlatedFindings.flatMap(
+ (finding) => finding.detectionRule.tags?.map((tag) => tag.value) || []
+ )
)
- )
- ).map((tactic, i) => {
- const link = `https://attack.mitre.org/techniques/${tactic
- .split('.')
- .slice(1)
- .join('/')
- .toUpperCase()}`;
-
- return (
-
-
-
- {tactic}
-
-
-
- );
- })}
-
+ ).map((tactic, i) => {
+ const link = `https://attack.mitre.org/techniques/${tactic
+ .split('.')
+ .slice(1)
+ .join('/')
+ .toUpperCase()}`;
+
+ return (
+
+
+
+ {tactic}
+
+
+
+ );
+ })}
+
+
);
};
- toggleView = () => {
- this.setState((prevState) => ({
- isGraphView: !prevState.isGraphView,
- }));
- };
-
render() {
const findingCardsData = this.state.specificFindingInfo;
const datePicker = (
From 60e6772aa4c6ab9a99ea254e702adcd3fa138319 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 21:40:22 -0800
Subject: [PATCH 08/19] fix: update as per comments on PR
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 565 +-----------------
.../containers/CorrelationsTable.tsx | 113 ++++
.../containers/CorrelationsTableFlyout.tsx | 201 +++++++
public/pages/Correlations/utils/helpers.tsx | 187 +++++-
4 files changed, 524 insertions(+), 542 deletions(-)
create mode 100644 public/pages/Correlations/containers/CorrelationsTable.tsx
create mode 100644 public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 47e4b442..aa03612b 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -40,11 +40,6 @@ import {
EuiFilterGroup,
EuiHorizontalRule,
EuiButtonGroup,
- EuiBasicTableColumn,
- EuiToolTip,
- EuiInMemoryTable,
- EuiTextColor,
- EuiLink,
EuiFieldSearch,
} from '@elastic/eui';
import { FilterItem, FilterGroup } from '../components/FilterGroup';
@@ -65,27 +60,17 @@ import { Network } from 'react-graph-vis';
import { getLogTypeLabel } from '../../LogTypes/utils/helpers';
import { NotificationsStart } from 'opensearch-dashboards/public';
import {
- capitalizeFirstLetter,
errorNotificationToast,
renderVisualization,
setBreadcrumbs,
} from '../../../utils/helpers';
import { PageHeader } from '../../../components/PageHeader/PageHeader';
import { ChartContainer } from '../../../components/Charts/ChartContainer';
-import {
- addInteractiveLegends,
- DateOpts,
- defaultDateFormat,
- defaultScaleDomain,
- defaultTimeUnit,
- getChartTimeUnit,
- getDomainRange,
- getTimeTooltip,
- getVisualizationSpec,
- getXAxis,
- getYAxis,
-} from '../../Overview/utils/helpers';
+import { getChartTimeUnit, getDomainRange } from '../../Overview/utils/helpers';
import { debounce } from 'lodash';
+import { CorrelationsTable } from './CorrelationsTable';
+import { CorrelationsTableFlyout } from './CorrelationsTableFlyout';
+import { getCorrelatedFindingsVisualizationSpec } from '../utils/helpers';
interface CorrelationsProps
extends RouteComponentProps<
@@ -105,7 +90,7 @@ interface SpecificFindingCorrelations {
correlatedFindings: CorrelationFinding[];
}
-interface CorrelationsTableData {
+export interface CorrelationsTableData {
id: string;
startTime: number;
correlationRule: string;
@@ -116,13 +101,6 @@ interface CorrelationsTableData {
resources: string[];
}
-interface FlyoutTableData {
- timestamp: string;
- mitreTactic: string[];
- detectionRule: string;
- severity: string;
-}
-
interface CorrelationsState {
recentlyUsedRanges: any[];
graphData: CorrelationGraphData;
@@ -757,36 +735,13 @@ export class Correlations extends React.Component {
- return getVisualizationSpec('Correlated Findings data overview', visualizationData, [
- addInteractiveLegends({
- mark: {
- type: 'bar',
- clip: true,
- },
- encoding: {
- tooltip: [getYAxis('correlatedFinding', 'Correlated Findings'), getTimeTooltip(dateOpts)],
- x: getXAxis(dateOpts),
- y: getYAxis('correlatedFinding', 'Count'),
- color: {
- field: 'title',
- legend: {
- title: 'Legend',
- },
- },
- },
- }),
- ]);
};
private generateVisualizationSpec = (connectedFindings: CorrelationFinding[][]) => {
@@ -807,7 +762,7 @@ export class Correlations extends React.Component {
- const alertSeverityMap: { [key: string]: string } = {
- '1': 'critical',
- '2': 'high',
- '3': 'medium',
- '4': 'low',
- '5': 'informational',
- };
-
- const columns: EuiBasicTableColumn[] = [
- {
- field: 'startTime',
- name: 'Start Time',
- sortable: true,
- dataType: 'date',
- render: (startTime: number) => {
- return new Date(startTime).toLocaleString();
- },
- },
- {
- field: 'correlationRule',
- name: 'Correlation Rule',
- sortable: true,
- render: (name: string) => name || DEFAULT_EMPTY_DATA,
- },
- {
- field: 'logTypes',
- name: 'Log Types',
- sortable: true,
- render: (logTypes: string[]) => {
- if (!logTypes || logTypes.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount = logTypes.length > MAX_DISPLAY ? logTypes.length - MAX_DISPLAY : 0;
- const displayedLogTypes = logTypes.slice(0, MAX_DISPLAY).map((logType) => {
- const label = logType;
- return {label};
- });
- const tooltipContent = (
- <>
- {logTypes.slice(MAX_DISPLAY).map((logType) => {
- const label = logType;
- return (
-
- {label}
-
- );
- })}
- >
- );
- return (
-
- {displayedLogTypes}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'alertSeverity',
- name: 'Alert Severity',
- sortable: true,
- render: (alertSeverity: string[]) => {
- if (!alertSeverity || alertSeverity.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount =
- alertSeverity.length > MAX_DISPLAY ? alertSeverity.length - MAX_DISPLAY : 0;
- const displayedSeverities = alertSeverity.slice(0, MAX_DISPLAY).map((severity) => {
- const label = alertSeverityMap[severity];
- const { background, text } = getSeverityColor(label);
- return (
-
- {label}
-
- );
- });
-
- const tooltipContent = (
- <>
- {alertSeverity.slice(MAX_DISPLAY).map((severity) => {
- const label = alertSeverityMap[severity];
- const { background, text } = getSeverityColor(label);
- return (
-
-
- {label}
-
-
- );
- })}
- >
- );
-
- return (
-
- {displayedSeverities}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'findingsSeverity',
- name: 'Findings Severity',
- sortable: true,
- render: (findingsSeverity: string[]) => {
- if (!findingsSeverity || findingsSeverity.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount =
- findingsSeverity.length > MAX_DISPLAY ? findingsSeverity.length - MAX_DISPLAY : 0;
- const displayedSeverities = findingsSeverity.slice(0, MAX_DISPLAY).map((severity) => {
- const label = getSeverityLabel(severity);
- const { background, text } = getSeverityColor(label);
- return (
-
- {label}
-
- );
- });
-
- const tooltipContent = (
-
- {findingsSeverity.slice(MAX_DISPLAY).map((severity) => {
- const label = getSeverityLabel(severity);
- const { background, text } = getSeverityColor(label);
- return (
-
-
- {label}
-
-
- );
- })}
-
- );
-
- return (
-
- {displayedSeverities}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'resources',
- name: 'Resources',
- sortable: true,
- render: (resources: string[]) => {
- if (!resources || resources.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount =
- resources.length > MAX_DISPLAY ? resources.length - MAX_DISPLAY : 0;
- const displayedResources = resources.slice(0, MAX_DISPLAY).map((resource) => {
- return (
-
- {resource}
-
- );
- });
-
- const tooltipContent = (
- <>
- {resources.slice(MAX_DISPLAY).map((resource) => {
- return (
-
- {resource}
-
- );
- })}
- >
- );
-
- return (
-
- {displayedResources}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'actions',
- name: 'Actions',
- render: (_, correlationTableRow: CorrelationsTableData) => {
- return (
-
- {
- this.openTableFlyout(correlationTableRow);
- }}
- />
-
- );
- },
- },
- ];
-
- const getRowProps = (item: any) => {
- return {
- 'data-test-subj': `row-${item.id}`,
- key: item.id,
- className: 'euiTableRow',
- };
- };
-
const filteredTableData = this.getFilteredTableData(this.state.correlationsTableData);
return (
<>
{this.renderCorrelatedFindingsChart()}
-
-
+
>
);
};
@@ -1252,200 +920,6 @@ export class Correlations extends React.Component {
- const { isFlyoutOpen, selectedTableRow, flyoutGraphData } = this.state;
-
- if (!isFlyoutOpen || !selectedTableRow) {
- return null;
- }
-
- const findingsTableColumns: EuiBasicTableColumn[] = [
- {
- field: 'timestamp',
- name: 'Time',
- render: (timestamp: string) => new Date(timestamp).toLocaleString(),
- sortable: true,
- },
- {
- field: 'mitreTactic',
- name: 'Mitre Tactic',
- sortable: true,
- render: (mitreTactic: string[]) => {
- if (!mitreTactic || mitreTactic.length === 0) return DEFAULT_EMPTY_DATA;
- const MAX_DISPLAY = 2;
- const remainingCount =
- mitreTactic.length > MAX_DISPLAY ? mitreTactic.length - MAX_DISPLAY : 0;
- const displayedmitreTactic = mitreTactic.slice(0, MAX_DISPLAY).map((logType) => {
- const label = logType;
- return {label};
- });
- const tooltipContent = (
- <>
- {mitreTactic.slice(MAX_DISPLAY).map((logType) => {
- const label = logType;
- return (
-
- {label}
-
- );
- })}
- >
- );
- return (
-
- {displayedmitreTactic}
- {remainingCount > 0 && (
-
- {`+${remainingCount} more`}
-
- )}
-
- );
- },
- },
- {
- field: 'detectionRule',
- name: 'Detection Rule',
- sortable: true,
- },
- {
- field: 'severity',
- name: 'Severity',
- render: (ruleSeverity: string) => {
- const severity = capitalizeFirstLetter(ruleSeverity) || DEFAULT_EMPTY_DATA;
- const { background, text } = getSeverityColor(severity);
-
- return (
-
- {severity}
-
- );
- },
- },
- ];
-
- const flyoutTableData: FlyoutTableData[] = [];
-
- selectedTableRow.correlatedFindings.map((correlatedFinding) => {
- flyoutTableData.push({
- timestamp: correlatedFinding.timestamp,
- mitreTactic:
- correlatedFinding.detectionRule.tags?.map((mitreTactic) => mitreTactic.value) || [],
- detectionRule: correlatedFinding.detectionRule.name,
- severity: correlatedFinding.detectionRule.severity,
- });
- });
-
- return (
-
-
-
- Correlation Details
-
-
-
-
-
-
- Time
-
- {new Date(selectedTableRow.startTime).toLocaleString()}
-
-
-
-
-
-
- Correlation Rule
-
- {selectedTableRow.correlationRule}
-
-
-
-
-
-
-
-
-
- Correlated Findings
-
- {selectedTableRow.correlatedFindings && (
-
- )}
-
-
-
- Findings
-
-
-
-
-
- Observed MITRE Attack Tactics
-
-
-
- {Array.from(
- new Set(
- selectedTableRow.correlatedFindings.flatMap(
- (finding) => finding.detectionRule.tags?.map((tag) => tag.value) || []
- )
- )
- ).map((tactic, i) => {
- const link = `https://attack.mitre.org/techniques/${tactic
- .split('.')
- .slice(1)
- .join('/')
- .toUpperCase()}`;
-
- return (
-
-
-
- {tactic}
-
-
-
- );
- })}
-
-
-
-
-
- );
- };
-
render() {
const findingCardsData = this.state.specificFindingInfo;
const datePicker = (
@@ -1518,7 +992,7 @@ export class Correlations extends React.Component
- {this.renderTableFlyout()}
+ {this.state.isFlyoutOpen && (
+
+ )}
>
);
}
diff --git a/public/pages/Correlations/containers/CorrelationsTable.tsx b/public/pages/Correlations/containers/CorrelationsTable.tsx
new file mode 100644
index 00000000..c7b57c0b
--- /dev/null
+++ b/public/pages/Correlations/containers/CorrelationsTable.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import React from 'react';
+import {
+ EuiBasicTableColumn,
+ EuiToolTip,
+ EuiSmallButtonIcon,
+ EuiInMemoryTable,
+} from '@elastic/eui';
+import { CorrelationsTableData } from './CorrelationsContainer';
+import { displayBadges, displaySeveritiesBadges, displayResourcesBadges } from '../utils/helpers';
+import { DEFAULT_EMPTY_DATA } from '../../../utils/constants';
+
+interface CorrelationsTableProps {
+ tableData: CorrelationsTableData[];
+ onViewDetails: (row: CorrelationsTableData) => void;
+}
+
+export const CorrelationsTable: React.FC = ({
+ tableData,
+ onViewDetails,
+}) => {
+ const alertSeverityMap: { [key: string]: string } = {
+ '1': 'critical',
+ '2': 'high',
+ '3': 'medium',
+ '4': 'low',
+ '5': 'informational',
+ };
+
+ const columns: EuiBasicTableColumn[] = [
+ {
+ field: 'startTime',
+ name: 'Start Time',
+ sortable: true,
+ dataType: 'date',
+ render: (startTime: number) => {
+ return new Date(startTime).toLocaleString();
+ },
+ },
+ {
+ field: 'correlationRule',
+ name: 'Correlation Rule',
+ sortable: true,
+ render: (name: string) => name || DEFAULT_EMPTY_DATA,
+ },
+ {
+ field: 'logTypes',
+ name: 'Log Types',
+ sortable: true,
+ render: (logTypes: string[]) => displayBadges(logTypes),
+ },
+ {
+ field: 'alertSeverity',
+ name: 'Alert Severity',
+ sortable: true,
+ render: (alertsSeverity: string[]) =>
+ displaySeveritiesBadges(alertsSeverity.map((severity) => alertSeverityMap[severity])),
+ },
+ {
+ field: 'findingsSeverity',
+ name: 'Findings Severity',
+ sortable: true,
+ render: (findingsSeverity: string[]) => displaySeveritiesBadges(findingsSeverity),
+ },
+ {
+ field: 'resources',
+ name: 'Resources',
+ sortable: true,
+ render: (resources: string[]) => displayResourcesBadges(resources),
+ },
+ {
+ field: 'actions',
+ name: 'Actions',
+ render: (_, correlationTableRow: CorrelationsTableData) => {
+ return (
+
+ onViewDetails(correlationTableRow)}
+ />
+
+ );
+ },
+ },
+ ];
+
+ const getRowProps = (item: any) => {
+ return {
+ 'data-test-subj': `row-${item.id}`,
+ key: item.id,
+ className: 'euiTableRow',
+ };
+ };
+
+ return (
+
+ );
+};
diff --git a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
new file mode 100644
index 00000000..c7c61106
--- /dev/null
+++ b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
@@ -0,0 +1,201 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import React from 'react';
+import {
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiTitle,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiTextColor,
+ EuiPanel,
+ EuiBasicTableColumn,
+ EuiInMemoryTable,
+ EuiBadge,
+ EuiLink,
+} from '@elastic/eui';
+import { displayBadges } from '../utils/helpers';
+import { DEFAULT_EMPTY_DATA } from '../../../utils/constants';
+import { capitalizeFirstLetter } from '../../../utils/helpers';
+import { CorrelationGraph } from '../components/CorrelationGraph';
+import { getSeverityColor, graphRenderOptions } from '../utils/constants';
+import { CorrelationFinding, CorrelationGraphData } from '../../../../types';
+import { CorrelationsTableData } from './CorrelationsContainer';
+
+interface FlyoutTableData {
+ timestamp: string;
+ mitreTactic: string[];
+ detectionRule: string;
+ severity: string;
+}
+
+interface CorrelationsTableFlyoutProps {
+ isFlyoutOpen: boolean;
+ selectedTableRow: CorrelationsTableData | null;
+ flyoutGraphData: CorrelationGraphData;
+ loadingGraphData: boolean;
+ onClose: () => void;
+ setNetwork: (network: any) => void;
+}
+
+export const CorrelationsTableFlyout: React.FC = ({
+ isFlyoutOpen,
+ selectedTableRow,
+ flyoutGraphData,
+ loadingGraphData,
+ onClose,
+ setNetwork,
+}) => {
+ if (!isFlyoutOpen || !selectedTableRow) {
+ return null;
+ }
+
+ const findingsTableColumns: EuiBasicTableColumn[] = [
+ {
+ field: 'timestamp',
+ name: 'Time',
+ render: (timestamp: string) => new Date(timestamp).toLocaleString(),
+ sortable: true,
+ },
+ {
+ field: 'mitreTactic',
+ name: 'Mitre Tactic',
+ sortable: true,
+ render: (mitreTactic: string[]) => displayBadges(mitreTactic),
+ },
+ {
+ field: 'detectionRule',
+ name: 'Detection Rule',
+ sortable: true,
+ },
+ {
+ field: 'severity',
+ name: 'Severity',
+ render: (ruleSeverity: string) => {
+ const severity = capitalizeFirstLetter(ruleSeverity) || DEFAULT_EMPTY_DATA;
+ const { background, text } = getSeverityColor(severity);
+
+ return (
+
+ {severity}
+
+ );
+ },
+ },
+ ];
+
+ const flyoutTableData: FlyoutTableData[] = selectedTableRow.correlatedFindings.map(
+ (correlatedFinding: CorrelationFinding) => ({
+ timestamp: correlatedFinding.timestamp,
+ mitreTactic:
+ correlatedFinding.detectionRule.tags?.map((mitreTactic) => mitreTactic.value) || [],
+ detectionRule: correlatedFinding.detectionRule.name,
+ severity: correlatedFinding.detectionRule.severity,
+ })
+ );
+
+ return (
+
+
+
+ Correlation Details
+
+
+
+
+
+
+ Time
+
+ {new Date(selectedTableRow.startTime).toLocaleString()}
+
+
+
+
+
+
+ Correlation Rule
+
+ {selectedTableRow.correlationRule}
+
+
+
+
+
+
+
+
+
+ Correlated Findings
+
+ {selectedTableRow.correlatedFindings && (
+
+ )}
+
+
+
+ Findings
+
+
+
+
+
+ Observed MITRE Attack Tactics
+
+
+
+ {Array.from(
+ new Set(
+ selectedTableRow.correlatedFindings.flatMap(
+ (finding: CorrelationFinding) =>
+ finding.detectionRule.tags?.map((tag) => tag.value) || []
+ )
+ )
+ ).map((tactic, i) => {
+ const link = `https://attack.mitre.org/techniques/${tactic
+ .split('.')
+ .slice(1)
+ .join('/')
+ .toUpperCase()}`;
+
+ return (
+
+
+
+ {tactic}
+
+
+
+ );
+ })}
+
+
+
+
+
+ );
+};
diff --git a/public/pages/Correlations/utils/helpers.tsx b/public/pages/Correlations/utils/helpers.tsx
index 32b543da..4f3c56a5 100644
--- a/public/pages/Correlations/utils/helpers.tsx
+++ b/public/pages/Correlations/utils/helpers.tsx
@@ -4,10 +4,31 @@
*/
import React from 'react';
-import { EuiBasicTableColumn, EuiBadge, EuiToolTip, EuiSmallButtonIcon, EuiLink } from '@elastic/eui';
+import {
+ EuiBasicTableColumn,
+ EuiBadge,
+ EuiToolTip,
+ EuiSmallButtonIcon,
+ EuiLink,
+ EuiFlexItem,
+ EuiFlexGroup,
+} from '@elastic/eui';
import { CorrelationRule, CorrelationRuleQuery, CorrelationRuleTableItem } from '../../../../types';
import { Search } from '@opensearch-project/oui/src/eui_components/basic_table';
import { formatRuleType, getLogTypeFilterOptions } from '../../../utils/helpers';
+import { DEFAULT_EMPTY_DATA } from '../../../utils/constants';
+import { getSeverityColor, getSeverityLabel } from './constants';
+import {
+ addInteractiveLegends,
+ DateOpts,
+ defaultDateFormat,
+ defaultScaleDomain,
+ defaultTimeUnit,
+ getTimeTooltip,
+ getVisualizationSpec,
+ getXAxis,
+ getYAxis,
+} from '../../Overview/utils/helpers';
export const getCorrelationRulesTableColumns = (
onRuleNameClick: (rule: CorrelationRule) => void,
@@ -70,6 +91,141 @@ export const getCorrelationRulesTableColumns = (
];
};
+export const displayBadges = (inputList: string[]) => {
+ if (!inputList || inputList.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount = inputList.length > MAX_DISPLAY ? inputList.length - MAX_DISPLAY : 0;
+ const displayedInputList = inputList.slice(0, MAX_DISPLAY).map((input) => {
+ const label = input;
+ return {label};
+ });
+ const tooltipContent = (
+ <>
+ {inputList.slice(MAX_DISPLAY).map((input) => {
+ const label = input;
+ return (
+
+ {label}
+
+ );
+ })}
+ >
+ );
+ return (
+
+ {displayedInputList}
+ {remainingCount > 0 && (
+
+
+ {`+${remainingCount} more`}
+
+
+ )}
+
+ );
+};
+
+export const displaySeveritiesBadges = (severities: string[]) => {
+ if (!severities || severities.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount = severities.length > MAX_DISPLAY ? severities.length - MAX_DISPLAY : 0;
+ const displayedSeverities = severities.slice(0, MAX_DISPLAY).map((severity) => {
+ const label = getSeverityLabel(severity);
+ const { background, text } = getSeverityColor(label);
+ return (
+
+ {label}
+
+ );
+ });
+
+ const tooltipContent = (
+
+ {severities.slice(MAX_DISPLAY).map((severity) => {
+ const label = getSeverityLabel(severity);
+ const { background, text } = getSeverityColor(label);
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+
+ );
+
+ return (
+
+ {displayedSeverities}
+ {remainingCount > 0 && (
+
+
+ {`+${remainingCount} more`}
+
+
+ )}
+
+ );
+};
+
+export const displayResourcesBadges = (resources: string[]) => {
+ if (!resources || resources.length === 0) return DEFAULT_EMPTY_DATA;
+ const MAX_DISPLAY = 2;
+ const remainingCount = resources.length > MAX_DISPLAY ? resources.length - MAX_DISPLAY : 0;
+ const displayedresources = resources.slice(0, MAX_DISPLAY).map((resources) => {
+ const label = resources;
+ return {label};
+ });
+ const tooltipContent = (
+ <>
+ {resources.slice(MAX_DISPLAY).map((resources) => {
+ const label = resources;
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+ >
+ );
+ return (
+
+ {displayedresources}
+ {remainingCount > 0 && (
+
+
+ {`+${remainingCount} more`}
+
+
+ )}
+
+ );
+};
+
export const getCorrelationRulesTableSearchConfig = (): Search => {
return {
box: {
@@ -87,3 +243,32 @@ export const getCorrelationRulesTableSearchConfig = (): Search => {
],
};
};
+
+export const getCorrelatedFindingsVisualizationSpec = (
+ visualizationData: any[],
+ dateOpts: DateOpts = {
+ timeUnit: defaultTimeUnit,
+ dateFormat: defaultDateFormat,
+ domain: defaultScaleDomain,
+ }
+) => {
+ return getVisualizationSpec('Correlated Findings data overview', visualizationData, [
+ addInteractiveLegends({
+ mark: {
+ type: 'bar',
+ clip: true,
+ },
+ encoding: {
+ tooltip: [getYAxis('correlatedFinding', 'Correlated Findings'), getTimeTooltip(dateOpts)],
+ x: getXAxis(dateOpts),
+ y: getYAxis('correlatedFinding', 'Count'),
+ color: {
+ field: 'title',
+ legend: {
+ title: 'Legend',
+ },
+ },
+ },
+ }),
+ ]);
+};
From 7f7a85938e11fb7f20814b7928659cb7d2e4a55d Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Tue, 28 Jan 2025 21:51:45 -0800
Subject: [PATCH 09/19] fix: move utility function into helpers
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 62 +-----------------
public/pages/Correlations/utils/helpers.tsx | 65 ++++++++++++++++++-
2 files changed, 66 insertions(+), 61 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index aa03612b..bc3fd51e 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -70,7 +70,7 @@ import { getChartTimeUnit, getDomainRange } from '../../Overview/utils/helpers';
import { debounce } from 'lodash';
import { CorrelationsTable } from './CorrelationsTable';
import { CorrelationsTableFlyout } from './CorrelationsTableFlyout';
-import { getCorrelatedFindingsVisualizationSpec } from '../utils/helpers';
+import { getCorrelatedFindingsVisualizationSpec, mapConnectedCorrelations } from '../utils/helpers';
interface CorrelationsProps
extends RouteComponentProps<
@@ -588,64 +588,6 @@ export class Correlations extends React.Component>();
- const findingsMap = new Map();
-
- correlations.forEach((correlation) => {
- const { finding1, finding2 } = correlation;
-
- findingsMap.set(finding1.id, finding1);
- findingsMap.set(finding2.id, finding2);
-
- if (!connectionsMap.has(finding1.id)) {
- connectionsMap.set(finding1.id, new Set());
- }
- connectionsMap.get(finding1.id)!.add(finding2.id);
-
- if (!connectionsMap.has(finding2.id)) {
- connectionsMap.set(finding2.id, new Set());
- }
- connectionsMap.get(finding2.id)!.add(finding1.id);
- });
-
- const visited = new Set();
- const connectedGroups: CorrelationFinding[][] = [];
-
- function depthFirstSearch(findingId: string, currentGroup: CorrelationFinding[]) {
- // Get all the connected correlated findings for the given finding
- visited.add(findingId);
- const finding = findingsMap.get(findingId);
- if (finding) {
- currentGroup.push(finding);
- }
-
- const connections = connectionsMap.get(findingId) || new Set();
- connections.forEach((connectedId) => {
- if (!visited.has(connectedId)) {
- depthFirstSearch(connectedId, currentGroup);
- }
- });
- }
-
- connectionsMap.forEach((_, findingId) => {
- if (!visited.has(findingId)) {
- const currentGroup: CorrelationFinding[] = [];
- depthFirstSearch(findingId, currentGroup);
- if (currentGroup.length > 0) {
- connectedGroups.push(currentGroup);
- }
- }
- });
-
- return connectedGroups;
- }
-
private fetchCorrelationsTableData = async () => {
try {
const start = datemath.parse(this.startTime);
@@ -658,7 +600,7 @@ export class Correlations extends React.Component {
+ const connectionsMap = new Map>();
+ const findingsMap = new Map();
+
+ correlations.forEach((correlation) => {
+ const { finding1, finding2 } = correlation;
+
+ findingsMap.set(finding1.id, finding1);
+ findingsMap.set(finding2.id, finding2);
+
+ if (!connectionsMap.has(finding1.id)) {
+ connectionsMap.set(finding1.id, new Set());
+ }
+ connectionsMap.get(finding1.id)!.add(finding2.id);
+
+ if (!connectionsMap.has(finding2.id)) {
+ connectionsMap.set(finding2.id, new Set());
+ }
+ connectionsMap.get(finding2.id)!.add(finding1.id);
+ });
+
+ const visited = new Set();
+ const connectedGroups: CorrelationFinding[][] = [];
+
+ function depthFirstSearch(findingId: string, currentGroup: CorrelationFinding[]) {
+ // Get all the connected correlated findings for the given finding
+ visited.add(findingId);
+ const finding = findingsMap.get(findingId);
+ if (finding) {
+ currentGroup.push(finding);
+ }
+
+ const connections = connectionsMap.get(findingId) || new Set();
+ connections.forEach((connectedId) => {
+ if (!visited.has(connectedId)) {
+ depthFirstSearch(connectedId, currentGroup);
+ }
+ });
+ }
+
+ connectionsMap.forEach((_, findingId) => {
+ if (!visited.has(findingId)) {
+ const currentGroup: CorrelationFinding[] = [];
+ depthFirstSearch(findingId, currentGroup);
+ if (currentGroup.length > 0) {
+ connectedGroups.push(currentGroup);
+ }
+ }
+ });
+
+ return connectedGroups;
+};
From 823f0f74dcaa8ca2a436a9691c36498c928076a8 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Wed, 29 Jan 2025 17:47:15 -0800
Subject: [PATCH 10/19] fix: make table as default view, add spinner, and alert
name in flyout
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 49 ++++++++++---------
.../containers/CorrelationsTableFlyout.tsx | 9 ++++
2 files changed, 35 insertions(+), 23 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index bc3fd51e..1e36c0ea 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -5,6 +5,7 @@
import {
CorrelationFinding,
CorrelationGraphData,
+ CorrelationRule,
DataSourceProps,
DateTimeFilter,
FindingItemType,
@@ -41,6 +42,7 @@ import {
EuiHorizontalRule,
EuiButtonGroup,
EuiFieldSearch,
+ EuiLoadingChart,
} from '@elastic/eui';
import { FilterItem, FilterGroup } from '../components/FilterGroup';
import {
@@ -99,6 +101,7 @@ export interface CorrelationsTableData {
findingsSeverity: string[];
correlatedFindings: CorrelationFinding[];
resources: string[];
+ correlationRuleObj: CorrelationRule | null;
}
interface CorrelationsState {
@@ -108,7 +111,7 @@ interface CorrelationsState {
logTypeFilterOptions: FilterItem[];
severityFilterOptions: FilterItem[];
loadingGraphData: boolean;
- isGraphView: Boolean;
+ isGraphView: boolean;
correlationsTableData: CorrelationsTableData[];
connectedFindings: CorrelationFinding[][];
isFlyoutOpen: boolean;
@@ -117,17 +120,6 @@ interface CorrelationsState {
flyoutGraphData: CorrelationGraphData;
}
-export interface CorrelationsTableProps {
- finding: FindingItemType;
- correlatedFindings: CorrelationFinding[];
- history: RouteComponentProps['history'];
- isLoading: boolean;
- filterOptions: {
- logTypes: Set;
- ruleSeverity: Set;
- };
-}
-
export class Correlations extends React.Component {
private correlationGraphNetwork?: Network;
@@ -140,7 +132,7 @@ export class Correlations extends React.Component {
+ private onRefresh = async () => {
this.updateState();
+ this.fetchCorrelationsTableData();
};
onLogTypeFilterChange = (items: FilterItem[]) => {
@@ -628,6 +621,7 @@ export class Correlations extends React.Component {
+ private renderCorrelationsTable = (loadingData: boolean) => {
+ if (loadingData) {
+ return (
+
+
+
+ );
+ }
+
const filteredTableData = this.getFilteredTableData(this.state.correlationsTableData);
return (
@@ -995,16 +998,16 @@ export class Correlations extends React.Component this.setState({ isGraphView: id === 'graph' })}
@@ -1015,7 +1018,7 @@ export class Correlations extends React.Component
{this.state.isGraphView
? this.renderCorrelationsGraph(this.state.loadingGraphData)
- : this.renderCorrelationsTable()}
+ : this.renderCorrelationsTable(this.state.loadingGraphData)}
diff --git a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
index c7c61106..02c0446f 100644
--- a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
+++ b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
@@ -116,6 +116,15 @@ export const CorrelationsTableFlyout: React.FC = (
+
+
+
+ Alert
+
+ {selectedTableRow.correlationRuleObj?.trigger?.name}
+
+
+
From 5716dc056464877f7121a4c20bb6513d954234bc Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Wed, 29 Jan 2025 19:27:24 -0800
Subject: [PATCH 11/19] fix: add a different state variable to track updates to
table
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../Correlations/containers/CorrelationsContainer.tsx | 8 ++++++--
.../Correlations/containers/CorrelationsTableFlyout.tsx | 8 ++++----
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 1e36c0ea..f1c1f549 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -111,6 +111,7 @@ interface CorrelationsState {
logTypeFilterOptions: FilterItem[];
severityFilterOptions: FilterItem[];
loadingGraphData: boolean;
+ loadingTableData: boolean;
isGraphView: boolean;
correlationsTableData: CorrelationsTableData[];
connectedFindings: CorrelationFinding[][];
@@ -132,6 +133,7 @@ export class Correlations extends React.Component
{this.state.isGraphView
? this.renderCorrelationsGraph(this.state.loadingGraphData)
- : this.renderCorrelationsTable(this.state.loadingGraphData)}
+ : this.renderCorrelationsTable(this.state.loadingTableData)}
@@ -1027,7 +1031,7 @@ export class Correlations extends React.Component
diff --git a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
index 02c0446f..fe6dd742 100644
--- a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
+++ b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
@@ -38,7 +38,7 @@ interface CorrelationsTableFlyoutProps {
isFlyoutOpen: boolean;
selectedTableRow: CorrelationsTableData | null;
flyoutGraphData: CorrelationGraphData;
- loadingGraphData: boolean;
+ loadingTableData: boolean;
onClose: () => void;
setNetwork: (network: any) => void;
}
@@ -47,7 +47,7 @@ export const CorrelationsTableFlyout: React.FC = (
isFlyoutOpen,
selectedTableRow,
flyoutGraphData,
- loadingGraphData,
+ loadingTableData,
onClose,
setNetwork,
}) => {
@@ -121,7 +121,7 @@ export const CorrelationsTableFlyout: React.FC = (
Alert
- {selectedTableRow.correlationRuleObj?.trigger?.name}
+ {selectedTableRow.correlationRuleObj?.trigger?.name || DEFAULT_EMPTY_DATA}
@@ -144,7 +144,7 @@ export const CorrelationsTableFlyout: React.FC = (
{selectedTableRow.correlatedFindings && (
Date: Wed, 29 Jan 2025 20:05:40 -0800
Subject: [PATCH 12/19] fix: add a different state variable to track updates to
table
---
public/pages/Correlations/containers/CorrelationsContainer.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index f1c1f549..77761995 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -182,6 +182,7 @@ export class Correlations extends React.Component
Date: Thu, 30 Jan 2025 12:12:54 -0800
Subject: [PATCH 13/19] fix: fix a bug that loading spinner is on when there is
no data
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 22 +++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 77761995..b01c546f 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -807,7 +807,7 @@ export class Correlations extends React.Component {
- if (loadingData) {
+ if (this.state.correlationsTableData.length && loadingData) {
return (
@@ -817,11 +817,29 @@ export class Correlations extends React.Component
0 ? (
<>
{this.renderCorrelatedFindingsChart()}
>
+ ) : (
+
+ No correlations found
+
+ }
+ body={
+
+ There are no correlated findings in the system.
+
+ }
+ actions={[
+
+ Create correlation rule
+ ,
+ ]}
+ />
);
};
From b5ec7a5db4b6df286ae67eed251dde0f4a407f7a Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Thu, 30 Jan 2025 12:56:08 -0800
Subject: [PATCH 14/19] fix: fix a bug that loading spinner is on when there is
no data
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 36 ++++++-------------
1 file changed, 11 insertions(+), 25 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index b01c546f..bf5f4a61 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -659,7 +659,6 @@ export class Correlations extends React.Component {
- if (this.state.correlationsTableData.length && loadingData) {
+ if (loadingData) {
return (
@@ -817,29 +817,10 @@ export class Correlations extends React.Component
0 ? (
+ return (
<>
- {this.renderCorrelatedFindingsChart()}
>
- ) : (
-
- No correlations found
-
- }
- body={
-
- There are no correlated findings in the system.
-
- }
- actions={[
-
- Create correlation rule
- ,
- ]}
- />
);
};
@@ -1040,9 +1021,14 @@ export class Correlations extends React.Component
- {this.state.isGraphView
- ? this.renderCorrelationsGraph(this.state.loadingGraphData)
- : this.renderCorrelationsTable(this.state.loadingTableData)}
+ {this.state.isGraphView ? (
+ this.renderCorrelationsGraph(this.state.loadingGraphData)
+ ) : (
+ <>
+ {this.renderCorrelatedFindingsChart()}
+ {this.renderCorrelationsTable(this.state.loadingTableData)}
+ >
+ )}
From 0cb955475f043aed19c7fae2ba4e0b314db79039 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Thu, 30 Jan 2025 13:10:35 -0800
Subject: [PATCH 15/19] fix: change graph configurations to make it
force-directed
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
public/pages/Correlations/utils/constants.tsx | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/public/pages/Correlations/utils/constants.tsx b/public/pages/Correlations/utils/constants.tsx
index 4bd4b254..22c208de 100644
--- a/public/pages/Correlations/utils/constants.tsx
+++ b/public/pages/Correlations/utils/constants.tsx
@@ -33,7 +33,16 @@ export const graphRenderOptions = {
height: '800px',
width: '100%',
physics: {
+ enabled: true,
+ barnesHut: {
+ gravitationalConstant: -2000,
+ centralGravity: 0.2,
+ springConstant: 0.05,
+ springLength: 100,
+ damping: 0.1,
+ },
stabilization: {
+ enabled: true,
fit: true,
iterations: 1000,
},
@@ -42,7 +51,7 @@ export const graphRenderOptions = {
zoomView: true,
zoomSpeed: 0.2,
dragView: true,
- dragNodes: false,
+ dragNodes: true,
multiselect: true,
tooltipDelay: 50,
hover: true,
From ee78c47f77e897fce166535df60c30bf687fbc88 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Thu, 30 Jan 2025 13:25:36 -0800
Subject: [PATCH 16/19] fix: change graph configurations to make it
force-directed
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
public/pages/Correlations/utils/constants.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/public/pages/Correlations/utils/constants.tsx b/public/pages/Correlations/utils/constants.tsx
index 22c208de..08ad33c2 100644
--- a/public/pages/Correlations/utils/constants.tsx
+++ b/public/pages/Correlations/utils/constants.tsx
@@ -35,16 +35,16 @@ export const graphRenderOptions = {
physics: {
enabled: true,
barnesHut: {
- gravitationalConstant: -2000,
- centralGravity: 0.2,
- springConstant: 0.05,
- springLength: 100,
- damping: 0.1,
+ gravitationalConstant: -7000,
+ centralGravity: 0.9,
+ springConstant: 0.01,
+ springLength: 125,
+ damping: 0.07,
},
stabilization: {
enabled: true,
fit: true,
- iterations: 1000,
+ iterations: 1500,
},
},
interaction: {
From 4a28185b993a0252d3a1bb32a89c90627f53fd38 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Thu, 30 Jan 2025 13:28:20 -0800
Subject: [PATCH 17/19] fix: increase correlations graph panel height
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../pages/Correlations/containers/CorrelationsTableFlyout.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
index fe6dd742..709ceb65 100644
--- a/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
+++ b/public/pages/Correlations/containers/CorrelationsTableFlyout.tsx
@@ -148,7 +148,7 @@ export const CorrelationsTableFlyout: React.FC = (
graph={flyoutGraphData.graph}
options={{
...graphRenderOptions,
- height: '300px',
+ height: '400px',
width: '100%',
autoResize: true,
}}
From 87c2b874c163bd06e83134e6b7eaae16942cd270 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Thu, 30 Jan 2025 16:50:34 -0800
Subject: [PATCH 18/19] seperate out table-view into a different file
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../containers/CorrelationsContainer.tsx | 240 +-------------
.../containers/CorrelationsTable.tsx | 6 +-
.../containers/CorrelationsTableView.tsx | 302 ++++++++++++++++++
public/pages/Correlations/utils/constants.tsx | 4 +-
4 files changed, 323 insertions(+), 229 deletions(-)
create mode 100644 public/pages/Correlations/containers/CorrelationsTableView.tsx
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index bf5f4a61..f03ecc2c 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -42,7 +42,6 @@ import {
EuiHorizontalRule,
EuiButtonGroup,
EuiFieldSearch,
- EuiLoadingChart,
} from '@elastic/eui';
import { FilterItem, FilterGroup } from '../components/FilterGroup';
import {
@@ -61,18 +60,11 @@ import { renderToStaticMarkup } from 'react-dom/server';
import { Network } from 'react-graph-vis';
import { getLogTypeLabel } from '../../LogTypes/utils/helpers';
import { NotificationsStart } from 'opensearch-dashboards/public';
-import {
- errorNotificationToast,
- renderVisualization,
- setBreadcrumbs,
-} from '../../../utils/helpers';
+import { errorNotificationToast, setBreadcrumbs } from '../../../utils/helpers';
import { PageHeader } from '../../../components/PageHeader/PageHeader';
-import { ChartContainer } from '../../../components/Charts/ChartContainer';
-import { getChartTimeUnit, getDomainRange } from '../../Overview/utils/helpers';
import { debounce } from 'lodash';
-import { CorrelationsTable } from './CorrelationsTable';
-import { CorrelationsTableFlyout } from './CorrelationsTableFlyout';
-import { getCorrelatedFindingsVisualizationSpec, mapConnectedCorrelations } from '../utils/helpers';
+import { CorrelationsTableView } from './CorrelationsTableView';
+import { mapConnectedCorrelations } from '../utils/helpers';
interface CorrelationsProps
extends RouteComponentProps<
@@ -115,10 +107,7 @@ interface CorrelationsState {
isGraphView: boolean;
correlationsTableData: CorrelationsTableData[];
connectedFindings: CorrelationFinding[][];
- isFlyoutOpen: boolean;
- selectedTableRow: CorrelationsTableData | null;
searchTerm: string;
- flyoutGraphData: CorrelationGraphData;
}
export class Correlations extends React.Component {
@@ -137,10 +126,7 @@ export class Correlations extends React.Component {
- let newGraphData: CorrelationGraphData = {
- graph: {
- nodes: [],
- edges: [],
- },
- events: {
- click: this.onNodeClick,
- },
- };
-
- if (correlationTableRow.correlatedFindings) {
- const correlationPairs = this.getCorrelationPairs(correlationTableRow.correlatedFindings);
- newGraphData = this.prepareGraphData(correlationPairs);
- }
-
- // Set all required state at once
- this.setState({
- isFlyoutOpen: true,
- selectedTableRow: correlationTableRow,
- flyoutGraphData: newGraphData,
- });
- };
-
- private closeTableFlyout = () => {
- this.setState({
- isFlyoutOpen: false,
- selectedTableRow: null,
- flyoutGraphData: {
- graph: { nodes: [], edges: [] },
- events: { click: this.onNodeClick },
- },
- });
- };
-
onFindingInspect = async (id: string, logType: string) => {
// get finding data and set the specificFindingInfo
const specificFindingInfo = await DataStore.correlations.getCorrelatedFindings(id, logType);
@@ -687,102 +638,6 @@ export class Correlations extends React.Component {
- const visData = connectedFindings.map((correlatedFindings) => {
- return {
- title: 'Correlated Findings',
- correlatedFinding: correlatedFindings.length,
- time: correlatedFindings[0].timestamp,
- };
- });
-
- const {
- dateTimeFilter = {
- startTime: DEFAULT_DATE_RANGE.start,
- endTime: DEFAULT_DATE_RANGE.end,
- },
- } = this.props;
-
- const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime);
-
- return getCorrelatedFindingsVisualizationSpec(visData, {
- timeUnit: chartTimeUnits.timeUnit,
- dateFormat: chartTimeUnits.dateFormat,
- domain: getDomainRange(
- [dateTimeFilter.startTime, dateTimeFilter.endTime],
- chartTimeUnits.timeUnit.unit
- ),
- });
- };
-
- private renderCorrelatedFindingsChart = () => {
- renderVisualization(
- this.generateVisualizationSpec(this.state.connectedFindings),
- 'correlated-findings-view'
- );
-
- return (
- <>
-
-
-
-
-
-
- Correlated Findings
-
-
-
-
-
-
-
-
-
-
- >
- );
- };
-
- private getFilteredTableData = (tableData: CorrelationsTableData[]): CorrelationsTableData[] => {
- const { logTypeFilterOptions, severityFilterOptions } = this.state;
- const alertSeverityMap: { [key: string]: string } = {
- '1': 'critical',
- '2': 'high',
- '3': 'medium',
- '4': 'low',
- '5': 'informational',
- };
-
- const selectedLogTypes = logTypeFilterOptions
- .filter((item) => item.checked === 'on' && item.visible)
- .map((item) => item.id);
-
- const selectedSeverities = severityFilterOptions
- .filter((item) => item.checked === 'on' && item.visible)
- .map((item) => item.id.toLowerCase());
-
- return tableData.filter((row) => {
- const logTypeMatch = row.logTypes.some((logType) => selectedLogTypes.includes(logType));
-
- const severityMatch = row.alertSeverity.some((severity) =>
- selectedSeverities.includes(alertSeverityMap[severity])
- );
-
- const searchLower = this.state.searchTerm.toLowerCase();
- const searchMatch =
- this.state.searchTerm === '' ||
- row.correlationRule?.toLowerCase().includes(searchLower) ||
- row.logTypes.some((type) => type.toLowerCase().includes(searchLower)) ||
- row.alertSeverity.some((severity) =>
- alertSeverityMap[severity].toLowerCase().includes(searchLower)
- ) ||
- row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower)) ||
- row.resources.some((resource) => resource.toLowerCase().includes(searchLower));
- return logTypeMatch && severityMatch && searchMatch;
- });
- };
-
private debouncedSearch = debounce((searchTerm: string) => {
this.setState({ searchTerm });
}, 300);
@@ -806,70 +661,6 @@ export class Correlations extends React.Component {
- if (loadingData) {
- return (
-
-
-
- );
- }
-
- const filteredTableData = this.getFilteredTableData(this.state.correlationsTableData);
-
- return (
- <>
-
- >
- );
- };
-
- private prepareGraphData = (correlationPairs: CorrelationFinding[][] | [any, any][]) => {
- const createdEdges = new Set();
- const createdNodes = new Set();
- const graphData: CorrelationGraphData = {
- graph: {
- nodes: [],
- edges: [],
- },
- events: {
- click: this.onNodeClick,
- },
- };
-
- correlationPairs.forEach((correlation: CorrelationFinding[]) => {
- const possibleCombination1 = `${correlation[0].id}:${correlation[1].id}`;
- const possibleCombination2 = `${correlation[1].id}:${correlation[0].id}`;
-
- if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
- return;
- }
-
- if (!createdNodes.has(correlation[0].id)) {
- this.addNode(graphData.graph.nodes, correlation[0]);
- createdNodes.add(correlation[0].id);
- }
- if (!createdNodes.has(correlation[1].id)) {
- this.addNode(graphData.graph.nodes, correlation[1]);
- createdNodes.add(correlation[1].id);
- }
- this.addEdge(graphData.graph.edges, correlation[0], correlation[1]);
- createdEdges.add(possibleCombination1);
- });
-
- return graphData;
- };
-
- private getCorrelationPairs = (correlatedFindings: any[]) => {
- const pairs: [any, any][] = [];
- for (let i = 0; i < correlatedFindings.length; i++) {
- for (let j = i + 1; j < correlatedFindings.length; j++) {
- pairs.push([correlatedFindings[i], correlatedFindings[j]]);
- }
- }
- return pairs;
- };
-
render() {
const findingCardsData = this.state.specificFindingInfo;
const datePicker = (
@@ -1025,23 +816,24 @@ export class Correlations extends React.Component
- {this.renderCorrelatedFindingsChart()}
- {this.renderCorrelationsTable(this.state.loadingTableData)}
+
>
)}
- {this.state.isFlyoutOpen && (
-
- )}
>
);
}
diff --git a/public/pages/Correlations/containers/CorrelationsTable.tsx b/public/pages/Correlations/containers/CorrelationsTable.tsx
index c7b57c0b..b8bcbff6 100644
--- a/public/pages/Correlations/containers/CorrelationsTable.tsx
+++ b/public/pages/Correlations/containers/CorrelationsTable.tsx
@@ -14,12 +14,12 @@ import { displayBadges, displaySeveritiesBadges, displayResourcesBadges } from '
import { DEFAULT_EMPTY_DATA } from '../../../utils/constants';
interface CorrelationsTableProps {
- tableData: CorrelationsTableData[];
+ correlationsTableData: CorrelationsTableData[];
onViewDetails: (row: CorrelationsTableData) => void;
}
export const CorrelationsTable: React.FC = ({
- tableData,
+ correlationsTableData,
onViewDetails,
}) => {
const alertSeverityMap: { [key: string]: string } = {
@@ -99,7 +99,7 @@ export const CorrelationsTable: React.FC = ({
return (
void;
+ addEdge: (edges: any[], f1: CorrelationFinding, f2: CorrelationFinding) => void;
+ onNodeClick: (params: any) => void;
+ setNetwork: (network: Network) => void;
+ createNodeTooltip: ({ detectionRule, timestamp, logType }: CorrelationFinding) => Element | null;
+}
+
+interface CorrelationsTableViewState {
+ isFlyoutOpen: boolean;
+ selectedTableRow: CorrelationsTableData | null;
+ flyoutGraphData: CorrelationGraphData;
+}
+
+export class CorrelationsTableView extends React.Component<
+ CorrelationsTableViewProps,
+ CorrelationsTableViewState
+> {
+ static defaultProps = {
+ dateTimeFilter: {
+ startTime: DEFAULT_DATE_RANGE.start,
+ endTime: DEFAULT_DATE_RANGE.end,
+ },
+ };
+
+ constructor(props: CorrelationsTableViewProps) {
+ super(props);
+ this.state = {
+ isFlyoutOpen: false,
+ selectedTableRow: null,
+ flyoutGraphData: { ...emptyGraphData },
+ };
+ }
+
+ private getCorrelationPairs = (correlatedFindings: any[]) => {
+ const pairs: [any, any][] = [];
+ for (let i = 0; i < correlatedFindings.length; i++) {
+ for (let j = i + 1; j < correlatedFindings.length; j++) {
+ pairs.push([correlatedFindings[i], correlatedFindings[j]]);
+ }
+ }
+ return pairs;
+ };
+
+ private prepareGraphData = (correlationPairs: CorrelationFinding[][] | [any, any][]) => {
+ const createdEdges = new Set();
+ const createdNodes = new Set();
+ const graphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.props.onNodeClick,
+ },
+ };
+
+ correlationPairs.forEach((correlation: CorrelationFinding[]) => {
+ const possibleCombination1 = `${correlation[0].id}:${correlation[1].id}`;
+ const possibleCombination2 = `${correlation[1].id}:${correlation[0].id}`;
+
+ if (createdEdges.has(possibleCombination1) || createdEdges.has(possibleCombination2)) {
+ return;
+ }
+
+ if (!createdNodes.has(correlation[0].id)) {
+ this.props.addNode(graphData.graph.nodes, correlation[0]);
+ createdNodes.add(correlation[0].id);
+ }
+ if (!createdNodes.has(correlation[1].id)) {
+ this.props.addNode(graphData.graph.nodes, correlation[1]);
+ createdNodes.add(correlation[1].id);
+ }
+ this.props.addEdge(graphData.graph.edges, correlation[0], correlation[1]);
+ createdEdges.add(possibleCombination1);
+ });
+
+ return graphData;
+ };
+
+ private openTableFlyout = (correlationTableRow: CorrelationsTableData) => {
+ let newGraphData: CorrelationGraphData = {
+ graph: {
+ nodes: [],
+ edges: [],
+ },
+ events: {
+ click: this.props.onNodeClick,
+ },
+ };
+ if (correlationTableRow.correlatedFindings) {
+ const correlationPairs = this.getCorrelationPairs(correlationTableRow.correlatedFindings);
+ newGraphData = this.prepareGraphData(correlationPairs);
+ }
+ this.setState({
+ isFlyoutOpen: true,
+ selectedTableRow: correlationTableRow,
+ flyoutGraphData: newGraphData,
+ });
+ };
+
+ private renderCorrelationsTable = (loadingData: boolean) => {
+ if (loadingData) {
+ return (
+
+
+
+ );
+ }
+ const filteredTableData = this.getFilteredTableData(this.props.correlationsTableData);
+ return (
+ <>
+
+ >
+ );
+ };
+
+ private renderCorrelatedFindingsChart = () => {
+ renderVisualization(
+ this.generateVisualizationSpec(this.props.connectedFindings),
+ 'correlated-findings-view'
+ );
+ return this.props.connectedFindings.length > 0 ? (
+ <>
+
+
+
+
+
+
+ Correlated Findings
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ No correlations found
+
+ }
+ body={
+
+ There are no correlated findings in the system.
+
+ }
+ actions={[
+
+ Create correlation rule
+ ,
+ ]}
+ />
+ );
+ };
+
+ private getFilteredTableData = (tableData: CorrelationsTableData[]): CorrelationsTableData[] => {
+ const { logTypeFilterOptions, severityFilterOptions, searchTerm } = this.props;
+ const alertSeverityMap: { [key: string]: string } = {
+ '1': 'critical',
+ '2': 'high',
+ '3': 'medium',
+ '4': 'low',
+ '5': 'informational',
+ };
+
+ const selectedLogTypes = logTypeFilterOptions
+ .filter((item) => item.checked === 'on' && item.visible)
+ .map((item) => item.id);
+
+ const selectedSeverities = severityFilterOptions
+ .filter((item) => item.checked === 'on' && item.visible)
+ .map((item) => item.id.toLowerCase());
+
+ return tableData.filter((row) => {
+ const logTypeMatch = row.logTypes.some((logType) => selectedLogTypes.includes(logType));
+
+ const severityMatch = row.alertSeverity.some((severity) =>
+ selectedSeverities.includes(alertSeverityMap[severity])
+ );
+
+ const searchLower = searchTerm.toLowerCase();
+ const searchMatch =
+ searchTerm === '' ||
+ row.correlationRule?.toLowerCase().includes(searchLower) ||
+ row.logTypes.some((type) => type.toLowerCase().includes(searchLower)) ||
+ row.alertSeverity.some((severity) =>
+ alertSeverityMap[severity].toLowerCase().includes(searchLower)
+ ) ||
+ row.findingsSeverity.some((severity) => severity.toLowerCase().includes(searchLower)) ||
+ row.resources.some((resource) => resource.toLowerCase().includes(searchLower));
+ return logTypeMatch && severityMatch && searchMatch;
+ });
+ };
+
+ private generateVisualizationSpec = (connectedFindings: CorrelationFinding[][]) => {
+ const visData = connectedFindings.map((correlatedFindings) => {
+ return {
+ title: 'Correlated Findings',
+ correlatedFinding: correlatedFindings.length,
+ time: correlatedFindings[0].timestamp,
+ };
+ });
+
+ const { dateTimeFilter } = this.props;
+ const chartTimeUnits = getChartTimeUnit(
+ dateTimeFilter?.startTime || DEFAULT_DATE_RANGE.start,
+ dateTimeFilter?.endTime || DEFAULT_DATE_RANGE.end
+ );
+
+ return getCorrelatedFindingsVisualizationSpec(visData, {
+ timeUnit: chartTimeUnits.timeUnit,
+ dateFormat: chartTimeUnits.dateFormat,
+ domain: getDomainRange(
+ [
+ dateTimeFilter?.startTime || DEFAULT_DATE_RANGE.start,
+ dateTimeFilter?.endTime || DEFAULT_DATE_RANGE.end,
+ ],
+ chartTimeUnits.timeUnit.unit
+ ),
+ });
+ };
+
+ private closeTableFlyout = () => {
+ this.setState({
+ isFlyoutOpen: false,
+ selectedTableRow: null,
+ flyoutGraphData: {
+ graph: { nodes: [], edges: [] },
+ events: { click: this.props.onNodeClick },
+ },
+ });
+ };
+
+ render() {
+ return (
+ <>
+ {this.renderCorrelatedFindingsChart()}
+ {this.renderCorrelationsTable(this.props.loadingTableData)}
+ {this.state.isFlyoutOpen && (
+
+ )}
+ >
+ );
+ }
+}
diff --git a/public/pages/Correlations/utils/constants.tsx b/public/pages/Correlations/utils/constants.tsx
index 08ad33c2..744d4f6a 100644
--- a/public/pages/Correlations/utils/constants.tsx
+++ b/public/pages/Correlations/utils/constants.tsx
@@ -36,10 +36,10 @@ export const graphRenderOptions = {
enabled: true,
barnesHut: {
gravitationalConstant: -7000,
- centralGravity: 0.9,
+ centralGravity: 0.5,
springConstant: 0.01,
springLength: 125,
- damping: 0.07,
+ damping: 0.1,
},
stabilization: {
enabled: true,
From cae89ad4a51abcd9d3c9445fbbd9f10f3bfdb5f9 Mon Sep 17 00:00:00 2001
From: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
Date: Fri, 31 Jan 2025 01:18:57 -0800
Subject: [PATCH 19/19] update according to comments on PR
Signed-off-by: vikhy-aws <191836418+vikhy-aws@users.noreply.github.com>
---
.../pages/Correlations/containers/CorrelationsContainer.tsx | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index f03ecc2c..abf74d15 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -590,11 +590,7 @@ export class Correlations extends React.Component