diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js
index 2033d9967..ca572887e 100644
--- a/cypress/support/helpers.js
+++ b/cypress/support/helpers.js
@@ -190,7 +190,12 @@ Cypress.Commands.add(
cy.get($tr).within(($tr) => {
data.map((rowData) => {
rowData.forEach((tdData) => {
- tdData && cy.get($tr).find('td').contains(`${tdData}`);
+ if (typeof tdData === 'string') {
+ tdData && cy.get($tr).find('td').contains(`${tdData}`);
+ } else {
+ // if rule is an object then use path
+ tdData && cy.get($tr).find('td').contains(`${tdData.path}`);
+ }
});
});
});
diff --git a/public/app.scss b/public/app.scss
index 2cff1f2d7..27e53780a 100644
--- a/public/app.scss
+++ b/public/app.scss
@@ -15,7 +15,8 @@ $euiTextColor: $euiColorDarkestShade !default;
@import "./pages/Overview/components/Widgets/WidgetContainer.scss";
@import "./pages/Main/components/Callout.scss";
@import "./pages/Detectors/components/ReviewFieldMappings/ReviewFieldMappings.scss";
-@import "./pages/Correlations//Correlations.scss";
+@import "./pages/Correlations/Correlations.scss";
+@import "./pages/Findings/components/CorrelationsTable/CorrelationsTable.scss";
.selected-radio-panel {
background-color: tintOrShade($euiColorPrimary, 90%, 70%);
diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx
index 081973f42..554de6f9d 100644
--- a/public/pages/Correlations/containers/CorrelationsContainer.tsx
+++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx
@@ -391,17 +391,20 @@ export class Correlations extends React.Component
{findingCardsData.correlatedFindings.map((finding, index) => {
return (
-
+ <>
+
+
+ >
);
})}
diff --git a/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss
new file mode 100644
index 000000000..ed98fdddb
--- /dev/null
+++ b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss
@@ -0,0 +1,6 @@
+.correlations-table-details-row {
+ .correlations-table-details-row-value {
+ font-weight: 600;
+ color: $euiColorDarkestShade;
+ }
+}
diff --git a/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx
new file mode 100644
index 000000000..b5bb8bf80
--- /dev/null
+++ b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx
@@ -0,0 +1,178 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { CorrelationFinding } from '../../../../../types';
+import { ruleTypes } from '../../../Rules/utils/constants';
+import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants';
+import { getSeverityBadge } from '../../../Rules/utils/helpers';
+import {
+ EuiButton,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiText,
+ EuiPanel,
+ EuiInMemoryTable,
+ EuiBasicTableColumn,
+} from '@elastic/eui';
+import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
+import { FindingItemType } from '../../containers/Findings/Findings';
+import { RouteComponentProps } from 'react-router-dom';
+
+export interface CorrelationsTableProps {
+ finding: FindingItemType;
+ correlatedFindings: CorrelationFinding[];
+ history: RouteComponentProps['history'];
+ isLoading: boolean;
+}
+
+export const CorrelationsTable: React.FC = ({
+ correlatedFindings,
+ finding,
+ history,
+ isLoading,
+}) => {
+ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
+ [key: string]: JSX.Element;
+ }>({});
+
+ const toggleCorrelationDetails = (item: any) => {
+ const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
+ if (itemIdToExpandedRowMapValues[item.id]) {
+ delete itemIdToExpandedRowMapValues[item.id];
+ } else {
+ itemIdToExpandedRowMapValues[item.id] = (
+
+
+
+ Finding ID
+
+
+
+ {item.id}
+
+
+
+
+
+
+ Threat detector
+
+
+
+ {item.detectorName}
+
+
+
+
+
+
+ Detection rule
+
+
+
+ {item.detectionRule?.name || '-'}
+
+
+
+
+ );
+ }
+
+ setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
+ };
+
+ const columns: EuiBasicTableColumn[] = [
+ {
+ field: 'timestamp',
+ name: 'Time',
+ sortable: true,
+ },
+ {
+ name: 'Correlated rule',
+ truncateText: true,
+ render: (item: CorrelationFinding) => item?.correlationRule.name || DEFAULT_EMPTY_DATA,
+ },
+ {
+ field: 'logType',
+ name: 'Log type',
+ sortable: true,
+ render: (category: string) =>
+ // TODO: This formatting may need some refactoring depending on the response payload
+ ruleTypes.find((ruleType) => ruleType.value === category)?.label || DEFAULT_EMPTY_DATA,
+ },
+ {
+ name: 'Rule severity',
+ truncateText: true,
+ align: 'center',
+ render: (item: CorrelationFinding) => getSeverityBadge(item.detectionRule.severity),
+ },
+ {
+ field: 'correlationScore',
+ name: 'Score',
+ sortable: true,
+ },
+ {
+ align: RIGHT_ALIGNMENT,
+ width: '40px',
+ isExpander: true,
+ render: (item: any) => (
+ toggleCorrelationDetails(item)}
+ aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
+ iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
+ />
+ ),
+ },
+ ];
+
+ const goToCorrelationsPage = () => {
+ history.push({
+ pathname: `${ROUTES.CORRELATIONS}`,
+ state: {
+ finding: finding,
+ correlatedFindings: correlatedFindings,
+ },
+ });
+ };
+
+ return (
+ <>
+
+
+
+ Correlated findings
+
+
+
+ goToCorrelationsPage()}
+ disabled={correlatedFindings.length === 0}
+ >
+ View correlations graph
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/public/pages/Findings/components/FindingDetailsFlyout.tsx b/public/pages/Findings/components/FindingDetailsFlyout.tsx
index e636c63a0..c3237abb9 100644
--- a/public/pages/Findings/components/FindingDetailsFlyout.tsx
+++ b/public/pages/Findings/components/FindingDetailsFlyout.tsx
@@ -30,8 +30,6 @@ import {
EuiIcon,
EuiTabs,
EuiTab,
- EuiInMemoryTable,
- EuiBasicTableColumn,
} from '@elastic/eui';
import { capitalizeFirstLetter, renderTime } from '../../../utils/helpers';
import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../utils/constants';
@@ -39,14 +37,14 @@ import { Query } from '../models/interfaces';
import { RuleViewerFlyout } from '../../Rules/components/RuleViewerFlyout/RuleViewerFlyout';
import { RuleSource } from '../../../../server/models/interfaces';
import { OpenSearchService, IndexPatternsService, CorrelationService } from '../../../services';
-import { getSeverityBadge, RuleTableItem } from '../../Rules/utils/helpers';
+import { RuleTableItem } from '../../Rules/utils/helpers';
import { CreateIndexPatternForm } from './CreateIndexPatternForm';
import { FindingItemType } from '../containers/Findings/Findings';
import { CorrelationFinding, RuleItemInfoBase } from '../../../../types';
import { FindingFlyoutTabId, FindingFlyoutTabs } from '../utils/constants';
import { DataStore } from '../../../store/DataStore';
import { RouteComponentProps } from 'react-router-dom';
-import { ruleTypes } from '../../Rules/utils/constants';
+import { CorrelationsTable } from './CorrelationsTable/CorrelationsTable';
interface FindingDetailsFlyoutProps extends RouteComponentProps {
finding: FindingItemType;
@@ -96,24 +94,33 @@ export default class FindingDetailsFlyout extends Component<
allFindings = await DataStore.findings.getAllFindings();
}
- DataStore.correlations
- .getCorrelatedFindings(id, detector._source?.detector_type)
- .then((findings) => {
- if (findings?.correlatedFindings.length) {
- let correlatedFindings: any[] = [];
- findings.correlatedFindings.map((finding) => {
- allFindings.map((item) => {
- if (finding.id === item.id) {
- correlatedFindings.push(finding);
- }
+ DataStore.correlations.getCorrelationRules().then((correlationRules) => {
+ DataStore.correlations
+ .getCorrelatedFindings(id, detector._source?.detector_type)
+ .then((findings) => {
+ if (findings?.correlatedFindings.length) {
+ let correlatedFindings: any[] = [];
+ findings.correlatedFindings.map((finding: CorrelationFinding) => {
+ allFindings.map((item: FindingItemType) => {
+ if (finding.id === item.id) {
+ correlatedFindings.push({
+ ...finding,
+ correlationRule: correlationRules.find(
+ (rule) => finding.rules?.indexOf(rule.id) !== -1
+ ),
+ });
+ }
+ });
});
+ this.setState({ correlatedFindings });
+ }
+ })
+ .finally(() => {
+ this.setState({
+ areCorrelationsLoading: false,
});
- this.setState({ correlatedFindings });
- }
- })
- .finally(() => {
- this.setState({ areCorrelationsLoading: false });
- });
+ });
+ });
};
componentDidMount(): void {
@@ -383,98 +390,23 @@ export default class FindingDetailsFlyout extends Component<
}
}
- private getTabContent(
- tabId: FindingFlyoutTabId,
- isDocumentLoading = false,
- areCorrelationsLoading = false
- ) {
+ private getTabContent(tabId: FindingFlyoutTabId, isDocumentLoading = false) {
switch (tabId) {
case FindingFlyoutTabId.CORRELATIONS:
- return this.createCorrelationsTable(areCorrelationsLoading);
+ return (
+
+ );
case FindingFlyoutTabId.DETAILS:
default:
return this.createFindingDetails(isDocumentLoading);
}
}
- private goToCorrelationsPage = () => {
- const { correlatedFindings } = this.state;
- const { finding } = this.props;
-
- this.props.history.push({
- pathname: `${ROUTES.CORRELATIONS}`,
- state: {
- finding: finding,
- correlatedFindings: correlatedFindings,
- },
- });
- };
-
- private createCorrelationsTable(areCorrelationsLoading: boolean) {
- const columns: EuiBasicTableColumn[] = [
- {
- field: 'timestamp',
- name: 'Time',
- sortable: true,
- },
- {
- field: 'id',
- name: 'Correlated finding id',
- },
- {
- field: 'logType',
- name: 'Log type',
- sortable: true,
- render: (category: string) =>
- // TODO: This formatting may need some refactoring depending on the response payload
- ruleTypes.find((ruleType) => ruleType.value === category)?.label || DEFAULT_EMPTY_DATA,
- },
- {
- name: 'Rule severity',
- truncateText: true,
- align: 'center',
- render: (item: CorrelationFinding) => getSeverityBadge(item.detectionRule.severity),
- },
- {
- field: 'correlationScore',
- name: 'Score',
- sortable: true,
- },
- ];
-
- return (
- <>
-
-
-
- Correlated findings
-
-
-
- this.goToCorrelationsPage()}
- disabled={this.state.correlatedFindings.length === 0}
- >
- View correlations graph
-
-
-
-
-
-
-
-
- >
- );
- }
-
private createFindingDetails(isDocumentLoading: boolean) {
const {
finding: { queries },
@@ -505,7 +437,7 @@ export default class FindingDetailsFlyout extends Component<
timestamp,
},
} = this.props;
- const { isDocumentLoading, areCorrelationsLoading } = this.state;
+ const { isDocumentLoading } = this.state;
return (
- {tab.name}
+ {tab.id === 'Correlations' ? (
+ <>
+ {tab.name} (
+ {this.state.areCorrelationsLoading
+ ? DEFAULT_EMPTY_DATA
+ : this.state.correlatedFindings.length}
+ )
+ >
+ ) : (
+ tab.name
+ )}
);
})}
diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts
index e735dd709..1a2bb5d9e 100644
--- a/public/store/CorrelationsStore.ts
+++ b/public/store/CorrelationsStore.ts
@@ -16,6 +16,10 @@ import { NotificationsStart } from 'opensearch-dashboards/public';
import { errorNotificationToast } from '../utils/helpers';
import { DEFAULT_EMPTY_DATA } from '../utils/constants';
+export interface ICorrelationsCache {
+ [key: string]: CorrelationRule[];
+}
+
export class CorrelationsStore implements ICorrelationsStore {
/**
* Correlation rules service instance
@@ -34,6 +38,21 @@ export class CorrelationsStore implements ICorrelationsStore {
*/
readonly notifications: NotificationsStart;
+ /**
+ * Keeps rule's data cached
+ *
+ * @property {ICorrelationsCache} cache
+ */
+ private cache: ICorrelationsCache = {};
+
+ /**
+ * Invalidates all rules data
+ */
+ private invalidateCache = () => {
+ this.cache = {};
+ return this;
+ };
+
constructor(
service: CorrelationService,
detectorsService: DetectorsService,
@@ -48,7 +67,7 @@ export class CorrelationsStore implements ICorrelationsStore {
}
public async createCorrelationRule(correlationRule: CorrelationRule): Promise {
- const response = await this.service.createCorrelationRule({
+ const response = await this.invalidateCache().service.createCorrelationRule({
name: correlationRule.name,
correlate: correlationRule.queries?.map((query) => ({
index: query.index,
@@ -69,10 +88,16 @@ export class CorrelationsStore implements ICorrelationsStore {
}
public async getCorrelationRules(index?: string): Promise {
+ const cacheKey: string = `getCorrelationRules:${JSON.stringify(arguments)}`;
+
+ if (this.cache[cacheKey]) {
+ return this.cache[cacheKey];
+ }
+
const response = await this.service.getCorrelationRules(index);
if (response?.ok) {
- return response.response.hits.hits.map((hit) => {
+ return (this.cache[cacheKey] = response.response.hits.hits.map((hit) => {
const queries: CorrelationRuleQuery[] = hit._source.correlate.map((queryData) => {
return {
index: queryData.index,
@@ -86,14 +111,14 @@ export class CorrelationsStore implements ICorrelationsStore {
name: hit._source.name,
queries,
};
- });
+ }));
}
return [];
}
public async deleteCorrelationRule(ruleId: string): Promise {
- const response = await this.service.deleteCorrelationRule(ruleId);
+ const response = await this.invalidateCache().service.deleteCorrelationRule(ruleId);
if (!response.ok) {
errorNotificationToast(this.notifications, 'delete', 'correlation rule', response.error);
@@ -154,6 +179,7 @@ export class CorrelationsStore implements ICorrelationsStore {
findings[f.id] = {
id: f.id,
logType: detector._source.detector_type,
+ detectorName: detector._source.name,
timestamp: new Date(f.timestamp).toLocaleString(),
detectionRule: rule
? {
diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts
index c9f55094c..4f085d1e7 100644
--- a/public/store/DataStore.ts
+++ b/public/store/DataStore.ts
@@ -7,15 +7,14 @@ import { RulesStore } from './RulesStore';
import { BrowserServices } from '../models/interfaces';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { DetectorsStore } from './DetectorsStore';
-import { ICorrelationsStore } from '../../types';
import { CorrelationsStore } from './CorrelationsStore';
-import { FindingsStore, IFindingsStore } from './FindingsStore';
+import { FindingsStore } from './FindingsStore';
export class DataStore {
public static rules: RulesStore;
public static detectors: DetectorsStore;
- public static correlations: ICorrelationsStore;
- public static findings: IFindingsStore;
+ public static correlations: CorrelationsStore;
+ public static findings: FindingsStore;
public static init = (services: BrowserServices, notifications: NotificationsStart) => {
const rulesStore = new RulesStore(services.ruleService, notifications);
diff --git a/types/Correlations.ts b/types/Correlations.ts
index 290638459..4db43c44e 100644
--- a/types/Correlations.ts
+++ b/types/Correlations.ts
@@ -26,9 +26,12 @@ export interface CorrelationGraphData {
export type CorrelationFinding = {
id: string;
correlationScore?: number;
+ correlationRule?: CorrelationFindingHit;
logType: string;
timestamp: string;
detectionRule: { name: string; severity: string };
+ detectorName?: string;
+ rules?: string[];
};
export interface CorrelationRuleQuery {
@@ -43,15 +46,15 @@ export interface CorrelationFieldCondition {
condition: 'AND' | 'OR';
}
-export interface CorrelationRule extends CorrelationRuleModel {
- id: string;
-}
-
export interface CorrelationRuleModel {
name: string;
queries: CorrelationRuleQuery[];
}
+export interface CorrelationRule extends CorrelationRuleModel {
+ id: string;
+}
+
export interface CorrelationRuleSourceQueries {
index: string;
query: string;