From a4a542d5063064ea45a0c2e7ff2f5b13bf819dbe Mon Sep 17 00:00:00 2001 From: Stevan Buzejic <30922513+stevanbz@users.noreply.github.com> Date: Fri, 18 Nov 2022 09:25:09 +0100 Subject: [PATCH] Monitor execution and finding integration tests (#139) Signed-off-by: Stevan Buzejic --- .../TransportIndexDetectorAction.java | 2 +- .../securityanalytics/TestHelpers.java | 99 ++ .../securityanalytics/findings/FindingIT.java | 8 +- .../resthandler/DetectorMonitorRestApiIT.java | 985 ++++++++++++++++++ .../resthandler/DetectorRestApiIT.java | 200 +--- 5 files changed, 1098 insertions(+), 196 deletions(-) create mode 100644 src/test/java/org/opensearch/securityanalytics/resthandler/DetectorMonitorRestApiIT.java diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java index 998676dcb..057033904 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java @@ -177,7 +177,7 @@ protected void doExecute(Task task, IndexDetectorRequest request, ActionListener asyncAction.start(); } - private void createMonitorFromQueries(String index, List> rulesById, Detector detector, ActionListener>listener, WriteRequest.RefreshPolicy refreshPolicy) throws SigmaError, IOException { + private void createMonitorFromQueries(String index, List> rulesById, Detector detector, ActionListener> listener, WriteRequest.RefreshPolicy refreshPolicy) throws SigmaError, IOException { List> docLevelRules = rulesById.stream().filter(it -> !it.getRight().isAggregationRule()).collect( Collectors.toList()); List> bucketLevelRules = rulesById.stream().filter(it -> it.getRight().isAggregationRule()).collect( diff --git a/src/test/java/org/opensearch/securityanalytics/TestHelpers.java b/src/test/java/org/opensearch/securityanalytics/TestHelpers.java index df509fb4e..3a5529278 100644 --- a/src/test/java/org/opensearch/securityanalytics/TestHelpers.java +++ b/src/test/java/org/opensearch/securityanalytics/TestHelpers.java @@ -457,6 +457,66 @@ public static String productIndexAvgAggRule(){ " condition: sel | avg(fieldA) by fieldC > 110"; } + public static String randomAggregationRule(String aggFunction, String signAndValue) { + String rule = "title: Remote Encrypting File System Abuse\n" + + "id: 5f92fff9-82e2-48eb-8fc1-8b133556a551\n" + + "description: Detects remote RPC calls to possibly abuse remote encryption service via MS-EFSR\n" + + "references:\n" + + " - https://attack.mitre.org/tactics/TA0008/\n" + + " - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-36942\n" + + " - https://github.com/jsecurity101/MSRPC-to-ATTACK/blob/main/documents/MS-EFSR.md\n" + + " - https://github.com/zeronetworks/rpcfirewall\n" + + " - https://zeronetworks.com/blog/stopping_lateral_movement_via_the_rpc_firewall/\n" + + "tags:\n" + + " - attack.defense_evasion\n" + + "status: experimental\n" + + "author: Sagie Dulce, Dekel Paz\n" + + "date: 2022/01/01\n" + + "modified: 2022/01/01\n" + + "logsource:\n" + + " product: rpc_firewall\n" + + " category: application\n" + + " definition: 'Requirements: install and apply the RPC Firewall to all processes with \"audit:true action:block uuid:df1941c5-fe89-4e79-bf10-463657acf44d or c681d488-d850-11d0-8c52-00c04fd90f7e'\n" + + "detection:\n" + + " sel:\n" + + " Opcode: Info\n" + + " condition: sel | %s(SeverityValue) by Version %s\n" + + "falsepositives:\n" + + " - Legitimate usage of remote file encryption\n" + + "level: high"; + return String.format(Locale.ROOT, rule, aggFunction, signAndValue); + } + + public static String randomAggregationRule(String aggFunction, String signAndValue, String opCode) { + String rule = "title: Remote Encrypting File System Abuse\n" + + "id: 5f92fff9-82e2-48eb-8fc1-8b133556a551\n" + + "description: Detects remote RPC calls to possibly abuse remote encryption service via MS-EFSR\n" + + "references:\n" + + " - https://attack.mitre.org/tactics/TA0008/\n" + + " - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-36942\n" + + " - https://github.com/jsecurity101/MSRPC-to-ATTACK/blob/main/documents/MS-EFSR.md\n" + + " - https://github.com/zeronetworks/rpcfirewall\n" + + " - https://zeronetworks.com/blog/stopping_lateral_movement_via_the_rpc_firewall/\n" + + "tags:\n" + + " - attack.defense_evasion\n" + + "status: experimental\n" + + "author: Sagie Dulce, Dekel Paz\n" + + "date: 2022/01/01\n" + + "modified: 2022/01/01\n" + + "logsource:\n" + + " product: rpc_firewall\n" + + " category: application\n" + + " definition: 'Requirements: install and apply the RPC Firewall to all processes with \"audit:true action:block uuid:df1941c5-fe89-4e79-bf10-463657acf44d or c681d488-d850-11d0-8c52-00c04fd90f7e'\n" + + "detection:\n" + + " sel:\n" + + " Opcode: %s\n" + + " condition: sel | %s(SeverityValue) by Version %s\n" + + "falsepositives:\n" + + " - Legitimate usage of remote file encryption\n" + + "level: high"; + return String.format(Locale.ROOT, rule, opCode, aggFunction, signAndValue); + } + public static String windowsIndexMapping() { return "\"properties\": {\n" + " \"AccessList\": {\n" + @@ -1455,6 +1515,45 @@ public static String windowsIndexMapping() { " }"; } + public static String randomDoc(int severity, int version, String opCode) { + String doc = "{\n" + + "\"EventTime\":\"2020-02-04T14:59:39.343541+00:00\",\n" + + "\"HostName\":\"EC2AMAZ-EPO7HKA\",\n" + + "\"Keywords\":\"9223372036854775808\",\n" + + "\"SeverityValue\":%s,\n" + + "\"Severity\":\"INFO\",\n" + + "\"EventID\":22,\n" + + "\"SourceName\":\"Microsoft-Windows-Sysmon\",\n" + + "\"ProviderGuid\":\"{5770385F-C22A-43E0-BF4C-06F5698FFBD9}\",\n" + + "\"Version\":%s,\n" + + "\"TaskValue\":22,\n" + + "\"OpcodeValue\":0,\n" + + "\"RecordNumber\":9532,\n" + + "\"ExecutionProcessID\":1996,\n" + + "\"ExecutionThreadID\":2616,\n" + + "\"Channel\":\"Microsoft-Windows-Sysmon/Operational\",\n" + + "\"Domain\":\"NT AUTHORITY\",\n" + + "\"AccountName\":\"SYSTEM\",\n" + + "\"UserID\":\"S-1-5-18\",\n" + + "\"AccountType\":\"User\",\n" + + "\"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\",\n" + + "\"Category\":\"Dns query (rule: DnsQuery)\",\n" + + "\"Opcode\":\"%s\",\n" + + "\"UtcTime\":\"2020-02-04 14:59:38.349\",\n" + + "\"ProcessGuid\":\"{b3c285a4-3cda-5dc0-0000-001077270b00}\",\n" + + "\"ProcessId\":\"1904\",\"QueryName\":\"EC2AMAZ-EPO7HKA\",\"QueryStatus\":\"0\",\n" + + "\"QueryResults\":\"172.31.46.38;\",\n" + + "\"Image\":\"C:\\\\Program Files\\\\nxlog\\\\regsvr32.exe\",\n" + + "\"EventReceivedTime\":\"2020-02-04T14:59:40.780905+00:00\",\n" + + "\"SourceModuleName\":\"in\",\n" + + "\"SourceModuleType\":\"im_msvistalog\",\n" + + "\"CommandLine\": \"eachtest\",\n" + + "\"Initiated\": \"true\"\n" + + "}"; + return String.format(Locale.ROOT, doc, severity, version, opCode); + + } + public static String randomDoc() { return "{\n" + "\"EventTime\":\"2020-02-04T14:59:39.343541+00:00\",\n" + diff --git a/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java b/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java index 7d0e1792a..7b34b7d94 100644 --- a/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java +++ b/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java @@ -165,6 +165,10 @@ public void testGetFindings_byDetectorType_success() throws IOException { " \"partial\":true" + "}" ); + + Response response = client().performRequest(createMappingRequest); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + // index 2 String index2 = createTestIndex("netflow_test", netFlowMappings()); @@ -178,7 +182,7 @@ public void testGetFindings_byDetectorType_success() throws IOException { "}" ); - Response response = client().performRequest(createMappingRequest); + response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); // Detector 1 - WINDOWS Detector detector1 = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of(randomDetectorType()), List.of(), List.of(), List.of(), List.of()))); @@ -234,7 +238,7 @@ public void testGetFindings_byDetectorType_success() throws IOException { Map executeResults = entityAsMap(executeResponse); int noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); - Assert.assertEquals(3, noOfSigmaRuleMatches); + Assert.assertEquals(5, noOfSigmaRuleMatches); // execute monitor 2 executeResponse = executeAlertingMonitor(monitorId2, Collections.emptyMap()); diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorMonitorRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorMonitorRestApiIT.java new file mode 100644 index 000000000..10e909b1c --- /dev/null +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorMonitorRestApiIT.java @@ -0,0 +1,985 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.resthandler; + +import static org.opensearch.securityanalytics.TestHelpers.randomAggregationRule; +import static org.opensearch.securityanalytics.TestHelpers.randomDetector; +import static org.opensearch.securityanalytics.TestHelpers.randomDetectorType; +import static org.opensearch.securityanalytics.TestHelpers.randomDetectorWithInputs; +import static org.opensearch.securityanalytics.TestHelpers.randomDoc; +import static org.opensearch.securityanalytics.TestHelpers.randomIndex; +import static org.opensearch.securityanalytics.TestHelpers.randomRule; +import static org.opensearch.securityanalytics.TestHelpers.windowsIndexMapping; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.commons.alerting.model.Monitor.MonitorType; +import org.opensearch.rest.RestStatus; +import org.opensearch.search.SearchHit; +import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; +import org.opensearch.securityanalytics.SecurityAnalyticsRestTestCase; +import org.opensearch.securityanalytics.config.monitors.DetectorMonitorConfig; +import org.opensearch.securityanalytics.model.Detector; +import org.opensearch.securityanalytics.model.DetectorInput; +import org.opensearch.securityanalytics.model.DetectorRule; +import org.opensearch.securityanalytics.model.Rule; + +public class DetectorMonitorRestApiIT extends SecurityAnalyticsRestTestCase { + /** + * 1. Creates detector with 5 doc prepackaged level rules and one doc level monitor based on the given rules + * 2. Creates two aggregation rules and assigns to a detector, while removing 5 prepackaged rules + * 3. Verifies that two bucket level monitor exists + * 4. Verifies the findings + * @throws IOException + */ + public void testRemoveDocLevelRuleAddAggregationRules_verifyFindings_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + Detector detector = randomDetector(getRandomPrePackagedRules()); + + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + String request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + SearchResponse response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request, true); + + assertEquals(5, response.getHits().getTotalHits().value); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + + // Verify that one document level monitor is created + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorAsMap = (Map) hit.getSourceAsMap().get("detector"); + List monitorIds = (List) (detectorAsMap).get("monitor_id"); + + assertEquals(1, monitorIds.size()); + + String monitorId = monitorIds.get(0); + String monitorType = ((Map) entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId))).get("monitor")).get("monitor_type"); + + assertEquals(MonitorType.DOC_LEVEL_MONITOR.getValue(), monitorType); + + // Create aggregation rules + String sumRuleId = createRule(randomAggregationRule( "sum", " > 2")); + String avgTermRuleId = createRule(randomAggregationRule( "avg", " > 1")); + // Update detector and empty doc level rules so detector contains only one aggregation rule + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(sumRuleId), new DetectorRule(avgTermRuleId)), + Collections.emptyList()); + Detector updatedDetector = randomDetectorWithInputs(List.of(input)); + + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(updatedDetector)); + + assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); + + Map updateResponseBody = asMap(updateResponse); + detectorId = updateResponseBody.get("_id").toString(); + + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + detectorAsMap = (Map) hit.getSourceAsMap().get("detector"); + monitorIds = (List) (detectorAsMap).get("monitor_id"); + + assertEquals(2, monitorIds.size()); + + indexDoc(index, "1", randomDoc(2, 4, "Info")); + indexDoc(index, "2", randomDoc(3, 4, "Info")); + + // Execute two bucket level monitors + for(String id: monitorIds){ + monitorType = ((Map) entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + id))).get("monitor")).get("monitor_type"); + Assert.assertEquals(MonitorType.BUCKET_LEVEL_MONITOR.getValue(), monitorType); + executeAlertingMonitor(id, Collections.emptyMap()); + } + // verify bucket level monitor findings + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + assertNotNull(getFindingsBody); + assertEquals(2, getFindingsBody.get("total_findings")); + + List aggRuleIds = List.of(sumRuleId, avgTermRuleId); + + List> findings = (List)getFindingsBody.get("findings"); + for(Map finding : findings) { + Set aggRulesFinding = ((List>)finding.get("queries")).stream().map(it -> it.get("id").toString()).collect( + Collectors.toSet()); + // Bucket monitor finding will have one rule + String aggRuleId = aggRulesFinding.iterator().next(); + + assertTrue(aggRulesFinding.contains(aggRuleId)); + + List findingDocs = (List)finding.get("related_doc_ids"); + Assert.assertEquals(2, findingDocs.size()); + assertTrue(Arrays.asList("1", "2").containsAll(findingDocs)); + } + + String findingDetectorId = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("detectorId").toString(); + assertEquals(detectorId, findingDetectorId); + + String findingIndex = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("index").toString(); + assertEquals(index, findingIndex); + } + + /** + * 1. Creates detector with 1 aggregation rule and one bucket level monitor based on the aggregation rule + * 2. Creates 5 prepackaged doc level rules and one custom doc level rule and removes the aggregation rule + * 3. Verifies that one doc level monitor exists + * 4. Verifies the findings + * @throws IOException + */ + public void testReplaceAggregationRuleWithDocRule_verifyFindings_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + String maxRuleId = createRule(randomAggregationRule( "max", " > 2")); + List detectorRules = List.of(new DetectorRule(maxRuleId)); + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, + Collections.emptyList()); + Detector detector = randomDetectorWithInputs(List.of(input)); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + String request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + SearchResponse response = executeSearchAndGetResponse(Rule.CUSTOM_RULES_INDEX, request, true); + + assertEquals(1, response.getHits().getTotalHits().value); + + request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + // Verify that one bucket level monitor is created + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorAsMap = (Map) hit.getSourceAsMap().get("detector"); + String monitorId = ((List) (detectorAsMap).get("monitor_id")).get(0); + + String monitorType = ((Map) entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId))).get("monitor")).get("monitor_type"); + + assertEquals(MonitorType.BUCKET_LEVEL_MONITOR.getValue(), monitorType); + + // Create random doc rule and 5 pre-packed rules and assign to detector + String randomDocRuleId = createRule(randomRule()); + List prepackagedRules = getRandomPrePackagedRules(); + input = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(randomDocRuleId)), + prepackagedRules.stream().map(DetectorRule::new).collect(Collectors.toList())); + Detector updatedDetector = randomDetectorWithInputs(List.of(input)); + + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(updatedDetector)); + + assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); + + Map updateResponseBody = asMap(updateResponse); + detectorId = updateResponseBody.get("_id").toString(); + + // Verify newly created doc level monitor + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + detectorAsMap = (Map) hit.getSourceAsMap().get("detector"); + List monitorIds = ((List) (detectorAsMap).get("monitor_id")); + + assertEquals(1, monitorIds.size()); + + monitorId = monitorIds.get(0); + monitorType = ((Map) entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId))).get("monitor")).get("monitor_type"); + + assertEquals(MonitorType.DOC_LEVEL_MONITOR.getValue(), monitorType); + + // Verify rules + request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request, true); + + assertEquals(6, response.getHits().getTotalHits().value); + + // Verify findings + indexDoc(index, "1", randomDoc(2, 5, "Info")); + indexDoc(index, "2", randomDoc(3, 5, "Info")); + + Response executeResponse = executeAlertingMonitor(monitorId, Collections.emptyMap()); + Map executeResults = entityAsMap(executeResponse); + int noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); + // 5 prepackaged and 1 custom doc level rule + assertEquals(6, noOfSigmaRuleMatches); + + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + assertNotNull(getFindingsBody); + // When doc level monitor is being applied one finding is generated per document + assertEquals(2, getFindingsBody.get("total_findings")); + + Set docRuleIds = new HashSet<>(prepackagedRules); + docRuleIds.add(randomDocRuleId); + + List> findings = (List)getFindingsBody.get("findings"); + List foundDocIds = new ArrayList<>(); + for(Map finding : findings) { + Set aggRulesFinding = ((List>)finding.get("queries")).stream().map(it -> it.get("id").toString()).collect( + Collectors.toSet()); + + assertTrue(docRuleIds.containsAll(aggRulesFinding)); + + List findingDocs = (List)finding.get("related_doc_ids"); + Assert.assertEquals(1, findingDocs.size()); + foundDocIds.addAll(findingDocs); + } + assertTrue(Arrays.asList("1", "2").containsAll(foundDocIds)); + } + + /** + * 1. Creates detector with prepackaged doc rules + * 2. Verifies that detector with doc level monitor is created + * 3. Removes all rules and updates detector + * 4. Verifies that detector doesn't have monitors attached + * + * @throws IOException + */ + public void testRemoveAllRulesAndUpdateDetector_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + Response createMappingResponse = client().performRequest(createMappingRequest); + + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + List randomPrepackagedRules = getRandomPrePackagedRules(); + Detector detector = randomDetector(randomPrepackagedRules); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + String request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + SearchResponse response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request, true); + + assertEquals(randomPrepackagedRules.size(), response.getHits().getTotalHits().value); + + request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + // Verify that one doc level monitor is created + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorAsMap = (Map) hit.getSourceAsMap().get("detector"); + List monitorIds = ((List) (detectorAsMap).get("monitor_id")); + + assertEquals(1, monitorIds.size()); + + String monitorId = monitorIds.get(0); + String monitorType = ((Map) entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId))).get("monitor")).get("monitor_type"); + + assertEquals(MonitorType.DOC_LEVEL_MONITOR.getValue(), monitorType); + + Detector updatedDetector = randomDetector(Collections.emptyList()); + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(updatedDetector)); + + assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); + + Map updateResponseBody = asMap(updateResponse); + detectorId = updateResponseBody.get("_id").toString(); + + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + detectorAsMap = (Map) hit.getSourceAsMap().get("detector"); + + assertTrue(((List) (detectorAsMap).get("monitor_id")).isEmpty()); + } + + /** + * 1. Creates detector with aggregation rule + * 2. Adds new aggregation rule + * 3. Updates a detector + * 4. Verifies that detector has 2 custom rules attached + * 5. Execute monitors and verifies findings + * + * @throws IOException + */ + public void testAddNewAggregationRule_verifyFindings_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + String sumRuleId = createRule(randomAggregationRule("sum", " > 1")); + List detectorRules = List.of(new DetectorRule(sumRuleId)); + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, + Collections.emptyList()); + Detector detector = randomDetectorWithInputs(List.of(input)); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + String request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + List inputArr = detectorMap.get("inputs"); + + assertEquals(1, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + // Test adding the new max monitor and updating the existing sum monitor + String maxRuleId = createRule(randomAggregationRule("max", " > 3")); + DetectorInput newInput = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(maxRuleId), new DetectorRule(sumRuleId)), + Collections.emptyList()); + Detector updatedDetector = randomDetectorWithInputs(List.of(newInput)); + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(updatedDetector)); + + assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); + + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + Map updatedDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + inputArr = updatedDetectorMap.get("inputs"); + + assertEquals(2, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + List monitorIds = ((List) (updatedDetectorMap).get("monitor_id")); + + assertEquals(2, monitorIds.size()); + + indexDoc(index, "1", randomDoc(2, 4, "Info")); + indexDoc(index, "2", randomDoc(3, 4, "Info")); + + for(String monitorId: monitorIds) { + Map monitor = (Map)(entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId)))).get("monitor"); + assertEquals(MonitorType.BUCKET_LEVEL_MONITOR.getValue(), monitor.get("monitor_type")); + executeAlertingMonitor(monitorId, Collections.emptyMap()); + } + + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + assertNotNull(getFindingsBody); + // Two bucket monitors are executed and only one finding is generated since maxRule is not fulfilling the trigger condition + assertEquals(1, getFindingsBody.get("total_findings")); + + Map finding = ((List) getFindingsBody.get("findings")).get(0); + + Set aggRulesFinding = ((List>) finding.get("queries")).stream().map(it -> it.get("id").toString()).collect( + Collectors.toSet()); + + assertEquals(sumRuleId, aggRulesFinding.iterator().next()); + + List findingDocs = ((List) finding.get("related_doc_ids")); + + assertEquals(2, findingDocs.size()); + assertTrue(Arrays.asList("1", "2").containsAll(findingDocs)); + + String findingDetectorId = ((Map)((List) getFindingsBody.get("findings")).get(0)).get("detectorId").toString(); + assertEquals(detectorId, findingDetectorId); + + String findingIndex = ((Map)((List) getFindingsBody.get("findings")).get(0)).get("index").toString(); + assertEquals(index, findingIndex); + } + + /** + * 1. Creates detector with 2 aggregation rule assigned + * 2. Verifies that 2 custom rules exists + * 3. Removes one rule and updates a detector + * 4. Verifies that detector has only one custom rule and one bucket level monitor + * + * @throws IOException + */ + public void testDeleteAggregationRule_verifyFindings_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + List aggRuleIds = new ArrayList<>(); + String avgRuleId = createRule(randomAggregationRule("avg", " > 1")); + aggRuleIds.add(avgRuleId); + String countRuleId = createRule(randomAggregationRule("count", " > 1")); + aggRuleIds.add(countRuleId); + + List detectorRules = aggRuleIds.stream().map(DetectorRule::new).collect(Collectors.toList()); + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, + Collections.emptyList()); + Detector detector = randomDetectorWithInputs(List.of(input)); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + String request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + List inputArr = detectorMap.get("inputs"); + + assertEquals(2, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + // Test deleting the aggregation rule + DetectorInput newInput = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(avgRuleId)), + Collections.emptyList()); + detector = randomDetectorWithInputs(List.of(newInput)); + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); + + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + Map updatedDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + inputArr = updatedDetectorMap.get("inputs"); + + assertEquals(1, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + inputArr = updatedDetectorMap.get("inputs"); + + assertEquals(1, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + // Verify monitors + List monitorIds = ((List) (updatedDetectorMap).get("monitor_id")); + + assertEquals(1, monitorIds.size()); + + Map monitor = (Map)(entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorIds.get(0))))).get("monitor"); + + assertEquals(MonitorType.BUCKET_LEVEL_MONITOR.getValue(), monitor.get("monitor_type")); + + indexDoc(index, "1", randomDoc(2, 4, "Info")); + indexDoc(index, "2", randomDoc(3, 4, "Info")); + indexDoc(index, "3", randomDoc(3, 4, "Test")); + executeAlertingMonitor(monitorIds.get(0), Collections.emptyMap()); + + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + assertNotNull(getFindingsBody); + + assertEquals(1, getFindingsBody.get("total_findings")); + + Map finding = ((List) getFindingsBody.get("findings")).get(0); + Set aggRulesFinding = ((List>) finding.get("queries")).stream().map(it -> it.get("id").toString()).collect( + Collectors.toSet()); + + assertEquals(avgRuleId, aggRulesFinding.iterator().next()); + + List findingDocs = (List) finding.get("related_doc_ids"); + // Matches two findings because of the opCode rule uses (Info) + assertEquals(2, findingDocs.size()); + assertTrue(Arrays.asList("1", "2").containsAll(findingDocs)); + + String findingDetectorId = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("detectorId").toString(); + assertEquals(detectorId, findingDetectorId); + + String findingIndex = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("index").toString(); + assertEquals(index, findingIndex); + } + + /** + * 1. Creates detector with 2 aggregation and prepackaged doc level rules + * 2. Replaces one aggregation rule with a new one + * 3. Verifies that number of rules is unchanged + * 4. Verifies monitor types + * 5. Verifies findings + * @throws IOException + */ + public void testReplaceAggregationRule_verifyFindings_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + List aggRuleIds = new ArrayList<>(); + String avgRuleId = createRule(randomAggregationRule("avg", " > 1")); + aggRuleIds.add(avgRuleId); + String minRuleId = createRule(randomAggregationRule("min", " > 1")); + aggRuleIds.add(minRuleId); + + List detectorRules = aggRuleIds.stream().map(DetectorRule::new).collect(Collectors.toList()); + List prepackagedDocRules = getRandomPrePackagedRules(); + + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, + prepackagedDocRules.stream().map(DetectorRule::new).collect(Collectors.toList())); + Detector detector = randomDetectorWithInputs(List.of(input)); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + String request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + List inputArr = detectorMap.get("inputs"); + + assertEquals(2, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + String maxRuleId = createRule(randomAggregationRule("max", " > 2")); + DetectorInput newInput = new DetectorInput("windows detector for security analytics", List.of("windows"), + List.of(new DetectorRule(avgRuleId), new DetectorRule(maxRuleId)), + getRandomPrePackagedRules().stream().map(DetectorRule::new).collect(Collectors.toList())); + detector = randomDetectorWithInputs(List.of(newInput)); + createResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Update detector failed", RestStatus.OK, restStatus(createResponse)); + + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + Map updatedDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + inputArr = updatedDetectorMap.get("inputs"); + + assertEquals(2, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + List monitorIds = ((List) (updatedDetectorMap).get("monitor_id")); + + assertEquals(3, monitorIds.size()); + + indexDoc(index, "1", randomDoc(2, 4, "Info")); + indexDoc(index, "2", randomDoc(3, 4, "Info")); + indexDoc(index, "3", randomDoc(3, 4, "Test")); + Map numberOfMonitorTypes = new HashMap<>(); + for(String monitorId: monitorIds) { + Map monitor = (Map)(entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId)))).get("monitor"); + numberOfMonitorTypes.merge(monitor.get("monitor_type"), 1, Integer::sum); + executeAlertingMonitor(monitorId, Collections.emptyMap()); + } + + assertEquals(2, numberOfMonitorTypes.get(MonitorType.BUCKET_LEVEL_MONITOR.getValue()).intValue()); + assertEquals(1, numberOfMonitorTypes.get(MonitorType.DOC_LEVEL_MONITOR.getValue()).intValue()); + // Verify findings + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + assertNotNull(getFindingsBody); + assertEquals(5, getFindingsBody.get("total_findings")); + + String findingDetectorId = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("detectorId").toString(); + assertEquals(detectorId, findingDetectorId); + + String findingIndex = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("index").toString(); + assertEquals(index, findingIndex); + + List docLevelFinding = new ArrayList<>(); + List> findings = (List)getFindingsBody.get("findings"); + + Set docLevelRules = new HashSet<>(prepackagedDocRules); + + for(Map finding : findings) { + List> queries = (List>)finding.get("queries"); + Set findingRules = queries.stream().map(it -> it.get("id").toString()).collect(Collectors.toSet()); + // In this test case all doc level rules are matching the finding rule ids + if(docLevelRules.containsAll(findingRules)) { + docLevelFinding.addAll((List)finding.get("related_doc_ids")); + } else { + String aggRuleId = findingRules.iterator().next(); + + List findingDocs = (List)finding.get("related_doc_ids"); + Assert.assertEquals(2, findingDocs.size()); + assertTrue(Arrays.asList("1", "2").containsAll(findingDocs)); + } + } + // Verify doc level finding + assertTrue(Arrays.asList("1", "2", "3").containsAll(docLevelFinding)); + } + + public void testMinAggregationRule_findingSuccess() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + List aggRuleIds = new ArrayList<>(); + String testOpCode = "Test"; + aggRuleIds.add(createRule(randomAggregationRule("min", " > 3", testOpCode))); + List detectorRules = aggRuleIds.stream().map(DetectorRule::new).collect(Collectors.toList()); + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, + Collections.emptyList()); + Detector detector = randomDetectorWithInputs(List.of(input)); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + String request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map detectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + + List monitorIds = ((List) (detectorMap).get("monitor_id")); + + indexDoc(index, "4", randomDoc(5, 3, testOpCode)); + indexDoc(index, "5", randomDoc(2, 3, testOpCode)); + indexDoc(index, "6", randomDoc(4, 3, testOpCode)); + indexDoc(index, "7", randomDoc(6, 2, testOpCode)); + indexDoc(index, "8", randomDoc(1, 1, testOpCode)); + + Map numberOfMonitorTypes = new HashMap<>(); + for (String monitorId: monitorIds) { + Map monitor = (Map)(entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId)))).get("monitor"); + numberOfMonitorTypes.merge(monitor.get("monitor_type"), 1, Integer::sum); + executeAlertingMonitor(monitorId, Collections.emptyMap()); + } + + // Verify findings + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + assertNotNull(getFindingsBody); + + List> findings = (List)getFindingsBody.get("findings"); + for (Map finding : findings) { + List findingDocs = (List)finding.get("related_doc_ids"); + Assert.assertEquals(1, findingDocs.size()); + assertTrue(Arrays.asList("7").containsAll(findingDocs)); + } + + String findingDetectorId = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("detectorId").toString(); + assertEquals(detectorId, findingDetectorId); + + String findingIndex = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("index").toString(); + assertEquals(index, findingIndex); + } + + + /** + * 1. Creates detector with aggregation and prepackaged rules + * (sum rule - should match docIds: 1, 2, 3; maxRule - 4, 5, 6, 7; minRule - 7) + * 2. Verifies monitor execution + * 3. Verifies findings + * + * @throws IOException + */ + public void testMultipleAggregationAndDocRules_findingSuccess() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response createMappingResponse = client().performRequest(createMappingRequest); + + assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); + + String infoOpCode = "Info"; + String testOpCode = "Test"; + + // 5 custom aggregation rules + String sumRuleId = createRule(randomAggregationRule("sum", " > 1", infoOpCode)); + String maxRuleId = createRule(randomAggregationRule("max", " > 3", testOpCode)); + String minRuleId = createRule(randomAggregationRule("min", " > 3", testOpCode)); + String avgRuleId = createRule(randomAggregationRule("avg", " > 3", infoOpCode)); + String cntRuleId = createRule(randomAggregationRule("count", " > 3", "randomTestCode")); + List aggRuleIds = List.of(sumRuleId, maxRuleId); + String randomDocRuleId = createRule(randomRule()); + List prepackagedRules = getRandomPrePackagedRules(); + + List detectorRules = List.of(new DetectorRule(sumRuleId), new DetectorRule(maxRuleId), new DetectorRule(minRuleId), + new DetectorRule(avgRuleId), new DetectorRule(cntRuleId), new DetectorRule(randomDocRuleId)); + + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, + prepackagedRules.stream().map(DetectorRule::new).collect(Collectors.toList())); + Detector detector = randomDetectorWithInputs(List.of(input)); + + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + + String request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + SearchResponse response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request, true); + + assertEquals(6, response.getHits().getTotalHits().value); + + assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + Map responseBody = asMap(createResponse); + String detectorId = responseBody.get("_id").toString(); + request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + detectorId + "\"\n" + + " }\n" + + " }\n" + + "}"; + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + Map updatedDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); + List inputArr = updatedDetectorMap.get("inputs"); + + assertEquals(6, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); + + List monitorIds = ((List) (updatedDetectorMap).get("monitor_id")); + + assertEquals(6, monitorIds.size()); + + indexDoc(index, "1", randomDoc(2, 4, infoOpCode)); + indexDoc(index, "2", randomDoc(3, 4, infoOpCode)); + indexDoc(index, "3", randomDoc(1, 4, infoOpCode)); + indexDoc(index, "4", randomDoc(5, 3, testOpCode)); + indexDoc(index, "5", randomDoc(2, 3, testOpCode)); + indexDoc(index, "6", randomDoc(4, 3, testOpCode)); + indexDoc(index, "7", randomDoc(6, 2, testOpCode)); + indexDoc(index, "8", randomDoc(1, 1, testOpCode)); + + Map numberOfMonitorTypes = new HashMap<>(); + + for (String monitorId: monitorIds) { + Map monitor = (Map)(entityAsMap(client().performRequest(new Request("GET", "/_plugins/_alerting/monitors/" + monitorId)))).get("monitor"); + numberOfMonitorTypes.merge(monitor.get("monitor_type"), 1, Integer::sum); + Response executeResponse = executeAlertingMonitor(monitorId, Collections.emptyMap()); + + // Assert monitor executions + Map executeResults = entityAsMap(executeResponse); + if (MonitorType.DOC_LEVEL_MONITOR.getValue().equals(monitor.get("monitor_type"))) { + int noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); + // 5 prepackaged and 1 custom doc level rule + assertEquals(6, noOfSigmaRuleMatches); + } else { + for(String ruleId: aggRuleIds) { + Object rule = (((Map)((Map)((List)((Map)executeResults.get("input_results")).get("results")).get(0)).get("aggregations")).get(ruleId)); + if(rule != null) { + if(ruleId == sumRuleId) { + assertRuleMonitorFinding(executeResults, ruleId,3, List.of("4")); + } else if (ruleId == maxRuleId) { + assertRuleMonitorFinding(executeResults, ruleId,5, List.of("2", "3")); + } + else if (ruleId == minRuleId) { + assertRuleMonitorFinding(executeResults, ruleId,1, List.of("2")); + } + } + } + } + } + + assertEquals(5, numberOfMonitorTypes.get(MonitorType.BUCKET_LEVEL_MONITOR.getValue()).intValue()); + assertEquals(1, numberOfMonitorTypes.get(MonitorType.DOC_LEVEL_MONITOR.getValue()).intValue()); + + Map params = new HashMap<>(); + params.put("detector_id", detectorId); + Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + + // Assert findings + assertNotNull(getFindingsBody); + // 8 findings from doc level rules, and 3 findings for aggregation (sum, max and min) + assertEquals(11, getFindingsBody.get("total_findings")); + + String findingDetectorId = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("detectorId").toString(); + assertEquals(detectorId, findingDetectorId); + + String findingIndex = ((Map)((List)getFindingsBody.get("findings")).get(0)).get("index").toString(); + assertEquals(index, findingIndex); + + List docLevelFinding = new ArrayList<>(); + List> findings = (List) getFindingsBody.get("findings"); + + Set docLevelRules = new HashSet<>(prepackagedRules); + docLevelRules.add(randomDocRuleId); + + for(Map finding : findings) { + List> queries = (List>)finding.get("queries"); + Set findingRuleIds = queries.stream().map(it -> it.get("id").toString()).collect(Collectors.toSet()); + // Doc level finding matches all doc level rules (including the custom one) in this test case + if(docLevelRules.containsAll(findingRuleIds)) { + docLevelFinding.addAll((List)finding.get("related_doc_ids")); + } else { + // In the case of bucket level monitors, queries will always contain one value + String aggRuleId = findingRuleIds.iterator().next(); + List findingDocs = (List)finding.get("related_doc_ids"); + + if(aggRuleId.equals(sumRuleId)) { + assertTrue(List.of("1", "2", "3").containsAll(findingDocs)); + } else if(aggRuleId.equals(maxRuleId)) { + assertTrue(List.of("4", "5", "6", "7").containsAll(findingDocs)); + } else if(aggRuleId.equals( minRuleId)) { + assertTrue(List.of("7").containsAll(findingDocs)); + } + } + } + + assertTrue(Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8").containsAll(docLevelFinding)); + } + + private static void assertRuleMonitorFinding(Map executeResults, String ruleId, int expectedDocCount, List expectedTriggerResult) { + List> buckets = ((List>)(((Map)((Map)((Map)((List)((Map) executeResults.get("input_results")).get("results")).get(0)).get("aggregations")).get("result_agg")).get("buckets"))); + Integer docCount = buckets.stream().mapToInt(it -> (Integer)it.get("doc_count")).sum(); + assertEquals(expectedDocCount, docCount.intValue()); + + List triggerResultBucketKeys = ((Map)((Map) ((Map)executeResults.get("trigger_results")).get(ruleId)).get("agg_result_buckets")).keySet().stream().collect(Collectors.toList()); + assertEquals(expectedTriggerResult, triggerResultBucketKeys); + } +} diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java index 6344067ef..90fe3b0d2 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java @@ -301,26 +301,18 @@ public void testCreatingADetectorWithAggregationRules() throws IOException { Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - String rule = productIndexAvgAggRule(); + String customAvgRuleId = createRule(productIndexAvgAggRule()); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), - new StringEntity(rule), new BasicHeader("Content-Type", "application/json")); - Assert.assertEquals("Create rule failed", RestStatus.CREATED, restStatus(createResponse)); - - Map responseBody = asMap(createResponse); - - String detectorId = responseBody.get("_id").toString(); - - DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(detectorId)), + DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(customAvgRuleId)), getRandomPrePackagedRules().stream().map(DetectorRule::new).collect(Collectors.toList())); Detector detector = randomDetectorWithInputs(List.of(input)); - createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); - responseBody = asMap(createResponse); + Map responseBody = asMap(createResponse); - detectorId = responseBody.get("_id").toString(); + String detectorId = responseBody.get("_id").toString(); int createdVersion = Integer.parseInt(responseBody.get("_version").toString()); Assert.assertNotEquals("response is missing Id", Detector.NO_ID, detectorId); Assert.assertTrue("incorrect version", createdVersion > 0); @@ -381,6 +373,8 @@ public void testCreatingADetectorWithAggregationRules() throws IOException { Assert.assertTrue(finding.containsKey("queries")); HashMap docLevelQuery = (HashMap) ((List) finding.get("queries")).get(0); String ruleId = docLevelQuery.get("id").toString(); + // Verify if the rule id in bucket level finding is the same as rule used for bucket monitor creation + assertEquals(customAvgRuleId, ruleId); Response getResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), null); String getDetectorResponseString = new String(getResponse.getEntity().getContent().readAllBytes()); Assert.assertTrue(getDetectorResponseString.contains(ruleId)); @@ -446,186 +440,6 @@ public void testUpdateADetector() throws IOException { Assert.assertEquals(6, response.getHits().getTotalHits().value); } - public void testUpdateDetectorAddingNewAggregationRule() throws IOException { - String index = createTestIndex(randomIndex(), productIndexMapping()); - - // Execute CreateMappingsAction to add alias mapping for index - Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); - // both req params and req body are supported - createMappingRequest.setJsonEntity( - "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + - " \"partial\":true" + - "}" - ); - - Response createMappingResponse = client().performRequest(createMappingRequest); - assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); - - String sumRuleId = createRule(sumAggregationTestRule()); - List detectorRules = List.of(new DetectorRule(sumRuleId)); - - DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, - Collections.emptyList()); - - Detector detector = randomDetectorWithInputs(List.of(input)); - - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); - Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); - - Map responseBody = asMap(createResponse); - - String detectorId = responseBody.get("_id").toString(); - - String request = "{\n" + - " \"query\" : {\n" + - " \"match_all\":{\n" + - " }\n" + - " }\n" + - "}"; - SearchResponse response = executeSearchAndGetResponse(Rule.CUSTOM_RULES_INDEX, request, true); - Assert.assertEquals(1, response.getHits().getTotalHits().value); - - // Test adding the new max monitor and updating the existing sum monitor - String maxRuleId = createRule(productIndexMaxAggRule()); - DetectorInput newInput = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(maxRuleId), new DetectorRule(sumRuleId)), - Collections.emptyList()); - Detector firstUpdatedDetector = randomDetectorWithInputs(List.of(newInput)); - Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(firstUpdatedDetector)); - Assert.assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); - request = "{\n" + - " \"query\" : {\n" + - " \"match_all\":{\n" + - " }\n" + - " }\n" + - "}"; - List hits = executeSearch(Detector.DETECTORS_INDEX, request); - SearchHit hit = hits.get(0); - Map firstUpdateDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); - List inputArr = firstUpdateDetectorMap.get("inputs"); - Assert.assertEquals(2, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); - } - - public void testUpdateDetectorDeletingExistingAggregationRule() throws IOException { - String index = createTestIndex(randomIndex(), productIndexMapping()); - - // Execute CreateMappingsAction to add alias mapping for index - Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); - // both req params and req body are supported - createMappingRequest.setJsonEntity( - "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + - " \"partial\":true" + - "}" - ); - - Response createMappingResponse = client().performRequest(createMappingRequest); - assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); - - List aggRuleIds = createAggregationRules(); - List detectorRules = aggRuleIds.stream().map(DetectorRule::new).collect(Collectors.toList()); - - DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, - Collections.emptyList()); - - Detector detector = randomDetectorWithInputs(List.of(input)); - - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); - Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); - - Map responseBody = asMap(createResponse); - - String detectorId = responseBody.get("_id").toString(); - - String request = "{\n" + - " \"query\" : {\n" + - " \"match_all\":{\n" + - " }\n" + - " }\n" + - "}"; - SearchResponse response = executeSearchAndGetResponse(Rule.CUSTOM_RULES_INDEX, request, true); - Assert.assertEquals(2, response.getHits().getTotalHits().value); - - // Test deleting the aggregation rule - DetectorInput newInput = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(aggRuleIds.get(0))), - Collections.emptyList()); - Detector firstUpdatedDetector = randomDetectorWithInputs(List.of(newInput)); - Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(firstUpdatedDetector)); - Assert.assertEquals("Update detector failed", RestStatus.OK, restStatus(updateResponse)); - request = "{\n" + - " \"query\" : {\n" + - " \"match_all\":{\n" + - " }\n" + - " }\n" + - "}"; - List hits = executeSearch(Detector.DETECTORS_INDEX, request); - SearchHit hit = hits.get(0); - Map firstUpdateDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); - List inputArr = firstUpdateDetectorMap.get("inputs"); - Assert.assertEquals(1, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); - } - - public void testUpdateDetectorWithAggregationAndDocLevelRules() throws IOException { - String index = createTestIndex(randomIndex(), productIndexMapping()); - - // Execute CreateMappingsAction to add alias mapping for index - Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); - // both req params and req body are supported - createMappingRequest.setJsonEntity( - "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + - " \"partial\":true" + - "}" - ); - - Response createMappingResponse = client().performRequest(createMappingRequest); - assertEquals(HttpStatus.SC_OK, createMappingResponse.getStatusLine().getStatusCode()); - - List aggRuleIds = createAggregationRules(); - List detectorRules = aggRuleIds.stream().map(DetectorRule::new).collect(Collectors.toList()); - - DetectorInput input = new DetectorInput("windows detector for security analytics", List.of("windows"), detectorRules, - getRandomPrePackagedRules().stream().map(DetectorRule::new).collect(Collectors.toList())); - - Detector detector = randomDetectorWithInputs(List.of(input)); - - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); - Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); - - Map responseBody = asMap(createResponse); - - String detectorId = responseBody.get("_id").toString(); - - String request = "{\n" + - " \"query\" : {\n" + - " \"match_all\":{\n" + - " }\n" + - " }\n" + - "}"; - SearchResponse response = executeSearchAndGetResponse(Rule.CUSTOM_RULES_INDEX, request, true); - Assert.assertEquals(2, response.getHits().getTotalHits().value); - - String maxRuleId = createRule(productIndexMaxAggRule()); - - DetectorInput newInput = new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(aggRuleIds.get(0)), new DetectorRule(maxRuleId)), - Collections.emptyList()); - - detector = randomDetectorWithInputs(List.of(newInput)); - createResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + detectorId, Collections.emptyMap(), toHttpEntity(detector)); - Assert.assertEquals("Update detector failed", RestStatus.OK, restStatus(createResponse)); - request = "{\n" + - " \"query\" : {\n" + - " \"match_all\":{\n" + - " }\n" + - " }\n" + - "}"; - List hits = executeSearch(Detector.DETECTORS_INDEX, request); - SearchHit hit = hits.get(0); - Map firstUpdateDetectorMap = (HashMap)(hit.getSourceAsMap().get("detector")); - List inputArr = firstUpdateDetectorMap.get("inputs"); - Assert.assertEquals(2, ((Map>) inputArr.get(0)).get("detector_input").get("custom_rules").size()); - } - @SuppressWarnings("unchecked") public void testDeletingADetector() throws IOException { String index = createTestIndex(randomIndex(), windowsIndexMapping());