diff --git a/models/interfaces.ts b/models/interfaces.ts index c06b08e4d..397d1dbad 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -3,25 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export interface Rule { - id: string; - category: string; - log_source: { - product?: string; - category?: string; - service?: string; - }; - title: string; - description: string; - tags: Array<{ value: string }>; - false_positives: Array<{ value: string }>; - level: string; - status: string; - references: Array<{ value: string }>; - author: string; - detection: string; -} - export interface PeriodSchedule { period: { interval: number; diff --git a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx index e2e3eb9bb..f1da52711 100644 --- a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx +++ b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx @@ -28,7 +28,7 @@ import { formatRuleType, renderTime, } from '../../../../utils/helpers'; -import { FindingsService, IndexPatternsService, OpenSearchService } from '../../../../services'; +import { IndexPatternsService, OpenSearchService } from '../../../../services'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { Finding } from '../../../Findings/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -38,7 +38,6 @@ import { Detector } from '../../../../../types'; export interface AlertFlyoutProps { alertItem: AlertItem; detector: Detector; - findingsService: FindingsService; notifications: NotificationsStart; opensearchService: OpenSearchService; indexPatternService: IndexPatternsService; @@ -71,21 +70,12 @@ export class AlertFlyout extends React.Component { this.setState({ loading: true }); - const { - alertItem: { detector_id }, - findingsService, - notifications, - } = this.props; + const { notifications } = this.props; try { - const findingRes = await findingsService.getFindings({ detectorId: detector_id }); - if (findingRes.ok) { - const relatedFindings = findingRes.response.findings.filter((finding) => - this.props.alertItem.finding_ids.includes(finding.id) - ); - this.setState({ findingItems: relatedFindings }); - } else { - errorNotificationToast(notifications, 'retrieve', 'findings', findingRes.error); - } + const relatedFindings = await DataStore.findings.getFindingsByIds( + this.props.alertItem.finding_ids + ); + this.setState({ findingItems: relatedFindings }); } catch (e: any) { errorNotificationToast(notifications, 'retrieve', 'findings', e); } diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index b5773e6ac..63486d71a 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -4,7 +4,6 @@ */ import { - DurationRange, EuiBasicTableColumn, EuiButton, EuiButtonIcon, @@ -18,6 +17,7 @@ import { EuiTitle, EuiToolTip, EuiEmptyPrompt, + EuiTableSelectionType, } from '@elastic/eui'; import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter'; import dateMath from '@elastic/datemath'; @@ -58,6 +58,8 @@ import { match, RouteComponentProps, withRouter } from 'react-router-dom'; import { DateTimeFilter } from '../../../Overview/models/interfaces'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { Detector } from '../../../../../types'; +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import { DataStore } from '../../../../store/DataStore'; export interface AlertsProps extends RouteComponentProps { alertService: AlertsService; @@ -66,7 +68,7 @@ export interface AlertsProps extends RouteComponentProps { opensearchService: OpenSearchService; notifications: NotificationsStart; indexPatternService: IndexPatternsService; - match: match; + match: match<{ detectorId: string }>; dateTimeFilter?: DateTimeFilter; setDateTimeFilter?: Function; } @@ -192,14 +194,14 @@ export class Alerts extends Component { name: 'Detector', sortable: true, dataType: 'string', - render: (detectorName) => detectorName || DEFAULT_EMPTY_DATA, + render: (detectorName: string) => detectorName || DEFAULT_EMPTY_DATA, }, { field: 'state', name: 'Status', sortable: true, dataType: 'string', - render: (status) => (status ? capitalizeFirstLetter(status) : DEFAULT_EMPTY_DATA), + render: (status: string) => (status ? capitalizeFirstLetter(status) : DEFAULT_EMPTY_DATA), }, { field: 'severity', @@ -300,7 +302,7 @@ export class Alerts extends Component { async getAlerts() { this.setState({ loading: true }); - const { alertService, detectorService, notifications } = this.props; + const { detectorService, notifications } = this.props; const { detectors } = this.state; try { const detectorsRes = await detectorService.getDetectors(); @@ -315,18 +317,11 @@ export class Alerts extends Component { for (let id of detectorIds) { if (!detectorId || detectorId === id) { - const alertsRes = await alertService.getAlerts({ detector_id: id }); - - if (alertsRes.ok) { - const detectorAlerts = alertsRes.response.alerts.map((alert) => { - const detector = detectors[id]; - if (!alert.detector_id) alert.detector_id = id; - return { ...alert, detectorName: detector.name }; - }); - alerts = alerts.concat(detectorAlerts); - } else { - errorNotificationToast(notifications, 'retrieve', 'alerts', alertsRes.error); - } + const detectorAlerts = await DataStore.alerts.getAlertsByDetector( + id, + detectors[id].name + ); + alerts = alerts.concat(detectorAlerts); } } @@ -334,7 +329,7 @@ export class Alerts extends Component { } else { errorNotificationToast(notifications, 'retrieve', 'detectors', detectorsRes.error); } - } catch (e) { + } catch (e: any) { errorNotificationToast(notifications, 'retrieve', 'alerts', e); } this.filterAlerts(); @@ -412,7 +407,7 @@ export class Alerts extends Component { } } } - } catch (e) { + } catch (e: any) { errorNotificationToast(notifications, 'acknowledge', 'alerts', e); } if (successCount) @@ -439,8 +434,8 @@ export class Alerts extends Component { endTime: DEFAULT_DATE_RANGE.end, }, } = this.props; - const severities = new Set(); - const statuses = new Set(); + const severities = new Set(); + const statuses = new Set(); filteredAlerts.forEach((alert) => { if (alert) { severities.add(alert.severity); @@ -477,11 +472,10 @@ export class Alerts extends Component { ], }; - const selection = { + const selection: EuiTableSelectionType = { onSelectionChange: this.onSelectionChange, - selectable: (item) => item.state === ALERT_STATE.ACTIVE, - selectableMessage: (selectable) => - selectable ? undefined : DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT, + selectable: (item: AlertItem) => item.state === ALERT_STATE.ACTIVE, + selectableMessage: (selectable) => (selectable ? '' : DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT), }; const sorting: any = { @@ -500,7 +494,6 @@ export class Alerts extends Component { detector={detectors[flyoutData.alertItem.detector_id]} onClose={this.onFlyoutClose} onAcknowledge={this.onAcknowledge} - findingsService={this.props.findingService} indexPatternService={this.props.indexPatternService} /> )} diff --git a/public/pages/Correlations/components/FindingCard.tsx b/public/pages/Correlations/components/FindingCard.tsx index 69358544f..6b5c77c06 100644 --- a/public/pages/Correlations/components/FindingCard.tsx +++ b/public/pages/Correlations/components/FindingCard.tsx @@ -25,7 +25,7 @@ export interface FindingCardProps { id: string; logType: string; timestamp: string; - detectionRule: { name: string; severity: string }; + detectionRule: CorrelationFinding['detectionRule']; correlationData?: { score: string; onInspect: (findingId: string, logType: string) => void; @@ -57,50 +57,56 @@ export const FindingCard: React.FC = ({ }); } - const badgePadding = '0px 4px'; + const badgePadding = '0px 6px'; const { text: severityText, background } = getSeverityColor(detectionRule.severity); - - const header = ( - - -
- - {getSeverityLabel(detectionRule.severity)} - - - {getLabelFromLogType(logType)} - -
-
- -
- - DataStore.findings.openFlyout(finding, findings, false)} - /> - - correlationData?.onInspect(id, logType)} - disabled={!correlationData} - /> -
-
-
+ const logTypeAndSeverityItem = ( +
+ + {getSeverityLabel(detectionRule.severity)} + + + {getLabelFromLogType(logType)} + +
); - - const attrList = ( - + const openFindingFlyoutButton = ( + + DataStore.findings.openFlyout(finding, findings, false)} + /> + ); - const relatedFindingCard = ( - - {header} + const pinnedFindingHeader = ( + <> + + + {id} + + {openFindingFlyoutButton} + - + + {logTypeAndSeverityItem} + + + {timestamp} + + + + + + ); + + const relatedFindingHeader = ( + <> + Correlation score{' '} @@ -117,6 +123,19 @@ export const FindingCard: React.FC = ({ {correlationData?.score} + +
+ {openFindingFlyoutButton} + correlationData?.onInspect(id, logType)} + /> +
+
+
+ + + {logTypeAndSeverityItem} {timestamp} @@ -124,17 +143,39 @@ export const FindingCard: React.FC = ({ - {attrList} + + ); + + const attrList = ( + + ); + + const relatedFindingCard = ( + + + + + + + {relatedFindingHeader} + {attrList} + + ); - return correlationData ? ( - relatedFindingCard - ) : ( + const pinnedFindingRuleTags = detectionRule.tags + ? detectionRule.tags.map((tag) => {tag.value}) + : null; + + const pinnedFindingCard = ( - {header} - + {pinnedFindingHeader} {attrList} + + {pinnedFindingRuleTags} ); + + return correlationData ? relatedFindingCard : pinnedFindingCard; }; diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx index ea2966f6f..a80caf1ad 100644 --- a/public/pages/Correlations/containers/CorrelationsContainer.tsx +++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx @@ -51,6 +51,7 @@ 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'; interface CorrelationsProps extends RouteComponentProps< @@ -144,6 +145,7 @@ export class Correlations extends React.Component @@ -221,14 +223,32 @@ export class Correlations extends React.Component node.id === findingId)!; + + if (node) { + detectorType = node.saLogType; + } else { + const allFindings = await DataStore.correlations.fetchAllFindings(); + detectorType = allFindings[findingId].logType; + } + + const correlatedFindingsInfo = await DataStore.correlations.getCorrelatedFindings( findingId, detectorType ); - this.setState({ specificFindingInfo: correlations, loadingGraphData: false }); - this.updateGraphDataState(correlations); + 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) { @@ -268,6 +288,7 @@ export class Correlations extends React.Component
- -

Finding

-
spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -460,7 +460,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -551,7 +551,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -569,7 +569,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -732,7 +732,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -750,7 +750,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -957,7 +957,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -975,7 +975,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", diff --git a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap index 40877b4b9..69f77eff5 100644 --- a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap +++ b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap @@ -2884,7 +2884,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -2902,7 +2902,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -2993,7 +2993,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -3011,7 +3011,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -3174,7 +3174,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -3192,7 +3192,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -3399,7 +3399,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -3417,7 +3417,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", diff --git a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap index bc7a33d54..7d04f1169 100644 --- a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap +++ b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap @@ -1684,7 +1684,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -1702,7 +1702,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -1793,7 +1793,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -1811,7 +1811,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -1974,7 +1974,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -1992,7 +1992,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", @@ -2199,7 +2199,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#e7664c", - "text": "black", + "text": "white", }, "name": "High", "priority": "2", @@ -2217,7 +2217,7 @@ exports[` spec renders the component 1`] = ` Object { "color": Object { "background": "#54b399", - "text": "black", + "text": "white", }, "name": "Low", "priority": "4", diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index d0120bd95..5af3f3b78 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -7,7 +7,6 @@ import React, { Component } from 'react'; import { RouteComponentProps, withRouter, match } from 'react-router-dom'; import { ContentPanel } from '../../../../components/ContentPanel'; import { - DurationRange, EuiFlexGroup, EuiFlexItem, EuiPanel, @@ -18,7 +17,6 @@ import { EuiLink, } from '@elastic/eui'; import FindingsTable from '../../components/FindingsTable'; -import FindingsService from '../../../../services/FindingsService'; import { DetectorsService, NotificationsService, @@ -55,17 +53,17 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { DateTimeFilter } from '../../../Overview/models/interfaces'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { DataStore } from '../../../../store/DataStore'; -import { CorrelationFinding, Detector, FeatureChannelList } from '../../../../../types'; +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import { CorrelationFinding, FeatureChannelList } from '../../../../../types'; interface FindingsProps extends RouteComponentProps { detectorService: DetectorsService; - findingsService: FindingsService; correlationService: CorrelationService; notificationsService: NotificationsService; indexPatternsService: IndexPatternsService; opensearchService: OpenSearchService; notifications: NotificationsStart; - match: match; + match: match<{ detectorId: string }>; dateTimeFilter?: DateTimeFilter; setDateTimeFilter?: Function; history: RouteComponentProps['history']; @@ -73,7 +71,6 @@ interface FindingsProps extends RouteComponentProps { interface FindingsState { loading: boolean; - detectors: Detector[]; findings: FindingItemType[]; notificationChannels: FeatureChannelList[]; rules: { [id: string]: RuleSource }; @@ -118,7 +115,6 @@ class Findings extends Component { const timeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); this.state = { loading: true, - detectors: [], findings: [], notificationChannels: [], rules: {}, @@ -154,44 +150,47 @@ class Findings extends Component { getFindings = async () => { this.setState({ loading: true }); - const { findingsService, detectorService, notifications } = this.props; + const { detectorService, notifications } = this.props; try { - const detectorsRes = await detectorService.getDetectors(); - if (detectorsRes.ok) { - const detectors = detectorsRes.response.hits.hits; - const ruleIds = new Set(); - let findings: FindingItemType[] = []; - - const detectorId = this.props.match.params['detectorId']; - for (let detector of detectors) { - if (!detectorId || detector._id === detectorId) { - const findingRes = await findingsService.getFindings({ detectorId: detector._id }); - - if (findingRes.ok) { - const detectorFindings: FindingItemType[] = findingRes.response.findings.map( - (finding) => { - finding.queries.forEach((rule) => ruleIds.add(rule.id)); - return { - ...finding, - detectorName: detector._source.name, - logType: detector._source.detector_type, - detector: detector, - }; - } - ); - findings = findings.concat(detectorFindings); - } else { - errorNotificationToast(notifications, 'retrieve', 'findings', findingRes.error); - } - } - } + const ruleIds = new Set(); + let findings: FindingItemType[] = []; - await this.getRules(Array.from(ruleIds)); + const detectorId = this.props.match.params['detectorId']; - this.setState({ findings, detectors: detectors.map((detector) => detector._source) }); + // Not looking for findings from specific detector + if (!detectorId) { + findings = await DataStore.findings.getAllFindings(); } else { - errorNotificationToast(notifications, 'retrieve', 'findings', detectorsRes.error); + // get findings for a detector + const detectorFindings = await DataStore.findings.getFindingsPerDetector(detectorId); + const getDetectorResponse = await detectorService.getDetectorWithId(detectorId); + + if (getDetectorResponse.ok) { + const detector = getDetectorResponse.response.detector; + findings = detectorFindings.map((finding) => { + return { + ...finding, + detectorName: detector.name, + logType: detector.detector_type, + detector: { + _id: getDetectorResponse.response._id, + _source: detector, + _index: '', + }, + correlations: [], + }; + }); + } else { + errorNotificationToast(notifications, 'retrieve', 'findings', getDetectorResponse.error); + } } + + findings.forEach((finding) => { + finding.queries.forEach((rule) => ruleIds.add(rule.id)); + }); + + await this.getRules(Array.from(ruleIds)); + this.setState({ findings }); } catch (e) { errorNotificationToast(notifications, 'retrieve', 'findings', e); } @@ -318,6 +317,7 @@ class Findings extends Component { finding['ruleName'] = rule.title; finding['ruleSeverity'] = rule.level === 'critical' ? rule.level : finding['ruleSeverity'] || rule.level; + finding['tags'] = rule.tags; } return finding; }); diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 764750a7b..e68a7ca97 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -350,7 +350,6 @@ export default class Main extends Component { {...props} setDateTimeFilter={this.setDateTimeFilter} dateTimeFilter={this.state.dateTimeFilter} - findingsService={services.findingsService} history={props.history} correlationService={services?.correlationsService} opensearchService={services.opensearchService} diff --git a/public/pages/Overview/models/OverviewViewModel.ts b/public/pages/Overview/models/OverviewViewModel.ts index fda596991..7f47f8e94 100644 --- a/public/pages/Overview/models/OverviewViewModel.ts +++ b/public/pages/Overview/models/OverviewViewModel.ts @@ -12,6 +12,7 @@ import { errorNotificationToast, isThreatIntelQuery } from '../../../utils/helpe import dateMath from '@elastic/datemath'; import moment from 'moment'; import { DataStore } from '../../../store/DataStore'; +import { Finding } from '../../../../types'; export interface OverviewViewModel { detectors: DetectorHit[]; @@ -78,35 +79,30 @@ export class OverviewViewModelActor { try { for (let id of detectorIds) { - const findingRes = await this.services?.findingsService.getFindings({ detectorId: id }); - - if (findingRes?.ok) { - const logType = detectorInfo.get(id)?.logType; - const detectorName = detectorInfo.get(id)?.name || ''; - const detectorFindings: any[] = findingRes.response.findings.map((finding) => { - const ruleQueries = finding.queries.filter(({ id }) => !isThreatIntelQuery(id)); - const ids = ruleQueries.map((query) => query.id); - ids.forEach((id) => ruleIds.add(id)); - - const findingTime = new Date(finding.timestamp); - findingTime.setMilliseconds(0); - findingTime.setSeconds(0); - return { - detector: detectorName, - findingName: finding.id, - id: finding.id, - time: findingTime, - logType: logType || '', - ruleId: ruleQueries[0]?.id || finding.queries[0].id, - ruleName: '', - ruleSeverity: '', - isThreatIntelOnlyFinding: finding.detectionType === 'Threat intelligence', - }; - }); - findingItems = findingItems.concat(detectorFindings); - } else { - errorNotificationToast(this.notifications, 'retrieve', 'findings', findingRes?.error); - } + let detectorFindings: Finding[] = await DataStore.findings.getFindingsPerDetector(id); + const logType = detectorInfo.get(id)?.logType; + const detectorName = detectorInfo.get(id)?.name || ''; + const detectorFindingItems: FindingItem[] = detectorFindings.map((finding) => { + const ruleQueries = finding.queries.filter(({ id }) => !isThreatIntelQuery(id)); + const ids = ruleQueries.map((query) => query.id); + ids.forEach((id) => ruleIds.add(id)); + + const findingTime = new Date(finding.timestamp); + findingTime.setMilliseconds(0); + findingTime.setSeconds(0); + return { + detector: detectorName, + findingName: finding.id, + id: finding.id, + time: findingTime, + logType: logType || '', + ruleId: ruleQueries[0]?.id || finding.queries[0].id, + ruleName: '', + ruleSeverity: '', + isThreatIntelOnlyFinding: finding.detectionType === 'Threat intelligence', + }; + }); + findingItems = findingItems.concat(detectorFindingItems); } } catch (e: any) { errorNotificationToast(this.notifications, 'retrieve', 'findings', e); @@ -131,31 +127,24 @@ export class OverviewViewModelActor { } private async updateAlerts() { - const detectorInfo = new Map(); - this.overviewViewModel.detectors.forEach((detector) => { - detectorInfo.set(detector._id, detector._source.detector_type); - }); - const detectorIds = detectorInfo.keys(); let alertItems: AlertItem[] = []; try { - for (let id of detectorIds) { - const alertsRes = await this.services?.alertService.getAlerts({ detector_id: id }); - - if (alertsRes?.ok) { - const logType = detectorInfo.get(id) as string; - const detectorAlertItems: AlertItem[] = alertsRes.response.alerts.map((alert) => ({ - id: alert.id, - severity: alert.severity, - time: alert.last_notification_time, - triggerName: alert.trigger_name, - logType, - acknowledged: !!alert.acknowledged_time, - })); - alertItems = alertItems.concat(detectorAlertItems); - } else { - errorNotificationToast(this.notifications, 'retrieve', 'alerts', alertsRes?.error); - } + for (let detector of this.overviewViewModel.detectors) { + const id = detector._id; + const detectorAlerts = await DataStore.alerts.getAlertsByDetector( + id, + detector._source.name + ); + const detectorAlertItems: AlertItem[] = detectorAlerts.map((alert) => ({ + id: alert.id, + severity: alert.severity, + time: alert.last_notification_time, + triggerName: alert.trigger_name, + logType: detector._source.detector_type, + acknowledged: !!alert.acknowledged_time, + })); + alertItems = alertItems.concat(detectorAlertItems); } } catch (e: any) { errorNotificationToast(this.notifications, 'retrieve', 'alerts', e); diff --git a/public/pages/Overview/models/interfaces.ts b/public/pages/Overview/models/interfaces.ts index a54c1f0ab..690c7c43c 100644 --- a/public/pages/Overview/models/interfaces.ts +++ b/public/pages/Overview/models/interfaces.ts @@ -27,7 +27,7 @@ export interface OverviewState { export interface FindingItem { id: string; - time: number; + time: Date; findingName: string; detector: string; logType: string; diff --git a/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx index 2ef66cc0c..d47b5a66d 100644 --- a/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx +++ b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx @@ -6,7 +6,7 @@ import { EuiCodeBlock } from '@elastic/eui'; import React from 'react'; import { mapRuleToYamlObject, mapYamlObjectToYamlString } from '../../utils/mappers'; -import { Rule } from '../../../../../models/interfaces'; +import { Rule } from '../../../../../types'; export interface RuleContentYamlViewerProps { rule: Rule; diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx index 2a7106d0d..b8080c407 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx @@ -8,12 +8,12 @@ import { RouteComponentProps } from 'react-router-dom'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { ROUTES } from '../../../../utils/constants'; import { EuiSpacer } from '@elastic/eui'; -import { Rule } from '../../../../../models/interfaces'; import { RuleEditorFormModel, ruleEditorStateDefaultValue } from './RuleEditorFormModel'; import { mapFormToRule, mapRuleToForm } from './mappers'; import { RuleEditorForm } from './RuleEditorForm'; import { validateRule } from '../../utils/helpers'; import { DataStore } from '../../../../store/DataStore'; +import { Rule } from '../../../../../types'; export interface RuleEditorProps { title: string | JSX.Element; diff --git a/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/YamlRuleEditorComponent.tsx b/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/YamlRuleEditorComponent.tsx index e8917122f..93c6c4f02 100644 --- a/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/YamlRuleEditorComponent.tsx +++ b/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/YamlRuleEditorComponent.tsx @@ -7,12 +7,12 @@ import React, { useState } from 'react'; import { load } from 'js-yaml'; import { EuiFormRow, EuiCodeEditor, EuiLink, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; import FormFieldHeader from '../../../../../../components/FormFieldHeader'; -import { Rule } from '../../../../../../../models/interfaces'; import { mapRuleToYamlObject, mapYamlObjectToYamlString, mapYamlObjectToRule, } from '../../../../utils/mappers'; +import { Rule } from '../../../../../../../types'; export interface YamlRuleEditorComponentProps { rule: Rule; diff --git a/public/pages/Rules/components/RuleEditor/mappers.ts b/public/pages/Rules/components/RuleEditor/mappers.ts index e89da903d..94480cfcc 100644 --- a/public/pages/Rules/components/RuleEditor/mappers.ts +++ b/public/pages/Rules/components/RuleEditor/mappers.ts @@ -2,7 +2,8 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { Rule } from '../../../../../models/interfaces'; + +import { Rule } from '../../../../../types'; import { getLogTypeFromLogSource } from '../../utils/helpers'; import { RuleEditorFormModel, ruleEditorStateDefaultValue } from './RuleEditorFormModel'; diff --git a/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx b/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx index c1d90474d..8cc55776a 100644 --- a/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx +++ b/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx @@ -6,14 +6,11 @@ import { BrowserServices } from '../../../../models/interfaces'; import { RuleEditorContainer } from '../../components/RuleEditor/RuleEditorContainer'; import React, { useContext } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { RouteComponentProps } from 'react-router-dom'; -import { BREADCRUMBS, ROUTES } from '../../../../utils/constants'; -import { Rule } from '../../../../../models/interfaces'; +import { BREADCRUMBS } from '../../../../utils/constants'; import { CoreServicesContext } from '../../../../components/core_services'; -import { setBreadCrumb, validateRule } from '../../utils/helpers'; +import { setBreadCrumb } from '../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { DataStore } from '../../../../store/DataStore'; import { RuleItemInfoBase } from '../../../../../types'; export interface DuplicateRuleProps @@ -30,31 +27,6 @@ export const DuplicateRule: React.FC = ({ }) => { const context = useContext(CoreServicesContext); setBreadCrumb(BREADCRUMBS.RULES_DUPLICATE, context?.chrome.setBreadcrumbs); - const footerActions: React.FC<{ rule: Rule }> = ({ rule }) => { - const onCreate = async () => { - if (!validateRule(rule, notifications!, 'create')) { - return; - } - const response = await DataStore.rules.createRule(rule); - - if (response) { - history.replace(ROUTES.RULES); - } - }; - - return ( - - - history.replace(ROUTES.RULES)}>Cancel - - - - Create - - - - ); - }; return ( { diff --git a/public/pages/Rules/containers/ImportRule/ImportRule.tsx b/public/pages/Rules/containers/ImportRule/ImportRule.tsx index d350a688c..575390f0f 100644 --- a/public/pages/Rules/containers/ImportRule/ImportRule.tsx +++ b/public/pages/Rules/containers/ImportRule/ImportRule.tsx @@ -8,7 +8,6 @@ import { RuleEditorContainer } from '../../components/RuleEditor/RuleEditorConta import React, { useCallback, useContext, useEffect, useState } from 'react'; import { EuiButton, EuiFilePicker, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { BREADCRUMBS, ROUTES } from '../../../../utils/constants'; -import { Rule } from '../../../../../models/interfaces'; import { RouteComponentProps } from 'react-router-dom'; import { dump, load } from 'js-yaml'; import { ContentPanel } from '../../../../components/ContentPanel'; @@ -16,6 +15,7 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { CoreServicesContext } from '../../../../components/core_services'; import { setBreadCrumb } from '../../utils/helpers'; import { yamlMediaTypes } from '../../utils/constants'; +import { Rule } from '../../../../../types'; import { DEFAULT_RULE_UUID } from '../../../../../common/constants'; export interface ImportRuleProps { diff --git a/public/pages/Rules/utils/constants.ts b/public/pages/Rules/utils/constants.ts index 389e863ba..75cd96ae1 100644 --- a/public/pages/Rules/utils/constants.ts +++ b/public/pages/Rules/utils/constants.ts @@ -31,7 +31,7 @@ export const ruleSeverity: { name: 'High', value: 'high', priority: '2', - color: { background: paletteColors[3], text: 'black' }, + color: { background: paletteColors[3], text: 'white' }, }, { name: 'Medium', @@ -43,7 +43,7 @@ export const ruleSeverity: { name: 'Low', value: 'low', priority: '4', - color: { background: paletteColors[1], text: 'black' }, + color: { background: paletteColors[1], text: 'white' }, }, { name: 'Informational', diff --git a/public/pages/Rules/utils/helpers.tsx b/public/pages/Rules/utils/helpers.tsx index 95a6a0a1f..776888624 100644 --- a/public/pages/Rules/utils/helpers.tsx +++ b/public/pages/Rules/utils/helpers.tsx @@ -12,7 +12,6 @@ import { } from '../../../utils/helpers'; import { ruleSeverity, ruleSource, ruleTypes, sigmaRuleLogSourceFields } from './constants'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; -import { Rule } from '../../../../models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { AUTHOR_REGEX, @@ -22,7 +21,7 @@ import { } from '../../../utils/validation'; import { dump, load } from 'js-yaml'; import { BREADCRUMBS } from '../../../utils/constants'; -import { RuleItemInfoBase, RulesTableColumnFields } from '../../../../types'; +import { RuleItemInfoBase, RulesTableColumnFields, Rule } from '../../../../types'; import { getSeverityColor, getSeverityLabel } from '../../Correlations/utils/constants'; export interface RuleTableItem { diff --git a/public/pages/Rules/utils/mappers.ts b/public/pages/Rules/utils/mappers.ts index 63daed700..abd4e057f 100644 --- a/public/pages/Rules/utils/mappers.ts +++ b/public/pages/Rules/utils/mappers.ts @@ -1,5 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { dump, load } from 'js-yaml'; -import { Rule } from '../../../../models/interfaces'; +import { Rule } from '../../../../types'; export const mapYamlObjectToYamlString = (rule: Rule): string => { try { diff --git a/public/services/AlertsService.ts b/public/services/AlertsService.ts index 79da15640..27f50cd71 100644 --- a/public/services/AlertsService.ts +++ b/public/services/AlertsService.ts @@ -22,10 +22,11 @@ export default class AlertsService { getAlerts = async ( detectorParams: GetAlertsParams ): Promise> => { - const { detectorType, detector_id } = detectorParams; + const { detectorType, detector_id, size, sortOrder, startIndex } = detectorParams; let query: GetAlertsParams | {} = { - sortOrder: 'desc', - size: 10000, + sortOrder: sortOrder || 'desc', + size: size || 10000, + startIndex: startIndex || 0, }; if (detector_id) { diff --git a/public/services/DetectorService.ts b/public/services/DetectorService.ts index 5b2d459ce..e6d79e9d6 100644 --- a/public/services/DetectorService.ts +++ b/public/services/DetectorService.ts @@ -12,7 +12,7 @@ import { UpdateDetectorResponse, } from '../../server/models/interfaces'; import { API } from '../../server/utils/constants'; -import { Detector, IDetectorService } from '../../types'; +import { Detector, GetDetectorResponse, IDetectorService } from '../../types'; export default class DetectorsService implements IDetectorService { constructor(private httpClient: HttpSetup) {} @@ -39,6 +39,13 @@ export default class DetectorsService implements IDetectorService { return res; }; + getDetectorWithId = async (id: string): Promise> => { + const url = `..${API.DETECTORS_BASE}/${id}`; + const res = (await this.httpClient.get(url)) as ServerResponse; + + return res; + }; + updateDetector = async ( detectorId: string, detector: Detector diff --git a/public/services/FindingsService.ts b/public/services/FindingsService.ts index 24d502b6d..1bbb2a1ed 100644 --- a/public/services/FindingsService.ts +++ b/public/services/FindingsService.ts @@ -5,8 +5,8 @@ import { HttpSetup } from 'opensearch-dashboards/public'; import { ServerResponse } from '../../server/models/types'; -import { GetFindingsParams, GetFindingsResponse } from '../../server/models/interfaces'; import { API } from '../../server/utils/constants'; +import { GetFindingsParams, GetFindingsResponse } from '../../types'; export default class FindingsService { httpClient: HttpSetup; @@ -18,24 +18,16 @@ export default class FindingsService { getFindings = async ( detectorParams: GetFindingsParams ): Promise> => { - const { detectorType, detectorId } = detectorParams; - let query: GetFindingsParams | {} = { + const findingIds = detectorParams.findingIds + ? JSON.stringify(detectorParams.findingIds) + : undefined; + const query: GetFindingsParams | {} = { sortOrder: 'desc', size: 10000, + ...detectorParams, + findingIds, }; - if (detectorId) { - query = { - ...query, - detectorId, - }; - } else if (detectorType) { - query = { - ...query, - detectorType, - }; - } - return await this.httpClient.get(`..${API.GET_FINDINGS}`, { query }); }; } diff --git a/public/services/RuleService.ts b/public/services/RuleService.ts index 326102892..9f9334b87 100644 --- a/public/services/RuleService.ts +++ b/public/services/RuleService.ts @@ -12,7 +12,7 @@ import { UpdateRuleResponse, } from '../../server/models/interfaces'; import { API } from '../../server/utils/constants'; -import { Rule } from '../../models/interfaces'; +import { Rule } from '../../types'; export default class RuleService { httpClient: HttpSetup; diff --git a/public/store/AlertsStore.ts b/public/store/AlertsStore.ts new file mode 100644 index 000000000..3a6ca7539 --- /dev/null +++ b/public/store/AlertsStore.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { AlertsService } from '../services'; +import { errorNotificationToast } from '../utils/helpers'; +import { GetAlertsResponse, ServerResponse } from '../../types'; + +export class AlertsStore { + constructor( + private readonly service: AlertsService, + private readonly notifications: NotificationsStart + ) {} + + public async getAlertsByDetector(detectorId: string, detectorName: string) { + let allAlerts: any[] = []; + const alertsSize = 10000; + + const firstGetAlertsRes = await this.service.getAlerts({ + detector_id: detectorId, + startIndex: 0, + size: alertsSize, + }); + + if (firstGetAlertsRes.ok) { + allAlerts = [...firstGetAlertsRes.response.alerts]; + let remainingAlerts = firstGetAlertsRes.response.total_alerts - alertsSize; + let startIndex = alertsSize + 1; + const getAlertsPromises: Promise>[] = []; + + while (remainingAlerts > 0) { + getAlertsPromises.push( + this.service.getAlerts({ detector_id: detectorId, startIndex, size: alertsSize }) + ); + remainingAlerts -= alertsSize; + startIndex += alertsSize; + } + + const alertsPromisesRes = await Promise.allSettled(getAlertsPromises); + + alertsPromisesRes.forEach((response) => { + if (response.status === 'fulfilled' && response.value.ok) { + allAlerts = allAlerts.concat(response.value.response.alerts); + } + }); + + allAlerts = allAlerts.map((alert) => { + if (!alert.detector_id) { + alert.detector_id = detectorId; + } + + return { + ...alert, + detectorName: detectorName, + }; + }); + } else { + errorNotificationToast(this.notifications, 'retrieve', 'alerts', firstGetAlertsRes.error); + } + + return allAlerts; + } +} diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 0bd8ebd4e..554bf53dc 100644 --- a/public/store/CorrelationsStore.ts +++ b/public/store/CorrelationsStore.ts @@ -15,6 +15,7 @@ import { DetectorsService, FindingsService, CorrelationService } from '../servic import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../utils/helpers'; import { DEFAULT_EMPTY_DATA } from '../utils/constants'; +import { DataStore } from './DataStore'; export interface ICorrelationsCache { [key: string]: CorrelationRule[]; @@ -253,31 +254,27 @@ export class CorrelationsStore implements ICorrelationsStore { const detectors = detectorsRes.response.hits.hits; let findings: { [id: string]: CorrelationFinding } = {}; for (let detector of detectors) { - const findingRes = await this.findingsService.getFindings({ detectorId: detector._id }); - - if (findingRes.ok) { - findingRes.response.findings.forEach((f) => { - 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 - ? { - name: rule._source.title, - severity: rule._source.level, - } - : { name: DEFAULT_EMPTY_DATA, severity: DEFAULT_EMPTY_DATA }, - }; - }); + const detectorFindings = await DataStore.findings.getFindingsPerDetector(detector._id); + detectorFindings.forEach((f) => { + 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 + ? { + name: rule._source.title, + severity: rule._source.level, + tags: rule._source.tags, + } + : { name: DEFAULT_EMPTY_DATA, severity: DEFAULT_EMPTY_DATA }, + }; + }); - this.allFindings = findings; - } else { - this.allFindings = {}; - } + this.allFindings = findings; } } @@ -303,6 +300,7 @@ export class CorrelationsStore implements ICorrelationsStore { correlatedFindings.push({ ...allFindings[f.finding], correlationScore: f.score < 0.01 ? '0.01' : f.score.toFixed(2), + rules: f.rules, }); } }); diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts index 6c179621b..b8e2b741c 100644 --- a/public/store/DataStore.ts +++ b/public/store/DataStore.ts @@ -10,6 +10,7 @@ import { DetectorsStore } from './DetectorsStore'; import { CorrelationsStore } from './CorrelationsStore'; import { FindingsStore } from './FindingsStore'; import { LogTypeStore } from './LogTypeStore'; +import { AlertsStore } from './AlertsStore'; export class DataStore { public static rules: RulesStore; @@ -17,6 +18,7 @@ export class DataStore { public static correlations: CorrelationsStore; public static findings: FindingsStore; public static logTypes: LogTypeStore; + public static alerts: AlertsStore; public static init = (services: BrowserServices, notifications: NotificationsStart) => { const rulesStore = new RulesStore(services.ruleService, notifications); @@ -43,5 +45,7 @@ export class DataStore { ); DataStore.logTypes = new LogTypeStore(services.logTypeService, notifications); + + DataStore.alerts = new AlertsStore(services.alertService, notifications); }; } diff --git a/public/store/FindingsStore.ts b/public/store/FindingsStore.ts index 6fd8532f1..284a7b229 100644 --- a/public/store/FindingsStore.ts +++ b/public/store/FindingsStore.ts @@ -10,6 +10,7 @@ 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'; +import { Finding, GetFindingsResponse, ServerResponse } from '../../types'; export interface IFindingsStore { readonly service: FindingsService; @@ -18,7 +19,11 @@ export interface IFindingsStore { readonly notifications: NotificationsStart; - getFindingsPerDetector: (detectorId: string) => Promise; + getFinding: (findingId: string) => Promise; + + getFindingsByIds: (findingIds: string[]) => Promise; + + getFindingsPerDetector: (detectorId: string) => Promise; getAllFindings: () => Promise; @@ -86,13 +91,58 @@ export class FindingsStore implements IFindingsStore { this.notifications = notifications; } - public getFindingsPerDetector = async (detectorId: string): Promise => { - let allFindings: FindingItemType[] = []; - const findingRes = await this.service.getFindings({ detectorId }); - if (findingRes.ok) { - allFindings = findingRes.response.findings as FindingItemType[]; + public getFinding = async (findingId: string): Promise => { + const getFindingRes = await this.service.getFindings({ findingIds: [findingId] }); + + if (getFindingRes.ok) { + return getFindingRes.response.findings[0] || undefined; + } + }; + + public getFindingsByIds = async (findingIds: string[]): Promise => { + const getFindingRes = await this.service.getFindings({ findingIds }); + + if (getFindingRes.ok) { + return getFindingRes.response.findings || []; + } else { + errorNotificationToast(this.notifications, 'retrieve', 'findings', getFindingRes.error); + } + + return []; + }; + + public getFindingsPerDetector = async (detectorId: string): Promise => { + let allFindings: Finding[] = []; + const findingsSize = 10000; + const firstGetFindingsRes = await this.service.getFindings({ + detector_id: detectorId, + startIndex: 0, + size: findingsSize, + }); + + if (firstGetFindingsRes.ok) { + allFindings = [...firstGetFindingsRes.response.findings]; + let remainingFindings = firstGetFindingsRes.response.total_findings - findingsSize; + let startIndex = findingsSize + 1; + const getFindingsPromises: Promise>[] = []; + + while (remainingFindings > 0) { + getFindingsPromises.push( + this.service.getFindings({ detector_id: detectorId, startIndex, size: findingsSize }) + ); + remainingFindings -= findingsSize; + startIndex += findingsSize; + } + + const findingsPromisesRes = await Promise.allSettled(getFindingsPromises); + + findingsPromisesRes.forEach((response) => { + if (response.status === 'fulfilled' && response.value.ok) { + allFindings = allFindings.concat(response.value.response.findings); + } + }); } else { - errorNotificationToast(this.notifications, 'retrieve', 'findings', findingRes.error); + errorNotificationToast(this.notifications, 'retrieve', 'findings', firstGetFindingsRes.error); } return allFindings; @@ -112,6 +162,7 @@ export class FindingsStore implements IFindingsStore { detectorName: detector._source.name, logType: detector._source.detector_type, detector: detector, + correlations: [], }; }); allFindings = allFindings.concat(findingsPerDetector); diff --git a/public/store/RulesStore.ts b/public/store/RulesStore.ts index 5a9e285e9..368cfd9fd 100644 --- a/public/store/RulesStore.ts +++ b/public/store/RulesStore.ts @@ -5,8 +5,7 @@ import { RuleService } from '../services'; import { load, safeDump } from 'js-yaml'; -import { RuleItemInfoBase, IRulesStore, IRulesCache } from '../../types'; -import { Rule } from '../../models/interfaces'; +import { RuleItemInfoBase, IRulesStore, IRulesCache, Rule } from '../../types'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../utils/helpers'; import { ruleTypes } from '../pages/Rules/utils/constants'; diff --git a/server/clusters/addAlertsMethods.ts b/server/clusters/addAlertsMethods.ts index eb02f3ade..7aaf94587 100644 --- a/server/clusters/addAlertsMethods.ts +++ b/server/clusters/addAlertsMethods.ts @@ -8,19 +8,19 @@ import { METHOD_NAMES, API, BASE_API_PATH } from '../utils/constants'; export function addAlertsMethods(securityAnalytics: any, createAction: any): void { securityAnalytics[METHOD_NAMES.GET_ALERTS] = createAction({ url: { - fmt: `${API.GET_ALERTS}?detector_id=<%=detector_id%>&sortOrder=<%=sortOrder%>&size=<%=size%>`, - req: { + fmt: `${API.GET_ALERTS}`, + params: { detector_id: { type: 'string', - required: false, }, sortOrder: { type: 'string', - required: false, }, size: { type: 'number', - required: false, + }, + startIndex: { + type: 'number', }, }, }, diff --git a/server/clusters/addFindingsMethods.ts b/server/clusters/addFindingsMethods.ts index 6b8f3b7a8..f930d56e2 100644 --- a/server/clusters/addFindingsMethods.ts +++ b/server/clusters/addFindingsMethods.ts @@ -8,19 +8,34 @@ import { METHOD_NAMES, API } from '../utils/constants'; export function addFindingsMethods(securityAnalytics: any, createAction: any): void { securityAnalytics[METHOD_NAMES.GET_FINDINGS] = createAction({ url: { - fmt: `${API.GET_FINDINGS}?detector_id=<%=detectorId%>&sortOrder=<%=sortOrder%>&size=<%=size%>`, - req: { - detectorId: { + fmt: `${API.GET_FINDINGS}`, + params: { + detector_id: { type: 'string', - required: false, }, sortOrder: { type: 'string', - required: false, }, size: { type: 'number', - required: false, + }, + detectorType: { + type: 'string', + }, + startIndex: { + type: 'number', + }, + detectionType: { + type: 'string', + }, + severity: { + type: 'string', + }, + searchString: { + type: 'string', + }, + findingIds: { + type: 'list', }, }, }, diff --git a/server/models/interfaces/Alerts.ts b/server/models/interfaces/Alerts.ts index 294cceb87..40204a47b 100644 --- a/server/models/interfaces/Alerts.ts +++ b/server/models/interfaces/Alerts.ts @@ -6,6 +6,7 @@ export type GetAlertsParams = { sortOrder?: string; size?: number; + startIndex?: number; } & ( | { detector_id: string; @@ -19,6 +20,8 @@ export type GetAlertsParams = { export interface GetAlertsResponse { alerts: AlertResponse[]; + total_alerts: number; + detectorType: string; } export interface AlertItem { diff --git a/server/models/interfaces/Findings.ts b/server/models/interfaces/Findings.ts index 1532c09dc..e474b2a9c 100644 --- a/server/models/interfaces/Findings.ts +++ b/server/models/interfaces/Findings.ts @@ -5,20 +5,6 @@ import { Finding } from '../../../public/pages/Findings/models/interfaces'; -export type GetFindingsParams = { - sortOrder?: string; - size?: number; -} & ( - | { - detectorId: string; - detectorType?: string; - } - | { - detectorType: string; - detectorId?: string; - } -); - export interface GetFindingsResponse { total_findings: number; findings: Finding[]; diff --git a/server/models/interfaces/Rules.ts b/server/models/interfaces/Rules.ts index 89afa6bae..5e568cbdc 100644 --- a/server/models/interfaces/Rules.ts +++ b/server/models/interfaces/Rules.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Rule } from '../../../models/interfaces'; +import { Rule } from '../../../types'; export interface CreateRuleParams { body: string; diff --git a/server/routes/AlertRoutes.ts b/server/routes/AlertRoutes.ts index c8001f50d..83620dc81 100644 --- a/server/routes/AlertRoutes.ts +++ b/server/routes/AlertRoutes.ts @@ -20,6 +20,7 @@ export function setupAlertsRoutes(services: NodeServices, router: IRouter) { detector_id: schema.maybe(schema.string()), sortOrder: schema.maybe(schema.string()), size: schema.maybe(schema.number()), + startIndex: schema.maybe(schema.number()), }), }, }, diff --git a/server/routes/FindingsRoutes.ts b/server/routes/FindingsRoutes.ts index 442531d11..89a1c4da9 100644 --- a/server/routes/FindingsRoutes.ts +++ b/server/routes/FindingsRoutes.ts @@ -17,9 +17,14 @@ export function setupFindingsRoutes(services: NodeServices, router: IRouter) { validate: { query: schema.object({ detectorType: schema.maybe(schema.string()), - detectorId: schema.maybe(schema.string()), + detector_id: schema.maybe(schema.string()), sortOrder: schema.maybe(schema.string()), size: schema.maybe(schema.number()), + startIndex: schema.maybe(schema.number()), + detectionType: schema.maybe(schema.string()), + severity: schema.maybe(schema.string()), + searchString: schema.maybe(schema.string()), + findingIds: schema.maybe(schema.arrayOf(schema.string())), }), }, }, diff --git a/server/services/FindingsService.ts b/server/services/FindingsService.ts index ce3cd97a0..c9ffbb2de 100644 --- a/server/services/FindingsService.ts +++ b/server/services/FindingsService.ts @@ -11,9 +11,9 @@ import { ResponseError, RequestHandlerContext, } from 'opensearch-dashboards/server'; -import { GetFindingsParams, GetFindingsResponse } from '../models/interfaces'; import { ServerResponse } from '../models/types'; import { CLIENT_DETECTOR_METHODS } from '../utils/constants'; +import { GetFindingsParams, GetFindingsResponse } from '../../types'; export default class FindingsService { osDriver: ILegacyCustomClusterClient; @@ -33,27 +33,7 @@ export default class FindingsService { IOpenSearchDashboardsResponse | ResponseError> > => { try { - const { detectorType, detectorId, sortOrder, size } = request.query; - const defaultParams = { - sortOrder, - size, - }; - let params: GetFindingsParams; - - if (detectorId) { - params = { - ...defaultParams, - detectorId, - }; - } else if (detectorType) { - params = { - ...defaultParams, - detectorType, - }; - } else { - throw Error(`Invalid request params: detectorId or detectorType must be specified`); - } - + const params: GetFindingsParams = { ...request.query }; const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); const getFindingsResponse: GetFindingsResponse = await callWithRequest( CLIENT_DETECTOR_METHODS.GET_FINDINGS, diff --git a/server/services/RuleService.ts b/server/services/RuleService.ts index e80780739..5d0b5cdef 100644 --- a/server/services/RuleService.ts +++ b/server/services/RuleService.ts @@ -22,10 +22,10 @@ import { UpdateRuleResponse, } from '../models/interfaces'; import { CLIENT_RULE_METHODS } from '../utils/constants'; -import { Rule } from '../../models/interfaces'; import { ServerResponse } from '../models/types'; import { load, safeDump } from 'js-yaml'; import moment from 'moment'; +import { Rule } from '../../types'; import { DEFAULT_RULE_UUID } from '../../common/constants'; export default class RulesService { diff --git a/test/mocks/Alerts/components/AlertFlyout/AlertFlyout.mock.ts b/test/mocks/Alerts/components/AlertFlyout/AlertFlyout.mock.ts index 988da9a51..5a258647a 100644 --- a/test/mocks/Alerts/components/AlertFlyout/AlertFlyout.mock.ts +++ b/test/mocks/Alerts/components/AlertFlyout/AlertFlyout.mock.ts @@ -8,7 +8,7 @@ import services from '../../../services'; import alertItemMock from '../../AlertItem.mock'; import detectorMock from '../../../Detectors/containers/Detectors/Detector.mock'; import { AlertFlyout } from '../../../../../public/pages/Alerts/components/AlertFlyout/AlertFlyout'; -const { openSearchService, findingsService, ruleService } = services; +const { opensearchService, findingsService, ruleService } = services; export default ({ alertItem: alertItemMock, @@ -16,7 +16,7 @@ export default ({ findingsService, ruleService, notifications: notificationsStartMock, - opensearchService: openSearchService, + opensearchService, onClose: jest.fn(), onAcknowledge: jest.fn(), } as unknown) as AlertFlyout; diff --git a/types/Correlations.ts b/types/Correlations.ts index 9b93a44a5..ca4ae61d2 100644 --- a/types/Correlations.ts +++ b/types/Correlations.ts @@ -7,6 +7,7 @@ import { Edge, GraphEvents, Node } from 'react-graph-vis'; import { FilterItem } from '../public/pages/Correlations/components/FilterGroup'; import { Query } from '@opensearch-project/oui/src/eui_components/search_bar/search_bar'; import { RuleInfo } from './Rule'; +import { DetectorHit } from './Detector'; export enum CorrelationsLevel { AllFindings = 'AllFindings', @@ -17,7 +18,7 @@ export type CorrelationRuleAction = 'Create' | 'Edit' | 'Readonly'; export interface CorrelationGraphData { graph: { - nodes: (Node & { chosen?: boolean })[]; + nodes: (Node & { chosen?: boolean; saLogType: string })[]; edges: Edge[]; }; events: GraphEvents; @@ -29,9 +30,10 @@ export type CorrelationFinding = { correlationRule?: CorrelationRule; logType: string; timestamp: string; - detectionRule: { name: string; severity: string }; + detectionRule: { name: string; severity: string; tags?: { value: string }[] }; detectorName?: string; rules?: string[]; + detector?: DetectorHit; }; export interface CorrelationRuleQuery { @@ -95,6 +97,7 @@ export interface CorrelationFindingHit { finding: string; detector_type: string; score: number; + rules: string[]; } export interface GetCorrelationFindingsResponse { findings: CorrelationFindingHit[]; diff --git a/types/Detector.ts b/types/Detector.ts index d4f521dcf..b51862792 100644 --- a/types/Detector.ts +++ b/types/Detector.ts @@ -89,7 +89,11 @@ export interface GetDetectorParams { detectorId: string; } -export interface GetDetectorResponse {} +export interface GetDetectorResponse { + _id: string; + _version: number; + detector: DetectorResponse; +} export interface SearchDetectorsParams { body: { diff --git a/types/Finding.ts b/types/Finding.ts index 3ca6815cf..dd9d9331b 100644 --- a/types/Finding.ts +++ b/types/Finding.ts @@ -11,6 +11,7 @@ export interface Finding { queries: Query[]; related_doc_ids: string[]; timestamp: number; + detectionType: string; } export interface Query { @@ -33,15 +34,19 @@ export interface FindingDocument { /** * API interfaces */ -export type GetFindingsParams = - | { - detectorId: string; - detectorType?: string; - } - | { - detectorType: string; - detectorId?: string; - }; +export type GetFindingsParams = { + sortOrder?: string; + startIndex?: number; + size?: number; + detector_id?: string; + detectorType?: string; + detectionType?: string; + severity?: string; + searchString?: string; + startTime?: number; + endTime?: number; + findingIds?: string[]; +}; export interface GetFindingsResponse { total_findings: number; diff --git a/types/Rule.ts b/types/Rule.ts index 01e82ebfc..314ef9374 100644 --- a/types/Rule.ts +++ b/types/Rule.ts @@ -9,14 +9,18 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; export interface Rule { id: string; category: string; - log_source: string; + log_source: { + product?: string; + category?: string; + service?: string; + }; title: string; description: string; - tags: { value: string }[]; - false_positives: { value: string }[]; + tags: Array<{ value: string }>; + false_positives: Array<{ value: string }>; level: string; status: string; - references: { value: string }[]; + references: Array<{ value: string }>; author: string; detection: string; }