Skip to content

Commit e3b550c

Browse files
authored
[backend] Improv log on Tanium executor endpoint retrieval
1 parent d12d9e7 commit e3b550c

File tree

4 files changed

+118
-73
lines changed

4 files changed

+118
-73
lines changed
Lines changed: 116 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.openbas.executors.tanium.client;
22

3+
import static org.apache.hc.core5.http.HttpHeaders.CONTENT_TYPE;
4+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
5+
36
import com.fasterxml.jackson.core.JsonProcessingException;
47
import com.fasterxml.jackson.core.type.TypeReference;
58
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -9,6 +12,7 @@
912
import io.openbas.service.EndpointService;
1013
import jakarta.validation.constraints.NotNull;
1114
import java.io.IOException;
15+
import java.nio.charset.StandardCharsets;
1216
import java.time.Instant;
1317
import java.time.ZoneOffset;
1418
import java.time.format.DateTimeFormatter;
@@ -19,8 +23,10 @@
1923
import org.apache.hc.client5.http.ClientProtocolException;
2024
import org.apache.hc.client5.http.classic.methods.HttpPost;
2125
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
26+
import org.apache.hc.core5.http.ClassicHttpResponse;
2227
import org.apache.hc.core5.http.io.entity.EntityUtils;
2328
import org.apache.hc.core5.http.io.entity.StringEntity;
29+
import org.springframework.http.HttpStatus;
2430
import org.springframework.stereotype.Service;
2531

