diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/index/CommandIndex.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/index/CommandIndex.java index 9af2d88c..bbaa90ae 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/index/CommandIndex.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/index/CommandIndex.java @@ -34,7 +34,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import com.wazuh.commandmanager.model.Document; +import com.wazuh.commandmanager.model.Order; import com.wazuh.commandmanager.settings.PluginSettings; import com.wazuh.commandmanager.utils.IndexTemplateUtils; @@ -72,22 +72,22 @@ public boolean indexExists() { /** * Indexes an array of documents asynchronously. * - * @param documents list of instances of the document model to persist in the index. + * @param orders list of instances of the document model to persist in the index. * @return A CompletableFuture with the RestStatus response from the operation */ - public CompletableFuture asyncBulkCreate(ArrayList documents) { + public CompletableFuture asyncBulkCreate(ArrayList orders) { final CompletableFuture future = new CompletableFuture<>(); final ExecutorService executor = this.threadPool.executor(ThreadPool.Names.WRITE); final BulkRequest bulkRequest = new BulkRequest(); - for (Document document : documents) { - log.info("Adding command with id [{}] to the bulk request", document.getId()); + for (Order order : orders) { + log.info("Adding command with id [{}] to the bulk request", order.getId()); try { - bulkRequest.add(createIndexRequest(document)); + bulkRequest.add(createIndexRequest(order)); } catch (IOException e) { log.error( "Error creating IndexRequest with document id [{}] due to {}", - document.getId(), + order.getId(), e.getMessage()); } } @@ -120,15 +120,15 @@ public CompletableFuture asyncBulkCreate(ArrayList documen /** * Create an IndexRequest object from a Document object. * - * @param document the document to create the IndexRequest for COMMAND_MANAGER_INDEX + * @param order the document to create the IndexRequest for COMMAND_MANAGER_INDEX * @return an IndexRequest object * @throws IOException thrown by XContentFactory.jsonBuilder() */ - private IndexRequest createIndexRequest(Document document) throws IOException { + private IndexRequest createIndexRequest(Order order) throws IOException { return new IndexRequest() .index(PluginSettings.getIndexName()) - .source(document.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS)) - .id(document.getId()) + .source(order.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .id(order.getId()) .create(true); } } diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Agent.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Agent.java index 5f8da173..4e899b97 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Agent.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Agent.java @@ -27,6 +27,8 @@ public class Agent implements ToXContentObject { public static final String AGENT = "agent"; public static final String GROUPS = "groups"; + public static final String ID = "id"; + private final String id; private final List groups; /** @@ -34,7 +36,8 @@ public class Agent implements ToXContentObject { * * @param groups Agent's groups */ - public Agent(List groups) { + public Agent(String id, List groups) { + this.id = id; this.groups = groups; } @@ -47,20 +50,27 @@ public Agent(List groups) { */ public static Agent parse(XContentParser parser) throws IOException { List groups = List.of(); + String id = null; while (parser.nextToken() != XContentParser.Token.END_OBJECT) { String fieldName = parser.currentName(); parser.nextToken(); - if (fieldName.equals(GROUPS)) { - groups = parser.list(); - } else { - parser.skipChildren(); + switch (fieldName) { + case GROUPS: + groups = parser.list(); + break; + case ID: + id = parser.text(); + break; + default: + parser.skipChildren(); + break; } } // Cast args field Object list to String list List convertedGroupFields = (List) (List) (groups); - return new Agent(convertedGroupFields); + return new Agent(id, convertedGroupFields); } @Override @@ -70,8 +80,17 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder.endObject(); } + /** + * Retrieves the agent's id. + * + * @return id of the agent. + */ + public String getId() { + return this.id; + } + @Override public String toString() { - return "Agent{" + "groups=" + groups + '}'; + return "Agent{" + "id='" + id + '\'' + ", groups=" + groups + '}'; } } diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Command.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Command.java index 35a22ac5..02c6566d 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Command.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Command.java @@ -16,11 +16,14 @@ */ package com.wazuh.commandmanager.model; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.common.UUIDs; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.RestRequest; import java.io.IOException; import java.util.ArrayList; @@ -28,6 +31,8 @@ import reactor.util.annotation.NonNull; +import static org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken; + /** Command's fields. */ public class Command implements ToXContentObject { public static final String COMMAND = "command"; @@ -46,6 +51,8 @@ public class Command implements ToXContentObject { private final Status status; private final Action action; + private static final Logger log = LogManager.getLogger(Command.class); + /** * Default constructor * @@ -181,6 +188,28 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder.endObject(); } + /** + * Parses the content of a RestRequest and retrieves a list of Command objects. + * + * @param request the RestRequest containing the command data. + * @return a list of Command objects parsed from the request content. + * @throws IOException if an error occurs while parsing the request content. + */ + public static List parse(RestRequest request) throws IOException { + // Request parsing + XContentParser parser = request.contentParser(); + List commands = new ArrayList<>(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + parser.nextToken(); + if (parser.nextToken() == XContentParser.Token.START_ARRAY) { + commands = Command.parseToArray(parser); + } else { + log.error("Token does not match {}", parser.currentToken()); + } + + return commands; + } + /** * Returns the nested Action fields. * @@ -217,16 +246,6 @@ public String getUser() { return this.user; } - /** - * Retrieves the status of this command. - * - * @return the status of the command. - * @see Status - */ - public Status getStatus() { - return this.status; - } - @Override public String toString() { return "Command{" diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Document.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Document.java deleted file mode 100644 index f6f277bf..00000000 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Document.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2024, Wazuh Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wazuh.commandmanager.model; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.common.UUIDs; -import org.opensearch.common.time.DateFormatter; -import org.opensearch.common.time.DateUtils; -import org.opensearch.common.time.FormatNames; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.*; -import org.opensearch.search.SearchHit; - -import java.io.IOException; -import java.time.ZonedDateTime; -import java.util.List; - -/** Command's target fields. */ -public class Document implements ToXContentObject { - private static final String DATE_FORMAT = FormatNames.DATE_TIME_NO_MILLIS.getSnakeCaseName(); - private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern(DATE_FORMAT); - public static final String TIMESTAMP = "@timestamp"; - public static final String DELIVERY_TIMESTAMP = "delivery_timestamp"; - private final Agent agent; - private final Command command; - private final String id; - private final ZonedDateTime timestamp; - private final ZonedDateTime deliveryTimestamp; - - private static final Logger log = LogManager.getLogger(Document.class); - - /** - * Default constructor. - * - * @param agent "agent" nested fields. - * @param command "command" nested fields. - */ - public Document(Agent agent, Command command) { - this.agent = agent; - this.command = command; - this.id = UUIDs.base64UUID(); - this.timestamp = DateUtils.nowWithMillisResolution(); - this.deliveryTimestamp = timestamp.plusSeconds(command.getTimeout()); - } - - /** - * Parses data from an XContentParser into this model. - * - * @param parser xcontent parser. - * @return initialized instance of Document. - * @throws IOException parsing error occurred. - */ - public static Document parse(XContentParser parser) throws IOException { - Agent agent = new Agent(List.of("groups000")); // TODO read agent from wazuh-agents index - Command command = null; - - while (parser.nextToken() != XContentParser.Token.END_OBJECT) { - String fieldName = parser.currentName(); - parser.nextToken(); - if (fieldName.equals(Command.COMMAND)) { - command = Command.parse(parser); - } else { - parser.skipChildren(); // TODO raise error as command values are required - } - } - - return new Document(agent, command); - } - - /** - * Returns the delivery timestamp from a search hit. - * - * @param hit search hit parser. - * @return delivery timestamp from Document in search hit. - */ - public static ZonedDateTime deliveryTimestampFromSearchHit(SearchHit hit) { - ZonedDateTime deliveryTimestamp = null; - - try { - XContentParser parser = - XContentHelper.createParser( - NamedXContentRegistry.EMPTY, - DeprecationHandler.IGNORE_DEPRECATIONS, - hit.getSourceRef(), - XContentType.JSON); - - parser.nextToken(); - while (parser.nextToken() != null) { - if (parser.currentToken().equals(XContentParser.Token.FIELD_NAME)) { - String fieldName = parser.currentName(); - if (fieldName.equals(Document.DELIVERY_TIMESTAMP)) { - parser.nextToken(); - deliveryTimestamp = ZonedDateTime.from(DATE_FORMATTER.parse(parser.text())); - } else { - parser.skipChildren(); - } - } - } - - parser.close(); - - } catch (IOException e) { - log.error("Delivery timestamp could not be parsed: {}", e.getMessage()); - } - return deliveryTimestamp; - } - - /** - * Returns the document's "_id". - * - * @return Document's ID - */ - public String getId() { - return this.id; - } - - /** - * Returns the Command object associated with this Document. - * - * @return Command object - */ - public Command getCommand() { - return this.command; - } - - /** - * Returns the timestamp at which the Command was delivered to the Agent. - * - * @return ZonedDateTime object representing the delivery timestamp - */ - public ZonedDateTime getDeliveryTimestamp() { - return this.deliveryTimestamp; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - this.agent.toXContent(builder, ToXContentObject.EMPTY_PARAMS); - this.command.toXContent(builder, ToXContentObject.EMPTY_PARAMS); - builder.field(TIMESTAMP, DATE_FORMATTER.format(this.timestamp)); - builder.field(DELIVERY_TIMESTAMP, DATE_FORMATTER.format(this.deliveryTimestamp)); - return builder.endObject(); - } - - @Override - public String toString() { - return "Document{" - + "@timestamp=" - + timestamp - + ", delivery_timestamp=" - + deliveryTimestamp - + ", agent=" - + agent - + ", command=" - + command - + '}'; - } -} diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Documents.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Documents.java deleted file mode 100644 index dc0e5596..00000000 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Documents.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2024, Wazuh Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wazuh.commandmanager.model; - -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.ArrayList; - -/** Documents model class. */ -public class Documents implements ToXContentObject { - public static final String DOCUMENTS = "_documents"; - public static final String ID = "_id"; - private final ArrayList documents; - - /** Default constructor. */ - public Documents() { - this.documents = new ArrayList<>(); - } - - /** - * Get the list of Document objects. - * - * @return the list of documents. - */ - public ArrayList getDocuments() { - return documents; - } - - /** - * Adds a document to the list of documents. - * - * @param document The document to add to the list. - */ - public void addDocument(Document document) { - this.documents.add(document); - } - - /** - * Fit this object into a XContentBuilder parser, preparing it for the reply of POST /commands. - * - * @param builder XContentBuilder builder - * @param params ToXContent.EMPTY_PARAMS - * @return XContentBuilder builder with the representation of this object. - * @throws IOException parsing error. - */ - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startArray(DOCUMENTS); - for (Document document : this.documents) { - builder.startObject(); - builder.field(ID, document.getId()); - builder.endObject(); - } - return builder.endArray(); - } -} diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Order.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Order.java index f8d9b144..62df1a74 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Order.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Order.java @@ -18,61 +18,84 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.common.UUIDs; +import org.opensearch.common.time.DateFormatter; +import org.opensearch.common.time.DateUtils; +import org.opensearch.common.time.FormatNames; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.xcontent.*; import org.opensearch.search.SearchHit; import java.io.IOException; -import java.util.Objects; +import java.time.ZonedDateTime; -import reactor.util.annotation.NonNull; - -/** Order model class. */ -public class Order implements ToXContent { - public static final String SOURCE = "source"; - public static final String USER = "user"; - public static final String DOCUMENT_ID = "document_id"; - private final String source; - private final Target target; - private final String user; - private final Action action; - private final String documentId; +/** + * This class represents the value to be stored in the commands index, it is the result of the + * processed command received through the API. + */ +public class Order implements ToXContentObject { + private static final String DATE_FORMAT = FormatNames.DATE_TIME_NO_MILLIS.getSnakeCaseName(); + private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern(DATE_FORMAT); + public static final String TIMESTAMP = "@timestamp"; + public static final String DELIVERY_TIMESTAMP = "delivery_timestamp"; + private final Agent agent; + private final Command command; + private final String id; + private final ZonedDateTime timestamp; + private final ZonedDateTime deliveryTimestamp; private static final Logger log = LogManager.getLogger(Order.class); /** - * Default constructor + * Default constructor. + * + * @param agent "agent" nested fields. + * @param command "command" nested fields. + */ + public Order(Agent agent, Command command) { + this.agent = agent; + this.command = command; + this.id = UUIDs.base64UUID(); + this.timestamp = DateUtils.nowWithMillisResolution(); + this.deliveryTimestamp = timestamp.plusSeconds(command.getTimeout()); + } + + /** + * Parses data from an XContentParser into this model. * - * @param source String field representing the origin of the command order - * @param target Object containing the destination's type and id. It is handled by its own model - * class - * @param user The requester of the command - * @param action An object containing the actual executable plus arguments and version. Handled - * by its own model class - * @param documentId The document ID from the index that holds commands. Used by the agent to - * report back the results of the action + * @param parser xcontent parser. + * @return initialized instance of Document. + * @throws IOException parsing error occurred. */ - public Order( - @NonNull String source, - @NonNull Target target, - @NonNull String user, - @NonNull Action action, - @NonNull String documentId) { - this.source = source; - this.target = target; - this.user = user; - this.action = action; - this.documentId = documentId; + public static Order parse(XContentParser parser) throws IOException { + Agent agent = null; + Command command = null; + + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + String fieldName = parser.currentName(); + parser.nextToken(); + if (fieldName.equals(Command.COMMAND)) { + command = Command.parse(parser); + } else if (fieldName.equals(Agent.AGENT)) { + agent = Agent.parse(parser); + } else { + parser.skipChildren(); // TODO raise error as command values are required + } + } + + return new Order(agent, command); } /** - * Parses a SearchHit into an order as expected by a Wazuh Agent + * Returns the delivery timestamp from a search hit. * - * @param hit The SearchHit result of a search - * @return An Order Object in accordance with the data model + * @param hit search hit parser. + * @return delivery timestamp from Document in search hit. */ - public static Order fromSearchHit(SearchHit hit) { + public static ZonedDateTime deliveryTimestampFromSearchHit(SearchHit hit) { + ZonedDateTime deliveryTimestamp = null; + try { XContentParser parser = XContentHelper.createParser( @@ -80,74 +103,76 @@ public static Order fromSearchHit(SearchHit hit) { DeprecationHandler.IGNORE_DEPRECATIONS, hit.getSourceRef(), XContentType.JSON); - Command command = null; - // Iterate over the JsonXContentParser's JsonToken until we hit null, - // which corresponds to end of data + + parser.nextToken(); while (parser.nextToken() != null) { - // Look for FIELD_NAME JsonToken s if (parser.currentToken().equals(XContentParser.Token.FIELD_NAME)) { String fieldName = parser.currentName(); - if (fieldName.equals(Command.COMMAND)) { - // Parse Command - command = Command.parse(parser); + if (fieldName.equals(Order.DELIVERY_TIMESTAMP)) { + parser.nextToken(); + deliveryTimestamp = ZonedDateTime.from(DATE_FORMATTER.parse(parser.text())); } else { parser.skipChildren(); } } } - // Create a new Order object with the Command's fields - return new Order( - Objects.requireNonNull(command).getSource(), - Objects.requireNonNull(command).getTarget(), - Objects.requireNonNull(command).getUser(), - Objects.requireNonNull(command).getAction(), - Objects.requireNonNull(hit).getId()); + + parser.close(); + } catch (IOException e) { - log.error("Order could not be parsed: {}", e.getMessage()); - } catch (NullPointerException e) { - log.error( - "Could not create Order object. One or more of the constructor's arguments was null: {}", - e.getMessage()); + log.error("Delivery timestamp could not be parsed: {}", e.getMessage()); } - return null; + return deliveryTimestamp; + } + + /** + * Returns the document's "_id". + * + * @return Document's ID + */ + public String getId() { + return this.id; + } + + /** + * Returns the Command object associated with this Document. + * + * @return Command object + */ + public Command getCommand() { + return this.command; } /** - * Used to serialize the Order's contents. + * Returns the timestamp at which the Command was delivered to the Agent. * - * @param builder The builder object we will add our Json to - * @param params Not used. Required by the interface. - * @return XContentBuilder with a Json object including this Order's fields - * @throws IOException Rethrown from IOException's XContentBuilder methods + * @return ZonedDateTime object representing the delivery timestamp */ + public ZonedDateTime getDeliveryTimestamp() { + return this.deliveryTimestamp; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(SOURCE, this.source); - builder.field(USER, this.user); - this.target.toXContent(builder, ToXContent.EMPTY_PARAMS); - this.action.toXContent(builder, ToXContent.EMPTY_PARAMS); - builder.field(DOCUMENT_ID, this.documentId); - + this.agent.toXContent(builder, ToXContentObject.EMPTY_PARAMS); + this.command.toXContent(builder, ToXContentObject.EMPTY_PARAMS); + builder.field(TIMESTAMP, DATE_FORMATTER.format(this.timestamp)); + builder.field(DELIVERY_TIMESTAMP, DATE_FORMATTER.format(this.deliveryTimestamp)); return builder.endObject(); } @Override public String toString() { - return "Order{" - + "action=" - + action - + ", source='" - + source - + '\'' - + ", target=" - + target - + ", user='" - + user - + '\'' - + ", document_id='" - + documentId - + '\'' + return "Document{" + + "@timestamp=" + + timestamp + + ", delivery_timestamp=" + + deliveryTimestamp + + ", agent=" + + agent + + ", command=" + + command + '}'; } } diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Orders.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Orders.java index 1fd86c97..fd11c4d1 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Orders.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Orders.java @@ -16,89 +16,127 @@ */ package com.wazuh.commandmanager.model; -import org.opensearch.core.xcontent.ToXContent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.search.SearchHit; import org.opensearch.search.SearchHits; import java.io.IOException; import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; -/** Orders model class. */ -public class Orders implements ToXContent { - public static final String ORDERS = "orders"; +import com.wazuh.commandmanager.settings.PluginSettings; +import com.wazuh.commandmanager.utils.Search; +/** Model that stores a list of Orders to be indexed at the commands index */ +public class Orders implements ToXContentObject { + public static final String ORDERS = "orders"; + public static final String ID = "_id"; private final ArrayList orders; + private static final Logger log = LogManager.getLogger(Orders.class); + /** Default constructor. */ public Orders() { this.orders = new ArrayList<>(); } /** - * Helper static method that takes the search results in SearchHits form and parses them into - * Order objects. It then puts together a json string meant for sending over HTTP - * - * @param searchHits the commands search result - * @return A json string payload with an array of orders to be processed - */ - - /** - * Static builder method that initializes an instance of Orders from a SearchHits instance. + * Get the list of Order objects. * - * @param searchHits search hits as returned from the search index query to the commands index. - * @return instance of Orders. + * @return the list of documents. */ - public static Orders fromSearchHits(SearchHits searchHits) { - Orders orders = new Orders(); - - // Iterate over search results - for (SearchHit hit : searchHits) { - // Parse the hit's order - Order order = Order.fromSearchHit(hit); - orders.add(order); - } - - return orders; + public ArrayList get() { + return this.orders; } /** - * Overwrites the array of orders + * Adds a document to the list of documents. * - * @param orders the list of orders to be set. + * @param order The document to add to the list. */ - public void setOrders(ArrayList orders) { - this.orders.clear(); - this.orders.addAll(orders); + public void add(Order order) { + this.orders.add(order); } /** - * Retrieves the list of orders. + * Fit this object into a XContentBuilder parser, preparing it for the reply of POST /commands. * - * @return the current list of Order objects. + * @param builder XContentBuilder builder + * @param params ToXContent.EMPTY_PARAMS + * @return XContentBuilder builder with the representation of this object. + * @throws IOException parsing error. */ - public ArrayList getOrders() { - return this.orders; + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startArray(ORDERS); + for (Order order : this.orders) { + builder.startObject(); + builder.field(ID, order.getId()); + builder.endObject(); + } + return builder.endArray(); } /** - * Adds an order to the orders array. + * Converts a list of Command objects into Orders by executing search queries. * - * @param order order to add. + * @param client the NodeClient used to execute search queries. + * @param commands the list of Command objects to be converted. + * @return an Orders object containing the generated orders. */ - private void add(Order order) { - this.orders.add(order); - } + @SuppressWarnings("unchecked") + public static Orders fromCommands(NodeClient client, List commands) { + Orders orders = new Orders(); - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - // Start an XContentBuilder array named "orders" - builder.startObject(); - builder.startArray(ORDERS); - for (Order order : this.orders) { - order.toXContent(builder, params); + for (Command command : commands) { + List agentList = new ArrayList<>(); + String queryField = ""; + Target.Type targetType = command.getTarget().getType(); + String targetId = command.getTarget().getId(); + + if (Objects.equals(targetType, Target.Type.GROUP)) { + queryField = "agent.groups"; + } else if (Objects.equals(targetType, Target.Type.AGENT)) { + queryField = "agent.id"; + } + + // Build and execute the search query + log.info("Searching for agents using field {} with value {}", queryField, targetId); + SearchHits hits = + Search.syncSearch( + client, PluginSettings.getAgentsIndex(), queryField, targetId); + if (hits != null) { + for (SearchHit hit : hits) { + final Map agentMap = + Search.getNestedObject(hit.getSourceAsMap(), "agent", Map.class); + if (agentMap != null) { + String agentId = (String) agentMap.get(Agent.ID); + List agentGroups = (List) agentMap.get(Agent.GROUPS); + Agent agent = new Agent(agentId, agentGroups); + agentList.add(agent); + } + } + log.info("Search retrieved {} agents.", agentList.size()); + } + + for (Agent agent : agentList) { + Command newCommand = + new Command( + command.getSource(), + new Target(Target.Type.AGENT, agent.getId()), + command.getTimeout(), + command.getUser(), + command.getAction()); + Order order = new Order(agent, newCommand); + orders.add(order); + } } - builder.endArray(); - return builder.endObject(); + return orders; } } diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Target.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Target.java index 55a538d4..0435d143 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Target.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/model/Target.java @@ -27,7 +27,51 @@ public class Target implements ToXContentObject { public static final String TARGET = "target"; public static final String TYPE = "type"; public static final String ID = "id"; - private final String type; + + /** Define the possible values for the Type object. */ + public enum Type { + AGENT("agent"), + GROUP("group"); + + /** The string value of the type. */ + private final String value; + + /** + * Constructs a new Type with the specified string value. + * + * @param value the string value of the type + */ + Type(String value) { + this.value = value; + } + + /** + * Gets the string value of the type. + * + * @return the string value of the type + */ + public String getValue() { + return value; + } + + /** + * Converts a string to the corresponding Type enum. + * + * @param value the string value to convert + * @return the corresponding Type enum + * @throws IllegalArgumentException if the string does not match any Type + */ + public static Type fromString(String value) { + for (Type type : Type.values()) { + if (type.value.equalsIgnoreCase(value)) { + return type; + } + } + throw new IllegalArgumentException("Invalid type: " + value); + } + } + + private final Type type; private final String id; /** @@ -36,11 +80,29 @@ public class Target implements ToXContentObject { * @param type The destination type. One of [`group`, `agent`, `server`] * @param id Unique identifier of the destination to send the command to. */ - public Target(String type, String id) { + public Target(Type type, String id) { this.type = type; this.id = id; } + /** + * Retrieves the id of this target. + * + * @return the target's id. + */ + public String getId() { + return this.id; + } + + /** + * Retrieves the type of this target. + * + * @return the target's type. + */ + public Type getType() { + return this.type; + } + /** * Parses data from an XContentParser into this model. * @@ -49,7 +111,7 @@ public Target(String type, String id) { * @throws IOException parsing error occurred. */ public static Target parse(XContentParser parser) throws IOException { - String type = ""; + Type type = null; String id = ""; while (parser.nextToken() != XContentParser.Token.END_OBJECT) { @@ -57,7 +119,7 @@ public static Target parse(XContentParser parser) throws IOException { parser.nextToken(); switch (fieldName) { case TYPE: - type = parser.text(); + type = Type.fromString(parser.text()); break; case ID: id = parser.text(); @@ -74,13 +136,13 @@ public static Target parse(XContentParser parser) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(TARGET); - builder.field(TYPE, this.type); + builder.field(TYPE, this.type.getValue()); builder.field(ID, this.id); return builder.endObject(); } @Override public String toString() { - return "Target{" + "type='" + type + '\'' + ", id='" + id + '\'' + '}'; + return "Target{" + "type='" + type.getValue() + '\'' + ", id='" + id + '\'' + '}'; } } 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 14764eec..aed22521 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 @@ -22,25 +22,18 @@ 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.util.ArrayList; -import java.util.List; -import java.util.Locale; +import java.util.*; import java.util.concurrent.CompletableFuture; 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.model.Documents; +import com.wazuh.commandmanager.model.*; import com.wazuh.commandmanager.settings.PluginSettings; -import static org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken; import static org.opensearch.rest.RestRequest.Method.POST; /** Handles HTTP requests to the POST the Commands API endpoint. */ @@ -77,7 +70,7 @@ protected RestChannelConsumer prepareRequest(final RestRequest request, final No throws IOException { switch (request.method()) { case POST: - return handlePost(request); + return handlePost(request, client); default: throw new IllegalArgumentException( "Unsupported HTTP method " + request.method().name()); @@ -88,41 +81,32 @@ protected RestChannelConsumer prepareRequest(final RestRequest request, final No * Handles a POST HTTP request. * * @param request POST HTTP request + * @param client NodeClient instance * @return a response to the request as BytesRestResponse. * @throws IOException thrown by the XContentParser methods. */ - private RestChannelConsumer handlePost(RestRequest request) throws IOException { + private RestChannelConsumer handlePost(RestRequest request, final NodeClient client) + throws IOException { log.info( "Received {} {} request id [{}] from host [{}]", request.method().name(), request.uri(), request.getRequestId(), request.header("Host")); - - /// Request validation - /// ================== - /// Fail fast. + // Request validation if (!request.hasContent()) { - // Bad request if body doesn't exist return channel -> { channel.sendResponse( new BytesRestResponse(RestStatus.BAD_REQUEST, "Body content is required")); }; } - - /// Request parsing - /// =============== - /// Retrieves and generates an array list of commands. - XContentParser parser = request.contentParser(); - List commands = new ArrayList<>(); - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - // The array of commands is inside the "commands" JSON object. - // This line moves the parser pointer to this object. - parser.nextToken(); - if (parser.nextToken() == XContentParser.Token.START_ARRAY) { - commands = Command.parseToArray(parser); - } else { - log.error("Token does not match {}", parser.currentToken()); + List commands = Command.parse(request); + // Validate commands are not empty + if (commands.isEmpty()) { + return channel -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.BAD_REQUEST, "No commands provided.")); + }; } /// Commands expansion @@ -132,25 +116,22 @@ private RestChannelConsumer handlePost(RestRequest request) throws IOException { // agents. /// Given a group of agents A with N agents, a total of N orders are generated. One for each // agent. - Documents documents = new Documents(); - for (Command command : commands) { - Document document = - new Document( - new Agent(List.of("groups000")), // TODO read agent from wazuh-agents - // index - command); - documents.addDocument(document); + Orders orders = Orders.fromCommands(client, commands); + // Validate that the orders are not empty + if (orders.get().isEmpty()) { + return channel -> { + channel.sendResponse( + new BytesRestResponse( + RestStatus.BAD_REQUEST, + "Cannot generate orders. Invalid agent IDs or groups.")); + }; } - /// Orders indexing - /// ================== - /// The orders are inserted into the index. + // Orders indexing CompletableFuture bulkRequestFuture = - this.commandIndex.asyncBulkCreate(documents.getDocuments()); + this.commandIndex.asyncBulkCreate(orders.get()); - /// Send response - /// ================== - /// Reply to the request. + // Send response return channel -> { bulkRequestFuture .thenAccept( @@ -158,7 +139,7 @@ private RestChannelConsumer handlePost(RestRequest request) throws IOException { try (XContentBuilder builder = channel.newBuilder()) { builder.startObject(); builder.field("_index", PluginSettings.getIndexName()); - documents.toXContent(builder, ToXContent.EMPTY_PARAMS); + orders.toXContent(builder, ToXContent.EMPTY_PARAMS); builder.field("result", restStatus.name()); builder.endObject(); channel.sendResponse( diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/PluginSettings.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/PluginSettings.java index bddfe6c4..4b1ef7ca 100644 --- a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/PluginSettings.java +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/settings/PluginSettings.java @@ -39,6 +39,7 @@ public class PluginSettings { private static final String JOB_INDEX = ".scheduled-commands"; private static final String COMMAND_INDEX_TEMPLATE = "index-template-commands"; private static final String COMMAND_INDEX = "wazuh-commands"; + private static final String AGENTS_INDEX = "wazuh-agents"; private static final String API_BASE_URI = "/_plugins/_command_manager"; private static final String API_COMMANDS_ENDPOINT = API_BASE_URI + "/commands"; @@ -202,6 +203,13 @@ public static String getIndexName() { return COMMAND_INDEX; } + /** + * @return the name of the agents index + */ + public static String getAgentsIndex() { + return AGENTS_INDEX; + } + /** * @return the index template */ diff --git a/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/Search.java b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/Search.java new file mode 100644 index 00000000..58caa613 --- /dev/null +++ b/plugins/command-manager/src/main/java/com/wazuh/commandmanager/utils/Search.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024, Wazuh Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wazuh.commandmanager.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.support.AbstractClient; +import org.opensearch.core.action.ActionListener; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.SearchHits; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.util.*; +import java.util.concurrent.CountDownLatch; + +/** Utility class for performing generic search operations on the OpenSearch cluster. */ +public class Search { + private static final Logger log = LogManager.getLogger(Search.class); + + /** + * Executes a synchronous search query on the specified index using a term query. + * + * @param client the AbstractClient used to execute the search query. + * @param index the name of the index to search. + * @param field the field to query. + * @param value the value to search for in the specified field. + * @return SearchHits object containing the search results. + */ + public static SearchHits syncSearch( + AbstractClient client, String index, String field, String value) { + BoolQueryBuilder boolQuery = + QueryBuilders.boolQuery().must(QueryBuilders.termQuery(field, value)); + return executeSearch(client, index, boolQuery); + } + + /** + * Executes a synchronous search query on the specified index. + * + * @param client the AbstractClient used to execute the search query. + * @param index the name of the index to search. + * @param boolQuery the boolean query to execute. + * @return SearchHits object containing the search results. + */ + private static SearchHits executeSearch( + AbstractClient client, String index, BoolQueryBuilder boolQuery) { + SearchRequest searchRequest = new SearchRequest(index); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(boolQuery); + searchRequest.source(searchSourceBuilder); + + final CountDownLatch latch = new CountDownLatch(1); + final SearchHits[] searchHits = new SearchHits[1]; + + client.search( + searchRequest, + new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + searchHits[0] = searchResponse.getHits(); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + log.error("", e); + latch.countDown(); + } + }); + try { + latch.await(); // Wait for the search to complete + } catch (InterruptedException e) { + log.error("Interrupted while waiting for search to complete", e); + } + return searchHits[0]; + } + + /** + * Retrieves a nested object from a map. + * + * @param map the map containing the nested object. + * @param key the key of the nested object. + * @param type the expected type of the nested object. + * @param the type parameter. + * @return the nested object if found, null otherwise. + * @throws ClassCastException if the nested object is not of the expected type. + */ + public static T getNestedObject(Map map, String key, Class type) { + final Object value = map.get(key); + if (value == null) { + return null; + } + if (type.isInstance(value)) { + // Make a defensive copy for supported types like Map or List + if (value instanceof Map) { + return type.cast(new HashMap<>((Map) value)); + } else if (value instanceof List) { + return type.cast(new ArrayList<>((List) value)); + } + // Return the value directly if it is immutable (e.g., String, Integer) + return type.cast(value); + } else { + throw new ClassCastException( + "Expected " + type.getName() + " but found " + value.getClass().getName()); + } + } +}