diff --git a/.github/workflows/security-test-workflow.yml b/.github/workflows/security-test-workflow.yml new file mode 100644 index 000000000..8c0b09e9b --- /dev/null +++ b/.github/workflows/security-test-workflow.yml @@ -0,0 +1,88 @@ +name: Security Test Workflow +# This workflow is triggered on pull requests and pushes to main or an OpenSearch release branch +on: + pull_request: + branches: + - "*" + push: + branches: + - "*" + +jobs: + build: + strategy: + matrix: + java: [ 11, 17 ] + # Job name + name: Build and test SecurityAnalytics + # This job runs on Linux + runs-on: ubuntu-latest + steps: + # This step uses the setup-java Github action: https://github.com/actions/setup-java + - name: Set Up JDK ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + # This step uses the checkout Github action: https://github.com/actions/checkout + - name: Checkout Branch + uses: actions/checkout@v2 + # This step uses the setup-java Github action: https://github.com/actions/setup-java + - name: Set Up JDK ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + - name: Build SecurityAnalytics + # Only assembling since the full build is governed by other workflows + run: ./gradlew assemble + + - name: Pull and Run Docker + run: | + plugin=`basename $(ls build/distributions/*.zip)` + list_of_files=`ls` + list_of_all_files=`ls build/distributions/` + version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-3` + plugin_version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-4` + qualifier=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-1` + candidate_version=`echo $plugin|awk -F- '{print $5}'| cut -d. -f 1-1` + docker_version=$version + + [[ -z $candidate_version ]] && candidate_version=$qualifier && qualifier="" + + echo plugin version plugin_version qualifier candidate_version docker_version + echo "($plugin) ($version) ($plugin_version) ($qualifier) ($candidate_version) ($docker_version)" + echo $ls $list_of_all_files + + if docker pull opensearchstaging/opensearch:$docker_version + then + echo "FROM opensearchstaging/opensearch:$docker_version" >> Dockerfile + echo "RUN if [ -d /usr/share/opensearch/plugins/opensearch-security-analytics ]; then /usr/share/opensearch/bin/opensearch-plugin remove opensearch-security-analytics; fi" >> Dockerfile + echo "ADD build/distributions/$plugin /tmp/" >> Dockerfile + echo "RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/$plugin" >> Dockerfile + + docker build -t opensearch-security-analytics:test . + echo "imagePresent=true" >> $GITHUB_ENV + else + echo "imagePresent=false" >> $GITHUB_ENV + fi + + - name: Run Docker Image + if: env.imagePresent == 'true' + run: | + cd .. + docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" opensearch-security-analytics:test + sleep 120 + + - name: Run SecurityAnalytics Test for security enabled test cases + if: env.imagePresent == 'true' + run: | + cluster_running=`curl -XGET https://localhost:9200/_cat/plugins -u admin:admin --insecure` + echo $cluster_running + security=`curl -XGET https://localhost:9200/_cat/plugins -u admin:admin --insecure |grep opensearch-security|wc -l` + echo $security + if [ $security -gt 0 ] + then + echo "Security plugin is available" + ./gradlew :integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=docker-cluster -Dsecurity=true -Dhttps=true -Duser=admin -Dpassword=admin + else + echo "Security plugin is NOT available skipping this run as tests without security have already been run" + fi diff --git a/build.gradle b/build.gradle index a33f0fe4d..17ba91318 100644 --- a/build.gradle +++ b/build.gradle @@ -165,8 +165,24 @@ integTest { systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath systemProperty "https", System.getProperty("https") + systemProperty "security", System.getProperty("security") systemProperty "user", System.getProperty("user") systemProperty "password", System.getProperty("password") + + 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" + } + } + + if (System.getProperty("https") != null || System.getProperty("https") == "true") { + filter { + excludeTestsMatching "org.opensearch.securityanalytics.*TransportIT" + } + } + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. doFirst { @@ -299,7 +315,15 @@ task integTestRemote(type: RestIntegTestTask) { if (System.getProperty("tests.rest.cluster") != null) { filter { - includeTestsMatching "org.opensearch.securityanalytics.*RestIT" + includeTestsMatching "org.opensearch.securityanalytics.*RestApiIT" + } + } + + 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/SecurityAnalyticsPlugin.java b/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java index 19ba64c79..4ccf26a33 100644 --- a/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java +++ b/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java @@ -171,6 +171,7 @@ public List getNamedXContent() { public List> getSettings() { return List.of( SecurityAnalyticsSettings.INDEX_TIMEOUT, + SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES, SecurityAnalyticsSettings.ALERT_HISTORY_ENABLED, SecurityAnalyticsSettings.ALERT_HISTORY_ROLLOVER_PERIOD, SecurityAnalyticsSettings.ALERT_HISTORY_INDEX_MAX_AGE, @@ -178,7 +179,6 @@ public List> getSettings() { SecurityAnalyticsSettings.ALERT_HISTORY_RETENTION_PERIOD, SecurityAnalyticsSettings.REQUEST_TIMEOUT, SecurityAnalyticsSettings.MAX_ACTION_THROTTLE_VALUE, - SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES, SecurityAnalyticsSettings.FINDING_HISTORY_ENABLED, SecurityAnalyticsSettings.FINDING_HISTORY_MAX_DOCS, SecurityAnalyticsSettings.FINDING_HISTORY_INDEX_MAX_AGE, diff --git a/src/main/java/org/opensearch/securityanalytics/action/AckAlertsAction.java b/src/main/java/org/opensearch/securityanalytics/action/AckAlertsAction.java index 212d2c815..0a71220a1 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/AckAlertsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/AckAlertsAction.java @@ -7,7 +7,7 @@ import org.opensearch.action.ActionType; public class AckAlertsAction extends ActionType { - public static final String NAME = "cluster:admin/opendistro/securityanalytics/alerts/ack"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/alerts/ack"; public static final AckAlertsAction INSTANCE = new AckAlertsAction(); public AckAlertsAction() { diff --git a/src/main/java/org/opensearch/securityanalytics/action/CreateIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/action/CreateIndexMappingsAction.java index c7c0fc8c4..9ddf48b66 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/CreateIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/CreateIndexMappingsAction.java @@ -9,7 +9,7 @@ public class CreateIndexMappingsAction extends ActionType{ - public static final String NAME = "cluster:admin/opendistro/securityanalytics/mapping/create"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/mapping/create"; public static final CreateIndexMappingsAction INSTANCE = new CreateIndexMappingsAction(); diff --git a/src/main/java/org/opensearch/securityanalytics/action/DeleteDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/action/DeleteDetectorAction.java index ffc87b8c2..c28e1673e 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/DeleteDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/DeleteDetectorAction.java @@ -9,7 +9,7 @@ public class DeleteDetectorAction extends ActionType { public static final DeleteDetectorAction INSTANCE = new DeleteDetectorAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/detector/delete"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/detector/delete"; public DeleteDetectorAction() { super(NAME, DeleteDetectorResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/DeleteRuleAction.java b/src/main/java/org/opensearch/securityanalytics/action/DeleteRuleAction.java index 1092f5e41..ac57e01e3 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/DeleteRuleAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/DeleteRuleAction.java @@ -9,7 +9,7 @@ public class DeleteRuleAction extends ActionType { public static final DeleteRuleAction INSTANCE = new DeleteRuleAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/rule/delete"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/rule/delete"; public DeleteRuleAction() { super(NAME, DeleteRuleResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/GetAlertsAction.java b/src/main/java/org/opensearch/securityanalytics/action/GetAlertsAction.java index 1d78ab0a2..df9422a77 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/GetAlertsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/GetAlertsAction.java @@ -9,7 +9,7 @@ public class GetAlertsAction extends ActionType { public static final GetAlertsAction INSTANCE = new GetAlertsAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/alerts/get"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/alerts/get"; public GetAlertsAction() { super(NAME, GetAlertsResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/GetDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/action/GetDetectorAction.java index bb6510ca7..2841f6dab 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/GetDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/GetDetectorAction.java @@ -9,7 +9,7 @@ public class GetDetectorAction extends ActionType { public static final GetDetectorAction INSTANCE = new GetDetectorAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/detector/get"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/detector/get"; public GetDetectorAction() { super(NAME, GetDetectorResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/GetFindingsAction.java b/src/main/java/org/opensearch/securityanalytics/action/GetFindingsAction.java index 7cb0c9415..8eb76ee01 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/GetFindingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/GetFindingsAction.java @@ -9,7 +9,7 @@ public class GetFindingsAction extends ActionType { public static final GetFindingsAction INSTANCE = new GetFindingsAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/findings/get"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/findings/get"; public GetFindingsAction() { super(NAME, GetFindingsResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/GetIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/action/GetIndexMappingsAction.java index 11adb0e5c..8177bda10 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/GetIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/GetIndexMappingsAction.java @@ -8,7 +8,7 @@ public class GetIndexMappingsAction extends ActionType{ - public static final String NAME = "cluster:admin/opendistro/securityanalytics/mapping/get"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/mapping/get"; public static final GetIndexMappingsAction INSTANCE = new GetIndexMappingsAction(); diff --git a/src/main/java/org/opensearch/securityanalytics/action/GetMappingsViewAction.java b/src/main/java/org/opensearch/securityanalytics/action/GetMappingsViewAction.java index 57d905df5..af032a93b 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/GetMappingsViewAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/GetMappingsViewAction.java @@ -8,7 +8,7 @@ public class GetMappingsViewAction extends ActionType{ - public static final String NAME = "cluster:admin/opendistro/securityanalytics/mapping/view/get"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/mapping/view/get"; public static final GetMappingsViewAction INSTANCE = new GetMappingsViewAction(); diff --git a/src/main/java/org/opensearch/securityanalytics/action/IndexDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/action/IndexDetectorAction.java index 0371ea158..18d2f219d 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/IndexDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/IndexDetectorAction.java @@ -9,7 +9,7 @@ public class IndexDetectorAction extends ActionType { public static final IndexDetectorAction INSTANCE = new IndexDetectorAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/detector/write"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/detector/write"; public IndexDetectorAction() { super(NAME, IndexDetectorResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/IndexRuleAction.java b/src/main/java/org/opensearch/securityanalytics/action/IndexRuleAction.java index 4a1c8f708..5500c34d2 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/IndexRuleAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/IndexRuleAction.java @@ -9,7 +9,7 @@ public class IndexRuleAction extends ActionType { public static final IndexRuleAction INSTANCE = new IndexRuleAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/rule/write"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/rule/write"; public IndexRuleAction() { super(NAME, IndexRuleResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/SearchDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/action/SearchDetectorAction.java index f33b3c84f..350ef1a32 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/SearchDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/SearchDetectorAction.java @@ -10,7 +10,7 @@ public class SearchDetectorAction extends ActionType { public static final SearchDetectorAction INSTANCE = new SearchDetectorAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/detector/search"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/detector/search"; public SearchDetectorAction() { super(NAME, SearchResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/SearchRuleAction.java b/src/main/java/org/opensearch/securityanalytics/action/SearchRuleAction.java index 967a2e1b9..3c0cdc2e7 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/SearchRuleAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/SearchRuleAction.java @@ -10,7 +10,7 @@ public class SearchRuleAction extends ActionType { public static final SearchRuleAction INSTANCE = new SearchRuleAction(); - public static final String NAME = "cluster:admin/opendistro/securityanalytics/rule/search"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/rule/search"; public SearchRuleAction() { super(NAME, SearchResponse::new); diff --git a/src/main/java/org/opensearch/securityanalytics/action/UpdateIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/action/UpdateIndexMappingsAction.java index 71f502fd5..3af858b14 100644 --- a/src/main/java/org/opensearch/securityanalytics/action/UpdateIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/action/UpdateIndexMappingsAction.java @@ -9,7 +9,7 @@ public class UpdateIndexMappingsAction extends ActionType{ - public static final String NAME = "cluster:admin/opendistro/securityanalytics/mapping/update"; + public static final String NAME = "cluster:admin/opensearch/securityanalytics/mapping/update"; public static final UpdateIndexMappingsAction INSTANCE = new UpdateIndexMappingsAction(); 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/main/java/org/opensearch/securityanalytics/findings/FindingsService.java b/src/main/java/org/opensearch/securityanalytics/findings/FindingsService.java index eba6c15bc..dc4a957a2 100644 --- a/src/main/java/org/opensearch/securityanalytics/findings/FindingsService.java +++ b/src/main/java/org/opensearch/securityanalytics/findings/FindingsService.java @@ -12,6 +12,7 @@ import java.util.stream.Collectors; 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.client.Client; @@ -51,7 +52,7 @@ public FindingsService(Client client) { * @param table group of search related parameters * @param listener ActionListener to get notified on response or error */ - public void getFindingsByDetectorId(String detectorId, Table table, ActionListener listener) { + public void getFindingsByDetectorId(String detectorId, Table table, ActionListener listener ) { this.client.execute(GetDetectorAction.INSTANCE, new GetDetectorRequest(detectorId, -3L), new ActionListener<>() { @Override @@ -102,7 +103,7 @@ public void onFailure(Exception e) { @Override public void onFailure(Exception e) { - listener.onFailure(SecurityAnalyticsException.wrap(e)); + listener.onFailure(e); } }); } @@ -167,7 +168,7 @@ public void getFindings( 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/main/java/org/opensearch/securityanalytics/model/Detector.java b/src/main/java/org/opensearch/securityanalytics/model/Detector.java index 4dffe6bd6..b8e83801c 100644 --- a/src/main/java/org/opensearch/securityanalytics/model/Detector.java +++ b/src/main/java/org/opensearch/securityanalytics/model/Detector.java @@ -212,7 +212,8 @@ public enum DetectorType { APACHE_ACCESS("apache_access"), CLOUDTRAIL("cloudtrail"), DNS("dns"), - S3("s3"); + S3("s3"), + TEST_WINDOWS("test_windows"); private String type; @@ -516,6 +517,10 @@ public List getMonitorIds() { return monitorIds; } + public void setUser(User user) { + this.user = user; + } + public void setId(String id) { this.id = id; } diff --git a/src/main/java/org/opensearch/securityanalytics/settings/SecurityAnalyticsSettings.java b/src/main/java/org/opensearch/securityanalytics/settings/SecurityAnalyticsSettings.java index 92b990eb6..bc18dab7d 100644 --- a/src/main/java/org/opensearch/securityanalytics/settings/SecurityAnalyticsSettings.java +++ b/src/main/java/org/opensearch/securityanalytics/settings/SecurityAnalyticsSettings.java @@ -90,7 +90,7 @@ public class SecurityAnalyticsSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ); - public static final Setting FILTER_BY_BACKEND_ROLES = Setting.boolSetting( + public static final Setting FILTER_BY_BACKEND_ROLES = Setting.boolSetting( "plugins.security_analytics.filter_by_backend_roles", false, Setting.Property.NodeScope, Setting.Property.Dynamic diff --git a/src/main/java/org/opensearch/securityanalytics/transport/SecureTransportAction.java b/src/main/java/org/opensearch/securityanalytics/transport/SecureTransportAction.java new file mode 100644 index 000000000..fc48b9a1f --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/transport/SecureTransportAction.java @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +import org.apache.lucene.search.join.ScoreMode; +import org.opensearch.commons.ConfigConstants; +import org.opensearch.commons.authuser.User; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.index.query.TermsQueryBuilder; +import org.opensearch.index.query.MatchQueryBuilder; + + +import java.util.List; +import java.util.stream.Collectors; + +/** + * TransportAction classes extend this interface to add filter-by-backend-roles functionality. + * + * 1. If filterBy is enabled + * a) Don't allow to create detector (throw error) if the logged-on user has no backend roles configured. + * + * 2. If filterBy is enabled and detector are created when filterBy is disabled: + * a) If backend_roles are saved with config, results will get filtered and data is shown + * b) If backend_roles are not saved with detector config, results will get filtered and no detectors + * will be displayed. + * c) Users can edit and save the detector to associate their backend_roles. + * + */ +public interface SecureTransportAction { + + static final Logger log = LogManager.getLogger(SecureTransportAction.class); + + /** + * reads the user from the thread context that is later used to serialize and save in config + */ + default User readUserFromThreadContext(ThreadPool threadPool) { + String userStr = threadPool.getThreadContext().getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT); + log.info("User and roles string from thread context: {}", userStr); + return User.parse(userStr); + } + + default boolean doFilterForUser(User user, boolean filterByEnabled ) { + log.debug("Is filterByEnabled: {} ; Is admin user: {}", filterByEnabled, isAdmin(user)); + if (isAdmin(user)) { + return false; + } else { + return filterByEnabled; + } + } + + /** + * 'all_access' role users are treated as admins. + */ + default boolean isAdmin(User user) { + if (user == null) { + return false; + } + if (user.getRoles().size() == 0) { + return false; + } + return user.getRoles().contains("all_access"); + } + + default String validateUserBackendRoles(User user, boolean filterByEnabled) { + if (filterByEnabled) { + if (user == null) { + return "Filter by user backend roles is enabled with security disabled."; + } else if (isAdmin(user)) { + return ""; + } else if (user.getBackendRoles().size() == 0) { + return "User doesn't have backend roles configured. Contact administrator"; + } + } + return ""; + } + + /** + * If FilterBy is enabled, this function verifies that the requester user has FilterBy permissions to access + * the resource. If FilterBy is disabled, we will assume the user has permissions and return true. + * + * This check will later to moved to the security plugin. + */ + default boolean checkUserPermissionsWithResource( + User requesterUser, + User resourceUser, + String resourceType, + String resourceId, + boolean filterByEnabled + ) { + + if (!doFilterForUser(requesterUser, filterByEnabled)) return true; + + List resourceBackendRoles = resourceUser.getBackendRoles(); + List requesterBackendRoles = requesterUser.getBackendRoles(); + + if (resourceBackendRoles == null ||requesterBackendRoles == null || isIntersectListsEmpty(resourceBackendRoles, requesterBackendRoles)) { + return false; + } + return true; + } + + + default boolean isIntersectListsEmpty(List a, List b) { + return (a.stream() + .distinct() + .filter(b::contains) + .collect(Collectors.toSet()).size()==0); + } + + default void addFilter(User user,SearchSourceBuilder searchSourceBuilder,String fieldName) { + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(searchSourceBuilder.query()); + boolQueryBuilder.filter(QueryBuilders.nestedQuery("detector", QueryBuilders.termsQuery(fieldName, user.getBackendRoles()), ScoreMode.Avg)); + searchSourceBuilder.query(boolQueryBuilder); + } + +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java index 4b2c7a384..bef747909 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java @@ -15,26 +15,34 @@ import org.opensearch.securityanalytics.mapper.MapperService; import org.opensearch.securityanalytics.action.CreateIndexMappingsRequest; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; public class TransportCreateIndexMappingsAction extends HandledTransportAction { private MapperService mapperService; private ClusterService clusterService; + private final ThreadPool threadPool; + + @Inject public TransportCreateIndexMappingsAction( TransportService transportService, ActionFilters actionFilters, + ThreadPool threadPool, MapperService mapperService, ClusterService clusterService ) { super(CreateIndexMappingsAction.NAME, transportService, actionFilters, CreateIndexMappingsRequest::new); this.clusterService = clusterService; this.mapperService = mapperService; + this.threadPool = threadPool; } @Override protected void doExecute(Task task, CreateIndexMappingsRequest request, ActionListener actionListener) { + this.threadPool.getThreadContext().stashContext(); + IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); if (index == null) { actionListener.onFailure(new IllegalStateException("Could not find index [" + request.getIndexName() + "]")); diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportDeleteDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportDeleteDetectorAction.java index 67d168ccb..fe9dcb051 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportDeleteDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportDeleteDetectorAction.java @@ -106,6 +106,7 @@ class AsyncDeleteDetectorAction { } void start() { + TransportDeleteDetectorAction.this.threadPool.getThreadContext().stashContext(); String detectorId = request.getDetectorId(); GetRequest getRequest = new GetRequest(Detector.DETECTORS_INDEX, detectorId); client.get(getRequest, diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetDetectorAction.java index 08aa224bb..c38deaac8 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetDetectorAction.java @@ -14,6 +14,8 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.commons.authuser.User; import org.opensearch.common.inject.Inject; import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.settings.Settings; @@ -30,12 +32,13 @@ import org.opensearch.securityanalytics.action.GetDetectorAction; import org.opensearch.securityanalytics.model.Detector; import org.opensearch.securityanalytics.model.DetectorInput; - import org.opensearch.securityanalytics.action.GetDetectorRequest; import org.opensearch.securityanalytics.action.GetDetectorResponse; - +import org.opensearch.securityanalytics.settings.SecurityAnalyticsSettings; import org.opensearch.securityanalytics.util.SecurityAnalyticsException; +import org.opensearch.securityanalytics.util.DetectorIndices; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import java.io.IOException; @@ -44,25 +47,52 @@ import static org.opensearch.rest.RestStatus.OK; -public class TransportGetDetectorAction extends HandledTransportAction { +public class TransportGetDetectorAction extends HandledTransportAction implements SecureTransportAction { private final Client client; private final NamedXContentRegistry xContentRegistry; + private final DetectorIndices detectorIndices; + + private final ClusterService clusterService; + + private final Settings settings; + + private final ThreadPool threadPool; + + private volatile Boolean filterByEnabled; + private static final Logger log = LogManager.getLogger(TransportGetDetectorAction.class); @Inject - public TransportGetDetectorAction(TransportService transportService, ActionFilters actionFilters, NamedXContentRegistry xContentRegistry, Client client) { + public TransportGetDetectorAction(TransportService transportService, ActionFilters actionFilters, DetectorIndices detectorIndices, ClusterService clusterService, NamedXContentRegistry xContentRegistry, Client client, Settings settings) { super(GetDetectorAction.NAME, transportService, actionFilters, GetDetectorRequest::new); this.xContentRegistry = xContentRegistry; this.client = client; + this.detectorIndices = detectorIndices; + this.clusterService = clusterService; + this.threadPool = this.detectorIndices.getThreadPool(); + this.settings = settings; + this.filterByEnabled = SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES.get(this.settings); + + this.clusterService.getClusterSettings().addSettingsUpdateConsumer(SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES, this::setFilterByEnabled); } @Override protected void doExecute(Task task, GetDetectorRequest request, ActionListener actionListener) { + User user = readUserFromThreadContext(this.threadPool); + + String validateBackendRoleMessage = validateUserBackendRoles(user, this.filterByEnabled); + if (!"".equals(validateBackendRoleMessage)) { + actionListener.onFailure(new OpenSearchStatusException("Do not have permissions to resource", RestStatus.FORBIDDEN)); + return; + } + + this.threadPool.getThreadContext().stashContext(); + GetRequest getRequest = new GetRequest(Detector.DETECTORS_INDEX, request.getDetectorId()) .version(request.getVersion()); @@ -81,8 +111,21 @@ public void onResponse(GetResponse response) { response.getSourceAsBytesRef(), XContentType.JSON ); detector = Detector.docParse(xcp, response.getId(), response.getVersion()); + assert detector != null; + // security is enabled and filterby is enabled + if (!checkUserPermissionsWithResource( + user, + detector.getUser(), + "detector", + detector.getId(), + TransportGetDetectorAction.this.filterByEnabled + ) + ) { + actionListener.onFailure(new OpenSearchStatusException("Do not have permissions to resource", RestStatus.FORBIDDEN)); + return; + } } - assert detector != null; + actionListener.onResponse(new GetDetectorResponse(detector.getId(), detector.getVersion(), OK, detector)); } catch (IOException ex) { actionListener.onFailure(ex); @@ -96,4 +139,8 @@ public void onFailure(Exception e) { }); } + private void setFilterByEnabled(boolean filterByEnabled) { + this.filterByEnabled = filterByEnabled; + } + } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetFindingsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetFindingsAction.java index 35fff632c..f279e4397 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetFindingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetFindingsAction.java @@ -10,16 +10,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.search.join.ScoreMode; +import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.commons.authuser.User; import org.opensearch.index.query.NestedQueryBuilder; import org.opensearch.index.query.QueryBuilders; +import org.opensearch.rest.RestStatus; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.securityanalytics.action.GetFindingsAction; import org.opensearch.securityanalytics.action.GetFindingsRequest; @@ -27,14 +32,17 @@ import org.opensearch.securityanalytics.action.SearchDetectorRequest; import org.opensearch.securityanalytics.findings.FindingsService; import org.opensearch.securityanalytics.model.Detector; +import org.opensearch.securityanalytics.settings.SecurityAnalyticsSettings; +import org.opensearch.securityanalytics.util.DetectorIndices; import org.opensearch.securityanalytics.util.DetectorUtils; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import static org.opensearch.securityanalytics.util.DetectorUtils.DETECTOR_TYPE_PATH; -public class TransportGetFindingsAction extends HandledTransportAction { +public class TransportGetFindingsAction extends HandledTransportAction implements SecureTransportAction { private final TransportSearchDetectorAction transportSearchDetectorAction; @@ -42,25 +50,50 @@ public class TransportGetFindingsAction extends HandledTransportAction actionListener) { + + User user = readUserFromThreadContext(this.threadPool); + + String validateBackendRoleMessage = validateUserBackendRoles(user, this.filterByEnabled); + if (!"".equals(validateBackendRoleMessage)) { + actionListener.onFailure(new OpenSearchStatusException("Do not have permissions to resource", RestStatus.FORBIDDEN)); + return; + } + if (request.getDetectorType() == null) { findingsService.getFindingsByDetectorId( request.getDetectorId(), request.getTable(), actionListener - ); + ); } else { // "detector" is nested type so we have to use nested query NestedQueryBuilder queryBuilder = @@ -105,4 +138,8 @@ public void onFailure(Exception e) { } } + private void setFilterByEnabled(boolean filterByEnabled) { + this.filterByEnabled = filterByEnabled; + } + } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetIndexMappingsAction.java index b9d790410..e3857a74a 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetIndexMappingsAction.java @@ -15,27 +15,33 @@ import org.opensearch.securityanalytics.action.GetIndexMappingsRequest; import org.opensearch.securityanalytics.action.GetIndexMappingsResponse; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; public class TransportGetIndexMappingsAction extends HandledTransportAction { private MapperService mapperService; private ClusterService clusterService; + private final ThreadPool threadPool; + @Inject public TransportGetIndexMappingsAction( TransportService transportService, ActionFilters actionFilters, GetIndexMappingsAction getIndexMappingsAction, MapperService mapperService, - ClusterService clusterService + ClusterService clusterService, + ThreadPool threadPool ) { super(getIndexMappingsAction.NAME, transportService, actionFilters, GetIndexMappingsRequest::new); this.clusterService = clusterService; this.mapperService = mapperService; + this.threadPool = threadPool; } @Override protected void doExecute(Task task, GetIndexMappingsRequest request, ActionListener actionListener) { + this.threadPool.getThreadContext().stashContext(); IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); if (index == null) { actionListener.onFailure(new IllegalStateException("Could not find index [" + request.getIndexName() + "]")); diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java index bf71bd312..9a0bfa16b 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java @@ -18,11 +18,13 @@ import org.opensearch.securityanalytics.action.GetMappingsViewResponse; import org.opensearch.securityanalytics.mapper.MapperService; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; public class TransportGetMappingsViewAction extends HandledTransportAction { private MapperService mapperService; private ClusterService clusterService; + private final ThreadPool threadPool; @Inject public TransportGetMappingsViewAction( @@ -30,15 +32,18 @@ public TransportGetMappingsViewAction( ActionFilters actionFilters, GetMappingsViewAction getMappingsViewAction, MapperService mapperService, - ClusterService clusterService + ClusterService clusterService, + ThreadPool threadPool ) { super(getMappingsViewAction.NAME, transportService, actionFilters, GetMappingsViewRequest::new); this.clusterService = clusterService; this.mapperService = mapperService; + this.threadPool = threadPool; } @Override protected void doExecute(Task task, GetMappingsViewRequest request, ActionListener actionListener) { + this.threadPool.getThreadContext().stashContext(); IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); if (index == null) { actionListener.onFailure(new IllegalStateException("Could not find index [" + request.getIndexName() + "]")); diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java index 5f66a1aec..a6515a32e 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexDetectorAction.java @@ -54,6 +54,7 @@ import org.opensearch.commons.alerting.model.DocumentLevelTrigger; import org.opensearch.commons.alerting.model.Monitor; import org.opensearch.commons.alerting.model.action.Action; +import org.opensearch.commons.authuser.User; import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilders; import org.opensearch.index.reindex.BulkByScrollResponse; @@ -85,7 +86,7 @@ import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; -public class TransportIndexDetectorAction extends HandledTransportAction { +public class TransportIndexDetectorAction extends HandledTransportAction implements SecureTransportAction { public static final String PLUGIN_OWNER_FIELD = "security_analytics"; private static final Logger log = LogManager.getLogger(TransportIndexDetectorAction.class); @@ -106,12 +107,14 @@ public class TransportIndexDetectorAction extends HandledTransportAction listener) { - AsyncIndexDetectorsAction asyncAction = new AsyncIndexDetectorsAction(task, request, listener); + User user = readUserFromThreadContext(this.threadPool); + + String validateBackendRoleMessage = validateUserBackendRoles(user, this.filterByEnabled); + if (!"".equals(validateBackendRoleMessage)) { + listener.onFailure(SecurityAnalyticsException.wrap(new OpenSearchStatusException(validateBackendRoleMessage, RestStatus.FORBIDDEN))); + return; + } + + AsyncIndexDetectorsAction asyncAction = new AsyncIndexDetectorsAction(user, task, request, listener); asyncAction.start(); } @@ -276,17 +290,22 @@ class AsyncIndexDetectorsAction { private final AtomicReference response; private final AtomicBoolean counter = new AtomicBoolean(); private final Task task; + private final User user; - AsyncIndexDetectorsAction(Task task, IndexDetectorRequest request, ActionListener listener) { + AsyncIndexDetectorsAction(User user, Task task, IndexDetectorRequest request, ActionListener listener) { this.task = task; this.request = request; this.listener = listener; + this.user = user; this.response = new AtomicReference<>(); } void start() { try { + + TransportIndexDetectorAction.this.threadPool.getThreadContext().stashContext(); + if (!detectorIndices.detectorIndexExists()) { detectorIndices.initDetectorIndex(new ActionListener<>() { @Override @@ -343,7 +362,6 @@ void prepareDetectorIndexing() throws IOException { void createDetector() { Detector detector = request.getDetector(); - String ruleTopic = detector.getDetectorType(); request.getDetector().setAlertsIndex(DetectorMonitorConfig.getAlertsIndex(ruleTopic)); @@ -353,6 +371,11 @@ void createDetector() { request.getDetector().setFindingsIndexPattern(DetectorMonitorConfig.getFindingsIndexPattern(ruleTopic)); request.getDetector().setRuleIndex(DetectorMonitorConfig.getRuleIndex(ruleTopic)); + User originalContextUser = this.user; + log.debug("user from original context is {}", originalContextUser); + request.getDetector().setUser(originalContextUser); + + if (!detector.getInputs().isEmpty()) { try { ruleTopicIndices.initRuleTopicIndex(detector.getRuleIndex(), new ActionListener<>() { @@ -391,6 +414,9 @@ public void onFailure(Exception e) { void updateDetector() { String id = request.getDetectorId(); + User originalContextUser = this.user; + log.debug("user from original context is {}", originalContextUser); + GetRequest request = new GetRequest(Detector.DETECTORS_INDEX, id); client.get(request, new ActionListener<>() { @Override @@ -407,7 +433,21 @@ public void onResponse(GetResponse response) { ); Detector detector = Detector.docParse(xcp, response.getId(), response.getVersion()); - onGetResponse(detector); + + // security is enabled and filterby is enabled + if (!checkUserPermissionsWithResource( + originalContextUser, + detector.getUser(), + "detector", + detector.getId(), + TransportIndexDetectorAction.this.filterByEnabled + ) + + ) { + onFailure(SecurityAnalyticsException.wrap(new OpenSearchStatusException("Do not have permissions to resource", RestStatus.FORBIDDEN))); + return; + } + onGetResponse(detector, detector.getUser()); } catch (IOException e) { onFailures(e); } @@ -420,7 +460,7 @@ public void onFailure(Exception e) { }); } - void onGetResponse(Detector currentDetector) { + void onGetResponse(Detector currentDetector, User user) { if (request.getDetector().getEnabled() && currentDetector.getEnabled()) { request.getDetector().setEnabledTime(currentDetector.getEnabledTime()); } @@ -429,12 +469,16 @@ void onGetResponse(Detector currentDetector) { String ruleTopic = detector.getDetectorType(); + log.debug("user in update detector {}", user); + + request.getDetector().setAlertsIndex(DetectorMonitorConfig.getAlertsIndex(ruleTopic)); request.getDetector().setAlertsHistoryIndex(DetectorMonitorConfig.getAlertsHistoryIndex(ruleTopic)); request.getDetector().setAlertsHistoryIndexPattern(DetectorMonitorConfig.getAlertsHistoryIndexPattern(ruleTopic)); request.getDetector().setFindingsIndex(DetectorMonitorConfig.getFindingsIndex(ruleTopic)); request.getDetector().setFindingsIndexPattern(DetectorMonitorConfig.getFindingsIndexPattern(ruleTopic)); request.getDetector().setRuleIndex(DetectorMonitorConfig.getRuleIndex(ruleTopic)); + request.getDetector().setUser(user); if (!detector.getInputs().isEmpty()) { try { @@ -755,4 +799,9 @@ private void finishHim(Detector detector, Exception t) { })); } } + + private void setFilterByEnabled(boolean filterByEnabled) { + this.filterByEnabled = filterByEnabled; + } + } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java index 5eef8807d..76c5e3b7e 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java @@ -129,6 +129,7 @@ class AsyncIndexRulesAction { } void start() { + TransportIndexRuleAction.this.threadPool.getThreadContext().stashContext(); try { if (!ruleIndices.ruleIndexExists(false)) { ruleIndices.initRuleIndex(new ActionListener<>() { diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchDetectorAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchDetectorAction.java index e669a0547..f4bab3762 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchDetectorAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchDetectorAction.java @@ -12,14 +12,19 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.commons.authuser.User; import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.rest.RestStatus; +import org.opensearch.common.settings.Settings; +import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.xcontent.NamedXContentRegistry; - +import org.opensearch.rest.RestStatus; import org.opensearch.securityanalytics.action.SearchDetectorAction; import org.opensearch.securityanalytics.action.SearchDetectorRequest; +import org.opensearch.securityanalytics.settings.SecurityAnalyticsSettings; +import org.opensearch.securityanalytics.util.DetectorIndices; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -27,25 +32,53 @@ import static org.opensearch.rest.RestStatus.OK; -public class TransportSearchDetectorAction extends HandledTransportAction { +public class TransportSearchDetectorAction extends HandledTransportAction implements SecureTransportAction { private final Client client; private final NamedXContentRegistry xContentRegistry; + private final ClusterService clusterService; + + private final DetectorIndices detectorIndices; + + private final Settings settings; + + private final ThreadPool threadPool; + + private volatile Boolean filterByEnabled; + private static final Logger log = LogManager.getLogger(TransportSearchDetectorAction.class); @Inject - public TransportSearchDetectorAction(TransportService transportService, ActionFilters actionFilters, NamedXContentRegistry xContentRegistry, Client client) { + public TransportSearchDetectorAction(TransportService transportService, ClusterService clusterService, DetectorIndices detectorIndices, ActionFilters actionFilters, NamedXContentRegistry xContentRegistry, Settings settings, Client client) { super(SearchDetectorAction.NAME, transportService, actionFilters, SearchDetectorRequest::new); this.xContentRegistry = xContentRegistry; this.client = client; + this.detectorIndices = detectorIndices; + this.clusterService = clusterService; + this.threadPool = this.detectorIndices.getThreadPool(); + this.settings = settings; + + this.filterByEnabled = SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES.get(this.settings); + + this.clusterService.getClusterSettings().addSettingsUpdateConsumer(SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES, this::setFilterByEnabled); } @Override protected void doExecute(Task task, SearchDetectorRequest searchDetectorRequest, ActionListener actionListener) { + User user = readUserFromThreadContext(this.threadPool); + + if (doFilterForUser(user, this.filterByEnabled)) { + // security is enabled and filterby is enabled + log.info("Filtering result by: {}", user.getBackendRoles()); + addFilter(user, searchDetectorRequest.searchRequest().source(), "detector.user.backend_roles.keyword"); + } + + this.threadPool.getThreadContext().stashContext(); + client.search(searchDetectorRequest.searchRequest(), new ActionListener<>() { @Override public void onResponse(SearchResponse response) { @@ -59,4 +92,8 @@ public void onFailure(Exception e) { }); } + private void setFilterByEnabled(boolean filterByEnabled) { + this.filterByEnabled = filterByEnabled; + } + } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchRuleAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchRuleAction.java index 94dc97465..1f7ad8c83 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchRuleAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportSearchRuleAction.java @@ -87,6 +87,7 @@ class AsyncSearchRulesAction { } void start() { + TransportSearchRuleAction.this.threadPool.getThreadContext().stashContext(); if (request.isPrepackaged()) { ruleIndices.initPrepackagedRulesIndex( new ActionListener<>() { diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportUpdateIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportUpdateIndexMappingsAction.java index 3a72981d5..08dad2c5d 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportUpdateIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportUpdateIndexMappingsAction.java @@ -15,6 +15,7 @@ import org.opensearch.securityanalytics.mapper.MapperService; import org.opensearch.securityanalytics.action.UpdateIndexMappingsRequest; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import java.io.IOException; @@ -24,10 +25,13 @@ public class TransportUpdateIndexMappingsAction extends HandledTransportAction actionListener) { + this.threadPool.getThreadContext().stashContext(); try { IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); if (index == null) { diff --git a/src/main/resources/OSMapping/mapper_topics.json b/src/main/resources/OSMapping/mapper_topics.json index 930adba62..f008a88f3 100644 --- a/src/main/resources/OSMapping/mapper_topics.json +++ b/src/main/resources/OSMapping/mapper_topics.json @@ -2,5 +2,6 @@ "netflow": "OSMapping/network/NetFlowMapping.json", "macos": "OSMapping/macos/mappings.json", "network": "OSMapping/network/mappings.json", - "windows": "OSMapping/windows/mappings.json" + "windows": "OSMapping/windows/mappings.json", + "test_windows": "OSMapping/test_windows/mappings.json" } \ No newline at end of file diff --git a/src/main/resources/OSMapping/test_windows/fieldmappings.yml b/src/main/resources/OSMapping/test_windows/fieldmappings.yml new file mode 100644 index 000000000..7567e715b --- /dev/null +++ b/src/main/resources/OSMapping/test_windows/fieldmappings.yml @@ -0,0 +1,11 @@ +# this file provides pre-defined mappings for Sigma fields defined for all Sigma rules under windows log group to their corresponding ECS Fields. +fieldmappings: + EventID: event_uid + HiveName: unmapped.HiveName + fieldB: mappedB + fieldA1: mappedA + CommandLine: windows-event_data-CommandLine + HostName: windows-hostname + Message: windows-message + Provider_Name: windows-provider-name + ServiceName: windows-servicename diff --git a/src/main/resources/OSMapping/test_windows/mappings.json b/src/main/resources/OSMapping/test_windows/mappings.json new file mode 100644 index 000000000..48cdda71d --- /dev/null +++ b/src/main/resources/OSMapping/test_windows/mappings.json @@ -0,0 +1,28 @@ +{ + "properties": { + "windows-event_data-CommandLine": { + "type": "alias", + "path": "CommandLine" + }, + "event_uid": { + "type": "alias", + "path": "EventID" + }, + "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" + } + } +} \ No newline at end of file diff --git a/src/main/resources/rules/test_windows/dns_query_win_regsvr32_network_activity.yml b/src/main/resources/rules/test_windows/dns_query_win_regsvr32_network_activity.yml new file mode 100644 index 000000000..0a9ffb60d --- /dev/null +++ b/src/main/resources/rules/test_windows/dns_query_win_regsvr32_network_activity.yml @@ -0,0 +1,35 @@ +title: Regsvr32 Network Activity +id: 36e037c4-c228-4866-b6a3-48eb292b9955 +related: + - id: c7e91a02-d771-4a6d-a700-42587e0b1095 + type: derived +description: Detects network connections and DNS queries initiated by Regsvr32.exe +references: + - https://pentestlab.blog/2017/05/11/applocker-bypass-regsvr32/ + - https://oddvar.moe/2017/12/13/applocker-case-study-how-insecure-is-it-really-part-1/ + - https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1117/T1117.md +tags: + - attack.execution + - attack.t1559.001 + - attack.defense_evasion + - attack.t1218.010 +author: Dmitriy Lifanov, oscd.community +status: experimental +date: 2019/10/25 +modified: 2021/09/21 +logsource: + category: dns_query + product: windows +detection: + selection: + Image|endswith: '\regsvr32.exe' + condition: selection +fields: + - ComputerName + - User + - Image + - DestinationIp + - DestinationPort +falsepositives: + - Unknown +level: high \ No newline at end of file diff --git a/src/main/resources/rules/test_windows/net_connection_win_regsvr32_network_activity.yml b/src/main/resources/rules/test_windows/net_connection_win_regsvr32_network_activity.yml new file mode 100644 index 000000000..79d24648f --- /dev/null +++ b/src/main/resources/rules/test_windows/net_connection_win_regsvr32_network_activity.yml @@ -0,0 +1,32 @@ +title: Regsvr32 Network Activity +id: c7e91a02-d771-4a6d-a700-42587e0b1095 +description: Detects network connections and DNS queries initiated by Regsvr32.exe +references: + - https://pentestlab.blog/2017/05/11/applocker-bypass-regsvr32/ + - https://oddvar.moe/2017/12/13/applocker-case-study-how-insecure-is-it-really-part-1/ + - https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1117/T1117.md +author: Dmitriy Lifanov, oscd.community +status: experimental +date: 2019/10/25 +modified: 2021/09/21 +logsource: + category: network_connection + product: windows +detection: + selection: + Image|endswith: '\regsvr32.exe' + condition: selection +fields: + - ComputerName + - User + - Image + - DestinationIp + - DestinationPort +falsepositives: + - Unknown +level: high +tags: + - attack.execution + - attack.t1559.001 + - attack.defense_evasion + - attack.t1218.010 \ No newline at end of file diff --git a/src/main/resources/rules/test_windows/proc_creation_win_susp_regsvr32_no_dll.yml b/src/main/resources/rules/test_windows/proc_creation_win_susp_regsvr32_no_dll.yml new file mode 100644 index 000000000..5fd6ffc94 --- /dev/null +++ b/src/main/resources/rules/test_windows/proc_creation_win_susp_regsvr32_no_dll.yml @@ -0,0 +1,38 @@ +title: Regsvr32 Command Line Without DLL +id: 50919691-7302-437f-8e10-1fe088afa145 +status: experimental +description: Detects a regsvr.exe execution that doesn't contain a DLL in the command line +author: Florian Roth +date: 2019/07/17 +modified: 2021/10/19 +references: + - https://app.any.run/tasks/34221348-072d-4b70-93f3-aa71f6ebecad/ +tags: + - attack.defense_evasion + - attack.t1574 + - attack.execution +logsource: + category: process_creation + product: windows +detection: + selection: + Image|endswith: '\regsvr32.exe' + filter: + CommandLine|contains: + - '.dll' + - '.ocx' + - '.cpl' + - '.ax' + - '.bav' + - '.ppl' + filter_null1_for_4688: + CommandLine: null + filter_null2_for_4688: + CommandLine: '' + condition: selection and not filter and not filter_null1_for_4688 and not filter_null2_for_4688 +fields: + - CommandLine + - ParentCommandLine +falsepositives: + - Unknown +level: high diff --git a/src/main/resources/rules/test_windows/proc_creation_win_system_exe_anomaly.yml b/src/main/resources/rules/test_windows/proc_creation_win_system_exe_anomaly.yml new file mode 100644 index 000000000..c90d1f04b --- /dev/null +++ b/src/main/resources/rules/test_windows/proc_creation_win_system_exe_anomaly.yml @@ -0,0 +1,81 @@ +title: System File Execution Location Anomaly +id: e4a6b256-3e47-40fc-89d2-7a477edd6915 +status: experimental +description: Detects a Windows program executable started in a suspicious folder +references: + - https://twitter.com/GelosSnake/status/934900723426439170 +author: Florian Roth, Patrick Bareiss, Anton Kutepov, oscd.community, Nasreddine Bencherchali +date: 2017/11/27 +modified: 2022/07/03 +tags: + - attack.defense_evasion + - attack.t1036 +logsource: + category: process_creation + product: windows +detection: + selection: + Image|endswith: + - '\svchost.exe' + - '\rundll32.exe' + - '\services.exe' + - '\powershell.exe' + - '\powershell_ise.exe' + - '\regsvr32.exe' + - '\spoolsv.exe' + - '\lsass.exe' + - '\smss.exe' + - '\csrss.exe' + - '\conhost.exe' + - '\wininit.exe' + - '\lsm.exe' + - '\winlogon.exe' + - '\explorer.exe' + - '\taskhost.exe' + - '\Taskmgr.exe' + - '\sihost.exe' + - '\RuntimeBroker.exe' + - '\smartscreen.exe' + - '\dllhost.exe' + - '\audiodg.exe' + - '\wlanext.exe' + - '\dashost.exe' + - '\schtasks.exe' + - '\cscript.exe' + - '\wscript.exe' + - '\wsl.exe' + - '\bitsadmin.exe' + - '\atbroker.exe' + - '\bcdedit.exe' + - '\certutil.exe' + - '\certreq.exe' + - '\cmstp.exe' + - '\consent.exe' + - '\defrag.exe' + - '\dism.exe' + - '\dllhst3g.exe' + - '\eventvwr.exe' + - '\msiexec.exe' + - '\runonce.exe' + - '\winver.exe' + - '\logonui.exe' + - '\userinit.exe' + - '\dwm.exe' + - '\LsaIso.exe' + - '\ntoskrnl.exe' + filter: + - Image|startswith: + - 'C:\Windows\System32\' + - 'C:\Windows\SysWOW64\' + - 'C:\Windows\WinSxS\' + - 'C:\avast! sandbox' + - Image|contains: '\SystemRoot\System32\' + - Image: 'C:\Windows\explorer.exe' + condition: selection and not filter +fields: + - ComputerName + - User + - Image +falsepositives: + - Exotic software +level: high diff --git a/src/main/resources/rules/test_windows/win_sample_rule.yml b/src/main/resources/rules/test_windows/win_sample_rule.yml new file mode 100644 index 000000000..b38d74cc8 --- /dev/null +++ b/src/main/resources/rules/test_windows/win_sample_rule.yml @@ -0,0 +1,24 @@ +title: QuarksPwDump Clearing Access History +id: 06724a9a-52fc-11ed-bdc3-0242ac120002 +status: experimental +description: Detects QuarksPwDump clearing access history in hive +author: Florian Roth +date: 2017/05/15 +modified: 2019/11/13 +tags: + - attack.credential_access + - attack.t1003 # an old one + - attack.t1003.002 + - attack.defense_evasion +level: critical +logsource: + product: windows + service: system +detection: + selection: + EventID: 22 + Message|contains: 'C:\\Program Files\\nxlog\\nxlog.exe' + HostName|startswith: 'EC2AMAZ' + condition: selection +falsepositives: + - Unknown \ No newline at end of file diff --git a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginRestIT.java b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginRestApiIT.java similarity index 89% rename from src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginRestIT.java rename to src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginRestApiIT.java index 06114f3d2..69019bbc5 100644 --- a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginRestIT.java +++ b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginRestApiIT.java @@ -10,13 +10,12 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.test.rest.OpenSearchRestTestCase; import java.io.IOException; import java.util.List; import java.util.Map; -public class SecurityAnalyticsPluginRestIT extends OpenSearchRestTestCase { +public class SecurityAnalyticsPluginRestApiIT extends SecurityAnalyticsRestTestCase { @SuppressWarnings("unchecked") public void testPluginsAreInstalled() throws IOException { diff --git a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginIT.java b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginTransportIT.java similarity index 94% rename from src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginIT.java rename to src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginTransportIT.java index 70233077c..ab8b374f1 100644 --- a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginIT.java +++ b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsPluginTransportIT.java @@ -17,7 +17,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class SecurityAnalyticsPluginIT extends OpenSearchIntegTestCase { +public class SecurityAnalyticsPluginTransportIT extends OpenSearchIntegTestCase { public void testPluginsAreInstalled() { NodesInfoRequest nodesInfoRequest = new NodesInfoRequest(); diff --git a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java index 501ae5d92..a20ed73f9 100644 --- a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java +++ b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java @@ -4,9 +4,9 @@ */ package org.opensearch.securityanalytics; +import org.apache.http.HttpHost; import java.util.ArrayList; import java.util.function.BiConsumer; -import java.io.File; import java.nio.file.Path; import org.apache.http.Header; import org.apache.http.HttpEntity; @@ -15,6 +15,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.message.BasicHeader; import org.junit.Assert; +import org.junit.After; import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Request; @@ -22,11 +23,14 @@ import org.opensearch.client.Response; import org.opensearch.client.ResponseException; import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; import org.opensearch.client.WarningsHandler; import org.opensearch.cluster.ClusterModule; import org.opensearch.cluster.metadata.MappingMetadata; +import org.opensearch.common.Strings; import org.opensearch.common.UUIDs; import org.opensearch.common.collect.ImmutableOpenMap; +import org.opensearch.common.io.PathUtils; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.DeprecationHandler; import org.opensearch.common.xcontent.NamedXContentRegistry; @@ -39,6 +43,9 @@ import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.commons.alerting.model.ScheduledJob; import org.opensearch.commons.alerting.util.IndexUtilsKt; +import org.opensearch.commons.rest.SecureRestClientBuilder; +import org.opensearch.commons.ConfigConstants; +import org.opensearch.index.IndexSettings; import org.opensearch.index.mapper.MapperService; import org.opensearch.rest.RestStatus; import org.opensearch.search.SearchHit; @@ -48,11 +55,12 @@ import org.opensearch.securityanalytics.config.monitors.DetectorMonitorConfig; import org.opensearch.securityanalytics.model.Detector; import org.opensearch.securityanalytics.model.Rule; -import org.opensearch.securityanalytics.util.DetectorIndices; -import org.opensearch.securityanalytics.util.RuleTopicIndices; import org.opensearch.test.rest.OpenSearchRestTestCase; + import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -62,7 +70,6 @@ import java.util.stream.Collectors; import static org.opensearch.action.admin.indices.create.CreateIndexRequest.MAPPINGS; -import static org.opensearch.securityanalytics.util.RuleTopicIndices.ruleTopicIndexMappings; import static org.opensearch.securityanalytics.util.RuleTopicIndices.ruleTopicIndexSettings; public class SecurityAnalyticsRestTestCase extends OpenSearchRestTestCase { @@ -108,6 +115,23 @@ protected String createTestIndex(String index, String mapping, Settings settings return index; } + protected String createTestIndex(RestClient client, String index, String mapping, Settings settings) throws IOException { + Request request = new Request("PUT", "/" + index); + String entity = "{\"settings\": " + Strings.toString(settings); + if (mapping != null) { + entity = entity + ",\"mappings\" : {" + mapping + "}"; + } + + entity = entity + "}"; + if (!settings.getAsBoolean(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)) { + expectSoftDeletesWarning(request, index); + } + + request.setJsonEntity(entity); + client.performRequest(request); + return index; + } + protected Response makeRequest(RestClient client, String method, String endpoint, Map params, HttpEntity entity, Header... headers) throws IOException { Request request = new Request(method, endpoint); @@ -212,7 +236,7 @@ protected List getRandomPrePackagedRules() throws IOException { " \"query\": {\n" + " \"bool\": {\n" + " \"must\": [\n" + - " { \"match\": {\"rule.category\": \"windows\"}}\n" + + " { \"match\": {\"rule.category\": \"" + TestHelpers.randomDetectorType() + "\"}}\n" + " ]\n" + " }\n" + " }\n" + @@ -891,6 +915,177 @@ private String alertingScheduledJobMappings() { " }"; } + protected boolean isHttps() { + return Boolean.parseBoolean(System.getProperty("https", "false")); + } + + protected boolean securityEnabled() { + return Boolean.parseBoolean(System.getProperty("security", "false")); + } + + @Override + protected String getProtocol() { + if (isHttps()) { + return "https"; + } else { + return "http"; + } + } + + @Override + protected Settings restAdminSettings() { + + return Settings + .builder() + .put("http.port", 9200) + .put(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_ENABLED, isHttps()) + .put(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_PEMCERT_FILEPATH, "sample.pem") + .put(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, "test-kirk.jks") + .put(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_PASSWORD, "changeit") + .put(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_KEYPASSWORD, "changeit") + .build(); + } + + + + @Override + protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOException + { + if (securityEnabled()) { + String keystore = settings.get(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_FILEPATH); + if (keystore != null) { + // create adminDN (super-admin) client + //log.info("keystore not null"); + URI uri = null; + try { + uri = SecurityAnalyticsRestTestCase.class.getClassLoader().getResource("sample.pem").toURI(); + } + catch(URISyntaxException e) { + return null; + } + Path configPath = PathUtils.get(uri).getParent().toAbsolutePath(); + return new SecureRestClientBuilder(settings, configPath).setSocketTimeout(60000).build(); + } + else { + // create client with passed user + String userName = System.getProperty("user"); + String password = System.getProperty("password"); + return new SecureRestClientBuilder(hosts, isHttps(), userName, password).setSocketTimeout(60000).build(); + } + } + else { + RestClientBuilder builder = RestClient.builder(hosts); + configureClient(builder, settings); + builder.setStrictDeprecationMode(true); + return builder.build(); + } + + } + + + protected void createCustomRole(String name, String clusterPermissions) throws IOException { + Request request = new Request("PUT", String.format(Locale.getDefault(), "/_plugins/_security/api/roles/%s", name)); + String entity = "{\n" + + "\"cluster_permissions\": [\n" + + "\"" + clusterPermissions + "\"\n" + + "]\n" + + "}"; + request.setJsonEntity(entity); + client().performRequest(request); + } + + protected void createUser(String name, String passwd, String[] backendRoles) throws IOException { + Request request = new Request("PUT", String.format(Locale.getDefault(), "/_plugins/_security/api/internalusers/%s", name)); + String broles = String.join(",", backendRoles); + //String roles = String.join(",", customRoles); + String entity = " {\n" + + "\"password\": \"" + passwd + "\",\n" + + "\"backend_roles\": [\"" + broles + "\"],\n" + + "\"attributes\": {\n" + + "}} "; + request.setJsonEntity(entity); + client().performRequest(request); + } + + protected void createUserRolesMapping(String role, String[] users) throws IOException { + Request request = new Request("PUT", String.format(Locale.getDefault(), "/_plugins/_security/api/rolesmapping/%s", role)); + String usersArr= String.join(",", users); + String entity = "{\n" + + " \"backend_roles\" : [ ],\n" + + " \"hosts\" : [ ],\n" + + "\"users\": [\"" + usersArr + "\"]\n" + + "}"; + request.setJsonEntity(entity); + client().performRequest(request); + } + + protected void enableOrDisableFilterBy(String trueOrFalse) throws IOException { + Request request = new Request("PUT", "_cluster/settings"); + String entity = "{\"persistent\":{\"plugins.security_analytics.filter_by_backend_roles\" : " + trueOrFalse + "}}"; + request.setJsonEntity(entity); + client().performRequest(request); + } + + protected void createUserWithDataAndCustomRole(String userName, String userPasswd, String roleName, String[] backendRoles, String clusterPermissions ) throws IOException { + String[] users = {userName}; + createUser(userName, userPasswd, backendRoles); + createCustomRole(roleName, clusterPermissions); + createUserRolesMapping(roleName, users); + } + + protected void createUserWithData(String userName, String userPasswd, String roleName, String[] backendRoles ) throws IOException { + String[] users = {userName}; + createUser(userName, userPasswd, backendRoles); + createUserRolesMapping(roleName, users); + } + + + + protected void deleteUser(String name) throws IOException { + Request request = new Request("DELETE", String.format(Locale.getDefault(), "/_plugins/_security/api/internalusers/%s", name)); + client().performRequest(request); + } + + @Override + protected boolean preserveIndicesUponCompletion() { + return true; + } + + boolean preserveODFEIndicesAfterTest() { + return false; + } + + @After + protected void wipeAllODFEIndices() throws IOException { + if (preserveODFEIndicesAfterTest()) return; + + Response response = client().performRequest(new Request("GET", "/_cat/indices?format=json&expand_wildcards=all")); + + XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue()); + XContentParser parser = xContentType.xContent().createParser( + NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + response.getEntity().getContent() + ); + + + for (Object index : parser.list()) { + Map jsonObject = (Map) index; + + String indexName = jsonObject.get("index").toString(); + // .opendistro_security isn't allowed to delete from cluster + if (!".opendistro_security".equals(indexName)) { + Request request = new Request("DELETE", String.format(Locale.getDefault(), "/%s", indexName)); + // TODO: remove PERMISSIVE option after moving system index access to REST API call + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.setWarningsHandler(WarningsHandler.PERMISSIVE); + request.setOptions(options.build()); + adminClient().performRequest(request); + } + } + } + + + public List getAlertIndices(String detectorType) throws IOException { Response response = client().performRequest(new Request("GET", "/_cat/indices/" + DetectorMonitorConfig.getAllAlertsIndicesPattern(detectorType) + "?format=json")); XContentParser xcp = createParser(XContentType.JSON.xContent(), response.getEntity().getContent()); diff --git a/src/test/java/org/opensearch/securityanalytics/TestHelpers.java b/src/test/java/org/opensearch/securityanalytics/TestHelpers.java index d94560d17..22e4f864e 100644 --- a/src/test/java/org/opensearch/securityanalytics/TestHelpers.java +++ b/src/test/java/org/opensearch/securityanalytics/TestHelpers.java @@ -118,7 +118,7 @@ public static Detector randomDetector(String name, if (triggers.size() == 0) { triggers = new ArrayList<>(); - DetectorTrigger trigger = new DetectorTrigger(null, "windows-trigger", "1", List.of("windows"), List.of("QuarksPwDump Clearing Access History"), List.of("high"), List.of("T0008"), List.of()); + DetectorTrigger trigger = new DetectorTrigger(null, "windows-trigger", "1", List.of(randomDetectorType()), List.of("QuarksPwDump Clearing Access History"), List.of("high"), List.of("T0008"), List.of()); triggers.add(trigger); } return new Detector(null, null, name, enabled, schedule, lastUpdateTime, enabledTime, detectorType, user, inputs, triggers, Collections.singletonList(""), "", "", "", "", "", ""); @@ -243,7 +243,7 @@ public static User randomUserEmpty() { } public static String randomDetectorType() { - return "windows"; + return "test_windows"; } public static DetectorInput randomDetectorInput() { diff --git a/src/test/java/org/opensearch/securityanalytics/alerts/AlertsIT.java b/src/test/java/org/opensearch/securityanalytics/alerts/AlertsIT.java index 75906c073..eb9ce5778 100644 --- a/src/test/java/org/opensearch/securityanalytics/alerts/AlertsIT.java +++ b/src/test/java/org/opensearch/securityanalytics/alerts/AlertsIT.java @@ -33,6 +33,7 @@ import static org.opensearch.securityanalytics.TestHelpers.netFlowMappings; import static org.opensearch.securityanalytics.TestHelpers.randomAction; +import static org.opensearch.securityanalytics.TestHelpers.randomDetectorType; import static org.opensearch.securityanalytics.TestHelpers.randomDetectorWithInputsAndTriggers; import static org.opensearch.securityanalytics.TestHelpers.randomDetectorWithTriggers; import static org.opensearch.securityanalytics.TestHelpers.randomDoc; @@ -51,7 +52,7 @@ public void testGetAlerts_success() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -64,7 +65,7 @@ public void testGetAlerts_success() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -134,7 +135,7 @@ public void testGetAlerts_success() throws IOException { hits = new ArrayList<>(); while (hits.size() == 0) { - hits = executeSearch(DetectorMonitorConfig.getAlertsIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); } // Call GetAlerts API @@ -172,7 +173,7 @@ public void testAckAlerts_WithInvalidDetectorAlertsCombination() throws IOExcept // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -233,7 +234,7 @@ public void testAckAlerts_WithInvalidDetectorAlertsCombination() throws IOExcept hits = new ArrayList<>(); while (hits.size() == 0) { - hits = executeSearch(DetectorMonitorConfig.getAlertsIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); } // Call GetAlerts API @@ -269,7 +270,7 @@ public void testGetAlerts_byDetectorType_success() throws IOException, Interrupt // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -277,7 +278,7 @@ public void testGetAlerts_byDetectorType_success() throws IOException, Interrupt Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -316,12 +317,12 @@ public void testGetAlerts_byDetectorType_success() throws IOException, Interrupt hits = new ArrayList<>(); while (hits.size() == 0) { - hits = executeSearch(DetectorMonitorConfig.getAlertsIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); } // Call GetAlerts API Map params = new HashMap<>(); - params.put("detectorType", Detector.DetectorType.WINDOWS.getDetectorType()); + params.put("detectorType", randomDetectorType()); Response getAlertsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); Map getAlertsBody = asMap(getAlertsResponse); // TODO enable asserts here when able @@ -336,7 +337,7 @@ public void testGetAlerts_byDetectorType_multipleDetectors_success() throws IOEx // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index1 + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -356,7 +357,7 @@ public void testGetAlerts_byDetectorType_multipleDetectors_success() throws IOEx Response 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("windows"), List.of(), List.of(), List.of(), List.of()))); + Detector detector1 = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of(randomDetectorType()), List.of(), List.of(), List.of(), List.of()))); Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector1)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -427,7 +428,7 @@ public void testGetAlerts_byDetectorType_multipleDetectors_success() throws IOEx hits = new ArrayList<>(); while (hits.size() == 0) { - hits = executeSearch(DetectorMonitorConfig.getAlertsIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); } hits = new ArrayList<>(); while (hits.size() == 0) { @@ -438,7 +439,7 @@ public void testGetAlerts_byDetectorType_multipleDetectors_success() throws IOEx // Call GetAlerts API for WINDOWS detector Map params = new HashMap<>(); - params.put("detectorType", Detector.DetectorType.WINDOWS.getDetectorType()); + params.put("detectorType", randomDetectorType()); Response getAlertsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); Map getAlertsBody = asMap(getAlertsResponse); Assert.assertEquals(1, getAlertsBody.get("total_alerts")); @@ -463,7 +464,7 @@ public void testAlertHistoryRollover_maxAge() throws IOException, InterruptedExc // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -471,7 +472,7 @@ public void testAlertHistoryRollover_maxAge() throws IOException, InterruptedExc Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -510,7 +511,7 @@ public void testAlertHistoryRollover_maxAge() throws IOException, InterruptedExc hits = new ArrayList<>(); while (hits.size() == 0) { - hits = executeSearch(DetectorMonitorConfig.getAlertsIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); } List alertIndices = getAlertIndices(detector.getDetectorType()); @@ -533,7 +534,7 @@ public void testAlertHistoryRollover_maxDocs() throws IOException, InterruptedEx // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -541,7 +542,7 @@ public void testAlertHistoryRollover_maxDocs() throws IOException, InterruptedEx Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -580,7 +581,7 @@ public void testAlertHistoryRollover_maxDocs() throws IOException, InterruptedEx hits = new ArrayList<>(); while (hits.size() == 0) { - hits = executeSearch(DetectorMonitorConfig.getAlertsIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getAlertsIndex(randomDetectorType()), request); } Map params = new HashMap<>(); 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 diff --git a/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java b/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java index 6937e0931..dde7251ae 100644 --- a/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java +++ b/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java @@ -25,6 +25,7 @@ import org.opensearch.securityanalytics.model.DetectorTrigger; import static org.opensearch.securityanalytics.TestHelpers.netFlowMappings; +import static org.opensearch.securityanalytics.TestHelpers.randomDetectorType; import static org.opensearch.securityanalytics.TestHelpers.randomDetectorWithTriggers; import static org.opensearch.securityanalytics.TestHelpers.randomDoc; import static org.opensearch.securityanalytics.TestHelpers.randomIndex; @@ -44,7 +45,7 @@ public void testGetFindings_byDetectorId_success() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -52,7 +53,7 @@ public void testGetFindings_byDetectorId_success() throws IOException { Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -96,7 +97,7 @@ public void testGetFindings_byDetectorType_oneDetector_success() throws IOExcept // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -104,7 +105,7 @@ public void testGetFindings_byDetectorType_oneDetector_success() throws IOExcept Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -148,7 +149,7 @@ public void testGetFindings_byDetectorType_success() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index1 + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -168,7 +169,7 @@ public void testGetFindings_byDetectorType_success() throws IOException { Response 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("windows"), List.of(), List.of(), List.of(), List.of()))); + Detector detector1 = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of(randomDetectorType()), List.of(), List.of(), List.of(), List.of()))); Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector1)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -258,7 +259,7 @@ public void testGetFindings_rolloverByMaxAge_success() throws IOException, Inter // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -266,7 +267,7 @@ public void testGetFindings_rolloverByMaxAge_success() throws IOException, Inter Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); @@ -321,7 +322,7 @@ public void testGetFindings_rolloverByMaxDoc_success() throws IOException, Inter // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -329,7 +330,7 @@ public void testGetFindings_rolloverByMaxDoc_success() throws IOException, Inter Response response = client().performRequest(createMappingRequest); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - Detector detector = randomDetectorWithTriggers(getRandomPrePackagedRules(), List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("windows"), List.of(), List.of(), List.of(), List.of()))); + 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(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); diff --git a/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java new file mode 100644 index 000000000..6af7ecc0b --- /dev/null +++ b/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java @@ -0,0 +1,287 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.securityanalytics.findings; + +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +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.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.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.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.opensearch.securityanalytics.TestHelpers.netFlowMappings; +import static org.opensearch.securityanalytics.TestHelpers.randomDetectorType; +import static org.opensearch.securityanalytics.TestHelpers.randomDetectorWithTriggers; +import static org.opensearch.securityanalytics.TestHelpers.randomDoc; +import static org.opensearch.securityanalytics.TestHelpers.randomIndex; +import static org.opensearch.securityanalytics.TestHelpers.windowsIndexMapping; + +public class SecureFindingRestApiIT 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 = "userFinding"; + 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 testGetFindings_byDetectorId_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 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()); + + 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); + + // try to do get finding as a user with read access + String userRead = "userReadFinding"; + 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 GetFindings API + Map params = new HashMap<>(); + params.put("detector_id", createdId); + Response getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + Assert.assertEquals(1, getFindingsBody.get("total_findings")); + + // 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 { + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + } catch (ResponseException e) + { + assertEquals("Get finding 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(); + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + getFindingsBody = entityAsMap(getFindingsResponse); + Assert.assertEquals(1, getFindingsBody.get("total_findings")); + + userReadOnlyClient.close(); + deleteUser(userRead); + + } + + public void testGetFindings_byDetectorType_success() throws IOException { + String index1 = 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\":\"" + index1 + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + // index 2 + String index2 = createTestIndex("netflow_test", netFlowMappings()); + + // Execute CreateMappingsAction to add alias mapping for index + createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index2 + "\"," + + " \"rule_topic\":\"netflow\", " + + " \"partial\":true" + + "}" + ); + + Response response = userClient.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()))); + Response createResponse = makeRequest(userClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector1)); + 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 monitorId1 = ((List) ((Map) hit.getSourceAsMap().get("detector")).get("monitor_id")).get(0); + // Detector 2 - NETWORK + DetectorInput inputNetflow = new DetectorInput("windows detector for security analytics", List.of("netflow_test"), Collections.emptyList(), + getPrePackagedRules("network").stream().map(DetectorRule::new).collect(Collectors.toList())); + Detector detector2 = randomDetectorWithTriggers( + getPrePackagedRules("network"), + List.of(new DetectorTrigger(null, "test-trigger", "1", List.of("network"), List.of(), List.of(), List.of(), List.of())), + Detector.DetectorType.NETWORK, + inputNetflow + ); + + createResponse = makeRequest(userClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector2)); + Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); + + responseBody = asMap(createResponse); + + createdId = responseBody.get("_id").toString(); + + request = "{\n" + + " \"query\" : {\n" + + " \"match\":{\n" + + " \"_id\": \"" + createdId + "\"\n" + + " }\n" + + " }\n" + + "}"; + hits = executeSearch(Detector.DETECTORS_INDEX, request); + hit = hits.get(0); + String monitorId2 = ((List) ((Map) hit.getSourceAsMap().get("detector")).get("monitor_id")).get(0); + + indexDoc(index1, "1", randomDoc()); + indexDoc(index2, "1", randomDoc()); + // execute monitor 1 + Response executeResponse = executeAlertingMonitor(monitorId1, Collections.emptyMap()); + Map executeResults = entityAsMap(executeResponse); + + int noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); + Assert.assertEquals(3, noOfSigmaRuleMatches); + + // execute monitor 2 + executeResponse = executeAlertingMonitor(monitorId2, Collections.emptyMap()); + executeResults = entityAsMap(executeResponse); + + noOfSigmaRuleMatches = ((List>) ((Map) executeResults.get("input_results")).get("results")).get(0).size(); + Assert.assertEquals(1, noOfSigmaRuleMatches); + + client().performRequest(new Request("POST", "_refresh")); + + + // try to do get finding as a user with read access + String userRead = "userReadFinding"; + 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 GetFindings API for first detector + Map params = new HashMap<>(); + params.put("detectorType", detector1.getDetectorType().toUpperCase()); + Response getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + Map getFindingsBody = entityAsMap(getFindingsResponse); + Assert.assertEquals(1, getFindingsBody.get("total_findings")); + // Call GetFindings API for second detector + params.clear(); + params.put("detectorType", detector2.getDetectorType().toUpperCase()); + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + getFindingsBody = entityAsMap(getFindingsResponse); + Assert.assertEquals(1, getFindingsBody.get("total_findings")); + + // 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 { + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + } catch (ResponseException e) + { + assertEquals("Get finding 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(); + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + getFindingsBody = entityAsMap(getFindingsResponse); + Assert.assertEquals(1, getFindingsBody.get("total_findings")); + + userReadOnlyClient.close(); + deleteUser(userRead); + } +} diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperIT.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java similarity index 98% rename from src/test/java/org/opensearch/securityanalytics/mapper/MapperIT.java rename to src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java index 213d0a923..32fd81db8 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperIT.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java @@ -10,21 +10,19 @@ import org.opensearch.client.Request; import org.opensearch.client.Response; import org.opensearch.client.ResponseException; -import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentParser; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.securityanalytics.SecurityAnalyticsClientUtils; import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; -import org.opensearch.securityanalytics.action.GetMappingsViewResponse; -import org.opensearch.test.rest.OpenSearchRestTestCase; +import org.opensearch.securityanalytics.SecurityAnalyticsRestTestCase; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; -public class MapperIT extends OpenSearchRestTestCase { +public class MapperRestApiIT extends SecurityAnalyticsRestTestCase { public void testCreateMappingSuccess() throws IOException { diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java index 372a75a8a..0e596e443 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java @@ -31,6 +31,7 @@ import java.util.stream.Collectors; 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; @@ -48,7 +49,7 @@ public void testCreatingADetector() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -101,7 +102,7 @@ public void testGettingADetector() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -132,7 +133,7 @@ public void testSearchingDetectors() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -168,7 +169,7 @@ public void testCreatingADetectorWithCustomRules() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -178,7 +179,7 @@ public void testCreatingADetectorWithCustomRules() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -232,7 +233,7 @@ public void testUpdateADetector() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -255,12 +256,12 @@ public void testUpdateADetector() throws IOException { " }\n" + " }\n" + "}"; - SearchResponse response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex("windows"), request, true); - Assert.assertEquals(1579, response.getHits().getTotalHits().value); + SearchResponse response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request, true); + Assert.assertEquals(5, response.getHits().getTotalHits().value); String rule = randomRule(); - createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + createResponse = makeRequest(client(), "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)); @@ -281,8 +282,8 @@ public void testUpdateADetector() throws IOException { " }\n" + " }\n" + "}"; - response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex("windows"), request, true); - Assert.assertEquals(1580, response.getHits().getTotalHits().value); + response = executeSearchAndGetResponse(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request, true); + Assert.assertEquals(6, response.getHits().getTotalHits().value); } @SuppressWarnings("unchecked") @@ -294,7 +295,7 @@ public void testDeletingADetector() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -328,7 +329,6 @@ public void testDeletingADetector() throws IOException { Assert.assertFalse(alertingMonitorExists(monitorId)); - // todo: change to assertFalse when alerting bug is fixed. https://github.com/opensearch-project/alerting/issues/581 Assert.assertFalse(doesIndexExist(String.format(Locale.getDefault(), ".opensearch-sap-%s-detectors-queries", "windows"))); hits = executeSearch(Detector.DETECTORS_INDEX, request); diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java index cd130c94e..4424fff14 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java @@ -15,7 +15,6 @@ import org.opensearch.search.SearchHit; import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; import org.opensearch.securityanalytics.SecurityAnalyticsRestTestCase; -import org.opensearch.securityanalytics.action.ValidateRulesRequest; import org.opensearch.securityanalytics.config.monitors.DetectorMonitorConfig; import org.opensearch.securityanalytics.model.Detector; import org.opensearch.securityanalytics.model.DetectorInput; @@ -29,6 +28,7 @@ import java.util.Map; import java.util.stream.Collectors; +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.randomEditedRule; @@ -42,7 +42,7 @@ public class RuleRestApiIT extends SecurityAnalyticsRestTestCase { public void testCreatingARule() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -62,7 +62,7 @@ public void testCreatingARule() throws IOException { " \"query\": {\n" + " \"bool\": {\n" + " \"must\": [\n" + - " { \"match\": {\"rule.category\": \"windows\"}}\n" + + " { \"match\": {\"rule.category\": \"" + randomDetectorType() + "\"}}\n" + " ]\n" + " }\n" + " }\n" + @@ -95,7 +95,7 @@ public void testCreatingARuleWithWrongSyntax() throws IOException { String rule = randomRuleWithErrors(); try { - makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", randomDetectorType()), new StringEntity(rule), new BasicHeader("Content-Type", "application/json")); } catch (ResponseException ex) { Map responseBody = asMap(ex.getResponse()); @@ -113,7 +113,7 @@ public void testSearchingPrepackagedRules() throws IOException { " \"query\": {\n" + " \"bool\": {\n" + " \"must\": [\n" + - " { \"match\": {\"rule.category\": \"windows\"}}\n" + + " { \"match\": {\"rule.category\": \"" + randomDetectorType() + "\"}}\n" + " ]\n" + " }\n" + " }\n" + @@ -126,7 +126,7 @@ public void testSearchingPrepackagedRules() throws IOException { Assert.assertEquals("Searching rules failed", RestStatus.OK, restStatus(searchResponse)); Map responseBody = asMap(searchResponse); - Assert.assertEquals(1579, ((Map) ((Map) responseBody.get("hits")).get("total")).get("value")); + Assert.assertEquals(5, ((Map) ((Map) responseBody.get("hits")).get("total")).get("value")); } @SuppressWarnings("unchecked") @@ -210,7 +210,7 @@ public void testSearchingPrepackagedRulesByAuthor() throws IOException { public void testSearchingCustomRules() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -221,7 +221,7 @@ public void testSearchingCustomRules() throws IOException { " \"query\": {\n" + " \"bool\": {\n" + " \"must\": [\n" + - " { \"match\": {\"rule.category\": \"windows\"}}\n" + + " { \"match\": {\"rule.category\": \"" + randomDetectorType() + "\"}}\n" + " ]\n" + " }\n" + " }\n" + @@ -245,7 +245,7 @@ public void testUpdatingUnusedRule() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -255,14 +255,14 @@ public void testUpdatingUnusedRule() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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(); - Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Map.of("category", "windows"), + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Map.of("category", randomDetectorType()), new StringEntity(randomEditedRule()), new BasicHeader("Content-Type", "application/json")); Assert.assertEquals("Update rule failed", RestStatus.OK, restStatus(updateResponse)); } @@ -275,7 +275,7 @@ public void testUpdatingUnusedRuleAfterDetectorIndexCreated() throws IOException // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -285,7 +285,7 @@ public void testUpdatingUnusedRuleAfterDetectorIndexCreated() throws IOException String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -300,7 +300,7 @@ public void testUpdatingUnusedRuleAfterDetectorIndexCreated() throws IOException createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); Assert.assertEquals("Create detector failed", RestStatus.CREATED, restStatus(createResponse)); - Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Map.of("category", "windows"), + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Map.of("category", randomDetectorType()), new StringEntity(randomEditedRule()), new BasicHeader("Content-Type", "application/json")); Assert.assertEquals("Update rule failed", RestStatus.OK, restStatus(updateResponse)); } @@ -314,7 +314,7 @@ public void testUpdatingUsedRule() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -324,7 +324,7 @@ public void testUpdatingUsedRule() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -365,14 +365,14 @@ public void testUpdatingUsedRule() throws IOException { Assert.assertEquals(6, noOfSigmaRuleMatches); try { - makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Collections.singletonMap("category", "windows"), + makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Collections.singletonMap("category", randomDetectorType()), new StringEntity(randomEditedRule()), new BasicHeader("Content-Type", "application/json")); } catch (ResponseException ex) { Assert.assertTrue(new String(ex.getResponse().getEntity().getContent().readAllBytes()) .contains(String.format(Locale.getDefault(), "Rule with id %s is actively used by detectors. Update can be forced by setting forced flag to true", createdId))); } - Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Map.of("category", "windows", "forced", "true"), + Response updateResponse = makeRequest(client(), "PUT", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Map.of("category", randomDetectorType(), "forced", "true"), new StringEntity(randomEditedRule()), new BasicHeader("Content-Type", "application/json")); Assert.assertEquals("Update rule failed", RestStatus.OK, restStatus(updateResponse)); @@ -405,7 +405,7 @@ public void testDeletingUnusedRule() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -415,7 +415,7 @@ public void testDeletingUnusedRule() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -434,7 +434,7 @@ public void testDeletingUnusedRuleAfterDetectorIndexCreated() throws IOException // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -444,7 +444,7 @@ public void testDeletingUnusedRuleAfterDetectorIndexCreated() throws IOException String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -470,7 +470,7 @@ public void testDeletingUsedRule() throws IOException { // both req params and req body are supported createMappingRequest.setJsonEntity( "{ \"index_name\":\"" + index + "\"," + - " \"rule_topic\":\"windows\", " + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + " \"partial\":true" + "}" ); @@ -480,7 +480,7 @@ public void testDeletingUsedRule() throws IOException { String rule = randomRule(); - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "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)); @@ -512,7 +512,7 @@ public void testDeletingUsedRule() throws IOException { " }\n" + " }\n" + "}"; - List hits = executeSearch(DetectorMonitorConfig.getRuleIndex("windows"), request); + List hits = executeSearch(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request); Assert.assertEquals(2, hits.size()); Response deleteResponse = makeRequest(client(), "DELETE", SecurityAnalyticsPlugin.RULE_BASE_URI + "/" + createdId, Collections.singletonMap("forced", "true"), null); @@ -525,7 +525,7 @@ public void testDeletingUsedRule() throws IOException { " }\n" + " }\n" + "}"; - hits = executeSearch(DetectorMonitorConfig.getRuleIndex("windows"), request); + hits = executeSearch(DetectorMonitorConfig.getRuleIndex(randomDetectorType()), request); Assert.assertEquals(0, hits.size()); index = Rule.CUSTOM_RULES_INDEX; @@ -603,7 +603,7 @@ public void testCustomRuleValidation() throws IOException { "level: high"; // Create rule #1 - Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", randomDetectorType()), new StringEntity(rule1), new BasicHeader("Content-Type", "application/json")); Assert.assertEquals("Create rule failed", RestStatus.CREATED, restStatus(createResponse)); @@ -615,7 +615,7 @@ public void testCustomRuleValidation() throws IOException { Assert.assertTrue("incorrect version", createdVersion > 0); Assert.assertEquals("Incorrect Location header", String.format(Locale.getDefault(), "%s/%s", SecurityAnalyticsPlugin.RULE_BASE_URI, rule1createdId), createResponse.getHeader("Location")); // Create rule #2 - createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", randomDetectorType()), new StringEntity(rule2), new BasicHeader("Content-Type", "application/json")); Assert.assertEquals("Create rule failed", RestStatus.CREATED, restStatus(createResponse)); diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java new file mode 100644 index 000000000..e4f760f93 --- /dev/null +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java @@ -0,0 +1,173 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.resthandler; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; +import org.junit.After; +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.common.settings.Settings; +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.junit.Assert; +import org.opensearch.securityanalytics.model.Detector; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.io.IOException; + +import static org.opensearch.securityanalytics.TestHelpers.*; +import static org.opensearch.securityanalytics.TestHelpers.randomDoc; + +public class SecureDetectorRestApiIT 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 CUSTOM_HR_ROLE = "HR"; + + static String TEST_IT_BACKEND_ROLE = "IT"; + + + + static Map roleToPermissionsMap = Map.ofEntries( + Map.entry(SECURITY_ANALYTICS_FULL_ACCESS_ROLE, "cluster:admin/opendistro/securityanalytics/detector/*"), + Map.entry(SECURITY_ANALYTICS_READ_ACCESS_ROLE, "cluster:admin/opendistro/securityanalytics/detector/read") + ); + + private RestClient userClient; + private final String user = "userDetector"; + + + @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 testCreateDetectorWithFullAccess() throws IOException { + String[] users = {user}; + //createUserRolesMapping("alerting_full_access", users); + String index = createTestIndex(client(), randomIndex(), windowsIndexMapping(), Settings.EMPTY); + + // 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 = randomDetector(getRandomPrePackagedRules()); + + 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(); + int createdVersion = Integer.parseInt(responseBody.get("_version").toString()); + Assert.assertNotEquals("response is missing Id", Detector.NO_ID, createdId); + Assert.assertTrue("incorrect version", createdVersion > 0); + Assert.assertEquals("Incorrect Location header", String.format(Locale.getDefault(), "%s/%s", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, createdId), createResponse.getHeader("Location")); + Assert.assertFalse(((Map) responseBody.get("detector")).containsKey("rule_topic_index")); + Assert.assertFalse(((Map) responseBody.get("detector")).containsKey("findings_index")); + Assert.assertFalse(((Map) responseBody.get("detector")).containsKey("alert_index")); + + 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(5, noOfSigmaRuleMatches); + + // try to do get detector as a user with read access + String userRead = "userRead"; + 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(); + Response getResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + createdId, Collections.emptyMap(), null); + Map getResponseBody = asMap(getResponse); + Assert.assertEquals(createdId, getResponseBody.get("_id")); + + + // Enable backend filtering and try to read detector as a user with no backend roles matching the user who created the detector + enableOrDisableFilterBy("true"); + try { + getResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + createdId, Collections.emptyMap(), null); + } catch (ResponseException e) + { + assertEquals("Get detector 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(); + getResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + createdId, Collections.emptyMap(), null); + getResponseBody = asMap(getResponse); + Assert.assertEquals(createdId, getResponseBody.get("_id")); + + //Search on id should give one result + + String queryJson = "{ \"query\": { \"match\": { \"_id\" : \"" + createdId + "\"} } }"; +// String queryJson = "{ \"query\": { \"match_all\": { } } }"; +// + HttpEntity requestEntity = new NStringEntity(queryJson, ContentType.APPLICATION_JSON); + Response searchResponse = makeRequest(userReadOnlyClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + "_search", Collections.emptyMap(), requestEntity); + Map searchResponseBody = asMap(searchResponse); + Assert.assertNotNull("response is not null", searchResponseBody); + Map searchResponseHits = (Map) searchResponseBody.get("hits"); + Map searchResponseTotal = (Map) searchResponseHits.get("total"); + Assert.assertEquals(1, searchResponseTotal.get("value")); + + userReadOnlyClient.close(); + deleteUser(userRead); + } +} \ No newline at end of file diff --git a/src/test/resources/sample.pem b/src/test/resources/sample.pem new file mode 100644 index 000000000..7ba92534e --- /dev/null +++ b/src/test/resources/sample.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEyTCCA7GgAwIBAgIGAWLrc1O2MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy +MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBeMRIwEAYKCZImiZPyLGQBGRYCZGUxDTAL +BgNVBAcMBHRlc3QxDTALBgNVBAoMBG5vZGUxDTALBgNVBAsMBG5vZGUxGzAZBgNV +BAMMEm5vZGUtMC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJa+f476vLB+AwK53biYByUwN+40D8jMIovGXm6wgT8+9Sbs899dDXgt +9CE1Beo65oP1+JUz4c7UHMrCY3ePiDt4cidHVzEQ2g0YoVrQWv0RedS/yx/DKhs8 +Pw1O715oftP53p/2ijD5DifFv1eKfkhFH+lwny/vMSNxellpl6NxJTiJVnQ9HYOL +gf2t971ITJHnAuuxUF48HcuNovW4rhtkXef8kaAN7cE3LU+A9T474ULNCKkEFPIl +ZAKN3iJNFdVsxrTU+CUBHzk73Do1cCkEvJZ0ZFjp0Z3y8wLY/gqWGfGVyA9l2CUq +eIZNf55PNPtGzOrvvONiui48vBKH1LsCAwEAAaOCAVkwggFVMIG8BgNVHSMEgbQw +gbGAFJI1DOAPHitF9k0583tfouYSl0BzoYGVpIGSMIGPMRMwEQYKCZImiZPyLGQB +GRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBs +ZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMSEw +HwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCAQEwHQYDVR0OBBYEFKyv +78ZmFjVKM9g7pMConYH7FVBHMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgXg +MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA1BgNVHREELjAsiAUq +AwQFBYISbm9kZS0wLmV4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZI +hvcNAQELBQADggEBAIOKuyXsFfGv1hI/Lkpd/73QNqjqJdxQclX57GOMWNbOM5H0 +5/9AOIZ5JQsWULNKN77aHjLRr4owq2jGbpc/Z6kAd+eiatkcpnbtbGrhKpOtoEZy +8KuslwkeixpzLDNISSbkeLpXz4xJI1ETMN/VG8ZZP1bjzlHziHHDu0JNZ6TnNzKr +XzCGMCohFfem8vnKNnKUneMQMvXd3rzUaAgvtf7Hc2LTBlf4fZzZF1EkwdSXhaMA +1lkfHiqOBxtgeDLxCHESZ2fqgVqsWX+t3qHQfivcPW6txtDyrFPRdJOGhiMGzT/t +e/9kkAtQRgpTb3skYdIOOUOV0WGQ60kJlFhAzIs= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-kirk.jks b/src/test/resources/test-kirk.jks new file mode 100644 index 000000000..174dbda65 Binary files /dev/null and b/src/test/resources/test-kirk.jks differ