Skip to content

Commit

Permalink
Apply rest type endpoint path (#108)
Browse files Browse the repository at this point in the history
* Apply Restful type endpoint path for the commands resource

* Extract POST requests handling to a separate method

* Add OpenAPI spec for the Command Manager API

* Test cycle

Sends the command to a fake Management API on incoming POST requests for the command manager plugin.

* Rename logger

* Improve logging

* Fix testPost_success() test

* Remove redundant anidation of the command fields on the POST endpoint

* Remove rest.action package
  • Loading branch information
AlexRuiz7 authored Oct 16, 2024
1 parent 1a5fe23 commit 0ecde29
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 69 deletions.
64 changes: 64 additions & 0 deletions plugins/command-manager/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
openapi: "3.0.3"
info:
title: Wazuh Indexer Command Manager API
version: "1.0"
servers:
- url: http://127.0.0.1:9200/_plugins/_command_manager
paths:
/commands:
post:
tags:
- "commands"
summary: Add a new command to the queue.
description: Add a new command to the queue.
requestBody:
required: true
content:
"application/json":
schema:
$ref: "#/components/schemas/Command"
responses:
"200":
description: OK

components:
schemas:
Command:
type: object
properties:
source:
type: string
example: "Engine"
user:
type: string
example: "user53"
target:
$ref: '#/components/schemas/Target'
action:
$ref: '#/components/schemas/Action'
timeout:
type: integer
example: 30
Target:
type: object
properties:
id:
type: string
example: "target4"
type:
type: string
example: "agent"
Action:
type: object
properties:
name:
type: string
example: "restart"
args:
type: array
items:
type: string
example: "/path/to/executable/arg6"
version:
type: string
example: "v4"
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
package com.wazuh.commandmanager;

import com.wazuh.commandmanager.index.CommandIndex;
import com.wazuh.commandmanager.rest.action.RestPostCommandAction;
import com.wazuh.commandmanager.rest.RestPostCommandAction;
import com.wazuh.commandmanager.utils.httpclient.HttpRestClient;
import com.wazuh.commandmanager.utils.httpclient.HttpRestClientDemo;
import org.opensearch.client.Client;
Expand Down Expand Up @@ -45,7 +45,8 @@
* Agents.
*/
public class CommandManagerPlugin extends Plugin implements ActionPlugin {
public static final String COMMAND_MANAGER_BASE_URI = "/_plugins/_commandmanager";
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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
*/
public class CommandIndex implements IndexingOperationListener {

private static final Logger logger = LogManager.getLogger(CommandIndex.class);
private static final Logger log = LogManager.getLogger(CommandIndex.class);

private final Client client;
private final ClusterService clusterService;
Expand Down Expand Up @@ -67,13 +67,13 @@ public CompletableFuture<RestStatus> asyncCreate(Document document) {
if (!indexTemplateExists(CommandManagerPlugin.COMMAND_MANAGER_INDEX_TEMPLATE_NAME)) {
putIndexTemplate(CommandManagerPlugin.COMMAND_MANAGER_INDEX_TEMPLATE_NAME);
} else {
logger.info(
log.info(
"Index template {} already exists. Skipping creation.",
CommandManagerPlugin.COMMAND_MANAGER_INDEX_TEMPLATE_NAME
);
}

logger.debug("Indexing command {}", document);
log.info("Indexing command with id [{}]", document.getId());
try {
IndexRequest request = new IndexRequest()
.index(CommandManagerPlugin.COMMAND_MANAGER_INDEX_NAME)
Expand All @@ -91,8 +91,8 @@ public CompletableFuture<RestStatus> asyncCreate(Document document) {
}
);
} catch (IOException e) {
logger.error(
"Failed to index command with ID {}: {}", document.getId(), e);
log.error(
"Error indexing command with id [{}] due to {}", document.getId(), e);
}
return future;
}
Expand All @@ -108,7 +108,7 @@ public boolean indexTemplateExists(String template_name) {
.state()
.metadata()
.templates();
logger.debug("Existing index templates: {} ", templates);
log.debug("Existing index templates: {} ", templates);

return templates.containsKey(template_name);
}
Expand Down Expand Up @@ -137,15 +137,15 @@ public void putIndexTemplate(String templateName) {
.putTemplate(putIndexTemplateRequest)
.actionGet();
if (acknowledgedResponse.isAcknowledged()) {
logger.info(
"Index template created successfully: {}",
log.info(
"Index template [{}] created successfully",
templateName
);
}
});

} catch (IOException e) {
logger.error("Error reading index template from filesystem {}", templateName);
log.error("Error reading index template [{}] from filesystem", templateName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import reactor.util.annotation.NonNull;

import java.io.IOException;
import java.util.ArrayList;

public class Command implements ToXContentObject {
public static final String COMMAND = "command";
Expand Down Expand Up @@ -64,9 +65,10 @@ public Command(
*
* @param parser XContentParser from the Rest Request
* @return instance of Command
* @throws IOException
* @throws IOException error parsing request content
* @throws IllegalArgumentException missing arguments
*/
public static Command parse(XContentParser parser) throws IOException {
public static Command parse(XContentParser parser) throws IOException, IllegalArgumentException {
String source = null;
Target target = null;
Integer timeout = null;
Expand Down Expand Up @@ -99,14 +101,34 @@ public static Command parse(XContentParser parser) throws IOException {
}
}

// TODO add proper validation
return new Command(
source,
target,
timeout,
user,
action
);
ArrayList<String> nullArguments = new ArrayList<>();
if (source == null) {
nullArguments.add("source");
}
if (target == null) {
nullArguments.add("target");
}
if (timeout == null) {
nullArguments.add("timeout");
}
if (user == null) {
nullArguments.add("user");
}
if (action == null) {
nullArguments.add("action");
}

if (!nullArguments.isEmpty()) {
throw new IllegalArgumentException("Missing arguments: " + nullArguments);
} else {
return new Command(
source,
target,
timeout,
user,
action
);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
package com.wazuh.commandmanager.rest.action;
package com.wazuh.commandmanager.rest;

import com.wazuh.commandmanager.CommandManagerPlugin;
import com.wazuh.commandmanager.index.CommandIndex;
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 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;
import org.opensearch.common.xcontent.XContentFactory;
import org.opensearch.core.rest.RestStatus;
import org.opensearch.core.xcontent.ToXContent;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;
import org.opensearch.rest.BaseRestHandler;
import org.opensearch.rest.BytesRestResponse;
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;
Expand All @@ -30,13 +39,13 @@

/**
* Handles HTTP requests to the POST
* {@value com.wazuh.commandmanager.CommandManagerPlugin#COMMAND_MANAGER_BASE_URI}
* {@value com.wazuh.commandmanager.CommandManagerPlugin#COMMANDS_URI}
* endpoint.
*/
public class RestPostCommandAction extends BaseRestHandler {

public static final String POST_COMMAND_ACTION_REQUEST_DETAILS = "post_command_action_request_details";
private static final Logger logger = LogManager.getLogger(RestPostCommandAction.class);
private static final Logger log = LogManager.getLogger(RestPostCommandAction.class);
private final CommandIndex commandIndex;

/**
Expand All @@ -61,22 +70,68 @@ public List<Route> routes() {
String.format(
Locale.ROOT,
"%s",
CommandManagerPlugin.COMMAND_MANAGER_BASE_URI
CommandManagerPlugin.COMMANDS_URI
)
)
);
}

@Override
protected RestChannelConsumer prepareRequest(
final RestRequest restRequest,
final RestRequest request,
final NodeClient client
) throws IOException {
switch (request.method()) {
case POST:
return handlePost(request);
default:
throw new IllegalArgumentException(
"Unsupported HTTP method " + request.method().name());
}
}

/**
* Handles a POST HTTP request.
*
* @param request POST HTTP request
* @return a response to the request as BytesRestResponse.
* @throws IOException thrown by the XContentParser methods.
*/
private RestChannelConsumer handlePost(RestRequest request) throws IOException {
log.info(
"Received {} {} request id [{}] from host [{}]",
request.method().name(),
request.uri(),
request.getRequestId(),
request.header("Host")
);
// Get request details
XContentParser parser = restRequest.contentParser();
XContentParser parser = request.contentParser();
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);

Document document = Document.parse(parser);
Command command = Command.parse(parser);
Document document = new Document(
new Agent(List.of("groups000")), // TODO read agent from .agents index
command
);

// 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 payload = document.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).toString();
SimpleHttpResponse response = httpClient.post(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());
}

// Send response
return channel -> {
Expand All @@ -89,8 +144,12 @@ protected RestChannelConsumer prepareRequest(
builder.field("result", restStatus.name());
builder.endObject();
channel.sendResponse(new BytesRestResponse(restStatus, builder));
} catch (Exception e) {
logger.error("Error indexing command: ", e);
} catch (IOException e) {
log.error("Error preparing response to [{}] request with id [{}] due to {}",
request.method().name(),
request.getRequestId(),
e.getMessage()
);
}
}).exceptionally(e -> {
channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()));
Expand Down
Loading

0 comments on commit 0ecde29

Please sign in to comment.