1
1
package io .openbas .executors .tanium .client ;
2
2
3
+ import static org .apache .hc .core5 .http .HttpHeaders .CONTENT_TYPE ;
4
+ import static org .springframework .http .MediaType .APPLICATION_JSON_VALUE ;
5
+
3
6
import com .fasterxml .jackson .core .JsonProcessingException ;
4
7
import com .fasterxml .jackson .core .type .TypeReference ;
5
8
import com .fasterxml .jackson .databind .ObjectMapper ;
9
12
import io .openbas .service .EndpointService ;
10
13
import jakarta .validation .constraints .NotNull ;
11
14
import java .io .IOException ;
15
+ import java .nio .charset .StandardCharsets ;
12
16
import java .time .Instant ;
13
17
import java .time .ZoneOffset ;
14
18
import java .time .format .DateTimeFormatter ;
19
23
import org .apache .hc .client5 .http .ClientProtocolException ;
20
24
import org .apache .hc .client5 .http .classic .methods .HttpPost ;
21
25
import org .apache .hc .client5 .http .impl .classic .CloseableHttpClient ;
26
+ import org .apache .hc .core5 .http .ClassicHttpResponse ;
22
27
import org .apache .hc .core5 .http .io .entity .EntityUtils ;
23
28
import org .apache .hc .core5 .http .io .entity .StringEntity ;
29
+ import org .springframework .http .HttpStatus ;
24
30
import org .springframework .stereotype .Service ;
25
31
26
32
@ RequiredArgsConstructor
@@ -37,82 +43,89 @@ public class TaniumExecutorClient {
37
43
// -- ENDPOINTS --
38
44
39
45
public DataEndpoints endpoints () {
40
- String jsonResponse = null ;
41
46
try {
42
47
final String formattedDateTime =
43
48
DateTimeFormatter .ofPattern ("yyyy-MM-dd'T'HH:mm:ss'Z'" )
44
49
.withZone (ZoneOffset .UTC )
45
50
.format (Instant .now ().minusMillis (EndpointService .DELETE_TTL ));
46
51
// https://help.tanium.com/bundle/ug_gateway_cloud/page/gateway/filter_syntax.html
47
52
String query =
48
- "{\n "
49
- + "\t endpoints(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
+
68
75
Map <String , Object > body = new HashMap <>();
69
76
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" );
74
84
}
75
- return this .objectMapper .readValue (jsonResponse , new TypeReference <>() {});
85
+
86
+ return response .data ;
76
87
} 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 );
81
89
throw new RuntimeException (e );
82
90
} 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 );
88
92
throw new RuntimeException (e );
89
93
}
90
94
}
91
95
92
96
public void executeAction (String endpointId , Integer packageID , String command ) {
93
97
try {
94
- String query =
95
- "mutation {\n "
96
- + "\t actionCreate(\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 );
115
127
} catch (IOException e ) {
128
+ log .error ("Error while executing action" , e );
116
129
throw new RuntimeException (e );
117
130
}
118
131
}
@@ -124,13 +137,56 @@ private String post(@NotNull final Map<String, Object> body) throws IOException
124
137
HttpPost httpPost = new HttpPost (this .config .getGatewayUrl ());
125
138
// Headers
126
139
httpPost .addHeader (KEY_HEADER , this .config .getApiKey ());
127
- httpPost .addHeader ("content-type" , "application/json" );
140
+ httpPost .addHeader (CONTENT_TYPE , APPLICATION_JSON_VALUE );
128
141
// 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 + "\n Body: " + result );
180
+ }
181
+ });
182
+
132
183
} catch (IOException e ) {
133
184
throw new ClientProtocolException ("Unexpected response" , e );
134
185
}
135
186
}
187
+
188
+ private static class GraphQLResponse <T > {
189
+
190
+ public T data ;
191
+ }
136
192
}
0 commit comments