diff --git a/openbas-api/src/main/java/io/openbas/executors/tanium/client/TaniumExecutorClient.java b/openbas-api/src/main/java/io/openbas/executors/tanium/client/TaniumExecutorClient.java index 83105df581..adb462ecd9 100644 --- a/openbas-api/src/main/java/io/openbas/executors/tanium/client/TaniumExecutorClient.java +++ b/openbas-api/src/main/java/io/openbas/executors/tanium/client/TaniumExecutorClient.java @@ -1,5 +1,8 @@ package io.openbas.executors.tanium.client; +import static org.apache.hc.core5.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,6 +12,7 @@ import io.openbas.service.EndpointService; import jakarta.validation.constraints.NotNull; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @@ -19,8 +23,10 @@ import org.apache.hc.client5.http.ClientProtocolException; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -37,7 +43,6 @@ public class TaniumExecutorClient { // -- ENDPOINTS -- public DataEndpoints endpoints() { - String jsonResponse = null; try { final String formattedDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") @@ -45,74 +50,82 @@ public DataEndpoints endpoints() { .format(Instant.now().minusMillis(EndpointService.DELETE_TTL)); // https://help.tanium.com/bundle/ug_gateway_cloud/page/gateway/filter_syntax.html String query = - "{\n" - + "\tendpoints(filter: {any: false, filters: [{memberOf: {id: " - + this.config.getComputerGroupId() - + "}}, {path: \"eidLastSeen\", op: GT, value: \"" - + formattedDateTime - + "\"}]}) {\n" - + " edges {\n" - + " node {\n" - + " id\n" - + " computerID\n" - + " name\n" - + " ipAddresses\n" - + " macAddresses\n" - + " eidLastSeen\n" - + " os { platform }\n" - + " processor { architecture }\n" - + " }\n" - + " }\n" - + " }\n" - + "}"; + String.format( + """ + query { + endpoints(filter: { + any: false, + filters: [ + {memberOf: {id: %d}}, + {path: "eidLastSeen", op: GT, value: "%s"} + ] + }) { + edges { + node { + id computerID name ipAddresses macAddresses eidLastSeen + os { platform } + processor { architecture } + } + } + } + } + """, + config.getComputerGroupId(), formattedDateTime); + Map body = new HashMap<>(); body.put("query", query); - jsonResponse = this.post(body); - if (jsonResponse == null || jsonResponse.isEmpty()) { - log.error("Received empty response from API for query: {}", query); - throw new RuntimeException("API returned an empty response"); + String jsonResponse = this.post(body); + + GraphQLResponse response = + objectMapper.readValue(jsonResponse, new TypeReference<>() {}); + + if (response == null || response.data == null) { + throw new RuntimeException("API response malformed or empty"); } - return this.objectMapper.readValue(jsonResponse, new TypeReference<>() {}); + + return response.data; } catch (JsonProcessingException e) { - log.error( - String.format( - "Failed to parse JSON response %s. Error: %s", jsonResponse, e.getMessage()), - e); + log.error(String.format("Failed to parse JSON response. Error: %s", e.getMessage()), e); throw new RuntimeException(e); } catch (IOException e) { - log.error( - String.format("I/O error occurred during API request. Error: %s", e.getMessage()), e); - throw new RuntimeException(e); - } catch (Exception e) { - log.error(String.format("Unexpected error occurred. Error: %s", e.getMessage()), e); + log.error("Error while querying endpoints", e); throw new RuntimeException(e); } } public void executeAction(String endpointId, Integer packageID, String command) { try { - String query = - "mutation {\n" - + "\tactionCreate(\n" - + " input: { name: \"OpenBAS Action\", package: { id: " - + packageID - + ", params: [\"" - + command.replace("\\", "\\\\").replace("\"", "\\\"") - + "\"] }, targets: { actionGroup: { id: " - + this.config.getActionGroupId() - + " }, endpoints: [" - + endpointId - + "] } }\n" - + ") {\n " - + " action {\n" - + " id\n" - + " }\n" - + " }\n" - + "}"; - Map body = new HashMap<>(); - body.put("query", query); - this.post(body); + String escapedCommand = command.replace("\\", "\\\\").replace("\"", "\\\""); + + String mutation = + String.format( + """ + mutation { + actionCreate( + input: { + name: "OpenBAS Action", + package: { + id: %d, + params: ["%s"] + }, + targets: { + actionGroup: { id: %d }, + endpoints: ["%s"] + } + } + ) { + action { id } + } + } + """, + packageID, escapedCommand, config.getActionGroupId(), endpointId); + + Map requestBody = new HashMap<>(); + requestBody.put("query", mutation); + + this.post(requestBody); } catch (IOException e) { + log.error("Error while executing action", e); throw new RuntimeException(e); } } @@ -124,13 +137,56 @@ private String post(@NotNull final Map body) throws IOException HttpPost httpPost = new HttpPost(this.config.getGatewayUrl()); // Headers httpPost.addHeader(KEY_HEADER, this.config.getApiKey()); - httpPost.addHeader("content-type", "application/json"); + httpPost.addHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE); // Body - StringEntity entity = new StringEntity(this.objectMapper.writeValueAsString(body)); - httpPost.setEntity(entity); - return httpClient.execute(httpPost, response -> EntityUtils.toString(response.getEntity())); + String json = this.objectMapper.writeValueAsString(body); + httpPost.setEntity(new StringEntity(json, StandardCharsets.UTF_8)); + + return httpClient.execute( + httpPost, + (ClassicHttpResponse response) -> { + int status = response.getCode(); + String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (HttpStatus.valueOf(response.getCode()).is2xxSuccessful()) { + Map responseMap = + objectMapper.readValue(result, new TypeReference<>() {}); + if (responseMap.containsKey("errors")) { + StringBuilder errorMessage = new StringBuilder("GraphQL errors detected:\n"); + for (Map error : + (Iterable>) responseMap.get("errors")) { + errorMessage.append("- ").append(error.get("message")).append("\n"); + + Map extensions = (Map) error.get("extensions"); + if (extensions != null && extensions.containsKey("argumentErrors")) { + for (Map argError : + (Iterable>) extensions.get("argumentErrors")) { + errorMessage + .append(" • ") + .append(argError.get("message")) + .append(" (code: ") + .append(argError.get("code")) + .append(")\n"); + } + } + } + throw new RuntimeException(errorMessage.toString()); + } + + return result; + } else { + throw new ClientProtocolException( + "Unexpected response status: " + status + "\nBody: " + result); + } + }); + } catch (IOException e) { throw new ClientProtocolException("Unexpected response", e); } } + + private static class GraphQLResponse { + + public T data; + } } diff --git a/openbas-api/src/main/java/io/openbas/executors/tanium/model/DataEndpoints.java b/openbas-api/src/main/java/io/openbas/executors/tanium/model/DataEndpoints.java index 4e56c867bd..16f1d25eec 100644 --- a/openbas-api/src/main/java/io/openbas/executors/tanium/model/DataEndpoints.java +++ b/openbas-api/src/main/java/io/openbas/executors/tanium/model/DataEndpoints.java @@ -7,5 +7,5 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class DataEndpoints { - private Endpoints data; + private EdgesEndpoints endpoints; } diff --git a/openbas-api/src/main/java/io/openbas/executors/tanium/model/Endpoints.java b/openbas-api/src/main/java/io/openbas/executors/tanium/model/Endpoints.java deleted file mode 100644 index 237ddf0d8d..0000000000 --- a/openbas-api/src/main/java/io/openbas/executors/tanium/model/Endpoints.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.openbas.executors.tanium.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Data; - -@Data -@JsonIgnoreProperties(ignoreUnknown = true) -public class Endpoints { - - private EdgesEndpoints endpoints; -} diff --git a/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java b/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java index 6e8e50a178..d1516fe168 100644 --- a/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java +++ b/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java @@ -89,7 +89,7 @@ public TaniumExecutorService( public void run() { log.info("Running Tanium executor endpoints gathering..."); List nodeEndpoints = - this.client.endpoints().getData().getEndpoints().getEdges().stream().toList(); + this.client.endpoints().getEndpoints().getEdges().stream().toList(); List endpointRegisterList = toAgentEndpoint(nodeEndpoints); log.info("Tanium executor provisioning based on " + endpointRegisterList.size() + " assets");