From 7fcfb5ae02704da761a693277d9f29832f6d93e7 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Tue, 14 Mar 2023 22:13:13 +0100 Subject: [PATCH] Common data store for the rules (#474) * [FEATURE] Common data store for the rules #473 Signed-off-by: Jovan Cvetkovic * [FEATURE] Common data store for the rules #473 Signed-off-by: Jovan Cvetkovic * [FEATURE] Common data store for the rules #473 Signed-off-by: Jovan Cvetkovic * [FEATURE] Common data store for the rules #473 Fix cypress create rules flaky tests #426 Signed-off-by: Jovan Cvetkovic * [FEATURE] Common data store for the rules #473 Fix cypress create rules flaky tests #426 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Common data store for the rules #473 Signed-off-by: Jovan Cvetkovic * [FEATURE] Common data store for the rules #473 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic --- cypress/fixtures/sample_rule.json | 117 ++++++ cypress/integration/2_rules.spec.js | 30 +- .../components/AlertFlyout/AlertFlyout.tsx | 13 +- .../pages/Alerts/containers/Alerts/Alerts.tsx | 5 +- .../DetectionRules/DetectionRules.tsx | 1 - .../containers/CreateDetector.tsx | 11 +- .../DetectorRulesView/DetectorRulesView.tsx | 16 +- .../DetectorRulesView.test.tsx.snap | 2 + .../UpdateAlertConditions.tsx | 86 ++-- .../UpdateAlertConditions.test.tsx.snap | 10 + .../UpdateDetectorBasicDetails.test.tsx.snap | 10 + .../components/UpdateRules/UpdateRules.tsx | 16 +- .../AlertTriggersView/AlertTriggersView.tsx | 45 +-- .../AlertTriggersView.test.tsx.snap | 372 +----------------- .../DetectorDetails.test.tsx.snap | 14 + .../DetectorDetailsView.test.tsx.snap | 6 + .../__snapshots__/Detectors.test.tsx.snap | 4 + .../EditFieldMappings.test.tsx.snap | 4 + .../Findings/containers/Findings/Findings.tsx | 10 +- .../Overview/models/OverviewViewModel.ts | 45 +-- .../utils/__snapshots__/helper.test.ts.snap | 2 +- public/pages/Overview/utils/helpers.ts | 2 +- .../RuleEditor/RuleEditorContainer.tsx | 18 +- .../RuleViewerFlyout/RuleViewerFlyout.tsx | 18 +- .../containers/CreateRule/CreateRule.tsx | 1 - .../DuplicateRule/DuplicateRule.tsx | 11 +- .../Rules/containers/EditRule/EditRule.tsx | 1 - .../containers/ImportRule/ImportRule.tsx | 10 +- public/pages/Rules/containers/Rules/Rules.tsx | 42 +- .../pages/Rules/models/RulesViewModelActor.ts | 99 ----- public/pages/Rules/models/types.ts | 8 - public/pages/Rules/store/RulesStore.test.ts | 53 +++ public/pages/Rules/store/RulesStore.ts | 222 +++++++++++ public/security_analytics_app.tsx | 2 + public/services/RuleService.ts | 23 +- public/store/DataStore.ts | 16 + test/mocks/Rules/RuleInfo.mock.ts | 5 +- test/mocks/services/index.ts | 5 +- test/setup.jest.ts | 31 +- types/Rule.ts | 31 ++ yarn.lock | 2 +- 41 files changed, 647 insertions(+), 772 deletions(-) create mode 100644 cypress/fixtures/sample_rule.json delete mode 100644 public/pages/Rules/models/RulesViewModelActor.ts delete mode 100644 public/pages/Rules/models/types.ts create mode 100644 public/pages/Rules/store/RulesStore.test.ts create mode 100644 public/pages/Rules/store/RulesStore.ts create mode 100644 public/store/DataStore.ts diff --git a/cypress/fixtures/sample_rule.json b/cypress/fixtures/sample_rule.json new file mode 100644 index 000000000..762ec91aa --- /dev/null +++ b/cypress/fixtures/sample_rule.json @@ -0,0 +1,117 @@ +{ + "ok": true, + "response": { + "hits": { + "hits": [ + { + "_index": ".opensearch-sap-pre-packaged-rules-config", + "_id": "503fe26e-b5f2-4944-a126-eab405cc06e51", + "_version": 1, + "_seq_no": 1885, + "_primary_term": 1, + "_score": 1, + "_source": { + "category": "network", + "title": "Kerberos Network Traffic RC4 Ticket Encryption", + "log_source": "", + "description": "Detects kerberos TGS request using RC4 encryption which may be indicative of kerberoasting", + "references": [ + { + "value": "https://adsecurity.org/?p=3458" + } + ], + "tags": [ + { + "value": "attack.credential_access" + }, + { + "value": "attack.t1558.003" + } + ], + "level": "medium", + "false_positives": [ + { + "value": "Normal enterprise SPN requests activity" + } + ], + "author": "sigma", + "status": "test", + "last_update_time": "2020-02-11T23:00:00.000Z", + "queries": [ + { + "value": "((zeek-kerberos-request_type: \"TGS\") AND (zeek-kerberos-cipher: \"rc4\\-hmac\")) AND ((NOT service: $*))" + } + ], + "query_field_names": [ + { + "value": "zeek-kerberos-cipher" + }, + { + "value": "service" + }, + { + "value": "zeek-kerberos-request_type" + } + ], + "aggregationQueries": [], + "rule": "title: Kerberos Network Traffic RC4 Ticket Encryption\nid: 503fe26e-b5f2-4944-a126-eab405cc06e5\nstatus: test\ndescription: Detects kerberos TGS request using RC4 encryption which may be indicative of kerberoasting\nauthor: sigma\nreferences:\n - https://adsecurity.org/?p=3458\ndate: 2020/02/12\nmodified: 2021/11/27\nlogsource:\n product: zeek\n service: kerberos\ndetection:\n selection:\n request_type: 'TGS'\n cipher: 'rc4-hmac'\n computer_acct:\n service|startswith: '$'\n condition: selection and not computer_acct\nfalsepositives:\n - Normal enterprise SPN requests activity\nlevel: medium\ntags:\n - attack.credential_access\n - attack.t1558.003\n" + } + }, + { + "_index": ".opensearch-sap-pre-packaged-rules-config", + "_id": "503fe26e-b5f2-4944-a126-eab405cc06e5", + "_version": 1, + "_seq_no": 1885, + "_primary_term": 1, + "_score": 1, + "_source": { + "category": "network", + "title": "Kerberos Network Traffic RC4 Ticket Encryption", + "log_source": "", + "description": "Detects kerberos TGS request using RC4 encryption which may be indicative of kerberoasting", + "references": [ + { + "value": "https://adsecurity.org/?p=3458" + } + ], + "tags": [ + { + "value": "attack.credential_access" + }, + { + "value": "attack.t1558.003" + } + ], + "level": "medium", + "false_positives": [ + { + "value": "Normal enterprise SPN requests activity" + } + ], + "author": "sigma", + "status": "test", + "last_update_time": "2020-02-11T23:00:00.000Z", + "queries": [ + { + "value": "((zeek-kerberos-request_type: \"TGS\") AND (zeek-kerberos-cipher: \"rc4\\-hmac\")) AND ((NOT service: $*))" + } + ], + "query_field_names": [ + { + "value": "zeek-kerberos-cipher" + }, + { + "value": "service" + }, + { + "value": "zeek-kerberos-request_type" + } + ], + "aggregationQueries": [], + "rule": "title: Kerberos Network Traffic RC4 Ticket Encryption\nid: 503fe26e-b5f2-4944-a126-eab405cc06e5\nstatus: test\ndescription: Detects kerberos TGS request using RC4 encryption which may be indicative of kerberoasting\nauthor: sigma\nreferences:\n - https://adsecurity.org/?p=3458\ndate: 2020/02/12\nmodified: 2021/11/27\nlogsource:\n product: zeek\n service: kerberos\ndetection:\n selection:\n request_type: 'TGS'\n cipher: 'rc4-hmac'\n computer_acct:\n service|startswith: '$'\n condition: selection and not computer_acct\nfalsepositives:\n - Normal enterprise SPN requests activity\nlevel: medium\ntags:\n - attack.credential_access\n - attack.t1558.003\n" + } + } + ] + } + } +} diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index e0d34deda..fa667841e 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -148,6 +148,9 @@ describe('Rules', () => { force: true, }); + // Enter the log type + cy.get('[data-test-subj="rule_status_dropdown"]').type(SAMPLE_RULE.status); + // Enter the name cy.get('[data-test-subj="rule_name_field"]').type(SAMPLE_RULE.name); @@ -162,23 +165,24 @@ describe('Rules', () => { // Enter the tags SAMPLE_RULE.tags.forEach((tag) => - cy.get('[data-test-subj="rule_tags_dropdown"]').type(`${tag}{enter}{esc}`) + cy.get('[data-test-subj="rule_tags_dropdown"]').type(`${tag}{enter}`) ); // Enter the reference cy.get('[data-test-subj="rule_references_field_0"]').type(SAMPLE_RULE.references); // Enter the false positive cases - cy.get('[data-test-subj="rule_false_positives_field_0"]').type(SAMPLE_RULE.falsePositive); + cy.get('[data-test-subj="rule_false_positives_field_0"]').type( + `${SAMPLE_RULE.falsePositive}{enter}` + ); // Enter the author - cy.get('[data-test-subj="rule_author_field"]').type(SAMPLE_RULE.author); - - // Enter the log type - cy.get('[data-test-subj="rule_status_dropdown"]').type(SAMPLE_RULE.status); + cy.get('[data-test-subj="rule_author_field"]').type(`${SAMPLE_RULE.author}{enter}`); // Enter the detection - cy.get('[data-test-subj="rule_detection_field"]').type(SAMPLE_RULE.detection); + cy.get('[data-test-subj="rule_detection_field"] textarea').type(SAMPLE_RULE.detection, { + force: true, + }); // Switch to YAML editor cy.get('[data-test-subj="change-editor-type"] label:nth-child(2)').click({ @@ -270,6 +274,15 @@ describe('Rules', () => { url: '/rules', }).as('deleteRule'); + cy.intercept('POST', 'rules/_search?prePackaged=true', { + delay: 5000, + }).as('getPrePackagedRules'); + + cy.intercept('POST', 'rules/_search?prePackaged=false', { + delay: 5000, + }).as('getCustomRules'); + + cy.wait('@rulesSearch'); cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); // Click the rule link to open the details flyout @@ -288,8 +301,11 @@ describe('Rules', () => { .then(() => cy.get('.euiModalFooter > .euiButton').contains('Delete').click()); cy.wait('@deleteRule'); + cy.wait('@getCustomRules'); + cy.wait('@getPrePackagedRules'); // Search for sample_detector, presumably deleted + cy.wait(3000); cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); // Click the rule link to open the details flyout cy.get('tbody').contains(SAMPLE_RULE.name).should('not.exist'); diff --git a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx index eaac9eebc..cdf5e22cd 100644 --- a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx +++ b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx @@ -28,19 +28,18 @@ import { formatRuleType, renderTime, } from '../../../../utils/helpers'; -import { FindingsService, RuleService, OpenSearchService } from '../../../../services'; +import { FindingsService, OpenSearchService } from '../../../../services'; import FindingDetailsFlyout from '../../../Findings/components/FindingDetailsFlyout'; import { Detector } from '../../../../../models/interfaces'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { Finding } from '../../../Findings/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { RulesViewModelActor } from '../../../Rules/models/RulesViewModelActor'; +import { DataStore } from '../../../../store/DataStore'; export interface AlertFlyoutProps { alertItem: AlertItem; detector: Detector; findingsService: FindingsService; - ruleService: RuleService; notifications: NotificationsStart; opensearchService: OpenSearchService; onClose: () => void; @@ -56,13 +55,9 @@ export interface AlertFlyoutState { } export class AlertFlyout extends React.Component { - private rulesViewModelActor: RulesViewModelActor; - constructor(props: AlertFlyoutProps) { super(props); - this.rulesViewModelActor = new RulesViewModelActor(props.ruleService); - this.state = { acknowledged: props.alertItem.state === ALERT_STATE.ACKNOWLEDGED, findingItems: [], @@ -113,12 +108,12 @@ export class AlertFlyout extends React.Component 0) { - const rulesResponse = await this.rulesViewModelActor.fetchRules({ + const rules = await DataStore.rules.getAllRules({ _id: ruleIds, }); const allRules: { [id: string]: RuleSource } = {}; - rulesResponse.forEach((hit) => (allRules[hit._id] = hit._source)); + rules.forEach((hit) => (allRules[hit._id] = hit._source)); this.setState({ rules: allRules }); } diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index 98334e393..3482e8fc2 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -41,7 +41,7 @@ import AlertsService from '../../../../services/AlertsService'; import DetectorService from '../../../../services/DetectorService'; import { AlertItem } from '../../../../../server/models/interfaces'; import { AlertFlyout } from '../../components/AlertFlyout/AlertFlyout'; -import { FindingsService, RuleService, OpenSearchService } from '../../../../services'; +import { FindingsService, OpenSearchService } from '../../../../services'; import { Detector } from '../../../../../models/interfaces'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT } from '../../utils/constants'; @@ -62,7 +62,6 @@ export interface AlertsProps extends RouteComponentProps { alertService: AlertsService; detectorService: DetectorService; findingService: FindingsService; - ruleService: RuleService; opensearchService: OpenSearchService; notifications: NotificationsStart; match: match; @@ -406,7 +405,6 @@ export class Alerts extends Component { }; render() { - const { ruleService } = this.props; const { alerts, alertsFiltered, @@ -485,7 +483,6 @@ export class Alerts extends Component { onClose={this.onFlyoutClose} onAcknowledge={this.onAcknowledge} findingsService={this.props.findingService} - ruleService={ruleService} /> )} diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx index f31596a85..121b2115b 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx @@ -88,7 +88,6 @@ export const DetectionRules: React.FC = ({ hideFlyout={() => setFlyoutData(() => null)} history={null as any} ruleTableItem={flyoutData} - ruleService={null as any} /> ) : null} { static contextType = CoreServicesContext; - private rulesViewModelActor: RulesViewModelActor; constructor(props: CreateDetectorProps) { super(props); - this.rulesViewModelActor = new RulesViewModelActor(props.services.ruleService); this.state = { currentStep: DetectorCreationStep.DEFINE_DETECTOR, detector: EMPTY_DEFAULT_DETECTOR, @@ -215,10 +213,9 @@ export default class CreateDetector extends Component rule.prePackaged); diff --git a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx index 12f7b01d3..ec0c0e677 100644 --- a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx +++ b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx @@ -4,7 +4,7 @@ */ import { ContentPanel } from '../../../../components/ContentPanel'; -import React, { useContext, useEffect, useState, useMemo } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { EuiAccordion, EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; import { ServicesContext } from '../../../../services'; @@ -15,7 +15,7 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { RulesTable } from '../../../Rules/components/RulesTable/RulesTable'; import { RuleTableItem } from '../../../Rules/utils/helpers'; import { RuleViewerFlyout } from '../../../Rules/components/RuleViewerFlyout/RuleViewerFlyout'; -import { RulesViewModelActor } from '../../../Rules/models/RulesViewModelActor'; +import { DataStore } from '../../../../store/DataStore'; export interface DetectorRulesViewProps { detector: Detector; @@ -59,11 +59,6 @@ export const DetectorRulesView: React.FC = (props) => { ]; const services = useContext(ServicesContext); - const rulesViewModelActor = useMemo( - () => (services ? new RulesViewModelActor(services.ruleService) : null), - [services] - ); - useEffect(() => { const updateRulesState = async () => { setLoading(true); @@ -74,10 +69,8 @@ export const DetectorRulesView: React.FC = (props) => { props.detector.inputs[0].detector_input.custom_rules.map((ruleInfo) => ruleInfo.id) ); - const allRules = await rulesViewModelActor?.fetchRules(undefined, { - bool: { - must: [{ match: { 'rule.category': `${props.detector.detector_type.toLowerCase()}` } }], - }, + const allRules = await DataStore.rules.getAllRules({ + 'rule.category': [props.detector.detector_type.toLowerCase()], }); const prePackagedRules = allRules?.filter((rule) => rule.prePackaged); @@ -127,7 +120,6 @@ export const DetectorRulesView: React.FC = (props) => { hideFlyout={() => setFlyoutData(() => null)} history={null as any} ruleTableItem={flyoutData} - ruleService={null as any} notifications={props.notifications} /> ) : null} diff --git a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap index 81ff640ce..cedeea49f 100644 --- a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap +++ b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap @@ -27,6 +27,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -49,6 +50,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, diff --git a/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx b/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx index d2c2984f0..32062a8b2 100644 --- a/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx +++ b/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx @@ -13,12 +13,7 @@ import { } from '../../../../../server/models/interfaces'; import { Detector } from '../../../../../models/interfaces'; import ConfigureAlerts from '../../../CreateDetector/components/ConfigureAlerts'; -import { - DetectorsService, - NotificationsService, - RuleService, - OpenSearchService, -} from '../../../../services'; +import { DetectorsService, NotificationsService, OpenSearchService } from '../../../../services'; import { RuleOptions } from '../../../../models/interfaces'; import { ROUTES, @@ -33,12 +28,12 @@ import { } from '../../../../utils/helpers'; import { DetectorCreationStep } from '../../../CreateDetector/models/types'; import { ServerResponse } from '../../../../../server/models/types'; +import { DataStore } from '../../../../store/DataStore'; export interface UpdateAlertConditionsProps extends RouteComponentProps { detectorService: DetectorsService; opensearchService: OpenSearchService; - ruleService: RuleService; notificationsService: NotificationsService; notifications: NotificationsStart; } @@ -84,7 +79,7 @@ export default class UpdateAlertConditions extends Component< getRules = async () => { try { - const { ruleService, detectorService } = this.props; + const { detectorService } = this.props; const { detector } = this.state; if (!detector.name) { const response = (await detectorService.getDetectors()) as ServerResponse< @@ -102,66 +97,35 @@ export default class UpdateAlertConditions extends Component< }); } } - const body = { - from: 0, - size: 5000, - query: { - nested: { - path: 'rule', - query: { - bool: { - must: [{ match: { 'rule.category': detector.detector_type.toLowerCase() } }], - }, - }, - }, - }, - }; + const terms = { 'rule.category': [detector.detector_type.toLowerCase()] }; - const prePackagedResponse = await ruleService.getRules(true, body); - const customResponse = await ruleService.getRules(false, body); + const customRules = await DataStore.rules.getCustomRules(terms); + const prePackagedRules = await DataStore.rules.getPrePackagedRules(terms); const allRules: { [id: string]: RuleSource } = {}; const rulesOptions = new Set(); - if (prePackagedResponse.ok) { - prePackagedResponse.response.hits.hits.forEach((hit) => { - allRules[hit._id] = hit._source; - const rule = allRules[hit._id]; - rulesOptions.add({ - name: rule.title, - id: hit._id, - severity: rule.level, - tags: rule.tags.map((tag) => tag.value), - }); + prePackagedRules.forEach((hit) => { + allRules[hit._id] = hit._source; + const rule = allRules[hit._id]; + rulesOptions.add({ + name: rule.title, + id: hit._id, + severity: rule.level, + tags: rule.tags.map((tag) => tag.value), }); - } else { - errorNotificationToast( - this.props.notifications, - 'retrieve', - 'pre-packaged rules', - prePackagedResponse.error - ); - } - - if (customResponse.ok) { - customResponse.response.hits.hits.forEach((hit) => { - allRules[hit._id] = hit._source; - const rule = allRules[hit._id]; - rulesOptions.add({ - name: rule.title, - id: hit._id, - severity: rule.level, - tags: rule.tags.map((tag) => tag.value), - }); + }); + + customRules.forEach((hit) => { + allRules[hit._id] = hit._source; + const rule = allRules[hit._id]; + rulesOptions.add({ + name: rule.title, + id: hit._id, + severity: rule.level, + tags: rule.tags.map((tag) => tag.value), }); - } else { - errorNotificationToast( - this.props.notifications, - 'retrieve', - 'custom rules', - customResponse.error - ); - } + }); this.setState({ rules: allRules, rulesOptions: Array.from(rulesOptions) }); } catch (e: any) { diff --git a/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap b/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap index c991a985f..5c1981715 100644 --- a/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap @@ -31,6 +31,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -53,6 +54,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -231,6 +233,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -253,6 +256,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -451,6 +455,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -473,6 +478,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -630,6 +636,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -652,6 +659,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -832,6 +840,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -854,6 +863,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap index e375ee4ae..a787a5377 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap @@ -31,6 +31,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -53,6 +54,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -225,6 +227,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -247,6 +250,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -398,6 +402,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -420,6 +425,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -941,6 +947,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -963,6 +970,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -1330,6 +1338,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -1352,6 +1361,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, diff --git a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx index 9e7193758..a193b3238 100644 --- a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx +++ b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx @@ -9,7 +9,7 @@ import { SearchDetectorsResponse, UpdateDetectorResponse, } from '../../../../../server/models/interfaces'; -import React, { useCallback, useContext, useEffect, useState, useMemo } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; import { Detector } from '../../../../../models/interfaces'; @@ -22,8 +22,8 @@ import { CoreServicesContext } from '../../../../components/core_services'; import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers'; import { RuleTableItem } from '../../../Rules/utils/helpers'; import { RuleViewerFlyout } from '../../../Rules/components/RuleViewerFlyout/RuleViewerFlyout'; -import { RulesViewModelActor } from '../../../Rules/models/RulesViewModelActor'; import { ContentPanel } from '../../../../components/ContentPanel'; +import { DataStore } from '../../../../store/DataStore'; export interface UpdateDetectorRulesProps extends RouteComponentProps< @@ -44,11 +44,6 @@ export const UpdateDetectorRules: React.FC = (props) = const detectorId = props.location.pathname.replace(`${ROUTES.EDIT_DETECTOR_RULES}/`, ''); const [flyoutData, setFlyoutData] = useState(null); - const rulesViewModelActor = useMemo( - () => (services ? new RulesViewModelActor(services.ruleService) : null), - [services] - ); - const context = useContext(CoreServicesContext); useEffect(() => { @@ -88,10 +83,8 @@ export const UpdateDetectorRules: React.FC = (props) = ); enabledRuleIds = enabledRuleIds.concat(enabledCustomRuleIds); - const allRules = await rulesViewModelActor?.fetchRules(undefined, { - bool: { - must: [{ match: { 'rule.category': `${detector.detector_type.toLowerCase()}` } }], - }, + const allRules = await DataStore.rules.getAllRules({ + 'rule.category': [detector.detector_type.toLowerCase()], }); const prePackagedRules = allRules?.filter((rule) => rule.prePackaged); @@ -214,7 +207,6 @@ export const UpdateDetectorRules: React.FC = (props) = hideFlyout={() => setFlyoutData(() => null)} history={null as any} ruleTableItem={flyoutData} - ruleService={null as any} /> ) : null} diff --git a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx index daf3e9f71..92e1d721b 100644 --- a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx +++ b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx @@ -13,11 +13,11 @@ import { ServerResponse } from '../../../../../server/models/types'; import { FeatureChannelList, GetChannelsResponse, - GetRulesResponse, RuleInfo, } from '../../../../../server/models/interfaces'; import { errorNotificationToast } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import { DataStore } from '../../../../store/DataStore'; export interface AlertTriggersViewProps { detector: Detector; @@ -50,43 +50,24 @@ export const AlertTriggersView: React.FC = ({ const getRules = async () => { const parseRules: { [key: string]: RuleInfo } = {}; + // Retrieve the custom rules. + const customRuleIds = detector.inputs[0].detector_input.custom_rules.map((rule) => rule.id); + if (customRuleIds.length > 0) { + const customRules = await DataStore.rules.getCustomRules({ _id: customRuleIds }); + + customRules.forEach((rule) => (parseRules[rule._id] = rule)); + } + // Retrieve the prepackaged rules. const prepackagedRuleIds = detector.inputs[0].detector_input.pre_packaged_rules.map( (rule) => rule.id ); if (prepackagedRuleIds.length > 0) { - const prePackagedResponse = (await services?.ruleService.getRules(true, { - from: 0, - size: 5000, - query: { nested: { path: 'rule', query: { terms: { _id: prepackagedRuleIds } } } }, - })) as ServerResponse; - - if (prePackagedResponse.ok) { - prePackagedResponse.response.hits.hits.forEach((rule) => (parseRules[rule._id] = rule)); - } else { - errorNotificationToast( - notifications, - 'retrieve', - 'pre-packaged rules', - prePackagedResponse.error - ); - } - } - - // Retrieve the custom rules. - const customRuleIds = detector.inputs[0].detector_input.custom_rules.map((rule) => rule.id); - if (customRuleIds.length > 0) { - const customResponse = (await services?.ruleService.getRules(true, { - from: 0, - size: 5000, - query: { nested: { path: 'rule', query: { terms: { _id: customRuleIds } } } }, - })) as ServerResponse; + const prePackagedRules = await DataStore.rules.getPrePackagedRules({ + _id: prepackagedRuleIds, + }); - if (customResponse.ok) { - customResponse.response.hits.hits.forEach((rule) => (parseRules[rule._id] = rule)); - } else { - errorNotificationToast(notifications, 'retrieve', 'custom rules', customResponse.error); - } + prePackagedRules.forEach((rule) => (parseRules[rule._id] = rule)); } // Set all enabled rules. diff --git a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap index 7c865224d..4b134ef5d 100644 --- a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap +++ b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap @@ -27,6 +27,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -49,6 +50,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -415,6 +417,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -437,6 +440,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -567,188 +571,7 @@ exports[` spec renders the component 1`] = ` key="trigger_id_0" notificationChannels={Array []} orderPosition={0} - rules={ - Object { - "detector_id_1": Object { - "_id": "detector_id_1", - "_index": ".windows", - "_source": Object { - "createdBy": "someone", - "detector_type": "detector_type", - "enabled": true, - "enabled_time": 1, - "id": "detector_id_1", - "inputs": Array [ - Object { - "detector_input": Object { - "custom_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - }, - ], - "description": "detectorDescription", - "indices": Array [ - ".windows", - ], - "pre_packaged_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - }, - ], - }, - }, - ], - "last_update_time": 1, - "name": "detector_name", - "schedule": Object { - "period": Object { - "interval": 1, - "unit": "minute", - }, - }, - "triggers": Array [ - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "id": "trigger_id_0", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "id": "trigger_id_1", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - ], - "type": "detector", - }, - }, - } - } + rules={Object {}} >
spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -1644,6 +1468,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -1774,188 +1599,7 @@ exports[` spec renders the component 1`] = ` key="trigger_id_1" notificationChannels={Array []} orderPosition={1} - rules={ - Object { - "detector_id_1": Object { - "_id": "detector_id_1", - "_index": ".windows", - "_source": Object { - "createdBy": "someone", - "detector_type": "detector_type", - "enabled": true, - "enabled_time": 1, - "id": "detector_id_1", - "inputs": Array [ - Object { - "detector_input": Object { - "custom_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - }, - ], - "description": "detectorDescription", - "indices": Array [ - ".windows", - ], - "pre_packaged_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - }, - ], - }, - }, - ], - "last_update_time": 1, - "name": "detector_name", - "schedule": Object { - "period": Object { - "interval": 1, - "unit": "minute", - }, - }, - "triggers": Array [ - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "id": "trigger_id_0", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "id": "trigger_id_1", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - ], - "type": "detector", - }, - }, - } - } + rules={Object {}} >
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 3de85c0a3..29921169b 100644 --- a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap +++ b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap @@ -31,6 +31,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -53,6 +54,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -549,6 +551,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -571,6 +574,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -728,6 +732,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -750,6 +755,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -962,6 +968,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -984,6 +991,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -1141,6 +1149,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -1163,6 +1172,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -2324,6 +2334,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -2346,6 +2357,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -2503,6 +2515,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -2525,6 +2538,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, 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 355e263f6..437227411 100644 --- a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap +++ b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap @@ -27,6 +27,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -49,6 +50,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -215,6 +217,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -237,6 +240,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -1352,6 +1356,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -1374,6 +1379,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, diff --git a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap index 210a4f2b3..7cf9f7c8f 100644 --- a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap +++ b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap @@ -389,6 +389,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -411,6 +412,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -1185,6 +1187,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -1207,6 +1210,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, diff --git a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap index 9bd7ee5b8..59bffb581 100644 --- a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap +++ b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap @@ -27,6 +27,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -49,6 +50,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, @@ -315,6 +317,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], "description": "detectorDescription", @@ -337,6 +340,7 @@ exports[` spec renders the component 1`] = ` }, "_version": 1, "id": "rule_id_1", + "prePackaged": true, }, ], }, diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index b3c357ea7..b76b654e6 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -21,7 +21,6 @@ import { DetectorsService, NotificationsService, OpenSearchService, - RuleService, IndexPatternsService, } from '../../../../services'; import { @@ -54,7 +53,7 @@ import { DetectorHit, RuleSource } from '../../../../../server/models/interfaces import { NotificationsStart } from 'opensearch-dashboards/public'; import { DateTimeFilter } from '../../../Overview/models/interfaces'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; -import { RulesViewModelActor } from '../../../Rules/models/RulesViewModelActor'; +import { DataStore } from '../../../../store/DataStore'; interface FindingsProps extends RouteComponentProps { detectorService: DetectorsService; @@ -62,7 +61,6 @@ interface FindingsProps extends RouteComponentProps { notificationsService: NotificationsService; indexPatternsService: IndexPatternsService; opensearchService: OpenSearchService; - ruleService: RuleService; notifications: NotificationsStart; match: match; dateTimeFilter?: DateTimeFilter; @@ -101,12 +99,10 @@ export const groupByOptions = [ class Findings extends Component { static contextType = CoreServicesContext; - private rulesViewModelActor: RulesViewModelActor; constructor(props: FindingsProps) { super(props); - this.rulesViewModelActor = new RulesViewModelActor(props.ruleService); const { dateTimeFilter = { startTime: DEFAULT_DATE_RANGE.start, @@ -199,12 +195,12 @@ class Findings extends Component { getRules = async (ruleIds: string[]) => { const { notifications } = this.props; try { - const rulesResponse = await this.rulesViewModelActor.fetchRules({ + const rules = await DataStore.rules.getAllRules({ _id: ruleIds, }); const allRules: { [id: string]: RuleSource } = {}; - rulesResponse.forEach((hit) => (allRules[hit._id] = hit._source)); + rules.forEach((hit) => (allRules[hit._id] = hit._source)); this.setState({ rules: allRules }); } catch (e) { diff --git a/public/pages/Overview/models/OverviewViewModel.ts b/public/pages/Overview/models/OverviewViewModel.ts index 429b21b74..2c09e4ad4 100644 --- a/public/pages/Overview/models/OverviewViewModel.ts +++ b/public/pages/Overview/models/OverviewViewModel.ts @@ -6,12 +6,12 @@ import { BrowserServices } from '../../../models/interfaces'; import { DetectorHit, RuleSource } from '../../../../server/models/interfaces'; import { AlertItem, FindingItem } from './interfaces'; -import { RuleService } from '../../../services'; import { DEFAULT_DATE_RANGE, DEFAULT_EMPTY_DATA } from '../../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../../../utils/helpers'; import dateMath from '@elastic/datemath'; import moment from 'moment'; +import { DataStore } from '../../../store/DataStore'; export interface OverviewViewModel { detectors: DetectorHit[]; @@ -46,46 +46,17 @@ export class OverviewViewModelActor { private async getRules(ruleIds: string[]): Promise<{ [id: string]: RuleSource }> { try { - const rulesService = this.services?.ruleService as RuleService; - const body = { - from: 0, - size: 5000, - query: { - nested: { - path: 'rule', - query: { - terms: { - _id: ruleIds, - }, - }, - }, - }, + const terms = { + _id: ruleIds, }; - const prePackagedResponse = await rulesService.getRules(true, body); - const customResponse = await rulesService.getRules(false, body); + const customResponse = await DataStore.rules.getCustomRules(terms); + const prePackagedResponse = await DataStore.rules.getPrePackagedRules(terms); const ruleById: { [id: string]: any } = {}; - if (prePackagedResponse.ok) { - prePackagedResponse.response.hits.hits.forEach((hit) => (ruleById[hit._id] = hit._source)); - } else { - errorNotificationToast( - this.notifications, - 'retrieve', - 'pre-packaged rules', - prePackagedResponse.error - ); - } - if (customResponse.ok) { - customResponse.response.hits.hits.forEach((hit) => (ruleById[hit._id] = hit._source)); - } else if (!customResponse.error?.includes('index doesnt exist')) { - errorNotificationToast( - this.notifications, - 'retrieve', - 'custom rules', - customResponse.error - ); - } + prePackagedResponse.forEach((hit) => (ruleById[hit._id] = hit._source)); + customResponse.forEach((hit) => (ruleById[hit._id] = hit._source)); + return ruleById; } catch (error: any) { errorNotificationToast(this.notifications, 'retrieve', 'rules', error); diff --git a/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap b/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap index 3378a43a5..65b0e88f2 100644 --- a/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap +++ b/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap @@ -384,7 +384,7 @@ Object { "format": "%Y-%m-%d %H:%M", "grid": false, }, - "bandPosition": 0.5, + "band": 0.5, "field": "time", "scale": Object { "domain": undefined, diff --git a/public/pages/Overview/utils/helpers.ts b/public/pages/Overview/utils/helpers.ts index 6484f36ce..b3307cfef 100644 --- a/public/pages/Overview/utils/helpers.ts +++ b/public/pages/Overview/utils/helpers.ts @@ -219,7 +219,7 @@ export function getOverviewVisualizationSpec( }, encoding: { x: getXAxis(dateOpts, { - bandPosition: 0.5, + band: 0.5, }), y: getYAxis('alert', 'Count'), tooltip: [getYAxis('alert', 'Alerts'), getTimeTooltip(dateOpts)], diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx index 55b6f639b..26e62040e 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx @@ -6,7 +6,6 @@ import React, { useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { RuleService } from '../../../../services'; import { ROUTES } from '../../../../utils/constants'; import { EuiSpacer } from '@elastic/eui'; import { Rule } from '../../../../../models/interfaces'; @@ -14,14 +13,13 @@ import { RuleEditorFormModel, ruleEditorStateDefaultValue } from './RuleEditorFo import { mapFormToRule, mapRuleToForm } from './mappers'; import { RuleEditorForm } from './RuleEditorForm'; import { validateRule } from '../../utils/helpers'; -import { errorNotificationToast } from '../../../../utils/helpers'; +import { DataStore } from '../../../../store/DataStore'; export interface RuleEditorProps { title: string; rule?: Rule; history: RouteComponentProps['history']; notifications?: NotificationsStart; - ruleService: RuleService; mode: 'create' | 'edit'; } @@ -36,7 +34,6 @@ export const RuleEditorContainer: React.FC = ({ notifications, title, rule, - ruleService, mode, }) => { const initialRuleValue = rule @@ -55,19 +52,12 @@ export const RuleEditorContainer: React.FC = ({ console.error('No rule id found'); return; } - result = await ruleService.updateRule(rule?.id, submitingRule.category, submitingRule); + result = await DataStore.rules.updateRule(rule?.id, submitingRule.category, submitingRule); } else { - result = await ruleService.createRule(submitingRule); + result = await DataStore.rules.createRule(submitingRule); } - if (!result.ok) { - errorNotificationToast( - notifications!, - mode === 'create' ? 'create' : 'save', - 'rule', - result.error - ); - } else { + if (result) { history.replace(ROUTES.RULES); } }; diff --git a/public/pages/Rules/components/RuleViewerFlyout/RuleViewerFlyout.tsx b/public/pages/Rules/components/RuleViewerFlyout/RuleViewerFlyout.tsx index a1c265da7..9ac1fb500 100644 --- a/public/pages/Rules/components/RuleViewerFlyout/RuleViewerFlyout.tsx +++ b/public/pages/Rules/components/RuleViewerFlyout/RuleViewerFlyout.tsx @@ -12,7 +12,6 @@ import { EuiTitle, EuiButtonIcon, } from '@elastic/eui'; -import { errorNotificationToast } from '../../../../utils/helpers'; import { ROUTES } from '../../../../utils/constants'; import React, { useMemo, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; @@ -20,13 +19,12 @@ import { RuleTableItem } from '../../utils/helpers'; import { DeleteRuleModal } from '../DeleteModal/DeleteModal'; import { RuleContentViewer } from '../RuleContentViewer/RuleContentViewer'; import { RuleViewerFlyoutHeaderActions } from './RuleViewFlyoutHeaderActions'; -import { RuleService } from '../../../../services'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import { DataStore } from '../../../../store/DataStore'; export interface RuleViewerFlyoutProps { history?: RouteComponentProps['history']; ruleTableItem: RuleTableItem; - ruleService?: RuleService; notifications?: NotificationsStart; hideFlyout: (refreshRules?: boolean) => void; } @@ -35,7 +33,6 @@ export const RuleViewerFlyout: React.FC = ({ history, hideFlyout, ruleTableItem, - ruleService, notifications, }) => { const [actionsPopoverOpen, setActionsPopoverOpen] = useState(false); @@ -79,18 +76,11 @@ export const RuleViewerFlyout: React.FC = ({ }; const onDeleteRuleConfirmed = async () => { - if (!ruleService) { - return; - } - const deleteRuleRes = await ruleService.deleteRule(ruleTableItem.ruleId); + const response = await DataStore.rules.deleteRule(ruleTableItem.ruleId); - if (deleteRuleRes.ok) { + if (response) { closeDeleteModal(); hideFlyout(true); - } else { - if (notifications) { - errorNotificationToast(notifications, 'delete', 'rule', deleteRuleRes.error); - } } }; @@ -122,7 +112,7 @@ export const RuleViewerFlyout: React.FC = ({

{ruleTableItem.title}

- {ruleService && history && ( + {history && ( {headerActions} diff --git a/public/pages/Rules/containers/CreateRule/CreateRule.tsx b/public/pages/Rules/containers/CreateRule/CreateRule.tsx index a9efe49c9..c44fd2504 100644 --- a/public/pages/Rules/containers/CreateRule/CreateRule.tsx +++ b/public/pages/Rules/containers/CreateRule/CreateRule.tsx @@ -28,7 +28,6 @@ export const CreateRule: React.FC = ({ history, services, notif history={history} notifications={notifications} mode={'create'} - ruleService={services.ruleService} /> ); }; diff --git a/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx b/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx index b8a1cddc0..c1d90474d 100644 --- a/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx +++ b/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx @@ -10,11 +10,11 @@ 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 { RuleItemInfoBase } from '../../models/types'; import { CoreServicesContext } from '../../../../components/core_services'; import { setBreadCrumb, validateRule } from '../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { errorNotificationToast } from '../../../../utils/helpers'; +import { DataStore } from '../../../../store/DataStore'; +import { RuleItemInfoBase } from '../../../../../types'; export interface DuplicateRuleProps extends RouteComponentProps { @@ -35,11 +35,9 @@ export const DuplicateRule: React.FC = ({ if (!validateRule(rule, notifications!, 'create')) { return; } - const updateRuleRes = await services.ruleService.createRule(rule); + const response = await DataStore.rules.createRule(rule); - if (!updateRuleRes.ok) { - errorNotificationToast(notifications!, 'create', 'rule', updateRuleRes.error); - } else { + if (response) { history.replace(ROUTES.RULES); } }; @@ -65,7 +63,6 @@ export const DuplicateRule: React.FC = ({ history={history} notifications={notifications} mode={'create'} - ruleService={services.ruleService} /> ); }; diff --git a/public/pages/Rules/containers/EditRule/EditRule.tsx b/public/pages/Rules/containers/EditRule/EditRule.tsx index d946b7e78..7623d3818 100644 --- a/public/pages/Rules/containers/EditRule/EditRule.tsx +++ b/public/pages/Rules/containers/EditRule/EditRule.tsx @@ -38,7 +38,6 @@ export const EditRule: React.FC = ({ rule={location.state.ruleItem._source} history={history} notifications={notifications} - ruleService={services.ruleService} /> ); }; diff --git a/public/pages/Rules/containers/ImportRule/ImportRule.tsx b/public/pages/Rules/containers/ImportRule/ImportRule.tsx index eee2a126f..b8bc76a0b 100644 --- a/public/pages/Rules/containers/ImportRule/ImportRule.tsx +++ b/public/pages/Rules/containers/ImportRule/ImportRule.tsx @@ -13,9 +13,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { dump, load } from 'js-yaml'; import { ContentPanel } from '../../../../components/ContentPanel'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { errorNotificationToast } from '../../../../utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; import { setBreadCrumb, validateRule } from '../../utils/helpers'; +import { DataStore } from '../../../../store/DataStore'; export interface ImportRuleProps { services: BrowserServices; @@ -77,7 +77,6 @@ export const ImportRule: React.FC = ({ history, services, notif history={history} notifications={notifications} mode={'create'} - ruleService={services.ruleService} rule={rule} /> ); @@ -124,12 +123,9 @@ export const ImportRule: React.FC = ({ history, services, notif if (!validateRule(rule, notifications!, 'create')) { return; } + const response = await DataStore.rules.createRule(rule); - const updateRuleRes = await services.ruleService.createRule(rule); - - if (!updateRuleRes.ok) { - errorNotificationToast(notifications!, 'create', 'rule', updateRuleRes.error); - } else { + if (response) { history.replace(ROUTES.RULES); } }; diff --git a/public/pages/Rules/containers/Rules/Rules.tsx b/public/pages/Rules/containers/Rules/Rules.tsx index e17eee4a9..ecdfcc49d 100644 --- a/public/pages/Rules/containers/Rules/Rules.tsx +++ b/public/pages/Rules/containers/Rules/Rules.tsx @@ -8,14 +8,14 @@ import { ServicesContext } from '../../../../services'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { BrowserServices } from '../../../../models/interfaces'; -import { RulesViewModelActor } from '../../models/RulesViewModelActor'; import { RulesTable } from '../../components/RulesTable/RulesTable'; import { RuleTableItem } from '../../utils/helpers'; -import { RuleItemInfoBase } from '../../models/types'; import { RuleViewerFlyout } from '../../components/RuleViewerFlyout/RuleViewerFlyout'; import { BREADCRUMBS, ROUTES } from '../../../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { CoreServicesContext } from '../../../../components/core_services'; +import { DataStore } from '../../../../store/DataStore'; +import { RuleItemInfoBase } from '../../../../../types'; export interface RulesProps extends RouteComponentProps { notifications?: NotificationsStart; @@ -24,32 +24,29 @@ export interface RulesProps extends RouteComponentProps { export const Rules: React.FC = (props) => { const services = useContext(ServicesContext) as BrowserServices; const context = useContext(CoreServicesContext); - const rulesViewModelActor = useMemo(() => new RulesViewModelActor(services.ruleService), [ - services, - ]); + const [allRules, setAllRules] = useState([]); const [flyoutData, setFlyoutData] = useState(undefined); const [loading, setLoading] = useState(false); - const ruleItems: RuleTableItem[] = useMemo( - () => - allRules.map((rule) => ({ - title: rule._source.title, - level: rule._source.level, - category: rule._source.category, - description: rule._source.description, - source: rule.prePackaged ? 'Sigma' : 'Custom', - ruleInfo: rule, - ruleId: rule._id, - })), - [allRules] - ); const getRules = useCallback(async () => { setLoading(true); - const allRules = await rulesViewModelActor.fetchRules(); - setAllRules(allRules); + + const allRules = await DataStore.rules.getAllRules(); + const rules = allRules.map((rule) => ({ + title: rule._source.title, + level: rule._source.level, + category: rule._source.category, + description: rule._source.description, + source: rule.prePackaged ? 'Sigma' : 'Custom', + ruleInfo: rule, + ruleId: rule._id, + })); + + setAllRules(rules); + setLoading(false); - }, [rulesViewModelActor]); + }, [DataStore.rules.getAllRules]); useEffect(() => { context?.chrome.setBreadcrumbs([BREADCRUMBS.SECURITY_ANALYTICS, BREADCRUMBS.RULES]); @@ -94,7 +91,6 @@ export const Rules: React.FC = (props) => { hideFlyout={hideFlyout} history={props.history} ruleTableItem={flyoutData} - ruleService={services.ruleService} notifications={props.notifications} /> ) : null} @@ -120,7 +116,7 @@ export const Rules: React.FC = (props) => { - + diff --git a/public/pages/Rules/models/RulesViewModelActor.ts b/public/pages/Rules/models/RulesViewModelActor.ts deleted file mode 100644 index 008026485..000000000 --- a/public/pages/Rules/models/RulesViewModelActor.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { load, safeDump } from 'js-yaml'; -import { RuleInfo } from '../../../../server/models/interfaces'; -import { RuleItemInfoBase } from './types'; -import { RuleService } from '../../../services'; -import { ruleTypes } from '../utils/constants'; - -export interface RulesViewModel { - allRules: RuleItemInfoBase[]; -} - -export class RulesViewModelActor { - private rulesViewModel: RulesViewModel; - - constructor(private readonly service: RuleService) { - this.rulesViewModel = { - allRules: [], - }; - } - - public get allRules(): RuleInfo[] { - return this.rulesViewModel.allRules; - } - - public async fetchRules( - terms?: { [key: string]: string[] }, - query?: any - ): Promise { - let prePackagedRules = await this.getRules(true, terms, query); - let customRules = await this.getRules(false, terms, query); - - prePackagedRules = this.extractAndAddDetection(prePackagedRules); - customRules = this.extractAndAddDetection(customRules); - this.rulesViewModel.allRules = customRules.concat(prePackagedRules); - - return this.rulesViewModel.allRules; - } - - private async getRules( - prePackaged: boolean, - terms?: { - [key: string]: string[]; - }, - query?: any - ): Promise { - const getRulesRes = await this.service.getRules(prePackaged, { - from: 0, - size: 5000, - query: { - nested: { - path: 'rule', - query: query || { - terms: terms - ? terms - : { - 'rule.category': ruleTypes, - }, - }, - }, - }, - }); - - if (getRulesRes?.ok) { - return getRulesRes.response.hits.hits.map((hit) => ({ - ...hit, - _source: { - ...hit._source, - prePackaged, - }, - prePackaged, - })); - } - - return []; - } - - private extractAndAddDetection(rules: RuleItemInfoBase[]): RuleItemInfoBase[] { - return rules.map((ruleInfo) => { - let detectionYaml = ''; - - try { - const detectionJson = load(ruleInfo._source.rule).detection; - detectionYaml = safeDump(detectionJson); - } catch (_error: any) {} - - return { - ...ruleInfo, - _source: { - ...ruleInfo._source, - detection: detectionYaml, - }, - }; - }); - } -} diff --git a/public/pages/Rules/models/types.ts b/public/pages/Rules/models/types.ts deleted file mode 100644 index 77d21381b..000000000 --- a/public/pages/Rules/models/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { RuleInfo } from '../../../../server/models/interfaces'; - -export type RuleItemInfoBase = RuleInfo & { prePackaged: boolean }; diff --git a/public/pages/Rules/store/RulesStore.test.ts b/public/pages/Rules/store/RulesStore.test.ts new file mode 100644 index 000000000..24a6584d4 --- /dev/null +++ b/public/pages/Rules/store/RulesStore.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataStore } from '../../../store/DataStore'; +import notificationsStartMock from '../../../../test/mocks/services/notifications/NotificationsStart.mock'; +import services from '../../../../test/mocks/services'; +import { RulesStore } from './RulesStore'; +import { expect } from '@jest/globals'; +import * as rulesResponseMock from '../../../../cypress/fixtures/sample_rule.json'; +describe('Rules store specs', () => { + Object.assign(services, { + ruleService: { + getRules: () => Promise.resolve(rulesResponseMock), + deleteRule: () => Promise.resolve(true), + }, + }); + + DataStore.init(services, notificationsStartMock); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('rules store should be created', () => { + expect(DataStore.rules instanceof RulesStore).toBe(true); + }); + + it('getRules should return an array', async () => { + const serviceSpy = jest.spyOn(DataStore.rules.service, 'getRules'); + + // first call + const rules = await DataStore.rules.getPrePackagedRules(); + expect(rules.length).toStrictEqual(2); + + // second call + await DataStore.rules.getPrePackagedRules(); + + // service.getRules is called only once as the second time is returned from the cache + expect(serviceSpy).toBeCalledTimes(1); + }); + + it('getAllRules should call getRules', async () => { + const getRulesSpy = jest + .spyOn(DataStore.rules, 'getRules') + .mockReturnValue(Promise.resolve([])); + + await DataStore.rules.getAllRules(); + + expect(getRulesSpy).toBeCalledTimes(2); + }); +}); diff --git a/public/pages/Rules/store/RulesStore.ts b/public/pages/Rules/store/RulesStore.ts new file mode 100644 index 000000000..774174b2a --- /dev/null +++ b/public/pages/Rules/store/RulesStore.ts @@ -0,0 +1,222 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RuleService } from '../../../services'; +import { load, safeDump } from 'js-yaml'; +import { RuleItemInfoBase, IRulesStore, IRulesCache } from '../../../../types'; +import { Rule } from '../../../../models/interfaces'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { errorNotificationToast } from '../../../utils/helpers'; +import { ruleTypes } from '../utils/constants'; +import _ from 'lodash'; + +/** + * Class is used to make rule's API calls and cache the rules. + * If there is a cache data requests are skipped and result is returned from the cache. + * If cache is invalidated then the request is made to get a new set of data. + * + * @class RulesStore + * @implements IRulesStore + * @param {BrowserServices} services Uses services to make API requests + */ +export class RulesStore implements IRulesStore { + /** + * Rule service instance + * + * @property {RuleService} service + * @readonly + */ + readonly service: RuleService; + + /** + * Notifications + * @property {NotificationsStart} + * @readonly + */ + readonly notifications: NotificationsStart; + + constructor(service: RuleService, notifications: NotificationsStart) { + this.service = service; + this.notifications = notifications; + } + + /** + * Keeps rule's data cached + * + * @property {IRulesCache} cache + */ + private cache: IRulesCache = {}; + + /** + * Invalidates all rules data + */ + private invalidateCache = () => { + this.cache = {}; + return this; + }; + + /** + * Returns all rules, custom and pre-packaged + * + * @method getAllRules + * @param {terms?: { [key: string]: string[] }} [terms] + * @returns {Promise} + */ + public async getAllRules(terms?: { [key: string]: string[] }): Promise { + let customRules = await this.getCustomRules(terms); + let prePackagedRules = await this.getPrePackagedRules(terms); + + prePackagedRules = this.validateAndAddDetection(prePackagedRules); + customRules = this.validateAndAddDetection(customRules); + + return customRules.concat(prePackagedRules); + } + + /** + * Returns only pre-packaged rules + * @param {{[p: string]: string[]}} terms + * @returns {Promise} + */ + public async getPrePackagedRules(terms?: { [key: string]: string[] }) { + return this.getRules(true, terms); + } + + /** + * Returns only custom rules + * @param {{[p: string]: string[]}} terms + * @returns {Promise} + */ + public async getCustomRules(terms?: { [key: string]: string[] }) { + return this.getRules(false, terms); + } + + /** + * Makes the request to get pre-packaged or custom rules + * + * @param {boolean} prePackaged + * @param {terms?: { [key: string]: string[] }} terms + * @returns {Promise} + */ + public async getRules( + prePackaged: boolean, + terms?: { [key: string]: string[] } + ): Promise { + const cacheKey: string = `getRules:${JSON.stringify(arguments)}`; + + if (this.cache[cacheKey]) { + return this.cache[cacheKey]; + } + + if (!terms) { + terms = { + 'rule.category': _.map(ruleTypes, 'value'), + }; + } + + const body = { + from: 0, + size: 5000, + query: { + nested: { + path: 'rule', + query: { + terms: { ...terms }, + }, + }, + }, + }; + + const response = await this.service.getRules(prePackaged, body); + + if (response?.ok) { + return (this.cache[cacheKey] = response.response.hits.hits.map((hit) => ({ + ...hit, + _source: { + ...hit._source, + prePackaged, + }, + prePackaged, + }))); + } else { + if (!response.error?.includes('index doesnt exist')) { + errorNotificationToast(this.notifications, 'retrieve', 'rules', response.error); + } + } + + return []; + } + + /** + * Create a new rule + * + * @param {Rule} rule + * @returns {Promise} + */ + public createRule = async (rule: Rule): Promise => { + const response = await this.invalidateCache().service.createRule(rule); + if (!response.ok) { + errorNotificationToast(this.notifications, 'create', 'rule', response.error); + } + + return response.ok; + }; + + /** + * Update a rule + * + * @param {string} id + * @param {string} category + * @param {Rule} rule + * @returns {Promise} + */ + public updateRule = async (id: string, category: string, rule: Rule): Promise => { + const response = await this.invalidateCache().service.updateRule(id, category, rule); + if (!response.ok) { + errorNotificationToast(this.notifications, 'update', 'rule', response.error); + } + + return response.ok; + }; + + /** + * Update a rule + * + * @param {string} id + * @returns {Promise} + */ + public deleteRule = async (id: string): Promise => { + const response = await this.invalidateCache().service.deleteRule(id); + if (!response.ok) { + errorNotificationToast(this.notifications, 'delete', 'rule', response.error); + } + + return response.ok; + }; + + /** + * Validates and adds detection yaml to rule items + * + * @param {RuleItemInfoBase[]} rules + * @returns {RuleItemInfoBase[]} + */ + private validateAndAddDetection(rules: RuleItemInfoBase[]): RuleItemInfoBase[] { + return rules.map((ruleInfo) => { + let detectionYaml = ''; + + try { + const detectionJson = load(ruleInfo._source.rule).detection; + detectionYaml = safeDump(detectionJson); + } catch (_error: any) {} + + return { + ...ruleInfo, + _source: { + ...ruleInfo._source, + detection: detectionYaml, + }, + }; + }); + } +} diff --git a/public/security_analytics_app.tsx b/public/security_analytics_app.tsx index b8747cd64..a29b2c8b2 100644 --- a/public/security_analytics_app.tsx +++ b/public/security_analytics_app.tsx @@ -26,6 +26,7 @@ import FieldMappingService from './services/FieldMappingService'; import RuleService from './services/RuleService'; import SavedObjectService from './services/SavedObjectService'; import { SecurityAnalyticsPluginStartDeps } from './plugin'; +import { DataStore } from './store/DataStore'; export function renderApp( coreStart: CoreStart, @@ -60,6 +61,7 @@ export function renderApp( }; const isDarkMode = coreStart.uiSettings.get('theme:darkMode') || false; + DataStore.init(services, coreStart.notifications); ReactDOM.render( diff --git a/public/services/RuleService.ts b/public/services/RuleService.ts index f4198cb3d..326102892 100644 --- a/public/services/RuleService.ts +++ b/public/services/RuleService.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { HttpSetup } from 'opensearch-dashboards/public'; import { ServerResponse } from '../../server/models/types'; import { @@ -5,7 +10,7 @@ import { DeleteRuleResponse, GetRulesResponse, UpdateRuleResponse, -} from '../../server/models/interfaces/Rules'; +} from '../../server/models/interfaces'; import { API } from '../../server/utils/constants'; import { Rule } from '../../models/interfaces'; @@ -18,23 +23,19 @@ export default class RuleService { getRules = async (prePackaged: boolean, body: any): Promise> => { const url = `..${API.RULES_BASE}/_search`; - const response = (await this.httpClient.post(url, { + return (await this.httpClient.post(url, { query: { prePackaged, }, body: JSON.stringify(body), })) as ServerResponse; - - return response; }; createRule = async (rule: Rule): Promise> => { const url = `..${API.RULES_BASE}`; - const response = (await this.httpClient.post(url, { + return (await this.httpClient.post(url, { body: JSON.stringify(rule), })) as ServerResponse; - - return response; }; updateRule = async ( @@ -43,20 +44,16 @@ export default class RuleService { rule: Rule ): Promise> => { const url = `..${API.RULES_BASE}/${ruleId}`; - const response = (await this.httpClient.put(url, { + return (await this.httpClient.put(url, { query: { category, }, body: JSON.stringify(rule), })) as ServerResponse; - - return response; }; deleteRule = async (ruleId: string): Promise> => { const url = `..${API.RULES_BASE}/${ruleId}`; - const response = (await this.httpClient.delete(url)) as ServerResponse; - - return response; + return (await this.httpClient.delete(url)) as ServerResponse; }; } diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts new file mode 100644 index 000000000..3a751b65c --- /dev/null +++ b/public/store/DataStore.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RulesStore } from '../pages/Rules/store/RulesStore'; +import { BrowserServices } from '../models/interfaces'; +import { NotificationsStart } from 'opensearch-dashboards/public'; + +export class DataStore { + public static rules: RulesStore; + + public static init = (services: BrowserServices, notifications: NotificationsStart) => { + DataStore.rules = new RulesStore(services.ruleService, notifications); + }; +} diff --git a/test/mocks/Rules/RuleInfo.mock.ts b/test/mocks/Rules/RuleInfo.mock.ts index 7c9c435b8..efb10d337 100644 --- a/test/mocks/Rules/RuleInfo.mock.ts +++ b/test/mocks/Rules/RuleInfo.mock.ts @@ -4,7 +4,7 @@ */ import ruleSourceMock from './RuleSource.mock'; -import { DetectorRuleInfo } from '../../../models/interfaces'; +import { RuleItemInfoBase } from '../../../types'; export default { id: 'rule_id_1', @@ -13,4 +13,5 @@ export default { _primary_term: 1, _source: ruleSourceMock, _version: 1, -} as DetectorRuleInfo; + prePackaged: true, +} as RuleItemInfoBase; diff --git a/test/mocks/services/index.ts b/test/mocks/services/index.ts index 83f456860..13e4adf4a 100644 --- a/test/mocks/services/index.ts +++ b/test/mocks/services/index.ts @@ -15,10 +15,11 @@ import ruleService from './ruleService.mock'; import findingsService from './findingsService.mock'; import alertService from './alertService.mock'; import indexService from './indexService.mock'; +import { BrowserServices } from '../../../public/models/interfaces'; const openSearchService = new OpenSearchService(httpClientMock, savedObjectsClientMock); -export default { +export default ({ alertService, detectorService, fieldMappingService, @@ -29,4 +30,4 @@ export default { httpClientMock, notificationsMock, openSearchService, -}; +} as unknown) as BrowserServices; diff --git a/test/setup.jest.ts b/test/setup.jest.ts index 1b8c3269a..a278c1c37 100644 --- a/test/setup.jest.ts +++ b/test/setup.jest.ts @@ -7,9 +7,12 @@ import 'jest-canvas-mock'; import '@testing-library/jest-dom/extend-expect'; import { configure } from '@testing-library/react'; import Enzyme from 'enzyme'; +// @ts-ignore import Adapter from 'enzyme-adapter-react-16'; -import { RulesViewModelActor } from '../public/pages/Rules/models/RulesViewModelActor'; import { contextServicesMock as mockContextServices } from './mocks/useContext.mock'; +import { DataStore } from '../public/store/DataStore'; +import services from './mocks/services'; +import notificationsStartMock from './mocks/services/notifications/NotificationsStart.mock'; Enzyme.configure({ adapter: new Adapter() }); @@ -73,30 +76,6 @@ jest.mock('moment', () => { return fakeMoment; }); -/** - * Mocks rules view model actor as it is instantiated in the component classes - * Mocked here so that is applied to all tests - */ -jest.mock('../public/pages/Rules/models/RulesViewModelActor.ts', () => { - const rulesViewModelActor = jest.requireActual( - '../public/pages/Rules/models/RulesViewModelActor.ts' - ); - const rulesViewModelActorMock = { - ...rulesViewModelActor, - getRules: () => - Promise.resolve({ - ok: true, - response: { - hits: { - hits: [], - }, - }, - }), - }; - - return rulesViewModelActorMock as RulesViewModelActor; -}); - /** * React useContext is mocked to return the mocked services * so that this works in all tests @@ -123,4 +102,6 @@ jest.mock('vega/build-es5/vega.js', () => { }; }); +DataStore.init(services, notificationsStartMock); + jest.setTimeout(10000); // in milliseconds diff --git a/types/Rule.ts b/types/Rule.ts index d0f500140..6caeac76b 100644 --- a/types/Rule.ts +++ b/types/Rule.ts @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { RuleService } from '../public/services'; +import { NotificationsStart } from 'opensearch-dashboards/public'; + export interface Rule { id: string; category: string; @@ -91,3 +94,31 @@ export interface DeleteRuleParams { } export interface DeleteRuleResponse {} + +export interface IRulesCache { + [key: string]: RuleItemInfoBase[]; +} + +export interface IRulesStore { + readonly service: RuleService; + + readonly notifications: NotificationsStart; + + getAllRules: (terms?: { [key: string]: string[] }, query?: any) => Promise; + + createRule: (rule: Rule) => Promise; + + updateRule: (id: string, category: string, rule: Rule) => Promise; + + deleteRule: (id: string) => Promise; + + getRules: ( + prePackaged: boolean, + terms?: { [key: string]: string[] }, + query?: any + ) => Promise; + + getPrePackagedRules: (terms?: { [key: string]: string[] }) => Promise; + + getCustomRules: (terms?: { [key: string]: string[] }) => Promise; +} diff --git a/yarn.lock b/yarn.lock index 11456e6e4..e21ad2a85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4179,7 +4179,7 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1, json5@^2.2.1, json5@^2.2.2: +json5@^1.0.1, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==