Skip to content

Commit

Permalink
[backend] Implement CrowdStrike native executor (#1366)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamuelHassine committed Jan 2, 2025
1 parent 7a8a756 commit 11260e8
Show file tree
Hide file tree
Showing 15 changed files with 627 additions and 1 deletion.
10 changes: 10 additions & 0 deletions openbas-api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ executor.tanium.action-group-id=4
executor.tanium.windows-package-id=
executor.tanium.unix-package-id=

# Executor CrowdStrike
executor.crowdstrike.enable=false
executor.crowdstrike.id=2a16dcc4-55ac-40fc-8110-d5968a46cdd1
executor.crowdstrike.api-url=https://api.us-2.crowdstrike.com
executor.crowdstrike.client-id=<crowdstrike-client-id>
executor.crowdstrike.client-secret=<crowdstrike-client-secret>
executor.crowdstrike.host-group=<host-group>
executor.crowdstrike.windows-script-name=OpenBAS Subprocessor (Windows)
executor.crowdstrike.unix-script-name=OpenBAS Subprocessor (Unix)

# Executor OpenBAS

# valid values: local | repository
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import io.openbas.database.repository.InjectStatusRepository;
import io.openbas.executors.caldera.config.CalderaExecutorConfig;
import io.openbas.executors.caldera.service.CalderaExecutorContextService;
import io.openbas.executors.crowdstrike.config.CrowdStrikeExecutorConfig;
import io.openbas.executors.crowdstrike.service.CrowdStrikeExecutorContextService;
import io.openbas.executors.openbas.service.OpenBASExecutorContextService;
import io.openbas.executors.tanium.config.TaniumExecutorConfig;
import io.openbas.executors.tanium.service.TaniumExecutorContextService;
Expand All @@ -27,6 +29,8 @@ public class ExecutionExecutorService {
private final CalderaExecutorContextService calderaExecutorContextService;
private final TaniumExecutorConfig taniumExecutorConfig;
private final TaniumExecutorContextService taniumExecutorContextService;
private final CrowdStrikeExecutorConfig crowdStrikeExecutorConfig;
private final CrowdStrikeExecutorContextService crowdStrikeExecutorContextService;
private final OpenBASExecutorContextService openBASExecutorContextService;
private final InjectStatusRepository injectStatusRepository;

Expand Down Expand Up @@ -93,6 +97,12 @@ private void launchExecutorContextForAsset(Inject inject, Asset asset) {
}
this.taniumExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint);
}
case "openbas_crowdstrike" -> {
if (!this.crowdStrikeExecutorConfig.isEnable()) {
throw new RuntimeException("Fatal error: CrowdStrike executor is not enabled");
}
this.crowdStrikeExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint);
}
case "openbas_agent" ->
this.openBASExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint);
default ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.openbas.executors.crowdstrike;

import io.openbas.asset.EndpointService;
import io.openbas.executors.crowdstrike.client.CrowdStrikeExecutorClient;
import io.openbas.executors.crowdstrike.config.CrowdStrikeExecutorConfig;
import io.openbas.executors.crowdstrike.service.CrowdStrikeExecutorContextService;
import io.openbas.executors.crowdstrike.service.CrowdStrikeExecutorService;
import io.openbas.integrations.ExecutorService;
import io.openbas.integrations.InjectorService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;

import java.time.Duration;

