/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ import { CorrelationFinding, CorrelationGraphData, DateTimeFilter } 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, EuiSuperDatePicker, EuiSpacer, EuiButtonEmpty, EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiButtonIcon, EuiText, EuiEmptyPrompt, EuiButton, EuiBadge, EuiFilterGroup, } from '@elastic/eui'; import { FilterItem, FilterGroup } from '../components/FilterGroup'; import { CoreServicesContext } from '../../../components/core_services'; 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 { FindingItemType } from '../../Findings/containers/Findings/Findings'; import datemath from '@elastic/datemath'; import { ruleSeverity } from '../../Rules/utils/constants'; import { renderToStaticMarkup } from 'react-dom/server'; import { Network } from 'react-graph-vis'; interface CorrelationsProps extends RouteComponentProps< any, any, { finding: FindingItemType; correlatedFindings: CorrelationFinding[] } > { setDateTimeFilter?: Function; dateTimeFilter?: DateTimeFilter; onMount: () => void; } interface SpecificFindingCorrelations { finding: CorrelationFinding; correlatedFindings: CorrelationFinding[]; } interface CorrelationsState { recentlyUsedRanges: any[]; graphData: CorrelationGraphData; specificFindingInfo?: SpecificFindingCorrelations; logTypeFilterOptions: FilterItem[]; severityFilterOptions: FilterItem[]; loadingGraphData: boolean; } export class Correlations extends React.Component { static contextType = CoreServicesContext; private correlationGraphNetwork?: Network; constructor(props: CorrelationsProps) { super(props); this.state = { recentlyUsedRanges: [DEFAULT_DATE_RANGE], graphData: { ...emptyGraphData }, logTypeFilterOptions: [...getDefaultLogTypeFilterItemOptions()], severityFilterOptions: [...defaultSeverityFilterItemOptions], specificFindingInfo: undefined, loadingGraphData: false, }; } 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 { this.context.chrome.setBreadcrumbs([BREADCRUMBS.SECURITY_ANALYTICS, BREADCRUMBS.CORRELATIONS]); this.updateState(); this.props.onMount(); } componentDidUpdate( prevProps: Readonly, prevState: Readonly, snapshot?: any ): void { if ( prevState.logTypeFilterOptions !== this.state.logTypeFilterOptions || prevState.severityFilterOptions !== this.state.severityFilterOptions || prevProps.dateTimeFilter !== this.props.dateTimeFilter ) { this.updateState(); } } private async updateState() { if (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, }, }, 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(`${correlation.finding1.id}:${correlation.finding2.id}`); }); this.setState({ graphData }); } } private onNodeClick = async (params: any) => { if (params.nodes.length !== 1) { return; } const findingId = params.nodes[0]; if (this.state.specificFindingInfo?.finding.id === findingId) { return; } const allFindings = await DataStore.correlations.fetchAllFindings(); const detectorType = allFindings[findingId].logType; const correlations = await DataStore.correlations.getCorrelatedFindings( findingId, detectorType ); this.setState({ specificFindingInfo: correlations }); this.updateGraphDataState(correlations); }; private updateGraphDataState(specificFindingInfo: SpecificFindingCorrelations) { const graphData: CorrelationGraphData = { graph: { nodes: [], edges: [], }, events: { click: this.onNodeClick, }, }; this.addNode(graphData.graph.nodes, specificFindingInfo.finding); specificFindingInfo.correlatedFindings.forEach((finding) => { this.addNode(graphData.graph.nodes, finding); this.addEdge(graphData.graph.edges, specificFindingInfo.finding, finding); }); this.setState({ graphData }); } private addNode(nodes: any[], finding: CorrelationFinding) { const borderColor = getSeverityColor(finding.detectionRule.severity).background; nodes.push({ id: finding.id, title: this.createNodeTooltip(finding), color: { background: borderColor, border: borderColor, highlight: { background: '#e7f5ff', border: borderColor, }, hover: { background: '#e7f5ff', border: borderColor, }, }, widthConstraint: { minimum: 40, }, borderWidth: 2, font: { multi: 'html', size: 12, }, chosen: true, }); } 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}
); 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 }); }; onSeverityFilterChange = (items: FilterItem[]) => { this.setState({ severityFilterOptions: items }); }; closeFlyout = () => { this.setState({ specificFindingInfo: undefined }); }; 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', })), }); }; setNetwork = (network: Network) => { this.correlationGraphNetwork = network; }; renderCorrelationsGraph(loadingData: boolean) { return this.state.graphData.graph.nodes.length > 0 || loadingData ? ( ) : (

No correlations found

} body={

There are no correlated findings in the system.

} actions={[ Create correlation rule , ]} /> ); } render() { const findingCardsData = this.state.specificFindingInfo; return ( <> {findingCardsData ? (

Correlation

Finding

Correlated Findings()

Higher correlation score indicated stronger correlation. {findingCardsData.correlatedFindings.map((finding, index) => { return ( <> ); })}
) : null}

Correlations

Reset filters Severity: {ruleSeverity.map((sev, idx) => ( {sev.value} ))} {this.renderCorrelationsGraph(this.state.loadingGraphData)}
); } }