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