From 8c2c88593b6a99964ff490353c27b1755e5a2a5b Mon Sep 17 00:00:00 2001 From: Raj Chakravarthi Date: Fri, 4 Nov 2022 19:59:33 -0400 Subject: [PATCH] secure integration tests for alerts Signed-off-by: Raj Chakravarthi --- build.gradle | 3 + .../alerts/AlertsService.java | 6 +- .../alerts/SecureAlertsRestApiIT.java | 291 ++++++++++++++++++ 3 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java diff --git a/build.gradle b/build.gradle index aa9038bc0..17ba91318 100644 --- a/build.gradle +++ b/build.gradle @@ -173,6 +173,7 @@ integTest { filter { excludeTestsMatching "org.opensearch.securityanalytics.resthandler.Secure*RestApiIT" excludeTestsMatching "org.opensearch.securityanalytics.findings.Secure*RestApiIT" + excludeTestsMatching "org.opensearch.securityanalytics.alerts.Secure*RestApiIT" } } @@ -321,6 +322,8 @@ task integTestRemote(type: RestIntegTestTask) { if (System.getProperty("https") == null || System.getProperty("https") == "false") { filter { excludeTestsMatching "org.opensearch.securityanalytics.resthandler.Secure*RestApiIT" + excludeTestsMatching "org.opensearch.securityanalytics.findings.Secure*RestApiIT" + excludeTestsMatching "org.opensearch.securityanalytics.alerts.Secure*RestApiIT" } } } diff --git a/src/main/java/org/opensearch/securityanalytics/alerts/AlertsService.java b/src/main/java/org/opensearch/securityanalytics/alerts/AlertsService.java index a2840adbc..3d7f228e7 100644 --- a/src/main/java/org/opensearch/securityanalytics/alerts/AlertsService.java +++ b/src/main/java/org/opensearch/securityanalytics/alerts/AlertsService.java @@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; import org.opensearch.action.support.GroupedActionListener; import org.opensearch.action.support.WriteRequest; @@ -17,6 +18,7 @@ import org.opensearch.commons.alerting.action.GetAlertsRequest; import org.opensearch.commons.alerting.model.Alert; import org.opensearch.commons.alerting.model.Table; +import org.opensearch.rest.RestStatus; import org.opensearch.securityanalytics.action.AckAlertsResponse; import org.opensearch.securityanalytics.action.AlertDto; import org.opensearch.securityanalytics.action.GetAlertsResponse; @@ -102,7 +104,7 @@ public void onFailure(Exception e) { @Override public void onFailure(Exception e) { - listener.onFailure(SecurityAnalyticsException.wrap(e)); + listener.onFailure(e); } }); } @@ -172,7 +174,7 @@ public void getAlerts( ActionListener listener ) { if (detectors.size() == 0) { - throw SecurityAnalyticsException.wrap(new IllegalArgumentException("detector list is empty!")); + throw new OpenSearchStatusException("detector list is empty!", RestStatus.NOT_FOUND); } List allMonitorIds = new ArrayList<>(); diff --git a/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java new file mode 100644 index 000000000..6aa4e1a6b --- /dev/null +++ b/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java @@ -0,0 +1,291 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.securityanalytics.alerts; + +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.entity.StringEntity; +import org.apache.http.message.BasicHeader; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.ResponseException; +import org.opensearch.client.RestClient; +import org.opensearch.commons.alerting.model.action.Action; +import org.opensearch.commons.rest.SecureRestClientBuilder; +import org.opensearch.rest.RestStatus; +import org.opensearch.search.SearchHit; +import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; +import org.opensearch.securityanalytics.SecurityAnalyticsRestTestCase; +import org.opensearch.securityanalytics.action.AlertDto; +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.DetectorTrigger; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import static org.opensearch.securityanalytics.TestHelpers.*; +import static org.opensearch.securityanalytics.settings.SecurityAnalyticsSettings.*; + +public class SecureAlertsRestApiIT extends SecurityAnalyticsRestTestCase { + + static String SECURITY_ANALYTICS_FULL_ACCESS_ROLE = "security_analytics_full_access"; + static String SECURITY_ANALYTICS_READ_ACCESS_ROLE = "security_analytics_read_access"; + static String TEST_HR_BACKEND_ROLE = "HR"; + static String TEST_IT_BACKEND_ROLE = "IT"; + private final String user = "userAlert"; + private RestClient userClient; + + @Before + public void create() throws IOException { + String[] backendRoles = { TEST_HR_BACKEND_ROLE }; + createUserWithData(user, user, SECURITY_ANALYTICS_FULL_ACCESS_ROLE, backendRoles ); + if (userClient == null) { + userClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), user, user).setSocketTimeout(60000).build(); + } + } + + @After + public void cleanup() throws IOException { + userClient.close(); + deleteUser(user); + } + + @SuppressWarnings("unchecked") + public void testGetAlerts_byDetectorId_success() throws IOException { + String index = createTestIndex(randomIndex(), windowsIndexMapping()); + + String rule = randomRule(); + + Response createResponse = makeRequest(userClient, "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", randomDetectorType()), + new StringEntity(rule), new BasicHeader("Content-Type", "application/json")); + Assert.assertEquals("Create rule failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + + String createdId = responseBody.get("_id").toString(); + + // 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 response = userClient.performRequest(createMappingRequest); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + + createAlertingMonitorConfigIndex(null); + Action triggerAction = randomAction(createDestination()); + + Detector detector = randomDetectorWithInputsAndTriggers(List.of(new DetectorInput("windows detector for security analytics", List.of("windows"), List.of(new DetectorRule(createdId)), + getRandomPrePackagedRules().stream().map(DetectorRule::new).collect(Collectors.toList()))), + List.of(new DetectorTrigger(null, "test-trigger", "1", List.of(), List.of(createdId), List.of(), List.of("attack.defense_evasion"), List.of(triggerAction)))); + + createResponse = makeRequest(userClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + responseBody = asMap(createResponse); + + createdId = responseBody.get("_id").toString(); + + String request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + createdId + "\"\n" + + " }\n" + + " }\n" + + "}"; + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + + String monitorId = ((List) ((Map) hit.getSourceAsMap().get("detector")).get("monitor_id")).get(0); + + indexDoc(index, "1", randomDoc()); + + Response executeResponse = executeAlertingMonitor(monitorId, Collections.emptyMap()); + Map executeResults = entityAsMap(executeResponse); + + int noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); + Assert.assertEquals(6, noOfSigmaRuleMatches); + + Assert.assertEquals(1, ((Map) executeResults.get("trigger_results")).values().size()); + + for (Map.Entry> triggerResult: ((Map>) executeResults.get("trigger_results")).entrySet()) { + Assert.assertEquals(1, ((Map) triggerResult.getValue().get("action_results")).values().size()); + + for (Map.Entry> alertActionResult: ((Map>) triggerResult.getValue().get("action_results")).entrySet()) { + Map actionResults = alertActionResult.getValue(); + + for (Map.Entry actionResult: actionResults.entrySet()) { + Map actionOutput = ((Map>) actionResult.getValue()).get("output"); + String expectedMessage = triggerAction.getSubjectTemplate().getIdOrCode().replace("{{ctx.detector.name}}", detector.getName()) + .replace("{{ctx.trigger.name}}", "test-trigger").replace("{{ctx.trigger.severity}}", "1"); + + Assert.assertEquals(expectedMessage, actionOutput.get("subject")); + Assert.assertEquals(expectedMessage, actionOutput.get("message")); + } + } + } + + request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + hits = new ArrayList<>(); + + while (hits.size() == 0) { + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); + } + + // try to do get finding as a user with read access + String userRead = "userReadAlert"; + String[] backendRoles = { TEST_IT_BACKEND_ROLE }; + createUserWithData( userRead, userRead, SECURITY_ANALYTICS_READ_ACCESS_ROLE, backendRoles ); + RestClient userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + + // Call GetAlerts API + Map params = new HashMap<>(); + params.put("detector_id", createdId); + Response getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + Map getAlertsBody = asMap(getAlertsResponse); + Assert.assertEquals(1, getAlertsBody.get("total_alerts")); + + // Enable backend filtering and try to read finding as a user with no backend roles matching the user who created the detector + enableOrDisableFilterBy("true"); + try { + getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + } catch (ResponseException e) + { + assertEquals("Get alert failed", RestStatus.FORBIDDEN, restStatus(e.getResponse())); + } + finally { + userReadOnlyClient.close(); + deleteUser(userRead); + } + + // recreate user with matching backend roles and try again + String[] newBackendRoles = { TEST_HR_BACKEND_ROLE }; + createUserWithData( userRead, userRead, SECURITY_ANALYTICS_READ_ACCESS_ROLE, newBackendRoles ); + userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + getAlertsBody = asMap(getAlertsResponse); + Assert.assertEquals(1, getAlertsBody.get("total_alerts")); + + userReadOnlyClient.close(); + deleteUser(userRead); + } + + + public void testGetAlerts_byDetectorType_success() throws IOException, InterruptedException { + 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 response = userClient.performRequest(createMappingRequest); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + + Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of(randomDetectorType()), List.of(), List.of(), List.of(), List.of()))); + + Response createResponse = makeRequest(userClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + + String createdId = responseBody.get("_id").toString(); + + String request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + createdId + "\"\n" + + " }\n" + + " }\n" + + "}"; + List hits = executeSearch(Detector.DETECTORS_INDEX, request); + SearchHit hit = hits.get(0); + + String monitorId = ((List) ((Map) hit.getSourceAsMap().get("detector")).get("monitor_id")).get(0); + + indexDoc(index, "1", randomDoc()); + + client().performRequest(new Request("POST", "_refresh")); + + Response executeResponse = executeAlertingMonitor(monitorId, Collections.emptyMap()); + Map executeResults = entityAsMap(executeResponse); + int noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); + Assert.assertEquals(5, noOfSigmaRuleMatches); + + request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + hits = new ArrayList<>(); + + while (hits.size() == 0) { + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); + } + + // try to do get finding as a user with read access + String userRead = "userReadAlert"; + String[] backendRoles = { TEST_IT_BACKEND_ROLE }; + createUserWithData( userRead, userRead, SECURITY_ANALYTICS_READ_ACCESS_ROLE, backendRoles ); + RestClient userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + + // Call GetAlerts API + Map params = new HashMap<>(); + params.put("detectorType", randomDetectorType()); + Response getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + Map getAlertsBody = asMap(getAlertsResponse); + Assert.assertEquals(1, getAlertsBody.get("total_alerts")); + + // Enable backend filtering and try to read finding as a user with no backend roles matching the user who created the detector + enableOrDisableFilterBy("true"); + try { + getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + } catch (ResponseException e) + { + assertEquals("Get alert failed", RestStatus.NOT_FOUND, restStatus(e.getResponse())); + } + finally { + userReadOnlyClient.close(); + deleteUser(userRead); + } + + // recreate user with matching backend roles and try again + String[] newBackendRoles = { TEST_HR_BACKEND_ROLE }; + createUserWithData( userRead, userRead, SECURITY_ANALYTICS_READ_ACCESS_ROLE, newBackendRoles ); + userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + getAlertsBody = asMap(getAlertsResponse); + Assert.assertEquals(1, getAlertsBody.get("total_alerts")); + + userReadOnlyClient.close(); + deleteUser(userRead); + + } + +} \ No newline at end of file