From 26aa359dfa1f4839c8a9c9feb168c50bb7d62a40 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Fri, 5 May 2023 12:48:58 -0700 Subject: [PATCH] [Backport 2.x] PRs for correlation engine and minor UX improvements (#577) * [BUG] Finding's fly-out has no correlations if open from alerts (#558) * [BUG] Finding's fly-out has no correlations if open from alerts #557 Signed-off-by: Jovan Cvetkovic * code review from https://github.com/opensearch-project/security-analytics-dashboards-plugin/pull/558#discussion_r1178479490 Signed-off-by: Jovan Cvetkovic * cypress tests wait interval updated to 400 Signed-off-by: Jovan Cvetkovic * cypress tests wait interval updated to 400 Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic * cypress | create detector specs update (#518) * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #491 Signed-off-by: Jovan Cvetkovic * [FEATURE] Provide empty states for Findings and Alerts page #471 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor and move field mapping to first the page of create detector feature #495 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #493 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #493 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor alert triggers per mocks #498 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #493 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create global state object for async requests #493 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor alert triggers per mocks #498 Signed-off-by: Jovan Cvetkovic * [FEATURE] Create detector \ Refactor alert triggers per mocks #498 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update detector details component #502 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update detector details component #502 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update detector details component #502 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update detector details component #502 Signed-off-by: Jovan Cvetkovic * Feature] update detector details component #504 Signed-off-by: Jovan Cvetkovic * Feature] update detector details component #504 Signed-off-by: Jovan Cvetkovic * Update detector details component #504 Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * [BUG] No space between the detector details and the rule panel #522 [BUG] A rule flyout without references have an empty link #521 [FEATURE] Update header size to use euiTitle--small #520 Signed-off-by: Jovan Cvetkovic * [BUG] No space between the detector details and the rule panel #522 [BUG] A rule flyout without references have an empty link #521 [FEATURE] Update header size to use euiTitle--small #520 Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * updated create detectors cypress specs Signed-off-by: Jovan Cvetkovic * refactored util methods into cypress commands Signed-off-by: Jovan Cvetkovic * refactored util methods into cypress commands Signed-off-by: Jovan Cvetkovic * cypress tests Signed-off-by: Jovan Cvetkovic * cypress tests wait interval updated to 400 Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic Signed-off-by: Amardeepsingh Siglani Co-authored-by: Amardeepsingh Siglani * added link to rules; simplified rule parsing (#571) Signed-off-by: Amardeepsingh Siglani * [FEATURE] Finding flyout loading state (#562) * [FEATURE] Set loading state for finding fly-out until the data is ready #559 Signed-off-by: Jovan Cvetkovic * fix tests Signed-off-by: Jovan Cvetkovic * fix tests Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic * Add correlation rule details into the finding details flyout #563 (#565) * Add correlation rule details into the finding details flyout #563 Signed-off-by: Jovan Cvetkovic * Add correlation rule details into the finding details flyout #563 Signed-off-by: Jovan Cvetkovic * fix tests Signed-off-by: Jovan Cvetkovic * Add correlation rule details into the finding details flyout #563 #565 Signed-off-by: Jovan Cvetkovic * Add correlation rule details into the finding details flyout #563 #565 Signed-off-by: Jovan Cvetkovic * Add correlation rule details into the finding details flyout #563 #565 Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic * UX improvements for correlation engine (#561) * passing props for date time filter (#551) Signed-off-by: Amardeepsingh Siglani * added loading state Signed-off-by: Amardeepsingh Siglani * ux improvements Signed-off-by: Amardeepsingh Siglani * transform vega-lite files for jest testing Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani * Add a details button to open the findings flyout from the correlations page. (#572) * Add a details button to open the findings flyout from the correlations page. #564 Signed-off-by: Jovan Cvetkovic * Add a details button to open the findings flyout from the correlations page. #564 Signed-off-by: Jovan Cvetkovic * [FEATURE] Add a details button to open the findings flyout from the correlations page. #564 Signed-off-by: Jovan Cvetkovic * fix tests Signed-off-by: Jovan Cvetkovic * code review Signed-off-by: Jovan Cvetkovic * code review Signed-off-by: Jovan Cvetkovic * [BUG] Wrong field mappings for the cloud trail logs #573 Signed-off-by: Jovan Cvetkovic * code review Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic * Wrong field mappings for the cloud trail logs (#574) * Wrong field mappings for the cloud trail logs #573 Signed-off-by: Jovan Cvetkovic * [BUG] Wrong field mappings for the cloud trail logs #573 Signed-off-by: Jovan Cvetkovic * [BUG] Wrong field mappings for the cloud trail logs #573 Signed-off-by: Jovan Cvetkovic * code review Signed-off-by: Jovan Cvetkovic * code review Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic * updated workflow file Signed-off-by: Amardeepsingh Siglani * updated osd branch for cypress tests Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Jovan Cvetkovic Signed-off-by: Amardeepsingh Siglani Co-authored-by: Jovan Cvetkovic --- .github/workflows/cypress-workflow.yml | 7 +- cypress.json | 1 + .../detector/create_dns_detector_data.json | 10 +- .../create_dns_detector_mappings_data.json | 6 +- .../detector/create_usb_detector_data.json | 6 +- .../create_usb_detector_mappings_data.json | 24 +- .../index/add_dns_index_data.json | 6 +- .../index/add_windows_index_data.json | 38 +- .../index/create_dns_settings.json | 6 +- .../index/create_windows_settings.json | 16 +- ... create_dns_rule_with_name_selection.json} | 6 +- .../create_dns_rule_with_type_selection.json | 26 + .../rule/create_network_rule.json | 4 +- .../rule/create_windows_usb_rule.json | 4 +- .../rule/sample_dns_field_mappings.json | 5 + cypress/fixtures/sample_alias_mappings.json | 12 +- cypress/fixtures/sample_detector.json | 14 +- .../fixtures/sample_dns_index_settings.json | 21 + cypress/fixtures/sample_document.json | 38 +- cypress/fixtures/sample_field_mappings.json | 24 +- cypress/fixtures/sample_index_settings.json | 33 - .../sample_windows_index_settings.json | 18 + cypress/integration/1_detectors.spec.js | 594 ++++---- cypress/integration/3_alerts.spec.js | 148 +- cypress/integration/4_findings.spec.js | 107 +- cypress/integration/5_integrations.spec.js | 4 +- cypress/support/commands.js | 7 +- cypress/support/helpers.js | 280 ++++ cypress/support/index.d.ts | 105 ++ cypress/support/typings.js | 22 + public/app.scss | 8 +- .../components/AlertFlyout/AlertFlyout.tsx | 61 +- public/pages/Correlations/Correlations.scss | 31 +- .../components/CorrelationGraph.tsx | 34 +- .../Correlations/components/FindingCard.scss | 9 + .../Correlations/components/FindingCard.tsx | 33 +- .../containers/CorrelationRules.tsx | 46 +- .../containers/CorrelationsContainer.tsx | 144 +- .../containers/CreateCorrelationRule.tsx | 170 ++- public/pages/Correlations/utils/constants.tsx | 1 + public/pages/Correlations/utils/helpers.tsx | 9 +- .../containers/ConfigureFieldMapping.tsx | 52 +- .../DetectorBasicDetailsView.tsx | 2 +- .../FieldMappingsView/FieldMappingsView.tsx | 13 +- .../DetectorDetails.test.tsx.snap | 4 +- .../DetectorDetailsView.test.tsx.snap | 4 +- .../FieldMappings/EditFieldMapping.tsx | 61 +- .../EditFieldMappings.test.tsx.snap | 1260 +++++++++-------- public/pages/Detectors/utils/helpers.ts | 21 + .../CorrelationsTable/CorrelationsTable.scss | 6 + .../CorrelationsTable/CorrelationsTable.tsx | 180 +++ .../components/FindingDetailsFlyout.tsx | 235 ++- .../FindingsTable/FindingsTable.tsx | 43 +- public/pages/Main/Main.tsx | 23 +- public/plugin.ts | 20 +- public/services/IndexPatternsService.ts | 1 - public/store/CorrelationsStore.ts | 75 +- public/store/DataStore.ts | 13 +- public/store/FindingsStore.ts | 148 ++ public/utils/constants.ts | 9 +- types/Correlations.ts | 17 +- 61 files changed, 2594 insertions(+), 1731 deletions(-) rename cypress/fixtures/integration_tests/rule/{create_dns_rule.json => create_dns_rule_with_name_selection.json} (72%) create mode 100644 cypress/fixtures/integration_tests/rule/create_dns_rule_with_type_selection.json create mode 100644 cypress/fixtures/integration_tests/rule/sample_dns_field_mappings.json create mode 100644 cypress/fixtures/sample_dns_index_settings.json delete mode 100644 cypress/fixtures/sample_index_settings.json create mode 100644 cypress/fixtures/sample_windows_index_settings.json create mode 100644 cypress/support/helpers.js create mode 100644 public/pages/Correlations/components/FindingCard.scss create mode 100644 public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss create mode 100644 public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx create mode 100644 public/store/FindingsStore.ts diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 84d3739b0..4ab2e45d1 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -7,7 +7,7 @@ on: branches: - "*" env: - OPENSEARCH_DASHBOARDS_VERSION: '2.x' + OPENSEARCH_DASHBOARDS_VERSION: '2.7.0' OPENSEARCH_VERSION: '2.7.0-SNAPSHOT' SECURITY_ANALYTICS_BRANCH: '2.x' jobs: @@ -94,12 +94,12 @@ jobs: # Window is slow so wait longer - name: Sleep until OSD server starts - windows if: ${{ matrix.os == 'windows-latest' }} - run: Start-Sleep -s 400 + run: Start-Sleep -s 450 shell: powershell - name: Sleep until OSD server starts - non-windows if: ${{ matrix.os != 'windows-latest' }} - run: sleep 300 + run: sleep 450 shell: bash - name: Install Cypress @@ -132,6 +132,7 @@ jobs: working-directory: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin command: yarn run cypress run wait-on: 'http://localhost:5601' + wait-on-timeout: 300 browser: chrome env: CYPRESS_CACHE_FOLDER: ${{ matrix.cypress_cache_folder }} diff --git a/cypress.json b/cypress.json index 48f248ec8..705b14add 100644 --- a/cypress.json +++ b/cypress.json @@ -5,6 +5,7 @@ "requestTimeout": 300000, "responseTimeout": 300000, "baseUrl": "http://localhost:5601", + "retries": 1, "env": { "opensearch_url": "localhost:9200", "opensearch_dashboards": "http://localhost:5601", diff --git a/cypress/fixtures/integration_tests/detector/create_dns_detector_data.json b/cypress/fixtures/integration_tests/detector/create_dns_detector_data.json index 276c56db2..e2f5447b8 100644 --- a/cypress/fixtures/integration_tests/detector/create_dns_detector_data.json +++ b/cypress/fixtures/integration_tests/detector/create_dns_detector_data.json @@ -27,19 +27,19 @@ "triggers": [ { "name": "DNS name alert", - "sev_levels": ["low"], - "tags": ["dns.low"], + "sev_levels": ["high"], + "tags": ["dns.high"], "actions": [ { "id": "", - "name": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: Cypress DNS Detector", + "name": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: Cypress DNS Detector", "destination_id": "", "subject_template": { - "source": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: Cypress DNS Detector", + "source": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: Cypress DNS Detector", "lang": "mustache" }, "message_template": { - "source": "Triggered alert condition: \nSeverity: 1 (Highest)\nThreat detector: Cypress DNS Detector\nDescription: Detects DNS names.\nDetector data sources:\n\tdns", + "source": "Triggered alert condition: \nSeverity: 1 (Highest) \nThreat detector: Cypress DNS Detector\nDescription: Detects DNS names.\nDetector data sources:\n\tdns", "lang": "mustache" }, "throttle_enabled": false, diff --git a/cypress/fixtures/integration_tests/detector/create_dns_detector_mappings_data.json b/cypress/fixtures/integration_tests/detector/create_dns_detector_mappings_data.json index e4056d577..6f9f869ea 100644 --- a/cypress/fixtures/integration_tests/detector/create_dns_detector_mappings_data.json +++ b/cypress/fixtures/integration_tests/detector/create_dns_detector_mappings_data.json @@ -2,15 +2,15 @@ "properties": { "dns-answers-type": { "type": "alias", - "path": "DnsAnswerType" + "path": "dns.answers.type" }, "dns-question-name": { "type": "alias", - "path": "DnsQuestionName" + "path": "dns.question.name" }, "dns-question-registered_domain": { "type": "alias", - "path": "DnsQuestionRegisteredDomain" + "path": "dns.question.registered_domain" } } } diff --git a/cypress/fixtures/integration_tests/detector/create_usb_detector_data.json b/cypress/fixtures/integration_tests/detector/create_usb_detector_data.json index 07392d280..b68c08406 100644 --- a/cypress/fixtures/integration_tests/detector/create_usb_detector_data.json +++ b/cypress/fixtures/integration_tests/detector/create_usb_detector_data.json @@ -27,7 +27,7 @@ "triggers": [ { "name": "USB plugged in alert", - "sev_levels": ["low"], + "sev_levels": ["high"], "tags": ["windows.usb"], "actions": [ { @@ -35,11 +35,11 @@ "name": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: USB Detector", "destination_id": "", "subject_template": { - "source": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: USB Detector", + "source": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: USB Detector", "lang": "mustache" }, "message_template": { - "source": "Triggered alert condition: \nSeverity: 1 (Highest)\nThreat detector: USB Detector\nDescription: Detect USB plugged in.\nDetector data sources:\n\twindows", + "source": "Triggered alert condition: \nSeverity: 1 (Highest) \nThreat detector: USB Detector\nDescription: Detect USB plugged in.\nDetector data sources:\n\twindows", "lang": "mustache" }, "throttle_enabled": false, diff --git a/cypress/fixtures/integration_tests/detector/create_usb_detector_mappings_data.json b/cypress/fixtures/integration_tests/detector/create_usb_detector_mappings_data.json index da81361fe..0cad430bc 100644 --- a/cypress/fixtures/integration_tests/detector/create_usb_detector_mappings_data.json +++ b/cypress/fixtures/integration_tests/detector/create_usb_detector_mappings_data.json @@ -1,28 +1,12 @@ { "properties": { - "event_uid": { + "winlog-event_id": { "type": "alias", - "path": "EventID" + "path": "winlog.event_id" }, - "windows-event_data-CommandLine": { + "winlog-provider_name": { "type": "alias", - "path": "CommandLine" - }, - "windows-hostname": { - "type": "alias", - "path": "HostName" - }, - "windows-message": { - "type": "alias", - "path": "Message" - }, - "windows-provider-name": { - "type": "alias", - "path": "Provider_Name" - }, - "windows-servicename": { - "type": "alias", - "path": "ServiceName" + "path": "winlog.provider_name" } } } diff --git a/cypress/fixtures/integration_tests/index/add_dns_index_data.json b/cypress/fixtures/integration_tests/index/add_dns_index_data.json index 35077a0f5..901c7c3e3 100644 --- a/cypress/fixtures/integration_tests/index/add_dns_index_data.json +++ b/cypress/fixtures/integration_tests/index/add_dns_index_data.json @@ -1,5 +1,5 @@ { - "DnsAnswerType": "QWE", - "DnsQuestionRegisteredDomain": "EC2AMAZ-EPWO7HKA", - "DnsQuestionName": "QWE" + "dns.answers.type": "AnswerType", + "dns.question.registered_domain": "EC2AMAZ-EPWO7HKA", + "dns.question.name": "QuestionName" } diff --git a/cypress/fixtures/integration_tests/index/add_windows_index_data.json b/cypress/fixtures/integration_tests/index/add_windows_index_data.json index c449c7584..f8b8b4e2e 100644 --- a/cypress/fixtures/integration_tests/index/add_windows_index_data.json +++ b/cypress/fixtures/integration_tests/index/add_windows_index_data.json @@ -1,39 +1,3 @@ { - "EventTime": "2020-02-04T14:59:39.343541+00:00", - "HostName": "EC2AMAZ-EPO7HKA", - "Keywords": "9223372036854775808", - "SeverityValue": 2, - "Severity": "ERROR", - "EventID": 2003, - "SourceName": "Microsoft-Windows-Sysmon", - "ProviderGuid": "{5770385F-C22A-43E0-BF4C-06F5698FFBD9}", - "Version": 5, - "TaskValue": 22, - "OpcodeValue": 0, - "RecordNumber": 9532, - "ExecutionProcessID": 1996, - "ExecutionThreadID": 2616, - "Channel": "Microsoft-Windows-Sysmon/Operational", - "Domain": "NT AUTHORITY", - "AccountName": "SYSTEM", - "UserID": "S-1-5-18", - "AccountType": "User", - "Message": "Dns query:\r\nRuleName: \r\nUtcTime: 2020-02-04 14:59:38.349\r\nProcessGuid: {b3c285a4-3cda-5dc0-0000-001077270b00}\r\nProcessId: 1904\r\nQueryName: EC2AMAZ-EPO7HKA\r\nQueryStatus: 0\r\nQueryResults: 172.31.46.38;\r\nImage: C:\\Program Files\\nxlog\\nxlog.exe", - "Category": "Dns query (rule: DnsQuery)", - "Opcode": "Info", - "UtcTime": "2020-02-04 14:59:38.349", - "ProcessGuid": "{b3c285a4-3cda-5dc0-0000-001077270b00}", - "ProcessId": "1904", - "QueryName": "EC2AMAZ-EPO7HKA", - "QueryStatus": "0", - "QueryResults": "172.31.46.38;", - "Image": "C:\\Program Files\\nxlog\\regsvr32.exe", - "EventReceivedTime": "2020-02-04T14:59:40.780905+00:00", - "SourceModuleName": "in", - "SourceModuleType": "im_msvistalog", - "CommandLine": "eachtest", - "Initiated": "true", - "Provider_Name": "Service_ws_Control_ws_Manager", - "TargetObject": "\\SOFTWARE\\Microsoft\\Office\\Outlook\\Security", - "EventType": "SetValue" + "winlog.event_id": "2003" } diff --git a/cypress/fixtures/integration_tests/index/create_dns_settings.json b/cypress/fixtures/integration_tests/index/create_dns_settings.json index 126659dc6..970a6089a 100644 --- a/cypress/fixtures/integration_tests/index/create_dns_settings.json +++ b/cypress/fixtures/integration_tests/index/create_dns_settings.json @@ -1,13 +1,13 @@ { "mappings": { "properties": { - "DnsAnswerType": { + "dns.answers.type": { "type": "text" }, - "DnsQuestionRegisteredDomain": { + "dns.question.name": { "type": "text" }, - "DnsQuestionName": { + "dns.question.registered_domain": { "type": "text" } } diff --git a/cypress/fixtures/integration_tests/index/create_windows_settings.json b/cypress/fixtures/integration_tests/index/create_windows_settings.json index f794e671e..480f63ba1 100644 --- a/cypress/fixtures/integration_tests/index/create_windows_settings.json +++ b/cypress/fixtures/integration_tests/index/create_windows_settings.json @@ -1,22 +1,10 @@ { "mappings": { "properties": { - "CommandLine": { - "type": "text" - }, - "EventID": { + "winlog.event_id": { "type": "integer" }, - "HostName": { - "type": "text" - }, - "Message": { - "type": "text" - }, - "Provider_Name": { - "type": "text" - }, - "ServiceName": { + "winlog.provider_name": { "type": "text" } } diff --git a/cypress/fixtures/integration_tests/rule/create_dns_rule.json b/cypress/fixtures/integration_tests/rule/create_dns_rule_with_name_selection.json similarity index 72% rename from cypress/fixtures/integration_tests/rule/create_dns_rule.json rename to cypress/fixtures/integration_tests/rule/create_dns_rule_with_name_selection.json index 5e38ab4bd..7c1e7c8fb 100644 --- a/cypress/fixtures/integration_tests/rule/create_dns_rule.json +++ b/cypress/fixtures/integration_tests/rule/create_dns_rule_with_name_selection.json @@ -12,12 +12,12 @@ ], "tags": [ { - "value": "dns.low" + "value": "dns.high" } ], "log_source": "", - "detection": "selection:\n query:\n - QWE\n - ASD\n - YXC\ncondition: selection", - "level": "low", + "detection": "selection:\n dns-question-name:\n - QuestionName\ncondition: selection", + "level": "high", "false_positives": [ { "value": "" diff --git a/cypress/fixtures/integration_tests/rule/create_dns_rule_with_type_selection.json b/cypress/fixtures/integration_tests/rule/create_dns_rule_with_type_selection.json new file mode 100644 index 000000000..e447a30d5 --- /dev/null +++ b/cypress/fixtures/integration_tests/rule/create_dns_rule_with_type_selection.json @@ -0,0 +1,26 @@ +{ + "id": "25b9c01c-350d-4b95-bed1-836d04a4f325", + "category": "dns", + "title": "Cypress DNS Type Rule", + "description": "Detects DNS type as QWE", + "status": "experimental", + "author": "Cypress Tests", + "references": [ + { + "value": "" + } + ], + "tags": [ + { + "value": "dns.high" + } + ], + "log_source": "", + "detection": "selection:\n dns-answers-type:\n - AnswerType\ncondition: selection", + "level": "high", + "false_positives": [ + { + "value": "" + } + ] +} diff --git a/cypress/fixtures/integration_tests/rule/create_network_rule.json b/cypress/fixtures/integration_tests/rule/create_network_rule.json index 43e69cff4..2937fc79d 100644 --- a/cypress/fixtures/integration_tests/rule/create_network_rule.json +++ b/cypress/fixtures/integration_tests/rule/create_network_rule.json @@ -12,12 +12,12 @@ ], "tags": [ { - "value": "network.low" + "value": "network.high" } ], "log_source": "", "detection": "selection:\n keywords:\n - erase\n - delete\n - YXC\ncondition: selection", - "level": "low", + "level": "high", "false_positives": [ { "value": "" diff --git a/cypress/fixtures/integration_tests/rule/create_windows_usb_rule.json b/cypress/fixtures/integration_tests/rule/create_windows_usb_rule.json index 20f59799a..fb14944c6 100644 --- a/cypress/fixtures/integration_tests/rule/create_windows_usb_rule.json +++ b/cypress/fixtures/integration_tests/rule/create_windows_usb_rule.json @@ -16,8 +16,8 @@ } ], "log_source": "", - "detection": "selection:\n EventID:\n - 2003\n - 2100\n - 2102\ncondition: selection", - "level": "low", + "detection": "selection:\n winlog-event_id:\n - 2003\n - 2100\n - 2102\ncondition: selection", + "level": "high", "false_positives": [ { "value": "" diff --git a/cypress/fixtures/integration_tests/rule/sample_dns_field_mappings.json b/cypress/fixtures/integration_tests/rule/sample_dns_field_mappings.json new file mode 100644 index 000000000..b2f9b698e --- /dev/null +++ b/cypress/fixtures/integration_tests/rule/sample_dns_field_mappings.json @@ -0,0 +1,5 @@ +{ + "dns-question-registered_domain": "dns.question.registered_domain", + "dns-question-name": "dns.question.name", + "dns-answers-type": "dns.answers.type" +} diff --git a/cypress/fixtures/sample_alias_mappings.json b/cypress/fixtures/sample_alias_mappings.json index cf08cc696..e0a1a5f88 100644 --- a/cypress/fixtures/sample_alias_mappings.json +++ b/cypress/fixtures/sample_alias_mappings.json @@ -1,16 +1,8 @@ { "properties": { - "source_ip": { + "winlog-event_id": { "type": "alias", - "path": "src_ip" - }, - "windows-event_data-CommandLine": { - "path": "CommandLine", - "type": "alias" - }, - "event_uid": { - "path": "EventID", - "type": "alias" + "path": "winlog.event_id" } } } diff --git a/cypress/fixtures/sample_detector.json b/cypress/fixtures/sample_detector.json index 67eca1110..a17853598 100644 --- a/cypress/fixtures/sample_detector.json +++ b/cypress/fixtures/sample_detector.json @@ -20,14 +20,18 @@ "id": "1a4bd6e3-4c6e-405d-a9a3-53a116e341d4" } ], - "custom_rules": [] + "custom_rules": [ + { + "id": "" + } + ] } } ], "triggers": [ { "name": "sample_alert_condition", - "sev_levels": [], + "sev_levels": ["high"], "tags": [], "actions": [ { @@ -35,11 +39,11 @@ "name": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: sample_detector", "destination_id": "", "subject_template": { - "source": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: sample_detector", + "source": "Triggered alert condition: - Severity: 1 (Highest) - Threat detector: sample_detector", "lang": "mustache" }, "message_template": { - "source": "Triggered alert condition: \nSeverity: 1 (Highest)\nThreat detector: sample_detector\nDescription: Description for sample_detector.\nDetector data sources:\n\twindows", + "source": "Triggered alert condition: \nSeverity: 1 (Highest) \nThreat detector: sample_detector\nDescription: Description for sample_detector.\nDetector data sources:\n\twindows", "lang": "mustache" }, "throttle_enabled": false, @@ -51,7 +55,7 @@ ], "types": ["windows"], "severity": "4", - "ids": ["1a4bd6e3-4c6e-405d-a9a3-53a116e341d4"] + "ids": [] } ] } diff --git a/cypress/fixtures/sample_dns_index_settings.json b/cypress/fixtures/sample_dns_index_settings.json new file mode 100644 index 000000000..02b01e771 --- /dev/null +++ b/cypress/fixtures/sample_dns_index_settings.json @@ -0,0 +1,21 @@ +{ + "mappings": { + "properties": { + "dns.question.name": { + "type": "text" + }, + "dns.answers.type": { + "type": "text" + }, + "dns.question.registered_domain": { + "type": "text" + } + } + }, + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "1" + } + } +} diff --git a/cypress/fixtures/sample_document.json b/cypress/fixtures/sample_document.json index d23b31895..521d2f677 100644 --- a/cypress/fixtures/sample_document.json +++ b/cypress/fixtures/sample_document.json @@ -1,39 +1,3 @@ { - "EventTime": "2020-02-04T14:59:39.343541+00:00", - "HostName": "EC2AMAZ-EPO7HKA", - "Keywords": "9223372036854775808", - "SeverityValue": 2, - "Severity": "INFO", - "EventID": 2003, - "SourceName": "Microsoft-Windows-Sysmon", - "ProviderGuid": "{5770385F-C22A-43E0-BF4C-06F5698FFBD9}", - "Version": 5, - "TaskValue": 22, - "OpcodeValue": 0, - "RecordNumber": 9532, - "ExecutionProcessID": 1996, - "ExecutionThreadID": 2616, - "Channel": "Microsoft-Windows-Sysmon/Operational", - "Domain": "NT AUTHORITY", - "AccountName": "SYSTEM", - "UserID": "S-1-5-18", - "AccountType": "User", - "Message": "Dns query:\r\nRuleName: \r\nUtcTime: 2020-02-04 14:59:38.349\r\nProcessGuid: {b3c285a4-3cda-5dc0-0000-001077270b00}\r\nProcessId: 1904\r\nQueryName: EC2AMAZ-EPO7HKA\r\nQueryStatus: 0\r\nQueryResults: 172.31.46.38;\r\nImage: C:\\Program Files\\nxlog\\nxlog.exe", - "Category": "Dns query (rule: DnsQuery)", - "Opcode": "Info", - "UtcTime": "2020-02-04 14:59:38.349", - "ProcessGuid": "{b3c285a4-3cda-5dc0-0000-001077270b00}", - "ProcessId": "1904", - "QueryName": "EC2AMAZ-EPO7HKA", - "QueryStatus": "0", - "QueryResults": "172.31.46.38;", - "Image": "C:\\Program Files\\nxlog\\regsvr32.exe", - "EventReceivedTime": "2020-02-04T14:59:40.780905+00:00", - "SourceModuleName": "in", - "SourceModuleType": "im_msvistalog", - "CommandLine": "eachtest", - "Initiated": "true", - "Provider_Name": "Microsoft-Windows-Kernel-General", - "TargetObject": "\\SOFTWARE\\Microsoft\\Office\\Outlook\\Security", - "EventType": "SetValue" + "winlog.event_id": 2003 } diff --git a/cypress/fixtures/sample_field_mappings.json b/cypress/fixtures/sample_field_mappings.json index 6e8d728fe..ff4eb1830 100644 --- a/cypress/fixtures/sample_field_mappings.json +++ b/cypress/fixtures/sample_field_mappings.json @@ -1,27 +1,7 @@ { "properties": { - "windows-hostname": { - "type": "alias", - "path": "HostName" - }, - "windows-message": { - "type": "alias", - "path": "Message" - }, - "windows-provider-name": { - "type": "alias", - "path": "Provider_Name" - }, - "windows-servicename": { - "type": "alias", - "path": "ServiceName" - }, - "windows-event_data-CommandLine": { - "path": "CommandLine", - "type": "alias" - }, - "event_uid": { - "path": "EventID", + "winlog-event_id": { + "path": "winlog.event_id", "type": "alias" } } diff --git a/cypress/fixtures/sample_index_settings.json b/cypress/fixtures/sample_index_settings.json deleted file mode 100644 index a8a5294a7..000000000 --- a/cypress/fixtures/sample_index_settings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "mappings": { - "properties": { - "CommandLine": { - "type": "text" - }, - "EventID": { - "type": "integer" - }, - "HostName": { - "type": "text" - }, - "Message": { - "type": "text" - }, - "Provider_Name": { - "type": "text" - }, - "ServiceName": { - "type": "text" - }, - "DnsQuestionName": { - "type": "text" - } - } - }, - "settings": { - "index": { - "number_of_shards": "1", - "number_of_replicas": "1" - } - } -} diff --git a/cypress/fixtures/sample_windows_index_settings.json b/cypress/fixtures/sample_windows_index_settings.json new file mode 100644 index 000000000..480f63ba1 --- /dev/null +++ b/cypress/fixtures/sample_windows_index_settings.json @@ -0,0 +1,18 @@ +{ + "mappings": { + "properties": { + "winlog.event_id": { + "type": "integer" + }, + "winlog.provider_name": { + "type": "text" + } + } + }, + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "1" + } + } +} diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 81a33f2cc..59c2214e6 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -4,153 +4,236 @@ */ import { OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; -import sample_index_settings from '../fixtures/sample_index_settings.json'; -import dns_rule_data from '../fixtures/integration_tests/rule/create_dns_rule.json'; - -const testMappings = { - properties: { - 'dns-question-name': { - type: 'alias', - path: 'DnsQuestionName', - }, - }, -}; - -const cypressDNSRule = dns_rule_data.title; +import sample_windows_index_settings from '../fixtures/sample_windows_index_settings.json'; +import sample_dns_index_settings from '../fixtures/sample_dns_index_settings.json'; +import dns_name_rule_data from '../fixtures/integration_tests/rule/create_dns_rule_with_name_selection.json'; +import dns_type_rule_data from '../fixtures/integration_tests/rule/create_dns_rule_with_type_selection.json'; +import dns_mapping_fields from '../fixtures/integration_tests/rule/sample_dns_field_mappings.json'; +import _ from 'lodash'; +import { getMappingFields } from '../../public/pages/Detectors/utils/helpers'; const cypressIndexDns = 'cypress-index-dns'; const cypressIndexWindows = 'cypress-index-windows'; const detectorName = 'test detector'; -const createDetector = (detectorName, dataSource, expectFailure) => { - // Locate Create detector button click to start - cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true }); +const cypressDNSRule = dns_name_rule_data.title; - // Check to ensure process started - cy.waitForPageLoad('create-detector', { - contains: 'Define detector', - }); +const getNameField = () => cy.getInputByPlaceholder('Enter a name for the detector.'); - // Enter a name for the detector in the appropriate input - cy.get(`input[placeholder="Enter a name for the detector."]`).focus().realType(detectorName); +const getNextButton = () => cy.getButtonByText('Next'); - // Select our pre-seeded data source (check cypressIndexDns) - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .find('input') - .focus() - .realType(dataSource); +const getCreateDetectorButton = () => cy.getButtonByText('Create detector'); + +const selectDnsLogType = () => cy.getRadioButtonById('dns').click({ force: true }); - cy.intercept({ - pathname: '/_plugins/_security_analytics/rules/_search', - query: { - prePackaged: 'true', - }, - }).as('getSigmaRules'); +const validateAlertPanel = (alertName) => + cy + .getElementByText('.euiTitle', 'Alert triggers') + .parentsUntil('.euiPanel') + .siblings() + .eq(2) + .within(() => cy.getElementByText('button', alertName)); - // Select threat detector type (Windows logs) - cy.get(`input[id="dns"]`).click({ force: true }); +const dataSourceLabel = 'Select or input source indexes or index patterns'; - cy.wait('@getSigmaRules').then(() => { - // Open Detection rules accordion - cy.get('[data-test-subj="detection-rules-btn"]').click({ force: true, timeout: 5000 }); +const getDataSourceField = () => cy.getFieldByLabel(dataSourceLabel); - cy.contains('table tr', 'DNS', { - timeout: 120000, +const openDetectorDetails = (detectorName) => { + cy.getInputByPlaceholder('Search threat detectors').type(`${detectorName}`).pressEnterKey(); + cy.getElementByText('.euiTableCellContent button', detectorName).click(); +}; + +const validateFieldMappingsTable = (message = '') => { + cy.wait('@getMappingsView').then((interception) => { + cy.wait(10000).then(() => { + cy.get('.reviewFieldMappings').should('be.visible'); + const properties = interception.response.body.response.properties; + const unmapped_field_aliases = interception.response.body.response.unmapped_field_aliases.map( + (field) => [field] + ); + + Cypress.log({ + message: `Validate table data - ${message}`, + }); + if (_.isEmpty(properties)) { + validatePendingFieldMappingsPanel(unmapped_field_aliases); + } else { + let items = getMappingFields(properties, [], ''); + items = items.map((item) => [item.ruleFieldName, item.logFieldName]); + validateAutomaticFieldMappingsPanel(items); + } }); }); +}; - // Check that correct page now showing - cy.contains('Configure field mapping'); +const editDetectorDetails = (detectorName, panelTitle) => { + cy.urlShouldContain('detector-details').then(() => { + cy.getElementByText('.euiTitle', detectorName); + cy.getElementByText('.euiPanel .euiTitle', panelTitle); + cy.getElementByText('.euiPanel .euiTitle', panelTitle) + .parent() + .siblings() + .within(() => cy.get('button').contains('Edit').click()); + }); +}; - if (!expectFailure) { - // Select appropriate names to map fields to - for (let field_name in testMappings.properties) { - const mappedTo = testMappings.properties[field_name].path; +const validateAutomaticFieldMappingsPanel = (mappings) => + cy.get('.editFieldMappings').within(() => { + cy.get('.euiAccordion__triggerWrapper button').then(($btn) => { + cy.get($btn).contains(`Automatically mapped fields (${mappings.length})`); + + // first check if the accordion is expanded, if not than expand the accordion + if ($btn[0].getAttribute('aria-expanded') === 'false') { + cy.get($btn[0]) + .click() + .then(() => { + cy.getElementByTestSubject('auto-mapped-fields-table') + .find('.euiBasicTable') + .validateTable(mappings); + }); + } + }); + }); - cy.contains('tr', field_name).within(() => { - cy.get(`[data-test-subj="detector-field-mappings-select"]`).click().type(mappedTo); +const validatePendingFieldMappingsPanel = (mappings) => { + cy.get('.editFieldMappings').within(() => { + // Pending field mappings + cy.getElementByText('.euiTitle', 'Pending field mappings') + .parents('.euiPanel') + .within(() => { + cy.getElementByTestSubject('pending-mapped-fields-table') + .find('.euiBasicTable') + .validateTable(mappings); }); - } - } + }); +}; - // Click Next button to continue - cy.get('button').contains('Next').click({ force: true }); +const createDetector = (detectorName, dataSource, expectFailure) => { + getCreateDetectorButton().click({ force: true }); - // Check that correct page now showing - cy.contains('Set up alert triggers'); + // TEST DETAILS PAGE + getNameField().type(detectorName); + getDataSourceField().selectComboboxItem(dataSource); - // Type name of new trigger - cy.get(`input[placeholder="Enter a name to describe the alert condition"]`) - .focus() - .realType('test_trigger'); + selectDnsLogType(); + + cy.getElementByText('.euiAccordion .euiTitle', 'Detection rules (14 selected)') + .click({ force: true, timeout: 5000 }) + .then(() => cy.contains('.euiTable .euiTableRow', 'Dns')); - // Type in (or select) tags for the alert condition - cy.get(`[data-test-subj="alert-tags-combo-box"]`) + cy.getElementByText('.euiAccordion .euiTitle', 'Configure field mapping - optional'); + cy.get('[aria-controls="mappedTitleFieldsAccordion"]').then(($btn) => { + // first check if the accordion is expanded, if not than expand the accordion + if ($btn && $btn[0] && $btn[0].getAttribute('aria-expanded') === 'false') { + $btn[0].click(); + } + }); + + // go to the alerts page + getNextButton().click({ force: true }); + + // TEST ALERTS PAGE + cy.getElementByText('.euiTitle.euiTitle--medium', 'Set up alert triggers'); + cy.getInputByPlaceholder('Enter a name to describe the alert condition').type('test_trigger'); + cy.getElementByTestSubject('alert-tags-combo-box') + .type(`attack.defense_evasion{enter}`) .find('input') .focus() - .realType('attack.defense_evasion') - .realPress('Enter'); + .blur(); - // Select applicable severity levels - cy.get(`[data-test-subj="security-levels-combo-box"]`).click({ force: true }); - cy.contains('1 (Highest)').click({ force: true }); + cy.getFieldByLabel('Specify alert severity').selectComboboxItem('1 (Highest)'); - // Continue to next page - cy.contains('Next').click({ force: true }); + // go to review page + getNextButton().click({ force: true }); - // Confirm page is reached - cy.contains('Review and create'); + // TEST REVIEW AND CREATE PAGE + cy.getElementByText('.euiTitle', 'Review and create'); + cy.getElementByText('.euiTitle', 'Detector details'); + cy.getElementByText('.euiTitle', 'Field mapping'); + cy.getElementByText('.euiTitle', 'Alert triggers'); - // Confirm field mappings registered - cy.contains('Field mapping'); + cy.validateDetailsItem('Detector name', detectorName); + cy.validateDetailsItem('Description', '-'); + cy.validateDetailsItem('Detector schedule', 'Every 1 minute'); + cy.validateDetailsItem('Detection rules', '14'); + cy.validateDetailsItem('Created at', '-'); + cy.validateDetailsItem('Last updated time', '-'); + cy.validateDetailsItem('Detector dashboard', 'Not available for this log type'); if (!expectFailure) { - for (let field in testMappings.properties) { - const mappedTo = testMappings.properties[field].path; - - cy.contains(field); - cy.contains(mappedTo); + let fields = []; + for (let field in dns_mapping_fields) { + fields.push([field, dns_mapping_fields[field]]); } + cy.getElementByText('.euiTitle', 'Field mapping') + .parentsUntil('.euiPanel') + .siblings() + .eq(2) + .validateTable(fields); } - // Confirm entries user has made - cy.contains('Detector details'); - cy.contains(detectorName); - cy.contains('dns'); - cy.contains('test_trigger'); + validateAlertPanel('test_trigger'); - // Create the detector - cy.get('button').contains('Create').click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: detectorName, - }); + cy.intercept('POST', '/mappings').as('createMappingsRequest'); + cy.intercept('POST', '/detectors').as('createDetectorRequest'); - cy.contains('Attempting to create the detector.'); + // create the detector + cy.getElementByText('button', 'Create').click({ force: true }); - // Confirm detector active - cy.contains(detectorName); - cy.contains('Active'); + // TEST DETECTOR DETAILS PAGE + cy.wait('@createMappingsRequest'); if (!expectFailure) { - cy.contains('Actions'); + cy.wait('@createDetectorRequest').then((interceptor) => { + const detectorId = interceptor.response.body.response._id; + + cy.url() + .should('contain', detectorId) + .then(() => { + cy.getElementByText('.euiCallOut', `Detector created successfully: ${detectorName}`); + + // Confirm detector state + cy.getElementByText('.euiTitle', detectorName); + cy.getElementByText('.euiHealth', 'Active').then(() => { + cy.validateDetailsItem('Detector name', detectorName); + cy.validateDetailsItem('Description', '-'); + cy.validateDetailsItem('Detector schedule', 'Every 1 minute'); + cy.validateDetailsItem('Detection rules', '14'); + cy.validateDetailsItem('Detector dashboard', 'Not available for this log type'); + + cy.wait(5000); // waiting for the page to be reloaded after pushing detector id into route + cy.getElementByText('button.euiTab', 'Alert triggers').should('be.visible').click(); + validateAlertPanel('test_trigger'); + + cy.intercept('GET', '/mappings?indexName').as('getMappingFields'); + cy.getElementByText('button.euiTab', 'Field mappings').should('be.visible').click(); + if (!expectFailure) { + let fields = []; + for (let field in dns_mapping_fields) { + fields.push([field, dns_mapping_fields[field]]); + } + cy.wait('@getMappingFields'); + cy.wait(2000); + cy.getElementByText('.euiTitle', 'Field mapping') + .parentsUntil('.euiPanel') + .siblings() + .eq(2) + .validateTable(fields); + } + }); + }); + }); } - - cy.contains('Detector configuration'); - cy.contains('Field mappings'); - cy.contains('Alert triggers'); - cy.contains('Detector details'); - cy.contains('Created at'); - cy.contains('Last updated time'); }; describe('Detectors', () => { before(() => { cy.cleanUpTests(); - cy.createIndex(cypressIndexWindows, sample_index_settings); + cy.createIndex(cypressIndexWindows, sample_windows_index_settings); // Create test index - cy.createIndex(cypressIndexDns, sample_index_settings).then(() => + cy.createIndex(cypressIndexDns, sample_dns_index_settings).then(() => cy .request('POST', '_plugins/_security_analytics/rules/_search?prePackaged=true', { from: 0, @@ -165,45 +248,69 @@ describe('Detectors', () => { .should('have.property', 'status', 200) ); - cy.createRule(dns_rule_data); + cy.createRule(dns_name_rule_data); + cy.createRule(dns_type_rule_data); }); beforeEach(() => { cy.intercept('/detectors/_search').as('detectorsSearch'); - // Visit Detectors page + + // Visit Detectors page before any test cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); - - // Check that correct page is showing - cy.waitForPageLoad('detectors', { - contains: 'Threat detectors', - }); }); - it('...should show mappings warning', () => { - // Locate Create detector button click to start - cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true }); + it('...should validate form', () => { + getCreateDetectorButton().click({ force: true }); - // Check to ensure process started - cy.waitForPageLoad('create-detector', { - contains: 'Define detector', - }); + getNextButton().should('be.disabled'); - // Select our pre-seeded data source (check cypressIndexDns) - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .find('input') - .focus() - .realType(cypressIndexDns); + getNameField().should('be.empty'); + getNameField().type('text').focus().blur(); + + getNameField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains( + 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.' + ); - // Select threat detector type (Windows logs) - cy.get(`input[id="dns"]`).click({ force: true }); + getNameField() + .type(' and more text') + .focus() + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .should('not.exist'); + getNextButton().should('be.disabled'); - // Select our pre-seeded data source (check cypressIndexDns) - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .find('input') + getDataSourceField() .focus() - .realType(cypressIndexWindows) - .realPress('Enter'); + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Select an input source'); + getNextButton().should('be.disabled'); + + getDataSourceField().selectComboboxItem(cypressIndexDns); + getDataSourceField() + .focus() + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + getNextButton().should('not.be.disabled'); + }); + + it('...should show mappings warning', () => { + getCreateDetectorButton().click({ force: true }); + + getDataSourceField().selectComboboxItem(cypressIndexDns); + + selectDnsLogType(); + + getDataSourceField().selectComboboxItem(cypressIndexWindows); + getDataSourceField().focus().blur(); cy.get('.euiCallOut') .should('be.visible') @@ -212,201 +319,136 @@ describe('Detectors', () => { ); }); - it('...can be created', () => { - createDetector(detectorName, cypressIndexDns, false); - cy.contains('Detector created successfully'); - }); - it('...can fail creation', () => { createDetector(`${detectorName}_fail`, '.kibana_1', true); - cy.contains('Create detector failed.'); + cy.getElementByText('.euiCallOut', 'Create detector failed.'); + }); + + it('...can be created', () => { + createDetector(detectorName, cypressIndexDns, false); + cy.getElementByText('.euiCallOut', 'Detector created successfully'); }); it('...basic details can be edited', () => { - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: detectorName, - }); + cy.intercept('GET', '/indices').as('getIndices'); + openDetectorDetails(detectorName); - // Click "Edit" button in detector details - cy.get(`[data-test-subj="edit-detector-basic-details"]`).click({ force: true }); + editDetectorDetails(detectorName, 'Detector details'); - // Confirm arrival at "Edit detector details" page - cy.waitForPageLoad('edit-detector-details', { - contains: 'Edit detector details', + cy.urlShouldContain('edit-detector-details').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector details'); }); - // Change detector name - cy.get(`input[placeholder="Enter a name for the detector."]`) - .realClick() - .ospClear() - .realType('test detector edited'); - - // Change detector description - cy.get(`[data-test-subj="define-detector-detector-description"]`) - .focus() - .realType('Edited description'); + cy.wait('@getIndices'); + getNameField().type('{selectall}{backspace}').type('test detector edited'); + cy.getTextareaByLabel('Description - optional').type('Edited description'); - // Change input source - cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true }); - cy.get(`[data-test-subj="define-detector-select-data-source"]`).type( - `${cypressIndexWindows}{enter}` - ); + getDataSourceField().clearCombobox(); + getDataSourceField().selectComboboxItem(cypressIndexWindows); - // Change detector scheduling - cy.get(`[data-test-subj="detector-schedule-number-select"]`).ospClear().focus().realType('10'); - cy.get(`[data-test-subj="detector-schedule-unit-select"]`).select('Hours'); + cy.getFieldByLabel('Run every').type('{selectall}{backspace}').type('10'); + cy.getFieldByLabel('Run every', 'select').select('Hours'); - // Save changes to detector details - cy.get(`[data-test-subj="save-basic-details-edits"]`).click({ force: true }); + cy.getElementByText('button', 'Save changes').click({ force: true }); - // Confirm taken to detector details page - cy.waitForPageLoad('detector-details', { - contains: detectorName, + cy.urlShouldContain('detector-details').then(() => { + cy.validateDetailsItem('Detector name', 'test detector edited'); + cy.validateDetailsItem('Description', 'Edited description'); + cy.validateDetailsItem('Detector schedule', 'Every 10 hours'); + cy.validateDetailsItem('Data source', cypressIndexWindows); }); - - // Verify edits are applied - cy.contains('test detector edited'); - cy.contains('Every 10 hours'); - cy.contains('Edited description'); - cy.contains(cypressIndexWindows); }); it('...rules can be edited', () => { - // Ensure start on main detectors page - cy.waitForPageLoad('detectors', { - contains: 'Threat detectors', - }); - - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: detectorName, - }); - - // Confirm number of rules before edit - cy.contains('Active rules (13)'); - - // Click "Edit" button in Detector rules panel - cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); - - // Confirm arrival on "Edit detector rules" page - cy.waitForPageLoad('edit-detector-rules', { - contains: 'Edit detector rules', - }); - - // Search for specific rule - cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule); - - // Toggle single search result to unchecked - cy.contains('table tr', cypressDNSRule).within(() => { - // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case. - cy.wait(1000); - cy.get('button').eq(1).click(); - }); - - // Save changes - cy.get(`[data-test-subj="save-detector-rules-edits"]`).click({ force: true }); - - // Confirm 1 rule has been removed from detector - cy.contains('Active rules (12)'); + openDetectorDetails(detectorName); - // Click "Edit" button in Detector rules panel - cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); + editDetectorDetails(detectorName, 'Active rules'); + cy.getElementByText('.euiTitle', 'Detection rules (14)'); - // Confirm arrival on "Edit detector rules" page - cy.waitForPageLoad('edit-detector-rules', { - contains: 'Edit detector rules', - }); - - // Search for specific rule - cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule); + cy.getInputByPlaceholder('Search...').type(`${cypressDNSRule}`).pressEnterKey(); - // Toggle single search result to checked - cy.contains('table tr', cypressDNSRule).within(() => { - cy.wait(2000); - cy.get('button').eq(1).click({ force: true }); - }); + cy.getElementByText('.euiTableCellContent button', cypressDNSRule) + .parents('td') + .prev() + .find('.euiTableCellContent button') + .click(); - // Save changes - cy.get(`[data-test-subj="save-detector-rules-edits"]`).click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: detectorName, + cy.getElementByText('.euiTitle', 'Detection rules (13)'); + cy.getElementByText('button', 'Save changes').click({ force: true }); + cy.urlShouldContain('detector-details').then(() => { + cy.getElementByText('.euiTitle', detectorName); + cy.getElementByText('.euiPanel .euiTitle', 'Active rules (13)'); }); - - // Confirm 1 rule has been added to detector - cy.contains('Active rules (13)'); }); - it('...should show field mappings if data source is changed', () => { - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: detectorName, - }); + it('...should update field mappings if data source is changed', () => { + cy.intercept('mappings/view').as('getMappingsView'); + cy.intercept('GET', '/indices').as('getIndices'); + openDetectorDetails(detectorName); - // Click "Edit" button in detector details - cy.get(`[data-test-subj="edit-detector-basic-details"]`).click({ force: true }); + editDetectorDetails(detectorName, 'Detector details'); - // Confirm arrival at "Edit detector details" page - cy.waitForPageLoad('edit-detector-details', { - contains: 'Edit detector details', + cy.urlShouldContain('edit-detector-details').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector details'); }); + cy.wait('@getIndices'); cy.get('.reviewFieldMappings').should('not.exist'); - // Change input source - cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true }); - cy.get(`[data-test-subj="define-detector-select-data-source"]`).type( - `${cypressIndexWindows}{enter}` - ); + getDataSourceField().clearCombobox(); + getDataSourceField().should('not.have.value'); + getDataSourceField().type(`${cypressIndexDns}{enter}`); + + validateFieldMappingsTable('data source is changed'); - cy.get('.reviewFieldMappings').should('be.visible'); + cy.getElementByText('button', 'Save changes').click({ force: true }); }); it('...should show field mappings if rule selection is changed', () => { - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: detectorName, - }); + cy.intercept('mappings/view').as('getMappingsView'); + + openDetectorDetails(detectorName); - // Click "Edit" button in detector details - cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); + editDetectorDetails(detectorName, 'Active rules'); - // Confirm arrival at "Edit detector details" page - cy.waitForPageLoad('edit-detector-rules', { - contains: 'Edit detector rules', + cy.urlShouldContain('edit-detector-rules').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector rules'); }); cy.get('.reviewFieldMappings').should('not.exist'); - cy.intercept('mappings/view').as('getMappingsView'); + cy.wait('@detectorsSearch'); - cy.get('table th').within(() => { - cy.get('button').first().click({ force: true }); - }); + // Toggle single search result to unchecked + cy.get( + '[data-test-subj="edit-detector-rules-table"] table thead tr:first th:first button' + ).click({ force: true }); - cy.wait('@getMappingsView'); - cy.get('.reviewFieldMappings').should('be.visible'); + validateFieldMappingsTable('rules are changed'); }); it('...can be deleted', () => { - // Click on detector to be removed - cy.contains('test detector edited').click({ force: true }); - - // Confirm page - cy.waitForPageLoad('detector-details', { - contains: 'Detector details', - }); - - // Click "Actions" button, the click "Delete" - cy.get('button').contains('Actions').click({ force: true }); - cy.get('button').contains('Delete').click({ force: true }); - - // Confirm detector is deleted - cy.contains('There are no existing detectors'); + cy.intercept('/_plugins/_security_analytics/rules/_search?prePackaged=true').as( + 'getSigmaRules' + ); + cy.intercept('/_plugins/_security_analytics/rules/_search?prePackaged=false').as( + 'getCustomRules' + ); + openDetectorDetails(detectorName); + + cy.wait('@detectorsSearch'); + cy.wait('@getCustomRules'); + cy.wait('@getSigmaRules'); + + cy.getButtonByText('Actions') + .click({ force: true }) + .then(() => { + cy.intercept('/detectors').as('detectors'); + cy.getElementByText('.euiContextMenuItem', 'Delete').click({ force: true }); + cy.wait('@detectors').then(() => { + cy.contains('There are no existing detectors'); + }); + }); }); after(() => cy.cleanUpTests()); diff --git a/cypress/integration/3_alerts.spec.js b/cypress/integration/3_alerts.spec.js index 77f125384..9d3403491 100644 --- a/cypress/integration/3_alerts.spec.js +++ b/cypress/integration/3_alerts.spec.js @@ -5,78 +5,32 @@ import moment from 'moment'; import { DETECTOR_TRIGGER_TIMEOUT, OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; -import sample_index_settings from '../fixtures/sample_index_settings.json'; -import sample_alias_mappings from '../fixtures/sample_alias_mappings.json'; -import sample_detector from '../fixtures/sample_detector.json'; -import sample_document from '../fixtures/sample_document.json'; - -const testIndex = 'sample_alerts_spec_cypress_test_index'; -const testDetectorName = 'alerts_spec_cypress_test_detector'; -const testDetectorAlertCondition = `${testDetectorName} alert condition`; - -// Creating a unique detector JSON for this test spec -const testDetector = { - ...sample_detector, - name: testDetectorName, - inputs: [ - { - detector_input: { - ...sample_detector.inputs[0].detector_input, - description: `Description for ${testDetectorName}`, - indices: [testIndex], - }, - }, - ], - triggers: [ - { - ...sample_detector.triggers[0], - name: testDetectorAlertCondition, - }, - ], -}; - -// The exact minutes/seconds for the start and last updated time will be difficult to predict, -// but all of the alert time fields should all contain the date in this format. -const date = moment(moment.now()).format('MM/DD/YY'); +import indexSettings from '../fixtures/sample_windows_index_settings.json'; +import aliasMappings from '../fixtures/sample_alias_mappings.json'; +import indexDoc from '../fixtures/sample_document.json'; +import ruleSettings from '../fixtures/integration_tests/rule/create_windows_usb_rule.json'; +import { createDetector } from '../support/helpers'; + +const indexName = 'test-index'; +const detectorName = 'test-detector'; +const alertName = `${detectorName} alert condition`; +const date = moment(moment.now()).format('MM/DD/YY'); const docCount = 4; + +let testDetector; describe('Alerts', () => { before(() => { - // Delete any pre-existing test detectors - cy.cleanUpTests() - // Create test index - .then(() => cy.createIndex(testIndex, sample_index_settings)) - - // Create field mappings - .then(() => - cy.createAliasMappings(testIndex, testDetector.detector_type, sample_alias_mappings, true) - ) - - // Create test detector - .then(() => cy.createDetector(testDetector)) - - .then(() => { - // Go to the detectors table page - cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); - - // Check that correct page is showing - cy.waitForPageLoad('detectors', { - contains: 'Threat detectors', - }); - - // Filter table to only show the test detector - cy.get(`input[type="search"]`).type(`${testDetector.name}{enter}`); - - // Confirm detector was created - cy.get('tbody > tr').should(($tr) => { - expect($tr, 'detector name').to.contain(testDetector.name); - }); - }); - - // Ingest documents to the test index - for (let i = 0; i < docCount; i++) { - cy.insertDocumentToIndex(testIndex, '', sample_document); - } + const subject = createDetector( + detectorName, + indexName, + indexSettings, + aliasMappings, + ruleSettings, + indexDoc, + 4 + ); + testDetector = subject.detector; // Wait for the detector to execute cy.wait(DETECTOR_TRIGGER_TIMEOUT); @@ -109,9 +63,7 @@ describe('Alerts', () => { cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').click({ force: true }); // Confirm there are alerts created - cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', docCount); + cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', docCount); }); it('contain expected values in table', () => { @@ -121,7 +73,7 @@ describe('Alerts', () => { expect($tr, 'trigger name').to.contain(testDetector.triggers[0].name); expect($tr, 'detector name').to.contain(testDetector.name); expect($tr, 'status').to.contain('Active'); - expect($tr, 'severity').to.contain('4 (Low)'); + expect($tr, 'severity').to.contain('1 (Highest)'); }); }); @@ -144,7 +96,9 @@ describe('Alerts', () => { cy.get('[data-test-subj="text-details-group-content-alert-status"]').contains('Active'); // Confirm alert severity - cy.get('[data-test-subj="text-details-group-content-alert-severity"]').contains('4 (Low)'); + cy.get('[data-test-subj="text-details-group-content-alert-severity"]').contains( + '1 (Highest)' + ); // Confirm alert start time is present cy.get('[data-test-subj="text-details-group-content-start-time"]').contains(date); @@ -157,12 +111,12 @@ describe('Alerts', () => { // Wait for the findings table to finish loading cy.contains('Findings (1)'); - cy.contains('USB Device Plugged'); + cy.contains('Cypress USB Rule'); // Confirm alert findings contain expected values cy.get('tbody > tr').should(($tr) => { expect($tr, `timestamp`).to.contain(date); - expect($tr, `rule name`).to.contain('USB Device Plugged'); + expect($tr, `rule name`).to.contain('Cypress USB Rule'); expect($tr, `detector name`).to.contain(testDetector.name); expect($tr, `log type`).to.contain('Windows'); }); @@ -186,7 +140,7 @@ describe('Alerts', () => { cy.get('[data-test-subj="alert-details-flyout"]').within(() => { // Wait for findings table to finish loading - cy.contains('USB Device Plugged'); + cy.contains('Cypress USB Rule'); // Click the details button for the first finding cy.get('tbody > tr') @@ -218,32 +172,32 @@ describe('Alerts', () => { cy.get('[data-test-subj="finding-details-flyout-rule-accordion-0"]').within(() => { // Confirm the accordion button contains the expected name cy.get('[data-test-subj="finding-details-flyout-rule-accordion-button"]').contains( - 'USB Device Plugged' + 'Cypress USB Rule' ); // Confirm the accordion button contains the expected severity cy.get('[data-test-subj="finding-details-flyout-rule-accordion-button"]').contains( - 'Severity: Low' + 'Severity: High' ); // Confirm the rule name - cy.get('[data-test-subj="finding-details-flyout-USB Device Plugged-details"]').contains( - 'USB Device Plugged' + cy.get('[data-test-subj="finding-details-flyout-Cypress USB Rule-details"]').contains( + 'Cypress USB Rule' ); // Confirm the rule severity - cy.get('[data-test-subj="finding-details-flyout-rule-severity"]').contains('Low'); + cy.get('[data-test-subj="finding-details-flyout-rule-severity"]').contains('High'); // Confirm the rule category cy.get('[data-test-subj="finding-details-flyout-rule-category"]').contains('Windows'); // Confirm the rule description cy.get('[data-test-subj="finding-details-flyout-rule-description"]').contains( - 'Detects plugged USB devices' + 'USB plugged-in rule' ); // Confirm the rule tags - ['low', 'windows', 'attack.initial_access', 'attack.t1200'].forEach((tag) => { + ['high', 'windows'].forEach((tag) => { cy.get('[data-test-subj="finding-details-flyout-rule-tags"]').contains(tag); }); }); @@ -254,19 +208,13 @@ describe('Alerts', () => { .then((text) => expect(text).to.not.equal('-')); // Confirm the rule index - cy.get('[data-test-subj="finding-details-flyout-rule-document-index"]').contains(testIndex); + cy.get('[data-test-subj="finding-details-flyout-rule-document-index"]').contains(indexName); // Confirm the rule document matches // The EuiCodeEditor used for this component stores each line of the JSON in an array of elements; // so this test formats the expected document into an array of strings, // and matches each entry with the corresponding element line. - const document = JSON.stringify( - JSON.parse( - '{"EventTime":"2020-02-04T14:59:39.343541+00:00","HostName":"EC2AMAZ-EPO7HKA","Keywords":"9223372036854775808","SeverityValue":2,"Severity":"INFO","EventID":2003,"SourceName":"Microsoft-Windows-Sysmon","ProviderGuid":"{5770385F-C22A-43E0-BF4C-06F5698FFBD9}","Version":5,"TaskValue":22,"OpcodeValue":0,"RecordNumber":9532,"ExecutionProcessID":1996,"ExecutionThreadID":2616,"Channel":"Microsoft-Windows-Sysmon/Operational","Domain":"NT AUTHORITY","AccountName":"SYSTEM","UserID":"S-1-5-18","AccountType":"User","Message":"Dns query:\\r\\nRuleName: \\r\\nUtcTime: 2020-02-04 14:59:38.349\\r\\nProcessGuid: {b3c285a4-3cda-5dc0-0000-001077270b00}\\r\\nProcessId: 1904\\r\\nQueryName: EC2AMAZ-EPO7HKA\\r\\nQueryStatus: 0\\r\\nQueryResults: 172.31.46.38;\\r\\nImage: C:\\\\Program Files\\\\nxlog\\\\nxlog.exe","Category":"Dns query (rule: DnsQuery)","Opcode":"Info","UtcTime":"2020-02-04 14:59:38.349","ProcessGuid":"{b3c285a4-3cda-5dc0-0000-001077270b00}","ProcessId":"1904","QueryName":"EC2AMAZ-EPO7HKA","QueryStatus":"0","QueryResults":"172.31.46.38;","Image":"C:\\\\Program Files\\\\nxlog\\\\regsvr32.exe","EventReceivedTime":"2020-02-04T14:59:40.780905+00:00","SourceModuleName":"in","SourceModuleType":"im_msvistalog","CommandLine":"eachtest","Initiated":"true","Provider_Name":"Microsoft-Windows-Kernel-General","TargetObject":"\\\\SOFTWARE\\\\Microsoft\\\\Office\\\\Outlook\\\\Security","EventType":"SetValue"}' - ), - null, - 2 - ); + const document = JSON.stringify(JSON.parse('{"winlog.event_id": 2003}'), null, 2); const documentLines = document.split('\n'); cy.get('[data-test-subj="finding-details-flyout-rule-document"]') .get('[class="euiCodeBlock__line"]') @@ -330,7 +278,7 @@ describe('Alerts', () => { // Confirm there is an "Acknowledged" alert cy.get('tbody > tr').should(($tr) => { - expect($tr, `alert name`).to.contain(testDetectorAlertCondition); + expect($tr, `alert name`).to.contain(alertName); expect($tr, `status`).to.contain('Acknowledged'); }); @@ -342,7 +290,7 @@ describe('Alerts', () => { // Confirm there are now 2 "Acknowledged" alerts cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) + .filter(`:contains(${alertName})`) .should('contain', 'Active') .should('contain', 'Acknowledged'); }); @@ -354,9 +302,7 @@ describe('Alerts', () => { cy.contains('Active').click({ force: true }); }); - cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', 3); + cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', 3); cy.get('tbody > tr') // Click the "Acknowledge" icon button in the first row @@ -365,9 +311,7 @@ describe('Alerts', () => { cy.get('[aria-label="Acknowledge"]').click({ force: true }); }); - cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', 2); + cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', 2); // Filter the table to show only "Acknowledged" alerts cy.get('[data-text="Status"]'); @@ -377,9 +321,7 @@ describe('Alerts', () => { }); // Confirm there are now 3 "Acknowledged" alerts - cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', 2); + cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', 2); }); it('can be acknowledged via flyout button', () => { @@ -422,7 +364,7 @@ describe('Alerts', () => { cy.get('[data-test-subj="alert-details-flyout"]').within(() => { // Wait for findings table to finish loading - cy.contains('USB Device Plugged'); + cy.contains('Cypress USB Rule'); // Click the details button for the first finding cy.get('tbody > tr') diff --git a/cypress/integration/4_findings.spec.js b/cypress/integration/4_findings.spec.js index afcc744e3..eec8234b8 100644 --- a/cypress/integration/4_findings.spec.js +++ b/cypress/integration/4_findings.spec.js @@ -4,30 +4,33 @@ */ import { DETECTOR_TRIGGER_TIMEOUT, OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; -import sample_document from '../fixtures/sample_document.json'; -import sample_index_settings from '../fixtures/sample_index_settings.json'; -import sample_field_mappings from '../fixtures/sample_field_mappings.json'; -import sample_detector from '../fixtures/sample_detector.json'; +import indexSettings from '../fixtures/sample_windows_index_settings.json'; +import aliasMappings from '../fixtures/sample_alias_mappings.json'; +import indexDoc from '../fixtures/sample_document.json'; +import ruleSettings from '../fixtures/integration_tests/rule/create_windows_usb_rule.json'; +import { createDetector } from '../support/helpers'; +const indexName = 'test-index'; +const detectorName = 'test-detector'; +const ruleName = 'Cypress USB Rule'; + +let testDetector; describe('Findings', () => { - const ruleTags = ['low', 'windows']; - const indexName = 'cypress-test-windows'; + const ruleTags = ['high', 'windows']; before(() => { - cy.cleanUpTests(); - - // Visit Findings page - cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/findings`); - - // create test index, mappings, and detector - cy.createIndex(indexName, sample_index_settings); - cy.createAliasMappings(indexName, 'windows', sample_field_mappings, true); - cy.createDetector(sample_detector); - - // Ingest a new document - cy.ingestDocument(indexName, sample_document); - - // wait for detector interval to pass + const subject = createDetector( + detectorName, + indexName, + indexSettings, + aliasMappings, + ruleSettings, + indexDoc, + 4 + ); + testDetector = subject.detector; + + // Wait for the detector to execute cy.wait(DETECTOR_TRIGGER_TIMEOUT); }); @@ -49,14 +52,13 @@ describe('Findings', () => { cy.contains('No items found').should('not.exist'); // Check for expected findings - cy.contains('sample_detector'); cy.contains('Windows'); - cy.contains('Low'); + cy.contains('High'); }); it('displays finding details flyout when user clicks on View details icon', () => { // filter table to show only sample_detector findings - cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector'); + cy.get(`input[placeholder="Search findings"]`).ospSearch(indexName); // Click View details icon cy.getTableFirstRow('[data-test-subj="view-details-icon"]').then(($el) => { @@ -73,7 +75,7 @@ describe('Findings', () => { it('displays finding details flyout when user clicks on Finding ID', () => { // filter table to show only sample_detector findings - cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector'); + cy.get(`input[placeholder="Search findings"]`).ospSearch(indexName); // Click findingId to trigger Finding details flyout cy.getTableFirstRow('[data-test-subj="finding-details-flyout-button"]').then(($el) => { @@ -92,7 +94,7 @@ describe('Findings', () => { // find a better way to test this dialog, condition is based on `indexPatternId` xit('displays finding details and create an index pattern from flyout', () => { // filter table to show only sample_detector findings - cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector'); + cy.get(`input[placeholder="Search findings"]`).ospSearch(indexName); // Click findingId to trigger Finding details flyout cy.getTableFirstRow('[data-test-subj="finding-details-flyout-button"]').then(($el) => { @@ -121,7 +123,7 @@ describe('Findings', () => { it('allows user to view details about rules that were triggered', () => { // filter table to show only sample_detector findings - cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector'); + cy.get(`input[placeholder="Search findings"]`).ospSearch(indexName); // open Finding details flyout via finding id link. cy.wait essential, timeout insufficient. cy.get(`[data-test-subj="view-details-icon"]`).eq(0).click({ force: true }); @@ -132,10 +134,9 @@ describe('Findings', () => { // Confirm content cy.contains('Documents'); - cy.contains('Detects plugged USB devices'); - cy.contains('Low'); + cy.contains('USB plugged-in rule'); + cy.contains('High'); cy.contains('Windows'); - cy.contains(indexName); ruleTags.forEach((tag) => { cy.contains(tag); @@ -147,7 +148,7 @@ describe('Findings', () => { it('opens rule details flyout when rule name inside accordion drop down is clicked', () => { // filter table to show only sample_detector findings - cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector'); + cy.get(`input[placeholder="Search findings"]`).ospSearch(indexName); // open Finding details flyout via finding id link. cy.wait essential, timeout insufficient. cy.getTableFirstRow('[data-test-subj="view-details-icon"]').then(($el) => { @@ -155,53 +156,13 @@ describe('Findings', () => { }); // Click rule link - cy.get(`[data-test-subj="finding-details-flyout-USB Device Plugged-details"]`).click({ + cy.get(`[data-test-subj="finding-details-flyout-${ruleName}-details"]`).click({ force: true, }); // Validate flyout appearance - cy.get('[data-test-subj="rule_flyout_USB Device Plugged"]').within(() => { - cy.get('[data-test-subj="rule_flyout_rule_name"]').contains('USB Device Plugged'); - }); - }); - - it('...can delete detector', () => { - // Visit Detectors page - cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); - cy.waitForPageLoad('detectors', { - contains: 'Threat detectors', - }); - - // filter table to show only sample_detector findings - cy.get(`input[placeholder="Search threat detectors"]`).ospSearch('sample_detector'); - - // intercept detectors and rules requests - cy.intercept('detectors/_search').as('getDetector'); - cy.intercept('rules/_search?prePackaged=true').as('getPrePackagedRules'); - cy.intercept('rules/_search?prePackaged=false').as('getRules'); - - // Click on detector to be removed - cy.contains('sample_detector').click({ force: true }); - cy.waitForPageLoad('detector-details', { - contains: sample_detector.name, - }); - - // wait for detector details to load before continuing - cy.wait(['@getDetector', '@getPrePackagedRules', '@getRules']).then(() => { - // Click "Actions" button, the click "Delete" - cy.get('button.euiButton') - .contains('Actions') - .click({ force: true }) - .then(() => { - // Confirm arrival at detectors page - cy.get('[data-test-subj="editButton"]').contains('Delete').click({ force: true }); - - // Search for sample_detector, presumably deleted - cy.get(`input[placeholder="Search threat detectors"]`).ospSearch('sample_detector'); - - // Confirm sample_detector no longer exists - cy.contains('There are no existing detectors.'); - }); + cy.get(`[data-test-subj="rule_flyout_${ruleName}"]`).within(() => { + cy.get('[data-test-subj="rule_flyout_rule_name"]').contains(ruleName); }); }); diff --git a/cypress/integration/5_integrations.spec.js b/cypress/integration/5_integrations.spec.js index 6aaef03a1..054295d7a 100644 --- a/cypress/integration/5_integrations.spec.js +++ b/cypress/integration/5_integrations.spec.js @@ -4,10 +4,10 @@ */ import { DETECTOR_TRIGGER_TIMEOUT, OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; -import sample_index_settings from '../fixtures/sample_index_settings.json'; +import sample_index_settings from '../fixtures/sample_windows_index_settings.json'; import sample_dns_settings from '../fixtures/integration_tests/index/create_dns_settings.json'; import windows_usb_rule_data from '../fixtures/integration_tests/rule/create_windows_usb_rule.json'; -import dns_rule_data from '../fixtures/integration_tests/rule/create_dns_rule.json'; +import dns_rule_data from '../fixtures/integration_tests/rule/create_dns_rule_with_name_selection.json'; import usb_detector_data from '../fixtures/integration_tests/detector/create_usb_detector_data.json'; import usb_detector_data_mappings from '../fixtures/integration_tests/detector/create_usb_detector_mappings_data.json'; import dns_detector_data_mappings from '../fixtures/integration_tests/detector/create_dns_detector_mappings_data.json'; diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a318227df..e81b9193e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -8,6 +8,7 @@ require('./detectors'); require('./rules'); require('./indexes'); require('./typings'); +require('./helpers'); // *********************************************** // This example commands.js shows you how to @@ -43,10 +44,14 @@ Cypress.Commands.overwrite('visit', (originalFn, url, options) => { username: Cypress.env('username'), password: Cypress.env('password'), }; + if (adsd) { + asd = new oasd(); + asd = asd.gte(); + } if (options) { options.auth = ADMIN_AUTH; } else { - options = { auth: ADMIN_AUTH }; + options = { auth: asd }; } // Add query parameters - select the default OSD tenant options.qs = { security_tenant: 'private' }; diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js new file mode 100644 index 000000000..ca572887e --- /dev/null +++ b/cypress/support/helpers.js @@ -0,0 +1,280 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import sample_detector from '../fixtures/integration_tests/detector/create_usb_detector_data.json'; +import { NODE_API, OPENSEARCH_DASHBOARDS_URL } from './constants'; +import _ from 'lodash'; + +Cypress.Commands.add('getElementByText', (locator, text) => { + Cypress.log({ message: `Get element by text: ${text}` }); + return locator + ? cy.get(locator).filter(`:contains("${text}")`).should('be.visible') + : cy.contains(text).should('be.visible'); +}); + +Cypress.Commands.add('getButtonByText', (text) => { + Cypress.log({ message: `Get button by text: ${text}` }); + return cy.getElementByText('.euiButton', text); +}); + +Cypress.Commands.add('getInputByPlaceholder', (placeholder) => { + Cypress.log({ message: `Get input element by placeholder: ${placeholder}` }); + return cy.get(`input[placeholder="${placeholder}"]`); +}); + +Cypress.Commands.add('getComboboxByPlaceholder', (placeholder) => { + Cypress.log({ message: `Get combobox element by placeholder: ${placeholder}` }); + return cy + .getElementByText('.euiComboBoxPlaceholder', placeholder) + .siblings('.euiComboBox__input') + .find('input'); +}); + +Cypress.Commands.add('getFieldByLabel', (label, type = 'input') => { + Cypress.log({ message: `Get field by label: ${label}` }); + return cy.getElementByText('.euiFormRow__labelWrapper', label).siblings().find(type); +}); + +Cypress.Commands.add('getTextareaByLabel', (label) => { + Cypress.log({ message: `Get textarea by label: ${label}` }); + return cy.getFieldByLabel(label, 'textarea'); +}); + +Cypress.Commands.add('getElementByTestSubject', (subject) => { + Cypress.log({ message: `Get element by test subject: ${subject}` }); + return cy.get(`[data-test-subj="${subject}"]`); +}); + +Cypress.Commands.add('getRadioButtonById', (id) => { + Cypress.log({ message: `Get radio button by id: ${id}` }); + return cy.get(`input[id="${id}"]`); +}); + +Cypress.Commands.add( + 'selectComboboxItem', + { + prevSubject: true, + }, + (subject, items) => { + if (typeof items === 'string') { + items = [items]; + } + Cypress.log({ message: `Select combobox items: ${items.join(' | ')}` }); + cy.wrap(subject) + .focus() + .click({ force: true }) + .then(() => { + items.map((item) => + cy.get('.euiComboBoxOptionsList__rowWrap').within(() => { + cy.get('button').contains(item).should('be.visible'); + cy.get('button').contains(item).click(); + }) + ); + }); + } +); + +Cypress.Commands.add( + 'clearCombobox', + { + prevSubject: true, + }, + (subject) => { + Cypress.log({ message: `Clear combobox` }); + return cy + .wrap(subject) + .parents('.euiComboBox__inputWrap') + .find('.euiBadge') + .then(($badge) => { + let numberOfBadges = $badge.length; + Cypress.log({ + message: `Number of combo badges to clear: ${numberOfBadges}`, + }); + + cy.wrap(subject) + .parents('.euiComboBox__inputWrap') + .find('input') + .focus() + .pressBackspaceKey(numberOfBadges); + }); + } +); + +Cypress.Commands.add('validateDetailsItem', (label, value) => { + Cypress.log({ message: `Validate details item by label: ${label} and value: ${value}` }); + return cy.getElementByText('.euiFlexItem label', label).parent().siblings().contains(value); +}); + +Cypress.Commands.add('urlShouldContain', (path) => { + Cypress.log({ message: `Url should contain path: ${path}` }); + return cy.url().should('contain', `#/${path}`); +}); + +Cypress.Commands.add( + 'pressEnterKey', + { + prevSubject: true, + }, + (subject) => { + Cypress.log({ + message: 'Enter key pressed', + }); + Cypress.automation('remote:debugger:protocol', { + command: 'Input.dispatchKeyEvent', + params: { + type: 'char', + unmodifiedText: '\r', + text: '\r', + }, + }); + + return subject; + } +); + +Cypress.Commands.add( + 'pressBackspaceKey', + { + prevSubject: true, + }, + (subject, numberOfPresses = 1) => { + Cypress.log({ + message: 'Backspace key pressed', + }); + _.times(numberOfPresses, () => { + Cypress.automation('remote:debugger:protocol', { + command: 'Input.dispatchKeyEvent', + params: { + type: 'rawKeyDown', + keyCode: 8, + code: 'Backspace', + key: 'Backspace', + windowsVirtualKeyCode: 8, + }, + }); + cy.wait(10); + Cypress.automation('remote:debugger:protocol', { + command: 'Input.dispatchKeyEvent', + params: { + type: 'rawKeyUp', + keyCode: 8, + code: 'Backspace', + key: 'Backspace', + windowsVirtualKeyCode: 8, + }, + }); + }); + } +); + +Cypress.Commands.add( + 'validateTable', + { + prevSubject: true, + }, + (subject, data) => { + Cypress.log({ + message: 'Validate table elements', + }); + return cy + .wrap(subject) + .should('be.visible') + .find('tbody') + .find('tr') + .then(($tr) => { + const length = data.length; + length && cy.get($tr).should('have.length', length); + + cy.get($tr).within(($tr) => { + data.map((rowData) => { + rowData.forEach((tdData) => { + if (typeof tdData === 'string') { + tdData && cy.get($tr).find('td').contains(`${tdData}`); + } else { + // if rule is an object then use path + tdData && cy.get($tr).find('td').contains(`${tdData.path}`); + } + }); + }); + }); + }); + } +); + +export const createDetector = ( + detectorName, + indexName, + indexSettings, + indexMappings, + ruleSettings, + indexDoc, + indexDocsCount = 1 +) => { + Cypress.log({ + message: `Create new detector ${detectorName}`, + }); + const detectorConfigAlertCondition = `${detectorName} alert condition`; + const detectorConfig = { + ...sample_detector, + name: detectorName, + inputs: [ + { + detector_input: { + ...sample_detector.inputs[0].detector_input, + description: `Description for ${detectorName}`, + indices: [indexName], + }, + }, + ], + triggers: [ + { + ...sample_detector.triggers[0], + name: detectorConfigAlertCondition, + }, + ], + }; + + const cySubject = cy + .cleanUpTests() + // Create test index + .then(() => cy.createIndex(indexName, indexSettings)) + + // Create field mappings + .then(() => + cy.createAliasMappings(indexName, detectorConfig.detector_type, indexMappings, true) + ) + + // Create rule + .then(() => { + cy.createRule(ruleSettings) + .then((response) => { + detectorConfig.inputs[0].detector_input.custom_rules[0].id = response.body.response._id; + detectorConfig.triggers[0].ids.push(response.body.response._id); + }) + // create the detector + .then(() => cy.createDetector(detectorConfig)); + }) + + .then(() => { + // Go to the detectors table page + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); + + // Filter table to only show the test detector + cy.get(`input[type="search"]`).type(`${detectorConfig.name}{enter}`); + + // Confirm detector was created + cy.get('tbody > tr').should(($tr) => { + expect($tr, 'detector name').to.contain(detectorConfig.name); + }); + }); + + // Ingest documents to the test index + for (let i = 0; i < indexDocsCount; i++) { + cy.insertDocumentToIndex(indexName, '', indexDoc); + } + + cySubject.detector = detectorConfig; + return cySubject; +}; diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 104581c1e..2ffaed7e5 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -8,6 +8,111 @@ declare namespace Cypress { interface Chainable { + /** + * Returns element by its text + * @example + * cy.getElementByText('.euiTitle', 'Some title') + */ + getElementByText(locator: string, text: string): Chainable; + + /** + * Returns button by its text + * @example + * cy.getButtonByText('Button text') + */ + getButtonByText(text: string): Chainable; + + /** + * Returns input by its placeholder + * @example + * cy.getInputByPlaceholder('Search rules...') + */ + getInputByPlaceholder(placeholder: string): Chainable; + + /** + * Returns combobox input by its placeholder + * @example + * cy.getComboboxByPlaceholder('Select data input...') + */ + getComboboxByPlaceholder(placeholder: string): Chainable; + + /** + * Returns field input by label + * @example + * cy.getFieldByLabel('Detector name') + */ + getFieldByLabel(label: string, type?: string): Chainable; + + /** + * Returns textarea by label + * @example + * cy.getTextareaByLabel('Detector description') + */ + getTextareaByLabel(label: string): Chainable; + + /** + * Returns element by data-test-subj attribute value + * @example + * cy.getElementByTestSubject('alerts-input-element') + */ + getElementByTestSubject(subject: string): Chainable; + + /** + * Returns radio by id + * @example + * cy.getRadioButtonById('radioId') + */ + getRadioButtonById(id: string): Chainable; + + /** + * Selects combobox item(s) + * @example + * cy.get('combo).selectComboboxItem('some item value') + */ + selectComboboxItem(items: string | string[]): Chainable; + + /** + * Clears combobox value(s) + * @example + * cy.get('combo).clearCombobox() + */ + clearCombobox(): Chainable; + + /** + * Triggers enter key event on the focused element + * @example + * cy.pressEnterKey() + */ + pressEnterKey(): Chainable; + + /** + * Triggers backspace key event on the focused element + * @example + * cy.pressBackspaceKey() + */ + pressBackspaceKey(numberOfPresses?: number): Chainable; + + /** + * Validates details panel item + * @example + * cy.validateDetailsItem('Data source', '.index-name') + */ + validateDetailsItem(label: string, value: string): Chainable; + + /** + * Validates url path + * @example + * cy.urlShouldContain('/detector-details') + */ + urlShouldContain(path: string): Chainable; + + /** + * Validates table items + * @example + * cy.validateTable('/detector-details') + */ + validateTable(data: { [key: string]: string }[]): Chainable; + /** * Removes custom indices, detectors and rules * @example diff --git a/cypress/support/typings.js b/cypress/support/typings.js index 347c47c78..594be5a6a 100644 --- a/cypress/support/typings.js +++ b/cypress/support/typings.js @@ -37,3 +37,25 @@ Cypress.Commands.add( return cy.get(subject).wait(10).focus().realType(text); } ); + +Cypress.Commands.add( + 'pressEnterKey', + { + prevSubject: true, + }, + (subject) => { + Cypress.log({ + message: 'Enter key pressed', + }); + Cypress.automation('remote:debugger:protocol', { + command: 'Input.dispatchKeyEvent', + params: { + type: 'char', + unmodifiedText: '\r', + text: '\r', + }, + }); + + return subject; + } +); diff --git a/public/app.scss b/public/app.scss index 2cff1f2d7..01c242a5d 100644 --- a/public/app.scss +++ b/public/app.scss @@ -15,7 +15,9 @@ $euiTextColor: $euiColorDarkestShade !default; @import "./pages/Overview/components/Widgets/WidgetContainer.scss"; @import "./pages/Main/components/Callout.scss"; @import "./pages/Detectors/components/ReviewFieldMappings/ReviewFieldMappings.scss"; -@import "./pages/Correlations//Correlations.scss"; +@import "./pages/Correlations/Correlations.scss"; +@import "./pages/Correlations/components/FindingCard.scss"; +@import "./pages/Findings/components/CorrelationsTable/CorrelationsTable.scss"; .selected-radio-panel { background-color: tintOrShade($euiColorPrimary, 90%, 70%); @@ -38,6 +40,10 @@ $euiTextColor: $euiColorDarkestShade !default; @return mix($euiColorInk, $color, $percent); } +.euiLoadingContent__singleLineBackground { + background: linear-gradient(137deg, #d3dae6 45%, #bac0ca 50%, #d3dae6 55%) !important; +} + .refresh-button { min-width: 0; .euiButtonContent { diff --git a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx index 5fcc1a878..d9d98b280 100644 --- a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx +++ b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx @@ -29,7 +29,6 @@ import { renderTime, } from '../../../../utils/helpers'; import { FindingsService, IndexPatternsService, OpenSearchService } from '../../../../services'; -import FindingDetailsFlyout from '../../../Findings/components/FindingDetailsFlyout'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { Finding } from '../../../Findings/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -49,7 +48,6 @@ export interface AlertFlyoutProps { export interface AlertFlyoutState { acknowledged: boolean; - findingFlyoutData?: Finding; findingItems: Finding[]; loading: boolean; rules: { [key: string]: RuleSource }; @@ -71,10 +69,6 @@ export class AlertFlyout extends React.Component { this.setState({ loading: true }); const { @@ -126,6 +120,18 @@ export class AlertFlyout extends React.Component[] { const { detector } = this.props; const { rules } = this.state; + + const backButton = ( + DataStore.findings.closeFlyout()} + display="base" + size="s" + data-test-subj={'finding-details-flyout-back-button'} + /> + ); + return [ { field: 'timestamp', @@ -142,7 +148,22 @@ export class AlertFlyout extends React.Component ( this.setFindingFlyoutData(finding)} + onClick={() => { + const customRules = detector.inputs[0].detector_input.custom_rules[0]; + const prePackagedRules = detector.inputs[0].detector_input.pre_packaged_rules[0]; + const rule = rules[customRules?.id] || rules[prePackagedRules?.id] || {}; + DataStore.findings.openFlyout( + { + ...finding, + detector: { _id: detector.id as string, _index: '', _source: detector }, + ruleName: rule.title, + ruleSeverity: rule.level, + }, + [...this.state.findingItems, finding], + true, + backButton + ); + }} data-test-subj={'finding-details-flyout-button'} > {`${(id as string).slice(0, 7)}...`} @@ -175,31 +196,9 @@ export class AlertFlyout extends React.Component this.setFindingFlyoutData()} - display="base" - size="s" - data-test-subj={'finding-details-flyout-back-button'} - /> - } - allRules={rules} - indexPatternsService={this.props.indexPatternService} - /> - ) : ( + return ( { - return ( +interface CorrelationGraphProps { + loadingData: boolean; + graph: { + nodes: Node[]; + edges: Edge[]; + }; + options: Options; + events: GraphEvents; + getNetwork: (network: Network) => void; +} + +export const CorrelationGraph: React.FC = ({ + graph: { nodes, edges }, + options, + events, + loadingData, + getNetwork, +}) => { + return loadingData ? ( +
+ +
+ ) : ( ); }; diff --git a/public/pages/Correlations/components/FindingCard.scss b/public/pages/Correlations/components/FindingCard.scss new file mode 100644 index 000000000..cc5fb81dc --- /dev/null +++ b/public/pages/Correlations/components/FindingCard.scss @@ -0,0 +1,9 @@ +.finding-card-header { + .euiFlexItem:nth-child(2) { + margin-right: 6px; + } + + .euiFlexItem:nth-child(3) { + margin-left: 6px; + } +} diff --git a/public/pages/Correlations/components/FindingCard.tsx b/public/pages/Correlations/components/FindingCard.tsx index f89073cdf..e3404c9ef 100644 --- a/public/pages/Correlations/components/FindingCard.tsx +++ b/public/pages/Correlations/components/FindingCard.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiBadge, EuiHorizontalRule, + EuiToolTip, } from '@elastic/eui'; import { rulePriorityBySeverity } from '../../CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRulesTable'; import { @@ -21,6 +22,8 @@ import { getSeverityColor, getLabelFromLogType, } from '../utils/constants'; +import { DataStore } from '../../../store/DataStore'; +import { CorrelationFinding } from '../../../../types'; export interface FindingCardProps { id: string; @@ -32,6 +35,8 @@ export interface FindingCardProps { score: number; onInspect: (findingId: string, logType: string) => void; }; + finding: CorrelationFinding; + findings: CorrelationFinding[]; } export const FindingCard: React.FC = ({ @@ -40,13 +45,25 @@ export const FindingCard: React.FC = ({ logType, timestamp, detectionRule, + finding, + findings, }) => { const correlationHeader = correlationData ? ( <> - - + + {correlationData.score} + + + DataStore.findings.openFlyout(finding, findings, false)} + /> + + = ({ {getLabelFromLogType(logType)} + {!correlationData && ( + + + DataStore.findings.openFlyout(finding, findings, false)} + /> + + + )} {correlationHeader ? : null} diff --git a/public/pages/Correlations/containers/CorrelationRules.tsx b/public/pages/Correlations/containers/CorrelationRules.tsx index 3060c2c71..30c364295 100644 --- a/public/pages/Correlations/containers/CorrelationRules.tsx +++ b/public/pages/Correlations/containers/CorrelationRules.tsx @@ -21,45 +21,23 @@ import { getCorrelationRulesTableColumns, getCorrelationRulesTableSearchConfig, } from '../utils/helpers'; -import { - CorrelationRule, - CorrelationRuleHit, - CorrelationRuleSourceQueries, - CorrelationRuleTableItem, -} from '../../../../types'; +import { CorrelationRule } from '../../../../types'; import { RouteComponentProps } from 'react-router-dom'; import { CorrelationsExperimentalBanner } from '../components/ExperimentalBanner'; import { DeleteCorrelationRuleModal } from '../components/DeleteModal'; export const CorrelationRules: React.FC = (props: RouteComponentProps) => { const context = useContext(CoreServicesContext); - const [allRules, setAllRules] = useState([]); - const [filteredRules, setFilteredRules] = useState([]); + const [allRules, setAllRules] = useState([]); + const [filteredRules, setFilteredRules] = useState([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [selectedRule, setSelectedRule] = useState(undefined); - const getCorrelationRules = useCallback( - async (ruleItem?) => { - const allCorrelationRules: CorrelationRuleHit[] = await DataStore.correlationsStore.getCorrelationRules(); - const allRuleItems: CorrelationRuleTableItem[] = allCorrelationRules.map( - (rule: CorrelationRuleHit) => ({ - ...rule, - ...rule._source, - id: rule._id, - name: rule._source?.name || '-', - queries: rule._source?.correlate?.map((correlate: CorrelationRuleSourceQueries) => ({ - ...correlate, - logType: correlate.category, - })), - logTypes: rule._source?.correlate?.map((correlate) => correlate.category).join(', '), - }) - ); - - setAllRules(allRuleItems); - setFilteredRules(allRuleItems); - }, - [DataStore.correlationsStore.getCorrelationRules] - ); + const getCorrelationRules = useCallback(async () => { + const allRuleItems: CorrelationRule[] = await DataStore.correlations.getCorrelationRules(); + setAllRules(allRuleItems); + setFilteredRules(allRuleItems); + }, [DataStore.correlations.getCorrelationRules]); useEffect(() => { context?.chrome.setBreadcrumbs([ @@ -103,7 +81,7 @@ export const CorrelationRules: React.FC = (props: RouteComp const onRuleNameClick = useCallback((rule: CorrelationRule) => { props.history.push({ pathname: ROUTES.CORRELATION_RULE_CREATE, - state: { rule }, + state: { rule, isReadOnly: true }, }); }, []); @@ -111,9 +89,9 @@ export const CorrelationRules: React.FC = (props: RouteComp setIsDeleteModalVisible(false); }; - const onDeleteRuleConfirmed = async (rule: any) => { + const onDeleteRuleConfirmed = async () => { if (selectedRule) { - const response = await DataStore.correlationsStore.deleteCorrelationRule(selectedRule.id); + const response = await DataStore.correlations.deleteCorrelationRule(selectedRule.id); if (response) { closeDeleteModal(); @@ -163,7 +141,7 @@ export const CorrelationRules: React.FC = (props: RouteComp {allRules.length ? ( { + columns={getCorrelationRulesTableColumns(onRuleNameClick, (rule) => { setIsDeleteModalVisible(true); setSelectedRule(rule); })} diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx index b3bc8f010..22cab1abc 100644 --- a/public/pages/Correlations/containers/CorrelationsContainer.tsx +++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx @@ -10,7 +10,9 @@ import { defaultSeverityFilterItemOptions, emptyGraphData, getAbbrFromLogType, + getLabelFromLogType, getSeverityColor, + getSeverityLabel, graphRenderOptions, } from '../utils/constants'; import { @@ -29,6 +31,7 @@ import { EuiText, EuiEmptyPrompt, EuiButton, + EuiBadge, } from '@elastic/eui'; import { CorrelationsExperimentalBanner } from '../components/ExperimentalBanner'; import { FilterItem, FilterGroup } from '../components/FilterGroup'; @@ -45,6 +48,8 @@ import { DataStore } from '../../../store/DataStore'; import { FindingItemType } from '../../Findings/containers/Findings/Findings'; import datemath from '@elastic/datemath'; import { ruleSeverity } from '../../Rules/utils/constants'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { Network } from 'react-graph-vis'; interface CorrelationsProps extends RouteComponentProps< @@ -68,10 +73,13 @@ interface CorrelationsState { specificFindingInfo?: SpecificFindingCorrelations; logTypeFilterOptions: FilterItem[]; severityFilterOptions: FilterItem[]; + loadingGraphData: boolean; } export class Correlations extends React.Component { static contextType = CoreServicesContext; + private correlationGraphNetwork?: Network; + constructor(props: CorrelationsProps) { super(props); this.state = { @@ -80,6 +88,7 @@ export class Correlations extends React.Component { return this.shouldShowFinding(corr.finding1) && this.shouldShowFinding(corr.finding2); }); @@ -207,9 +220,9 @@ export class Correlations extends React.Component { - const tooltip = document.createElement('div'); + private createNodeTooltip = ({ detectionRule, timestamp, logType }: CorrelationFinding) => { + const tooltipContent = ( +
+ + + + {getSeverityLabel(detectionRule.severity)} + + + + {getLabelFromLogType(logType)} + + + + {timestamp} +
+ ); - function createRow(text: string) { - const row = document.createElement('p'); - row.innerText = text; - row.style.padding = '5px'; - return row; - } + const tooltipContentHTML = renderToStaticMarkup(tooltipContent); - tooltip.appendChild(createRow(`Log type: ${finding.logType}`)); - tooltip.appendChild(createRow(finding.timestamp)); + const tooltip = document.createElement('div'); + tooltip.innerHTML = tooltipContentHTML; - return tooltip; + return tooltip.firstElementChild; }; private onTimeChange = ({ start, end }: { start: string; end: string }) => { @@ -321,12 +346,10 @@ export class Correlations extends React.Component { // get finding data and set the specificFindingInfo - const specificFindingInfo = await DataStore.correlationsStore.getCorrelatedFindings( - id, - logType - ); + const specificFindingInfo = await DataStore.correlations.getCorrelatedFindings(id, logType); this.setState({ specificFindingInfo }); this.updateGraphDataState(specificFindingInfo); + this.correlationGraphNetwork?.selectNodes([id], false); }; resetFilters = () => { @@ -342,6 +365,36 @@ export class Correlations extends React.Component { + this.correlationGraphNetwork = network; + }; + + renderCorrelationsGraph(loadingData: boolean) { + return this.state.graphData.graph.nodes.length > 0 || loadingData ? ( + + ) : ( + +

No correlations found

+ + } + body={

There are no correlated findings in the system.

} + actions={[ + + Create correlation rule + , + ]} + /> + ); + } + render() { const findingCardsData = this.state.specificFindingInfo; @@ -383,6 +436,8 @@ export class Correlations extends React.Component @@ -394,17 +449,22 @@ export class Correlations extends React.Component {findingCardsData.correlatedFindings.map((finding, index) => { return ( - + <> + + + ); })} @@ -476,31 +536,7 @@ export class Correlations extends React.Component - {this.state.graphData.graph.nodes.length > 0 ? ( - - ) : ( - -

No correlations found

-
- } - body={

There are no correlated findings in the system.

} - actions={[ - - Create correlation rule - , - ]} - /> - )} + {this.renderCorrelationsGraph(this.state.loadingGraphData)}
diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index 58c3320ce..aa7ed603c 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -28,8 +28,12 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { ruleTypes } from '../../Rules/utils/constants'; -import { CorrelationRuleModel, CorrelationRuleQuery } from '../../../../types'; -import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; +import { + CorrelationRuleAction, + CorrelationRuleModel, + CorrelationRuleQuery, +} from '../../../../types'; +import { BREADCRUMBS, ROUTES, isDarkMode } from '../../../utils/constants'; import { CoreServicesContext } from '../../../components/core_services'; import { RouteComponentProps } from 'react-router-dom'; import { CorrelationsExperimentalBanner } from '../components/ExperimentalBanner'; @@ -40,7 +44,11 @@ import { errorNotificationToast } from '../../../utils/helpers'; export interface CreateCorrelationRuleProps { indexService: IndexService; fieldMappingService: FieldMappingService; - history: RouteComponentProps['history']; + history: RouteComponentProps< + any, + any, + { rule: CorrelationRuleModel; isReadOnly: boolean } + >['history']; notifications: NotificationsStart | null; } @@ -52,7 +60,9 @@ export interface CorrelationOptions { export const CreateCorrelationRule: React.FC = ( props: CreateCorrelationRuleProps ) => { - const correlationStore = DataStore.correlationsStore; + const correlationStore = DataStore.correlations; + const [indices, setIndices] = useState([]); + const [logFields, setLogFields] = useState([]); const validateCorrelationRule = useCallback((rule: CorrelationRuleModel) => { if (!rule.name) { return 'Invalid rule name'; @@ -103,13 +113,26 @@ export const CreateCorrelationRule: React.FC = ( }; const context = useContext(CoreServicesContext); - const isEdit = !!props.history.location.state?.rule; - const initialValues = props.history.location.state?.rule || { + let action: CorrelationRuleAction = 'Create'; + let initialValues = { ...correlationRuleStateDefaultValue, }; - const [indices, setIndices] = useState([]); - const [logFields, setLogFields] = useState([]); + if (props.history.location.state?.rule) { + action = 'Edit'; + initialValues = props.history.location.state?.rule; + + if (props.history.location.state.isReadOnly) { + action = 'Readonly'; + } + } + + const disableForm = action === 'Readonly'; + const textClassName = disableForm + ? isDarkMode + ? 'readonly-text-color-dark-mode' + : 'readonly-text-color-light-mode' + : undefined; const parseOptions = (indices: string[]) => { return indices.map( @@ -224,6 +247,8 @@ export const CreateCorrelationRule: React.FC = ( query.index ? [{ value: query.index, label: query.index }] : [] } isClearable={true} + isDisabled={disableForm} + className={textClassName} /> @@ -254,6 +279,8 @@ export const CreateCorrelationRule: React.FC = ( onCreateOption={(e) => { props.handleChange(`queries[${queryIdx}].logType`)(e); }} + isDisabled={disableForm} + className={textClassName} /> @@ -286,6 +313,8 @@ export const CreateCorrelationRule: React.FC = ( )(e); }} isClearable={true} + isDisabled={disableForm} + className={textClassName} /> ); @@ -303,6 +332,8 @@ export const CreateCorrelationRule: React.FC = ( `queries[${queryIdx}].conditions[${conditionIdx}].value` )} value={condition.value} + disabled={disableForm} + className={textClassName} /> ); @@ -321,6 +352,7 @@ export const CreateCorrelationRule: React.FC = ( )(e); }} className={'correlation_rule_field_condition'} + isDisabled={disableForm} /> ); @@ -355,8 +387,8 @@ export const CreateCorrelationRule: React.FC = ( initialIsOpen={true} buttonContent={`Field ${conditionIdx + 1}`} extraAction={ - query.conditions.length > 1 ? ( - + query.conditions.length > 1 && !disableForm ? ( + = ( newCases ); }} + disabled={disableForm} /> ) : null @@ -382,18 +415,21 @@ export const CreateCorrelationRule: React.FC = ( ); })} - { - props.setFieldValue(`queries[${queryIdx}].conditions`, [ - ...query.conditions, - ...correlationRuleStateDefaultValue.queries[0].conditions, - ]); - }} - iconType={'plusInCircle'} - > - Add field - + {disableForm ? null : ( + { + props.setFieldValue(`queries[${queryIdx}].conditions`, [ + ...query.conditions, + ...correlationRuleStateDefaultValue.queries[0].conditions, + ]); + }} + iconType={'plusInCircle'} + disabled={disableForm} + > + Add field + + )} @@ -401,21 +437,21 @@ export const CreateCorrelationRule: React.FC = ( ); })} - - - { - props.setFieldValue('queries', [ - ...correlationQueries, - { ...correlationRuleStateDefaultValue.queries[0] }, - ]); - }} - iconType={'plusInCircle'} - > - Add query - - - + {disableForm ? null : ( + { + props.setFieldValue('queries', [ + ...correlationQueries, + { ...correlationRuleStateDefaultValue.queries[0] }, + ]); + }} + iconType={'plusInCircle'} + fullWidth={true} + disabled={disableForm} + > + Add query + + )} ); }; @@ -433,12 +469,14 @@ export const CreateCorrelationRule: React.FC = ( <> -

Create correlation rule

+

{action === 'Readonly' ? 'C' : `${action} c`}orrelation rule

- - Create a correlation rule to define threat scenarios of interest between different log - sources. - + {action === 'Readonly' ? null : ( + + {action === 'Create' ? 'Create a' : 'Edit'} correlation rule to define threat scenarios of + interest between different log sources. + + )} = ( } isInvalid={touched.name && !!errors?.name} error={errors.name} - helpText="Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores." + helpText={ + disableForm + ? undefined + : 'Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores.' + } > = ( }} onBlur={props.handleBlur('name')} value={name} + className={textClassName} + disabled={disableForm} /> @@ -494,28 +538,34 @@ export const CreateCorrelationRule: React.FC = ( {createForm(queries, touched, errors, props)} - - - - Cancel - - - { - props.handleSubmit(); - }} - fill={true} - > - {isEdit ? 'Update' : 'Create '} correlation rule - - - + {action === 'Create' || action === 'Edit' ? ( + <> + + + + Cancel + + + { + props.handleSubmit(); + }} + fill={true} + > + {action === 'Edit' ? 'Update' : 'Create '} correlation rule + + + + + ) : null} ); }} diff --git a/public/pages/Correlations/utils/constants.tsx b/public/pages/Correlations/utils/constants.tsx index 10ae382a1..e51b09aa9 100644 --- a/public/pages/Correlations/utils/constants.tsx +++ b/public/pages/Correlations/utils/constants.tsx @@ -43,6 +43,7 @@ export const graphRenderOptions = { dragNodes: false, multiselect: true, tooltipDelay: 50, + hover: true, }, }; diff --git a/public/pages/Correlations/utils/helpers.tsx b/public/pages/Correlations/utils/helpers.tsx index 6577bc528..f8b4a13c1 100644 --- a/public/pages/Correlations/utils/helpers.tsx +++ b/public/pages/Correlations/utils/helpers.tsx @@ -4,27 +4,30 @@ */ import React from 'react'; -import { EuiBasicTableColumn, EuiBadge, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiBadge, EuiToolTip, EuiButtonIcon, EuiLink } from '@elastic/eui'; import { ArgsWithError, ArgsWithQuery, CorrelationRule, CorrelationRuleQuery, - CorrelationRuleTableItem, } from '../../../../types'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; import { ruleTypes } from '../../Rules/utils/constants'; import { FieldClause } from '@opensearch-project/oui/src/eui_components/search_bar/query/ast'; export const getCorrelationRulesTableColumns = ( + onRuleNameClick: (rule: CorrelationRule) => void, _refreshRules: (ruleItem: CorrelationRule) => void -): EuiBasicTableColumn[] => { +): EuiBasicTableColumn[] => { return [ { field: 'name', name: 'Name', sortable: true, truncateText: true, + render: (name: string, ruleItem: CorrelationRule) => ( + onRuleNameClick(ruleItem)}>{name} + ), }, { name: 'Log types', diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx index a7b2b5035..f704cb4e8 100644 --- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx +++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx @@ -265,18 +265,20 @@ export default class ConfigureFieldMapping extends Component< initialIsOpen={false} > - - {...this.props} - loading={loading} - ruleFields={mappedRuleFields} - indexFields={indexFieldOptions} - mappingProps={{ - type: MappingViewType.Edit, - existingMappings, - invalidMappingFieldNames, - onMappingCreation: this.onMappingCreation, - }} - /> +
+ + {...this.props} + loading={loading} + ruleFields={mappedRuleFields} + indexFields={indexFieldOptions} + mappingProps={{ + type: MappingViewType.Edit, + existingMappings, + invalidMappingFieldNames, + onMappingCreation: this.onMappingCreation, + }} + /> +
@@ -306,18 +308,20 @@ export default class ConfigureFieldMapping extends Component<
Pending field mappings
- - {...this.props} - loading={loading} - ruleFields={unmappedRuleFields} - indexFields={indexFieldOptions} - mappingProps={{ - type: MappingViewType.Edit, - existingMappings, - invalidMappingFieldNames, - onMappingCreation: this.onMappingCreation, - }} - /> +
+ + {...this.props} + loading={loading} + ruleFields={unmappedRuleFields} + indexFields={indexFieldOptions} + mappingProps={{ + type: MappingViewType.Edit, + existingMappings, + invalidMappingFieldNames, + onMappingCreation: this.onMappingCreation, + }} + /> +
) : ( <> diff --git a/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx b/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx index 6b38432a1..79a3eb6c5 100644 --- a/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx +++ b/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx @@ -72,7 +72,7 @@ export const DetectorBasicDetailsView: React.FC = content: ( <> {inputs[0].detector_input.indices.map((ind: string) => ( - {ind} + {ind} ))} ), diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx index da8599dfd..e85131194 100644 --- a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx +++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx @@ -12,6 +12,7 @@ import { FieldMapping } from '../../../../../models/interfaces'; import { errorNotificationToast } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { Detector } from '../../../../../types'; +import { getMappingFields } from '../../utils/helpers'; export interface FieldMappingsViewProps { detector: Detector; @@ -60,13 +61,11 @@ export const FieldMappingsView: React.FC = ({ if (getMappingRes?.ok) { const mappingsData = getMappingRes.response[indexName]; if (mappingsData) { - let items: FieldMappingsTableItem[] = []; - Object.entries(mappingsData.mappings.properties).forEach((entry) => { - items.push({ - ruleFieldName: entry[0], - logFieldName: entry[1].path, - }); - }); + const items: FieldMappingsTableItem[] = getMappingFields( + mappingsData.mappings.properties, + [], + '' + ); setFieldMappingItems(items); } 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 75c01fb24..d0da566e6 100644 --- a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap +++ b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap @@ -1854,7 +1854,9 @@ exports[` spec renders the component 1`] = ` onBlur={[Function]} onFocus={[Function]} > - +
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 c31dc65f6..dbeb80f0b 100644 --- a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap +++ b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap @@ -897,7 +897,9 @@ exports[` spec renders the component 1`] = ` onBlur={[Function]} onFocus={[Function]} > - +
diff --git a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx index c3fb44c31..a7ed79344 100644 --- a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx +++ b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx @@ -19,6 +19,8 @@ import { FieldMapping } from '../../../../../models/interfaces'; import FieldMappingService from '../../../../services/FieldMappingService'; import { MappingViewType } from '../../../CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable'; import { Detector } from '../../../../../types'; +import { FieldMappingsTableItem } from '../../../CreateDetector/models/interfaces'; +import { getMappingFields } from '../../utils/helpers'; import _ from 'lodash'; export interface ruleFieldToIndexFieldMap { @@ -117,13 +119,14 @@ export default class EditFieldMappings extends Component< const mappingsRes = await this.props.fieldMappingService?.getMappings(indexName); if (mappingsRes?.ok) { const mappedFieldsInfo = mappingsRes.response[indexName].mappings.properties; - let mappedRuleFields = Object.keys(mappedFieldsInfo); + const items: FieldMappingsTableItem[] = getMappingFields(mappedFieldsInfo, [], ''); + let mappedRuleFields = _.map(items, 'ruleFieldName'); unmappedRuleFields = unmappedRuleFields.filter((ruleField) => { return !mappedRuleFields.includes(ruleField); }); - mappedRuleFields.forEach((ruleField) => { - existingMappings[ruleField] = mappedFieldsInfo[ruleField].path; + items.forEach((ruleField) => { + existingMappings[ruleField.ruleFieldName] = ruleField.logFieldName; }); for (let key in existingMappings) { @@ -241,18 +244,20 @@ export default class EditFieldMappings extends Component< } > - - {...this.props} - loading={loading} - ruleFields={mappedRuleFields} - indexFields={logFieldOptions} - mappingProps={{ - type: MappingViewType.Edit, - existingMappings, - invalidMappingFieldNames, - onMappingCreation: this.onMappingCreation, - }} - /> +
+ + {...this.props} + loading={loading} + ruleFields={mappedRuleFields} + indexFields={logFieldOptions} + mappingProps={{ + type: MappingViewType.Edit, + existingMappings, + invalidMappingFieldNames, + onMappingCreation: this.onMappingCreation, + }} + /> +
@@ -272,18 +277,20 @@ export default class EditFieldMappings extends Component< - - {...this.props} - loading={loading} - ruleFields={unmappedRuleFields} - indexFields={logFieldOptions} - mappingProps={{ - type: MappingViewType.Edit, - existingMappings, - invalidMappingFieldNames, - onMappingCreation: this.onMappingCreation, - }} - /> +
+ + {...this.props} + loading={loading} + ruleFields={unmappedRuleFields} + indexFields={logFieldOptions} + mappingProps={{ + type: MappingViewType.Edit, + existingMappings, + invalidMappingFieldNames, + onMappingCreation: this.onMappingCreation, + }} + /> +
) : ( 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 c9551db91..e934dc1c0 100644 --- a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap +++ b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap @@ -293,277 +293,209 @@ exports[` spec renders the component 1`] = ` className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall" />
- + - -

- There are no field mappings. -

- - } - style={ - Object { - "maxWidth": "45em", - } - } - /> + ], + "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", + } } - onTableChange={[Function]} - pagination={ - Object { - "pageIndex": 0, + fieldMappings={Array []} + filedMappingService={ + FieldMappingService { + "createMappings": [Function], + "getMappings": [Function], + "getMappingsView": [Function], + "httpClient": [MockFunction], } } - responsive={true} - sorting={ + indexFields={Array []} + loading={false} + mappingProps={ Object { - "sort": Object { - "direction": "asc", - "field": "ruleFieldName", - }, + "existingMappings": Object {}, + "invalidMappingFieldNames": Array [], + "onMappingCreation": [Function], + "type": 1, } } - tableLayout="fixed" + replaceFieldMappings={[MockFunction]} + ruleFields={Array []} > - spec renders the component 1`] = ` isSelectable={false} items={Array []} loading={false} - noItemsMessage={ + message={ @@ -617,106 +549,151 @@ exports[` spec renders the component 1`] = ` } /> } - onChange={[Function]} + onTableChange={[Function]} pagination={ Object { - "hidePerPageOptions": undefined, "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "totalItemCount": 0, } } responsive={true} sorting={ Object { - "allowNeutralSort": true, "sort": Object { "direction": "asc", - "field": "Detector field name", + "field": "ruleFieldName", }, } } tableLayout="fixed" > -
+

+ There are no field mappings. +

+ + } + style={ + Object { + "maxWidth": "45em", + } + } + /> + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 25, + 50, + ], + "totalItemCount": 0, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": Object { + "direction": "asc", + "field": "Detector field name", + }, + } + } + tableLayout="fixed" > -
- -
- +
+ +
-
- -
- - -
- + + +
-
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" +
-
-
+ spec renders the component 1`] = ` onClick={[Function]} size="xs" > - - + + + +
-
-
-
- -
-
-
- -
- - - + + + + + + + + + - - - - - - - - + + + + + - - - - + + - - Detector field name - - - - - - - - - + + - - - - + + - - Maps to - - - - - - - - - + + - - - - - - Log source field name - - - - - - - - - - - - - - - - - - + + - + + + + + + + + - - - - - -
- -
+ +
+ +
+ Detector field name + + + + + + + Maps to + + + + + + - -
-
- - -

- There are no field mappings. -

- - } - style={ - Object { - "maxWidth": "45em", - } - } + -
+ + + Status + + + + + + + + +
+
+ + +

+ There are no field mappings. +

+ + } style={ Object { "maxWidth": "45em", } } > - - - -
- -
-

- There are no field mappings. -

-
-
-
-
-
-
-
- - - -
-
+ + +
+ +
+

+ There are no field mappings. +

+
+
+
+
+
+ +
+ + +
+ + + + + + + + +
-
- - - + + + +
diff --git a/public/pages/Detectors/utils/helpers.ts b/public/pages/Detectors/utils/helpers.ts index 6be7e84a2..1794f31de 100644 --- a/public/pages/Detectors/utils/helpers.ts +++ b/public/pages/Detectors/utils/helpers.ts @@ -4,6 +4,7 @@ */ import { DetectorHit } from '../../../../server/models/interfaces'; +import { FieldMappingsTableItem } from '../../../../types'; export function getDetectorIds(detectors: DetectorHit[]) { return detectors.map((detector) => detector._id).join(', '); @@ -12,3 +13,23 @@ export function getDetectorIds(detectors: DetectorHit[]) { export function getDetectorNames(detectors: DetectorHit[]) { return detectors.map((detector) => detector._source.name).join(', '); } + +export const getMappingFields = ( + properties: any, + items: FieldMappingsTableItem[] = [], + prefix: string = '' +): FieldMappingsTableItem[] => { + for (let field in properties) { + const fullFieldName = prefix ? `${prefix}.${field}` : field; + const nextProperties = properties[field].properties; + if (!nextProperties) { + items.push({ + ruleFieldName: fullFieldName, + logFieldName: properties[field].path, + }); + } else { + getMappingFields(nextProperties, items, fullFieldName); + } + } + return items; +}; diff --git a/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss new file mode 100644 index 000000000..ed98fdddb --- /dev/null +++ b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.scss @@ -0,0 +1,6 @@ +.correlations-table-details-row { + .correlations-table-details-row-value { + font-weight: 600; + color: $euiColorDarkestShade; + } +} diff --git a/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx new file mode 100644 index 000000000..90783f294 --- /dev/null +++ b/public/pages/Findings/components/CorrelationsTable/CorrelationsTable.tsx @@ -0,0 +1,180 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { CorrelationFinding } from '../../../../../types'; +import { ruleTypes } from '../../../Rules/utils/constants'; +import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants'; +import { getSeverityBadge } from '../../../Rules/utils/helpers'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiPanel, + EuiInMemoryTable, + EuiBasicTableColumn, +} from '@elastic/eui'; +import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; +import { FindingItemType } from '../../containers/Findings/Findings'; +import { RouteComponentProps } from 'react-router-dom'; +import { DataStore } from '../../../../store/DataStore'; + +export interface CorrelationsTableProps { + finding: FindingItemType; + correlatedFindings: CorrelationFinding[]; + history: RouteComponentProps['history']; + isLoading: boolean; +} + +export const CorrelationsTable: React.FC = ({ + correlatedFindings, + finding, + history, + isLoading, +}) => { + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ + [key: string]: JSX.Element; + }>({}); + + const toggleCorrelationDetails = (item: any) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + itemIdToExpandedRowMapValues[item.id] = ( + + + + Finding ID + + + + {item.id} + + + + + + + Threat detector + + + + {item.detectorName} + + + + + + + Detection rule + + + + {item.detectionRule?.name || '-'} + + + + + ); + } + + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; + + const columns: EuiBasicTableColumn[] = [ + { + field: 'timestamp', + name: 'Time', + sortable: true, + }, + { + name: 'Correlated rule', + truncateText: true, + render: (item: CorrelationFinding) => item?.correlationRule.name || DEFAULT_EMPTY_DATA, + }, + { + field: 'logType', + name: 'Log type', + sortable: true, + render: (category: string) => + // TODO: This formatting may need some refactoring depending on the response payload + ruleTypes.find((ruleType) => ruleType.value === category)?.label || DEFAULT_EMPTY_DATA, + }, + { + name: 'Rule severity', + truncateText: true, + align: 'center', + render: (item: CorrelationFinding) => getSeverityBadge(item.detectionRule.severity), + }, + { + field: 'correlationScore', + name: 'Score', + sortable: true, + }, + { + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + render: (item: any) => ( + toggleCorrelationDetails(item)} + aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + /> + ), + }, + ]; + + const goToCorrelationsPage = () => { + DataStore.findings.closeFlyout(); + history.push({ + pathname: `${ROUTES.CORRELATIONS}`, + state: { + finding: finding, + correlatedFindings: correlatedFindings, + }, + }); + }; + + return ( + <> + + + +

Correlated findings

+
+
+ + goToCorrelationsPage()} + disabled={correlatedFindings.length === 0} + > + View correlations graph + + +
+ + + + + + + ); +}; diff --git a/public/pages/Findings/components/FindingDetailsFlyout.tsx b/public/pages/Findings/components/FindingDetailsFlyout.tsx index 948899f60..a8c356d1e 100644 --- a/public/pages/Findings/components/FindingDetailsFlyout.tsx +++ b/public/pages/Findings/components/FindingDetailsFlyout.tsx @@ -32,6 +32,7 @@ import { EuiTab, EuiInMemoryTable, EuiBasicTableColumn, + EuiLoadingContent, } from '@elastic/eui'; import { capitalizeFirstLetter, renderTime } from '../../../utils/helpers'; import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../utils/constants'; @@ -39,24 +40,27 @@ import { Query } from '../models/interfaces'; import { RuleViewerFlyout } from '../../Rules/components/RuleViewerFlyout/RuleViewerFlyout'; import { RuleSource } from '../../../../server/models/interfaces'; import { OpenSearchService, IndexPatternsService, CorrelationService } from '../../../services'; -import { getSeverityBadge, RuleTableItem } from '../../Rules/utils/helpers'; +import { RuleTableItem } from '../../Rules/utils/helpers'; import { CreateIndexPatternForm } from './CreateIndexPatternForm'; import { FindingItemType } from '../containers/Findings/Findings'; import { CorrelationFinding, RuleItemInfoBase } from '../../../../types'; import { FindingFlyoutTabId, FindingFlyoutTabs } from '../utils/constants'; import { DataStore } from '../../../store/DataStore'; -import { RouteComponentProps } from 'react-router-dom'; import { ruleTypes } from '../../Rules/utils/constants'; +import { CorrelationsTable } from './CorrelationsTable/CorrelationsTable'; -interface FindingDetailsFlyoutProps extends RouteComponentProps { +export interface FindingDetailsFlyoutBaseProps { finding: FindingItemType; findings: FindingItemType[]; + shouldLoadAllFindings: boolean; backButton?: React.ReactNode; - allRules: { [id: string]: RuleSource }; +} + +export interface FindingDetailsFlyoutProps extends FindingDetailsFlyoutBaseProps { opensearchService: OpenSearchService; indexPatternsService: IndexPatternsService; correlationService: CorrelationService; - closeFlyout: () => void; + history: History; } interface FindingDetailsFlyoutState { @@ -66,6 +70,9 @@ interface FindingDetailsFlyoutState { isCreateIndexPatternModalVisible: boolean; selectedTab: { id: string; content: React.ReactNode | null }; correlatedFindings: CorrelationFinding[]; + allRules: { [id: string]: RuleSource }; + isDocumentLoading: boolean; + areCorrelationsLoading: boolean; } export default class FindingDetailsFlyout extends Component< @@ -78,41 +85,86 @@ export default class FindingDetailsFlyout extends Component< loading: false, ruleViewerFlyoutData: null, isCreateIndexPatternModalVisible: false, - selectedTab: { id: FindingFlyoutTabId.DETAILS, content: null }, + selectedTab: { + id: FindingFlyoutTabId.DETAILS, + content: ( + <> + +

Rule details

+
+ + + + ), + }, correlatedFindings: [], + isDocumentLoading: true, + areCorrelationsLoading: true, + allRules: {}, }; } - componentDidMount(): void { - this.getIndexPatternId().then((patternId) => { - if (patternId) { - this.setState({ indexPatternId: patternId }); - } - }); - + getCorrelations = async () => { const { id, detector } = this.props.finding; - const allFindings = this.props.findings; - DataStore.correlationsStore - .getCorrelatedFindings(id, detector._source?.detector_type) - .then((findings) => { - if (findings?.correlatedFindings.length) { - let correlatedFindings: any[] = []; - findings.correlatedFindings.map((finding) => { - allFindings.map((item) => { - if (finding.id === item.id) { - correlatedFindings.push(finding); - } + let allFindings = this.props.findings; + if (this.props.shouldLoadAllFindings) { + // if findings come from the alerts fly-out, we need to get all the findings to match those with the correlations + allFindings = await DataStore.findings.getAllFindings(); + } + + DataStore.correlations.getCorrelationRules().then((correlationRules) => { + DataStore.correlations + .getCorrelatedFindings(id, detector._source?.detector_type) + .then((findings) => { + if (findings?.correlatedFindings.length) { + let correlatedFindings: any[] = []; + findings.correlatedFindings.map((finding: CorrelationFinding) => { + allFindings.map((item: FindingItemType) => { + if (finding.id === item.id) { + correlatedFindings.push({ + ...finding, + correlationRule: correlationRules.find( + (rule) => finding.rules?.indexOf(rule.id) !== -1 + ), + }); + } + }); }); + this.setState({ correlatedFindings }); + } + }) + .finally(() => { + this.setState({ + areCorrelationsLoading: false, }); - this.setState({ correlatedFindings }); + }); + }); + }; + + componentDidMount(): void { + this.getIndexPatternId() + .then((patternId) => { + if (patternId) { + this.setState({ indexPatternId: patternId }); } + }) + .finally(() => { + this.setState({ isDocumentLoading: false }); }); - this.setState({ - selectedTab: { - id: FindingFlyoutTabId.DETAILS, - content: this.getTabContent(FindingFlyoutTabId.DETAILS), - }, + this.getCorrelations(); + + DataStore.rules.getAllRules().then((rules) => { + const allRules: { [id: string]: RuleSource } = {}; + rules.forEach((hit) => (allRules[hit._id] = hit._source)); + this.setState({ allRules }, () => { + this.setState({ + selectedTab: { + id: FindingFlyoutTabId.DETAILS, + content: this.getTabContent(FindingFlyoutTabId.DETAILS), + }, + }); + }); }); } @@ -153,7 +205,7 @@ export default class FindingDetailsFlyout extends Component< }; renderRuleDetails = (rules: Query[] = []) => { - const { allRules } = this.props; + const { allRules = {} } = this.state; return rules.map((rule, key) => { const fullRule = allRules[rule.id]; const severity = capitalizeFirstLetter(fullRule.level); @@ -246,7 +298,7 @@ export default class FindingDetailsFlyout extends Component< return patternId; }; - renderFindingDocuments() { + renderFindingDocuments(isDocumentLoading: boolean) { const { finding: { index, document_list, related_doc_ids }, } = this.props; @@ -266,6 +318,7 @@ export default class FindingDetailsFlyout extends Component< { if (indexPatternId) { @@ -361,94 +414,24 @@ export default class FindingDetailsFlyout extends Component< } } - private getTabContent(tabId: FindingFlyoutTabId) { + private getTabContent(tabId: FindingFlyoutTabId, isDocumentLoading = false) { switch (tabId) { case FindingFlyoutTabId.CORRELATIONS: - return this.createCorrelationsTable(); + return ( + + ); case FindingFlyoutTabId.DETAILS: default: - return this.createFindingDetails(); + return this.createFindingDetails(isDocumentLoading); } } - private goToCorrelationsPage = () => { - const { correlatedFindings } = this.state; - const { finding } = this.props; - - this.props.history.push({ - pathname: `${ROUTES.CORRELATIONS}`, - state: { - finding: finding, - correlatedFindings: correlatedFindings, - }, - }); - }; - - private createCorrelationsTable() { - const columns: EuiBasicTableColumn[] = [ - { - field: 'timestamp', - name: 'Time', - sortable: true, - }, - { - field: 'id', - name: 'Correlated finding id', - }, - { - field: 'logType', - name: 'Log type', - sortable: true, - render: (category: string) => - // TODO: This formatting may need some refactoring depending on the response payload - ruleTypes.find((ruleType) => ruleType.value === category)?.label || DEFAULT_EMPTY_DATA, - }, - { - name: 'Rule severity', - truncateText: true, - align: 'center', - render: (item: CorrelationFinding) => getSeverityBadge(item.detectionRule.severity), - }, - { - field: 'correlationScore', - name: 'Score', - sortable: true, - }, - ]; - - return ( - <> - - - -

Correlated findings

-
-
- - this.goToCorrelationsPage()} - disabled={this.state.correlatedFindings.length === 0} - > - View correlations graph - - -
- - - - - - - ); - } - - private createFindingDetails() { + private createFindingDetails(isDocumentLoading: boolean) { const { finding: { queries }, } = this.props; @@ -461,13 +444,13 @@ export default class FindingDetailsFlyout extends Component< {this.renderRuleDetails(queries)} - {this.renderFindingDocuments()} + {this.renderFindingDocuments(isDocumentLoading)} ); } render() { - const { closeFlyout, backButton } = this.props; + const { backButton } = this.props; const { finding: { id, @@ -478,9 +461,10 @@ export default class FindingDetailsFlyout extends Component< timestamp, }, } = this.props; + const { isDocumentLoading } = this.state; return (
@@ -558,11 +542,24 @@ export default class FindingDetailsFlyout extends Component< isSelected={tab.id === this.state.selectedTab.id} onClick={() => { this.setState({ - selectedTab: { id: tab.id, content: this.getTabContent(tab.id) }, + selectedTab: { + id: tab.id, + content: this.getTabContent(tab.id, isDocumentLoading), + }, }); }} > - {tab.name} + {tab.id === 'Correlations' ? ( + <> + {tab.name} ( + {this.state.areCorrelationsLoading + ? DEFAULT_EMPTY_DATA + : this.state.correlatedFindings.length} + ) + + ) : ( + tab.name + )} ); })} diff --git a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx index 2b0c2fa3b..2790af14c 100644 --- a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx +++ b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx @@ -24,13 +24,13 @@ import { IndexPatternsService, CorrelationService, } from '../../../../services'; -import FindingDetailsFlyout from '../FindingDetailsFlyout'; import { Finding } from '../../models/interfaces'; import CreateAlertFlyout from '../CreateAlertFlyout'; import { NotificationChannelTypeOptions } from '../../../CreateDetector/components/ConfigureAlerts/models/interfaces'; import { FindingItemType } from '../../containers/Findings/Findings'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { RuleSource } from '../../../../../server/models/interfaces'; +import { DataStore } from '../../../../store/DataStore'; interface FindingsTableProps extends RouteComponentProps { detectorService: DetectorsService; @@ -114,41 +114,6 @@ export default class FindingsTable extends Component { - if (this.state.flyoutOpen) this.closeFlyout(); - else { - const { findings, rules } = this.props; - const { findingsFiltered, filteredFindings } = this.state; - - const logTypes = new Set(); - const severities = new Set(); - filteredFindings.forEach((finding) => { - if (finding) { - const queryId = finding.queries[0].id; - logTypes.add(rules[queryId].category); - severities.add(rules[queryId].level); - } - }); - - this.setState({ - flyout: ( - - ), - flyoutOpen: true, - selectedFinding: finding, - }); - } - }; - renderCreateAlertFlyout = (finding: Finding) => { if (this.state.flyoutOpen) this.closeFlyout(); else { @@ -206,7 +171,7 @@ export default class FindingsTable extends Component ( this.renderFindingDetailsFlyout(finding)} + onClick={() => DataStore.findings.openFlyout(finding, this.state.filteredFindings)} data-test-subj={'finding-details-flyout-button'} > {`${(id as string).slice(0, 7)}...`} @@ -253,7 +218,9 @@ export default class FindingsTable extends Component this.renderFindingDetailsFlyout(finding)} + onClick={() => + DataStore.findings.openFlyout(finding, this.state.filteredFindings) + } />
), diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index e2cfd639b..f8df933cf 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -45,6 +45,9 @@ import { DataStore } from '../../store/DataStore'; import { CreateCorrelationRule } from '../Correlations/containers/CreateCorrelationRule'; import { CorrelationRules } from '../Correlations/containers/CorrelationRules'; import { Correlations } from '../Correlations/containers/CorrelationsContainer'; +import FindingDetailsFlyout, { + FindingDetailsFlyoutBaseProps, +} from '../Findings/components/FindingDetailsFlyout'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -82,6 +85,7 @@ interface MainState { dateTimeFilter: DateTimeFilter; callout?: ICalloutProps; toasts?: Toast[]; + findingFlyout: FindingDetailsFlyoutBaseProps | null; } const navItemIndexByRoute: { [route: string]: number } = { @@ -102,11 +106,19 @@ export default class Main extends Component { startTime: DEFAULT_DATE_RANGE.start, endTime: DEFAULT_DATE_RANGE.end, }, + findingFlyout: null, }; DataStore.detectors.setHandlers(this.showCallout, this.showToast); + DataStore.findings.setFlyoutCallback(this.showFindingFlyout); } + showFindingFlyout = (findingFlyout: FindingDetailsFlyoutBaseProps | null) => { + this.setState({ + findingFlyout, + }); + }; + showCallout = (callout?: ICalloutProps) => { this.setState({ callout, @@ -180,7 +192,7 @@ export default class Main extends Component { history, } = this.props; - const { callout } = this.state; + const { callout, findingFlyout } = this.state; const sideNav: EuiSideNavItemType<{ style: any }>[] = [ { name: Navigation.SecurityAnalytics, @@ -302,6 +314,15 @@ export default class Main extends Component { )} {callout ? : null} + {findingFlyout ? ( + + ) : null} { - constructor(private readonly initializerContext: PluginInitializerContext) { - // can retrieve config from initializerContext - } - public setup( core: CoreSetup, - plugins: SecurityAnalyticsPluginSetupDeps + _plugins: SecurityAnalyticsPluginSetupDeps ): SecurityAnalyticsPluginSetup { core.application.register({ id: PLUGIN_NAME, @@ -52,10 +42,12 @@ export class SecurityAnalyticsPlugin return renderApp(coreStart, params, ROUTES.LANDING_PAGE, depsStart); }, }); + setDarkMode(core.uiSettings.get('theme:darkMode')); + return {}; } - public start(core: CoreStart): SecurityAnalyticsPluginStart { + public start(_core: CoreStart): SecurityAnalyticsPluginStart { return {}; } } diff --git a/public/services/IndexPatternsService.ts b/public/services/IndexPatternsService.ts index 69796a37e..d65d1ba42 100644 --- a/public/services/IndexPatternsService.ts +++ b/public/services/IndexPatternsService.ts @@ -35,7 +35,6 @@ export default class IndexPatternsService { ): Promise> | Promise> => { let indexPattern; const indexPatterns = await this.getIndexPatterns(); - console.log('indexPatterns', indexPatterns); indexPatterns?.some((indexRef) => { if (indexRef.references.findIndex((reference) => reference.id === detectorId) > -1) { indexPattern = indexRef; diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 9faffc2f9..89fa8795d 100644 --- a/public/store/CorrelationsStore.ts +++ b/public/store/CorrelationsStore.ts @@ -4,9 +4,10 @@ */ import { + CorrelationFieldCondition, CorrelationFinding, CorrelationRule, - CorrelationRuleHit, + CorrelationRuleQuery, ICorrelationsStore, IRulesStore, } from '../../types'; @@ -15,6 +16,10 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../utils/helpers'; import { DEFAULT_EMPTY_DATA } from '../utils/constants'; +export interface ICorrelationsCache { + [key: string]: CorrelationRule[]; +} + export class CorrelationsStore implements ICorrelationsStore { /** * Correlation rules service instance @@ -33,6 +38,21 @@ export class CorrelationsStore implements ICorrelationsStore { */ readonly notifications: NotificationsStart; + /** + * Keeps rule's data cached + * + * @property {ICorrelationsCache} cache + */ + private cache: ICorrelationsCache = {}; + + /** + * Invalidates all rules data + */ + private invalidateCache = () => { + this.cache = {}; + return this; + }; + constructor( service: CorrelationService, detectorsService: DetectorsService, @@ -47,7 +67,7 @@ export class CorrelationsStore implements ICorrelationsStore { } public async createCorrelationRule(correlationRule: CorrelationRule): Promise { - const response = await this.service.createCorrelationRule({ + const response = await this.invalidateCache().service.createCorrelationRule({ name: correlationRule.name, correlate: correlationRule.queries?.map((query) => ({ index: query.index, @@ -67,18 +87,38 @@ export class CorrelationsStore implements ICorrelationsStore { return response.ok; } - public async getCorrelationRules(index?: string): Promise { + public async getCorrelationRules(index?: string): Promise { + const cacheKey: string = `getCorrelationRules:${JSON.stringify(arguments)}`; + + if (this.cache[cacheKey]) { + return this.cache[cacheKey]; + } + const response = await this.service.getCorrelationRules(index); if (response?.ok) { - return response.response.hits.hits; + return (this.cache[cacheKey] = response.response.hits.hits.map((hit) => { + const queries: CorrelationRuleQuery[] = hit._source.correlate.map((queryData) => { + return { + index: queryData.index, + logType: queryData.category, + conditions: this.parseRuleQueryString(queryData.query), + }; + }); + + return { + id: hit._id, + name: hit._source.name, + queries, + }; + })); } return []; } public async deleteCorrelationRule(ruleId: string): Promise { - const response = await this.service.deleteCorrelationRule(ruleId); + const response = await this.invalidateCache().service.deleteCorrelationRule(ruleId); if (!response.ok) { errorNotificationToast(this.notifications, 'delete', 'correlation rule', response.error); @@ -137,8 +177,11 @@ export class CorrelationsStore implements ICorrelationsStore { const rule = allRules.find((rule) => rule._id === f.queries[0].id); findings[f.id] = { + ...f, id: f.id, logType: detector._source.detector_type, + detector: detector, + detectorName: detector._source.name, timestamp: new Date(f.timestamp).toLocaleString(), detectionRule: rule ? { @@ -186,6 +229,7 @@ export class CorrelationsStore implements ICorrelationsStore { return { finding: { + ...allFindings[finding], id: finding, logType: detector_type, timestamp: '', @@ -194,4 +238,25 @@ export class CorrelationsStore implements ICorrelationsStore { correlatedFindings: [], }; } + + private parseRuleQueryString(queryString: string): CorrelationFieldCondition[] { + const queries: CorrelationFieldCondition[] = []; + const orConditions = queryString.trim().split(/ OR /gi); + + orConditions.forEach((cond, conditionIndex) => { + cond.split(/ AND /gi).forEach((fieldInfo: string, index: number) => { + const s = fieldInfo.match(/(?:\\:|[^:])+/g); + if (s) { + const [name, value] = s; + queries.push({ + name, + value, + condition: index === 0 && conditionIndex !== 0 ? 'OR' : 'AND', + }); + } + }); + }); + + return queries; + } } diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts index 2f2906a81..4f085d1e7 100644 --- a/public/store/DataStore.ts +++ b/public/store/DataStore.ts @@ -7,13 +7,14 @@ import { RulesStore } from './RulesStore'; import { BrowserServices } from '../models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { DetectorsStore } from './DetectorsStore'; -import { ICorrelationsStore } from '../../types'; import { CorrelationsStore } from './CorrelationsStore'; +import { FindingsStore } from './FindingsStore'; export class DataStore { public static rules: RulesStore; public static detectors: DetectorsStore; - public static correlationsStore: ICorrelationsStore; + public static correlations: CorrelationsStore; + public static findings: FindingsStore; public static init = (services: BrowserServices, notifications: NotificationsStart) => { const rulesStore = new RulesStore(services.ruleService, notifications); @@ -25,7 +26,13 @@ export class DataStore { services.savedObjectsService ); - DataStore.correlationsStore = new CorrelationsStore( + DataStore.findings = new FindingsStore( + services.findingsService, + services.detectorsService, + notifications + ); + + DataStore.correlations = new CorrelationsStore( services.correlationsService, services.detectorsService, services.findingsService, diff --git a/public/store/FindingsStore.ts b/public/store/FindingsStore.ts new file mode 100644 index 000000000..6fd8532f1 --- /dev/null +++ b/public/store/FindingsStore.ts @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DetectorsService, FindingsService } from '../services'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { RouteComponentProps } from 'react-router-dom'; +import { errorNotificationToast } from '../utils/helpers'; +import { FindingItemType } from '../pages/Findings/containers/Findings/Findings'; +import { FindingDetailsFlyoutBaseProps } from '../pages/Findings/components/FindingDetailsFlyout'; + +export interface IFindingsStore { + readonly service: FindingsService; + + readonly detectorsService: DetectorsService; + + readonly notifications: NotificationsStart; + + getFindingsPerDetector: (detectorId: string) => Promise; + + getAllFindings: () => Promise; + + setFlyoutCallback: ( + flyoutCallback: (findingFlyout: FindingDetailsFlyoutBaseProps | null) => void + ) => void; + + openFlyout: ( + finding: FindingItemType, + findings: FindingItemType[], + shouldLoadAllFindings: boolean, + backButton?: React.ReactNode + ) => void; + + closeFlyout: () => void; +} + +export interface IFindingsCache {} + +/** + * Findings store + * + * @class FindingsStore + * @implements IDetectorsStore + * @param {BrowserServices} services Uses services to make API requests + */ +export class FindingsStore implements IFindingsStore { + /** + * Findings service instance + * + * @property {FindingsService} service + * @readonly + */ + readonly service: FindingsService; + + /** + * Detectors service instance + * + * @property {DetectorsService} detectorsService + * @readonly + */ + readonly detectorsService: DetectorsService; + + /** + * Notifications + * @property {NotificationsStart} + * @readonly + */ + readonly notifications: NotificationsStart; + + /** + * Router history + * @property {RouteComponentProps['history']} + * @readonly + */ + history: RouteComponentProps['history'] | undefined = undefined; + + constructor( + service: FindingsService, + detectorsService: DetectorsService, + notifications: NotificationsStart + ) { + this.service = service; + this.detectorsService = detectorsService; + this.notifications = notifications; + } + + public getFindingsPerDetector = async (detectorId: string): Promise => { + let allFindings: FindingItemType[] = []; + const findingRes = await this.service.getFindings({ detectorId }); + if (findingRes.ok) { + allFindings = findingRes.response.findings as FindingItemType[]; + } else { + errorNotificationToast(this.notifications, 'retrieve', 'findings', findingRes.error); + } + + return allFindings; + }; + + public getAllFindings = async (): Promise => { + let allFindings: FindingItemType[] = []; + const detectorsRes = await this.detectorsService.getDetectors(); + if (detectorsRes.ok) { + const detectors = detectorsRes.response.hits.hits; + + for (let detector of detectors) { + const findings = await this.getFindingsPerDetector(detector._id); + const findingsPerDetector: FindingItemType[] = findings.map((finding) => { + return { + ...finding, + detectorName: detector._source.name, + logType: detector._source.detector_type, + detector: detector, + }; + }); + allFindings = allFindings.concat(findingsPerDetector); + } + } + + return allFindings; + }; + + public setFlyoutCallback = ( + flyoutCallback: (findingFlyout: FindingDetailsFlyoutBaseProps | null) => void + ): void => { + this.openFlyoutCallback = flyoutCallback; + }; + + public openFlyoutCallback = (findingFlyout: FindingDetailsFlyoutBaseProps | null) => {}; + + closeFlyout = () => this.openFlyoutCallback(null); + + public openFlyout = ( + finding: FindingItemType, + findings: FindingItemType[], + shouldLoadAllFindings: boolean = false, + backButton?: React.ReactNode + ) => { + const flyout = { + finding, + findings, + shouldLoadAllFindings, + backButton, + } as FindingDetailsFlyoutBaseProps; + this.openFlyoutCallback(flyout); + }; +} diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 30697890b..daf2aca12 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -16,11 +16,14 @@ export const DEFAULT_DATE_RANGE = { start: 'now-24h', end: 'now' }; export const PLUGIN_NAME = 'opensearch_security_analytics_dashboards'; export const OS_NOTIFICATION_PLUGIN = 'opensearch-notifications'; -// TODO: Replace with actual documentation link once it's available -export const DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/'; - export const DEFAULT_EMPTY_DATA = '-'; +export let isDarkMode: boolean = false; + +export function setDarkMode(isDarkModeSetting: boolean) { + isDarkMode = isDarkModeSetting; +} + export const ROUTES = Object.freeze({ ALERTS: '/alerts', DETECTORS: '/detectors', diff --git a/types/Correlations.ts b/types/Correlations.ts index a5eac1320..4db43c44e 100644 --- a/types/Correlations.ts +++ b/types/Correlations.ts @@ -13,6 +13,8 @@ export enum CorrelationsLevel { Finding = 'Finding', } +export type CorrelationRuleAction = 'Create' | 'Edit' | 'Readonly'; + export interface CorrelationGraphData { graph: { nodes: (Node & { chosen?: boolean })[]; @@ -24,9 +26,12 @@ export interface CorrelationGraphData { export type CorrelationFinding = { id: string; correlationScore?: number; + correlationRule?: CorrelationFindingHit; logType: string; timestamp: string; detectionRule: { name: string; severity: string }; + detectorName?: string; + rules?: string[]; }; export interface CorrelationRuleQuery { @@ -41,15 +46,15 @@ export interface CorrelationFieldCondition { condition: 'AND' | 'OR'; } -export interface CorrelationRule extends CorrelationRuleModel { - id: string; -} - export interface CorrelationRuleModel { name: string; queries: CorrelationRuleQuery[]; } +export interface CorrelationRule extends CorrelationRuleModel { + id: string; +} + export interface CorrelationRuleSourceQueries { index: string; query: string; @@ -104,10 +109,8 @@ export interface CreateCorrelationRuleResponse { export interface DeleteCorrelationRuleResponse {} -export type CorrelationRuleTableItem = CorrelationRule & { logTypes: string }; - export interface ICorrelationsStore { - getCorrelationRules(): Promise; + getCorrelationRules(): Promise; getCorrelatedFindings( finding: string, detector_type: string,