From 327dd231c01d19e601ac9fb27a2fd4d56391545e Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 7 Sep 2023 12:35:19 -0700 Subject: [PATCH] [2.10] Latest tests security analytics (#826) * updated tests Signed-off-by: Amardeepsingh Siglani * excluded a couple tests; fixed alerts tests Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani --- .../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 + .../sample_alias_mappings.json | 12 +- .../sample_detector.json | 14 +- .../sample_dns_index_settings.json | 21 + .../sample_document.json | 38 +- .../sample_field_mappings.json | 24 +- .../sample_index_settings.json | 33 - .../sample_windows_index_settings.json | 18 + .../1_detectors.spec.js | 792 +++++++++++------- .../2_rules.spec.js | 678 +++++++++++---- .../3_alerts.spec.js | 217 ++--- .../4_findings.spec.js | 117 +-- .../commands.js | 537 +++++++++++- .../constants.js | 99 ++- .../detectors.js | 80 -- .../index.d.ts | 283 +++++++ .../index.js | 27 + .../rules.js | 65 -- .../typings.js | 39 - 31 files changed, 2168 insertions(+), 1083 deletions(-) rename cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/{create_dns_rule.json => create_dns_rule_with_name_selection.json} (72%) create mode 100644 cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_type_selection.json create mode 100644 cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/sample_dns_field_mappings.json create mode 100644 cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_dns_index_settings.json delete mode 100644 cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_index_settings.json create mode 100644 cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_windows_index_settings.json delete mode 100644 cypress/utils/plugins/security-analytics-dashboards-plugin/detectors.js create mode 100644 cypress/utils/plugins/security-analytics-dashboards-plugin/index.d.ts create mode 100644 cypress/utils/plugins/security-analytics-dashboards-plugin/index.js delete mode 100644 cypress/utils/plugins/security-analytics-dashboards-plugin/rules.js delete mode 100644 cypress/utils/plugins/security-analytics-dashboards-plugin/typings.js diff --git a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_dns_detector_data.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_dns_detector_data.json index 276c56db2..e2f5447b8 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_dns_detector_data.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_dns_detector_mappings_data.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_dns_detector_mappings_data.json index e4056d577..6f9f869ea 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_dns_detector_mappings_data.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_data.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_data.json index 07392d280..b68c08406 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_data.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_mappings_data.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_mappings_data.json index da81361fe..0cad430bc 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_mappings_data.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/index/add_dns_index_data.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/add_dns_index_data.json index 35077a0f5..901c7c3e3 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/add_dns_index_data.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/index/add_windows_index_data.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/add_windows_index_data.json index c449c7584..f8b8b4e2e 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/add_windows_index_data.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/index/create_dns_settings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/create_dns_settings.json index 126659dc6..970a6089a 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/create_dns_settings.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/index/create_windows_settings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/create_windows_settings.json index f794e671e..480f63ba1 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/index/create_windows_settings.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_name_selection.json similarity index 72% rename from cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule.json rename to cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_name_selection.json index 5e38ab4bd..7c1e7c8fb 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_type_selection.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_type_selection.json new file mode 100644 index 000000000..e447a30d5 --- /dev/null +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_network_rule.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_network_rule.json index 43e69cff4..2937fc79d 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_network_rule.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_windows_usb_rule.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_windows_usb_rule.json index 20f59799a..fb14944c6 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_windows_usb_rule.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/integration_tests/rule/sample_dns_field_mappings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/sample_dns_field_mappings.json new file mode 100644 index 000000000..b2f9b698e --- /dev/null +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_alias_mappings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_alias_mappings.json index cf08cc696..e0a1a5f88 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_alias_mappings.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_detector.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_detector.json index 67eca1110..a17853598 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_detector.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_dns_index_settings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_dns_index_settings.json new file mode 100644 index 000000000..02b01e771 --- /dev/null +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_document.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_document.json index d23b31895..521d2f677 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_document.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_field_mappings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_field_mappings.json index 6e8d728fe..ff4eb1830 100644 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_field_mappings.json +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_index_settings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_index_settings.json deleted file mode 100644 index a8a5294a7..000000000 --- a/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/sample_windows_index_settings.json b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/sample_windows_index_settings.json new file mode 100644 index 000000000..480f63ba1 --- /dev/null +++ b/cypress/fixtures/plugins/security-analytics-dashboards-plugin/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/plugins/security-analytics-dashboards-plugin/1_detectors.spec.js b/cypress/integration/plugins/security-analytics-dashboards-plugin/1_detectors.spec.js index 0d821420c..997b56ab7 100644 --- a/cypress/integration/plugins/security-analytics-dashboards-plugin/1_detectors.spec.js +++ b/cypress/integration/plugins/security-analytics-dashboards-plugin/1_detectors.spec.js @@ -3,171 +3,270 @@ * SPDX-License-Identifier: Apache-2.0 */ -import sample_index_settings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_index_settings.json'; -import dns_rule_data from '../../../fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule.json'; -import { BACKEND_BASE_PATH } from '../../../utils/base_constants'; import { NODE_API, OPENSEARCH_DASHBOARDS_URL, } from '../../../utils/plugins/security-analytics-dashboards-plugin/constants'; +import sample_windows_index_settings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_windows_index_settings.json'; +import sample_dns_index_settings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_dns_index_settings.json'; +import dns_name_rule_data from '../../../fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_name_selection.json'; +import dns_type_rule_data from '../../../fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_dns_rule_with_type_selection.json'; +import _ from 'lodash'; -const testMappings = { - properties: { - 'dns-question-name': { - type: 'alias', - path: 'DnsQuestionName', - }, - }, -}; +const cypressIndexDns = 'cypress-index-dns'; +const cypressIndexWindows = 'cypress-index-windows'; +const detectorName = 'test detector'; +const cypressLogTypeDns = 'dns'; -const cypressDNSRule = dns_rule_data.title; +const cypressDNSRule = dns_name_rule_data.title; -const createDetector = (detectorName, dataSource, expectFailure) => { - // Locate Create detector button click to start - cy.get('.euiButton') - .filter(':contains("Create detector")') - .click({ force: true }); +const getNameField = () => + cy.getInputByPlaceholder('Enter a name for the detector.'); - // Check to ensure process started - cy.contains('Define detector'); +const getNextButton = () => cy.getButtonByText('Next'); - // Enter a name for the detector in the appropriate input - cy.get(`input[placeholder="Enter a name for the detector."]`) - .focus() - .realType(detectorName); +const getCreateDetectorButton = () => cy.getButtonByText('Create detector'); - // Select our pre-seeded data source (check cypressIndexDns) - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .find('input') - .focus() - .realType(dataSource); - - cy.intercept({ - pathname: NODE_API.RULES_SEARCH, - query: { - prePackaged: 'true', - }, - }).as('getSigmaRules'); - - // Select threat detector type (Windows logs) - cy.get(`input[id="dns"]`).click({ force: true }); - - cy.wait('@getSigmaRules').then(() => { - // Open Detection rules accordion - cy.get('[data-test-subj="detection-rules-btn"]').click({ - force: true, - timeout: 5000, - }); +const validateAlertPanel = (alertName) => + cy + .getElementByText('.euiTitle', 'Alert triggers') + .parentsUntil('.euiPanel') + .siblings() + .eq(2) + .within(() => cy.getElementByText('button', alertName)); + +const dataSourceLabel = 'Select or input source indexes or index patterns'; + +const getDataSourceField = () => cy.getFieldByLabel(dataSourceLabel); + +const logTypeLabel = 'Select a log type you would like to detect'; + +const getLogTypeField = () => cy.getFieldByLabel(logTypeLabel); + +const openDetectorDetails = (detectorName) => { + cy.getInputByPlaceholder('Search threat detectors') + .type(`${detectorName}`) + .pressEnterKey(); + cy.getElementByText('.euiTableCellContent button', detectorName).click(); +}; + +const getMappingFields = (properties, items = [], prefix = '') => { + for (let field in properties) { + const fullFieldName = prefix ? `${prefix}.${field}` : field; + const nextProperties = properties[field].properties; + if (!nextProperties) { + items.push({ + ruleFieldName: fullFieldName, + logFieldName: properties[field].path, + }); + } else { + getMappingFields(nextProperties, items, fullFieldName); + } + } + return items; +}; - cy.contains('table tr', 'DNS', { - timeout: 120000, +const validateFieldMappingsTable = (message = '') => { + cy.wait('@getMappingsView').then((interception) => { + cy.wait(10000).then(() => { + cy.get('.reviewFieldMappings').should('be.visible'); + const properties = interception.response.body.response.properties; + const unmapped_field_aliases = + interception.response.body.response.unmapped_field_aliases + .map((field) => [field]) + .sort() + .slice(0, 10); + + Cypress.log({ + message: `Validate table data - ${message}`, + }); + if (_.isEmpty(properties)) { + validatePendingFieldMappingsPanel(unmapped_field_aliases); + } else { + let items = getMappingFields(properties, [], ''); + items = items.map((item) => [item.ruleFieldName, item.logFieldName]); + validateAutomaticFieldMappingsPanel(items); + } }); }); +}; - // Check that correct page now showing - cy.contains('Configure field mapping'); +const editDetectorDetails = (detectorName, panelTitle) => { + cy.urlShouldContain('detector-details').then(() => { + cy.getElementByText('.euiTitle', detectorName); + cy.getElementByText('.euiPanel .euiTitle', panelTitle); + cy.getElementByText('.euiPanel .euiTitle', panelTitle) + .parent() + .siblings() + .within(() => cy.get('button').contains('Edit').click()); + }); +}; - if (!expectFailure) { - // Select appropriate names to map fields to - for (let field_name in testMappings.properties) { - const mappedTo = testMappings.properties[field_name].path; +const validateAutomaticFieldMappingsPanel = (mappings) => + cy.get('.editFieldMappings').within(() => { + cy.get('.euiAccordion__triggerWrapper button').then(($btn) => { + cy.get($btn).contains(`Automatically mapped fields (${mappings.length})`); - cy.contains('tr', field_name).within(() => { - cy.get(`[data-test-subj="detector-field-mappings-select"]`) + // 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() - .type(mappedTo); + .then(() => { + cy.getElementByTestSubject('auto-mapped-fields-table') + .find('.euiBasicTable') + .validateTable(mappings); + }); + } + }); + }); + +const validatePendingFieldMappingsPanel = (mappings) => { + cy.get('.editFieldMappings').within(() => { + // Pending field mappings + cy.getElementByText('.euiTitle', 'Pending field mappings') + .parents('.euiPanel') + .within(() => { + cy.getElementByTestSubject('pending-mapped-fields-table') + .find('.euiBasicTable') + .validateTable(mappings); }); - } - } + }); +}; - // Click Next button to continue - cy.get('button').contains('Next').click({ force: true }); +const fillDetailsForm = (detectorName, dataSource) => { + getNameField().type(detectorName); + getDataSourceField().selectComboboxItem(dataSource); + getDataSourceField().blur(); + getLogTypeField().selectComboboxItem(cypressLogTypeDns); + getLogTypeField().blur(); +}; - // Check that correct page now showing - cy.contains('Set up alert triggers'); +const createDetector = (detectorName, dataSource, expectFailure) => { + getCreateDetectorButton().click({ force: true }); + + fillDetailsForm(detectorName, dataSource); + + cy.getElementByText( + '.euiAccordion .euiTitle', + 'Detection rules (14 selected)' + ) + .click({ force: true, timeout: 5000 }) + .then(() => cy.contains('.euiTable .euiTableRow', 'Dns')); + + 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(); + } + }); - // Type name of new trigger - cy.get(`input[placeholder="Enter a name to describe the alert condition"]`) - .focus() - .realType('test_trigger'); + // go to the alerts page + getNextButton().click({ force: true }); - // Type in (or select) tags for the alert condition - cy.get(`[data-test-subj="alert-tags-combo-box"]`) + // 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'); - - // Select applicable severity levels - cy.get(`[data-test-subj="security-levels-combo-box"]`).click({ force: true }); - cy.contains('1 (Highest)').click({ force: true }); + .blur(); - // Continue to next page - cy.contains('Next').click({ force: true }); + cy.getFieldByLabel('Specify alert severity').selectComboboxItem( + '1 (Highest)' + ); - // Confirm page is reached - cy.contains('Review and create'); + // go to review page + getNextButton().click({ force: true }); - // Confirm field mappings registered - cy.contains('Field mapping'); + // 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'); - if (!expectFailure) { - for (let field in testMappings.properties) { - const mappedTo = testMappings.properties[field].path; - - cy.contains(field); - cy.contains(mappedTo); - } - } + 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' + ); - // Confirm entries user has made - cy.contains('Detector details'); - cy.contains(detectorName); - cy.contains('dns'); - cy.contains('test_trigger'); + validateAlertPanel('test_trigger'); - // Create the detector - cy.get('button').contains('Create').click({ force: true }); - cy.contains(detectorName); + cy.intercept('POST', NODE_API.MAPPINGS_BASE).as('createMappingsRequest'); + cy.intercept('POST', NODE_API.DETECTORS_BASE).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.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', () => { - const cypressIndexDns = 'cypress-index-dns'; - const cypressIndexWindows = 'cypress-index-windows'; - const detectorName = 'test detector'; +const openCreateForm = () => getCreateDetectorButton().click({ force: true }); + +const getDescriptionField = () => + cy.getTextareaByLabel('Description - optional'); +const getTriggerNameField = () => cy.getFieldByLabel('Trigger name'); +describe('Detectors', () => { before(() => { cy.cleanUpTests(); - cy.createIndex(cypressIndexWindows, null, sample_index_settings); + cy.createIndex(cypressIndexWindows, sample_windows_index_settings); // Create test index - cy.createIndex(cypressIndexDns, null, sample_index_settings).then(() => + cy.createIndex(cypressIndexDns, sample_dns_index_settings).then(() => cy - .request({ - method: 'POST', - url: `${BACKEND_BASE_PATH}${NODE_API.RULES_BASE}/_search?pre_packaged=true`, - headers: { - 'osd-xsrf': true, - }, - body: { + .request( + 'POST', + '_plugins/_security_analytics/rules/_search?prePackaged=true', + { from: 0, size: 5000, query: { @@ -178,239 +277,324 @@ 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(NODE_API.SEARCH_DETECTORS).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.contains('Threat detectors'); - }); + describe('...should validate form fields', () => { + beforeEach(() => { + cy.intercept(NODE_API.SEARCH_DETECTORS).as('detectorsSearch'); - it('...should show mappings warning', () => { - // Locate Create detector button click to start - cy.get('.euiButton') - .filter(':contains("Create detector")') - .click({ force: true }); - - // Check to ensure process started - cy.contains('Define detector'); - - // Select our pre-seeded data source (check cypressIndexDns) - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .find('input') - .focus() - .realType(cypressIndexDns); - - // Select threat detector type (Windows logs) - cy.get(`input[id="dns"]`).click({ force: true }); - - // Select our pre-seeded data source (check cypressIndexDns) - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .find('input') - .focus() - .realType(cypressIndexWindows) - .realPress('Enter'); - - cy.get('.euiCallOut') - .should('be.visible') - .contains( - 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.' - ); - }); + // Visit Detectors page before any test + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); + cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); - 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.'); - }); + openCreateForm(); + }); - it('...basic details can be edited', () => { - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.contains('Detector details'); - cy.contains(detectorName); + it('...should validate name field', () => { + getNameField().should('be.empty'); + getNameField().focus().blur(); + getNameField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Enter a name.'); + + getNameField().type('text').focus().blur(); + + getNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.' + ); + + getNameField() + .type('{selectall}') + .type('{backspace}') + .type('tex&') + .focus() + .blur(); + + getNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.' + ); + + getNameField() + .type('{selectall}') + .type('{backspace}') + .type('Detector name') + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); - // Click "Edit" button in detector details - cy.get(`[data-test-subj="edit-detector-basic-details"]`).click({ - force: true, + it('...should validate description field', () => { + const longDescriptionText = + 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.'; + + getDescriptionField().should('be.empty'); + + getDescriptionField().type(longDescriptionText).focus().blur(); + + getDescriptionField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.' + ); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector description...') + .focus() + .blur(); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector name') + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); }); - // Confirm arrival at "Edit detector details" page - cy.contains('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'); - - // Change input source - cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true }); - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .realType(cypressIndexWindows) - .realPress('Enter'); - - // 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'); - - // Save changes to detector details - cy.get(`[data-test-subj="save-basic-details-edits"]`).click({ - force: true, + it('...should validate data source field', () => { + getDataSourceField() + .focus() + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Select an input source.'); + + getDataSourceField().selectComboboxItem(cypressIndexDns); + getDataSourceField() + .focus() + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); }); - // Confirm taken to detector details page - cy.contains(detectorName); + it('...should validate next button', () => { + getNextButton().should('be.disabled'); - // Verify edits are applied - cy.contains('test detector edited'); - cy.contains('Every 10 hours'); - cy.contains('Edited description'); - cy.contains(cypressIndexWindows); - }); + fillDetailsForm(detectorName, cypressIndexDns); + getNextButton().should('be.enabled'); + }); - it('...rules can be edited', () => { - // Ensure start on main detectors page - cy.contains('Threat detectors'); + it('...should validate alerts page', () => { + fillDetailsForm(detectorName, cypressIndexDns); + getNextButton().click({ force: true }); + getTriggerNameField().should('be.empty'); - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.contains('Detector details'); - cy.contains(detectorName); + getTriggerNameField().focus().blur(); + getTriggerNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains('Enter a name.'); - // Confirm number of rules before edit - cy.contains('Active rules (13)'); + getTriggerNameField().type('Trigger name').focus().blur(); - // Click "Edit" button in Detector rules panel - cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); + getTriggerNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); - // Confirm arrival on "Edit detector rules" page - cy.contains('Edit detector rules'); + getNextButton().should('be.enabled'); - // Search for specific rule - cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule); + getTriggerNameField() + .type('{selectall}') + .type('{backspace}') + .focus() + .blur(); + getNextButton().should('be.disabled'); - // 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({ force: true }); + cy.getButtonByText('Remove').click({ force: true }); + getNextButton().should('be.enabled'); }); - // Save changes - cy.get(`[data-test-subj="save-detector-rules-edits"]`).click({ - force: true, - }); + it('...should show mappings warning', () => { + fillDetailsForm(detectorName, cypressIndexDns); - // Confirm 1 rule has been removed from detector - cy.contains('Active rules (12)'); + getDataSourceField().selectComboboxItem(cypressIndexWindows); + getDataSourceField().focus().blur(); - // Click "Edit" button in Detector rules panel - cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); + cy.get('[data-test-subj="define-detector-diff-log-types-warning"]') + .should('be.visible') + .contains( + 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.' + ); + }); + }); - // Confirm arrival on "Edit detector rules" page - cy.contains('Edit detector rules'); + describe('...validate create detector flow', () => { + beforeEach(() => { + cy.intercept(NODE_API.SEARCH_DETECTORS) + .as('detectorsSearch') + .as('detectorsSearch'); - // Search for specific rule - cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule); + // Visit Detectors page before any test + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); + cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); + }); - // Toggle single search result to checked - cy.contains('table tr', cypressDNSRule).within(() => { - cy.wait(2000); - cy.get('button').eq(1).click({ force: true }); + it('...can fail creation', () => { + createDetector(`${detectorName}_fail`, '.kibana_1', true); + cy.getElementByText('.euiCallOut', 'Create detector failed.'); }); - // Save changes - cy.get(`[data-test-subj="save-detector-rules-edits"]`).click({ - force: true, + it('...can be created', () => { + createDetector(detectorName, cypressIndexDns, false); + cy.getElementByText('.euiCallOut', 'Detector created successfully'); }); - cy.contains(detectorName); - // Confirm 1 rule has been added to detector - cy.contains('Active rules (13)'); - }); + it('...basic details can be edited', () => { + cy.intercept('GET', NODE_API.INDICES_BASE).as('getIndices'); + openDetectorDetails(detectorName); + + editDetectorDetails(detectorName, 'Detector details'); + + cy.urlShouldContain('edit-detector-details').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector details'); + }); + + cy.wait('@getIndices'); + getNameField() + .type('{selectall}{backspace}') + .type('test detector edited'); + cy.getTextareaByLabel('Description - optional').type( + 'Edited description' + ); + + getDataSourceField().clearCombobox(); + getDataSourceField().selectComboboxItem(cypressIndexWindows); + + cy.getFieldByLabel('Run every').type('{selectall}{backspace}').type('10'); + cy.getFieldByLabel('Run every', 'select').select('Hours'); - it('...should update field mappings if data source is changed', () => { - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.contains('Detector details'); - cy.contains(detectorName); + cy.getElementByText('button', 'Save changes').click({ force: true }); - // Click "Edit" button in detector details - cy.get(`[data-test-subj="edit-detector-basic-details"]`).click({ - force: true, + 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); + }); }); - // Confirm arrival at "Edit detector details" page - cy.contains('Edit detector details'); + it('...rules can be edited', () => { + openDetectorDetails(detectorName); - cy.get('.reviewFieldMappings').should('not.exist'); + editDetectorDetails(detectorName, 'Active rules'); + cy.getElementByText('.euiTitle', 'Detection rules (14)'); - // Change input source - cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true }); - cy.get(`[data-test-subj="define-detector-select-data-source"]`) - .type(cypressIndexWindows) - .realPress('Enter'); - }); + cy.getInputByPlaceholder('Search...') + .type(`${cypressDNSRule}`) + .pressEnterKey(); - it('...should update field mappings if rule selection is changed', () => { - // Click on detector name - cy.contains(detectorName).click({ force: true }); - cy.contains('Detector details'); - cy.contains(detectorName); + cy.getElementByText('.euiTableCellContent button', cypressDNSRule) + .parents('td') + .prev() + .find('.euiTableCellContent button') + .click(); - // Click "Edit" button in detector details - cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); + 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)'); + }); + }); + + xit('...should update field mappings if data source is changed', () => { + cy.intercept( + `${NODE_API.MAPPINGS_VIEW}?indexName=cypress-index-dns&ruleTopic=dns` + ).as('getMappingsView'); + cy.intercept('GET', NODE_API.INDICES_BASE).as('getIndices'); + openDetectorDetails(detectorName); + + editDetectorDetails(detectorName, 'Detector details'); + + cy.urlShouldContain('edit-detector-details').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector details'); + }); - // Confirm arrival at "Edit detector details" page - cy.contains('Edit detector rules'); + cy.wait('@getIndices'); + cy.get('.reviewFieldMappings').should('not.exist'); - cy.get('.reviewFieldMappings').should('not.exist'); + getDataSourceField().clearCombobox(); + getDataSourceField().should('not.have.value'); + getDataSourceField().type(`${cypressIndexDns}{enter}`); - cy.intercept(NODE_API.MAPPINGS_VIEW).as('getMappingsView'); + validateFieldMappingsTable('data source is changed'); - cy.get('table th').within(() => { - cy.get('button').first().click({ force: true }); + cy.getElementByText('button', 'Save changes').click({ force: true }); }); - cy.get('.reviewFieldMappings').should('be.visible'); - }); + xit('...should show field mappings if rule selection is changed', () => { + cy.intercept( + `${NODE_API.MAPPINGS_VIEW}?indexName=cypress-index-windows&ruleTopic=dns` + ).as('getMappingsView'); + + openDetectorDetails(detectorName); + + editDetectorDetails(detectorName, 'Active rules'); + + cy.urlShouldContain('edit-detector-rules').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector rules'); + }); - it('...can be deleted', () => { - // Click on detector to be removed - cy.contains('test detector edited').click({ force: true }); + cy.get('.reviewFieldMappings').should('not.exist'); - // Confirm page - cy.contains('Detector details'); + cy.wait('@detectorsSearch'); - // Click "Actions" button, the click "Delete" - cy.get('button').contains('Actions').click({ force: true }); - cy.get('button').contains('Delete').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 }); - // Confirm detector is deleted - cy.contains('There are no existing detectors'); + validateFieldMappingsTable('rules are changed'); + }); + + it('...can be deleted', () => { + cy.intercept(`${NODE_API.RULES_BASE}/_search?prePackaged=true`).as( + 'getSigmaRules' + ); + cy.intercept(`${NODE_API.RULES_BASE}/_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(`${NODE_API.DETECTORS_BASE}/_search`).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/plugins/security-analytics-dashboards-plugin/2_rules.spec.js b/cypress/integration/plugins/security-analytics-dashboards-plugin/2_rules.spec.js index 3132e7ff2..ac9998122 100644 --- a/cypress/integration/plugins/security-analytics-dashboards-plugin/2_rules.spec.js +++ b/cypress/integration/plugins/security-analytics-dashboards-plugin/2_rules.spec.js @@ -13,14 +13,11 @@ const SAMPLE_RULE = { name: `Cypress test rule ${uniqueId}`, logType: 'windows', description: 'This is a rule used to test the rule creation workflow.', - detection: - 'selection:\n Provider_Name: Service Control Manager\nEventID: 7045\nServiceName: ZzNetSvc\n{backspace}{backspace}condition: selection', detectionLine: [ - 'selection:', - 'Provider_Name: Service Control Manager', - 'EventID: 7045', - 'ServiceName: ZzNetSvc', - 'condition: selection', + 'condition: Selection_1', + 'Selection_1:', + 'FieldKey|contains:', + '- FieldValue', ], severity: 'critical', tags: [ @@ -52,10 +49,7 @@ const YAML_RULE_LINES = [ `- '${SAMPLE_RULE.references}'`, `author: ${SAMPLE_RULE.author}`, `detection:`, - ...SAMPLE_RULE.detection - .replaceAll(' ', '') - .replaceAll('{backspace}', '') - .split('\n'), + ...SAMPLE_RULE.detectionLine, ]; const checkRulesFlyout = () => { @@ -156,204 +150,540 @@ const checkRulesFlyout = () => { }); }; +const getCreateButton = () => cy.get('[data-test-subj="create_rule_button"]'); +const getNameField = () => cy.getFieldByLabel('Rule name'); +const getRuleStatusField = () => cy.getFieldByLabel('Rule Status'); +const getDescriptionField = () => cy.getFieldByLabel('Description - optional'); +const getAuthorField = () => cy.getFieldByLabel('Author'); +const getLogTypeField = () => cy.getFieldByLabel('Log type'); +const getRuleLevelField = () => cy.getFieldByLabel('Rule level (severity)'); +const getSelectionPanelByIndex = (index) => + cy.get(`[data-test-subj="detection-visual-editor-${index}"]`); +const getSelectionNameField = () => cy.get('[data-test-subj="selection_name"]'); +const getMapKeyField = () => + cy.get('[data-test-subj="selection_field_key_name"]'); +const getMapValueField = () => + cy.get('[data-test-subj="selection_field_value"]'); +const getMapListField = () => cy.get('[data-test-subj="selection_field_list"]'); +const getListRadioField = () => cy.get('[for="selection-map-list-0-0"]'); +const getTextRadioField = () => cy.get('[for="selection-map-value-0-0"]'); +const getConditionField = () => + cy.get('[data-test-subj="rule_detection_field"]'); +const getConditionAddButton = () => + cy.get('[data-test-subj="condition-add-selection-btn"]'); +const getConditionRemoveButton = (index) => + cy.get(`[data-test-subj="selection-exp-field-item-remove-${index}"]`); +const getRuleSubmitButton = () => + cy.get('[data-test-subj="submit_rule_form_button"]'); +const getTagField = (index) => + cy.get(`[data-test-subj="rule_tags_field_${index}"]`); +const getReferenceFieldByIndex = (index) => + cy.get(`[data-test-subj="rule_references_field_${index}"]`); +const getFalsePositiveFieldByIndex = (index) => + cy.get(`[data-test-subj="rule_false_positives_field_${index}"]`); + +const toastShouldExist = () => { + submitRule(); + cy.get('.euiToast').contains('Failed to create rule:'); +}; + +const submitRule = () => getRuleSubmitButton().click({ force: true }); +const fillCreateForm = () => { + // rule overview + getNameField().type(SAMPLE_RULE.name); + getDescriptionField().type(SAMPLE_RULE.description); + getAuthorField().type(`${SAMPLE_RULE.author}`); + + // rule details + getLogTypeField().type(SAMPLE_RULE.logType); + getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); + + // rule detection + getSelectionPanelByIndex(0).within(() => { + getSelectionNameField().should('have.value', 'Selection_1'); + getMapKeyField().type('FieldKey'); + + getTextRadioField().click({ force: true }); + getMapValueField().type('FieldValue'); + }); + + getConditionAddButton().click({ force: true }); + + // rule additional details + SAMPLE_RULE.tags.forEach((tag, idx) => { + getTagField(idx).type(tag); + idx < SAMPLE_RULE.tags.length - 1 && + cy.getButtonByText('Add tag').click({ force: true }); + }); + + getReferenceFieldByIndex(0).type(SAMPLE_RULE.references); + getFalsePositiveFieldByIndex(0).type(SAMPLE_RULE.falsePositive); +}; + describe('Rules', () => { before(() => cy.cleanUpTests()); - beforeEach(() => { - cy.intercept({ - pathname: NODE_API.RULES_SEARCH, - query: { - prePackaged: 'true', - }, - }).as('rulesSearch'); - // Visit Rules page - cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/rules`); - cy.wait('@rulesSearch').should('have.property', 'state', 'Complete'); - - // Check that correct page is showing - cy.contains('Rules'); - }); - it('...can be created', () => { - // Click "create new rule" button - cy.get('[data-test-subj="create_rule_button"]').click({ - force: true, + describe('...should validate form fields', () => { + beforeEach(() => { + cy.intercept(`${NODE_API.RULES_BASE}/_search?prePackaged=true`).as( + 'rulesSearch' + ); + // Visit Rules page + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/rules`); + cy.wait('@rulesSearch').should('have.property', 'state', 'Complete'); + + // Check that correct page is showing + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); + + getCreateButton().click({ force: true }); }); - // Enter the log type - cy.get('[data-test-subj="rule_status_dropdown"]').type(SAMPLE_RULE.status); - - // Enter the name - cy.get('[data-test-subj="rule_name_field"]').type(SAMPLE_RULE.name); - - // Enter the log type - cy.get('[data-test-subj="rule_type_dropdown"]').type(SAMPLE_RULE.logType); - - // Enter the description - cy.get('[data-test-subj="rule_description_field"]').type( - SAMPLE_RULE.description - ); - - // Enter the severity - cy.get('[data-test-subj="rule_severity_dropdown"]').type( - SAMPLE_RULE.severity + '{enter}' - ); - - // Enter the tags - SAMPLE_RULE.tags.forEach((tag) => - cy.get('[data-test-subj="rule_tags_dropdown"]').type(`${tag}{enter}`) - ); - - // Enter the reference - cy.contains('Add another URL').click(); - cy.get('[data-test-subj="rule_references_field_0"]').type( - SAMPLE_RULE.references - ); - - // Enter the false positive cases - cy.get('[data-test-subj="rule_false_positives_field_0"]').type( - `${SAMPLE_RULE.falsePositive}{enter}` - ); - - // Enter the author - cy.get('[data-test-subj="rule_author_field"]').type( - `${SAMPLE_RULE.author}{enter}` - ); - - // Enter the detection - cy.get('[data-test-subj="rule_detection_field"] textarea').type( - SAMPLE_RULE.detection, - { - force: true, - } - ); + it('...should validate rule name', () => { + getNameField().containsHelperText( + 'Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores' + ); - // Switch to YAML editor - cy.get('[data-test-subj="change-editor-type"] label:nth-child(2)').click({ - force: true, + getNameField().should('be.empty'); + getNameField().focus().blur(); + getNameField().containsError('Rule name is required'); + getNameField().type('text').focus().blur(); + getNameField().containsError('Invalid rule name.'); + + getNameField() + .type('{selectall}') + .type('{backspace}') + .type('tex&') + .focus() + .blur(); + getNameField().containsError('Invalid rule name.'); + + getNameField() + .type('{selectall}') + .type('{backspace}') + .type('Rule name') + .focus() + .blur() + .shouldNotHaveError(); }); - YAML_RULE_LINES.forEach((line) => - cy.get('[data-test-subj="rule_yaml_editor"]').contains(line) - ); + it('...should validate rule description field', () => { + const longDescriptionText = + 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.'; + + getDescriptionField().should('be.empty'); + getDescriptionField().type(longDescriptionText).focus().blur(); + + getDescriptionField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.' + ); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector description...') + .focus() + .blur(); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector name') + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); - cy.intercept({ - url: NODE_API.RULES_BASE, - }).as('getRules'); + it('...should validate author', () => { + getAuthorField().containsHelperText( + 'Combine multiple authors separated with a comma' + ); - // Click "create" button - cy.get('[data-test-subj="submit_rule_form_button"]').click({ - force: true, + getAuthorField().should('be.empty'); + getAuthorField().focus().blur(); + getAuthorField().containsError('Author name is required'); + getAuthorField().type('text').focus().blur(); + getAuthorField().containsError('Invalid author.'); + + getAuthorField() + .type('{selectall}') + .type('{backspace}') + .type('tex&') + .focus() + .blur(); + getAuthorField().containsError('Invalid author.'); + + getAuthorField() + .type('{selectall}') + .type('{backspace}') + .type('Rule name') + .focus() + .blur() + .shouldNotHaveError(); }); - cy.wait('@getRules'); + it('...should validate log type field', () => { + getLogTypeField().should('be.empty'); + getLogTypeField().focus().blur(); + getLogTypeField().containsError('Log type is required'); - cy.contains('Rules'); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); + getLogTypeField().focus().blur().shouldNotHaveError(); + }); - checkRulesFlyout(); - }); + it('...should validate rule level field', () => { + getRuleLevelField().should('be.empty'); + getRuleLevelField().focus().blur(); + getRuleLevelField().containsError('Rule level is required'); - it('...can be edited', () => { - cy.contains('Rules'); + getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); + getRuleLevelField().focus().blur().shouldNotHaveError(); + }); + + it('...should validate rule status field', () => { + getRuleStatusField().containsValue(SAMPLE_RULE.status); + getRuleStatusField().focus().blur().shouldNotHaveError(); + + getRuleStatusField().clearCombobox(); + getRuleStatusField().focus().blur(); + getRuleStatusField().containsError('Rule status is required'); + }); + + it('...should validate selection', () => { + getSelectionPanelByIndex(0).within(() => { + getSelectionNameField().should('have.value', 'Selection_1'); + getSelectionNameField().clearValue(); + getSelectionNameField().focus().blur(); + getSelectionNameField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Selection name is required'); + + getSelectionNameField().type('Selection_1'); + getSelectionNameField() + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); + }); + + it('...should validate selection map key field', () => { + getSelectionPanelByIndex(0).within(() => { + getMapKeyField().should('be.empty'); + getMapKeyField().focus().blur(); + getMapKeyField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Key name is required'); + + getMapKeyField().type('FieldKey'); + getMapKeyField() + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); + }); - cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); - cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ - force: true, + it('...should validate selection map value field', () => { + getSelectionPanelByIndex(0).within(() => { + getMapValueField().should('be.empty'); + getMapValueField().focus().blur(); + getMapValueField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Value is required'); + + getMapValueField().type('FieldValue'); + getMapValueField() + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); }); - cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) - .find('button') - .contains('Action') - .click({ force: true }) - .then(() => { - // Confirm arrival at detectors page - cy.get('.euiPopover__panel') - .find('button') - .contains('Edit') - .click({ force: true }); + it('...should validate selection map list field', () => { + getSelectionPanelByIndex(0).within(() => { + getListRadioField().click({ force: true }); + getMapListField().should('be.empty'); + getMapListField().focus().blur(); + getMapListField() + .parentsUntil('.euiFormRow') + .contains('Value is required'); + + getMapListField().type('FieldValue'); + getMapListField() + .focus() + .blur() + .parents('.euiFormRow') + .find('.euiFormErrorText') + .should('not.exist'); }); + }); - const ruleNameSelector = '[data-test-subj="rule_name_field"]'; - cy.get(ruleNameSelector).clear(); - - SAMPLE_RULE.name += ' edited'; - cy.get(ruleNameSelector).type(SAMPLE_RULE.name); - cy.get(ruleNameSelector).should('have.value', SAMPLE_RULE.name); - - // Enter the log type - const logSelector = '[data-test-subj="rule_type_dropdown"]'; - cy.get(logSelector).within(() => - cy.get('.euiFormControlLayoutClearButton').click({ force: true }) - ); - SAMPLE_RULE.logType = 'dns'; - YAML_RULE_LINES[2] = `product: ${SAMPLE_RULE.logType}`; - YAML_RULE_LINES[3] = `title: ${SAMPLE_RULE.name}`; - cy.get(logSelector).type(SAMPLE_RULE.logType).type('{enter}'); - cy.get(logSelector).contains(SAMPLE_RULE.logType, { - matchCase: false, + it('...should validate condition field', () => { + getConditionField().scrollIntoView(); + getConditionField().find('.euiFormErrorText').should('not.exist'); + getRuleSubmitButton().click({ force: true }); + getConditionField() + .parents('.euiFormRow__fieldWrapper') + .contains('Condition is required'); + + getConditionAddButton().click({ force: true }); + getConditionField().find('.euiFormErrorText').should('not.exist'); + + getConditionRemoveButton(0).click({ force: true }); + getConditionField() + .parents('.euiFormRow__fieldWrapper') + .contains('Condition is required'); }); - const ruleDescriptionSelector = '[data-test-subj="rule_description_field"]'; - SAMPLE_RULE.description += ' edited'; - YAML_RULE_LINES[4] = `description: ${SAMPLE_RULE.description}`; - cy.get(ruleDescriptionSelector).clear(); - cy.get(ruleDescriptionSelector).type(SAMPLE_RULE.description); - cy.get(ruleDescriptionSelector).should( - 'have.value', - SAMPLE_RULE.description - ); - - // Click "create" button - cy.get('[data-test-subj="submit_rule_form_button"]').click({ - force: true, + it('...should validate tag field', () => { + getTagField(0).should('be.empty'); + getTagField(0).type('wrong.tag').focus().blur(); + getTagField(0) + .parents('.euiFormRow__fieldWrapper') + .contains("Tags must start with 'attack.'"); + + getTagField(0).clearValue().type('attack.tag'); + getTagField(0) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); }); - cy.contains('Rules'); + it('...should validate form', () => { + toastShouldExist(); + fillCreateForm(); + + // rule name field + getNameField().clearValue(); + toastShouldExist(); + getNameField().type('Rule name'); + + // author field + getAuthorField().clearValue(); + toastShouldExist(); + getAuthorField().type('John Doe'); + + // log field + getLogTypeField().clearCombobox(); + toastShouldExist(); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); + + // severity field + getRuleLevelField().clearCombobox(); + toastShouldExist(); + getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); + + // status field + getRuleStatusField().clearCombobox(); + toastShouldExist(); + getRuleStatusField().selectComboboxItem(SAMPLE_RULE.status); + + // selection name field + getSelectionPanelByIndex(0).within(() => + getSelectionNameField().type('{selectall}').type('{backspace}') + ); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => + getSelectionNameField().type('Selection_1') + ); + + // selection map key field + getSelectionPanelByIndex(0).within(() => + getMapKeyField().type('{selectall}').type('{backspace}') + ); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => + getMapKeyField().type('FieldKey') + ); + + // selection map value field + getSelectionPanelByIndex(0).within(() => + getMapValueField().type('{selectall}').type('{backspace}') + ); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => + getMapValueField().type('FieldValue') + ); + + // selection map list field + getSelectionPanelByIndex(0).within(() => { + getListRadioField().click({ force: true }); + getMapListField().clearValue(); + }); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => { + getListRadioField().click({ force: true }); + getMapListField().type('FieldValue'); + }); - checkRulesFlyout(); + // condition field + getConditionRemoveButton(0).click({ force: true }); + toastShouldExist(); + getConditionAddButton().click({ force: true }); + + // tags field + getTagField(0).clearValue().type('wrong.tag'); + toastShouldExist(); + getTagField(0).clearValue().type('attack.tag'); + }); }); - it('...can be deleted', () => { - cy.intercept(`${NODE_API.RULES_SEARCH}?prePackaged=true`, { - delay: 5000, - }).as('getPrePackagedRules'); + describe('...should validate create rule flow', () => { + beforeEach(() => { + cy.intercept(`${NODE_API.RULES_BASE}/_search?prePackaged=false`).as( + 'rulesSearch' + ); + // Visit Rules page + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/rules`); + cy.wait('@rulesSearch').should('have.property', 'state', 'Complete'); + + // Check that correct page is showing + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); + }); + + it('...can be created', () => { + getCreateButton().click({ force: true }); + + fillCreateForm(); - cy.intercept(`${NODE_API.RULES_SEARCH}?prePackaged=false`, { - delay: 5000, - }).as('getCustomRules'); + // Switch to YAML editor + cy.get('[data-test-subj="change-editor-type"] label:nth-child(2)').click({ + force: true, + }); - cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); + YAML_RULE_LINES.forEach((line) => + cy.get('[data-test-subj="rule_yaml_editor"]').contains(line) + ); - // Click the rule link to open the details flyout - cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ - force: true, + cy.intercept({ + url: `${NODE_API.RULES_BASE}/_search?prePackaged=false`, + }).as('getRules'); + + submitRule(); + + cy.wait('@getRules'); + + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); + + checkRulesFlyout(); }); - cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) - .find('button') - .contains('Action') - .click({ force: true }) - .then(() => { - // Confirm arrival at detectors page - cy.get('.euiPopover__panel') - .find('button') - .contains('Delete') - .click({ force: true }) - .then(() => - cy - .get('.euiModalFooter > .euiButton') - .contains('Delete') - .click({ force: true }) - ); + it('...can be edited', () => { + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); - cy.wait('@getCustomRules'); - cy.wait('@getPrePackagedRules'); + cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); + cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ + force: true, + }); + + cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) + .find('button') + .contains('Action') + .click({ force: true }) + .then(() => { + // Confirm arrival at detectors page + cy.get('.euiPopover__panel').find('button').contains('Edit').click(); + }); - // Search for sample_detector, presumably deleted - cy.wait(3000); - cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); - // Click the rule link to open the details flyout - cy.get('tbody').contains(SAMPLE_RULE.name).should('not.exist'); + getNameField().clear(); + + SAMPLE_RULE.name += ' edited'; + getNameField().type(SAMPLE_RULE.name); + getNameField().should('have.value', SAMPLE_RULE.name); + + getLogTypeField().clearCombobox(); + SAMPLE_RULE.logType = 'dns'; + YAML_RULE_LINES[2] = `product: ${SAMPLE_RULE.logType}`; + YAML_RULE_LINES[3] = `title: ${SAMPLE_RULE.name}`; + getLogTypeField().type(SAMPLE_RULE.logType).type('{enter}'); + getLogTypeField() + .containsValue(SAMPLE_RULE.logType) + .contains(SAMPLE_RULE.logType); + + SAMPLE_RULE.description += ' edited'; + YAML_RULE_LINES[4] = `description: ${SAMPLE_RULE.description}`; + getDescriptionField().clear(); + getDescriptionField().type(SAMPLE_RULE.description); + getDescriptionField().should('have.value', SAMPLE_RULE.description); + + cy.intercept({ + url: `${NODE_API.RULES_BASE}/_search?prePackaged=false`, + }).as('getRules'); + + submitRule(); + + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); + + cy.wait('@getRules'); + + checkRulesFlyout(); + }); + + it('...can be deleted', () => { + cy.intercept('POST', `${NODE_API.RULES_BASE}/_search?prePackaged=true`, { + delay: 5000, + }).as('getPrePackagedRules'); + + cy.intercept('POST', `${NODE_API.RULES_BASE}/_search?prePackaged=false`, { + delay: 5000, + }).as('getCustomRules'); + + cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); + + // Click the rule link to open the details flyout + cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ + force: true, }); + + cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) + .find('button') + .contains('Action') + .click({ force: true }) + .then(() => { + // Confirm arrival at detectors page + cy.get('.euiPopover__panel') + .find('button') + .contains('Delete') + .click() + .then(() => + cy.get('.euiModalFooter > .euiButton').contains('Delete').click() + ); + + cy.wait(5000); + cy.wait('@getCustomRules'); + cy.wait('@getPrePackagedRules'); + + // Search for sample_detector, presumably deleted + cy.wait(3000); + cy.get(`input[placeholder="Search rules"]`).ospSearch( + SAMPLE_RULE.name + ); + // Click the rule link to open the details flyout + cy.get('tbody').contains(SAMPLE_RULE.name).should('not.exist'); + }); + }); }); after(() => cy.cleanUpTests()); diff --git a/cypress/integration/plugins/security-analytics-dashboards-plugin/3_alerts.spec.js b/cypress/integration/plugins/security-analytics-dashboards-plugin/3_alerts.spec.js index a4ab6400c..f90d88941 100644 --- a/cypress/integration/plugins/security-analytics-dashboards-plugin/3_alerts.spec.js +++ b/cypress/integration/plugins/security-analytics-dashboards-plugin/3_alerts.spec.js @@ -5,92 +5,43 @@ import { DETECTOR_TRIGGER_TIMEOUT, - NODE_API, OPENSEARCH_DASHBOARDS_URL, + NODE_API, + createDetector, } from '../../../utils/plugins/security-analytics-dashboards-plugin/constants'; -import sample_index_settings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_index_settings.json'; -import sample_alias_mappings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_alias_mappings.json'; -import sample_detector from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_detector.json'; -import sample_document from '../../../fixtures/plugins/security-analytics-dashboards-plugin/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. - -// Moment is not available in this repository, so refactored this variable to use Date. -// const date = moment(moment.now()).format('MM/DD/YY'); -const now = new Date(Date.now()); -const month = - now.getMonth() + 1 < 10 ? `0${now.getMonth() + 1}` : `${now.getMonth() + 1}`; -const day = now.getDate() < 10 ? `0${now.getDate()}` : `${now.getDate()}`; -const year = `${now.getFullYear()}`.substr(2); -const date = `${month}/${day}/${year}`; +import indexSettings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_windows_index_settings.json'; +import aliasMappings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_alias_mappings.json'; +import indexDoc from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_document.json'; +import ruleSettings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_windows_usb_rule.json'; + +const indexName = 'test-index'; +const detectorName = 'test-detector'; +const alertName = `${detectorName} alert condition`; + +function getFormattedDate(date) { + let year = date.getFullYear() % 100; + let month = (1 + date.getMonth()).toString().padStart(2, '0'); + let day = date.getDate().toString().padStart(2, '0'); + return month + '/' + day + '/' + year; +} + +const date = getFormattedDate(new Date(Date.now())); //moment(moment.now()).format('MM/DD/YY'); const docCount = 4; + +let testDetectorCfg; + describe('Alerts', () => { before(() => { - // Delete any pre-existing test detectors - cy.cleanUpTests() - // Create test index - .then(() => cy.createIndex(testIndex, null, 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.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); - } + testDetectorCfg = createDetector( + detectorName, + indexName, + indexSettings, + aliasMappings, + ruleSettings, + indexDoc, + 4 + ); // Wait for the detector to execute cy.wait(DETECTOR_TRIGGER_TIMEOUT); @@ -98,16 +49,18 @@ describe('Alerts', () => { beforeEach(() => { // Visit Alerts table page - cy.intercept(NODE_API.SEARCH_DETECTORS).as('detectorsSearch'); + cy.intercept(`${NODE_API.DETECTORS_BASE}/_search`).as('detectorsSearch'); // Visit Detectors page cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/alerts`); cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); // Wait for page to load - cy.contains('Security alerts'); + cy.waitForPageLoad('alerts', { + contains: 'Security alerts', + }); // Filter table to only show alerts for the test detector - cy.get(`input[type="search"]`).type(`${testDetector.name}{enter}`); + cy.get(`input[type="search"]`).type(`${testDetectorCfg.name}{enter}`); // Adjust the date range picker to display alerts from today cy.get( @@ -128,7 +81,7 @@ describe('Alerts', () => { // Confirm there are alerts created cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) + .filter(`:contains(${alertName})`) .should('have.length', docCount); }); @@ -136,10 +89,10 @@ describe('Alerts', () => { // Confirm there is a row containing the expected values cy.get('tbody > tr').should(($tr) => { expect($tr, 'start time').to.contain(date); - expect($tr, 'trigger name').to.contain(testDetector.triggers[0].name); - expect($tr, 'detector name').to.contain(testDetector.name); + expect($tr, 'trigger name').to.contain(testDetectorCfg.triggers[0].name); + expect($tr, 'detector name').to.contain(testDetectorCfg.name); expect($tr, 'status').to.contain('Active'); - expect($tr, 'severity').to.contain('4 (Low)'); + expect($tr, 'severity').to.contain('1 (Highest)'); }); }); @@ -156,7 +109,7 @@ describe('Alerts', () => { // Confirm alert condition name cy.get( '[data-test-subj="text-details-group-content-alert-trigger-name"]' - ).contains(testDetector.triggers[0].name); + ).contains(testDetectorCfg.triggers[0].name); // Confirm alert status cy.get( @@ -166,7 +119,7 @@ describe('Alerts', () => { // Confirm alert severity cy.get( '[data-test-subj="text-details-group-content-alert-severity"]' - ).contains('4 (Low)'); + ).contains('1 (Highest)'); // Confirm alert start time is present cy.get( @@ -180,19 +133,19 @@ describe('Alerts', () => { // Confirm alert detector name cy.get('[data-test-subj="text-details-group-content-detector"]').contains( - testDetector.name + testDetectorCfg.name ); // 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, `detector name`).to.contain(testDetector.name); - expect($tr, `log type`).to.contain('Windows'); + expect($tr, `rule name`).to.contain('Cypress USB Rule'); + expect($tr, `detector name`).to.contain(testDetectorCfg.name); + expect($tr, `log type`).to.contain('windows'); }); // Close the flyout @@ -216,7 +169,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') @@ -243,7 +196,7 @@ describe('Alerts', () => { // Confirm finding detector name cy.get( '[data-test-subj="finding-details-flyout-detector-link"]' - ).contains(testDetector.name); + ).contains(testDetectorCfg.name); // Confirm there's only 1 rule details accordion cy.get( @@ -257,22 +210,22 @@ describe('Alerts', () => { // Confirm the accordion button contains the expected name cy.get( '[data-test-subj="finding-details-flyout-rule-accordion-button"]' - ).contains('USB Device Plugged'); + ).contains('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'); + ).contains('Severity: High'); // Confirm the rule name cy.get( - '[data-test-subj="finding-details-flyout-USB Device Plugged-details"]' - ).contains('USB Device Plugged'); + '[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'); + ).contains('High'); // Confirm the rule category cy.get( @@ -282,16 +235,14 @@ describe('Alerts', () => { // Confirm the rule description cy.get( '[data-test-subj="finding-details-flyout-rule-description"]' - ).contains('Detects plugged USB devices'); + ).contains('USB plugged-in rule'); // Confirm the rule tags - ['low', 'windows', 'attack.initial_access', 'attack.t1200'].forEach( - (tag) => { - cy.get( - '[data-test-subj="finding-details-flyout-rule-tags"]' - ).contains(tag); - } - ); + ['high', 'windows'].forEach((tag) => { + cy.get( + '[data-test-subj="finding-details-flyout-rule-tags"]' + ).contains(tag); + }); }); // Confirm the rule document ID is present @@ -302,16 +253,14 @@ describe('Alerts', () => { // Confirm the rule index cy.get( '[data-test-subj="finding-details-flyout-rule-document-index"]' - ).contains(testIndex); + ).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"}' - ), + JSON.parse('{"winlog.event_id": 2003}'), null, 2 ); @@ -350,7 +299,7 @@ describe('Alerts', () => { cy.get('[data-test-subj="alert-details-flyout"]').within(() => { cy.get( '[data-test-subj="text-details-group-content-alert-trigger-name"]' - ).contains(testDetector.triggers[0].name); + ).contains(testDetectorCfg.triggers[0].name); }); }); @@ -369,6 +318,11 @@ describe('Alerts', () => { .within(() => { cy.get('[class="euiCheckbox__input"]').click({ force: true }); }); + cy.get('tbody > tr') + .last() + .within(() => { + cy.get('[class="euiCheckbox__input"]').click({ force: true }); + }); // Press the "Acknowledge" button cy.get('[data-test-subj="acknowledge-button"]').click({ force: true }); @@ -384,21 +338,28 @@ 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'); }); + // Confirm there are now 2 "Acknowledged" alerts + cy.get('tbody > tr') + .filter(`:contains(${alertName})`) + .should('have.length', 2); + // Filter the table to show only "Active" alerts - cy.get('[data-text="Status"]'); cy.get('[class="euiFilterSelect__items"]').within(() => { cy.contains('Acknowledged').click({ force: true }); + cy.contains('Active').click({ force: true }); }); // Confirm there are now 2 "Acknowledged" alerts cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('contain', 'Active') - .should('contain', 'Acknowledged'); + .filter(`:contains(${alertName})`) + .should('contain', 'Active'); + cy.get('tbody > tr') + .filter(`:contains(${alertName})`) + .should('have.length', 2); }); it('can be acknowledged via row button', () => { @@ -409,8 +370,8 @@ describe('Alerts', () => { }); cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', 3); + .filter(`:contains(${alertName})`) + .should('have.length', 2); cy.get('tbody > tr') // Click the "Acknowledge" icon button in the first row @@ -420,11 +381,11 @@ describe('Alerts', () => { }); cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', 2); + .filter(`:contains(${alertName})`) + .should('have.length', 1); // Filter the table to show only "Acknowledged" alerts - cy.get('[data-text="Status"]'); + cy.get('[data-text="Status"]').click({ force: true }); cy.get('[class="euiFilterSelect__items"]').within(() => { cy.contains('Active').click({ force: true }); cy.contains('Acknowledged').click({ force: true }); @@ -432,8 +393,8 @@ describe('Alerts', () => { // Confirm there are now 3 "Acknowledged" alerts cy.get('tbody > tr') - .filter(`:contains(${testDetectorAlertCondition})`) - .should('have.length', 2); + .filter(`:contains(${alertName})`) + .should('have.length', 3); }); it('can be acknowledged via flyout button', () => { @@ -484,7 +445,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') @@ -506,7 +467,7 @@ describe('Alerts', () => { // Confirm the detector details page is for the expected detector cy.get('[data-test-subj="detector-details-detector-name"]').contains( - testDetector.name + testDetectorCfg.name ); }); diff --git a/cypress/integration/plugins/security-analytics-dashboards-plugin/4_findings.spec.js b/cypress/integration/plugins/security-analytics-dashboards-plugin/4_findings.spec.js index 60e255f9a..462c83fa2 100644 --- a/cypress/integration/plugins/security-analytics-dashboards-plugin/4_findings.spec.js +++ b/cypress/integration/plugins/security-analytics-dashboards-plugin/4_findings.spec.js @@ -4,34 +4,34 @@ */ import { + createDetector, DETECTOR_TRIGGER_TIMEOUT, - NODE_API, OPENSEARCH_DASHBOARDS_URL, } from '../../../utils/plugins/security-analytics-dashboards-plugin/constants'; -import sample_document from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_document.json'; -import sample_index_settings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_index_settings.json'; -import sample_field_mappings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_field_mappings.json'; -import sample_detector from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_detector.json'; +import indexSettings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_windows_index_settings.json'; +import aliasMappings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_alias_mappings.json'; +import indexDoc from '../../../fixtures/plugins/security-analytics-dashboards-plugin/sample_document.json'; +import ruleSettings from '../../../fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/rule/create_windows_usb_rule.json'; + +const indexName = 'test-index'; +const detectorName = 'test-detector'; +const ruleName = 'Cypress USB Rule'; 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, null, sample_index_settings); - cy.createAliasMappings(indexName, 'windows', sample_field_mappings, true); - cy.createDetector(sample_detector); - - // Ingest a new document - cy.insertDocumentToIndex(indexName, '', sample_document); + createDetector( + detectorName, + indexName, + indexSettings, + aliasMappings, + ruleSettings, + indexDoc, + 4 + ); - // wait for detector interval to pass + // Wait for the detector to execute cy.wait(DETECTOR_TRIGGER_TIMEOUT); }); @@ -40,7 +40,11 @@ describe('Findings', () => { cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/findings`); // Wait for page to load - cy.contains('Findings'); + cy.waitForPageLoad('findings', { + contains: 'Findings', + }); + + cy.wait(5000); }); it('displays findings based on recently ingested data', () => { @@ -51,14 +55,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('windows'); + 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) => { @@ -77,7 +80,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( @@ -98,7 +101,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 }); @@ -111,10 +114,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); @@ -126,7 +128,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) => { @@ -135,61 +137,14 @@ describe('Findings', () => { // Click rule link cy.get( - `[data-test-subj="finding-details-flyout-USB Device Plugged-details"]` + `[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.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(NODE_API.SEARCH_DETECTORS).as('getDetector'); - cy.intercept(`${NODE_API.RULES_SEARCH}?prePackaged=true`).as( - 'getPrePackagedRules' - ); - cy.intercept(`${NODE_API.RULES_SEARCH}?prePackaged=false`).as('getRules'); - - // Click on detector to be removed - cy.contains('sample_detector').click({ force: true }); - cy.contains('Detector details'); - cy.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/utils/plugins/security-analytics-dashboards-plugin/commands.js b/cypress/utils/plugins/security-analytics-dashboards-plugin/commands.js index cfecd118f..d0de8fcfc 100644 --- a/cypress/utils/plugins/security-analytics-dashboards-plugin/commands.js +++ b/cypress/utils/plugins/security-analytics-dashboards-plugin/commands.js @@ -3,9 +3,70 @@ * SPDX-License-Identifier: Apache-2.0 */ -require('./detectors'); -require('./rules'); -require('./typings'); +const { + OPENSEARCH_DASHBOARDS_URL, + OPENSEARCH_DASHBOARDS, +} = require('./constants'); +const { NODE_API } = require('./constants'); +const { BACKEND_BASE_PATH } = require('../../base_constants'); + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +// Be able to add default options to cy.request(), https://github.com/cypress-io/cypress/issues/726 +Cypress.Commands.overwrite('request', (originalFn, ...args) => { + let defaults = {}; + // Add the basic authentication header when security enabled in the Opensearch cluster + const ADMIN_AUTH = { + username: Cypress.env('username'), + password: Cypress.env('password'), + }; + if (Cypress.env('SECURITY_ENABLED')) { + defaults.auth = ADMIN_AUTH; + } + + let options = {}; + if (typeof args[0] === 'object' && args[0] !== null) { + options = Object.assign({}, args[0]); + } else if (args.length === 1) { + [options.url] = args; + } else if (args.length === 2) { + [options.method, options.url] = args; + } else if (args.length === 3) { + [options.method, options.url, options.body] = args; + } + + Object.assign(options, { + headers: { + 'osd-xsrf': '', + }, + }); + + return originalFn(Object.assign({}, defaults, options)); +}); Cypress.Commands.add('cleanUpTests', () => { cy.deleteAllCustomRules(); @@ -17,3 +78,473 @@ Cypress.Commands.add('getTableFirstRow', (selector) => { if (!selector) return cy.get('tbody > tr').first(); return cy.get('tbody > tr:first').find(selector); }); + +Cypress.Commands.add( + 'waitForPageLoad', + (pathname, { timeout = 60000, contains = null }) => { + const fullUrl = `${OPENSEARCH_DASHBOARDS_URL}/${pathname}`; + Cypress.log({ + message: `Wait for url: ${fullUrl} to be loaded.`, + }); + cy.url({ timeout: timeout }) + .should('include', fullUrl) + .then(() => { + contains && cy.contains(contains).should('be.visible'); + }); + } +); + +Cypress.Commands.add('createDetector', (detectorJSON) => { + cy.request( + 'POST', + `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}`, + detectorJSON + ); +}); + +Cypress.Commands.add( + 'createAliasMappings', + (indexName, ruleTopic, aliasMappingsBody, partial = true) => { + const body = { + index_name: indexName, + rule_topic: ruleTopic, + partial: partial, + alias_mappings: aliasMappingsBody, + }; + cy.request({ + method: 'POST', + url: `${BACKEND_BASE_PATH}${NODE_API.MAPPINGS_BASE}`, + body: body, + }); + } +); + +Cypress.Commands.add('updateDetector', (detectorId, detectorJSON) => { + cy.request( + 'PUT', + `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}/${detectorId}`, + detectorJSON + ); +}); + +Cypress.Commands.add('deleteDetector', (detectorName) => { + const body = { + from: 0, + size: 5000, + query: { + nested: { + path: 'detector', + query: { + bool: { + must: [{ match: { 'detector.name': detectorName } }], + }, + }, + }, + }, + }; + cy.request({ + method: 'POST', + url: `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}/_search`, + failOnStatusCode: false, + body, + }).then((response) => { + if (response.status === 200) { + for (let hit of response.body.hits.hits) { + cy.request( + 'DELETE', + `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}/${hit._id}` + ); + } + } + }); +}); + +Cypress.Commands.add('deleteAllDetectors', () => { + cy.request({ + method: 'DELETE', + url: `${BACKEND_BASE_PATH}/.opensearch-sap-detectors-config`, + failOnStatusCode: false, + }).as('deleteAllDetectors'); + cy.get('@deleteAllDetectors').should((response) => { + expect(response.status).to.be.oneOf([200, 404]); + }); +}); + +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(' | ')}` }); + items.map((item) => cy.wrap(subject).type(item).type('{enter}')); + } +); + +Cypress.Commands.add( + 'clearCombobox', + { + prevSubject: true, + }, + (subject) => { + Cypress.log({ message: `Clear combobox` }); + return cy.wrap(subject).type('{selectall}{backspace}'); + // .parents('.euiFormRow__fieldWrapper') + // .find('[data-test-subj="comboBoxClearButton"]') + // .click({ force: true }); + } +); + +Cypress.Commands.add( + 'containsValue', + { + prevSubject: true, + }, + (subject, value) => + cy.wrap(subject).parents('.euiFormRow__fieldWrapper').contains(value, { + matchCase: false, + }) +); + +Cypress.Commands.add( + 'clearValue', + { + prevSubject: true, + }, + (subject) => cy.wrap(subject).type('{selectall}').type('{backspace}') +); + +Cypress.Commands.add( + 'containsError', + { + prevSubject: true, + }, + (subject, errorText) => + cy + .wrap(subject) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains(errorText) +); + +Cypress.Commands.add( + 'containsHelperText', + { + prevSubject: true, + }, + (subject, helperText) => + cy + .wrap(subject) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormHelpText') + .contains(helperText) +); + +Cypress.Commands.add( + 'shouldNotHaveError', + { + prevSubject: true, + }, + (subject) => + cy + .wrap(subject) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist') +); + +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( + 'validateTable', + { + prevSubject: true, + }, + (subject, data) => { + Cypress.log({ + message: 'Validate table elements', + }); + return cy + .wrap(subject) + .should('be.visible') + .find('tbody') + .find('tr') + .then(($tr) => { + const length = data.length; + length && cy.get($tr).should('have.length', length); + + cy.get($tr).within(($tr) => { + data.map((rowData) => { + rowData.forEach((tdData) => { + if (typeof tdData === 'string') { + tdData && cy.get($tr).find('td').contains(`${tdData}`); + } else { + // if rule is an object then use path + tdData && cy.get($tr).find('td').contains(`${tdData.path}`); + } + }); + }); + }); + }); + } +); + +Cypress.Commands.add('createIndex', (index, settings = {}) => { + cy.request('PUT', `${BACKEND_BASE_PATH}/${index}`, settings).should( + 'have.property', + 'status', + 200 + ); +}); + +Cypress.Commands.add('createIndexTemplate', (name, template) => { + cy.request( + 'PUT', + `${BACKEND_BASE_PATH}${NODE_API.INDEX_TEMPLATE_BASE}/${name}`, + template + ); +}); + +Cypress.Commands.add('ingestDocument', (indexId, documentJSON) => { + cy.request('POST', `${BACKEND_BASE_PATH}/${indexId}/_doc`, documentJSON); +}); + +Cypress.Commands.add( + 'insertDocumentToIndex', + (indexName, documentId, documentBody) => { + cy.request({ + method: 'POST', + url: `${BACKEND_BASE_PATH}/${indexName}/_doc/${documentId}`, + body: documentBody, + }); + } +); + +Cypress.Commands.add('deleteIndex', (indexName, options = {}) => { + cy.request({ + method: 'DELETE', + url: `${BACKEND_BASE_PATH}/${indexName}`, + failOnStatusCode: false, + ...options, + }); +}); + +Cypress.Commands.add('deleteAllIndices', () => { + cy.request({ + method: 'DELETE', + url: `${BACKEND_BASE_PATH}/index*,sample*,opensearch_dashboards*,test*,cypress*`, + failOnStatusCode: false, + }).as('deleteAllIndices'); + cy.get('@deleteAllIndices').should((response) => { + // Both statuses are a pass, 200 means deleted successfully and 404 there was no index to delete + expect(response.status).to.be.oneOf([200, 404]); + }); +}); + +Cypress.Commands.add('createRule', (ruleJSON) => { + return cy.request({ + method: 'POST', + url: `${OPENSEARCH_DASHBOARDS}${NODE_API.RULES_BASE}?category=${ruleJSON.category}`, + body: JSON.stringify(ruleJSON), + }); +}); + +Cypress.Commands.add('updateRule', (ruleId, ruleJSON) => { + cy.request( + 'PUT', + `${BACKEND_BASE_PATH}${NODE_API.RULES_BASE}/${ruleId}`, + ruleJSON + ); +}); + +Cypress.Commands.add('deleteRule', (ruleName) => { + const body = { + from: 0, + size: 5000, + query: { + nested: { + path: 'rule', + query: { + bool: { + must: [{ match: { 'rule.title': 'Cypress test rule' } }], + }, + }, + }, + }, + }; + cy.request({ + method: 'POST', + url: `${BACKEND_BASE_PATH}${NODE_API.RULES_BASE}/_search?pre_packaged=false`, + failOnStatusCode: false, + body, + }).then((response) => { + if (response.status === 200) { + for (let hit of response.body.hits.hits) { + if (hit._source.title === ruleName) + cy.request( + 'DELETE', + `${BACKEND_BASE_PATH}${NODE_API.RULES_BASE}/${hit._id}?forced=true` + ); + } + } + }); +}); + +Cypress.Commands.add('deleteAllCustomRules', () => { + const url = `${BACKEND_BASE_PATH}/.opensearch-sap-custom-rules-config`; + cy.request({ + method: 'DELETE', + url: url, + failOnStatusCode: false, + body: { query: { match_all: {} } }, + }).as('deleteAllCustomRules'); + cy.get('@deleteAllCustomRules').should((response) => { + expect(response.status).to.be.oneOf([200, 404]); + }); +}); + +Cypress.Commands.add( + 'ospSearch', + { + prevSubject: true, + }, + (subject, text) => { + return cy.get(subject).clear().ospType(text).realPress('Enter'); + } +); + +Cypress.Commands.add( + 'ospClear', + { + prevSubject: true, + }, + (subject) => { + return cy + .get(subject) + .wait(100) + .type('{selectall}{backspace}') + .clear({ force: true }) + .invoke('val', ''); + } +); + +Cypress.Commands.add( + 'ospType', + { + prevSubject: true, + }, + (subject, text) => { + return cy.get(subject).wait(10).focus().realType(text); + } +); + +Cypress.Commands.add( + 'pressEnterKey', + { + prevSubject: true, + }, + (subject) => { + Cypress.log({ + message: 'Enter key pressed', + }); + Cypress.automation('remote:debugger:protocol', { + command: 'Input.dispatchKeyEvent', + params: { + type: 'char', + unmodifiedText: '\r', + text: '\r', + }, + }); + + return subject; + } +); diff --git a/cypress/utils/plugins/security-analytics-dashboards-plugin/constants.js b/cypress/utils/plugins/security-analytics-dashboards-plugin/constants.js index 1b6c46edf..a0f510043 100644 --- a/cypress/utils/plugins/security-analytics-dashboards-plugin/constants.js +++ b/cypress/utils/plugins/security-analytics-dashboards-plugin/constants.js @@ -3,27 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { BASE_PATH } from '../../base_constants'; - -export const PLUGIN_NAME = 'opensearch_security_analytics_dashboards'; -export const BASE_API_PATH = '/_plugins/_security_analytics'; +import sample_detector from '../../../fixtures/plugins/security-analytics-dashboards-plugin/integration_tests/detector/create_usb_detector_data.json'; export const TWENTY_SECONDS_TIMEOUT = { timeout: 20000 }; + export const DETECTOR_TRIGGER_TIMEOUT = 65000; export const FEATURE_SYSTEM_INDICES = { DETECTORS_INDEX: '.opensearch-detectors-config', DETECTOR_QUERIES_INDEX: '.opensearch-sap-windows-detectors-queries', PRE_PACKAGED_RULES_INDEX: '.opensearch-pre-packaged-rules-config', - CUSTOM_RULES_INDEX: '.opensearch-sap-custom-rules-config', + CUSTOM_RULES_INDEX: '.opensearch-custom-rules-config', WINDOWS_ALERTS_INDEX: '.opensearch-sap-windows-alerts*', WINDOWS_FINDINGS_INDEX: '.opensearch-sap-windows-findings*', }; +export const PLUGIN_NAME = 'opensearch_security_analytics_dashboards'; + +export const BASE_API_PATH = '/_plugins/_security_analytics'; + export const NODE_API = { DETECTORS_BASE: `${BASE_API_PATH}/detectors`, + CORRELATION_BASE: `${BASE_API_PATH}/correlation/rules`, SEARCH_DETECTORS: `${BASE_API_PATH}/detectors/_search`, INDICES_BASE: `${BASE_API_PATH}/indices`, + FINDINGS_BASE: `${BASE_API_PATH}/findings`, GET_FINDINGS: `${BASE_API_PATH}/findings/_search`, DOCUMENT_IDS_QUERY: `${BASE_API_PATH}/document_ids_query`, TIME_RANGE_QUERY: `${BASE_API_PATH}/time_range_query`, @@ -31,11 +35,92 @@ export const NODE_API = { MAPPINGS_VIEW: `${BASE_API_PATH}/mappings/view`, GET_ALERTS: `${BASE_API_PATH}/alerts`, RULES_BASE: `${BASE_API_PATH}/rules`, - RULES_SEARCH: `${BASE_API_PATH}/rules/_search`, CHANNELS: `${BASE_API_PATH}/_notifications/channels`, PLUGINS: `${BASE_API_PATH}/_notifications/plugins`, ACKNOWLEDGE_ALERTS: `${BASE_API_PATH}/detectors/{detector_id}/_acknowledge/alerts`, + UPDATE_ALIASES: `${BASE_API_PATH}/update_aliases`, + CORRELATIONS: `${BASE_API_PATH}/correlations`, + LOGTYPE_BASE: `${BASE_API_PATH}/logtype`, INDEX_TEMPLATE_BASE: '/_index_template', }; -export const OPENSEARCH_DASHBOARDS_URL = `${BASE_PATH}/app/${PLUGIN_NAME}#`; +export const { baseUrl: OPENSEARCH_DASHBOARDS } = Cypress.config(); +export const OPENSEARCH_DASHBOARDS_URL = `${OPENSEARCH_DASHBOARDS}/app/${PLUGIN_NAME}#`; + +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, + }, + ], + }; + + 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); + } + + return detectorConfig; +}; diff --git a/cypress/utils/plugins/security-analytics-dashboards-plugin/detectors.js b/cypress/utils/plugins/security-analytics-dashboards-plugin/detectors.js deleted file mode 100644 index 10207f7ae..000000000 --- a/cypress/utils/plugins/security-analytics-dashboards-plugin/detectors.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const { BACKEND_BASE_PATH } = require('../../base_constants'); -const { NODE_API } = require('./constants'); - -Cypress.Commands.add('createDetector', (detectorJSON) => { - cy.request( - 'POST', - `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}`, - detectorJSON - ); -}); - -Cypress.Commands.add( - 'createAliasMappings', - (indexName, ruleTopic, aliasMappingsBody, partial = true) => { - const body = { - index_name: indexName, - rule_topic: ruleTopic, - partial: partial, - alias_mappings: aliasMappingsBody, - }; - cy.request({ - method: 'POST', - url: `${BACKEND_BASE_PATH}${NODE_API.MAPPINGS_BASE}`, - body: body, - }); - } -); - -Cypress.Commands.add('updateDetector', (detectorId, detectorJSON) => { - cy.request( - 'PUT', - `${BACKEND_BASE_PATH}/${NODE_API.DETECTORS_BASE}/${detectorId}`, - detectorJSON - ); -}); - -Cypress.Commands.add('deleteSAPDetector', (detectorName) => { - const body = { - from: 0, - size: 5000, - query: { - nested: { - path: 'detector', - query: { - bool: { - must: [{ match: { 'detector.name': detectorName } }], - }, - }, - }, - }, - }; - cy.request({ - method: 'POST', - url: `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}/_search`, - failOnStatusCode: false, - body, - }).then((response) => { - if (response.status === 200) { - for (let hit of response.body.hits.hits) { - cy.request( - 'DELETE', - `${BACKEND_BASE_PATH}${NODE_API.DETECTORS_BASE}/${hit._id}` - ); - } - } - }); -}); - -Cypress.Commands.add('deleteAllDetectors', () => { - cy.request({ - method: 'DELETE', - url: `${BACKEND_BASE_PATH}/.opensearch-sap-detectors-config`, - failOnStatusCode: false, - }); -}); diff --git a/cypress/utils/plugins/security-analytics-dashboards-plugin/index.d.ts b/cypress/utils/plugins/security-analytics-dashboards-plugin/index.d.ts new file mode 100644 index 000000000..1225c9c59 --- /dev/null +++ b/cypress/utils/plugins/security-analytics-dashboards-plugin/index.d.ts @@ -0,0 +1,283 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// eslint-disable-next-line +/// + +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; + + /** + * Should clear a field value (use with text and textarea fields) + * @example + * cy.getFieldByLabel('Rule name').clearValue() + */ + clearValue(): Chainable; + + /** + * Validates that field contains value + * Should be used with combobox or other fields that don't print its value in inputs + * @example + * cy.getFieldByLabel('Rule name').containsValue('Name') + */ + containsValue(value: string): Chainable; + + /** + * Validates that field has error text + * @example + * cy.getFieldByLabel('Rule name').containsError('This fields is invalid') + */ + containsError(errorText: string): Chainable; + + /** + * Validates that field has helper text + * @example + * cy.getFieldByLabel('Rule name').containsHelperText('Use this field for...') + */ + containsHelperText(helperText: string): Chainable; + + /** + * Should not have error text + * @example + * cy.getFieldByLabel('Rule name').shouldNotHaveError() + */ + shouldNotHaveError(): 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 + * cy.cleanUpTests() + */ + cleanUpTests(): Chainable; + + /** + * Returns table first row + * Finds elements deeper in a row with selector + * @param {string} selector + * @example + * cy.getTableFirstRow() + * cy.getTableFirstRow('td') + */ + getTableFirstRow(selector: string): Chainable; + + /** + * Waits for page to be loaded + * @param {string} pathname + * @param {any} opts + * @example + * cy.waitForPageLoad('detectors') + * cy.waitForPageLoad('detectors', { + * timeout: 20000, + * contains: 'text to verify' + * }) + */ + waitForPageLoad(pathname: string, opts?: any): Chainable; + + /** + * Returns table first row + * Can find elements deeper in a row with selector + * @param {string} text + * @example + * cy.get('selector').ospSearch('Txt to write into input') + */ + ospSearch(text: string): Chainable; + + /** + * Clears input text + * @example + * cy.get('selector').ospClear() + */ + ospClear(): Chainable; + + /** + * Returns table first row + * Can find elements deeper in a row with selector + * @param {string} text + * @example + * cy.get('selector').ospType('Txt to write into input') + */ + ospType(text: string): Chainable; + + /** + * Creates index with policy + * @example + * cy.createIndex("some_index", "some_policy") + */ + createIndex(index: string, settings?: object): Chainable; + + /** + * Creates an index template. + * @example + * cy.createIndexTemplate("some_index_template", { "index_patterns": "abc", "properties": { ... } }) + */ + createIndexTemplate(name: string, template: object): Chainable; + + /** + /** + * Deletes all indices in cluster + * @example + * cy.deleteAllIndices() + */ + deleteAllIndices(): Chainable; + + /** + * Deletes all custom rules in cluster + * @example + * cy.deleteAllCustomRules() + */ + deleteAllCustomRules(): Chainable; + + /** + * Deletes all detectors in cluster + * @example + * cy.deleteAllDetectors() + */ + deleteAllDetectors(): Chainable; + + /** + * Creates a detector + * @example + * cy.createPolicy({ "detector_type": ... }) + */ + createDetector(detectorJSON: object): Chainable; + + /** + * Creates a fields mapping aliases for detector + * @example + * cy.createAliasMappings('indexName', 'windows', {...}, true) + */ + createAliasMappings( + indexName: string, + ruleTopic: string, + aliasMappingsBody: object, + partial: boolean + ): Chainable; + + /** + * Creates a custom rule + * @example + * cy.createRule({}) + */ + createRule(ruleJSON: object): Chainable; + + /** + * Updates settings for index + * @example + * cy.updateIndexSettings("some_index", settings) + */ + updateDetector(detectorId: string, detectorJSON: object): Chainable; + + /** + * Deletes detector by its name + * @example + * cy.deleteDetector("Cypress detector name") + */ + deleteDetector(name: string): Chainable; + } +} diff --git a/cypress/utils/plugins/security-analytics-dashboards-plugin/index.js b/cypress/utils/plugins/security-analytics-dashboards-plugin/index.js new file mode 100644 index 000000000..435865356 --- /dev/null +++ b/cypress/utils/plugins/security-analytics-dashboards-plugin/index.js @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Import commands.js using ES2015 syntax: +import './commands'; +import 'cypress-real-events'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/; +Cypress.on('uncaught:exception', (err) => { + /* returning false here prevents Cypress from failing the test */ + if (resizeObserverLoopErrRe.test(err.message)) { + return false; + } +}); + +// Switch the base URL of Opensearch when security enabled in the cluster +// Not doing this for Dashboards because it can still use http when security enabled +if (Cypress.env('SECURITY_ENABLED')) { + Cypress.env('opensearch', `https://${Cypress.env('openSearchUrl')}`); +} else { + Cypress.env('opensearch', `http://${Cypress.env('openSearchUrl')}`); +} diff --git a/cypress/utils/plugins/security-analytics-dashboards-plugin/rules.js b/cypress/utils/plugins/security-analytics-dashboards-plugin/rules.js deleted file mode 100644 index 53313fa94..000000000 --- a/cypress/utils/plugins/security-analytics-dashboards-plugin/rules.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const { BASE_PATH } = require('../../base_constants'); -const { FEATURE_SYSTEM_INDICES, NODE_API } = require('./constants'); - -Cypress.Commands.add('createRule', (ruleJSON) => { - cy.request({ - method: 'POST', - url: `${BASE_PATH}${NODE_API.RULES_BASE}?category=${ruleJSON.category}`, - body: JSON.stringify(ruleJSON), - headers: { - 'osd-xsrf': false, - }, - }); -}); - -Cypress.Commands.add('updateRule', (ruleId, ruleJSON) => { - cy.request('PUT', `${BASE_PATH}/${NODE_API.RULES_BASE}/${ruleId}`, ruleJSON); -}); - -Cypress.Commands.add('deleteRule', (ruleName) => { - const body = { - from: 0, - size: 5000, - query: { - nested: { - path: 'rule', - query: { - bool: { - must: [{ match: { 'rule.title': 'Cypress test rule' } }], - }, - }, - }, - }, - }; - cy.request({ - method: 'POST', - url: `${BASE_PATH}${NODE_API.RULES_BASE}/_search?pre_packaged=false`, - failOnStatusCode: false, - body, - }).then((response) => { - if (response.status === 200) { - for (let hit of response.body.hits.hits) { - if (hit._source.title === ruleName) - cy.request( - 'DELETE', - `${BASE_PATH}${NODE_API.RULES_BASE}/${hit._id}?forced=true` - ); - } - } - }); -}); - -Cypress.Commands.add('deleteAllCustomRules', () => { - const url = `${BASE_PATH}/${FEATURE_SYSTEM_INDICES.CUSTOM_RULES_INDEX}`; - cy.request({ - method: 'DELETE', - url: url, - failOnStatusCode: false, - body: { query: { match_all: {} } }, - }); -}); diff --git a/cypress/utils/plugins/security-analytics-dashboards-plugin/typings.js b/cypress/utils/plugins/security-analytics-dashboards-plugin/typings.js deleted file mode 100644 index c354bf6f7..000000000 --- a/cypress/utils/plugins/security-analytics-dashboards-plugin/typings.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -Cypress.Commands.add( - 'ospSearch', - { - prevSubject: true, - }, - (subject, text) => { - return cy.get(subject).clear().ospType(text); - } -); - -Cypress.Commands.add( - 'ospClear', - { - prevSubject: true, - }, - (subject) => { - return cy - .get(subject) - .wait(100) - .type('{selectall}{backspace}') - .clear({ force: true }) - .invoke('val', ''); - } -); - -Cypress.Commands.add( - 'ospType', - { - prevSubject: true, - }, - (subject, text) => { - return cy.get(subject).wait(10).focus().realType(text).realPress('Enter'); - } -);