diff --git a/plugins/command-manager/build.gradle b/plugins/command-manager/build.gradle index 7597000..eb235ae 100644 --- a/plugins/command-manager/build.gradle +++ b/plugins/command-manager/build.gradle @@ -1,3 +1,4 @@ + import org.opensearch.gradle.test.RestIntegTestTask buildscript { @@ -75,7 +76,9 @@ def versions = [ httpcore5: "5.3.1", slf4j: "2.0.16", log4j: "2.23.1", - conscrypt: "2.5.2" + conscrypt: "2.5.2", + mockito: "5.12.0", + junit:"4.13.2" ] dependencies { @@ -85,6 +88,9 @@ dependencies { api "org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}" api "org.slf4j:slf4j-api:${versions.slf4j}" api "org.conscrypt:conscrypt-openjdk-uber:${versions.conscrypt}" + + testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "junit:junit:${versions.junit}" } // This requires an additional Jar not published as part of build-tools @@ -108,6 +114,7 @@ repositories { test { include '**/*Tests.class' + jvmArgs "-Djava.security.policy=./plugins/command-manager/src/main/plugin-metadata/plugin-security.policy/plugin-security.policy" } task integTest(type: RestIntegTestTask) { @@ -117,6 +124,7 @@ task integTest(type: RestIntegTestTask) { } tasks.named("check").configure { dependsOn(integTest) } + integTest { // The --debug-jvm command-line option makes the cluster debuggable; this makes the tests debuggable if (System.getProperty("test.debug") != null) { @@ -126,9 +134,14 @@ integTest { testClusters.integTest { testDistribution = "INTEG_TEST" - + //testDistribution = "ARCHIVE" // This installs our plugin into the testClusters plugin(project.tasks.bundlePlugin.archiveFile) + + // add customized keystore + keystore 'm_api.auth.username', 'admin' + keystore 'm_api.auth.password', 'test' + keystore 'm_api.uri', 'https://httpbin.org/post' } run { diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/CommandManagerPlugin.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/CommandManagerPlugin.java index fe11b1d..f044e9a 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/CommandManagerPlugin.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/CommandManagerPlugin.java @@ -12,16 +12,14 @@ import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNodes; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.IndexScopedSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.settings.SettingsFilter; +import org.opensearch.common.settings.*; import org.opensearch.core.common.io.stream.NamedWriteableRegistry; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.env.Environment; import org.opensearch.env.NodeEnvironment; import org.opensearch.plugins.ActionPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.ReloadablePlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; @@ -30,6 +28,7 @@ import org.opensearch.watcher.ResourceWatcherService; import java.io.IOException; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -37,21 +36,23 @@ import com.wazuh.commandmanager.index.CommandIndex; import com.wazuh.commandmanager.rest.RestPostCommandAction; +import com.wazuh.commandmanager.settings.CommandManagerSettings; import com.wazuh.commandmanager.utils.httpclient.HttpRestClient; -import com.wazuh.commandmanager.utils.httpclient.HttpRestClientDemo; /** * The Command Manager plugin exposes an HTTP API with a single endpoint to receive raw commands * from the Wazuh Server. These commands are processed, indexed and sent back to the Server for its * delivery to, in most cases, the Agents. */ -public class CommandManagerPlugin extends Plugin implements ActionPlugin { +public class CommandManagerPlugin extends Plugin implements ActionPlugin, ReloadablePlugin { public static final String COMMAND_MANAGER_BASE_URI = "/_plugins/_command_manager"; public static final String COMMANDS_URI = COMMAND_MANAGER_BASE_URI + "/commands"; public static final String COMMAND_MANAGER_INDEX_NAME = ".commands"; public static final String COMMAND_MANAGER_INDEX_TEMPLATE_NAME = "index-template-commands"; private CommandIndex commandIndex; + private CommandManagerSettings commandManagerSettings; + // private static final Logger log = LogManager.getLogger(CommandManagerSettings.class); @Override public Collection createComponents( @@ -68,10 +69,8 @@ public Collection createComponents( Supplier repositoriesServiceSupplier) { this.commandIndex = new CommandIndex(client, clusterService, threadPool); - // HttpRestClient stuff - String uri = "https://httpbin.org/post"; - String payload = "{\"message\": \"Hello world!\"}"; - HttpRestClientDemo.run(uri, payload); + this.commandManagerSettings = CommandManagerSettings.getInstance(environment); + return Collections.emptyList(); } @@ -83,7 +82,26 @@ public List getRestHandlers( SettingsFilter settingsFilter, IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster) { - return Collections.singletonList(new RestPostCommandAction(this.commandIndex)); + return Collections.singletonList( + new RestPostCommandAction(this.commandIndex, this.commandManagerSettings)); + } + + @Override + public List> getSettings() { + return Arrays.asList( + // Register API settings + CommandManagerSettings.M_API_AUTH_USERNAME, + CommandManagerSettings.M_API_AUTH_PASSWORD, + CommandManagerSettings.M_API_URI); + } + + @Override + public void reload(Settings settings) { + // secure settings should be readable + // final CommandManagerSettings commandManagerSettings = + // CommandManagerSettings.getClientSettings(secureSettingsPassword); + // I don't know what I have to do when we want to reload the settings already + // xxxService.refreshAndClearCache(commandManagerSettings); } /** diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/rest/RestPostCommandAction.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/rest/RestPostCommandAction.java index ef7884c..f0a8034 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/rest/RestPostCommandAction.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/rest/RestPostCommandAction.java @@ -9,7 +9,6 @@ package com.wazuh.commandmanager.rest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; -import org.apache.hc.core5.net.URIBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.client.node.NodeClient; @@ -23,8 +22,6 @@ import org.opensearch.rest.RestRequest; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -34,7 +31,8 @@ import com.wazuh.commandmanager.model.Agent; import com.wazuh.commandmanager.model.Command; import com.wazuh.commandmanager.model.Document; -import com.wazuh.commandmanager.utils.httpclient.HttpRestClient; +import com.wazuh.commandmanager.settings.CommandManagerSettings; +import com.wazuh.commandmanager.utils.httpclient.HttpRestClientDemo; import static org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken; import static org.opensearch.rest.RestRequest.Method.POST; @@ -49,14 +47,16 @@ public class RestPostCommandAction extends BaseRestHandler { "post_command_action_request_details"; private static final Logger log = LogManager.getLogger(RestPostCommandAction.class); private final CommandIndex commandIndex; + private final CommandManagerSettings settings; /** * Default constructor * * @param commandIndex persistence layer */ - public RestPostCommandAction(CommandIndex commandIndex) { + public RestPostCommandAction(CommandIndex commandIndex, CommandManagerSettings settings) { this.commandIndex = commandIndex; + this.settings = settings; } public String getName() { @@ -108,19 +108,15 @@ private RestChannelConsumer handlePost(RestRequest request) throws IOException { // Commands delivery to the Management API. // Note: needs to be decoupled from the Rest handler (job scheduler task). - HttpRestClient httpClient = HttpRestClient.getInstance(); try { - String uri = "https://httpbin.org/post"; - // String uri = "https://127.0.0.1:5000"; - URI receiverURI = new URIBuilder(uri).build(); + String receiverURI = this.settings.getUri(); String payload = document.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS) .toString(); - SimpleHttpResponse response = httpClient.post(receiverURI, payload, document.getId()); + SimpleHttpResponse response = + HttpRestClientDemo.runWithResponse(receiverURI, payload, document.getId()); log.info("Received response to POST request with code [{}]", response.getCode()); log.info("Raw response:\n{}", response.getBodyText()); - } catch (URISyntaxException e) { - log.error("Bad URI: {}", e.getMessage()); } catch (Exception e) { log.error("Error reading response: {}", e.getMessage()); } diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/CommandManagerSettings.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/CommandManagerSettings.java new file mode 100644 index 0000000..5023102 --- /dev/null +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/CommandManagerSettings.java @@ -0,0 +1,114 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package com.wazuh.commandmanager.settings; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.SecureSetting; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.settings.SecureString; +import org.opensearch.env.Environment; + + +public class CommandManagerSettings { + + /** The access key (ie login username) for connecting to api. */ + public static final Setting M_API_AUTH_USERNAME = + SecureSetting.secureString("m_api.auth.username", null); + + /** The secret key (ie password) for connecting to api. */ + public static final Setting M_API_AUTH_PASSWORD = + SecureSetting.secureString("m_api.auth.password", null); + + /** The uri for connecting to api. */ + public static final Setting M_API_URI = + SecureSetting.secureString("m_api.uri", null); + + /** The access key (ie login username) for connecting to api. */ + private final String authUsername; + + /** The password for connecting to api. */ + private final String authPassword; + + /** The uri for connecting to api. */ + private final String uri; + + private static final Logger log = LogManager.getLogger(CommandManagerSettings.class); + private static CommandManagerSettings instance; + + /** Private default constructor */ + private CommandManagerSettings( + String authUsername, String authPassword, String uri) { + this.authUsername = authUsername; + this.authPassword = authPassword; + this.uri = uri; + log.info("CommandManagerSettings created "); + } + + /** + * Singleton instance accessor + * + * @return {@link CommandManagerSettings#instance} + */ + public static CommandManagerSettings getInstance(Environment environment) { + if (CommandManagerSettings.instance == null) { + instance = CommandManagerSettings.getSettings(environment); + } + return CommandManagerSettings.instance; + } + + /** Parse settings for a single client. */ + public static CommandManagerSettings getSettings( + Environment environment) { + + final Settings settings = environment.settings(); + if (settings != null) { + log.info("Settings created with the keystore information."); + try (SecureString authUsername = M_API_AUTH_USERNAME.get(settings); + SecureString authPassword = M_API_AUTH_PASSWORD.get(settings); + SecureString uri = M_API_URI.get(settings); ) { + return new CommandManagerSettings( + authUsername.toString(), + authPassword.toString(), + uri.toString()); + } + }else{ + return null; + } + } + + + public String getAuthPassword() { + return authPassword; + } + + public String getAuthUsername() { + return authUsername; + } + + public String getUri() { + return uri; + } + + @Override + public String toString() { + return "CommandManagerSettings{" + + " authUsername='" + + authUsername + + '\'' + + ", authPassword='" + + authPassword + + '\'' + + ", uri='" + + uri + + '\'' + + '}'; + } +} diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/CommandManagerSettingsException.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/CommandManagerSettingsException.java new file mode 100644 index 0000000..7ff2fc8 --- /dev/null +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/CommandManagerSettingsException.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package com.wazuh.commandmanager.settings; + +public class CommandManagerSettingsException extends Exception { + + // Default constructor + public CommandManagerSettingsException() { + super(); + } + + // Constructor that accepts a message + public CommandManagerSettingsException(String message) { + super(message); + } + + // Constructor that accepts a message and a cause + public CommandManagerSettingsException(String message, Throwable cause) { + super(message, cause); + } + + // Constructor that accepts a cause + public CommandManagerSettingsException(Throwable cause) { + super(cause); + } + + // Exception for the case when load keystore failed + public static CommandManagerSettingsException loadSettingsFailed(String keyStorePath, String errorMessage) { + return new CommandManagerSettingsException("Load settings from: " + keyStorePath + " failed. Error: " + errorMessage); + } + + // Exception for the case when reload plugin with the keystore failed + public static CommandManagerSettingsException reloadPluginFailed(String pluginName) { + return new CommandManagerSettingsException("Reload failed for plugin: " + pluginName); + } +} diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/httpclient/HttpRestClientDemo.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/httpclient/HttpRestClientDemo.java index 2fff5c6..f8cde0e 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/httpclient/HttpRestClientDemo.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/httpclient/HttpRestClientDemo.java @@ -51,4 +51,30 @@ public static void run(String endpoint, String body) { return null; }); } + + /** + * Demo method to test the {@link HttpRestClient} class. + * + * @param endpoint POST's requests endpoint as a well-formed URI + * @param body POST's request body as a JSON string. + * @return + */ + public static SimpleHttpResponse runWithResponse(String endpoint, String body, String docId) { + log.info("Executing POST request"); + SimpleHttpResponse response; + response = + AccessController.doPrivileged( + (PrivilegedAction) + () -> { + HttpRestClient httpClient = HttpRestClient.getInstance(); + try { + URI host = new URIBuilder(endpoint).build(); + return httpClient.post(host, body, docId); + } catch (URISyntaxException e) { + log.error("Bad URI:{}", e.getMessage()); + } + return null; + }); + return response; + } } diff --git a/plugins/command-manager/src/main/plugin-metadata/plugin-security.policy b/plugins/command-manager/src/main/plugin-metadata/plugin-security.policy index 6e4d716..99dcb0b 100644 --- a/plugins/command-manager/src/main/plugin-metadata/plugin-security.policy +++ b/plugins/command-manager/src/main/plugin-metadata/plugin-security.policy @@ -1,3 +1,4 @@ grant { permission java.net.SocketPermission "*", "connect,resolve"; -}; \ No newline at end of file + permission java.io.FilePermission "*", "read,write"; +}; diff --git a/plugins/command-manager/src/test/java/com/wazuh/commandmanager/settings/CommandManagerSettingsTests.java b/plugins/command-manager/src/test/java/com/wazuh/commandmanager/settings/CommandManagerSettingsTests.java new file mode 100644 index 0000000..50b3595 --- /dev/null +++ b/plugins/command-manager/src/test/java/com/wazuh/commandmanager/settings/CommandManagerSettingsTests.java @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package com.wazuh.commandmanager.settings; + +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.test.OpenSearchIntegTestCase; +import org.junit.Before; + +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.*; + +@OpenSearchIntegTestCase.ClusterScope(scope = OpenSearchIntegTestCase.Scope.SUITE) +public class CommandManagerSettingsTests extends OpenSearchIntegTestCase { + @Mock private Environment mockEnvironment; + + @InjectMocks private CommandManagerSettings commandManagerSettings; + + Settings testSettings; + + @Before + @Override + public void setUp() throws Exception { + mockEnvironment = mock(Environment.class); + commandManagerSettings = CommandManagerSettings.getInstance(mockEnvironment); + super.setUp(); + } + + public void testGetSettingsWithValidValues() throws Exception{ + final MockSecureSettings secureSettings = new MockSecureSettings(); + try{ + secureSettings.setString("m_api.auth.username", "testUser"); + secureSettings.setString("m_api.auth.password", "testPassword"); + secureSettings.setString("m_api.uri", "https://httpbin.org/post"); + testSettings = Settings.builder().setSecureSettings(secureSettings).build(); + }finally { + when(mockEnvironment.settings()).thenReturn(testSettings); + + // Call getSettings and expect a CommandManagerSettings object + commandManagerSettings = CommandManagerSettings.getSettings(mockEnvironment); + + assertNotNull("Expect that the CommandManagerSettings object is not null",commandManagerSettings); + assertEquals("The m_api.auth.username must be the same","testUser", commandManagerSettings.getAuthUsername()); + assertEquals("The m_api.auth.password must be the same","testPassword", commandManagerSettings.getAuthPassword()); + assertEquals("The m_api.uri must be the same","https://httpbin.org/post", commandManagerSettings.getUri());// Cleanup + secureSettings.close(); + } + } + + public void testSingletonBehavior() throws Exception { + final MockSecureSettings secureSettings = new MockSecureSettings(); + try{ + secureSettings.setString("m_api.auth.username", "testUser"); + testSettings = Settings.builder().setSecureSettings(secureSettings).build(); + }finally { + when(mockEnvironment.settings()).thenReturn(testSettings); + + CommandManagerSettings settings1 = CommandManagerSettings.getInstance(mockEnvironment); + CommandManagerSettings settings2 = CommandManagerSettings.getInstance(mockEnvironment); + assertEquals("Both instances should be the same",settings1, settings2); + } + } +}