From e90b633639aa408e22a5d884436217631fa67718 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 4 May 2023 20:36:30 +0200 Subject: [PATCH 1/2] 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 --- 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 | 582 ++++---- 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 | 275 ++++ cypress/support/index.d.ts | 105 ++ .../containers/ConfigureFieldMapping.tsx | 52 +- .../FieldMappings/EditFieldMapping.tsx | 52 +- .../EditFieldMappings.test.tsx.snap | 1260 +++++++++-------- 31 files changed, 1577 insertions(+), 1333 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 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 17987b68b..0ad469c2d 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -4,153 +4,224 @@ */ 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'; 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 = () => { + 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; + let mappingFields = {}; + unmapped_field_aliases.map((field) => { + mappingFields[field] = undefined; + }); + + if (_.isEmpty(properties)) { + validatePendingFieldMappingsPanel(Object.entries(mappingFields)); + } else { + validateAutomaticFieldMappingsPanel(Object.entries(properties)); + } }); }); +}; - // 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); }); - } - } + }); +}; + +const createDetector = (detectorName, dataSource, expectFailure) => { + getCreateDetectorButton().click({ force: true }); - // Click Next button to continue - cy.get('button').contains('Next').click({ force: true }); + // TEST DETAILS PAGE + getNameField().type(detectorName); + getDataSourceField().selectComboboxItem(dataSource); - // Check that correct page now showing - cy.contains('Set up alert triggers'); + selectDnsLogType(); - // Type name of new trigger - cy.get(`input[placeholder="Enter a name to describe the alert condition"]`) - .focus() - .realType('test_trigger'); + 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); - } + cy.getElementByText('.euiTitle', 'Field mapping') + .parentsUntil('.euiPanel') + .siblings() + .eq(2) + .validateTable(Object.entries(dns_mapping_fields)); } - // Confirm entries user 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) { + cy.wait('@getMappingFields'); + cy.wait(2000); + cy.getElementByText('.euiTitle', 'Field mapping') + .parentsUntil('.euiPanel') + .siblings() + .eq(2) + .validateTable(Object.entries(dns_mapping_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 +236,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 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 +307,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(); - 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(); }); 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..2033d9967 --- /dev/null +++ b/cypress/support/helpers.js @@ -0,0 +1,275 @@ +/* + * 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) => { + tdData && cy.get($tr).find('td').contains(`${tdData}`); + }); + }); + }); + }); + } +); + +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/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/containers/FieldMappings/EditFieldMapping.tsx b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx index c3fb44c31..263dfdb08 100644 --- a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx +++ b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx @@ -241,18 +241,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 +274,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. +

+
+
+
+
+
+ +
+ + +
+ + + + + + + + +
-
- - - + + + +
From 9e683e2ab05fdd7965dcd73a7a69b1317a3450c4 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 4 May 2023 12:13:29 -0700 Subject: [PATCH 2/2] added link to rules; simplified rule parsing (#571) Signed-off-by: Amardeepsingh Siglani --- public/pages/Correlations/Correlations.scss | 31 +++- .../containers/CorrelationRules.tsx | 44 ++--- .../containers/CreateCorrelationRule.tsx | 168 ++++++++++++------ public/pages/Correlations/utils/helpers.tsx | 9 +- public/plugin.ts | 20 +-- public/store/CorrelationsStore.ts | 42 ++++- public/utils/constants.ts | 9 +- types/Correlations.ts | 6 +- 8 files changed, 210 insertions(+), 119 deletions(-) diff --git a/public/pages/Correlations/Correlations.scss b/public/pages/Correlations/Correlations.scss index 78038d353..33c931485 100644 --- a/public/pages/Correlations/Correlations.scss +++ b/public/pages/Correlations/Correlations.scss @@ -5,4 +5,33 @@ .correlation_rule_field_condition .euiButtonGroup__buttons { box-shadow: none; -} \ No newline at end of file +} + +#sa-correlations-network .vis-tooltip { + background-color: #535353; + padding: 0px; +} + +.readonly-text-color-light-mode { + &.euiFieldText { + color: #535353; + -webkit-text-fill-color: #535353; + } + + .euiComboBoxPill.euiComboBoxPill--plainText { + color: #535353; + -webkit-text-fill-color: #535353; + } +} + +.readonly-text-color-dark-mode { + &.euiFieldText { + color: #DFE5EF; + -webkit-text-fill-color: #DFE5EF; + } + + .euiComboBoxPill.euiComboBoxPill--plainText { + color: #DFE5EF; + -webkit-text-fill-color: #DFE5EF; + } +} diff --git a/public/pages/Correlations/containers/CorrelationRules.tsx b/public/pages/Correlations/containers/CorrelationRules.tsx index e9d2a27e6..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.correlations.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.correlations.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,7 +89,7 @@ export const CorrelationRules: React.FC = (props: RouteComp setIsDeleteModalVisible(false); }; - const onDeleteRuleConfirmed = async (rule: any) => { + const onDeleteRuleConfirmed = async () => { if (selectedRule) { const response = await DataStore.correlations.deleteCorrelationRule(selectedRule.id); @@ -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/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index 6d3b1efed..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; } @@ -53,6 +61,8 @@ export const CreateCorrelationRule: React.FC = ( props: CreateCorrelationRuleProps ) => { 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/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/plugin.ts b/public/plugin.ts index 89c02359c..d058114aa 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -3,14 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, -} from '../../../src/core/public'; -import { PLUGIN_NAME, ROUTES } from './utils/constants'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { PLUGIN_NAME, ROUTES, setDarkMode } from './utils/constants'; import { SecurityAnalyticsPluginSetup, SecurityAnalyticsPluginStart } from './index'; import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../src/plugins/data/public'; @@ -29,13 +23,9 @@ export class SecurityAnalyticsPlugin SecurityAnalyticsPluginSetupDeps, SecurityAnalyticsPluginStartDeps > { - 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/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 9faffc2f9..e735dd709 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'; @@ -67,11 +68,25 @@ export class CorrelationsStore implements ICorrelationsStore { return response.ok; } - public async getCorrelationRules(index?: string): Promise { + public async getCorrelationRules(index?: string): Promise { const response = await this.service.getCorrelationRules(index); if (response?.ok) { - return response.response.hits.hits; + return 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 []; @@ -194,4 +209,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/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..290638459 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 })[]; @@ -104,10 +106,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,