diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 6e13d90f0..854b1982b 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -94,12 +94,12 @@ jobs: # Window is slow so wait longer - name: Sleep until OSD server starts - windows if: ${{ matrix.os == 'windows-latest' }} - run: Start-Sleep -s 400 + run: Start-Sleep -s 450 shell: powershell - name: Sleep until OSD server starts - non-windows if: ${{ matrix.os != 'windows-latest' }} - run: sleep 400 + run: sleep 450 shell: bash - name: Install Cypress @@ -132,6 +132,7 @@ jobs: working-directory: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin command: yarn run cypress run wait-on: 'http://localhost:5601' + wait-on-timeout: 300 browser: chrome env: CYPRESS_CACHE_FOLDER: ${{ matrix.cypress_cache_folder }} diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 0ad469c2d..79733044a 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -243,7 +243,7 @@ describe('Detectors', () => { beforeEach(() => { cy.intercept('/detectors/_search').as('detectorsSearch'); - // Visit Detectors page + // Visit Detectors page before any test cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); }); diff --git a/public/app.scss b/public/app.scss index 27e53780a..01c242a5d 100644 --- a/public/app.scss +++ b/public/app.scss @@ -16,6 +16,7 @@ $euiTextColor: $euiColorDarkestShade !default; @import "./pages/Main/components/Callout.scss"; @import "./pages/Detectors/components/ReviewFieldMappings/ReviewFieldMappings.scss"; @import "./pages/Correlations/Correlations.scss"; +@import "./pages/Correlations/components/FindingCard.scss"; @import "./pages/Findings/components/CorrelationsTable/CorrelationsTable.scss"; .selected-radio-panel { @@ -39,6 +40,10 @@ $euiTextColor: $euiColorDarkestShade !default; @return mix($euiColorInk, $color, $percent); } +.euiLoadingContent__singleLineBackground { + background: linear-gradient(137deg, #d3dae6 45%, #bac0ca 50%, #d3dae6 55%) !important; +} + .refresh-button { min-width: 0; .euiButtonContent { diff --git a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx index 9c3870339..d9d98b280 100644 --- a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx +++ b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx @@ -29,7 +29,6 @@ import { renderTime, } from '../../../../utils/helpers'; import { FindingsService, IndexPatternsService, OpenSearchService } from '../../../../services'; -import FindingDetailsFlyout from '../../../Findings/components/FindingDetailsFlyout'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { Finding } from '../../../Findings/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -49,7 +48,6 @@ export interface AlertFlyoutProps { export interface AlertFlyoutState { acknowledged: boolean; - findingFlyoutData?: Finding; findingItems: Finding[]; loading: boolean; rules: { [key: string]: RuleSource }; @@ -71,10 +69,6 @@ export class AlertFlyout extends React.Component { this.setState({ loading: true }); const { @@ -126,6 +120,18 @@ export class AlertFlyout extends React.Component[] { const { detector } = this.props; const { rules } = this.state; + + const backButton = ( + DataStore.findings.closeFlyout()} + display="base" + size="s" + data-test-subj={'finding-details-flyout-back-button'} + /> + ); + return [ { field: 'timestamp', @@ -142,7 +148,22 @@ export class AlertFlyout extends React.Component ( this.setFindingFlyoutData(finding)} + onClick={() => { + const customRules = detector.inputs[0].detector_input.custom_rules[0]; + const prePackagedRules = detector.inputs[0].detector_input.pre_packaged_rules[0]; + const rule = rules[customRules?.id] || rules[prePackagedRules?.id] || {}; + DataStore.findings.openFlyout( + { + ...finding, + detector: { _id: detector.id as string, _index: '', _source: detector }, + ruleName: rule.title, + ruleSeverity: rule.level, + }, + [...this.state.findingItems, finding], + true, + backButton + ); + }} data-test-subj={'finding-details-flyout-button'} > {`${(id as string).slice(0, 7)}...`} @@ -175,32 +196,9 @@ export class AlertFlyout extends React.Component this.setFindingFlyoutData()} - display="base" - size="s" - data-test-subj={'finding-details-flyout-back-button'} - /> - } - allRules={rules} - indexPatternsService={this.props.indexPatternService} - /> - ) : ( + return ( void; }; + finding: CorrelationFinding; + findings: CorrelationFinding[]; } export const FindingCard: React.FC = ({ @@ -40,13 +45,25 @@ export const FindingCard: React.FC = ({ logType, timestamp, detectionRule, + finding, + findings, }) => { const correlationHeader = correlationData ? ( <> - - + + {correlationData.score} + + + DataStore.findings.openFlyout(finding, findings, false)} + /> + + = ({ {getLabelFromLogType(logType)} + {!correlationData && ( + + + DataStore.findings.openFlyout(finding, findings, false)} + /> + + + )} {correlationHeader ? : null} diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx index a4b93f7ea..22cab1abc 100644 --- a/public/pages/Correlations/containers/CorrelationsContainer.tsx +++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx @@ -133,8 +133,10 @@ export class Correlations extends React.Component @@ -456,6 +460,8 @@ export class Correlations extends React.Component diff --git a/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx index b5bb8bf80..90783f294 100644 --- a/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx +++ b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx @@ -22,6 +22,7 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { FindingItemType } from '../../containers/Findings/Findings'; import { RouteComponentProps } from 'react-router-dom'; +import { DataStore } from '../../../../store/DataStore'; export interface CorrelationsTableProps { finding: FindingItemType; @@ -131,6 +132,7 @@ export const CorrelationsTable: React.FC = ({ ]; const goToCorrelationsPage = () => { + DataStore.findings.closeFlyout(); history.push({ pathname: `${ROUTES.CORRELATIONS}`, state: { diff --git a/public/pages/Findings/components/FindingDetailsFlyout.tsx b/public/pages/Findings/components/FindingDetailsFlyout.tsx index c3237abb9..a8c356d1e 100644 --- a/public/pages/Findings/components/FindingDetailsFlyout.tsx +++ b/public/pages/Findings/components/FindingDetailsFlyout.tsx @@ -30,6 +30,9 @@ import { EuiIcon, EuiTabs, EuiTab, + EuiInMemoryTable, + EuiBasicTableColumn, + EuiLoadingContent, } from '@elastic/eui'; import { capitalizeFirstLetter, renderTime } from '../../../utils/helpers'; import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../utils/constants'; @@ -43,19 +46,21 @@ 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 { +export interface FindingDetailsFlyoutBaseProps { finding: FindingItemType; findings: FindingItemType[]; + shouldLoadAllFindings: boolean; backButton?: React.ReactNode; - allRules: { [id: string]: RuleSource }; +} + +export interface FindingDetailsFlyoutProps extends FindingDetailsFlyoutBaseProps { opensearchService: OpenSearchService; indexPatternsService: IndexPatternsService; correlationService: CorrelationService; - closeFlyout: () => void; - shouldLoadAllFindings: boolean; + history: History; } interface FindingDetailsFlyoutState { @@ -65,6 +70,7 @@ interface FindingDetailsFlyoutState { isCreateIndexPatternModalVisible: boolean; selectedTab: { id: string; content: React.ReactNode | null }; correlatedFindings: CorrelationFinding[]; + allRules: { [id: string]: RuleSource }; isDocumentLoading: boolean; areCorrelationsLoading: boolean; } @@ -79,10 +85,22 @@ export default class FindingDetailsFlyout extends Component< loading: false, ruleViewerFlyoutData: null, isCreateIndexPatternModalVisible: false, - selectedTab: { id: FindingFlyoutTabId.DETAILS, content: null }, + selectedTab: { + id: FindingFlyoutTabId.DETAILS, + content: ( + <> + +

