Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -37,82 +43,89 @@ public class TaniumExecutorClient {
// -- ENDPOINTS --

public DataEndpoints endpoints() {
String jsonResponse = null;
try {
final String formattedDateTime =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
.withZone(ZoneOffset.UTC)
.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<String, Object> 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<DataEndpoints> 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<String, Object> 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<String, Object> requestBody = new HashMap<>();
requestBody.put("query", mutation);

this.post(requestBody);
} catch (IOException e) {
log.error("Error while executing action", e);
throw new RuntimeException(e);
}
}
Expand All @@ -124,13 +137,56 @@ private String post(@NotNull final Map<String, Object> 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<String, Object> responseMap =
objectMapper.readValue(result, new TypeReference<>() {});
if (responseMap.containsKey("errors")) {
StringBuilder errorMessage = new StringBuilder("GraphQL errors detected:\n");
for (Map<String, Object> error :
(Iterable<Map<String, Object>>) responseMap.get("errors")) {
errorMessage.append("- ").append(error.get("message")).append("\n");

Map<String, Object> extensions = (Map<String, Object>) error.get("extensions");
if (extensions != null && extensions.containsKey("argumentErrors")) {
for (Map<String, Object> argError :
(Iterable<Map<String, Object>>) 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<T> {

public T data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public class DataEndpoints {

private Endpoints data;
private EdgesEndpoints endpoints;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public TaniumExecutorService(
public void run() {
log.info("Running Tanium executor endpoints gathering...");
List<NodeEndpoint> nodeEndpoints =
this.client.endpoints().getData().getEndpoints().getEdges().stream().toList();
this.client.endpoints().getEndpoints().getEdges().stream().toList();
List<AgentRegisterInput> endpointRegisterList = toAgentEndpoint(nodeEndpoints);
log.info("Tanium executor provisioning based on " + endpointRegisterList.size() + " assets");

Expand Down