@RequiredArgsConstructor
@Service
public class CrowdStrikeExecutor {

private final CrowdStrikeExecutorConfig config;
private final ThreadPoolTaskScheduler taskScheduler;
private final CrowdStrikeExecutorClient client;
private final EndpointService endpointService;
private final CrowdStrikeExecutorContextService crowdStrikeExecutorContextService;
private final ExecutorService executorService;
private final InjectorService injectorService;

@PostConstruct
public void init() {
CrowdStrikeExecutorService service =
new CrowdStrikeExecutorService(
this.executorService,
this.client,
this.config,
this.crowdStrikeExecutorContextService,
this.endpointService,
this.injectorService);
if (this.config.isEnable()) {
this.taskScheduler.scheduleAtFixedRate(service, Duration.ofSeconds(60));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.openbas.executors.crowdstrike.client;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.openbas.executors.crowdstrike.config.CrowdStrikeExecutorConfig;
import io.openbas.executors.crowdstrike.model.Authentication;
import io.openbas.executors.crowdstrike.model.CrowdStrikeSession;
import io.openbas.executors.crowdstrike.model.ResourcesHosts;
import io.openbas.executors.crowdstrike.model.ResourcesSession;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.java.Log;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

@RequiredArgsConstructor
@Service
@Log
public class CrowdStrikeExecutorClient {

private static final Integer AUTH_TIMEOUT = 300;
private static final String OAUTH_URI = "/oauth2/token";
private static final String ENDPOINTS_URI = "/devices/combined/host-group-members/v1";
private static final String SESSION_URI = "/real-time-response/entities/sessions/v1";
private static final String REAL_TIME_RESPONSE_URI = "/real-time-response/entities/active-responder-command/v1";

private final CrowdStrikeExecutorConfig config;
private final ObjectMapper objectMapper = new ObjectMapper();

private Instant lastAuthentication = Instant.now().minusSeconds(AUTH_TIMEOUT);
private String token;

// -- ENDPOINTS --

public ResourcesHosts devices() {
try {
String jsonResponse = this.get(ENDPOINTS_URI + "?id=" + this.config.getHostGroup());
return this.objectMapper.readValue(jsonResponse, new TypeReference<>() {});
} catch (JsonProcessingException e) {
log.log(Level.SEVERE, "Failed to parse JSON response. Error: {}", e.getMessage());
throw new RuntimeException(e);
} catch (IOException e) {
log.log(Level.SEVERE, "I/O error occurred during API request. Error: {}", e.getMessage());
throw new RuntimeException(e);
} catch (Exception e) {
log.log(Level.SEVERE, "Unexpected error occurred. Error: {}", e.getMessage());
throw new RuntimeException(e);
}
}

public void executeAction(String deviceId, String scriptName, String command) {
try {
// Open remote session
Map<String, Object> bodySession = new HashMap<>();
bodySession.put("device_id", deviceId);
bodySession.put("queue_offline", false);
String jsonSessionResponse = this.post(SESSION_URI, bodySession);
ResourcesSession sessions = this.objectMapper.readValue(jsonSessionResponse, new TypeReference<>() {});
CrowdStrikeSession session = sessions.getResources().getFirst();
if( session == null ) {
log.log(Level.SEVERE, "Cannot get the session on the selected device");
throw new RuntimeException("Cannot get the session on the selected device");
}
// Execute the command
Map<String, Object> bodyCommand = new HashMap<>();
bodyCommand.put("session_id", session.getSession_id());
bodyCommand.put("base_command", "runscript");
bodyCommand.put("command_string", "runscript -CloudFile=\"" + scriptName + "\" -CommandLine=```'{\"command\":\"" + command + "\"}'```");
this.post(REAL_TIME_RESPONSE_URI, bodyCommand);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

// -- PRIVATE --

private String get(@NotBlank final String uri) throws IOException {
if( this.lastAuthentication.isBefore(Instant.now().minusSeconds(AUTH_TIMEOUT))) {
this.authenticate();
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(this.config.getApiUrl() + uri);
// Headers
httpGet.addHeader("Authorization", "Bearer " + this.token);
return httpClient.execute(httpGet, response -> EntityUtils.toString(response.getEntity()));
} catch (IOException e) {
throw new ClientProtocolException("Unexpected response for request on: " + uri);
}
}

private String post(@NotBlank final String uri, @NotNull final Map<String, Object> body) throws IOException {
if( this.lastAuthentication.isBefore(Instant.now().minusSeconds(AUTH_TIMEOUT))) {
this.authenticate();
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(this.config.getApiUrl() + uri);
// Headers
httpPost.addHeader("Authorization", "Bearer " + this.token);
httpPost.addHeader("content-type", "application/json");
// Body
StringEntity entity = new StringEntity(this.objectMapper.writeValueAsString(body));
httpPost.setEntity(entity);
return httpClient.execute(httpPost, response -> EntityUtils.toString(response.getEntity()));
} catch (IOException e) {
throw new ClientProtocolException("Unexpected response");
}
}

private void authenticate() throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(this.config.getApiUrl() + OAUTH_URI);
// Headers
httpPost.addHeader("content-type", "application/x-www-form-urlencoded");
// Body
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("client_id", this.config.getClientId()));
params.add(new BasicNameValuePair("client_secret", this.config.getClientSecret()));
params.add(new BasicNameValuePair("grant_type", "client_credentials"));
httpPost.setEntity(new UrlEncodedFormEntity(params));
String jsonResponse = httpClient.execute(httpPost, response -> EntityUtils.toString(response.getEntity()));
Authentication auth = this.objectMapper.readValue(jsonResponse, new TypeReference<>() {});
this.token = auth.getAccess_token();
this.lastAuthentication = Instant.now();
} catch (IOException e) {
throw new ClientProtocolException("Unexpected response");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.openbas.executors.crowdstrike.config;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Setter
@Component
@ConfigurationProperties(prefix = "executor.crowdstrike")
public class CrowdStrikeExecutorConfig {

@Getter private boolean enable;

@Getter @NotBlank private String id;

@Getter @NotBlank private String apiUrl;

@Getter @NotBlank private String clientId;

@Getter @NotBlank private String clientSecret;

@Getter @NotBlank private String hostGroup;

@Getter @NotBlank private String windowsScriptName;

@Getter @NotBlank private String unixScriptName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.openbas.executors.crowdstrike.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Authentication {

private String access_token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.openbas.executors.crowdstrike.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.openbas.executors.tanium.model.Os;
import io.openbas.executors.tanium.model.Processor;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CrowdStrikeDevice {

private String device_id;
private String hostname;
private String platform_name;
private String os_version;
private String external_ip;
private String connection_ip;
private String mac_address;
private String os_product_name;
private String last_seen;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.openbas.executors.crowdstrike.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.openbas.executors.tanium.model.Os;
import io.openbas.executors.tanium.model.Processor;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CrowdStrikeSession {

private String session_id;
private String device_id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.openbas.executors.crowdstrike.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import java.util.List;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResourcesHosts {

private List<CrowdStrikeDevice> resources;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.openbas.executors.crowdstrike.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import java.util.List;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResourcesSession {

private List<CrowdStrikeSession> resources;
}
Loading

0 comments on commit 11260e8

Please sign in to comment.