Rule details

+
+ + + + ), + }, correlatedFindings: [], isDocumentLoading: true, areCorrelationsLoading: true, + allRules: {}, }; } @@ -136,11 +154,17 @@ export default class FindingDetailsFlyout extends Component< this.getCorrelations(); - this.setState({ - selectedTab: { - id: FindingFlyoutTabId.DETAILS, - content: this.getTabContent(FindingFlyoutTabId.DETAILS), - }, + DataStore.rules.getAllRules().then((rules) => { + const allRules: { [id: string]: RuleSource } = {}; + rules.forEach((hit) => (allRules[hit._id] = hit._source)); + this.setState({ allRules }, () => { + this.setState({ + selectedTab: { + id: FindingFlyoutTabId.DETAILS, + content: this.getTabContent(FindingFlyoutTabId.DETAILS), + }, + }); + }); }); } @@ -181,7 +205,7 @@ export default class FindingDetailsFlyout extends Component< }; renderRuleDetails = (rules: Query[] = []) => { - const { allRules } = this.props; + const { allRules = {} } = this.state; return rules.map((rule, key) => { const fullRule = allRules[rule.id]; const severity = capitalizeFirstLetter(fullRule.level); @@ -426,7 +450,7 @@ export default class FindingDetailsFlyout extends Component< } render() { - const { closeFlyout, backButton } = this.props; + const { backButton } = this.props; const { finding: { id, @@ -440,7 +464,7 @@ export default class FindingDetailsFlyout extends Component< const { isDocumentLoading } = this.state; return (
diff --git a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx index 3009fbd18..2790af14c 100644 --- a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx +++ b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx @@ -24,13 +24,13 @@ import { IndexPatternsService, CorrelationService, } from '../../../../services'; -import FindingDetailsFlyout from '../FindingDetailsFlyout'; import { Finding } from '../../models/interfaces'; import CreateAlertFlyout from '../CreateAlertFlyout'; import { NotificationChannelTypeOptions } from '../../../CreateDetector/components/ConfigureAlerts/models/interfaces'; import { FindingItemType } from '../../containers/Findings/Findings'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { RuleSource } from '../../../../../server/models/interfaces'; +import { DataStore } from '../../../../store/DataStore'; interface FindingsTableProps extends RouteComponentProps { detectorService: DetectorsService; @@ -114,42 +114,6 @@ export default class FindingsTable extends Component { - if (this.state.flyoutOpen) this.closeFlyout(); - else { - const { findings, rules } = this.props; - const { findingsFiltered, filteredFindings } = this.state; - - const logTypes = new Set(); - const severities = new Set(); - filteredFindings.forEach((finding) => { - if (finding) { - const queryId = finding.queries[0].id; - logTypes.add(rules[queryId].category); - severities.add(rules[queryId].level); - } - }); - - this.setState({ - flyout: ( - - ), - flyoutOpen: true, - selectedFinding: finding, - }); - } - }; - renderCreateAlertFlyout = (finding: Finding) => { if (this.state.flyoutOpen) this.closeFlyout(); else { @@ -207,7 +171,7 @@ export default class FindingsTable extends Component ( this.renderFindingDetailsFlyout(finding)} + onClick={() => DataStore.findings.openFlyout(finding, this.state.filteredFindings)} data-test-subj={'finding-details-flyout-button'} > {`${(id as string).slice(0, 7)}...`} @@ -254,7 +218,9 @@ export default class FindingsTable extends Component this.renderFindingDetailsFlyout(finding)} + onClick={() => + DataStore.findings.openFlyout(finding, this.state.filteredFindings) + } /> ), diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index e2cfd639b..f8df933cf 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -45,6 +45,9 @@ import { DataStore } from '../../store/DataStore'; import { CreateCorrelationRule } from '../Correlations/containers/CreateCorrelationRule'; import { CorrelationRules } from '../Correlations/containers/CorrelationRules'; import { Correlations } from '../Correlations/containers/CorrelationsContainer'; +import FindingDetailsFlyout, { + FindingDetailsFlyoutBaseProps, +} from '../Findings/components/FindingDetailsFlyout'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -82,6 +85,7 @@ interface MainState { dateTimeFilter: DateTimeFilter; callout?: ICalloutProps; toasts?: Toast[]; + findingFlyout: FindingDetailsFlyoutBaseProps | null; } const navItemIndexByRoute: { [route: string]: number } = { @@ -102,11 +106,19 @@ export default class Main extends Component { startTime: DEFAULT_DATE_RANGE.start, endTime: DEFAULT_DATE_RANGE.end, }, + findingFlyout: null, }; DataStore.detectors.setHandlers(this.showCallout, this.showToast); + DataStore.findings.setFlyoutCallback(this.showFindingFlyout); } + showFindingFlyout = (findingFlyout: FindingDetailsFlyoutBaseProps | null) => { + this.setState({ + findingFlyout, + }); + }; + showCallout = (callout?: ICalloutProps) => { this.setState({ callout, @@ -180,7 +192,7 @@ export default class Main extends Component { history, } = this.props; - const { callout } = this.state; + const { callout, findingFlyout } = this.state; const sideNav: EuiSideNavItemType<{ style: any }>[] = [ { name: Navigation.SecurityAnalytics, @@ -302,6 +314,15 @@ export default class Main extends Component { )} {callout ? : null} + {findingFlyout ? ( + + ) : null} > | Promise> => { let indexPattern; const indexPatterns = await this.getIndexPatterns(); - console.log('indexPatterns', indexPatterns); indexPatterns?.some((indexRef) => { if (indexRef.references.findIndex((reference) => reference.id === detectorId) > -1) { indexPattern = indexRef; diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 1a2bb5d9e..89fa8795d 100644 --- a/public/store/CorrelationsStore.ts +++ b/public/store/CorrelationsStore.ts @@ -177,8 +177,10 @@ export class CorrelationsStore implements ICorrelationsStore { const rule = allRules.find((rule) => rule._id === f.queries[0].id); findings[f.id] = { + ...f, id: f.id, logType: detector._source.detector_type, + detector: detector, detectorName: detector._source.name, timestamp: new Date(f.timestamp).toLocaleString(), detectionRule: rule @@ -227,6 +229,7 @@ export class CorrelationsStore implements ICorrelationsStore { return { finding: { + ...allFindings[finding], id: finding, logType: detector_type, timestamp: '', diff --git a/public/store/FindingsStore.ts b/public/store/FindingsStore.ts index 4a65bc6a5..6fd8532f1 100644 --- a/public/store/FindingsStore.ts +++ b/public/store/FindingsStore.ts @@ -3,11 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; import { DetectorsService, FindingsService } from '../services'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { RouteComponentProps } from 'react-router-dom'; import { errorNotificationToast } from '../utils/helpers'; import { FindingItemType } from '../pages/Findings/containers/Findings/Findings'; +import { FindingDetailsFlyoutBaseProps } from '../pages/Findings/components/FindingDetailsFlyout'; export interface IFindingsStore { readonly service: FindingsService; @@ -19,6 +21,19 @@ export interface IFindingsStore { getFindingsPerDetector: (detectorId: string) => Promise; getAllFindings: () => Promise; + + setFlyoutCallback: ( + flyoutCallback: (findingFlyout: FindingDetailsFlyoutBaseProps | null) => void + ) => void; + + openFlyout: ( + finding: FindingItemType, + findings: FindingItemType[], + shouldLoadAllFindings: boolean, + backButton?: React.ReactNode + ) => void; + + closeFlyout: () => void; } export interface IFindingsCache {} @@ -105,4 +120,29 @@ export class FindingsStore implements IFindingsStore { return allFindings; }; + + public setFlyoutCallback = ( + flyoutCallback: (findingFlyout: FindingDetailsFlyoutBaseProps | null) => void + ): void => { + this.openFlyoutCallback = flyoutCallback; + }; + + public openFlyoutCallback = (findingFlyout: FindingDetailsFlyoutBaseProps | null) => {}; + + closeFlyout = () => this.openFlyoutCallback(null); + + public openFlyout = ( + finding: FindingItemType, + findings: FindingItemType[], + shouldLoadAllFindings: boolean = false, + backButton?: React.ReactNode + ) => { + const flyout = { + finding, + findings, + shouldLoadAllFindings, + backButton, + } as FindingDetailsFlyoutBaseProps; + this.openFlyoutCallback(flyout); + }; }