2632
@RequiredArgsConstructor
@@ -37,82 +43,89 @@ public class TaniumExecutorClient {
3743
// -- ENDPOINTS --
3844

3945
public DataEndpoints endpoints() {
40-
String jsonResponse = null;
4146
try {
4247
final String formattedDateTime =
4348
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
4449
.withZone(ZoneOffset.UTC)
4550
.format(Instant.now().minusMillis(EndpointService.DELETE_TTL));
4651
// https://help.tanium.com/bundle/ug_gateway_cloud/page/gateway/filter_syntax.html
4752
String query =
48-
"{\n"
49-
+ "\tendpoints(filter: {any: false, filters: [{memberOf: {id: "
50-
+ this.config.getComputerGroupId()
51-
+ "}}, {path: \"eidLastSeen\", op: GT, value: \""
52-
+ formattedDateTime
53-
+ "\"}]}) {\n"
54-
+ " edges {\n"
55-
+ " node {\n"
56-
+ " id\n"
57-
+ " computerID\n"
58-
+ " name\n"
59-
+ " ipAddresses\n"
60-
+ " macAddresses\n"
61-
+ " eidLastSeen\n"
62-
+ " os { platform }\n"
63-
+ " processor { architecture }\n"
64-
+ " }\n"
65-
+ " }\n"
66-
+ " }\n"
67-
+ "}";
53+
String.format(
54+
"""
55+
query {
56+
endpoints(filter: {
57+
any: false,
58+
filters: [
59+
{memberOf: {id: %d}},
60+
{path: "eidLastSeen", op: GT, value: "%s"}
61+
]
62+
}) {
63+
edges {
64+
node {
65+
id computerID name ipAddresses macAddresses eidLastSeen
66+
os { platform }
67+
processor { architecture }
68+
}
69+
}
70+
}
71+
}
72+
""",
73+
config.getComputerGroupId(), formattedDateTime);
74+
6875
Map<String, Object> body = new HashMap<>();
6976
body.put("query", query);
70-
jsonResponse = this.post(body);
71-
if (jsonResponse == null || jsonResponse.isEmpty()) {
72-
log.error("Received empty response from API for query: {}", query);
73-
throw new RuntimeException("API returned an empty response");
77+
String jsonResponse = this.post(body);
78+
79+
GraphQLResponse<DataEndpoints> response =
80+
objectMapper.readValue(jsonResponse, new TypeReference<>() {});
81+
82+
if (response == null || response.data == null) {
83+
throw new RuntimeException("API response malformed or empty");
7484
}
75-
return this.objectMapper.readValue(jsonResponse, new TypeReference<>() {});
85+
86+
return response.data;
7687
} catch (JsonProcessingException e) {
77-
log.error(
78-
String.format(
79-
"Failed to parse JSON response %s. Error: %s", jsonResponse, e.getMessage()),
80-
e);
88+
log.error(String.format("Failed to parse JSON response. Error: %s", e.getMessage()), e);
8189
throw new RuntimeException(e);
8290
} catch (IOException e) {
83-
log.error(
84-
String.format("I/O error occurred during API request. Error: %s", e.getMessage()), e);
85-
throw new RuntimeException(e);
86-
} catch (Exception e) {
87-
log.error(String.format("Unexpected error occurred. Error: %s", e.getMessage()), e);
91+
log.error("Error while querying endpoints", e);
8892
throw new RuntimeException(e);
8993
}
9094
}
9195

9296
public void executeAction(String endpointId, Integer packageID, String command) {
9397
try {
94-
String query =
95-
"mutation {\n"
96-
+ "\tactionCreate(\n"
97-
+ " input: { name: \"OpenBAS Action\", package: { id: "
98-
+ packageID
99-
+ ", params: [\""
100-
+ command.replace("\\", "\\\\").replace("\"", "\\\"")
101-
+ "\"] }, targets: { actionGroup: { id: "
102-
+ this.config.getActionGroupId()
103-
+ " }, endpoints: ["
104-
+ endpointId
105-
+ "] } }\n"
106-
+ ") {\n "
107-
+ " action {\n"
108-
+ " id\n"
109-
+ " }\n"
110-
+ " }\n"
111-
+ "}";
112-
Map<String, Object> body = new HashMap<>();
113-
body.put("query", query);
114-
this.post(body);
98+
String escapedCommand = command.replace("\\", "\\\\").replace("\"", "\\\"");
99+
100+
String mutation =
101+
String.format(
102+
"""
103+
mutation {
104+
actionCreate(
105+
input: {
106+
name: "OpenBAS Action",
107+
package: {
108+
id: %d,
109+
params: ["%s"]
110+
},
111+
targets: {
112+
actionGroup: { id: %d },
113+
endpoints: ["%s"]
114+
}
115+
}
116+
) {
117+
action { id }
118+
}
119+
}
120+
""",
121+
packageID, escapedCommand, config.getActionGroupId(), endpointId);
122+
123+
Map<String, Object> requestBody = new HashMap<>();
124+
requestBody.put("query", mutation);
125+
126+
this.post(requestBody);
115127
} catch (IOException e) {
128+
log.error("Error while executing action", e);
116129
throw new RuntimeException(e);
117130
}
118131
}
@@ -124,13 +137,56 @@ private String post(@NotNull final Map<String, Object> body) throws IOException
124137
HttpPost httpPost = new HttpPost(this.config.getGatewayUrl());
125138
// Headers
126139
httpPost.addHeader(KEY_HEADER, this.config.getApiKey());
127-
httpPost.addHeader("content-type", "application/json");
140+
httpPost.addHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE);
128141
// Body
129-
StringEntity entity = new StringEntity(this.objectMapper.writeValueAsString(body));
130-
httpPost.setEntity(entity);
131-
return httpClient.execute(httpPost, response -> EntityUtils.toString(response.getEntity()));
142+
String json = this.objectMapper.writeValueAsString(body);
143+
httpPost.setEntity(new StringEntity(json, StandardCharsets.UTF_8));
144+
145+
return httpClient.execute(
146+
httpPost,
147+
(ClassicHttpResponse response) -> {
148+
int status = response.getCode();
149+
String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
150+
151+
if (HttpStatus.valueOf(response.getCode()).is2xxSuccessful()) {
152+
Map<String, Object> responseMap =
153+
objectMapper.readValue(result, new TypeReference<>() {});
154+
if (responseMap.containsKey("errors")) {
155+
StringBuilder errorMessage = new StringBuilder("GraphQL errors detected:\n");
156+
for (Map<String, Object> error :
157+
(Iterable<Map<String, Object>>) responseMap.get("errors")) {
158+
errorMessage.append("- ").append(error.get("message")).append("\n");
159+
160+
Map<String, Object> extensions = (Map<String, Object>) error.get("extensions");
161+
if (extensions != null && extensions.containsKey("argumentErrors")) {
162+
for (Map<String, Object> argError :
163+
(Iterable<Map<String, Object>>) extensions.get("argumentErrors")) {
164+
errorMessage
165+
.append(" • ")
166+
.append(argError.get("message"))
167+
.append(" (code: ")
168+
.append(argError.get("code"))
169+
.append(")\n");
170+
}
171+
}
172+
}
173+
throw new RuntimeException(errorMessage.toString());
174+
}
175+
176+
return result;
177+
} else {
178+
throw new ClientProtocolException(
179+
"Unexpected response status: " + status + "\nBody: " + result);
180+
}
181+
});
182+
132183
} catch (IOException e) {
133184
throw new ClientProtocolException("Unexpected response", e);
134185
}
135186
}
187+
188+
private static class GraphQLResponse<T> {
189+
190+
public T data;
191+
}
136192
}

openbas-api/src/main/java/io/openbas/executors/tanium/model/DataEndpoints.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
@JsonIgnoreProperties(ignoreUnknown = true)
88
public class DataEndpoints {
99

10-
private Endpoints data;
10+
private EdgesEndpoints endpoints;
1111
}

openbas-api/src/main/java/io/openbas/executors/tanium/model/Endpoints.java

Lines changed: 0 additions & 11 deletions
This file was deleted.

openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public TaniumExecutorService(
8989
public void run() {
9090
log.info("Running Tanium executor endpoints gathering...");
9191
List<NodeEndpoint> nodeEndpoints =
92-
this.client.endpoints().getData().getEndpoints().getEdges().stream().toList();
92+
this.client.endpoints().getEndpoints().getEdges().stream().toList();
9393
List<AgentRegisterInput> endpointRegisterList = toAgentEndpoint(nodeEndpoints);
9494
log.info("Tanium executor provisioning based on " + endpointRegisterList.size() + " assets");
9595

0 commit comments

Comments
 (0)