diff --git a/common/constants.ts b/common/constants.ts new file mode 100644 index 000000000..059fb806d --- /dev/null +++ b/common/constants.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const DEFAULT_RULE_UUID = '25b9c01c-350d-4b95-bed1-836d04a4f324'; diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 6a08e3852..213b7992d 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -12,6 +12,7 @@ import _ from 'lodash'; import { getMappingFields } from '../../public/pages/Detectors/utils/helpers'; import { getLogTypeLabel } from '../../public/pages/LogTypes/utils/helpers'; import { setupIntercept } from '../support/helpers'; +import { descriptionErrorString } from '../../public/utils/validation'; const cypressIndexDns = 'cypress-index-dns'; const cypressIndexWindows = 'cypress-index-windows'; @@ -279,9 +280,7 @@ describe('Detectors', () => { getDescriptionField() .parents('.euiFormRow__fieldWrapper') .find('.euiFormErrorText') - .contains( - 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.' - ); + .contains(descriptionErrorString); getDescriptionField() .type('{selectall}') diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index a3935a8bc..a05bc4f0e 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -6,13 +6,14 @@ import { OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; import { getLogTypeLabel } from '../../public/pages/LogTypes/utils/helpers'; import { setupIntercept } from '../support/helpers'; +import { ruleDescriptionErrorString } from '../../public/utils/validation'; const uniqueId = Cypress._.random(0, 1e6); const SAMPLE_RULE = { name: `Cypress test rule ${uniqueId}`, logType: 'windows', description: 'This is a rule used to test the rule creation workflow.', - detectionLine: ['condition: Selection_1', 'Selection_1:', 'FieldKey|contains:', '- FieldValue'], + detectionLine: ['condition: Selection_1', 'Selection_1:', 'FieldKey|all:', '- FieldValue'], severity: 'Critical', tags: ['attack.persistence', 'attack.privilege_escalation', 'attack.t1543.003'], references: 'https://nohello.com', @@ -231,18 +232,15 @@ describe('Rules', () => { }); it('...should validate rule description field', () => { - const longDescriptionText = - 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.'; + const invalidDescriptionText = 'This is a invalid % description.'; getDescriptionField().should('be.empty'); - getDescriptionField().type(longDescriptionText).focus().blur(); + getDescriptionField().type(invalidDescriptionText).focus().blur(); getDescriptionField() .parents('.euiFormRow__fieldWrapper') .find('.euiFormErrorText') - .contains( - 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.' - ); + .contains(ruleDescriptionErrorString); getDescriptionField() .type('{selectall}') @@ -268,10 +266,8 @@ describe('Rules', () => { getAuthorField().should('be.empty'); getAuthorField().focus().blur(); getAuthorField().containsError('Author name is required'); - getAuthorField().type('text').focus().blur(); - getAuthorField().containsError('Invalid author.'); - getAuthorField().type('{selectall}').type('{backspace}').type('tex&').focus().blur(); + getAuthorField().type('{selectall}').type('{backspace}').type('tex%').focus().blur(); getAuthorField().containsError('Invalid author.'); getAuthorField() 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.test.tsx b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.test.tsx index 1bece3508..175057ba1 100644 --- a/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.test.tsx +++ b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.test.tsx @@ -6,13 +6,14 @@ import React from 'react'; import { render } from '@testing-library/react'; import { RuleContentYamlViewer } from './RuleContentYamlViewer'; +import { DEFAULT_RULE_UUID } from '../../../../../common/constants'; describe(' spec', () => { it('renders the component', () => { const { container } = render( = ({ title, rule, mode, + validateOnMount, }) => { const initialRuleValue = rule ? { ...mapRuleToForm(rule), id: ruleEditorStateDefaultValue.id } @@ -73,6 +75,7 @@ export const RuleEditorContainer: React.FC = ({ mode={mode} notifications={notifications} initialValue={initialRuleValue} + validateOnMount={validateOnMount} cancel={goToRulesList} submit={onSubmit} /> diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx index 863456cf9..a835d0c82 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx @@ -23,8 +23,14 @@ import { } from '@elastic/eui'; import { ContentPanel } from '../../../../components/ContentPanel'; import { FieldTextArray } from './components/FieldTextArray'; -import { ruleStatus } from '../../utils/constants'; -import { AUTHOR_REGEX, validateDescription, validateName } from '../../../../utils/validation'; +import { ruleSeverity, ruleStatus, ruleTypes } from '../../utils/constants'; +import { + AUTHOR_REGEX, + RULE_DESCRIPTION_REGEX, + ruleDescriptionErrorString, + validateDescription, + validateName, +} from '../../../../utils/validation'; import { RuleEditorFormModel } from './RuleEditorFormModel'; import { FormSubmissionErrorToastNotification } from './FormSubmitionErrorToastNotification'; import { YamlRuleEditorComponent } from './components/YamlRuleEditorComponent/YamlRuleEditorComponent'; @@ -33,10 +39,12 @@ import { DetectionVisualEditor } from './DetectionVisualEditor'; import { useCallback } from 'react'; import { getLogTypeOptions } from '../../../../utils/helpers'; import { getLogTypeLabel } from '../../../LogTypes/utils/helpers'; +import { getSeverityLabel } from '../../../Correlations/utils/constants'; export interface VisualRuleEditorProps { initialValue: RuleEditorFormModel; notifications?: NotificationsStart; + validateOnMount?: boolean; submit: (values: RuleEditorFormModel) => void; cancel: () => void; mode: 'create' | 'edit'; @@ -63,6 +71,7 @@ export const RuleEditorForm: React.FC = ({ cancel, mode, title, + validateOnMount, }) => { const [selectedEditorType, setSelectedEditorType] = useState('visual'); const [isDetectionInvalid, setIsDetectionInvalid] = useState(false); @@ -94,6 +103,7 @@ export const RuleEditorForm: React.FC = ({ return ( { const errors: FormikErrors = {}; @@ -105,13 +115,17 @@ export const RuleEditorForm: React.FC = ({ } } - if (values.description && !validateDescription(values.description)) { - errors.description = - 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.'; + if ( + values.description && + !validateDescription(values.description, RULE_DESCRIPTION_REGEX) + ) { + errors.description = ruleDescriptionErrorString; } if (!values.logType) { errors.logType = 'Log type is required'; + } else if (!ruleTypes.some((type) => type.value === values.logType)) { + errors.logType = `Invalid log type`; } if (!values.detection) { @@ -120,6 +134,8 @@ export const RuleEditorForm: React.FC = ({ if (!values.level) { errors.level = 'Rule level is required'; + } else if (!ruleSeverity.some((sev) => sev.value === values.level)) { + errors.level = `Invalid rule level. Should be one of critical, high, medium, low, informational`; } if (!values.author) { @@ -132,6 +148,8 @@ export const RuleEditorForm: React.FC = ({ if (!values.status) { errors.status = 'Rule status is required'; + } else if (!ruleStatus.includes(values.status)) { + errors.status = `Invalid rule status. Should be one of experimental, test, stable`; } if (!validateTags(values.tags)) { @@ -192,12 +210,12 @@ export const RuleEditorForm: React.FC = ({ Rule name } - isInvalid={props.touched.name && !!props.errors?.name} + isInvalid={!!props.errors?.name} error={props.errors.name} helpText="Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores" > { @@ -240,11 +258,11 @@ export const RuleEditorForm: React.FC = ({ } helpText="Combine multiple authors separated with a comma" - isInvalid={props.touched.author && !!props.errors?.author} + isInvalid={!!props.errors?.author} error={props.errors.author} > { @@ -273,11 +291,11 @@ export const RuleEditorForm: React.FC = ({ Log type } - isInvalid={props.touched.logType && !!props.errors?.logType} + isInvalid={!!props.errors?.logType} error={props.errors.logType} > = ({ Rule level (severity) } - isInvalid={props.touched.level && !!props.errors?.level} + isInvalid={!!props.errors?.level} error={props.errors.level} > ({ label: name, value }))} singleSelection={{ asPlainText: true }} onChange={(e) => { props.handleChange('level')(e[0]?.value ? e[0].value : ''); @@ -338,7 +351,12 @@ export const RuleEditorForm: React.FC = ({ onBlur={props.handleBlur('level')} selectedOptions={ props.values.level - ? [{ value: props.values.level, label: props.values.level }] + ? [ + { + value: props.values.level, + label: getSeverityLabel(props.values.level), + }, + ] : [] } /> @@ -352,11 +370,11 @@ export const RuleEditorForm: React.FC = ({ Rule Status } - isInvalid={props.touched.status && !!props.errors?.status} + isInvalid={!!props.errors?.status} error={props.errors.status} > ({ value: type, label: type }))} @@ -387,7 +405,7 @@ export const RuleEditorForm: React.FC = ({ { @@ -439,7 +457,7 @@ export const RuleEditorForm: React.FC = ({ addButtonName="Add tag" fields={props.values.tags} error={props.errors.tags} - isInvalid={props.touched.tags && !!props.errors.tags} + isInvalid={!!props.errors.tags} onChange={(tags) => { props.touched.tags = true; props.setFieldValue('tags', tags); @@ -467,7 +485,7 @@ export const RuleEditorForm: React.FC = ({ addButtonName="Add URL" fields={props.values.references} error={props.errors.references} - isInvalid={props.touched.references && !!props.errors.references} + isInvalid={!!props.errors?.references} onChange={(references) => { props.touched.references = true; props.setFieldValue('references', references); @@ -495,7 +513,7 @@ export const RuleEditorForm: React.FC = ({ addButtonName="Add false positive" fields={props.values.falsePositives} error={props.errors.falsePositives} - isInvalid={props.touched.falsePositives && !!props.errors.falsePositives} + isInvalid={!!props.errors?.falsePositives} onChange={(falsePositives) => { props.touched.falsePositives = true; props.setFieldValue('falsePositives', falsePositives); diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts b/public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts index ac4bba29b..752b8e198 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts +++ b/public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { DEFAULT_RULE_UUID } from '../../../../../common/constants'; import { ruleStatus } from '../../utils/constants'; export interface RuleEditorFormModel { @@ -21,7 +22,7 @@ export interface RuleEditorFormModel { } export const ruleEditorStateDefaultValue: RuleEditorFormModel = { - id: '25b9c01c-350d-4b95-bed1-836d04a4f324', + id: DEFAULT_RULE_UUID, log_source: {}, logType: '', name: '', diff --git a/public/pages/Rules/components/RuleEditor/__snapshots__/DetectionVisualEditor.test.tsx.snap b/public/pages/Rules/components/RuleEditor/__snapshots__/DetectionVisualEditor.test.tsx.snap index ec4313442..251117067 100644 --- a/public/pages/Rules/components/RuleEditor/__snapshots__/DetectionVisualEditor.test.tsx.snap +++ b/public/pages/Rules/components/RuleEditor/__snapshots__/DetectionVisualEditor.test.tsx.snap @@ -201,9 +201,9 @@ Object { > - contains + all
- contains + all